AutoCompletePopup.jsm 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. const Cc = Components.classes;
  6. const Ci = Components.interfaces;
  7. const Cu = Components.utils;
  8. this.EXPORTED_SYMBOLS = [ "AutoCompletePopup" ];
  9. Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  10. Cu.import("resource://gre/modules/Services.jsm");
  11. // nsITreeView implementation that feeds the autocomplete popup
  12. // with the search data.
  13. var AutoCompleteTreeView = {
  14. // nsISupports
  15. QueryInterface: XPCOMUtils.generateQI([Ci.nsITreeView,
  16. Ci.nsIAutoCompleteController]),
  17. // Private variables
  18. treeBox: null,
  19. results: [],
  20. // nsITreeView
  21. selection: null,
  22. get rowCount() { return this.results.length; },
  23. setTree: function(treeBox) { this.treeBox = treeBox; },
  24. getCellText: function(idx, column) { return this.results[idx].value },
  25. isContainer: function(idx) { return false; },
  26. getCellValue: function(idx, column) { return false },
  27. isContainerOpen: function(idx) { return false; },
  28. isContainerEmpty: function(idx) { return false; },
  29. isSeparator: function(idx) { return false; },
  30. isSorted: function() { return false; },
  31. isEditable: function(idx, column) { return false; },
  32. canDrop: function(idx, orientation, dt) { return false; },
  33. getLevel: function(idx) { return 0; },
  34. getParentIndex: function(idx) { return -1; },
  35. hasNextSibling: function(idx, after) { return idx < this.results.length - 1 },
  36. toggleOpenState: function(idx) { },
  37. getCellProperties: function(idx, column) {
  38. if (this.results && this.results[idx]) {
  39. return this.results[idx].style || "";
  40. } else {
  41. return "";
  42. }
  43. },
  44. getRowProperties: function(idx) { return ""; },
  45. getImageSrc: function(idx, column) { return null; },
  46. getProgressMode : function(idx, column) { },
  47. cycleHeader: function(column) { },
  48. cycleCell: function(idx, column) { },
  49. selectionChanged: function() { },
  50. performAction: function(action) { },
  51. performActionOnCell: function(action, index, column) { },
  52. getColumnProperties: function(column) { return ""; },
  53. // nsIAutoCompleteController
  54. get matchCount() {
  55. return this.rowCount;
  56. },
  57. handleEnter: function(aIsPopupSelection) {
  58. AutoCompletePopup.handleEnter(aIsPopupSelection);
  59. },
  60. stopSearch: function() {},
  61. // Internal JS-only API
  62. clearResults: function() {
  63. this.results = [];
  64. },
  65. setResults: function(results) {
  66. this.results = results;
  67. },
  68. };
  69. this.AutoCompletePopup = {
  70. MESSAGES: [
  71. "FormAutoComplete:SelectBy",
  72. "FormAutoComplete:GetSelectedIndex",
  73. "FormAutoComplete:SetSelectedIndex",
  74. "FormAutoComplete:MaybeOpenPopup",
  75. "FormAutoComplete:ClosePopup",
  76. "FormAutoComplete:Disconnect",
  77. "FormAutoComplete:RemoveEntry",
  78. "FormAutoComplete:Invalidate",
  79. ],
  80. init: function() {
  81. for (let msg of this.MESSAGES) {
  82. Services.mm.addMessageListener(msg, this);
  83. }
  84. },
  85. uninit: function() {
  86. for (let msg of this.MESSAGES) {
  87. Services.mm.removeMessageListener(msg, this);
  88. }
  89. },
  90. handleEvent: function(evt) {
  91. switch (evt.type) {
  92. case "popupshowing": {
  93. this.sendMessageToBrowser("FormAutoComplete:PopupOpened");
  94. break;
  95. }
  96. case "popuphidden": {
  97. this.sendMessageToBrowser("FormAutoComplete:PopupClosed");
  98. this.openedPopup = null;
  99. this.weakBrowser = null;
  100. evt.target.removeEventListener("popuphidden", this);
  101. evt.target.removeEventListener("popupshowing", this);
  102. break;
  103. }
  104. }
  105. },
  106. // Along with being called internally by the receiveMessage handler,
  107. // this function is also called directly by the login manager, which
  108. // uses a single message to fill in the autocomplete results. See
  109. // "RemoteLogins:autoCompleteLogins".
  110. showPopupWithResults: function({ browser, rect, dir, results }) {
  111. if (!results.length || this.openedPopup) {
  112. // We shouldn't ever be showing an empty popup, and if we
  113. // already have a popup open, the old one needs to close before
  114. // we consider opening a new one.
  115. return;
  116. }
  117. let window = browser.ownerDocument.defaultView;
  118. let tabbrowser = window.gBrowser;
  119. if (Services.focus.activeWindow != window ||
  120. tabbrowser.selectedBrowser != browser) {
  121. // We were sent a message from a window or tab that went into the
  122. // background, so we'll ignore it for now.
  123. return;
  124. }
  125. this.weakBrowser = Cu.getWeakReference(browser);
  126. this.openedPopup = browser.autoCompletePopup;
  127. this.openedPopup.hidden = false;
  128. // don't allow the popup to become overly narrow
  129. this.openedPopup.setAttribute("width", Math.max(100, rect.width));
  130. this.openedPopup.style.direction = dir;
  131. AutoCompleteTreeView.setResults(results);
  132. this.openedPopup.view = AutoCompleteTreeView;
  133. this.openedPopup.selectedIndex = -1;
  134. this.openedPopup.invalidate();
  135. if (results.length) {
  136. // Reset fields that were set from the last time the search popup was open
  137. this.openedPopup.mInput = null;
  138. this.openedPopup.showCommentColumn = false;
  139. this.openedPopup.showImageColumn = false;
  140. this.openedPopup.addEventListener("popuphidden", this);
  141. this.openedPopup.addEventListener("popupshowing", this);
  142. this.openedPopup.openPopupAtScreenRect("after_start", rect.left, rect.top,
  143. rect.width, rect.height, false,
  144. false);
  145. } else {
  146. this.closePopup();
  147. }
  148. },
  149. invalidate(results) {
  150. if (!this.openedPopup) {
  151. return;
  152. }
  153. if (!results.length) {
  154. this.closePopup();
  155. } else {
  156. AutoCompleteTreeView.setResults(results);
  157. // We need to re-set the view in order for the
  158. // tree to know the view has changed.
  159. this.openedPopup.view = AutoCompleteTreeView;
  160. this.openedPopup.invalidate();
  161. }
  162. },
  163. closePopup() {
  164. if (this.openedPopup) {
  165. try {
  166. // Note that hidePopup() closes the popup immediately,
  167. // so popuphiding or popuphidden events will be fired
  168. // and handled during this call.
  169. this.openedPopup.hidePopup();
  170. } catch(e) {
  171. Cu.reportError(e);
  172. console.log("Debug: ", this.openedPopup);
  173. }
  174. }
  175. AutoCompleteTreeView.clearResults();
  176. },
  177. removeLogin(login) {
  178. Services.logins.removeLogin(login);
  179. },
  180. receiveMessage: function(message) {
  181. if (!message.target.autoCompletePopup) {
  182. // Returning false to pacify ESLint, but this return value is
  183. // ignored by the messaging infrastructure.
  184. return false;
  185. }
  186. switch (message.name) {
  187. case "FormAutoComplete:SelectBy": {
  188. this.openedPopup.selectBy(message.data.reverse, message.data.page);
  189. break;
  190. }
  191. case "FormAutoComplete:GetSelectedIndex": {
  192. if (this.openedPopup) {
  193. return this.openedPopup.selectedIndex;
  194. }
  195. // If the popup was closed, then the selection
  196. // has not changed.
  197. return -1;
  198. }
  199. case "FormAutoComplete:SetSelectedIndex": {
  200. let { index } = message.data;
  201. if (this.openedPopup) {
  202. this.openedPopup.selectedIndex = index;
  203. }
  204. break;
  205. }
  206. case "FormAutoComplete:MaybeOpenPopup": {
  207. let { results, rect, dir } = message.data;
  208. this.showPopupWithResults({ browser: message.target, rect, dir,
  209. results });
  210. break;
  211. }
  212. case "FormAutoComplete:Invalidate": {
  213. let { results } = message.data;
  214. this.invalidate(results);
  215. break;
  216. }
  217. case "FormAutoComplete:ClosePopup": {
  218. this.closePopup();
  219. break;
  220. }
  221. case "FormAutoComplete:Disconnect": {
  222. // The controller stopped controlling the current input, so clear
  223. // any cached data. This is necessary cause otherwise we'd clear data
  224. // only when starting a new search, but the next input could not support
  225. // autocomplete and it would end up inheriting the existing data.
  226. AutoCompleteTreeView.clearResults();
  227. break;
  228. }
  229. }
  230. // Returning false to pacify ESLint, but this return value is
  231. // ignored by the messaging infrastructure.
  232. return false;
  233. },
  234. /**
  235. * Despite its name, this handleEnter is only called when the user clicks on
  236. * one of the items in the popup since the popup is rendered in the parent process.
  237. * The real controller's handleEnter is called directly in the content process
  238. * for other methods of completing a selection (e.g. using the tab or enter
  239. * keys) since the field with focus is in that process.
  240. */
  241. handleEnter(aIsPopupSelection) {
  242. if (this.openedPopup) {
  243. this.sendMessageToBrowser("FormAutoComplete:HandleEnter", {
  244. selectedIndex: this.openedPopup.selectedIndex,
  245. isPopupSelection: aIsPopupSelection,
  246. });
  247. }
  248. },
  249. /**
  250. * If a browser exists that AutoCompletePopup knows about,
  251. * sends it a message. Otherwise, this is a no-op.
  252. *
  253. * @param {string} msgName
  254. * The name of the message to send.
  255. * @param {object} data
  256. * The optional data to send with the message.
  257. */
  258. sendMessageToBrowser(msgName, data) {
  259. let browser = this.weakBrowser ? this.weakBrowser.get()
  260. : null;
  261. if (browser) {
  262. browser.messageManager.sendAsyncMessage(msgName, data);
  263. }
  264. },
  265. stopSearch: function() {}
  266. }