From fd4031a587f481a24013a3e27c520d49e0f07ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Qu=C3=A8ze?= Date: Wed, 17 Dec 2014 16:20:01 +0100 Subject: [PATCH] Bug 1106559 - Improve the search preference UI, r=felipe,dao. --- browser/components/preferences/jar.mn | 1 - .../components/preferences/preferences.xul | 2 +- browser/components/preferences/search.css | 29 - browser/components/preferences/search.js | 518 ++++++++++++++++-- browser/components/preferences/search.xul | 32 +- browser/themes/linux/jar.mn | 1 + browser/themes/linux/preferences/search.css | 39 ++ browser/themes/osx/jar.mn | 7 + .../osx/preferences/checkbox-yosemite.png | Bin 0 -> 1033 bytes .../osx/preferences/checkbox-yosemite@2x.png | Bin 0 -> 2434 bytes browser/themes/osx/preferences/checkbox.png | Bin 0 -> 1685 bytes .../themes/osx/preferences/checkbox@2x.png | Bin 0 -> 3699 bytes browser/themes/osx/preferences/search.css | 54 ++ browser/themes/windows/jar.mn | 10 + .../themes/windows/preferences/checkbox-8.png | Bin 0 -> 705 bytes .../windows/preferences/checkbox-aero.png | Bin 0 -> 1566 bytes .../windows/preferences/checkbox-classic.png | Bin 0 -> 284 bytes .../windows/preferences/checkbox-xp.png | Bin 0 -> 1417 bytes browser/themes/windows/preferences/search.css | 59 ++ 19 files changed, 687 insertions(+), 65 deletions(-) delete mode 100644 browser/components/preferences/search.css create mode 100644 browser/themes/linux/preferences/search.css create mode 100644 browser/themes/osx/preferences/checkbox-yosemite.png create mode 100644 browser/themes/osx/preferences/checkbox-yosemite@2x.png create mode 100644 browser/themes/osx/preferences/checkbox.png create mode 100644 browser/themes/osx/preferences/checkbox@2x.png create mode 100644 browser/themes/osx/preferences/search.css create mode 100644 browser/themes/windows/preferences/checkbox-8.png create mode 100644 browser/themes/windows/preferences/checkbox-aero.png create mode 100644 browser/themes/windows/preferences/checkbox-classic.png create mode 100644 browser/themes/windows/preferences/checkbox-xp.png create mode 100644 browser/themes/windows/preferences/search.css diff --git a/browser/components/preferences/jar.mn b/browser/components/preferences/jar.mn index 384ae71c5f5a..717af639044f 100644 --- a/browser/components/preferences/jar.mn +++ b/browser/components/preferences/jar.mn @@ -45,7 +45,6 @@ browser.jar: content/browser/preferences/sync.js #endif content/browser/preferences/search.xul - content/browser/preferences/search.css content/browser/preferences/search.js * content/browser/preferences/tabs.xul * content/browser/preferences/tabs.js diff --git a/browser/components/preferences/preferences.xul b/browser/components/preferences/preferences.xul index 8783c558aa1c..984775f45c89 100644 --- a/browser/components/preferences/preferences.xul +++ b/browser/components/preferences/preferences.xul @@ -16,7 +16,7 @@ --> - + diff --git a/browser/components/preferences/search.css b/browser/components/preferences/search.css deleted file mode 100644 index 9866ec618972..000000000000 --- a/browser/components/preferences/search.css +++ /dev/null @@ -1,29 +0,0 @@ -/* 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/. */ - -#oneClickProvidersList richlistitem { - -moz-binding: url("chrome://global/content/bindings/checkbox.xml#checkbox"); - -moz-padding-start: 5px; - height: 22px; /* setting the height of checkboxes is required to let the - window auto-sizing code work. */ -} - -#oneClickProvidersList { - height: 178px; -} - -.searchengine-menuitem > .menu-iconic-left { - display: -moz-box -} - -.checkbox-label-box { - -moz-box-align: center; - -moz-appearance: none; - border: none; -} - -.checkbox-icon { - margin: 3px 3px; - max-width: 16px; -} diff --git a/browser/components/preferences/search.js b/browser/components/preferences/search.js index 36b06cc976c4..04fcdae173d4 100644 --- a/browser/components/preferences/search.js +++ b/browser/components/preferences/search.js @@ -4,13 +4,45 @@ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +const ENGINE_FLAVOR = "text/x-moz-search-engine"; + +var gEngineView = null; + var gSearchPane = { init: function () { + gEngineView = new EngineView(new EngineStore()); + document.getElementById("engineList").view = gEngineView; + this.buildDefaultEngineDropDown(); + + Services.obs.addObserver(this, "browser-search-engine-modified", false); + window.addEventListener("unload", () => { + Services.obs.removeObserver(this, "browser-search-engine-modified", false); + }); + }, + + buildDefaultEngineDropDown: function() { + // This is called each time something affects the list of engines. let list = document.getElementById("defaultEngine"); - let currentEngine = Services.search.currentEngine.name; - Services.search.getVisibleEngines().forEach(e => { + let currentEngine; + + // First, try to preserve the current selection. + if (list.selectedItem) + currentEngine = list.selectedItem.label; + + // If there's no current selection, use the current default engine. + if (!currentEngine) + currentEngine = Services.search.currentEngine.name; + + // If the current engine isn't in the list any more, select the first item. + let engines = gEngineView._engineStore._engines; + if (!engines.some(e => e.name == currentEngine)) + currentEngine = engines[0].name; + + // Now clean-up and rebuild the list. + list.removeAllItems(); + gEngineView._engineStore._engines.forEach(e => { let item = list.appendItem(e.name); item.setAttribute("class", "menuitem-iconic searchengine-menuitem menuitem-with-favicon"); if (e.iconURI) @@ -19,50 +51,472 @@ var gSearchPane = { if (e.name == currentEngine) list.selectedItem = item; }); - - this.displayOneClickEnginesList(); - - document.getElementById("oneClickProvidersList") - .addEventListener("CheckboxStateChange", gSearchPane.saveOneClickEnginesList); }, - displayOneClickEnginesList: function () { - let richlistbox = document.getElementById("oneClickProvidersList"); - let pref = document.getElementById("browser.search.hiddenOneOffs").value; - let hiddenList = pref ? pref.split(",") : []; + observe: function(aEngine, aTopic, aVerb) { + if (aTopic == "browser-search-engine-modified") { + aEngine.QueryInterface(Components.interfaces.nsISearchEngine); + switch (aVerb) { + case "engine-added": + gEngineView._engineStore.addEngine(aEngine); + gEngineView.rowCountChanged(gEngineView.lastIndex, 1); + gSearchPane.buildDefaultEngineDropDown(); + break; + case "engine-changed": + gEngineView._engineStore.reloadIcons(); + gEngineView.invalidate(); + break; + case "engine-removed": + case "engine-current": + case "engine-default": + // Not relevant + break; + } + } + }, - while (richlistbox.firstChild) - richlistbox.firstChild.remove(); + onTreeSelect: function() { + document.getElementById("removeEngineButton").disabled = + gEngineView.selectedIndex == -1 || gEngineView.lastIndex == 0; + }, - let currentEngine = Services.search.currentEngine.name; - Services.search.getVisibleEngines().forEach(e => { - if (e.name == currentEngine) - return; + onTreeKeyPress: function(aEvent) { + let index = gEngineView.selectedIndex; + let tree = document.getElementById("engineList"); + if (aEvent.charCode == KeyEvent.DOM_VK_SPACE) { + // Space toggles the checkbox. + let newValue = !gEngineView._engineStore.engines[index].shown; + gEngineView.setCellValue(index, tree.columns.getFirstColumn(), + newValue.toString()); + } + else { + let isMac = Services.appinfo.OS == "Darwin"; + if ((isMac && aEvent.keyCode == KeyEvent.DOM_VK_RETURN) || + (!isMac && aEvent.keyCode == KeyEvent.DOM_VK_F2)) + tree.startEditing(index, tree.columns.getLastColumn()); + } + }, - let item = document.createElement("richlistitem"); - item.setAttribute("label", e.name); - if (hiddenList.indexOf(e.name) == -1) - item.setAttribute("checked", "true"); - if (e.iconURI) - item.setAttribute("src", e.iconURI.spec); - richlistbox.appendChild(item); - }); + onRestoreDefaults: function() { + let num = gEngineView._engineStore.restoreDefaultEngines(); + gEngineView.rowCountChanged(0, num); + gEngineView.invalidate(); + }, + + showRestoreDefaults: function(aEnable) { + document.getElementById("restoreDefaultSearchEngines").disabled = !aEnable; + }, + + remove: function() { + gEngineView._engineStore.removeEngine(gEngineView.selectedEngine); + let index = gEngineView.selectedIndex; + gEngineView.rowCountChanged(index, -1); + gEngineView.invalidate(); + gEngineView.selection.select(Math.min(index, gEngineView.lastIndex)); + gEngineView.ensureRowIsVisible(gEngineView.currentIndex); + document.getElementById("engineList").focus(); + }, + + editKeyword: function(aEngine, aNewKeyword) { + if (aNewKeyword) { + let bduplicate = false; + let eduplicate = false; + let dupName = ""; + + try { + let bmserv = + Components.classes["@mozilla.org/browser/nav-bookmarks-service;1"] + .getService(Components.interfaces.nsINavBookmarksService); + if (bmserv.getURIForKeyword(aNewKeyword)) + bduplicate = true; + } catch(ex) {} + + // Check for duplicates in changes we haven't committed yet + let engines = gEngineView._engineStore.engines; + for each (let engine in engines) { + if (engine.alias == aNewKeyword && + engine.name != aEngine.name) { + eduplicate = true; + dupName = engine.name; + break; + } + } + + // Notify the user if they have chosen an existing engine/bookmark keyword + if (eduplicate || bduplicate) { + let strings = document.getElementById("engineManagerBundle"); + let dtitle = strings.getString("duplicateTitle"); + let bmsg = strings.getString("duplicateBookmarkMsg"); + let emsg = strings.getFormattedString("duplicateEngineMsg", [dupName]); + + Services.prompt.alert(window, dtitle, eduplicate ? emsg : bmsg); + return false; + } + } + + gEngineView._engineStore.changeEngine(aEngine, "alias", aNewKeyword); + gEngineView.invalidate(); + return true; }, saveOneClickEnginesList: function () { - let richlistbox = document.getElementById("oneClickProvidersList"); let hiddenList = []; - for (let child of richlistbox.childNodes) { - if (!child.checked) - hiddenList.push(child.getAttribute("label")); + for (let engine of gEngineView._engineStore.engines) { + if (!engine.shown) + hiddenList.push(engine.name); } document.getElementById("browser.search.hiddenOneOffs").value = hiddenList.join(","); }, setDefaultEngine: function () { - Services.search.currentEngine = - document.getElementById("defaultEngine").selectedItem.engine; - this.displayOneClickEnginesList(); + if (document.documentElement.instantApply) { + Services.search.currentEngine = + document.getElementById("defaultEngine").selectedItem.engine; + } } }; + +function onDragEngineStart(event) { + var selectedIndex = gEngineView.selectedIndex; + if (selectedIndex >= 0) { + event.dataTransfer.setData(ENGINE_FLAVOR, selectedIndex.toString()); + event.dataTransfer.effectAllowed = "move"; + } +} + +// "Operation" objects +function EngineMoveOp(aEngineClone, aNewIndex) { + if (!aEngineClone) + throw new Error("bad args to new EngineMoveOp!"); + this._engine = aEngineClone.originalEngine; + this._newIndex = aNewIndex; +} +EngineMoveOp.prototype = { + _engine: null, + _newIndex: null, + commit: function EMO_commit() { + Services.search.moveEngine(this._engine, this._newIndex); + } +}; + +function EngineRemoveOp(aEngineClone) { + if (!aEngineClone) + throw new Error("bad args to new EngineRemoveOp!"); + this._engine = aEngineClone.originalEngine; +} +EngineRemoveOp.prototype = { + _engine: null, + commit: function ERO_commit() { + Services.search.removeEngine(this._engine); + } +}; + +function EngineUnhideOp(aEngineClone, aNewIndex) { + if (!aEngineClone) + throw new Error("bad args to new EngineUnhideOp!"); + this._engine = aEngineClone.originalEngine; + this._newIndex = aNewIndex; +} +EngineUnhideOp.prototype = { + _engine: null, + _newIndex: null, + commit: function EUO_commit() { + this._engine.hidden = false; + Services.search.moveEngine(this._engine, this._newIndex); + } +}; + +function EngineChangeOp(aEngineClone, aProp, aValue) { + if (!aEngineClone) + throw new Error("bad args to new EngineChangeOp!"); + + this._engine = aEngineClone.originalEngine; + this._prop = aProp; + this._newValue = aValue; +} +EngineChangeOp.prototype = { + _engine: null, + _prop: null, + _newValue: null, + commit: function ECO_commit() { + this._engine[this._prop] = this._newValue; + } +}; + +function EngineStore() { + let pref = document.getElementById("browser.search.hiddenOneOffs").value; + this.hiddenList = pref ? pref.split(",") : []; + + this._engines = Services.search.getVisibleEngines().map(this._cloneEngine, this); + this._defaultEngines = Services.search.getDefaultEngines().map(this._cloneEngine, this); + + if (document.documentElement.instantApply) { + this._ops = { + push: function(op) { op.commit(); } + }; + } + else { + this._ops = []; + document.documentElement.addEventListener("beforeaccept", () => { + gEngineView._engineStore.commit(); + }); + } + + // check if we need to disable the restore defaults button + var someHidden = this._defaultEngines.some(function (e) e.hidden); + gSearchPane.showRestoreDefaults(someHidden); +} +EngineStore.prototype = { + _engines: null, + _defaultEngines: null, + _ops: null, + + get engines() { + return this._engines; + }, + set engines(val) { + this._engines = val; + return val; + }, + + _getIndexForEngine: function ES_getIndexForEngine(aEngine) { + return this._engines.indexOf(aEngine); + }, + + _getEngineByName: function ES_getEngineByName(aName) { + for each (var engine in this._engines) + if (engine.name == aName) + return engine; + + return null; + }, + + _cloneEngine: function ES_cloneEngine(aEngine) { + var clonedObj={}; + for (var i in aEngine) + clonedObj[i] = aEngine[i]; + clonedObj.originalEngine = aEngine; + clonedObj.shown = this.hiddenList.indexOf(clonedObj.name) == -1; + return clonedObj; + }, + + // Callback for Array's some(). A thisObj must be passed to some() + _isSameEngine: function ES_isSameEngine(aEngineClone) { + return aEngineClone.originalEngine == this.originalEngine; + }, + + commit: function ES_commit() { + for (op of this._ops) + op.commit(); + + Services.search.currentEngine = + document.getElementById("defaultEngine").selectedItem.engine; + }, + + addEngine: function ES_addEngine(aEngine) { + this._engines.push(this._cloneEngine(aEngine)); + }, + + moveEngine: function ES_moveEngine(aEngine, aNewIndex) { + if (aNewIndex < 0 || aNewIndex > this._engines.length - 1) + throw new Error("ES_moveEngine: invalid aNewIndex!"); + var index = this._getIndexForEngine(aEngine); + if (index == -1) + throw new Error("ES_moveEngine: invalid engine?"); + + if (index == aNewIndex) + return; // nothing to do + + // Move the engine in our internal store + var removedEngine = this._engines.splice(index, 1)[0]; + this._engines.splice(aNewIndex, 0, removedEngine); + + this._ops.push(new EngineMoveOp(aEngine, aNewIndex)); + }, + + removeEngine: function ES_removeEngine(aEngine) { + var index = this._getIndexForEngine(aEngine); + if (index == -1) + throw new Error("invalid engine?"); + + this._engines.splice(index, 1); + this._ops.push(new EngineRemoveOp(aEngine)); + if (this._defaultEngines.some(this._isSameEngine, aEngine)) + gSearchPane.showRestoreDefaults(true); + gSearchPane.buildDefaultEngineDropDown(); + }, + + restoreDefaultEngines: function ES_restoreDefaultEngines() { + var added = 0; + + for (var i = 0; i < this._defaultEngines.length; ++i) { + var e = this._defaultEngines[i]; + + // If the engine is already in the list, just move it. + if (this._engines.some(this._isSameEngine, e)) { + this.moveEngine(this._getEngineByName(e.name), i); + } else { + // Otherwise, add it back to our internal store + this._engines.splice(i, 0, e); + this._ops.push(new EngineUnhideOp(e, i)); + added++; + } + } + gSearchPane.showRestoreDefaults(false); + gSearchPane.buildDefaultEngineDropDown(); + return added; + }, + + changeEngine: function ES_changeEngine(aEngine, aProp, aNewValue) { + var index = this._getIndexForEngine(aEngine); + if (index == -1) + throw new Error("invalid engine?"); + + this._engines[index][aProp] = aNewValue; + this._ops.push(new EngineChangeOp(aEngine, aProp, aNewValue)); + }, + + reloadIcons: function ES_reloadIcons() { + this._engines.forEach(function (e) { + e.uri = e.originalEngine.uri; + }); + } +}; + +function EngineView(aEngineStore) { + this._engineStore = aEngineStore; +} +EngineView.prototype = { + _engineStore: null, + tree: null, + + get lastIndex() { + return this.rowCount - 1; + }, + get selectedIndex() { + var seln = this.selection; + if (seln.getRangeCount() > 0) { + var min = {}; + seln.getRangeAt(0, min, {}); + return min.value; + } + return -1; + }, + get selectedEngine() { + return this._engineStore.engines[this.selectedIndex]; + }, + + // Helpers + rowCountChanged: function (index, count) { + this.tree.rowCountChanged(index, count); + }, + + invalidate: function () { + this.tree.invalidate(); + }, + + ensureRowIsVisible: function (index) { + this.tree.ensureRowIsVisible(index); + }, + + getSourceIndexFromDrag: function (dataTransfer) { + return parseInt(dataTransfer.getData(ENGINE_FLAVOR)); + }, + + // nsITreeView + get rowCount() { + return this._engineStore.engines.length; + }, + + getImageSrc: function(index, column) { + if (column.id == "engineName" && this._engineStore.engines[index].iconURI) + return this._engineStore.engines[index].iconURI.spec; + return ""; + }, + + getCellText: function(index, column) { + if (column.id == "engineName") + return this._engineStore.engines[index].name; + else if (column.id == "engineKeyword") + return this._engineStore.engines[index].alias; + return ""; + }, + + setTree: function(tree) { + this.tree = tree; + }, + + canDrop: function(targetIndex, orientation, dataTransfer) { + var sourceIndex = this.getSourceIndexFromDrag(dataTransfer); + return (sourceIndex != -1 && + sourceIndex != targetIndex && + sourceIndex != targetIndex + orientation); + }, + + drop: function(dropIndex, orientation, dataTransfer) { + var sourceIndex = this.getSourceIndexFromDrag(dataTransfer); + var sourceEngine = this._engineStore.engines[sourceIndex]; + + const nsITreeView = Components.interfaces.nsITreeView; + if (dropIndex > sourceIndex) { + if (orientation == nsITreeView.DROP_BEFORE) + dropIndex--; + } else { + if (orientation == nsITreeView.DROP_AFTER) + dropIndex++; + } + + this._engineStore.moveEngine(sourceEngine, dropIndex); + gSearchPane.showRestoreDefaults(true); + gSearchPane.buildDefaultEngineDropDown(); + + // Redraw, and adjust selection + this.invalidate(); + this.selection.select(dropIndex); + }, + + selection: null, + getRowProperties: function(index) { return ""; }, + getCellProperties: function(index, column) { return ""; }, + getColumnProperties: function(column) { return ""; }, + isContainer: function(index) { return false; }, + isContainerOpen: function(index) { return false; }, + isContainerEmpty: function(index) { return false; }, + isSeparator: function(index) { return false; }, + isSorted: function(index) { return false; }, + getParentIndex: function(index) { return -1; }, + hasNextSibling: function(parentIndex, index) { return false; }, + getLevel: function(index) { return 0; }, + getProgressMode: function(index, column) { }, + getCellValue: function(index, column) { + if (column.id == "engineShown") + return this._engineStore.engines[index].shown; + return undefined; + }, + toggleOpenState: function(index) { }, + cycleHeader: function(column) { }, + selectionChanged: function() { }, + cycleCell: function(row, column) { }, + isEditable: function(index, column) { return column.id != "engineName"; }, + isSelectable: function(index, column) { return false; }, + setCellValue: function(index, column, value) { + if (column.id == "engineShown") { + this._engineStore.engines[index].shown = value == "true"; + gEngineView.invalidate(); + gSearchPane.saveOneClickEnginesList(); + } + }, + setCellText: function(index, column, value) { + if (column.id == "engineKeyword") { + if (!gSearchPane.editKeyword(this._engineStore.engines[index], value)) { + setTimeout(() => { + document.getElementById("engineList").startEditing(index, column); + }, 0); + } + } + }, + performAction: function(action) { }, + performActionOnRow: function(action, index) { }, + performActionOnCell: function(action, index, column) { } +}; diff --git a/browser/components/preferences/search.xul b/browser/components/preferences/search.xul index c9190dc154f2..8e36d4091718 100644 --- a/browser/components/preferences/search.xul +++ b/browser/components/preferences/search.xul @@ -34,6 +34,8 @@