Bug 540009 - revise about:config screen design for touch use [r=mark.finkle]
authorVivien Nicolas <21@vingtetun.org>
Wed, 24 Mar 2010 11:31:09 -0400
changeset 66055 a705deb34f8e50ce8a924446e6f3e5cb1d10d5bb
parent 66054 1efa9086bf91ecdd5dfc3e812181f39e7cf45ab3
child 66056 fbfb4e6f3da5bffd7970bef178e109cd075b8e50
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmark
bugs540009
Bug 540009 - revise about:config screen design for touch use [r=mark.finkle]
mobile/chrome/content/bindings.xml
mobile/chrome/content/bindings/setting.xml
mobile/chrome/content/browser-ui.js
mobile/chrome/content/browser.css
mobile/chrome/content/browser.js
mobile/chrome/content/config.js
mobile/chrome/content/config.xul
mobile/chrome/jar.mn
mobile/locales/en-US/chrome/config.dtd
mobile/locales/jar.mn
mobile/themes/hildon/config.css
mobile/themes/hildon/jar.mn
mobile/themes/wince/config.css
mobile/themes/wince/jar.mn
--- a/mobile/chrome/content/bindings.xml
+++ b/mobile/chrome/content/bindings.xml
@@ -352,16 +352,24 @@
           }
         ]]>
       </handler>
     </handlers>
   </binding>
 
   <binding id="place-base">
     <content/>
+
+    <handlers>
+      <handler event="click" button="0">
+        if (this.control)
+          this.control._fireOpen(event, this);
+      </handler>
+    </handlers>
+
     <implementation>
       <field name="_uri">null</field>
       <field name="_control">null</field>
       <field name="_isEditing">false</field>
 
       <field name="_ioService">
         Components.classes["@mozilla.org/network/io-service;1"]
                   .getService(Components.interfaces.nsIIOService);
@@ -632,27 +640,36 @@
             document.getAnonymousElementByAttribute(this, "anonid", "bookmark-manage").hidden = !this._isEditing;
           ]]>
         </body>
       </method>
     </implementation>
   </binding>
 
   <binding id="place-label" extends="chrome://browser/content/bindings.xml#place-base">
+    <handlers>
+      <handler event="click" button="0">
+        <![CDATA[
+          if (this.control)
+            this.control.openFolder(this.previousSibling.itemId);
+        ]]>
+      </handler>
+    </handlers>
+
     <content align="center">
       <xul:spacer xbl:inherits="width=indent"/>
       <xul:image anonid="favicon" class="bookmark-folder-image"/>
       <xul:label anonid="name" crop="end" flex="1" xbl:inherits="value=title"/>
     </content>
   </binding>
 
   <binding id="place-list">
     <content orient="vertical" flex="1">
       <xul:vbox anonid="parent-items" class="place-list-parents" />
-      <xul:richlistbox anonid="child-items" class="place-list-children" flex="1"/>
+      <xul:richlistbox anonid="child-items" class="place-list-children" flex="1" batch="25"/>
     </content>
     <implementation>
       <constructor>
         <![CDATA[
           this._type = this.getAttribute("type");
           this._mode = this.getAttribute("mode");
 
           this._folderParents = {};
@@ -704,22 +721,16 @@
       <field name="_parents">
         document.getAnonymousElementByAttribute(this, "anonid", "parent-items");
       </field>
       <field name="_children">
         document.getAnonymousElementByAttribute(this, "anonid", "child-items");
       </field>
 
       <field name="scrollBoxObject">this._children.scrollBoxObject</field>
-      <!-- This won't be updated when the window size changes, but that's OK,
-           since we don't need an exact value - we just use it to get an
-           approximate scroll position for deciding whether to append additional
-           items (see batchSize/_insertItems()).
-        -->
-      <field name="_childrenHeight">this.scrollBoxObject.height;</field>
 
       <property name="items" readonly="true" onget="return this._children.childNodes"/>
 
       <field name="mobileRoot"><![CDATA[
         PlacesUtils.annotations.getItemsWithAnnotation("mobile/bookmarksRoot", {})[0];
       ]]></field>
 
       <property name="isRootFolder" readonly="true">
@@ -856,24 +867,16 @@
               }
             }
             rootNode.containerOpen = false;
             return items;
           ]]>
         </body>
       </method>
 
-      <!-- Number of elements to add to the list initially. If there are more
-           than this many bookmarks to display, only add them to the list once
-           the user has scrolled towards them. This is a performance
-           optimization to avoid locking up while attempting to append hundreds
-           of bookmarks to our richlistbox.
-        -->
-      <field name="batchSize">25</field>
-
       <method name="openFolder">
         <parameter name="aRootFolder"/>
         <body>
           <![CDATA[
             aRootFolder = aRootFolder || this.mobileRoot;
 
             this._activeItem = null;
 
@@ -894,117 +897,32 @@
 
               let parent = document.createElementNS(XULNS, "placelabel");
               parent.setAttribute("class", "bookmark-folder");
               parent.setAttribute("itemid", folderId);
               parent.setAttribute("indent", 0);
               parent.setAttribute("title", title);
               parents.insertBefore(parent, parents.firstChild);
 
-              // XXX Fix me - use <handler>?
-              parent.addEventListener("click", function(e) { self.openFolder(e.target.previousSibling.itemId); }, false);
-              
               folderId = this._folderParents[folderId] || PlacesUtils.bookmarks.getFolderIdForItem(folderId);
             } while (folderId != PlacesUtils.bookmarks.placesRoot)
 
             let children = this._children;
             while (children.firstChild)
               children.removeChild(children.firstChild);
 
             children.scrollBoxObject.scrollTo(0, 0);
 
-            this._childItems = (aRootFolder == this._fakeDesktopFolderId) ? this._desktopChildren.concat()
-                                                                          : this._getChildren(aRootFolder);
+            let items = (aRootFolder == this._fakeDesktopFolderId) ? this._desktopChildren.concat()
+                                                                   : this._getChildren(aRootFolder);
 
             if (aRootFolder == this.mobileRoot && !this.isDesktopFolderEmpty())
-              this._childItems.unshift(this._desktopFolder);
-
-            this._insertItems();
-          ]]>
-        </body>
-      </method>
-
-      <method name="close">
-        <body>
-          <![CDATA[
-            // Clear out references to child items, and remove event listener
-            // if needed
-            this._childItems = null;
-            if (this._scrollListenerAdded) {
-              this._children.removeEventListener("scroll", this._scrollListener, false);
-              this._scrollListenerAdded = false;
-            }
-          ]]>
-        </body>        
-      </method>
-
-      <method name="_insertItems">
-        <body>
-          <![CDATA[
-            let items = this._childItems.splice(0, this.batchSize);
-
-            if (!items.length)
-              return; // no items to insert
-
-            let children = this._children;
-            let itemsRemaining = this._childItems.length > 0;
-            if (itemsRemaining && !this._scrollListenerAdded) {
-              // We're not going to insert all items, so add a scroll listener
-              // to know when to add them.
-              this._children.addEventListener("scroll", this._scrollListener, false);
-              this._scrollListenerAdded = true;
-            }
+              items.unshift(this._desktopFolder);
 
-            if (!itemsRemaining && this._scrollListenerAdded) {
-              // Can get rid of the scroll listener now that all items are added.
-              this._children.removeEventListener("scroll", this._scrollListener, false);
-              this._scrollListenerAdded = false;
-            }
-
-            let fragment = document.createDocumentFragment();            
-            let count = items.length;
-            for (let i=0; i<count; i++)
-              fragment.appendChild(this.createItem(items[i]));
-            children.appendChild(fragment);
-
-            // make sure we recalculate the scrollHeight of the children
-            this._childrenScrollHeight = -1;
-          ]]>
-        </body>
-      </method>
-
-      <field name="_scrollListener"><![CDATA[
-        ({
-          self: this,
-          handleEvent: function () {
-            this.self._checkForInsert();
-          }
-        })
-      ]]></field>
-
-      <method name="_checkForInsert">
-        <body>
-          <![CDATA[
-            if (this._childrenScrollHeight == -1) {
-              // Need to force a reflow to get the right value here since we may
-              // have just added children.
-              Browser.forceChromeReflow();
-              let scrollheight = {};
-              this.scrollBoxObject.getScrolledSize({}, scrollheight);
-              this._childrenScrollHeight = scrollheight.value;
-            }
-
-            let y = {};
-            this.scrollBoxObject.getPosition({}, y);
-            let scrollRatio = (y.value + this._childrenHeight) / this._childrenScrollHeight;
-
-            // If we're scrolled 80% to the bottom of the list, append the next
-            // set of items
-            if (scrollRatio > 0.8)
-              this._insertItems();
+            children.setItems(items.map(this.createItem));
           ]]>
         </body>
       </method>
 
       <method name="createItem">
         <parameter name="aItem"/>
         <body>
           <![CDATA[
@@ -1016,20 +934,16 @@
             child.setAttribute("title", aItem.title);
             child.setAttribute("uri", aItem.uri);
             child.setAttribute("tags", aItem.tags);
             if (aItem.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER)
               child.setAttribute("type", "folder");
             else
               child.setAttribute("src", aItem.icon);
 
-            // XXX make a <handler>
-            let self = this;
-            child.addEventListener("click", function(e) { self._fireOpen(e, child); }, false);
-
             return child;
           ]]>
         </body>
       </method>
 
       <method name="removeItem">
         <parameter name="aItem"/>
         <body>
@@ -1068,16 +982,79 @@
               func.call(this, event);
             }
           ]]>
         </body>
       </method>
     </implementation>
   </binding>
 
+  <binding id="richlistbox-batch" extends="chrome://global/content/bindings/richlistbox.xml#richlistbox">
+    <handlers>
+      <handler event="scroll">
+        <![CDATA[
+          // if there no more items to insert, just return early
+          if (this._items.length == 0)
+            return;
+
+          if (this._contentScrollHeight == -1) {
+            let scrollheight = {};
+            this.scrollBoxObject.getScrolledSize({}, scrollheight);
+            this._contentScrollHeight = scrollheight.value;
+          }
+
+          let y = {};
+          this.scrollBoxObject.getPosition({}, y);
+          let scrollRatio = (y.value + this._childrenHeight) / this._contentScrollHeight;
+
+          // If we're scrolled 80% to the bottom of the list, append the next
+          // set of items
+          if (scrollRatio > 0.8)
+            this._insertItems();
+        ]]>
+      </handler>
+    </handlers>
+    <implementation>
+      <!-- Number of elements to add to the list initially. If there are more
+           than this many elements to display, only add them to the list once
+           the user has scrolled towards them. This is a performance
+           optimization to avoid locking up while attempting to append hundreds
+           of nodes to our richlistbox.
+      -->
+      <property name="batchSize" readonly="true" onget="return this.getAttribute('batch')"/>
+
+      <field name="_childrenHeight">this.scrollBoxObject.height;</field>
+      <field name="_items">[]</field>
+
+      <method name="setItems">
+        <parameter name="aItems"/>
+        <body><![CDATA[
+          this._items = aItems;
+          this._insertItems();
+        ]]></body>
+      </method>
+
+      <method name="_insertItems">
+        <body><![CDATA[
+          let items = this._items.splice(0, this.batchSize);
+          if (!items.length)
+            return; // no items to insert
+
+          let count = items.length;
+          for (let i = 0; i<count; i++)
+            this.appendChild(items[i]);
+
+
+          // make sure we recalculate the scrollHeight of the content
+          this._contentScrollHeight = -1;
+        ]]></body>
+      </method>
+    </implementation>
+  </binding>
+
   <binding id="richlistitem" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
     <handlers>
       <handler event="mousedown" phase="capturing">
         <![CDATA[
           event.stopPropagation();
         ]]>
       </handler>
     </handlers>
--- a/mobile/chrome/content/bindings/setting.xml
+++ b/mobile/chrome/content/bindings/setting.xml
@@ -179,17 +179,17 @@
       <xul:box flex="1" class="prefbox">
         <xul:vbox flex="1">
           <xul:label class="preftitle" xbl:inherits="value=title" crop="end" flex="1"/>
           <xul:label class="prefdesc" xbl:inherits="value=desc" crop="end" flex="1">
             <children/>
           </xul:label>
         </xul:vbox>
         <xul:hbox anonid="input-container">
-          <xul:checkbox anonid="input" xbl:inherits="disabled" oncommand="inputChanged();"/>
+          <xul:checkbox anonid="input" xbl:inherits="disabled,onlabel,offlabel" oncommand="inputChanged();"/>
         </xul:hbox>
       </xul:box>
     </content>
 
     <implementation>
       <method name="valueFromPreference">
         <body>
         <![CDATA[
@@ -332,17 +332,16 @@
       </method>
 
       <method name="valueToPreference">
         <body>
         <![CDATA[
           let iss = Components.classes["@mozilla.org/supports-string;1"]
                               .createInstance(Components.interfaces.nsISupportsString);
           iss.data = this.value;
-          Components.reportError(this.value)
           this._prefs
               .setComplexValue(this.pref, Components.interfaces.nsISupportsString, iss);
         ]]>
         </body>
       </method>
 
       <property name="value" onget="return this.input.value;" onset="return this.input.value=val;"/>
     </implementation>
--- a/mobile/chrome/content/browser-ui.js
+++ b/mobile/chrome/content/browser-ui.js
@@ -1044,18 +1044,16 @@ var BookmarkList = {
 
     this._manageButton = document.getElementById("tool-bookmarks-manage");
     this._manageButton.disabled = (this._bookmarks.items.length == 0);
   },
 
   close: function() {
     BrowserUI.updateStar();
 
-    this._bookmarks.close();
-
     if (this._bookmarks.manageUI)
       this.toggleManage();
     this._bookmarks.blur();
     this._bookmarks.removeEventListener("BookmarkRemove", this, true);
 
     this._panel.hidden = true;
     BrowserUI.popDialog();
   },
--- a/mobile/chrome/content/browser.css
+++ b/mobile/chrome/content/browser.css
@@ -93,16 +93,20 @@ menulist {
   -moz-binding: url("chrome://browser/content/bindings.xml#menulist");
 }
 
 #select-list > option {
   -moz-binding: url("chrome://browser/content/bindings.xml#chrome-select-option");
 }
 
 /* richlist defaults ------------------------------------------------------- */
+richlistbox[batch] {
+  -moz-binding: url("chrome://browser/content/bindings.xml#richlistbox-batch");
+}
+
 richlistitem {
   -moz-binding: url("chrome://browser/content/bindings.xml#richlistitem");
 }
 
 richlistitem[typeName="local"] {
   -moz-binding: url("chrome://browser/content/bindings/extensions.xml#extension-local");
 }
 
--- a/mobile/chrome/content/browser.js
+++ b/mobile/chrome/content/browser.js
@@ -1350,18 +1350,20 @@ Browser.MainDragger.prototype = {
     let elem = this.draggedFrame;
     let doffset = new Point(dx, dy);
     let render = false;
 
     // First calculate any panning to take sidebars out of view
     let panOffset = this._panControlsAwayOffset(doffset);
 
     // do HTML overflow or XUL panning
-    if (this.contentScrollbox && !doffset.isZero())
+    if (this.contentScrollbox && !doffset.isZero()) {
       this._panScrollbox(this.contentScrollbox, doffset);
+      render = true;
+    }
 
     // Do all iframe panning
     if (elem) {
       while (elem.frameElement && !doffset.isZero()) {
         this._panFrame(elem, doffset);
         elem = elem.frameElement;
         render = true;
       }
new file mode 100644
--- /dev/null
+++ b/mobile/chrome/content/config.js
@@ -0,0 +1,410 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Mobile Browser.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+
+var ViewConfig = {
+  get _container() {
+    delete this._container;
+    return this._container = document.getElementById("prefs-container");
+  },
+
+  get _editor() {
+    delete this._editor;
+    return this._editor = document.getElementById("editor");
+  },
+
+  init: function init() {
+    window.addEventListener("resize", this, false);
+    window.addEventListener("prefchange", this, false);
+    window.addEventListener("prefnew", this, false);
+
+    this._handleWindowResize();
+    this.filter("");
+  },
+
+  uninit: function uninit() {
+    window.removeEventListener("resize", this, false);
+    window.removeEventListener("prefchange", this, false);
+    window.removeEventListener("prefnew", this, false);
+  },
+
+  filter: function filter(aValue) {
+    let row = document.getElementById("editor-row");
+    row.setAttribute("hidden", aValue != "");
+
+    let container = this._container;
+    container.scrollBoxObject.scrollTo(0, 0);
+    // Clear the list by replacing with a shallow copy
+    let empty = container.cloneNode(false);
+    empty.appendChild(row);
+    container.parentNode.replaceChild(empty, container);
+    this._container = empty;
+
+    let result = Utils.getPrefs(aValue);
+    this._container.setItems(result.map(this._createItem, this));
+  },
+
+  open: function open(aType) {
+    let buttons = document.getElementById("editor-buttons-add");
+    buttons.setAttribute("hidden", "true");
+    let nameField = document.getElementById("editor-name");
+    nameField.value = "";
+
+    let shouldFocus = false;
+    let setting = document.getElementById("editor-setting");
+    switch (aType) {
+      case Ci.nsIPrefBranch.PREF_INT:
+        setting.setAttribute("type", "integer");
+        break;
+      case Ci.nsIPrefBranch.PREF_BOOL:
+        setting.setAttribute("type", "bool");
+        break;
+      case Ci.nsIPrefBranch.PREF_STRING:
+        setting.setAttribute("type", "string");
+        break;
+    }
+
+    setting.removeAttribute("title");
+    setting.removeAttribute("pref");
+    if (setting.input)
+      setting.input.value = "";
+
+    document.getElementById("editor-container").appendChild(this._editor);
+    this._editor.setAttribute("hidden", "false");
+    this._currentItem = null;
+    nameField.focus();
+  },
+
+  close: function close(aValid) {
+    this._editor.setAttribute("hidden", "true");
+    let buttons = document.getElementById("editor-buttons-add");
+    buttons.setAttribute("hidden", "false");
+
+    if (aValid) {
+      let name = document.getElementById("editor-name").value;
+      if (name != "") {
+        let setting = document.getElementById("editor-setting");
+        setting.setAttribute("pref", name);
+        setting.valueToPreference();
+      }
+    }
+    document.getElementById("editor-container").appendChild(this._editor);
+  },
+
+  _currentItem: null,
+  edit: function(aItem) {
+    if (!aItem)
+      return;
+
+    let pref = Utils.getPref(aItem.getAttribute("name"));
+    if (pref.lock || !pref.name || aItem == this._currentItem)
+      return;
+
+    this.close(false);
+    this._currentItem = aItem;
+
+    let setting = document.getElementById("editor-setting");
+    let shouldFocus = false;
+    switch (pref.type) {
+      case Ci.nsIPrefBranch.PREF_BOOL:
+        setting.setAttribute("type", "bool");
+        break;
+
+      case Ci.nsIPrefBranch.PREF_INT:
+        setting.setAttribute("type", "integer");
+        setting.setAttribute("increment", this.getIncrementForValue(pref.value));
+        shouldFocus = true;
+        break;
+
+      case Ci.nsIPrefBranch.PREF_STRING:
+        setting.setAttribute("type", "string");
+        shouldFocus = true;
+        break;
+    }
+
+    setting.setAttribute("title", pref.name);
+    setting.setAttribute("pref", pref.name);
+
+    this._container.insertBefore(this._editor, aItem);
+
+    let resetButton = document.getElementById("editor-reset");
+    resetButton.setAttribute("disabled", pref.default);
+
+    this._editor.setAttribute("default", pref.default);
+    this._editor.setAttribute("hidden", "false");
+
+    if (shouldFocus && setting.input)
+      setting.input.focus();
+  },
+
+  reset: function reset(aItem) {
+    let setting = document.getElementById("editor-setting");
+    let pref = Utils.getPref(setting.getAttribute("pref"));
+    if (!pref.default)
+      Utils.resetPref(pref.name);
+  },
+
+  handleEvent: function handleEvent(aEvent) {
+    switch (aEvent.type) {
+      case "resize":
+        this._handleWindowResize();
+        break;
+
+      case "prefchange":
+      case "prefnew":
+        this._handlePrefChange(aEvent.detail, aEvent.type == "prefnew");
+        break;
+    }
+  },
+
+  _handleWindowResize: function _handleWindowResize() {
+    let mainBox = document.getElementById("main-container");
+    let textbox = document.getElementById("textbox");
+    let height = window.outerHeight - textbox.getBoundingClientRect().height;
+
+    mainBox.setAttribute("height", height);
+  },
+
+  _handlePrefChange: function _handlePrefChange(aIndex, aNew) {
+    let isEditing = !this._editor.hidden;
+    let shouldUpdateEditor = false;
+    if (isEditing) {
+      let setting = document.getElementById("editor-setting");
+      let editorIndex = Utils.getPrefIndex(setting.getAttribute("pref"));
+      shouldUpdateEditor = (aIndex == editorIndex);
+      if(shouldUpdateEditor || aIndex > editorIndex)
+        aIndex += 1;
+    }
+
+    // add 1 because of the new pref row
+    let item = this._container.childNodes[aIndex + 1];
+    if (!item) // the pref is not viewable
+      return;
+
+    if (aNew) {
+      let pref = Utils.getPrefByIndex(aIndex);
+      let row = this._createItem(pref);
+      this._container.insertBefore(row, item);
+      return;
+    }
+
+    let pref = Utils.getPref(item.getAttribute("name"));
+    if (shouldUpdateEditor) {
+      this._editor.setAttribute("default", pref.default);
+
+      let resetButton = document.getElementById("editor-reset");
+      resetButton.disabled = pref.default;
+    }
+
+    item.setAttribute("default", pref.default);
+    item.lastChild.setAttribute("value", pref.value);
+  },
+
+  _createItem: function _createItem(aPref) {
+    let row = document.createElement("richlistitem");
+
+    row.setAttribute("name", aPref.name);
+    row.setAttribute("type", aPref.type);
+    row.setAttribute("default", aPref.default);
+
+    let label = document.createElement("label");
+    label.setAttribute("class", "preftitle");
+    label.setAttribute("value", aPref.name);
+    label.setAttribute("crop", "end");
+    row.appendChild(label);
+
+    label = document.createElement("label");
+    label.setAttribute("class", "prefvalue");
+    label.setAttribute("value", aPref.value);
+    label.setAttribute("crop", "end");
+    row.appendChild(label);
+
+    return row;
+  },
+
+  getIncrementForValue: function getIncrementForValue(aValue) {
+    let count = 0;
+    while (aValue > 10) {
+      aValue /= 10;
+      count++;
+    }
+
+    return Math.max(1, count * 10);
+  }
+};
+
+var Utils = {
+  QueryInterface: function(aIID) {
+    if (!aIID.equals(Ci.nsIObserver) && !aIID.equals(Ci.nsISupportsWeakReference))
+      throw Components.results.NS_ERROR_NO_INTERFACE;
+    return this;
+  },
+
+  get _branch() {
+    delete this._branch;
+    let prefService = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService);
+    this._branch = prefService.getBranch(null).QueryInterface(Ci.nsIPrefBranch2);
+    this._branch.addObserver("", this, true);
+    return this._branch;
+  },
+
+  get _preferences() {
+    delete this._preferences;
+    let list = this._branch.getChildList("", {}).filter(function(element) {
+      return !(/^capability\./.test(element));
+    });
+    return this._preferences = list.sort().map(this.getPref, this);
+  },
+
+  getPrefs: function getPrefs(aValue) {
+    let result = this._preferences.slice();;
+    if (aValue != "") {
+      let reg = this._generateRegexp(aValue);
+      if (!reg)
+        return [];
+
+      result = this._preferences.filter(function(element, index, array) {
+        return reg.test(element.name + ";" + element.value);
+      });
+    }
+
+    return result;
+  },
+
+  getPref: function getPref(aPrefName) {
+    let branch = this._branch;
+    let pref = {
+                 name: aPrefName,
+                 value:  "",
+                 default: !branch.prefHasUserValue(aPrefName),
+                 lock: branch.prefIsLocked(aPrefName),
+                 type: branch.getPrefType(aPrefName)
+               };
+
+    try {
+      switch (pref.type) {
+        case Ci.nsIPrefBranch.PREF_BOOL:
+          pref.value = branch.getBoolPref(aPrefName).toString();
+          break;
+        case Ci.nsIPrefBranch.PREF_INT:
+          pref.value = branch.getIntPref(aPrefName).toString();
+          break;
+        default:
+        case Ci.nsIPrefBranch.PREF_STRING:
+          pref.value = branch.getComplexValue(aPrefName, Ci.nsISupportsString).data;
+          // Try in case it's a localized string (will throw an exception if not)
+          if (pref.default && /^chrome:\/\/.+\/locale\/.+\.properties/.test(pref.value))
+            pref.value = branch.getComplexValue(aPrefName, Ci.nsIPrefLocalizedString).data;
+          break;
+      }
+    } catch (e) {}
+
+    return pref;
+  },
+
+  getPrefByIndex: function getPrefByIndex(aIndex) {
+    return this._preferences[aIndex];
+  },
+
+  getPrefIndex: function getPrefIndex(aPrefName) {
+    let prefs = this._preferences;
+    let high = prefs.length - 1;
+    let low = 0, middle, element;
+
+    while (low <= high) {
+      middle = parseInt((low + high) / 2)
+      element = prefs[middle];
+
+      if (element.name > aPrefName)
+        high = middle - 1;
+      else if (element.name < aPrefName)
+        low = middle + 1;
+      else
+        return middle;
+    }
+
+    return -1;
+  },
+
+  resetPref: function resetPref(aPrefName) {
+    this._branch.clearUserPref(aPrefName);
+  },
+
+  observe: function observe(aSubject, aTopic, aPrefName) {
+    if (aTopic != "nsPref:changed" || /^capability\./.test(aPrefName)) // avoid displaying "private" preferences
+      return;
+
+    let type = "prefchange";
+    let index = this.getPrefIndex(aPrefName);
+    if (index != - 1) {
+      // update the inner array
+      let pref = this.getPref(aPrefName);
+      this._preferences[index].value = pref.value;
+    }
+    else {
+      // XXX we could do better here
+      let list = this._branch.getChildList("", {}).filter(function(element, index, array) {
+        return !(/^capability\./.test(element));
+      });
+      this._preferences = list.sort().map(this.getPref, this);
+
+      type = "prefnew";
+      index = this.getPrefIndex(aPrefName);
+    }
+
+    let evt = document.createEvent("UIEvents");
+    evt.initUIEvent(type, true, true, window, index);
+    window.dispatchEvent(evt);
+  },
+
+  _generateRegexp: function _generateRegexp(aValue) {
+    if (aValue.charAt(0) == '/') {
+      try {
+        let rv = aValue.match(/^\/(.*)\/(i?)$/);
+        return RegExp(rv[1], rv[2]);
+      }
+      catch (e) {
+        return null; // Do nothing on incomplete or bad RegExp
+      }
+    }
+
+    return RegExp(aValue.replace(/([^* \w])/g, "\\$1").replace(/^\*+/, "")
+                        .replace(/\*+/g, ".*"), "i");
+  }
+};
+
new file mode 100644
--- /dev/null
+++ b/mobile/chrome/content/config.xul
@@ -0,0 +1,97 @@
+<?xml version="1.0"?>
+
+<!-- ***** BEGIN LICENSE BLOCK *****
+   - Version: MPL 1.1/GPL 2.0/LGPL 2.1
+   -
+   - The contents of this file are subject to the Mozilla Public License Version
+   - 1.1 (the "License"); you may not use this file except in compliance with
+   - the License. You may obtain a copy of the License at
+   - http://www.mozilla.org/MPL/
+   -
+   - Software distributed under the License is distributed on an "AS IS" basis,
+   - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+   - for the specific language governing rights and limitations under the
+   - License.
+   -
+   - The Original Code is Mozilla Mobile Browser.
+   -
+   - The Initial Developer of the Original Code is
+   - Mozilla Corporation.
+   - Portions created by the Initial Developer are Copyright (C) 2008
+   - the Initial Developer. All Rights Reserved.
+   -
+   - Contributor(s):
+   -
+   - Alternatively, the contents of this file may be used under the terms of
+   - either the GNU General Public License Version 2 or later (the "GPL"), or
+   - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+   - in which case the provisions of the GPL or the LGPL are applicable instead
+   - of those above. If you wish to allow use of your version of this file only
+   - under the terms of either the GPL or the LGPL, and not to allow others to
+   - use your version of this file under the terms of the MPL, indicate your
+   - decision by deleting the provisions above and replace them with the notice
+   - and other provisions required by the LGPL or the GPL. If you do not delete
+   - the provisions above, a recipient may use your version of this file under
+   - the terms of any one of the MPL, the GPL or the LGPL.
+   -
+   - ***** END LICENSE BLOCK ***** -->
+
+<?xml-stylesheet href="chrome://browser/skin/platform.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/browser.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/config.css" type="text/css"?>
+
+<!DOCTYPE window [
+<!ENTITY % configDTD SYSTEM "chrome://browser/locale/config.dtd">
+%configDTD;
+]>
+
+<window id="about:config"
+        onload="ViewConfig.init();"
+        onunload="ViewConfig.uninit();"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+  <script type="application/x-javascript" src="chrome://browser/content/config.js"/>
+
+  <vbox class="panel-dark" flex="1">
+    <textbox id="textbox"
+             oncommand="ViewConfig.filter(this.value)"
+             type="search"
+             timeout="400"
+             emptytext="&empty.label;"/>
+
+    <hbox id="main-container" class="panel-dark">
+      <richlistbox id="prefs-container" flex="1" onselect="ViewConfig.edit(this.selectedItem)" batch="25">
+        <richlistitem id="editor-row">
+          <vbox id="editor-container" flex="1">
+
+            <hbox align="center" flex="1">
+              <label value="&newpref.label;" flex="1"/>
+              <spacer flex="1" />
+              <hbox id="editor-buttons-add">
+                <button label="&integer.label;" oncommand="ViewConfig.open(Components.interfaces.nsIPrefBranch.PREF_INT)"/>
+                <button label="&boolean.label;" oncommand="ViewConfig.open(Components.interfaces.nsIPrefBranch.PREF_BOOL)"/>
+                <button label="&string.label;" oncommand="ViewConfig.open(Components.interfaces.nsIPrefBranch.PREF_STRING)"/>
+              </hbox>
+            </hbox>
+
+            <vbox id="editor" hidden="true">
+              <hbox align="center">
+                <textbox id="editor-name" flex="1"/>
+                <setting id="editor-setting" onlabel="true" offlabel="false" flex="1"/>
+              </hbox>
+              <hbox id="editor-buttons">
+                <button id="editor-cancel" label="&cancel.label;" oncommand="ViewConfig.close(false)"/>
+                <spacer flex="1"/>
+                <button id="editor-reset" label="&reset.label;" oncommand="ViewConfig.reset(this.parentNode.parentNode.nextSibling)"/>
+                <button id="editor-done" label="&done.label;" oncommand="ViewConfig.close(true)"/>
+              </hbox>
+            </vbox>
+
+          </vbox>
+        </richlistitem>
+      </richlistbox>
+    </hbox>
+  </vbox>
+</window>
+
--- a/mobile/chrome/jar.mn
+++ b/mobile/chrome/jar.mn
@@ -1,13 +1,15 @@
 #filter substitution
 
 chrome.jar:
 % content browser %content/
 * content/about.xhtml                  (content/about.xhtml)
+  content/config.xul                   (content/config.xul)
+  content/config.js                    (content/config.js)
   content/aboutCertError.xhtml         (content/aboutCertError.xhtml)
   content/aboutCertError.css           (content/aboutCertError.css)
   content/languages.properties         (content/languages.properties)
 * content/browser.xul                  (content/browser.xul)
 * content/browser.js                   (content/browser.js)
 * content/browser-ui.js                (content/browser-ui.js)
   content/commandUtil.js               (content/commandUtil.js)
   content/bindings.xml                 (content/bindings.xml)
@@ -38,8 +40,10 @@ chrome.jar:
   content/downloads.js                 (content/downloads.js)
   content/console.js                   (content/console.js)
   content/prompt/alert.xul             (content/prompt/alert.xul)
   content/prompt/confirm.xul           (content/prompt/confirm.xul)
   content/prompt/prompt.xul            (content/prompt/prompt.xul)
   content/prompt/promptPassword.xul    (content/prompt/promptPassword.xul)
   content/prompt/select.xul            (content/prompt/select.xul)
   content/prompt/prompt.js             (content/prompt/prompt.js)
+
+% override chrome://global/content/config.xul chrome://browser/content/config.xul
new file mode 100644
--- /dev/null
+++ b/mobile/locales/en-US/chrome/config.dtd
@@ -0,0 +1,10 @@
+<!ENTITY empty.label            "Search">
+<!ENTITY newpref.label          "Add a new preference">
+
+<!ENTITY cancel.label           "Cancel">
+<!ENTITY reset.label            "Reset">
+<!ENTITY done.label             "Done">
+
+<!ENTITY integer.label          "Integer">
+<!ENTITY string.label           "String">
+<!ENTITY boolean.label          "Boolean">
--- a/mobile/locales/jar.mn
+++ b/mobile/locales/jar.mn
@@ -1,16 +1,17 @@
 #filter substitution
 
 @AB_CD@.jar:
 % locale browser @AB_CD@ %locale/@AB_CD@/browser/
   locale/@AB_CD@/browser/about.dtd                (%chrome/about.dtd)
   locale/@AB_CD@/browser/aboutCertError.dtd       (%chrome/aboutCertError.dtd)
   locale/@AB_CD@/browser/browser.dtd              (%chrome/browser.dtd)
   locale/@AB_CD@/browser/browser.properties       (%chrome/browser.properties)
+  locale/@AB_CD@/browser/config.dtd               (%chrome/config.dtd)
   locale/@AB_CD@/browser/firstrun.dtd             (%chrome/firstrun.dtd)
   locale/@AB_CD@/browser/region.properties        (%chrome/region.properties)
   locale/@AB_CD@/browser/preferences.dtd          (%chrome/preferences.dtd)
   locale/@AB_CD@/browser/checkbox.dtd             (%chrome/checkbox.dtd)
   locale/@AB_CD@/browser/notification.dtd         (%chrome/notification.dtd)
   locale/@AB_CD@/browser/prompt.dtd               (%chrome/prompt.dtd)
   locale/@AB_CD@/browser/bookmarks.json           (bookmarks.json)
   locale/@AB_CD@/browser/searchplugins/list.txt   (%searchplugins/list.txt)
new file mode 100644
--- /dev/null
+++ b/mobile/themes/hildon/config.css
@@ -0,0 +1,108 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Mobile Browser.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+richlistitem {
+  -moz-box-align: center;
+}
+
+richlistitem .preftitle {
+  min-width: 200px;
+  -moz-box-flex: 1;
+  margin-right: 8px;
+}
+
+/* XXX look  + sync */
+richlistitem[default="false"] .preftitle {
+  font-weight: bold;
+}
+
+richlistitem .prefvalue {
+  min-width: 200px;
+  -moz-box-flex: 4;
+  text-align: right;
+  color: grey;
+}
+
+/* Editor */
+#editor-row {
+  padding: 0;
+  background: #E9E9E9;
+}
+
+#editor {
+  border-bottom: 1px solid rgb(207,207,207);
+}
+
+#editor > hbox > #editor-name,
+#editor > hbox > #editor-cancel,
+#editor > hbox > #editor-done {
+  display: none;
+}
+
+#editor-container > #editor > hbox > #editor-name,
+#editor-container > #editor > hbox > #editor-cancel,
+#editor-container > #editor > hbox > #editor-done {
+  display: block;
+}
+
+#editor-container > #editor > hbox > #editor-reset {
+  display: none;
+}
+
+#editor + richlistitem {
+  display: none;
+}
+
+#editor[default="false"] .preftitle {
+  font-weight: bold;
+}
+
+#editor-setting .prefbox {
+  border-color: transparent !important;
+}
+
+#editor-setting[type="string"] [anonid="input-container"] {
+  -moz-box-flex: 4;
+}
+
+#editor-setting[type="string"] [anonid="input-container"] > textbox {
+  -moz-box-flex: 1;
+}
+
+#editor-buttons {
+  margin: 2px;
+}
+
--- a/mobile/themes/hildon/jar.mn
+++ b/mobile/themes/hildon/jar.mn
@@ -1,15 +1,16 @@
 #filter substitution
 
 chrome.jar:
 % skin browser classic/1.0 %
   aboutCertError.css                   (aboutCertError.css)
   aboutPage.css                        (aboutPage.css)
   about.css                            (about.css)
+  config.css                           (config.css)
   firstRun.css                         (firstRun.css)
   header.css                           (header.css)
   platform.css                         (platform.css)
   browser.css                          (browser.css)
   notification.css                     (notification.css)
 % override chrome://global/skin/about.css chrome://browser/skin/about.css
 
   images/addons.png                    (images/addons.png)
new file mode 100644
--- /dev/null
+++ b/mobile/themes/wince/config.css
@@ -0,0 +1,108 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Mobile Browser.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+richlistitem {
+  -moz-box-align: center;
+}
+
+richlistitem .preftitle {
+  min-width: 100px;
+  -moz-box-flex: 1;
+  margin-right: 1.1mm;
+}
+
+/* XXX look  + sync */
+richlistitem[default="false"] .preftitle {
+  font-weight: bold;
+}
+
+richlistitem .prefvalue {
+  min-width: 100px;
+  -moz-box-flex: 4;
+  text-align: right;
+  color: grey;
+}
+
+/* Editor */
+#editor-row {
+  padding: 0;
+  background: #E9E9E9;
+}
+
+#editor {
+  border-bottom: 0.05mm solid rgb(207,207,207);
+}
+
+#editor > hbox > #editor-name,
+#editor > hbox > #editor-cancel,
+#editor > hbox > #editor-done {
+  display: none;
+}
+
+#editor-container > #editor > hbox > #editor-name,
+#editor-container > #editor > hbox > #editor-cancel,
+#editor-container > #editor > hbox > #editor-done {
+  display: block;
+}
+
+#editor-container > #editor > hbox > #editor-reset {
+  display: none;
+}
+
+#editor + richlistitem {
+  display: none;
+}
+
+#editor[default="false"] .preftitle {
+  font-weight: bold;
+}
+
+#editor-setting .prefbox {
+  border-bottom: 0 solid transparent !important;
+}
+
+#editor-setting[type="string"] [anonid="input-container"] {
+  -moz-box-flex: 4;
+}
+
+#editor-setting[type="string"] [anonid="input-container"] > textbox {
+  -moz-box-flex: 1;
+}
+
+#editor-buttons {
+  margin: 0.5mm;
+}
+
--- a/mobile/themes/wince/jar.mn
+++ b/mobile/themes/wince/jar.mn
@@ -1,14 +1,15 @@
 #filter substitution
 
 chrome.jar:
 % skin browser classic/1.0 %
   aboutCertError.css                   (aboutCertError.css)
   aboutPage.css                        (aboutPage.css)
+  config.css                           (config.css)
   firstRun.css                         (firstRun.css)
   header.css                           (header.css)
   platform.css                         (platform.css)
   browser.css                          (browser.css)
   browser-high.css                     (browser-high.css)
   browser-low.css                      (browser-low.css)
   notification.css                     (notification.css)