engineManager.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  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. Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  5. Components.utils.import("resource://gre/modules/Services.jsm");
  6. XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
  7. "resource://gre/modules/PlacesUtils.jsm");
  8. XPCOMUtils.defineLazyModuleGetter(this, "Task",
  9. "resource://gre/modules/Task.jsm");
  10. const Ci = Components.interfaces;
  11. const Cc = Components.classes;
  12. const ENGINE_FLAVOR = "text/x-moz-search-engine";
  13. const BROWSER_SUGGEST_PREF = "browser.search.suggest.enabled";
  14. var gEngineView = null;
  15. var gEngineManagerDialog = {
  16. init: function() {
  17. gEngineView = new EngineView(new EngineStore());
  18. var suggestEnabled = Services.prefs.getBoolPref(BROWSER_SUGGEST_PREF);
  19. document.getElementById("enableSuggest").checked = suggestEnabled;
  20. var tree = document.getElementById("engineList");
  21. tree.view = gEngineView;
  22. Services.obs.addObserver(this, "browser-search-engine-modified", false);
  23. },
  24. destroy: function() {
  25. // Remove the observer
  26. Services.obs.removeObserver(this, "browser-search-engine-modified");
  27. },
  28. observe: function(aEngine, aTopic, aVerb) {
  29. if (aTopic == "browser-search-engine-modified") {
  30. aEngine.QueryInterface(Ci.nsISearchEngine);
  31. switch (aVerb) {
  32. case "engine-added":
  33. gEngineView._engineStore.addEngine(aEngine);
  34. gEngineView.rowCountChanged(gEngineView.lastIndex, 1);
  35. break;
  36. case "engine-changed":
  37. gEngineView._engineStore.reloadIcons();
  38. gEngineView.invalidate();
  39. break;
  40. case "engine-removed":
  41. case "engine-current":
  42. case "engine-default":
  43. // Not relevant
  44. break;
  45. }
  46. }
  47. },
  48. onOK: function() {
  49. // Set the preference
  50. var newSuggestEnabled = document.getElementById("enableSuggest").checked;
  51. Services.prefs.setBoolPref(BROWSER_SUGGEST_PREF, newSuggestEnabled);
  52. // Commit the changes
  53. gEngineView._engineStore.commit();
  54. },
  55. onRestoreDefaults: function() {
  56. var num = gEngineView._engineStore.restoreDefaultEngines();
  57. gEngineView.rowCountChanged(0, num);
  58. gEngineView.invalidate();
  59. },
  60. showRestoreDefaults: function(val) {
  61. document.documentElement.getButton("extra2").disabled = !val;
  62. },
  63. loadAddEngines: function() {
  64. this.onOK();
  65. window.opener.BrowserSearch.loadAddEngines();
  66. window.close();
  67. },
  68. remove: function() {
  69. gEngineView._engineStore.removeEngine(gEngineView.selectedEngine);
  70. var index = gEngineView.selectedIndex;
  71. gEngineView.rowCountChanged(index, -1);
  72. gEngineView.invalidate();
  73. gEngineView.selection.select(Math.min(index, gEngineView.lastIndex));
  74. gEngineView.ensureRowIsVisible(gEngineView.currentIndex);
  75. document.getElementById("engineList").focus();
  76. },
  77. /**
  78. * Moves the selected engine either up or down in the engine list
  79. * @param aDir
  80. * -1 to move the selected engine down, +1 to move it up.
  81. */
  82. bump: function(aDir) {
  83. var selectedEngine = gEngineView.selectedEngine;
  84. var newIndex = gEngineView.selectedIndex - aDir;
  85. gEngineView._engineStore.moveEngine(selectedEngine, newIndex);
  86. gEngineView.invalidate();
  87. gEngineView.selection.select(newIndex);
  88. gEngineView.ensureRowIsVisible(newIndex);
  89. this.showRestoreDefaults(true);
  90. document.getElementById("engineList").focus();
  91. },
  92. editKeyword: Task.async(function* () {
  93. var selectedEngine = gEngineView.selectedEngine;
  94. if (!selectedEngine)
  95. return;
  96. var alias = { value: selectedEngine.alias };
  97. var strings = document.getElementById("engineManagerBundle");
  98. var title = strings.getString("editTitle");
  99. var msg = strings.getFormattedString("editMsg", [selectedEngine.name]);
  100. while (Services.prompt.prompt(window, title, msg, alias, null, {})) {
  101. var bduplicate = false;
  102. var eduplicate = false;
  103. var dupName = "";
  104. if (alias.value != "") {
  105. // Check for duplicates in Places keywords.
  106. bduplicate = !!(yield PlacesUtils.keywords.fetch(alias.value));
  107. // Check for duplicates in changes we haven't committed yet
  108. let engines = gEngineView._engineStore.engines;
  109. for each (let engine in engines) {
  110. if (engine.alias == alias.value &&
  111. engine.name != selectedEngine.name) {
  112. eduplicate = true;
  113. dupName = engine.name;
  114. break;
  115. }
  116. }
  117. }
  118. // Notify the user if they have chosen an existing engine/bookmark keyword
  119. if (eduplicate || bduplicate) {
  120. var dtitle = strings.getString("duplicateTitle");
  121. var bmsg = strings.getString("duplicateBookmarkMsg");
  122. var emsg = strings.getFormattedString("duplicateEngineMsg", [dupName]);
  123. Services.prompt.alert(window, dtitle, eduplicate ? emsg : bmsg);
  124. } else {
  125. gEngineView._engineStore.changeEngine(selectedEngine, "alias",
  126. alias.value);
  127. gEngineView.invalidate();
  128. break;
  129. }
  130. }
  131. }),
  132. onSelect: function() {
  133. // Buttons only work if an engine is selected and it's not the last engine,
  134. // the latter is true when the selected is first and last at the same time.
  135. var lastSelected = (gEngineView.selectedIndex == gEngineView.lastIndex);
  136. var firstSelected = (gEngineView.selectedIndex == 0);
  137. var noSelection = (gEngineView.selectedIndex == -1);
  138. document.getElementById("cmd_remove")
  139. .setAttribute("disabled", noSelection ||
  140. (firstSelected && lastSelected));
  141. document.getElementById("cmd_moveup")
  142. .setAttribute("disabled", noSelection || firstSelected);
  143. document.getElementById("cmd_movedown")
  144. .setAttribute("disabled", noSelection || lastSelected);
  145. document.getElementById("cmd_editkeyword")
  146. .setAttribute("disabled", noSelection);
  147. }
  148. };
  149. function onDragEngineStart(event) {
  150. var selectedIndex = gEngineView.selectedIndex;
  151. if (selectedIndex >= 0) {
  152. event.dataTransfer.setData(ENGINE_FLAVOR, selectedIndex.toString());
  153. event.dataTransfer.effectAllowed = "move";
  154. }
  155. }
  156. // "Operation" objects
  157. function EngineMoveOp(aEngineClone, aNewIndex) {
  158. if (!aEngineClone)
  159. throw new Error("bad args to new EngineMoveOp!");
  160. this._engine = aEngineClone.originalEngine;
  161. this._newIndex = aNewIndex;
  162. }
  163. EngineMoveOp.prototype = {
  164. _engine: null,
  165. _newIndex: null,
  166. commit: function() {
  167. Services.search.moveEngine(this._engine, this._newIndex);
  168. }
  169. }
  170. function EngineRemoveOp(aEngineClone) {
  171. if (!aEngineClone)
  172. throw new Error("bad args to new EngineRemoveOp!");
  173. this._engine = aEngineClone.originalEngine;
  174. }
  175. EngineRemoveOp.prototype = {
  176. _engine: null,
  177. commit: function() {
  178. Services.search.removeEngine(this._engine);
  179. }
  180. }
  181. function EngineUnhideOp(aEngineClone, aNewIndex) {
  182. if (!aEngineClone)
  183. throw new Error("bad args to new EngineUnhideOp!");
  184. this._engine = aEngineClone.originalEngine;
  185. this._newIndex = aNewIndex;
  186. }
  187. EngineUnhideOp.prototype = {
  188. _engine: null,
  189. _newIndex: null,
  190. commit: function() {
  191. this._engine.hidden = false;
  192. Services.search.moveEngine(this._engine, this._newIndex);
  193. }
  194. }
  195. function EngineChangeOp(aEngineClone, aProp, aValue) {
  196. if (!aEngineClone)
  197. throw new Error("bad args to new EngineChangeOp!");
  198. this._engine = aEngineClone.originalEngine;
  199. this._prop = aProp;
  200. this._newValue = aValue;
  201. }
  202. EngineChangeOp.prototype = {
  203. _engine: null,
  204. _prop: null,
  205. _newValue: null,
  206. commit: function() {
  207. this._engine[this._prop] = this._newValue;
  208. }
  209. }
  210. function EngineStore() {
  211. this._engines = Services.search.getVisibleEngines().map(this._cloneEngine);
  212. this._defaultEngines = Services.search.getDefaultEngines().map(this._cloneEngine);
  213. this._ops = [];
  214. // check if we need to disable the restore defaults button
  215. var someHidden = this._defaultEngines.some(function(e) e.hidden);
  216. gEngineManagerDialog.showRestoreDefaults(someHidden);
  217. }
  218. EngineStore.prototype = {
  219. _engines: null,
  220. _defaultEngines: null,
  221. _ops: null,
  222. get engines() {
  223. return this._engines;
  224. },
  225. set engines(val) {
  226. this._engines = val;
  227. return val;
  228. },
  229. _getIndexForEngine: function(aEngine) {
  230. return this._engines.indexOf(aEngine);
  231. },
  232. _getEngineByName: function(aName) {
  233. for each (var engine in this._engines)
  234. if (engine.name == aName)
  235. return engine;
  236. return null;
  237. },
  238. _cloneEngine: function(aEngine) {
  239. var clonedObj={};
  240. for (var i in aEngine)
  241. clonedObj[i] = aEngine[i];
  242. clonedObj.originalEngine = aEngine;
  243. return clonedObj;
  244. },
  245. // Callback for Array's some(). A thisObj must be passed to some()
  246. _isSameEngine: function(aEngineClone) {
  247. return aEngineClone.originalEngine == this.originalEngine;
  248. },
  249. commit: function() {
  250. var currentEngine = this._cloneEngine(Services.search.currentEngine);
  251. for (var i = 0; i < this._ops.length; i++)
  252. this._ops[i].commit();
  253. // Restore currentEngine if it is a default engine that is still visible.
  254. // Needed if the user deletes currentEngine and then restores it.
  255. if (this._defaultEngines.some(this._isSameEngine, currentEngine) &&
  256. !currentEngine.originalEngine.hidden)
  257. Services.search.currentEngine = currentEngine.originalEngine;
  258. },
  259. addEngine: function(aEngine) {
  260. this._engines.push(this._cloneEngine(aEngine));
  261. },
  262. moveEngine: function(aEngine, aNewIndex) {
  263. if (aNewIndex < 0 || aNewIndex > this._engines.length - 1)
  264. throw new Error("ES_moveEngine: invalid aNewIndex!");
  265. var index = this._getIndexForEngine(aEngine);
  266. if (index == -1)
  267. throw new Error("ES_moveEngine: invalid engine?");
  268. if (index == aNewIndex)
  269. return; // nothing to do
  270. // Move the engine in our internal store
  271. var removedEngine = this._engines.splice(index, 1)[0];
  272. this._engines.splice(aNewIndex, 0, removedEngine);
  273. this._ops.push(new EngineMoveOp(aEngine, aNewIndex));
  274. },
  275. removeEngine: function(aEngine) {
  276. var index = this._getIndexForEngine(aEngine);
  277. if (index == -1)
  278. throw new Error("invalid engine?");
  279. this._engines.splice(index, 1);
  280. this._ops.push(new EngineRemoveOp(aEngine));
  281. if (this._defaultEngines.some(this._isSameEngine, aEngine))
  282. gEngineManagerDialog.showRestoreDefaults(true);
  283. },
  284. restoreDefaultEngines: function() {
  285. var added = 0;
  286. for (var i = 0; i < this._defaultEngines.length; ++i) {
  287. var e = this._defaultEngines[i];
  288. // If the engine is already in the list, just move it.
  289. if (this._engines.some(this._isSameEngine, e)) {
  290. this.moveEngine(this._getEngineByName(e.name), i);
  291. } else {
  292. // Otherwise, add it back to our internal store
  293. this._engines.splice(i, 0, e);
  294. this._ops.push(new EngineUnhideOp(e, i));
  295. added++;
  296. }
  297. }
  298. gEngineManagerDialog.showRestoreDefaults(false);
  299. return added;
  300. },
  301. changeEngine: function(aEngine, aProp, aNewValue) {
  302. var index = this._getIndexForEngine(aEngine);
  303. if (index == -1)
  304. throw new Error("invalid engine?");
  305. this._engines[index][aProp] = aNewValue;
  306. this._ops.push(new EngineChangeOp(aEngine, aProp, aNewValue));
  307. },
  308. reloadIcons: function() {
  309. this._engines.forEach(function(e) {
  310. e.uri = e.originalEngine.uri;
  311. });
  312. }
  313. }
  314. function EngineView(aEngineStore) {
  315. this._engineStore = aEngineStore;
  316. }
  317. EngineView.prototype = {
  318. _engineStore: null,
  319. tree: null,
  320. get lastIndex() {
  321. return this.rowCount - 1;
  322. },
  323. get selectedIndex() {
  324. var seln = this.selection;
  325. if (seln.getRangeCount() > 0) {
  326. var min = {};
  327. seln.getRangeAt(0, min, {});
  328. return min.value;
  329. }
  330. return -1;
  331. },
  332. get selectedEngine() {
  333. return this._engineStore.engines[this.selectedIndex];
  334. },
  335. // Helpers
  336. rowCountChanged: function(index, count) {
  337. this.tree.rowCountChanged(index, count);
  338. },
  339. invalidate: function() {
  340. this.tree.invalidate();
  341. },
  342. ensureRowIsVisible: function(index) {
  343. this.tree.ensureRowIsVisible(index);
  344. },
  345. getSourceIndexFromDrag: function(dataTransfer) {
  346. return parseInt(dataTransfer.getData(ENGINE_FLAVOR));
  347. },
  348. // nsITreeView
  349. get rowCount() {
  350. return this._engineStore.engines.length;
  351. },
  352. getImageSrc: function(index, column) {
  353. if (column.id == "engineName" && this._engineStore.engines[index].iconURI)
  354. return this._engineStore.engines[index].iconURI.spec;
  355. return "";
  356. },
  357. getCellText: function(index, column) {
  358. if (column.id == "engineName")
  359. return this._engineStore.engines[index].name;
  360. else if (column.id == "engineKeyword")
  361. return this._engineStore.engines[index].alias;
  362. return "";
  363. },
  364. setTree: function(tree) {
  365. this.tree = tree;
  366. },
  367. canDrop: function(targetIndex, orientation, dataTransfer) {
  368. var sourceIndex = this.getSourceIndexFromDrag(dataTransfer);
  369. return (sourceIndex != -1 &&
  370. sourceIndex != targetIndex &&
  371. sourceIndex != targetIndex + orientation);
  372. },
  373. drop: function(dropIndex, orientation, dataTransfer) {
  374. var sourceIndex = this.getSourceIndexFromDrag(dataTransfer);
  375. var sourceEngine = this._engineStore.engines[sourceIndex];
  376. if (dropIndex > sourceIndex) {
  377. if (orientation == Ci.nsITreeView.DROP_BEFORE)
  378. dropIndex--;
  379. } else {
  380. if (orientation == Ci.nsITreeView.DROP_AFTER)
  381. dropIndex++;
  382. }
  383. this._engineStore.moveEngine(sourceEngine, dropIndex);
  384. gEngineManagerDialog.showRestoreDefaults(true);
  385. // Redraw, and adjust selection
  386. this.invalidate();
  387. this.selection.select(dropIndex);
  388. },
  389. selection: null,
  390. getRowProperties: function(index) { return ""; },
  391. getCellProperties: function(index, column) { return ""; },
  392. getColumnProperties: function(column) { return ""; },
  393. isContainer: function(index) { return false; },
  394. isContainerOpen: function(index) { return false; },
  395. isContainerEmpty: function(index) { return false; },
  396. isSeparator: function(index) { return false; },
  397. isSorted: function(index) { return false; },
  398. getParentIndex: function(index) { return -1; },
  399. hasNextSibling: function(parentIndex, index) { return false; },
  400. getLevel: function(index) { return 0; },
  401. getProgressMode: function(index, column) { },
  402. getCellValue: function(index, column) { },
  403. toggleOpenState: function(index) { },
  404. cycleHeader: function(column) { },
  405. selectionChanged: function() { },
  406. cycleCell: function(row, column) { },
  407. isEditable: function(index, column) { return false; },
  408. isSelectable: function(index, column) { return false; },
  409. setCellValue: function(index, column, value) { },
  410. setCellText: function(index, column, value) { },
  411. performAction: function(action) { },
  412. performActionOnRow: function(action, index) { },
  413. performActionOnCell: function(action, index, column) { }
  414. };