summaryrefslogtreecommitdiffstats
path: root/application/palemoon/modules
diff options
context:
space:
mode:
Diffstat (limited to 'application/palemoon/modules')
-rw-r--r--application/palemoon/modules/AutoCompletePopup.jsm293
-rw-r--r--application/palemoon/modules/PopupNotifications.jsm121
-rw-r--r--application/palemoon/modules/moz.build1
3 files changed, 399 insertions, 16 deletions
diff --git a/application/palemoon/modules/AutoCompletePopup.jsm b/application/palemoon/modules/AutoCompletePopup.jsm
new file mode 100644
index 000000000..c3698f905
--- /dev/null
+++ b/application/palemoon/modules/AutoCompletePopup.jsm
@@ -0,0 +1,293 @@
+/* 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 Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = [ "AutoCompletePopup" ];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// nsITreeView implementation that feeds the autocomplete popup
+// with the search data.
+var AutoCompleteTreeView = {
+ // nsISupports
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITreeView,
+ Ci.nsIAutoCompleteController]),
+
+ // Private variables
+ treeBox: null,
+ results: [],
+
+ // nsITreeView
+ selection: null,
+
+ get rowCount() { return this.results.length; },
+ setTree: function(treeBox) { this.treeBox = treeBox; },
+ getCellText: function(idx, column) { return this.results[idx].value },
+ isContainer: function(idx) { return false; },
+ getCellValue: function(idx, column) { return false },
+ isContainerOpen: function(idx) { return false; },
+ isContainerEmpty: function(idx) { return false; },
+ isSeparator: function(idx) { return false; },
+ isSorted: function() { return false; },
+ isEditable: function(idx, column) { return false; },
+ canDrop: function(idx, orientation, dt) { return false; },
+ getLevel: function(idx) { return 0; },
+ getParentIndex: function(idx) { return -1; },
+ hasNextSibling: function(idx, after) { return idx < this.results.length - 1 },
+ toggleOpenState: function(idx) { },
+ getCellProperties: function(idx, column) { return this.results[idx].style || ""; },
+ getRowProperties: function(idx) { return ""; },
+ getImageSrc: function(idx, column) { return null; },
+ getProgressMode : function(idx, column) { },
+ cycleHeader: function(column) { },
+ cycleCell: function(idx, column) { },
+ selectionChanged: function() { },
+ performAction: function(action) { },
+ performActionOnCell: function(action, index, column) { },
+ getColumnProperties: function(column) { return ""; },
+
+ // nsIAutoCompleteController
+ get matchCount() {
+ return this.rowCount;
+ },
+
+ handleEnter: function(aIsPopupSelection) {
+ AutoCompletePopup.handleEnter(aIsPopupSelection);
+ },
+
+ stopSearch: function() {},
+
+ // Internal JS-only API
+ clearResults: function() {
+ this.results = [];
+ },
+
+ setResults: function(results) {
+ this.results = results;
+ },
+};
+
+this.AutoCompletePopup = {
+ MESSAGES: [
+ "FormAutoComplete:SelectBy",
+ "FormAutoComplete:GetSelectedIndex",
+ "FormAutoComplete:SetSelectedIndex",
+ "FormAutoComplete:MaybeOpenPopup",
+ "FormAutoComplete:ClosePopup",
+ "FormAutoComplete:Disconnect",
+ "FormAutoComplete:RemoveEntry",
+ "FormAutoComplete:Invalidate",
+ ],
+
+ init: function() {
+ for (let msg of this.MESSAGES) {
+ Services.mm.addMessageListener(msg, this);
+ }
+ },
+
+ uninit: function() {
+ for (let msg of this.MESSAGES) {
+ Services.mm.removeMessageListener(msg, this);
+ }
+ },
+
+ handleEvent: function(evt) {
+ switch (evt.type) {
+ case "popupshowing": {
+ this.sendMessageToBrowser("FormAutoComplete:PopupOpened");
+ break;
+ }
+
+ case "popuphidden": {
+ this.sendMessageToBrowser("FormAutoComplete:PopupClosed");
+ this.openedPopup = null;
+ this.weakBrowser = null;
+ evt.target.removeEventListener("popuphidden", this);
+ evt.target.removeEventListener("popupshowing", this);
+ break;
+ }
+ }
+ },
+
+ // Along with being called internally by the receiveMessage handler,
+ // this function is also called directly by the login manager, which
+ // uses a single message to fill in the autocomplete results. See
+ // "RemoteLogins:autoCompleteLogins".
+ showPopupWithResults: function({ browser, rect, dir, results }) {
+ if (!results.length || this.openedPopup) {
+ // We shouldn't ever be showing an empty popup, and if we
+ // already have a popup open, the old one needs to close before
+ // we consider opening a new one.
+ return;
+ }
+
+ let window = browser.ownerDocument.defaultView;
+ let tabbrowser = window.gBrowser;
+ if (Services.focus.activeWindow != window ||
+ tabbrowser.selectedBrowser != browser) {
+ // We were sent a message from a window or tab that went into the
+ // background, so we'll ignore it for now.
+ return;
+ }
+
+ this.weakBrowser = Cu.getWeakReference(browser);
+ this.openedPopup = browser.autoCompletePopup;
+ this.openedPopup.hidden = false;
+ // don't allow the popup to become overly narrow
+ this.openedPopup.setAttribute("width", Math.max(100, rect.width));
+ this.openedPopup.style.direction = dir;
+
+ AutoCompleteTreeView.setResults(results);
+ this.openedPopup.view = AutoCompleteTreeView;
+ this.openedPopup.selectedIndex = -1;
+ this.openedPopup.invalidate();
+
+ if (results.length) {
+ // Reset fields that were set from the last time the search popup was open
+ this.openedPopup.mInput = null;
+ this.openedPopup.showCommentColumn = false;
+ this.openedPopup.showImageColumn = false;
+ this.openedPopup.addEventListener("popuphidden", this);
+ this.openedPopup.addEventListener("popupshowing", this);
+ this.openedPopup.openPopupAtScreenRect("after_start", rect.left, rect.top,
+ rect.width, rect.height, false,
+ false);
+ } else {
+ this.closePopup();
+ }
+ },
+
+ invalidate(results) {
+ if (!this.openedPopup) {
+ return;
+ }
+
+ if (!results.length) {
+ this.closePopup();
+ } else {
+ AutoCompleteTreeView.setResults(results);
+ // We need to re-set the view in order for the
+ // tree to know the view has changed.
+ this.openedPopup.view = AutoCompleteTreeView;
+ this.openedPopup.invalidate();
+ }
+ },
+
+ closePopup() {
+ if (this.openedPopup) {
+ // Note that hidePopup() closes the popup immediately,
+ // so popuphiding or popuphidden events will be fired
+ // and handled during this call.
+ this.openedPopup.hidePopup();
+ }
+ AutoCompleteTreeView.clearResults();
+ },
+
+ removeLogin(login) {
+ Services.logins.removeLogin(login);
+ },
+
+ receiveMessage: function(message) {
+ if (!message.target.autoCompletePopup) {
+ // Returning false to pacify ESLint, but this return value is
+ // ignored by the messaging infrastructure.
+ return false;
+ }
+
+ switch (message.name) {
+ case "FormAutoComplete:SelectBy": {
+ this.openedPopup.selectBy(message.data.reverse, message.data.page);
+ break;
+ }
+
+ case "FormAutoComplete:GetSelectedIndex": {
+ if (this.openedPopup) {
+ return this.openedPopup.selectedIndex;
+ }
+ // If the popup was closed, then the selection
+ // has not changed.
+ return -1;
+ }
+
+ case "FormAutoComplete:SetSelectedIndex": {
+ let { index } = message.data;
+ if (this.openedPopup) {
+ this.openedPopup.selectedIndex = index;
+ }
+ break;
+ }
+
+ case "FormAutoComplete:MaybeOpenPopup": {
+ let { results, rect, dir } = message.data;
+ this.showPopupWithResults({ browser: message.target, rect, dir,
+ results });
+ break;
+ }
+
+ case "FormAutoComplete:Invalidate": {
+ let { results } = message.data;
+ this.invalidate(results);
+ break;
+ }
+
+ case "FormAutoComplete:ClosePopup": {
+ this.closePopup();
+ break;
+ }
+
+ case "FormAutoComplete:Disconnect": {
+ // The controller stopped controlling the current input, so clear
+ // any cached data. This is necessary cause otherwise we'd clear data
+ // only when starting a new search, but the next input could not support
+ // autocomplete and it would end up inheriting the existing data.
+ AutoCompleteTreeView.clearResults();
+ break;
+ }
+ }
+ // Returning false to pacify ESLint, but this return value is
+ // ignored by the messaging infrastructure.
+ return false;
+ },
+
+ /**
+ * Despite its name, this handleEnter is only called when the user clicks on
+ * one of the items in the popup since the popup is rendered in the parent process.
+ * The real controller's handleEnter is called directly in the content process
+ * for other methods of completing a selection (e.g. using the tab or enter
+ * keys) since the field with focus is in that process.
+ */
+ handleEnter(aIsPopupSelection) {
+ if (this.openedPopup) {
+ this.sendMessageToBrowser("FormAutoComplete:HandleEnter", {
+ selectedIndex: this.openedPopup.selectedIndex,
+ isPopupSelection: aIsPopupSelection,
+ });
+ }
+ },
+
+ /**
+ * If a browser exists that AutoCompletePopup knows about,
+ * sends it a message. Otherwise, this is a no-op.
+ *
+ * @param {string} msgName
+ * The name of the message to send.
+ * @param {object} data
+ * The optional data to send with the message.
+ */
+ sendMessageToBrowser(msgName, data) {
+ let browser = this.weakBrowser ? this.weakBrowser.get()
+ : null;
+ if (browser) {
+ browser.messageManager.sendAsyncMessage(msgName, data);
+ }
+ },
+
+ stopSearch: function() {}
+}
diff --git a/application/palemoon/modules/PopupNotifications.jsm b/application/palemoon/modules/PopupNotifications.jsm
index d2faf52c3..15c8915ed 100644
--- a/application/palemoon/modules/PopupNotifications.jsm
+++ b/application/palemoon/modules/PopupNotifications.jsm
@@ -4,9 +4,9 @@
this.EXPORTED_SYMBOLS = ["PopupNotifications"];
-var Cc = Components.classes, Ci = Components.interfaces;
+var Cc = Components.classes, Ci = Components.interfaces, Cu = Components.utils;
-Components.utils.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
const NOTIFICATION_EVENT_DISMISSED = "dismissed";
const NOTIFICATION_EVENT_REMOVED = "removed";
@@ -33,6 +33,18 @@ function getAnchorFromBrowser(aBrowser) {
return null;
}
+function getNotificationFromElement(aElement) {
+ // Need to find the associated notification object, which is a bit tricky
+ // since it isn't associated with the element directly - this is kind of
+ // gross and very dependent on the structure of the popupnotification
+ // binding's content.
+ let notificationEl;
+ let parent = aElement;
+ while (parent && (parent = aElement.ownerDocument.getBindingParent(parent)))
+ notificationEl = parent;
+ return notificationEl;
+}
+
/**
* Notification object describes a single popup notification.
*
@@ -194,7 +206,8 @@ PopupNotifications.prototype = {
* - label (string): the button's label.
* - accessKey (string): the button's accessKey.
* - callback (function): a callback to be invoked when the button is
- * pressed.
+ * pressed, is passed an object that contains the following fields:
+ * - checkboxChecked: (boolean) If the optional checkbox is checked.
* If null, the notification will not have a button, and
* secondaryActions will be ignored.
* @param secondaryActions
@@ -234,6 +247,26 @@ PopupNotifications.prototype = {
* removed when they would have otherwise been dismissed
* (i.e. any time the popup is closed due to user
* interaction).
+ * checkbox: An object that allows you to add a checkbox and
+ * control its behavior with these fields:
+ * label:
+ * (required) Label to be shown next to the checkbox.
+ * checked:
+ * (optional) Whether the checkbox should be checked
+ * by default. Defaults to false.
+ * checkedState:
+ * (optional) An object that allows you to customize
+ * the notification state when the checkbox is checked.
+ * disableMainAction:
+ * (optional) Whether the mainAction is disabled.
+ * Defaults to false.
+ * warningLabel:
+ * (optional) A (warning) text that is shown below the
+ * checkbox. Pass null to hide.
+ * uncheckedState:
+ * (optional) An object that allows you to customize
+ * the notification state when the checkbox is not checked.
+ * Has the same attributes as checkedState.
* popupIconURL:
* A string. URL of the image to be displayed in the popup.
* Normally specified in CSS using list-style-image and the
@@ -552,6 +585,25 @@ PopupNotifications.prototype = {
}
}
+ let checkbox = n.options.checkbox;
+ if (checkbox && checkbox.label) {
+ let checked = n._checkboxChecked != null ? n._checkboxChecked : !!checkbox.checked;
+
+ popupnotification.setAttribute("checkboxhidden", "false");
+ popupnotification.setAttribute("checkboxchecked", checked);
+ popupnotification.setAttribute("checkboxlabel", checkbox.label);
+
+ popupnotification.setAttribute("checkboxcommand", "PopupNotifications._onCheckboxCommand(event);");
+
+ if (checked) {
+ this._setNotificationUIState(popupnotification, checkbox.checkedState);
+ } else {
+ this._setNotificationUIState(popupnotification, checkbox.uncheckedState);
+ }
+ } else {
+ popupnotification.setAttribute("checkboxhidden", "true");
+ }
+
this.panel.appendChild(popupnotification);
// The popupnotification may be hidden if we got it from the chrome
@@ -560,6 +612,32 @@ PopupNotifications.prototype = {
}, this);
},
+ _setNotificationUIState(notification, state={}) {
+ notification.setAttribute("mainactiondisabled", state.disableMainAction || "false");
+
+ if (state.warningLabel) {
+ notification.setAttribute("warninglabel", state.warningLabel);
+ notification.setAttribute("warninghidden", "false");
+ } else {
+ notification.setAttribute("warninghidden", "true");
+ }
+ },
+
+ _onCheckboxCommand(event) {
+ let notificationEl = getNotificationFromElement(event.originalTarget);
+ let checked = notificationEl.checkbox.checked;
+ let notification = notificationEl.notification;
+
+ // Save checkbox state to be able to persist it when re-opening the doorhanger.
+ notification._checkboxChecked = checked;
+
+ if (checked) {
+ this._setNotificationUIState(notificationEl, notification.options.checkbox.checkedState);
+ } else {
+ this._setNotificationUIState(notificationEl, notification.options.checkbox.uncheckedState);
+ }
+ },
+
_showPanel: function PopupNotifications_showPanel(notificationsToShow, anchorElement) {
this.panel.hidden = false;
@@ -752,8 +830,12 @@ PopupNotifications.prototype = {
},
_fireCallback: function PopupNotifications_fireCallback(n, event) {
- if (n.options.eventCallback)
- n.options.eventCallback.call(n, event);
+ try {
+ if (n.options.eventCallback)
+ n.options.eventCallback.call(n, event);
+ } catch (error) {
+ Cu.reportError(error);
+ }
},
_onPopupHidden: function PopupNotifications_onPopupHidden(event) {
@@ -789,15 +871,7 @@ PopupNotifications.prototype = {
},
_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;
+ let notificationEl = getNotificationFromElement(event.originalTarget);
if (!notificationEl)
throw "PopupNotifications_onButtonCommand: couldn't find notification element";
@@ -819,7 +893,14 @@ PopupNotifications.prototype = {
timeSinceShown + "ms");
return;
}
- notification.mainAction.callback.call();
+
+ try {
+ notification.mainAction.callback.call(undefined, {
+ checkboxChecked: notificationEl.checkbox.checked
+ });
+ } catch (error) {
+ Cu.reportError(error);
+ }
this._remove(notification);
this._update();
@@ -830,8 +911,16 @@ PopupNotifications.prototype = {
if (!target.action || !target.notification)
throw "menucommand target has no associated action/notification";
+ let notificationEl = target.parentElement;
event.stopPropagation();
- target.action.callback.call();
+
+ try {
+ target.action.callback.call(undefined, {
+ checkboxChecked: notificationEl.checkbox.checked
+ });
+ } catch (error) {
+ Cu.reportError(error);
+ }
this._remove(target.notification);
this._update();
diff --git a/application/palemoon/modules/moz.build b/application/palemoon/modules/moz.build
index f7717ef89..67fd22338 100644
--- a/application/palemoon/modules/moz.build
+++ b/application/palemoon/modules/moz.build
@@ -9,6 +9,7 @@
EXTRA_JS_MODULES += [ 'promise.js' ]
EXTRA_JS_MODULES += [
+ 'AutoCompletePopup.jsm',
'BrowserNewTabPreloader.jsm',
'CharsetMenu.jsm',
'FormSubmitObserver.jsm',