diff options
Diffstat (limited to 'application/palemoon/modules')
-rw-r--r-- | application/palemoon/modules/AutoCompletePopup.jsm | 293 | ||||
-rw-r--r-- | application/palemoon/modules/PopupNotifications.jsm | 121 | ||||
-rw-r--r-- | application/palemoon/modules/moz.build | 1 |
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', |