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