Mirror of roytam1's UXP fork just in case Moonchild and Tobin decide to go after him
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

299 lines
9.4 KiB

/* 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) {
if (this.results && this.results[idx]) {
return this.results[idx].style || "";
} else {
return "";
}
},
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() {}
}