Merge m-c to s-c
authorNick Alexander <nalexander@mozilla.com>
Wed, 19 Jun 2013 16:12:15 -0700
changeset 147626 9419343d5a2a899e055ed3347121775f94c6d372
parent 147625 a6a071d45b98ccaa70f30c6580e728455839b9ec (current diff)
parent 147097 2e9f36bf54dc7d8f0ee1b88f9f377bd0a52e2d73 (diff)
child 147627 75763abd1bdf646e95f5d65da91a9e564d43c2c4
push id2697
push userbbajaj@mozilla.com
push dateMon, 05 Aug 2013 18:49:53 +0000
treeherdermozilla-beta@dfec938c7b63 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone24.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to s-c
accessible/src/jsat/UtteranceGenerator.jsm
accessible/tests/mochitest/jsat/utterance.js
browser/metro/base/tests/mochitest/browser_plugin_input.html
browser/metro/base/tests/mochitest/browser_plugin_input_keyboard.js
browser/metro/base/tests/mochitest/browser_plugin_input_mouse.js
content/media/test/use_large_cache.js
dom/icc/interfaces/nsIDOMICCCardLockErrorEvent.idl
dom/interfaces/core/nsIDocumentRegister.idl
dom/interfaces/svg/nsIDOMSVGAnimatedString.idl
dom/interfaces/svg/nsIDOMSVGDocument.idl
js/src/jit-test/tests/auto-regress/bug827821.js
js/src/jit-test/tests/collections/Array-iterator-surfaces.js
js/src/jspropertycache.cpp
js/src/jspropertycache.h
js/src/jspropertycacheinlines.h
testing/mochitest/harness-overlay.xul
testing/mozbase/mozinfo/README.md
testing/mozbase/mozinstall/README.md
toolkit/components/search/tests/xpcshell/test_engineselect.js
toolkit/content/tests/widgets/use_large_cache.js
xpcom/glue/nsCycleCollectionJSRuntime.h
--- a/CLOBBER
+++ b/CLOBBER
@@ -12,9 +12,9 @@
 #          O               O
 #          |               |
 #          O <-- Clobber   O  <-- Clobber
 #
 # Note: The description below will be part of the error message shown to users.
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
-Bug 879831 needed to clobber for the removal of jsprobes.cpp
+Bug 877859 Valgrind header location updates for FxOS Valgrind
--- a/accessible/public/ia2/Makefile.in
+++ b/accessible/public/ia2/Makefile.in
@@ -15,18 +15,16 @@ IA2DIR        = $(topsrcdir)/other-licen
 include $(DEPTH)/config/autoconf.mk
 
 DEFINES       += -DREGISTER_PROXY_DLL
 
 GARBAGE       += $(MIDL_GENERATED_FILES)
 
 FORCE_SHARED_LIB = 1
 
-SRCS_IN_OBJDIR   = 1
-
 # Please keep this list in sync with the moz.build file until the rest of this
 # Makefile is ported over.
 MIDL_INTERFACES = \
   Accessible2.idl \
   Accessible2_2.idl \
   AccessibleAction.idl \
   AccessibleApplication.idl \
   AccessibleComponent.idl \
--- a/accessible/public/msaa/Makefile.in
+++ b/accessible/public/msaa/Makefile.in
@@ -13,18 +13,16 @@ DEFFILE = $(win_srcdir)/AccessibleMarsha
 include $(DEPTH)/config/autoconf.mk
 
 DEFINES += -DREGISTER_PROXY_DLL
 
 GARBAGE += $(MIDL_GENERATED_FILES) done_gen dlldata.c
 
 FORCE_SHARED_LIB = 1
 
-SRCS_IN_OBJDIR	= 1
-
 CSRCS	= \
 	dlldata.c \
 	ISimpleDOMNode_p.c \
 	ISimpleDOMNode_i.c \
 	ISimpleDOMDocument_p.c \
 	ISimpleDOMDocument_i.c \
 	ISimpleDOMText_p.c \
 	ISimpleDOMText_i.c \
--- a/accessible/src/generic/HyperTextAccessible.cpp
+++ b/accessible/src/generic/HyperTextAccessible.cpp
@@ -2107,28 +2107,36 @@ HyperTextAccessible::ScrollSubstringToPo
 
 ////////////////////////////////////////////////////////////////////////////////
 // Accessible public
 
 // Accessible protected
 ENameValueFlag
 HyperTextAccessible::NativeName(nsString& aName)
 {
+  // Check @alt attribute for invalid img elements.
+  bool hasImgAlt = false;
+  if (mContent->IsHTML(nsGkAtoms::img)) {
+    hasImgAlt = mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::alt, aName);
+    if (!aName.IsEmpty())
+      return eNameOK;
+  }
+
   ENameValueFlag nameFlag = AccessibleWrap::NativeName(aName);
   if (!aName.IsEmpty())
     return nameFlag;
 
   // Get name from title attribute for HTML abbr and acronym elements making it
   // a valid name from markup. Otherwise their name isn't picked up by recursive
   // name computation algorithm. See NS_OK_NAME_FROM_TOOLTIP.
   if (IsAbbreviation() &&
       mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::title, aName))
     aName.CompressWhitespace();
 
-  return eNameOK;
+  return hasImgAlt ? eNoNameOnPurpose : eNameOK;
 }
 
 void
 HyperTextAccessible::InvalidateChildren()
 {
   mOffsets.Clear();
 
   AccessibleWrap::InvalidateChildren();
--- a/accessible/src/jsat/AccessFu.jsm
+++ b/accessible/src/jsat/AccessFu.jsm
@@ -463,16 +463,20 @@ var Output = {
       this._bridge.handleGeckoMessage(JSON.stringify(androidEvent));
     }
   },
 
   Haptic: function Haptic(aDetails, aBrowser) {
     Utils.win.navigator.vibrate(aDetails.pattern);
   },
 
+  Braille: function Braille(aDetails, aBrowser) {
+    Logger.debug('Braille output: ' + aDetails.text);
+  },
+
   _adjustBounds: function(aJsonBounds, aBrowser) {
     let bounds = new Rect(aJsonBounds.left, aJsonBounds.top,
                           aJsonBounds.right - aJsonBounds.left,
                           aJsonBounds.bottom - aJsonBounds.top);
     let vp = Utils.getViewport(Utils.win) || { zoom: 1.0, offsetY: 0 };
     let root = Utils.win;
     let offset = { left: -root.mozInnerScreenX, top: -root.mozInnerScreenY };
     let scale = 1 / Utils.getPixelsPerCSSPixel(Utils.win);
--- a/accessible/src/jsat/Makefile.in
+++ b/accessible/src/jsat/Makefile.in
@@ -11,19 +11,19 @@ include $(DEPTH)/config/autoconf.mk
 
 INSTALL_TARGETS += ACCESSFU
 
 ACCESSFU_FILES := \
   AccessFu.jsm \
   EventManager.jsm \
   jar.mn \
   Makefile.in \
+  OutputGenerator.jsm \
   Presentation.jsm \
   TouchAdapter.jsm \
   TraversalRules.jsm \
   Utils.jsm \
-  UtteranceGenerator.jsm \
   $(NULL)
 
 ACCESSFU_DEST = $(FINAL_TARGET)/modules/accessibility
 
 include $(topsrcdir)/config/rules.mk
 
rename from accessible/src/jsat/UtteranceGenerator.jsm
rename to accessible/src/jsat/OutputGenerator.jsm
--- a/accessible/src/jsat/UtteranceGenerator.jsm
+++ b/accessible/src/jsat/OutputGenerator.jsm
@@ -9,197 +9,163 @@ const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 const INCLUDE_DESC = 0x01;
 const INCLUDE_NAME = 0x02;
 const INCLUDE_CUSTOM = 0x04;
 const NAME_FROM_SUBTREE_RULE = 0x08;
 
-const UTTERANCE_DESC_FIRST = 0;
+const OUTPUT_DESC_FIRST = 0;
+const OUTPUT_DESC_LAST = 1;
 
-Cu.import('resource://gre/modules/accessibility/Utils.jsm');
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Utils',
+  'resource://gre/modules/accessibility/Utils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'PrefCache',
+  'resource://gre/modules/accessibility/Utils.jsm');
 
 let gUtteranceOrder = new PrefCache('accessibility.accessfu.utterance');
 
 var gStringBundle = Cc['@mozilla.org/intl/stringbundle;1'].
   getService(Ci.nsIStringBundleService).
   createBundle('chrome://global/locale/AccessFu.properties');
 
-this.EXPORTED_SYMBOLS = ['UtteranceGenerator'];
-
+this.EXPORTED_SYMBOLS = ['UtteranceGenerator', 'BrailleGenerator'];
 
-/**
- * Generates speech utterances from objects, actions and state changes.
- * An utterance is an array of strings.
- *
- * It should not be assumed that flattening an utterance array would create a
- * gramatically correct sentence. For example, {@link genForObject} might
- * return: ['graphic', 'Welcome to my home page'].
- * Each string element in an utterance should be gramatically correct in itself.
- * Another example from {@link genForObject}: ['list item 2 of 5', 'Alabama'].
- *
- * An utterance is ordered from the least to the most important. Speaking the
- * last string usually makes sense, but speaking the first often won't.
- * For example {@link genForAction} might return ['button', 'clicked'] for a
- * clicked event. Speaking only 'clicked' makes sense. Speaking 'button' does
- * not.
- */
-this.UtteranceGenerator = {
-  gActionMap: {
-    jump: 'jumpAction',
-    press: 'pressAction',
-    check: 'checkAction',
-    uncheck: 'uncheckAction',
-    select: 'selectAction',
-    open: 'openAction',
-    close: 'closeAction',
-    switch: 'switchAction',
-    click: 'clickAction',
-    collapse: 'collapseAction',
-    expand: 'expandAction',
-    activate: 'activateAction',
-    cycle: 'cycleAction'
-  },
+this.OutputGenerator = {
 
   /**
-   * Generates an utterance for a PivotContext.
+   * Generates output for a PivotContext.
    * @param {PivotContext} aContext object that generates and caches
    *    context information for a given accessible and its relationship with
    *    another accessible.
    * @return {Array} An array of strings. Depending on the utterance order,
    *    the strings describe the context for an accessible object either
    *    starting from the accessible's ancestry or accessible's subtree.
    */
   genForContext: function genForContext(aContext) {
-    let utterance = [];
-    let addUtterance = function addUtterance(aAccessible) {
-      utterance.push.apply(utterance,
-        UtteranceGenerator.genForObject(aAccessible));
+    let output = [];
+    let self = this;
+    let addOutput = function addOutput(aAccessible) {
+      output.push.apply(output, self.genForObject(aAccessible));
     };
     let ignoreSubtree = function ignoreSubtree(aAccessible) {
       let roleString = Utils.AccRetrieval.getStringRole(aAccessible.role);
-      let nameRule = UtteranceGenerator.roleRuleMap[roleString] || 0;
+      let nameRule = self.roleRuleMap[roleString] || 0;
       // Ignore subtree if the name is explicit and the role's name rule is the
       // NAME_FROM_SUBTREE_RULE.
       return (nameRule & NAME_FROM_SUBTREE_RULE) &&
         (Utils.getAttributes(aAccessible)['explicit-name'] === 'true');
     };
-    let utteranceOrder = gUtteranceOrder.value || UTTERANCE_DESC_FIRST;
+    let outputOrder = typeof gUtteranceOrder.value == 'number' ?
+                      gUtteranceOrder.value : this.defaultOutputOrder;
+    let contextStart = this._getContextStart(aContext);
 
-    if (utteranceOrder === UTTERANCE_DESC_FIRST) {
-      aContext.newAncestry.forEach(addUtterance);
-      addUtterance(aContext.accessible);
-      [addUtterance(node) for
+    if (outputOrder === OUTPUT_DESC_FIRST) {
+      contextStart.forEach(addOutput);
+      addOutput(aContext.accessible);
+      [addOutput(node) for
         (node of aContext.subtreeGenerator(true, ignoreSubtree))];
     } else {
-      [addUtterance(node) for
+      [addOutput(node) for
         (node of aContext.subtreeGenerator(false, ignoreSubtree))];
-      addUtterance(aContext.accessible);
-      aContext.newAncestry.reverse().forEach(addUtterance);
+      addOutput(aContext.accessible);
+      contextStart.reverse().forEach(addOutput);
     }
 
     // Clean up the white space.
     let trimmed;
-    utterance = [trimmed for (word of utterance) if (trimmed = word.trim())];
-
-    return utterance;
+    output = [trimmed for (word of output) if (trimmed = word.trim())];
+    return output;
   },
 
 
   /**
-   * Generates an utterance for an object.
+   * Generates output for an object.
    * @param {nsIAccessible} aAccessible accessible object to generate utterance
    *    for.
    * @return {Array} Two string array. The first string describes the object
    *    and its states. The second string is the object's name. Whether the
    *    object's description or it's role is included is determined by
    *    {@link roleRuleMap}.
    */
   genForObject: function genForObject(aAccessible) {
     let roleString = Utils.AccRetrieval.getStringRole(aAccessible.role);
-
-    let func = this.objectUtteranceFunctions[roleString] ||
-      this.objectUtteranceFunctions.defaultFunc;
+    let func = this.objectOutputFunctions[roleString.replace(' ', '')] ||
+      this.objectOutputFunctions.defaultFunc;
 
     let flags = this.roleRuleMap[roleString] || 0;
 
     if (aAccessible.childCount == 0)
       flags |= INCLUDE_NAME;
 
     let state = {};
     let extState = {};
     aAccessible.getState(state, extState);
     let states = {base: state.value, ext: extState.value};
 
     return func.apply(this, [aAccessible, roleString, states, flags]);
   },
 
   /**
-   * Generates an utterance for an action performed.
-   * TODO: May become more verbose in the future.
+   * Generates output for an action performed.
    * @param {nsIAccessible} aAccessible accessible object that the action was
    *    invoked in.
    * @param {string} aActionName the name of the action, one of the keys in
    *    {@link gActionMap}.
    * @return {Array} A one string array with the action.
    */
-  genForAction: function genForAction(aObject, aActionName) {
-    return [gStringBundle.GetStringFromName(this.gActionMap[aActionName])];
-  },
+  genForAction: function genForAction(aObject, aActionName) {},
 
   /**
-   * Generates an utterance for an announcement. Basically attempts to localize
+   * Generates output for an announcement. Basically attempts to localize
    * the announcement string.
    * @param {string} aAnnouncement unlocalized announcement.
    * @return {Array} A one string array with the announcement.
    */
-  genForAnnouncement: function genForAnnouncement(aAnnouncement) {
-    try {
-      return [gStringBundle.GetStringFromName(aAnnouncement)];
-    } catch (x) {
-      return [aAnnouncement];
-    }
-  },
+  genForAnnouncement: function genForAnnouncement(aAnnouncement) {},
 
   /**
-   * Generates an utterance for a tab state change.
+   * Generates output for a tab state change.
    * @param {nsIAccessible} aAccessible accessible object of the tab's attached
    *    document.
    * @param {string} aTabState the tab state name, see
    *    {@link Presenter.tabStateChanged}.
    * @return {Array} The tab state utterace.
    */
-  genForTabStateChange: function genForTabStateChange(aObject, aTabState) {
-    switch (aTabState) {
-      case 'newtab':
-        return [gStringBundle.GetStringFromName('tabNew')];
-      case 'loading':
-        return [gStringBundle.GetStringFromName('tabLoading')];
-      case 'loaded':
-        return [aObject.name || '',
-                gStringBundle.GetStringFromName('tabLoaded')];
-      case 'loadstopped':
-        return [gStringBundle.GetStringFromName('tabLoadStopped')];
-      case 'reload':
-        return [gStringBundle.GetStringFromName('tabReload')];
-      default:
-        return [];
+  genForTabStateChange: function genForTabStateChange(aObject, aTabState) {},
+
+  /**
+   * Generates output for announcing entering and leaving editing mode.
+   * @param {aIsEditing} boolean true if we are in editing mode
+   * @return {Array} The mode utterance
+   */
+  genForEditingMode: function genForEditingMode(aIsEditing) {},
+
+  _getContextStart: function getContextStart(aContext) {},
+
+  _addName: function _addName(aOutput, aAccessible, aFlags) {
+    let name;
+    if (Utils.getAttributes(aAccessible)['explicit-name'] === 'true' ||
+      (aFlags & INCLUDE_NAME)) {
+      name = aAccessible.name;
+    }
+
+    if (name) {
+      let outputOrder = typeof gUtteranceOrder.value == 'number' ?
+                        gUtteranceOrder.value : this.defaultOutputOrder;
+      aOutput[outputOrder === OUTPUT_DESC_FIRST ?
+        'push' : 'unshift'](name);
     }
   },
 
-  /**
-   * Generates an utterance for announcing entering and leaving editing mode.
-   * @param {aIsEditing} boolean true if we are in editing mode
-   * @return {Array} The mode utterance
-   */
-  genForEditingMode: function genForEditingMode(aIsEditing) {
-    return [gStringBundle.GetStringFromName(
-              aIsEditing ? 'editingMode' : 'navigationMode')];
-  },
+  _getLocalizedRole: function _getLocalizedRole(aRoleStr) {},
+
+  _getLocalizedStates: function _getLocalizedStates(aStates) {},
 
   roleRuleMap: {
     'menubar': INCLUDE_DESC,
     'scrollbar': INCLUDE_DESC,
     'grip': INCLUDE_DESC,
     'alert': INCLUDE_DESC | INCLUDE_NAME,
     'menupopup': INCLUDE_DESC,
     'menuitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
@@ -263,45 +229,129 @@ this.UtteranceGenerator = {
     'term': NAME_FROM_SUBTREE_RULE,
     'definition': NAME_FROM_SUBTREE_RULE,
     'key': NAME_FROM_SUBTREE_RULE,
     'image map': INCLUDE_DESC,
     'option': INCLUDE_DESC,
     'listbox': INCLUDE_DESC,
     'definitionlist': INCLUDE_DESC | INCLUDE_NAME},
 
-  objectUtteranceFunctions: {
-    defaultFunc: function defaultFunc(aAccessible, aRoleStr, aStates, aFlags) {
-      let utterance = [];
+  objectOutputFunctions: {
+    _generateBaseOutput: function _generateBaseOutput(aAccessible, aRoleStr, aStates, aFlags) {
+      let output = [];
 
       if (aFlags & INCLUDE_DESC) {
         let desc = this._getLocalizedStates(aStates);
         let roleStr = this._getLocalizedRole(aRoleStr);
         if (roleStr)
           desc.push(roleStr);
-        utterance.push(desc.join(' '));
+        output.push(desc.join(' '));
       }
 
-      this._addName(utterance, aAccessible, aFlags);
+      this._addName(output, aAccessible, aFlags);
 
-      return utterance;
+      return output;
     },
 
     entry: function entry(aAccessible, aRoleStr, aStates, aFlags) {
-      let utterance = [];
+      let output = [];
       let desc = this._getLocalizedStates(aStates);
       desc.push(this._getLocalizedRole(
                   (aStates.ext & Ci.nsIAccessibleStates.EXT_STATE_MULTI_LINE) ?
                     'textarea' : 'entry'));
 
-      utterance.push(desc.join(' '));
+      output.push(desc.join(' '));
+
+      this._addName(output, aAccessible, aFlags);
+
+      return output;
+    }
+  }
+};
+
+/**
+ * Generates speech utterances from objects, actions and state changes.
+ * An utterance is an array of strings.
+ *
+ * It should not be assumed that flattening an utterance array would create a
+ * gramatically correct sentence. For example, {@link genForObject} might
+ * return: ['graphic', 'Welcome to my home page'].
+ * Each string element in an utterance should be gramatically correct in itself.
+ * Another example from {@link genForObject}: ['list item 2 of 5', 'Alabama'].
+ *
+ * An utterance is ordered from the least to the most important. Speaking the
+ * last string usually makes sense, but speaking the first often won't.
+ * For example {@link genForAction} might return ['button', 'clicked'] for a
+ * clicked event. Speaking only 'clicked' makes sense. Speaking 'button' does
+ * not.
+ */
+this.UtteranceGenerator = {
+  __proto__: OutputGenerator,
+
+  defaultOutputOrder: OUTPUT_DESC_FIRST,
 
-      this._addName(utterance, aAccessible, aFlags);
+  gActionMap: {
+    jump: 'jumpAction',
+    press: 'pressAction',
+    check: 'checkAction',
+    uncheck: 'uncheckAction',
+    select: 'selectAction',
+    open: 'openAction',
+    close: 'closeAction',
+    switch: 'switchAction',
+    click: 'clickAction',
+    collapse: 'collapseAction',
+    expand: 'expandAction',
+    activate: 'activateAction',
+    cycle: 'cycleAction'
+  },
+
+  //TODO: May become more verbose in the future.
+  genForAction: function genForAction(aObject, aActionName) {
+    return [gStringBundle.GetStringFromName(this.gActionMap[aActionName])];
+  },
+
+  genForAnnouncement: function genForAnnouncement(aAnnouncement) {
+    try {
+      return [gStringBundle.GetStringFromName(aAnnouncement)];
+    } catch (x) {
+      return [aAnnouncement];
+    }
+  },
 
-      return utterance;
+  genForTabStateChange: function genForTabStateChange(aObject, aTabState) {
+    switch (aTabState) {
+      case 'newtab':
+        return [gStringBundle.GetStringFromName('tabNew')];
+      case 'loading':
+        return [gStringBundle.GetStringFromName('tabLoading')];
+      case 'loaded':
+        return [aObject.name || '',
+                gStringBundle.GetStringFromName('tabLoaded')];
+      case 'loadstopped':
+        return [gStringBundle.GetStringFromName('tabLoadStopped')];
+      case 'reload':
+        return [gStringBundle.GetStringFromName('tabReload')];
+      default:
+        return [];
+    }
+  },
+
+  genForEditingMode: function genForEditingMode(aIsEditing) {
+    return [gStringBundle.GetStringFromName(
+              aIsEditing ? 'editingMode' : 'navigationMode')];
+  },
+
+  objectOutputFunctions: {
+    defaultFunc: function defaultFunc(aAccessible, aRoleStr, aStates, aFlags) {
+      return OutputGenerator.objectOutputFunctions._generateBaseOutput.apply(this, arguments);
+    },
+
+    entry: function entry(aAccessible, aRoleStr, aStates, aFlags) {
+      return OutputGenerator.objectOutputFunctions.entry.apply(this, arguments);
     },
 
     heading: function heading(aAccessible, aRoleStr, aStates, aFlags) {
       let level = {};
       aAccessible.groupPosition(level, {}, {});
       let utterance =
         [gStringBundle.formatStringFromName('headingLevel', [level.value], 1)];
 
@@ -333,35 +383,25 @@ this.UtteranceGenerator = {
     definitionlist: function definitionlist(aAccessible, aRoleStr, aStates, aFlags) {
       return this._getListUtterance
         (aAccessible, aRoleStr, aFlags, aAccessible.childCount / 2);
     },
 
     application: function application(aAccessible, aRoleStr, aStates, aFlags) {
       // Don't utter location of applications, it gets tiring.
       if (aAccessible.name != aAccessible.DOMNode.location)
-        return this.objectUtteranceFunctions.defaultFunc.apply(this,
+        return this.objectOutputFunctions.defaultFunc.apply(this,
           [aAccessible, aRoleStr, aStates, aFlags]);
 
       return [];
     }
   },
 
-  _addName: function _addName(utterance, aAccessible, aFlags) {
-    let name;
-    if (Utils.getAttributes(aAccessible)['explicit-name'] === 'true' ||
-      (aFlags & INCLUDE_NAME)) {
-      name = aAccessible.name;
-    }
-
-    if (name) {
-      let utteranceOrder = gUtteranceOrder.value || UTTERANCE_DESC_FIRST;
-      utterance[utteranceOrder === UTTERANCE_DESC_FIRST ?
-        'push' : 'unshift'](name);
-    }
+  _getContextStart: function _getContextStart(aContext) {
+    return aContext.newAncestry;
   },
 
   _getLocalizedRole: function _getLocalizedRole(aRoleStr) {
     try {
       return gStringBundle.GetStringFromName(aRoleStr.replace(' ', ''));
     } catch (x) {
       return '';
     }
@@ -414,8 +454,123 @@ this.UtteranceGenerator = {
       (gStringBundle.formatStringFromName('listItemCount', [aItemCount], 1));
     let utterance = [desc.join(' ')];
 
     this._addName(utterance, aAccessible, aFlags);
 
     return utterance;
   }
 };
+
+
+this.BrailleGenerator = {
+  __proto__: OutputGenerator,
+
+  defaultOutputOrder: OUTPUT_DESC_LAST,
+
+  objectOutputFunctions: {
+    defaultFunc: function defaultFunc(aAccessible, aRoleStr, aStates, aFlags) {
+      let braille = OutputGenerator.objectOutputFunctions._generateBaseOutput.apply(this, arguments);
+
+      if (aAccessible.indexInParent === 1 &&
+          aAccessible.parent.role == Ci.nsIAccessibleRole.ROLE_LISTITEM &&
+          aAccessible.previousSibling.role == Ci.nsIAccessibleRole.ROLE_STATICTEXT) {
+        if (aAccessible.parent.parent && aAccessible.parent.parent.DOMNode &&
+            aAccessible.parent.parent.DOMNode.nodeName == 'UL') {
+          braille.unshift('*');
+        } else {
+          braille.unshift(aAccessible.previousSibling.name);
+        }
+      }
+
+      return braille;
+    },
+
+    listitem: function listitem(aAccessible, aRoleStr, aStates, aFlags) {
+      let braille = [];
+
+      this._addName(braille, aAccessible, aFlags);
+
+      return braille;
+    },
+
+    statictext: function statictext(aAccessible, aRoleStr, aStates, aFlags) {
+      // Since we customize the list bullet's output, we add the static
+      // text from the first node in each listitem, so skip it here.
+      if (aAccessible.parent.role == Ci.nsIAccessibleRole.ROLE_LISTITEM) {
+        return [];
+      }
+
+      return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
+    },
+
+    _useStateNotRole: function _useStateNotRole(aAccessible, aRoleStr, aStates, aFlags) {
+      let braille = [];
+
+      let desc = this._getLocalizedStates(aStates);
+      braille.push(desc.join(' '));
+
+      this._addName(braille, aAccessible, aFlags);
+
+      return braille;
+    },
+
+    checkbutton: function checkbutton(aAccessible, aRoleStr, aStates, aFlags) {
+      return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
+    },
+
+    radiobutton: function radiobutton(aAccessible, aRoleStr, aStates, aFlags) {
+      return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
+    },
+
+    togglebutton: function radiobutton(aAccessible, aRoleStr, aStates, aFlags) {
+      return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
+    },
+
+    entry: function entry(aAccessible, aRoleStr, aStates, aFlags) {
+      return OutputGenerator.objectOutputFunctions.entry.apply(this, arguments);
+    }
+  },
+
+  _getContextStart: function _getContextStart(aContext) {
+    if (aContext.accessible.parent.role == Ci.nsIAccessibleRole.ROLE_LINK) {
+      return [aContext.accessible.parent];
+    }
+
+    return [];
+  },
+
+  _getLocalizedRole: function _getLocalizedRole(aRoleStr) {
+    try {
+      return gStringBundle.GetStringFromName(aRoleStr.replace(' ', '') + 'Abbr');
+    } catch (x) {
+      try {
+        return gStringBundle.GetStringFromName(aRoleStr.replace(' ', ''));
+      } catch (y) {
+        return '';
+      }
+    }
+  },
+
+  _getLocalizedStates: function _getLocalizedStates(aStates) {
+    let stateBraille = [];
+
+    let getCheckedState = function getCheckedState() {
+      let resultMarker = [];
+      let state = aStates.base;
+      let fill = !!(state & Ci.nsIAccessibleStates.STATE_CHECKED) ||
+                 !!(state & Ci.nsIAccessibleStates.STATE_PRESSED);
+
+      resultMarker.push('(');
+      resultMarker.push(fill ? 'x' : ' ');
+      resultMarker.push(')');
+
+      return resultMarker.join('');
+    };
+
+    if (aStates.base & Ci.nsIAccessibleStates.STATE_CHECKABLE) {
+      stateBraille.push(getCheckedState());
+    }
+
+    return stateBraille;
+  }
+
+};
--- a/accessible/src/jsat/Presentation.jsm
+++ b/accessible/src/jsat/Presentation.jsm
@@ -4,18 +4,27 @@
 
 'use strict';
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
-Cu.import('resource://gre/modules/accessibility/Utils.jsm');
-Cu.import('resource://gre/modules/accessibility/UtteranceGenerator.jsm');
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Utils',
+  'resource://gre/modules/accessibility/Utils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'Logger',
+  'resource://gre/modules/accessibility/Utils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'PivotContext',
+  'resource://gre/modules/accessibility/Utils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'UtteranceGenerator',
+  'resource://gre/modules/accessibility/OutputGenerator.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'BrailleGenerator',
+  'resource://gre/modules/accessibility/OutputGenerator.jsm');
 
 this.EXPORTED_SYMBOLS = ['Presentation'];
 
 /**
  * The interface for all presenter classes. A presenter could be, for example,
  * a speech output module, or a visual cursor indicator.
  */
 function Presenter() {}
@@ -214,25 +223,35 @@ AndroidPresenter.prototype = {
     if (isExploreByTouch) {
       // This isn't really used by TalkBack so this is a half-hearted attempt
       // for now.
       androidEvents.push({eventType: this.ANDROID_VIEW_HOVER_EXIT, text: []});
     }
 
     let state = Utils.getStates(aContext.accessible)[0];
 
+    let brailleText = '';
+    if (Utils.AndroidSdkVersion >= 16) {
+      if (!this._braillePresenter) {
+        this._braillePresenter = new BraillePresenter();
+      }
+      brailleText = this._braillePresenter.pivotChanged(aContext, aReason).
+                         details.text;
+    }
+
     androidEvents.push({eventType: (isExploreByTouch) ?
                           this.ANDROID_VIEW_HOVER_ENTER : focusEventType,
                         text: UtteranceGenerator.genForContext(aContext),
                         bounds: aContext.bounds,
                         clickable: aContext.accessible.actionCount > 0,
                         checkable: !!(state &
                                       Ci.nsIAccessibleStates.STATE_CHECKABLE),
                         checked: !!(state &
-                                    Ci.nsIAccessibleStates.STATE_CHECKED)});
+                                    Ci.nsIAccessibleStates.STATE_CHECKED),
+                        brailleText: brailleText});
 
 
     return {
       type: this.type,
       details: androidEvents
     };
   },
 
@@ -362,16 +381,39 @@ HapticPresenter.prototype = {
 
   PIVOT_CHANGE_PATTHERN: [20],
 
   pivotChanged: function HapticPresenter_pivotChanged(aContext, aReason) {
     return { type: this.type, details: { pattern: this.PIVOT_CHANGE_PATTHERN } };
   }
 };
 
+/**
+ * A braille presenter
+ */
+
+this.BraillePresenter = function BraillePresenter() {};
+
+BraillePresenter.prototype = {
+  __proto__: Presenter.prototype,
+
+  type: 'Braille',
+
+  pivotChanged: function BraillePresenter_pivotChanged(aContext, aReason) {
+    if (!aContext.accessible) {
+      return null;
+    }
+
+    let text = BrailleGenerator.genForContext(aContext);
+
+    return { type: this.type, details: {text: text.join(' ')} };
+  }
+
+};
+
 this.Presentation = {
   get presenters() {
     delete this.presenters;
     this.presenters = [new VisualPresenter()];
 
     if (Utils.MozBuildApp == 'mobile/android') {
       this.presenters.push(new AndroidPresenter());
     } else {
--- a/accessible/src/jsat/TraversalRules.jsm
+++ b/accessible/src/jsat/TraversalRules.jsm
@@ -9,16 +9,18 @@ const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 this.EXPORTED_SYMBOLS = ['TraversalRules'];
 
 Cu.import('resource://gre/modules/accessibility/Utils.jsm');
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 
+let gSkipEmptyImages = new PrefCache('accessibility.accessfu.skip_empty_images');
+
 function BaseTraversalRule(aRoles, aMatchFunc) {
   this._matchRoles = aRoles;
   this._matchFunc = aMatchFunc;
 }
 
 BaseTraversalRule.prototype = {
     getMatchRoles: function BaseTraversalRule_getmatchRoles(aRules) {
       aRules.value = this._matchRoles;
@@ -98,16 +100,18 @@ this.TraversalRules = {
           let parent = aAccessible.parent;
           // Ignore prefix static text in list items. They are typically bullets or numbers.
           if (parent.childCount > 1 && aAccessible.indexInParent == 0 &&
               parent.role == Ci.nsIAccessibleRole.ROLE_LISTITEM)
             return Ci.nsIAccessibleTraversalRule.FILTER_IGNORE;
 
           return Ci.nsIAccessibleTraversalRule.FILTER_MATCH;
         }
+      case Ci.nsIAccessibleRole.ROLE_GRAPHIC:
+        return TraversalRules._shouldSkipImage(aAccessible);
       default:
         // Ignore the subtree, if there is one. So that we don't land on
         // the same content that was already presented by its parent.
         return Ci.nsIAccessibleTraversalRule.FILTER_MATCH |
           Ci.nsIAccessibleTraversalRule.FILTER_IGNORE_SUBTREE;
       }
     }
   ),
@@ -163,17 +167,20 @@ this.TraversalRules = {
      Ci.nsIAccessibleRole.ROLE_PAGETAB,
      Ci.nsIAccessibleRole.ROLE_RADIOBUTTON,
      Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM,
      Ci.nsIAccessibleRole.ROLE_SLIDER,
      Ci.nsIAccessibleRole.ROLE_CHECKBUTTON,
      Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM]),
 
   Graphic: new BaseTraversalRule(
-    [Ci.nsIAccessibleRole.ROLE_GRAPHIC]),
+    [Ci.nsIAccessibleRole.ROLE_GRAPHIC],
+    function Graphic_match(aAccessible) {
+      return TraversalRules._shouldSkipImage(aAccessible);
+    }),
 
   Heading: new BaseTraversalRule(
     [Ci.nsIAccessibleRole.ROLE_HEADING]),
 
   ListItem: new BaseTraversalRule(
     [Ci.nsIAccessibleRole.ROLE_LISTITEM,
      Ci.nsIAccessibleRole.ROLE_TERM]),
 
@@ -206,10 +213,17 @@ this.TraversalRules = {
   Separator: new BaseTraversalRule(
     [Ci.nsIAccessibleRole.ROLE_SEPARATOR]),
 
   Table: new BaseTraversalRule(
     [Ci.nsIAccessibleRole.ROLE_TABLE]),
 
   Checkbox: new BaseTraversalRule(
     [Ci.nsIAccessibleRole.ROLE_CHECKBUTTON,
-     Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM])
+     Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM]),
+
+  _shouldSkipImage: function _shouldSkipImage(aAccessible) {
+    if (gSkipEmptyImages.value && aAccessible.name === '') {
+      return Ci.nsIAccessibleTraversalRule.FILTER_IGNORE;
+    }
+    return Ci.nsIAccessibleTraversalRule.FILTER_MATCH;
+  }
 };
--- a/accessible/src/jsat/content-script.js
+++ b/accessible/src/jsat/content-script.js
@@ -11,16 +11,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, 'Presentation',
   'resource://gre/modules/accessibility/Presentation.jsm');
 XPCOMUtils.defineLazyModuleGetter(this, 'TraversalRules',
   'resource://gre/modules/accessibility/TraversalRules.jsm');
 XPCOMUtils.defineLazyModuleGetter(this, 'Utils',
   'resource://gre/modules/accessibility/Utils.jsm');
 XPCOMUtils.defineLazyModuleGetter(this, 'EventManager',
   'resource://gre/modules/accessibility/EventManager.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'ObjectWrapper',
+  'resource://gre/modules/ObjectWrapper.jsm');
 
 Logger.debug('content-script.js');
 
 let eventManager = null;
 
 function virtualCursorControl(aMessage) {
   if (Logger.logLevel >= Logger.DEBUG)
     Logger.debug(aMessage.name, JSON.stringify(aMessage.json));
@@ -177,16 +179,31 @@ function scroll(aMessage) {
     let horiz = aMessage.json.horizontal;
     let page = aMessage.json.page;
 
     // Search up heirarchy for scrollable element.
     let acc = vc.position;
     while (acc) {
       let elem = acc.DOMNode;
 
+      // This is inspired by IndieUI events. Once they are
+      // implemented, it should be easy to transition to them.
+      // https://dvcs.w3.org/hg/IndieUI/raw-file/tip/src/indie-ui-events.html#scrollrequest
+      let uiactions = elem.getAttribute ? elem.getAttribute('uiactions') : '';
+      if (uiactions && uiactions.split(' ').indexOf('scroll') >= 0) {
+        let evt = elem.ownerDocument.createEvent('CustomEvent');
+        let details = horiz ? { deltaX: page * elem.clientWidth } :
+          { deltaY: page * elem.clientHeight };
+        evt.initCustomEvent(
+          'scrollrequest', true, true,
+          ObjectWrapper.wrap(details, elem.ownerDocument.defaultView));
+        if (!elem.dispatchEvent(evt))
+          return;
+      }
+
       // We will do window scrolling next.
       if (elem == content.document)
         break;
 
       if (!horiz && elem.clientHeight < elem.scrollHeight) {
         let s = content.getComputedStyle(elem);
         if (s.overflowY == 'scroll' || s.overflowY == 'auto') {
           elem.scrollTop += page * elem.clientHeight;
@@ -197,35 +214,16 @@ function scroll(aMessage) {
       if (horiz) {
         if (elem.clientWidth < elem.scrollWidth) {
           let s = content.getComputedStyle(elem);
           if (s.overflowX == 'scroll' || s.overflowX == 'auto') {
             elem.scrollLeft += page * elem.clientWidth;
             return true;
           }
         }
-
-        let controllers = acc.
-          getRelationByType(
-            Ci.nsIAccessibleRelation.RELATION_CONTROLLED_BY);
-        for (let i = 0; controllers.targetsCount > i; i++) {
-          let controller = controllers.getTarget(i);
-          // If the section has a controlling slider, it should be considered
-          // the page-turner.
-          if (controller.role == Ci.nsIAccessibleRole.ROLE_SLIDER) {
-            // Sliders are controlled with ctrl+right/left. I just decided :)
-            let evt = content.document.createEvent('KeyboardEvent');
-            evt.initKeyEvent(
-              'keypress', true, true, null,
-              true, false, false, false,
-              (page > 0) ? evt.DOM_VK_RIGHT : evt.DOM_VK_LEFT, 0);
-            controller.DOMNode.dispatchEvent(evt);
-            return true;
-          }
-        }
       }
       acc = acc.parent;
     }
 
     // Scroll window.
     if (!horiz && content.scrollMaxY &&
         ((page > 0 && content.scrollY < content.scrollMaxY) ||
          (page < 0 && content.scrollY > 0))) {
--- a/accessible/tests/mochitest/jsat/Makefile.in
+++ b/accessible/tests/mochitest/jsat/Makefile.in
@@ -8,15 +8,16 @@ topsrcdir = @top_srcdir@
 srcdir = @srcdir@
 VPATH = @srcdir@
 relativesrcdir = @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_A11Y_FILES =\
 jsatcommon.js \
-utterance.js \
+output.js \
 test_alive.html \
 test_explicit_names.html \
 test_utterance_order.html \
+test_braille.html \
 $(NULL)
 
 include $(topsrcdir)/config/rules.mk
rename from accessible/tests/mochitest/jsat/utterance.js
rename to accessible/tests/mochitest/jsat/output.js
--- a/accessible/tests/mochitest/jsat/utterance.js
+++ b/accessible/tests/mochitest/jsat/output.js
@@ -1,70 +1,80 @@
 const Cu = Components.utils;
 const PREF_UTTERANCE_ORDER = "accessibility.accessfu.utterance";
 
 Cu.import('resource://gre/modules/accessibility/Utils.jsm');
-Cu.import("resource://gre/modules/accessibility/UtteranceGenerator.jsm",
-  this);
+Cu.import("resource://gre/modules/accessibility/OutputGenerator.jsm", this);
 
 /**
- * Test context utterance generation.
+ * Test context output generation.
  *
- * @param expected {Array} expected utterance.
+ * @param expected {Array} expected output.
  * @param aAccOrElmOrID    identifier to get an accessible to test.
  * @param aOldAccOrElmOrID optional identifier to get an accessible relative to
  *                         the |aAccOrElmOrID|.
+ * @param aGenerator       the output generator to use when generating accessible
+ *                         output
  *
  * Note: if |aOldAccOrElmOrID| is not provided, the |aAccOrElmOrID| must be
  * scoped to the "root" element in markup.
  */
-function testContextUtterance(expected, aAccOrElmOrID, aOldAccOrElmOrID) {
+function testContextOutput(expected, aAccOrElmOrID, aOldAccOrElmOrID, aGenerator) {
   aOldAccOrElmOrID = aOldAccOrElmOrID || "root";
   var accessible = getAccessible(aAccOrElmOrID);
   var oldAccessible = getAccessible(aOldAccOrElmOrID);
   var context = new PivotContext(accessible, oldAccessible);
-  var utterance = UtteranceGenerator.genForContext(context);
-  isDeeply(utterance, expected,
-    "Context utterance is correct for " + aAccOrElmOrID);
+  var output = aGenerator.genForContext(context);
+  isDeeply(output, expected,
+    "Context output is correct for " + aAccOrElmOrID);
 }
 
 /**
- * Test object utterance generated array that includes names.
- * Note: test ignores utterances without the name.
+ * Test object output generated array that includes names.
+ * Note: test ignores outputs without the name.
  *
  * @param aAccOrElmOrID identifier to get an accessible to test.
+ * @param aGenerator    the output generator to use when generating accessible
+ *                      output
  */
-function testObjectUtterance(aAccOrElmOrID) {
+function testObjectOutput(aAccOrElmOrID, aGenerator) {
   var accessible = getAccessible(aAccOrElmOrID);
-  var utterance = UtteranceGenerator.genForObject(accessible);
-  var utteranceOrder;
+  var output = aGenerator.genForObject(accessible);
+  var outputOrder;
   try {
-    utteranceOrder = SpecialPowers.getIntPref(PREF_UTTERANCE_ORDER);
+    outputOrder = SpecialPowers.getIntPref(PREF_UTTERANCE_ORDER);
   } catch (ex) {
     // PREF_UTTERANCE_ORDER not set.
-    utteranceOrder = 0;
+    outputOrder = 0;
   }
-  var expectedNameIndex = utteranceOrder === 0 ? utterance.length - 1 : 0;
-  var nameIndex = utterance.indexOf(accessible.name);
+  var expectedNameIndex = outputOrder === 0 ? output.length - 1 : 0;
+  var nameIndex = output.indexOf(accessible.name);
 
   if (nameIndex > -1) {
-    ok(utterance.indexOf(accessible.name) === expectedNameIndex,
-      "Object utterance is correct for " + aAccOrElmOrID);
+    ok(output.indexOf(accessible.name) === expectedNameIndex,
+      "Object output is correct for " + aAccOrElmOrID);
   }
 }
 
 /**
- * Test object and context utterance for an accessible.
+ * Test object and context output for an accessible.
  *
- * @param expected {Array} expected utterance.
+ * @param expected {Array} expected output.
  * @param aAccOrElmOrID    identifier to get an accessible to test.
  * @param aOldAccOrElmOrID optional identifier to get an accessible relative to
  *                         the |aAccOrElmOrID|.
+ * @param aOutputKind      the type of output
  */
-function testUtterance(expected, aAccOrElmOrID, aOldAccOrElmOrID) {
-  testContextUtterance(expected, aAccOrElmOrID, aOldAccOrElmOrID);
-  // Just need to test object utterance for individual
+function testOutput(expected, aAccOrElmOrID, aOldAccOrElmOrID, aOutputKind) {
+  var generator;
+  if (aOutputKind === 1) {
+    generator = UtteranceGenerator;
+  } else {
+    generator = BrailleGenerator;
+  }
+  testContextOutput(expected, aAccOrElmOrID, aOldAccOrElmOrID, generator);
+  // Just need to test object output for individual
   // accOrElmOrID.
   if (aOldAccOrElmOrID) {
     return;
   }
-  testObjectUtterance(aAccOrElmOrID);
-}
\ No newline at end of file
+  testObjectOutput(aAccOrElmOrID, generator);
+}
new file mode 100644
--- /dev/null
+++ b/accessible/tests/mochitest/jsat/test_braille.html
@@ -0,0 +1,114 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=876475
+-->
+  <head>
+    <title>[AccessFu] braille generation test</title>
+    <meta charset="utf-8">
+    <link rel="stylesheet" type="text/css"
+          href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+    <script type="application/javascript"
+            src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+    <script type="application/javascript"
+            src="../common.js"></script>
+    <script type="application/javascript"
+            src="./output.js"></script>
+    <script type="application/javascript">
+
+      function doTest() {
+        // Test the following accOrElmOrID (with optional old accOrElmOrID).
+        // Note: each accOrElmOrID entry maps to a unique object braille
+        // generator function within the BrailleGenerator.
+        var tests = [{
+          accOrElmOrID: "link",
+          expected: [["lnk", "Link"], ["Link", "lnk"]]
+        },{
+          accOrElmOrID: "button",
+          expected: [["btn", "I am a button"], ["I am a button", "btn"]]
+        },{
+          accOrElmOrID: "password_input",
+          expected: [["passwdtxt", "Secret Password"], ["Secret Password", "passwdtxt"]]
+        },{
+          accOrElmOrID: "checkbox_unchecked",
+          expected: [["( )", "checkboxtext"], ["checkboxtext", "( )"]]
+        },{
+          accOrElmOrID: "checkbox_checked",
+          expected: [["(x)", "some more checkbox text"], ["some more checkbox text", "(x)"]]
+        },{
+          accOrElmOrID: "radio_unselected",
+          expected: [["( )", "any old radio button"], ["any old radio button", "( )"]]
+        },{
+          accOrElmOrID: "radio_selected",
+          expected: [["(x)", "a unique radio button"], ["a unique radio button", "(x)"]]
+        },{
+          accOrElmOrID: "togglebutton_notpressed",
+          expected: [["( )", "I ain't pressed"], ["I ain't pressed", "( )"]]
+        },{
+          accOrElmOrID: "togglebutton_pressed",
+          expected: [["(x)", "I am pressed!"], ["I am pressed!", "(x)"]]
+        },{
+          accOrElmOrID: "ul_li_one",
+          expected: [["*", "ul item 1"], ["*", "ul item 1"]]
+        },{
+          accOrElmOrID: "ol_li_one",
+          expected: [["1.", "ol item 1"], ["1.", "ol item 1"]]
+        },{
+          accOrElmOrID: "textarea",
+          expected: [["txtarea", "Here lies treasure."], ["Here lies treasure.", "txtarea"]]
+        }];
+
+        // Test all possible braille order preference values.
+        tests.forEach(function run(test) {
+          var brailleOrderValues = [0, 1];
+          brailleOrderValues.forEach(
+            function testBrailleOrder(brailleOrder) {
+              SpecialPowers.setIntPref(PREF_UTTERANCE_ORDER, brailleOrder);
+              var expected = test.expected[brailleOrder];
+              testOutput(expected, test.accOrElmOrID, test.oldAccOrElmOrID, 2);
+            }
+          );
+        });
+
+        // If there was an original utterance order preference, revert to it.
+        SpecialPowers.clearUserPref(PREF_UTTERANCE_ORDER);
+        SimpleTest.finish();
+      }
+
+      SimpleTest.waitForExplicitFinish();
+      addA11yLoadEvent(doTest);
+
+    </script>
+  </head>
+  <body>
+    <div id="root">
+      <p id="display"></p>
+      <div id="content" style="display: none"></div>
+      <pre id="test"></pre>
+      <a href="example.com" id="link">Link</a>
+      <button id="button">I am a button</button>
+      <label for="password_input">Secret Password</label><input id="password_input" type="password"></input>
+      <label for="checkbox_unchecked">checkboxtext</label><input id="checkbox_unchecked" type="checkbox"></input>
+      <label for="checkbox_checked">some more checkbox text</label><input id="checkbox_checked" type="checkbox" checked></input>
+      <label for="radio_unselected">any old radio button</label><input id="radio_unselected" type="radio"></input>
+      <label for="radio_selected">a unique radio button</label><input id="radio_selected" type="radio" checked></input>
+      <div id="togglebutton_notpressed" aria-pressed="false" role="button" tabindex="-1">I ain't pressed</div>
+      <div id="togglebutton_pressed" aria-pressed="true" role="button" tabindex="-1">I am pressed!</div>
+      <ol id="ordered_list">
+        <li id="ol_li_one">ol item 1</li>
+        <li id="ol_li_two">ol item 2</li>
+        <li id="ol_li_three">ol item 3</li>
+        <li id="ol_li_three">ol item 4</li>
+      </ol>
+      <ul id="unordered_list">
+        <li id="ul_li_one">ul item 1</li>
+        <li id="ul_li_two">ul item 2</li>
+        <li id="ul_li_three">ul item 3</li>
+        <li id="ul_li_three">ul item 4</li>
+      </ul>
+      <textarea id="textarea" cols="80" rows="5">
+        Here lies treasure.
+      </textarea>
+   </div>
+  </body>
+</html>
--- a/accessible/tests/mochitest/jsat/test_explicit_names.html
+++ b/accessible/tests/mochitest/jsat/test_explicit_names.html
@@ -4,17 +4,17 @@
 
   <link rel="stylesheet" type="text/css"
         href="chrome://mochikit/content/tests/SimpleTest/test.css" />
   <script type="application/javascript"
           src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script type="application/javascript"
           src="../common.js"></script>
   <script type="application/javascript"
-          src="utterance.js"></script>
+          src="output.js"></script>
   <script type="application/javascript">
 
     function doTest() {
       // Test the following accOrElmOrID.
       var tests = [{
         accOrElmOrID: "anchor1",
         expected: ["link", "title"]
       }, {
@@ -82,21 +82,24 @@
         // Test pivot to the table cell from the "apples" link.
         accOrElmOrID: "cell",
         oldAccOrElmOrID: "apples",
         expected: ["List of Fruits", "list 4 items", "First item", "link",
           "Apples", "link", "Bananas", "link", "Peaches", "Last item", "link",
           "Plums"]
       }];
 
+      SpecialPowers.setIntPref(PREF_UTTERANCE_ORDER, 0);
+
       // Test various explicit names vs the utterance generated from subtrees.
       tests.forEach(function run(test) {
-        testUtterance(test.expected, test.accOrElmOrID, test.oldAccOrElmOrID);
+        testOutput(test.expected, test.accOrElmOrID, test.oldAccOrElmOrID, 1);
       });
 
+      SpecialPowers.clearUserPref(PREF_UTTERANCE_ORDER);
       SimpleTest.finish();
     }
 
     SimpleTest.waitForExplicitFinish();
     addA11yLoadEvent(doTest);
   </script>
 </head>
 <body>
@@ -158,9 +161,9 @@
       <li class="yui3-u repost">
         <a href="#repost" title="repost" data-repost-button="1" data-reposted="0" data-post-id="5098826">
           <i aria-label="repost" class="icon-repost"></i>
         </a>
       </li>
     </ul>
   </div>
 </body>
-</html>
\ No newline at end of file
+</html>
--- a/accessible/tests/mochitest/jsat/test_utterance_order.html
+++ b/accessible/tests/mochitest/jsat/test_utterance_order.html
@@ -8,17 +8,17 @@ https://bugzilla.mozilla.org/show_bug.cg
     <meta charset="utf-8">
     <link rel="stylesheet" type="text/css"
           href="chrome://mochikit/content/tests/SimpleTest/test.css" />
     <script type="application/javascript"
             src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
     <script type="application/javascript"
             src="../common.js"></script>
     <script type="application/javascript"
-            src="./utterance.js"></script>
+            src="./output.js"></script>
     <script type="application/javascript">
 
       function doTest() {
         // Test the following accOrElmOrID (with optional old accOrElmOrID).
         // Note: each accOrElmOrID entry maps to a unique object utterance
         // generator function within the UtteranceGenerator.
         var tests = [{
           accOrElmOrID: "anchor",
@@ -115,17 +115,17 @@ https://bugzilla.mozilla.org/show_bug.cg
 
         // Test all possible utterance order preference values.
         tests.forEach(function run(test) {
           var utteranceOrderValues = [0, 1];
           utteranceOrderValues.forEach(
             function testUtteranceOrder(utteranceOrder) {
               SpecialPowers.setIntPref(PREF_UTTERANCE_ORDER, utteranceOrder);
               var expected = test.expected[utteranceOrder];
-              testUtterance(expected, test.accOrElmOrID, test.oldAccOrElmOrID);
+              testOutput(expected, test.accOrElmOrID, test.oldAccOrElmOrID, 1);
             }
           );
         });
 
         // If there was an original utterance order preference, revert to it.
         SpecialPowers.clearUserPref(PREF_UTTERANCE_ORDER);
         SimpleTest.finish();
       }
--- a/accessible/tests/mochitest/name/test_general.html
+++ b/accessible/tests/mochitest/name/test_general.html
@@ -203,16 +203,17 @@
       testName("ph_text", "a placeholder");
       testName("ph_textarea", "a placeholder");
       testName("ph_text2", "a label");
       testName("ph_textarea2", "a label");
       testName("ph_text3", "a label");
 
       // Test equation image
       testName("img_eq", "x^2 + y^2 + z^2")
+      testName("input_img_eq", "x^2 + y^2 + z^2")
       testName("txt_eq", "x^2 + y^2 + z^2")
 
       ////////////////////////////////////////////////////////////////////////
       // tests for duplicate announcement of content
 
       testName("test_note", null);
 
       SimpleTest.finish();
@@ -601,16 +602,17 @@
   <textarea id="ph_textarea2" cols="5" aria-labelledby="ph_text2" 
             placeholder="meh"></textarea>
 
   <label for="ph_text3">a label</label>
   <input id="ph_text3" placeholder="meh" />
 
   <p>Image: 
     <img id="img_eq" role="math" src="foo" alt="x^2 + y^2 + z^2">
+    <input type="image"  id="input_img_eq" src="foo" alt="x^2 + y^2 + z^2">
   </p>
 
   <p>Text: 
     <span id="txt_eq" role="math" title="x^2 + y^2 + z^2">x<sup>2</sup> + 
       y<sup>2</sup> + z<sup>2</sup></span>
 
   <!-- duplicate announcement -->
   <div id="test_note" role="note">subtree</div>
--- a/accessible/tests/mochitest/tree/Makefile.in
+++ b/accessible/tests/mochitest/tree/Makefile.in
@@ -29,16 +29,17 @@ MOCHITEST_A11Y_FILES =\
 		test_dockids.html \
 		test_filectrl.html \
 		test_formctrl.html \
 		test_formctrl.xul \
 		test_gencontent.html \
 		test_groupbox.xul \
 		test_iframe.html \
 		test_img.html \
+		test_invalid_img.xhtml \
 		test_invalidationlist.html \
 		test_list.html \
 		test_map.html \
 		test_media.html \
 		test_select.html \
 		test_tabbox.xul \
 		test_tabbrowser.xul \
 		test_table.html \
new file mode 100644
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_invalid_img.xhtml
@@ -0,0 +1,50 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <title>invalid html img</title>
+
+  <link rel="stylesheet" type="text/css"
+        href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+  <script type="application/javascript"
+          src="../common.js"></script>
+  <script type="application/javascript"
+          src="../role.js"></script>
+
+  <script>
+  <![CDATA[
+    function doTest()
+    {
+      document.getElementsByTagName("img")[0].firstChild.data = "2";
+
+      var accTree = {
+        role: ROLE_TEXT_CONTAINER,
+        children: [ { role: ROLE_TEXT_LEAF } ]
+      };
+      testAccessibleTree("the_img", accTree);
+      SimpleTest.finish();
+    }
+
+    SimpleTest.waitForExplicitFinish();
+    addA11yLoadEvent(doTest);
+  ]]>
+  </script>
+</head>
+<body>
+
+  <a target="_blank"
+     title="use HyperTextAccessible for invalid img"
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=852129">
+    Mozilla Bug 852129
+  </a>
+
+  <p id="display"></p>
+  <div id="content" style="display: none"></div>
+  <pre id="test">
+  </pre>
+
+  <img id="the_img">1</img>
+</body>
+</html>
--- a/addon-sdk/source/test/tabs/test-firefox-tabs.js
+++ b/addon-sdk/source/test/tabs/test-firefox-tabs.js
@@ -1145,8 +1145,12 @@ function openBrowserWindow(callback, url
 // Helper for calling code at window close
 function closeBrowserWindow(window, callback) {
   window.addEventListener("unload", function unload() {
     window.removeEventListener("unload", unload, false);
     callback();
   }, false);
   window.close();
 }
+
+// Test disabled on Linux because of bug 882867
+if (require("sdk/system/runtime").OS == "Linux")
+  module.exports = {};
--- a/b2g/app/Makefile.in
+++ b/b2g/app/Makefile.in
@@ -46,17 +46,16 @@ LOCAL_INCLUDES += -I$(topsrcdir)/xpcom/b
 LOCAL_INCLUDES += -I$(DEPTH)/build
 
 DEFINES += -DXPCOM_GLUE
 STL_FLAGS=
 
 LIBS += $(JEMALLOC_LIBS)
 
 LIBS += \
-  $(EXTRA_DSO_LIBS) \
   $(XPCOM_STANDALONE_GLUE_LDOPTS) \
   $(NULL)
 
 ifeq (gonk,$(MOZ_WIDGET_TOOLKIT))
 LIBS += \
   -lbinder \
   -lutils \
   $(NULL)
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -372,33 +372,42 @@ pref("browser.link.open_newwindow.restri
 pref("dom.mozBrowserFramesEnabled", true);
 
 // Enable a (virtually) unlimited number of mozbrowser processes.
 // We'll run out of PIDs on UNIX-y systems before we hit this limit.
 pref("dom.ipc.processCount", 100000);
 
 pref("dom.ipc.browser_frames.oop_by_default", false);
 
-// Temporary permission hack for WebSMS
+// WebSMS
 pref("dom.sms.enabled", true);
 pref("dom.sms.strict7BitEncoding", false); // Disabled by default.
 pref("dom.sms.requestStatusReport", true); // Enabled by default.
 
-// Temporary permission hack for WebContacts
+// WebContacts
 pref("dom.mozContacts.enabled", true);
 pref("dom.navigator-property.disable.mozContacts", false);
 pref("dom.global-constructor.disable.mozContact", false);
 
+// Shortnumber matching needed for e.g. Brazil:
+// 01187654321 can be found with 87654321
+pref("dom.phonenumber.substringmatching.BR", 8);
+pref("dom.phonenumber.substringmatching.CO", 10);
+pref("dom.phonenumber.substringmatching.VE", 7);
+
 // WebAlarms
 pref("dom.mozAlarms.enabled", true);
 
 // SimplePush
 pref("services.push.enabled", true);
+// Is the network connection allowed to be up?
+// This preference should be used in UX to enable/disable push.
+pref("services.push.connection.enabled", true);
 // serverURL to be assigned by services team
-pref("services.push.serverURL", "");
+pref("services.push.serverURL", "wss://push.services.mozilla.com/");
 pref("services.push.userAgentID", "");
 // Exponential back-off start is 5 seconds like in HTTP/1.1.
 // Maximum back-off is pingInterval.
 pref("services.push.retryBaseInterval", 5000);
 // Interval at which to ping PushServer to check connection status. In
 // milliseconds. If no reply is received within requestTimeout, the connection
 // is considered closed.
 pref("services.push.pingInterval", 1800000); // 30 minutes
@@ -630,16 +639,18 @@ pref("dom.ipc.processPrelaunch.delayMs",
 // whichever comes first.
 pref("dom.ipc.systemMessageCPULockTimeoutSec", 30);
 
 // Ignore the "dialog=1" feature in window.open.
 pref("dom.disable_window_open_dialog_feature", true);
 
 // Screen reader support
 pref("accessibility.accessfu.activate", 2);
+// Whether to skip images with empty alt text
+pref("accessibility.accessfu.skip_empty_images", true);
 
 // Enable hit-target fluffing
 pref("ui.touch.radius.enabled", false);
 pref("ui.touch.radius.leftmm", 3);
 pref("ui.touch.radius.topmm", 5);
 pref("ui.touch.radius.rightmm", 3);
 pref("ui.touch.radius.bottommm", 2);
 
--- a/b2g/chrome/content/settings.js
+++ b/b2g/chrome/content/settings.js
@@ -169,16 +169,21 @@ SettingsListener.observe('language.curre
     function(value) {
       Services.prefs.setBoolPref('dom.sms.requestStatusReport', value);
   });
 
   SettingsListener.observe('ril.cellbroadcast.disabled', false,
     function(value) {
       Services.prefs.setBoolPref('ril.cellbroadcast.disabled', value);
   });
+
+  SettingsListener.observe('ril.radio.disabled', false,
+    function(value) {
+      Services.prefs.setBoolPref('ril.radio.disabled', value);
+  });
 })();
 
 //=================== DeviceInfo ====================
 Components.utils.import('resource://gre/modules/XPCOMUtils.jsm');
 Components.utils.import('resource://gre/modules/ctypes.jsm');
 (function DeviceInfoToSettings() {
   XPCOMUtils.defineLazyServiceGetter(this, 'gSettingsService',
                                      '@mozilla.org/settingsService;1',
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -374,18 +374,37 @@ var shell = {
         type = 'back-button';
         break;
       case evt.DOM_VK_CONTEXT_MENU: // Menu button
         type = 'menu-button';
         break;
       case evt.DOM_VK_F1: // headset button
         type = 'headset-button';
         break;
-      default:                      // Anything else is a real key
-        return;  // Don't filter it at all; let it propagate to Gaia
+    }
+
+    let mediaKeys = {
+      'MediaNextTrack': 'media-next-track-button',
+      'MediaPreviousTrack': 'media-previous-track-button',
+      'MediaPause': 'media-pause-button',
+      'MediaPlay': 'media-play-button',
+      'MediaPlayPause': 'media-play-pause-button',
+      'MediaStop': 'media-stop-button',
+      'MediaRewind': 'media-rewind-button',
+      'FastFwd': 'media-fast-forward-button'
+    };
+
+    let isMediaKey = false;
+    if (mediaKeys[evt.key]) {
+      isMediaKey = true;
+      type = mediaKeys[evt.key];
+    }
+
+    if (!type) {
+      return;
     }
 
     // If we didn't return, then the key event represents a hardware key
     // and we need to prevent it from propagating to Gaia
     evt.stopImmediatePropagation();
     evt.preventDefault(); // Prevent keypress events (when #501496 is fixed).
 
     // If it is a key down or key up event, we send a chrome event to Gaia.
@@ -403,16 +422,22 @@ var shell = {
 
     // Let applications receive the headset button key press/release event.
     if (evt.keyCode == evt.DOM_VK_F1 && type !== this.lastHardwareButtonEventType) {
       this.lastHardwareButtonEventType = type;
       gSystemMessenger.broadcastMessage('headset-button', type);
       return;
     }
 
+    if (isMediaKey) {
+      this.lastHardwareButtonEventType = type;
+      gSystemMessenger.broadcastMessage('media-button', type);
+      return;
+    }
+
     // On my device, the physical hardware buttons (sleep and volume)
     // send multiple events (press press release release), but the
     // soft home button just sends one.  This hack is to manually
     // "debounce" the keys. If the type of this event is the same as
     // the type of the last one, then don't send it.  We'll never send
     // two presses or two releases in a row.
     // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=761067
     if (type !== this.lastHardwareButtonEventType) {
--- a/b2g/components/Makefile.in
+++ b/b2g/components/Makefile.in
@@ -4,17 +4,17 @@
 
 DEPTH      = @DEPTH@
 topsrcdir  = @top_srcdir@
 srcdir     = @srcdir@
 VPATH      = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
-EXTRA_PP_COMPONENTS = \
+DISABLED_EXTRA_PP_COMPONENTS = \
         ActivitiesGlue.js \
         AlertsService.js \
         B2GAboutRedirector.js \
         B2GComponents.manifest \
         ContentHandler.js \
         ContentPermissionPrompt.js \
         DirectoryProvider.js \
         FilePicker.js \
@@ -31,12 +31,12 @@ EXTRA_PP_COMPONENTS = \
 EXTRA_JS_MODULES = \
 	Keyboard.jsm \
 	TelURIParser.jsm \
 	SignInToWebsite.jsm \
 	ErrorPage.jsm \
 	$(NULL)
 
 ifdef MOZ_UPDATER
-EXTRA_PP_COMPONENTS += UpdatePrompt.js
+DISABLED_EXTRA_PP_COMPONENTS += UpdatePrompt.js
 endif
 
 include $(topsrcdir)/config/rules.mk
--- a/b2g/components/moz.build
+++ b/b2g/components/moz.build
@@ -7,8 +7,31 @@
 TEST_DIRS += ['test']
 
 XPIDL_SOURCES += [
     'b2g.idl',
 ]
 
 MODULE = 'B2GComponents'
 
+EXTRA_PP_COMPONENTS += [
+    'ActivitiesGlue.js',
+    'AlertsService.js',
+    'B2GAboutRedirector.js',
+    'B2GComponents.manifest',
+    'ContentHandler.js',
+    'ContentPermissionPrompt.js',
+    'DirectoryProvider.js',
+    'FilePicker.js',
+    'MailtoProtocolHandler.js',
+    'MozKeyboard.js',
+    'PaymentGlue.js',
+    'ProcessGlobal.js',
+    'RecoveryService.js',
+    'SmsProtocolHandler.js',
+    'TelProtocolHandler.js',
+    'YoutubeProtocolHandler.js',
+]
+
+if CONFIG['MOZ_UPDATER']:
+    EXTRA_PP_COMPONENTS += [
+        'UpdatePrompt.js',
+    ]
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,4 +1,4 @@
 {
-    "revision": "a7f713158285156858dcf774ce51782ac01c938c", 
+    "revision": "393b5209f9bc300017f2e037b1d0beeda4da7519", 
     "repo_path": "/integration/gaia-central"
 }
new file mode 100644
--- /dev/null
+++ b/b2g/config/inari/config.json
@@ -0,0 +1,36 @@
+{
+    "config_version": 2,
+    "tooltool_manifest": "releng-inari.tt",
+    "mock_target": "mozilla-centos6-i386",
+    "mock_packages": ["ccache", "make", "bison", "flex", "gcc", "g++", "mpfr", "zlib-devel", "ncurses-devel", "zip", "autoconf213", "glibc-static", "perl-Digest-SHA", "wget", "alsa-lib", "atk", "cairo", "dbus-glib", "fontconfig", "freetype", "glib2", "gtk2", "libXRender", "libXt", "pango", "mozilla-python27-mercurial", "openssh-clients", "nss-devel", "java-1.6.0-openjdk-devel", "git"],
+    "mock_files": [["/home/cltbld/.ssh", "/home/mock_mozilla/.ssh"]],
+    "build_targets": [],
+    "upload_files": [
+        "{objdir}/dist/b2g-*.crashreporter-symbols.zip",
+        "{objdir}/dist/b2g-*.tar.gz",
+        "{workdir}/sources.xml"
+    ],
+    "zip_files": [
+        ["{workdir}/out/target/product/inari/*.img", "out/target/product/inari/"],
+        ["{workdir}/boot.img", "out/target/product/inari/"],
+        "{workdir}/flash.sh",
+        "{workdir}/load-config.sh",
+        "{workdir}/.config",
+        "{workdir}/sources.xml"
+    ],
+    "env": {
+        "VARIANT": "user",
+        "MOZILLA_OFFICIAL": "1",
+        "B2GUPDATER": "1"
+    },
+    "b2g_manifest": "inari.xml",
+    "b2g_manifest_branch": "master",
+    "additional_source_tarballs": ["backup-inari.tar.xz"],
+    "gecko_l10n_root": "http://hg.mozilla.org/l10n-central",
+    "gaia": {
+        "l10n": {
+            "vcs": "hgtool",
+            "root": "http://hg.mozilla.org/gaia-l10n"
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/b2g/config/inari/releng-inari.tt
@@ -0,0 +1,20 @@
+[
+{
+"size": 4249600,
+"digest": "9f2150350e6fb2e8fe8744f47c02799865de16f8539844374188c2b421d8a044c213af57c8fe9dc7ad2038150687b50eba251dc782fa606674a86d8acc9e3dad",
+"algorithm": "sha512",
+"filename": "boot.img"
+},
+{
+"size": 33335060,
+"digest": "aa60b13458fabcb60c671e80db4dfc0cda3090fc5df27215cec3fb01f9cf2ac4403c93be99d1816a1f2d903febd4b027f7193a5fcec1aacf18ebb419ee9f32d7",
+"algorithm": "sha512",
+"filename": "backup-inari.tar.xz"
+},
+{
+"size": 1570553,
+"digest": "ea03de74df73b05e939c314cd15c54aac7b5488a407b7cc4f5f263f3049a1f69642c567dd35c43d0bc3f0d599d0385a26ab2dd947a6b18f9044e4918b382eea7",
+"algorithm": "sha512",
+"filename": "Adreno200-AU_LINUX_ANDROID_ICS_CHOCO_CS.04.00.03.06.001.zip"
+}
+]
--- a/browser/app/Makefile.in
+++ b/browser/app/Makefile.in
@@ -49,17 +49,16 @@ LOCAL_INCLUDES += \
   -I$(topsrcdir)/xpcom/build \
   -I$(DEPTH)/build \
   $(NULL)
 
 DEFINES += -DXPCOM_GLUE
 STL_FLAGS=
 
 LIBS += \
-	$(EXTRA_DSO_LIBS) \
 	$(XPCOM_STANDALONE_GLUE_LDOPTS) \
 	$(NULL)
 
 ifdef MOZ_LINKER
 LIBS += $(MOZ_ZLIB_LIBS)
 endif
 
 ifdef HAVE_CLOCK_MONOTONIC
--- a/browser/app/blocklist.xml
+++ b/browser/app/blocklist.xml
@@ -1,10 +1,10 @@
 <?xml version="1.0"?>
-<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1370548649000">
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1370990544000">
   <emItems>
       <emItem  blockID="i350" id="sqlmoz@facebook.com">
                         <versionRange  minVersion="0" maxVersion="*" severity="3">
                     </versionRange>
                   </emItem>
       <emItem  blockID="i58" id="webmaster@buzzzzvideos.info">
                         <versionRange  minVersion="0" maxVersion="*">
                     </versionRange>
@@ -217,16 +217,20 @@
       <emItem  blockID="i100" id="{394DCBA4-1F92-4f8e-8EC9-8D2CB90CB69B}">
                         <versionRange  minVersion="2.5.0" maxVersion="2.5.0" severity="1">
                     </versionRange>
                   </emItem>
       <emItem  blockID="i338" id="{1FD91A9C-410C-4090-BBCC-55D3450EF433}">
                         <versionRange  minVersion="0" maxVersion="*" severity="3">
                     </versionRange>
                   </emItem>
+      <emItem  blockID="i364" id="{FE1DEEEA-DB6D-44b8-83F0-34FC0F9D1052}">
+                        <versionRange  minVersion="0" maxVersion="*" severity="1">
+                    </versionRange>
+                  </emItem>
       <emItem  blockID="i59" id="ghostviewer@youtube2.com">
                         <versionRange  minVersion="0" maxVersion="*">
                     </versionRange>
                   </emItem>
       <emItem  blockID="i222" id="dealcabby@jetpack">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                   </emItem>
@@ -248,16 +252,20 @@
                               <versionRange  minVersion="9.0a1" maxVersion="9.0" />
                           </targetApplication>
                     </versionRange>
                   </emItem>
       <emItem  blockID="i256" id="/^[0-9a-f]+@[0-9a-f]+\.info/">
                         <versionRange  minVersion="0" maxVersion="*" severity="3">
                     </versionRange>
                   </emItem>
+      <emItem  blockID="i370" id="happylyrics@hpyproductions.net">
+                        <versionRange  minVersion="0" maxVersion="*" severity="1">
+                    </versionRange>
+                  </emItem>
       <emItem  blockID="i22" id="ShopperReports@ShopperReports.com">
                         <versionRange  minVersion="3.1.22.0" maxVersion="3.1.22.0">
                     </versionRange>
                   </emItem>
       <emItem  blockID="i44" id="sigma@labs.mozilla">
                         </emItem>
       <emItem  blockID="i246" id="support@vide1flash2.com">
                         <versionRange  minVersion="0" maxVersion="*" severity="3">
@@ -798,17 +806,17 @@
       <pluginItem  os="Darwin" blockID="p242">
             <match name="description" exp="Flip4Mac" />                                          <versionRange  minVersion="0" maxVersion="2.4.3.999" severity="1">
                                 <targetApplication  id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
                               <versionRange  minVersion="18.0a1" maxVersion="*" />
                           </targetApplication>
                   </versionRange>
                   </pluginItem>
       <pluginItem  blockID="p248">
-                  <match name="filename" exp="Scorch\.plugin" />                      <versionRange  minVersion="0" maxVersion="6.2.0" severity="1"></versionRange>
+                  <match name="filename" exp="Scorch\.plugin" />                      <versionRange  minVersion="0" maxVersion="6.2.0b88" severity="1"></versionRange>
                   </pluginItem>
       <pluginItem  blockID="p250">
                   <match name="filename" exp="npFoxitReaderPlugin\.dll" />                      <versionRange  minVersion="0" maxVersion="2.2.1.530" severity="0" vulnerabilitystatus="2"></versionRange>
                   </pluginItem>
       <pluginItem  os="Darwin" blockID="p252">
                   <match name="filename" exp="AdobePDFViewerNPAPI\.plugin" />                      <versionRange  minVersion="11.0.0" maxVersion="11.0.01" severity="1"></versionRange>
                   </pluginItem>
       <pluginItem  blockID="p254">
@@ -925,16 +933,19 @@
                   </pluginItem>
       <pluginItem  blockID="p332">
             <match name="description" exp="^Shockwave Flash 11.(0|1) r[0-9]{1,3}$" />      <match name="filename" exp="libflashplayer\.so" />                                    <versionRange  severity="0" vulnerabilitystatus="1">
                                 <targetApplication  id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
                               <versionRange  minVersion="17.0.4" maxVersion="17.0.*" />
                           </targetApplication>
                   </versionRange>
                   </pluginItem>
+      <pluginItem  blockID="p366">
+                  <match name="filename" exp="Scorch\.plugin" />                      <versionRange  minVersion="6.2.0" maxVersion="6.2.0" severity="1"></versionRange>
+                  </pluginItem>
     </pluginItems>
 
   <gfxItems>
     <gfxBlacklistEntry  blockID="g35">      <os>WINNT 6.1</os>      <vendor>0x10de</vendor>              <devices>
                       <device>0x0a6c</device>
                   </devices>
             <feature>DIRECT2D</feature>      <featureStatus>BLOCKED_DRIVER_VERSION</featureStatus>      <driverVersion>8.17.12.5896</driverVersion>      <driverVersionComparator>LESS_THAN_OR_EQUAL</driverVersionComparator>    </gfxBlacklistEntry>
     <gfxBlacklistEntry  blockID="g36">      <os>WINNT 6.1</os>      <vendor>0x10de</vendor>              <devices>
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -3607,16 +3607,17 @@ function updateCharacterEncodingMenuStat
  * function in findbar.xml
  */
 function mimeTypeIsTextBased(aMimeType)
 {
   return aMimeType.startsWith("text/") ||
          aMimeType.endsWith("+xml") ||
          aMimeType == "application/x-javascript" ||
          aMimeType == "application/javascript" ||
+         aMimeType == "application/json" ||
          aMimeType == "application/xml" ||
          aMimeType == "mozilla.application/cached-xul";
 }
 
 var XULBrowserWindow = {
   // Stored Status, Link and Loading values
   status: "",
   defaultStatus: "",
--- a/browser/base/content/test/browser_datareporting_notification.js
+++ b/browser/base/content/test/browser_datareporting_notification.js
@@ -18,17 +18,16 @@ function sendNotifyRequest(name) {
 
   let policy = new ns.DataReportingPolicy(policyPrefs, hrPrefs, service);
   policy.firstRunDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
 
   is(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED, "Policy is in unnotified state.");
 
   service.healthReporter.onInit().then(function onInit() {
     is(policy.ensureNotifyResponse(new Date()), false, "User has not responded to policy.");
-    is(policy.notifyState, policy.STATE_NOTIFY_WAIT, "Policy is waiting for notification response.");
   });
 
   return policy;
 }
 
 /**
  * Wait for a <notification> to be closed then call the specified callback.
  */
@@ -48,25 +47,27 @@ function waitForNotificationClose(notifi
         cb();
       }
     }
   });
 
   observer.observe(parent, {childList: true});
 }
 
+let dumpAppender, rootLogger;
+
 function test() {
   waitForExplicitFinish();
 
   let ns = {};
   Components.utils.import("resource://services-common/log4moz.js", ns);
-  let rootLogger = ns.Log4Moz.repository.rootLogger;
-  let appender = new ns.Log4Moz.DumpAppender();
-  appender.level = ns.Log4Moz.Level.All;
-  rootLogger.addAppender(appender);
+  rootLogger = ns.Log4Moz.repository.rootLogger;
+  dumpAppender = new ns.Log4Moz.DumpAppender();
+  dumpAppender.level = ns.Log4Moz.Level.All;
+  rootLogger.addAppender(dumpAppender);
 
   let notification = document.getElementById("global-notificationbox");
   let policy;
 
   notification.addEventListener("AlertActive", function active() {
     notification.removeEventListener("AlertActive", active, true);
 
     executeSoon(function afterNotification() {
@@ -121,16 +122,19 @@ function test_multiple_windows() {
         }
 
         if (!childWindowClosed) {
           dump("Not finishing test yet because child window isn't closed.\n");
           return;
         }
 
         dump("Finishing multiple window test.\n");
+        rootLogger.removeAppender(dumpAppender);
+        delete dumpAppender;
+        delete rootLogger;
         finish();
       }
 
       let closeCount = 0;
       function onAlertClose() {
         closeCount++;
 
         if (closeCount != 2) {
--- a/browser/base/content/test/browser_locationBarCommand.js
+++ b/browser/base/content/test/browser_locationBarCommand.js
@@ -29,26 +29,26 @@ saveURL = function() {
 function runAltLeftClickTest() {
   info("Running test: Alt left click");
   triggerCommand(true, { altKey: true });
 }
 
 function runShiftLeftClickTest() {
   let listener = new WindowListener(getBrowserURL(), function(aWindow) {
     Services.wm.removeListener(listener);
-    addPageShowListener(aWindow.gBrowser, function() {
+    addPageShowListener(aWindow.gBrowser.selectedBrowser, function() {
       info("URL should be loaded in a new window");
       is(gURLBar.value, "", "Urlbar reverted to original value");       
       is(gFocusManager.focusedElement, null, "There should be no focused element");
       is(gFocusManager.focusedWindow, aWindow.gBrowser.contentWindow, "Content window should be focused");
       is(aWindow.gURLBar.value, TEST_VALUE, "New URL is loaded in new window");
 
       aWindow.close();
       runNextTest();
-    });
+    }, "http://example.com/");
   });
   Services.wm.addListener(listener);
 
   info("Running test: Shift left click");
   triggerCommand(true, { shiftKey: true });
 }
 
 function runNextTest() {
@@ -56,17 +56,17 @@ function runNextTest() {
   if (!test) {
     finish();
     return;
   }
   
   info("Running test: " + test.desc);
   // Tab will be blank if test.startValue is null
   let tab = gBrowser.selectedTab = gBrowser.addTab(test.startValue);
-  addPageShowListener(gBrowser, function() {
+  addPageShowListener(gBrowser.selectedBrowser, function() {
     triggerCommand(test.click, test.event);
     test.check(tab);
 
     // Clean up
     while (gBrowser.tabs.length > 1)
       gBrowser.removeTab(gBrowser.selectedTab)
     runNextTest();
   });
@@ -158,20 +158,23 @@ function checkCurrent(aTab) {
 function checkNewTab(aTab) {
   info("URL should be loaded in a new focused tab");
   is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered");
   is(gFocusManager.focusedElement, null, "There should be no focused element");
   is(gFocusManager.focusedWindow, gBrowser.contentWindow, "Content window should be focused");
   isnot(gBrowser.selectedTab, aTab, "New URL was loaded in a new tab");
 }
 
-function addPageShowListener(aBrowser, aFunc) {
-  aBrowser.selectedBrowser.addEventListener("pageshow", function loadListener() {
-    aBrowser.selectedBrowser.removeEventListener("pageshow", loadListener, false);
-    aFunc();
+function addPageShowListener(browser, cb, expectedURL) {
+  browser.addEventListener("pageshow", function pageShowListener() {
+    info("pageshow: " + browser.currentURI.spec);
+    if (expectedURL && browser.currentURI.spec != expectedURL)
+      return; // ignore pageshows for non-expected URLs
+    browser.removeEventListener("pageshow", pageShowListener, false);
+    cb();
   });
 }
 
 function WindowListener(aURL, aCallback) {
   this.callback = aCallback;
   this.url = aURL;
 }
 WindowListener.prototype = {
--- a/browser/base/content/test/browser_urlbar_search_healthreport.js
+++ b/browser/base/content/test/browser_urlbar_search_healthreport.js
@@ -40,26 +40,26 @@ function test() {
       }
 
       let tab = gBrowser.addTab();
       gBrowser.selectedTab = tab;
 
       gURLBar.value = "firefox health report";
       gURLBar.handleCommand();
 
-      executeSoon(function afterSearch() {
+      executeSoon(() => executeSoon(() => {
         gBrowser.removeTab(tab);
 
         m.getValues().then(function onData(data) {
           ok(data.days.hasDay(now), "FHR has data for today.");
           let day = data.days.getDay(now);
           ok(day.has(field), "FHR has url bar count for today.");
 
           let newCount = day.get(field);
 
           is(newCount, oldCount + 1, "Exactly one search has been recorded.");
           finish();
         });
-      });
+      }));
     });
   });
 }
 
--- a/browser/base/content/test/test_contextmenu.html
+++ b/browser/base/content/test/test_contextmenu.html
@@ -707,20 +707,20 @@ function runTest(testNum) {
 
         invokeItemAction("0");
         closeContextMenu();
 
         // run mozRequestFullScreen on the element we're testing
         var full_screen_element = subwindow.document.getElementById("test-dom-full-screen");
         var openDomFullScreen = function() {
             subwindow.removeEventListener("mozfullscreenchange", openDomFullScreen, false);
-            SpecialPowers.clearUserPref("full-screen-api.allow-trusted-requests-only");
             openContextMenuFor(dom_full_screen, true); // Invoke context menu for next test.
         }
         subwindow.addEventListener("mozfullscreenchange", openDomFullScreen, false);
+        SpecialPowers.setBoolPref("full-screen-api.approval-required", false);
         SpecialPowers.setBoolPref("full-screen-api.allow-trusted-requests-only", false);
         full_screen_element.mozRequestFullScreen();
         break;
 
         case 22:
         // Context menu for DOM Fullscreen mode (NOTE: this is *NOT* on an img)
         checkContextMenu(["context-leave-dom-fullscreen", true,
                           "---",                          null,
@@ -736,21 +736,21 @@ function runTest(testNum) {
                           "---",                          null,
                           "context-viewsource",           true,
                           "context-viewinfo",             true
                          ].concat(inspectItems));
         closeContextMenu();
         var full_screen_element = subwindow.document.getElementById("test-dom-full-screen");
         var openPagemenu = function() {
             subwindow.removeEventListener("mozfullscreenchange", openPagemenu, false);
+            SpecialPowers.clearUserPref("full-screen-api.approval-required");
             SpecialPowers.clearUserPref("full-screen-api.allow-trusted-requests-only");
             openContextMenuFor(pagemenu, true); // Invoke context menu for next test.
         }
         subwindow.addEventListener("mozfullscreenchange", openPagemenu, false);
-        SpecialPowers.setBoolPref("full-screen-api.allow-trusted-requests-only", false);
         subwindow.document.mozCancelFullScreen();
         break;
 
     case 23:
         // Context menu for element with assigned content context menu
         // The shift key should bypass content context menu processing
         checkContextMenu(["context-back",         false,
                           "context-forward",      false,
--- a/browser/components/Makefile.in
+++ b/browser/components/Makefile.in
@@ -8,16 +8,16 @@ srcdir    = @srcdir@
 VPATH     = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 DISABLED_EXTRA_COMPONENTS = \
   BrowserComponents.manifest \
   $(NULL)
 
-EXTRA_PP_COMPONENTS = \
+DISABLED_EXTRA_PP_COMPONENTS = \
   nsBrowserContentHandler.js \
   nsBrowserGlue.js \
   $(NULL)
 
 EXTRA_JS_MODULES = distribution.js
 
 include $(topsrcdir)/config/rules.mk
--- a/browser/components/feeds/src/Makefile.in
+++ b/browser/components/feeds/src/Makefile.in
@@ -20,16 +20,16 @@ DEFINES += \
 	$(NULL)
 
 DISABLED_EXTRA_COMPONENTS = \
   BrowserFeeds.manifest \
 	FeedConverter.js \
 	WebContentConverter.js \
   $(NULL)
 
-EXTRA_PP_COMPONENTS = \
+DISABLED_EXTRA_PP_COMPONENTS = \
 	FeedWriter.js \
 	$(NULL)
 
 
 LOCAL_INCLUDES = -I$(srcdir)/../../build
 
 include $(topsrcdir)/config/rules.mk
--- a/browser/components/feeds/src/moz.build
+++ b/browser/components/feeds/src/moz.build
@@ -10,8 +10,12 @@ CPP_SOURCES += [
     'nsFeedSniffer.cpp',
 ]
 
 EXTRA_COMPONENTS += [
     'BrowserFeeds.manifest',
     'FeedConverter.js',
     'WebContentConverter.js',
 ]
+
+EXTRA_PP_COMPONENTS += [
+    'FeedWriter.js',
+]
--- a/browser/components/migration/src/Makefile.in
+++ b/browser/components/migration/src/Makefile.in
@@ -14,37 +14,37 @@ LIBRARY_NAME	= migration_s
 FORCE_STATIC_LIB = 1
 USE_STATIC_LIBS = 1
 
 DISABLED_EXTRA_COMPONENTS = \
   ProfileMigrator.js \
   FirefoxProfileMigrator.js \
   $(NULL)
 
-EXTRA_PP_COMPONENTS = \
+DISABLED_EXTRA_PP_COMPONENTS = \
   ChromeProfileMigrator.js \
   $(NULL)
 
 ifeq ($(OS_ARCH),WINNT)
 DISABLED_EXTRA_COMPONENTS += IEProfileMigrator.js \
                     $(NULL)
 
-EXTRA_PP_COMPONENTS += SafariProfileMigrator.js \
+DISABLED_EXTRA_PP_COMPONENTS += SafariProfileMigrator.js \
                        $(NULL)
 
 DEFINES += -DHAS_IE_MIGRATOR -DHAS_SAFARI_MIGRATOR
 endif
 
 ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT))
-EXTRA_PP_COMPONENTS += SafariProfileMigrator.js \
+DISABLED_EXTRA_PP_COMPONENTS += SafariProfileMigrator.js \
                        $(NULL)
 DEFINES += -DHAS_SAFARI_MIGRATOR
 endif
 
-EXTRA_PP_COMPONENTS += \
+DISABLED_EXTRA_PP_COMPONENTS += \
 	BrowserProfileMigrators.manifest \
 	$(NULL)
 
 EXTRA_PP_JS_MODULES = \
 	MigrationUtils.jsm \
 	$(NULL)
 
 include $(topsrcdir)/config/rules.mk
--- a/browser/components/migration/src/moz.build
+++ b/browser/components/migration/src/moz.build
@@ -15,8 +15,23 @@ EXTRA_COMPONENTS += [
     'FirefoxProfileMigrator.js',
     'ProfileMigrator.js',
 ]
 
 if CONFIG['OS_ARCH'] == 'WINNT':
     EXTRA_COMPONENTS += [
         'IEProfileMigrator.js',
     ]
+
+EXTRA_PP_COMPONENTS += [
+    'BrowserProfileMigrators.manifest',
+    'ChromeProfileMigrator.js',
+]
+
+if CONFIG['OS_ARCH'] == 'WINNT':
+    EXTRA_PP_COMPONENTS += [
+        'SafariProfileMigrator.js',
+    ]
+
+if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa':
+    EXTRA_PP_COMPONENTS += [
+        'SafariProfileMigrator.js',
+    ]
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -34,8 +34,12 @@ XPIDL_SOURCES += [
 
 XPIDL_MODULE = 'browsercompsbase'
 
 MODULE = 'browsercomps'
 
 EXTRA_COMPONENTS += [
     'BrowserComponents.manifest',
 ]
+EXTRA_PP_COMPONENTS += [
+    'nsBrowserContentHandler.js',
+    'nsBrowserGlue.js',
+]
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -312,16 +312,33 @@ BrowserGlue.prototype = {
             reporter.getProvider("org.mozilla.searches").recordSearch(name,
                                                                       "urlbar");
           } catch (ex) {
             Cu.reportError(ex);
           }
         });
         break;
 #endif
+      case "browser-search-engine-modified":
+        if (data != "engine-default" && data != "engine-current") {
+          break;
+        }
+        // Enforce that the search service's defaultEngine is always equal to
+        // its currentEngine. The search service will notify us any time either
+        // of them are changed (either by directly setting the relevant prefs,
+        // i.e. if add-ons try to change this directly, or if the
+        // nsIBrowserSearchService setters are called).
+        let ss = Services.search;
+        if (ss.currentEngine.name == ss.defaultEngine.name)
+          return;
+        if (data == "engine-current")
+          ss.defaultEngine = ss.currentEngine;
+        else
+          ss.currentEngine = ss.defaultEngine;
+        break;
     }
   },
 
   // initialization (called on application startup) 
   _init: function BG__init() {
     let os = Services.obs;
     os.addObserver(this, "prefservice:after-app-defaults", false);
     os.addObserver(this, "final-ui-startup", false);
@@ -346,16 +363,17 @@ BrowserGlue.prototype = {
     os.addObserver(this, "distribution-customization-complete", false);
     os.addObserver(this, "places-shutdown", false);
     this._isPlacesShutdownObserver = true;
     os.addObserver(this, "handle-xul-text-link", false);
     os.addObserver(this, "profile-before-change", false);
 #ifdef MOZ_SERVICES_HEALTHREPORT
     os.addObserver(this, "keyword-search", false);
 #endif
+    os.addObserver(this, "browser-search-engine-modified", false);
   },
 
   // cleanup (called on application shutdown)
   _dispose: function BG__dispose() {
     let os = Services.obs;
     os.removeObserver(this, "prefservice:after-app-defaults");
     os.removeObserver(this, "final-ui-startup");
     os.removeObserver(this, "sessionstore-windows-restored");
@@ -379,16 +397,17 @@ BrowserGlue.prototype = {
       os.removeObserver(this, "places-database-locked");
     if (this._isPlacesShutdownObserver)
       os.removeObserver(this, "places-shutdown");
     os.removeObserver(this, "handle-xul-text-link");
     os.removeObserver(this, "profile-before-change");
 #ifdef MOZ_SERVICES_HEALTHREPORT
     os.removeObserver(this, "keyword-search");
 #endif
+    os.removeObserver(this, "browser-search-engine-modified");
   },
 
   _onAppDefaults: function BG__onAppDefaults() {
     // apply distribution customizations (prefs)
     // other customizations are applied in _finalUIStartup()
     this._distributionCustomizer.applyPrefDefaults();
   },
 
--- a/browser/components/preferences/in-content/privacy.xul
+++ b/browser/components/preferences/in-content/privacy.xul
@@ -66,17 +66,17 @@
 </preferences>
 
 <hbox class="heading" data-category="panePrivacy" hidden="true">
   <image class="preference-icon" type="privacy"/>
   <html:h1>&panePrivacy.title;</html:h1>
 </hbox>
 
 <!-- Tracking -->
-<groupbox id="trackingGroup" data-category="panePrivacy" hidden="true">
+<groupbox id="trackingGroup" data-category="panePrivacy" hidden="true" align="start">
   <caption label="&tracking.label;"/>
   <radiogroup id="doNotTrackSelection" orient="vertical"
               preference="privacy.donottrackheader.value"
               onsynctopreference="return gPrivacyPane.setTrackingPrefs()"
               onsyncfrompreference="return gPrivacyPane.getTrackingPrefs()">
     <radio id="dntnotrack" value="1" label="&dntTrackingNotOkay.label2;"
             accesskey="&dntTrackingNotOkay.accesskey;" />
     <radio id="dntdotrack" value="0" label="&dntTrackingOkay.label2;"
--- a/browser/components/preferences/privacy.xul
+++ b/browser/components/preferences/privacy.xul
@@ -77,17 +77,17 @@
 
     </preferences>
     
     <stringbundle id="bundlePreferences" src="chrome://browser/locale/preferences/preferences.properties"/>
     
     <script type="application/javascript" src="chrome://browser/content/preferences/privacy.js"/>
 
     <!-- Tracking -->
-    <groupbox id="trackingGroup">
+    <groupbox id="trackingGroup" align="start">
       <caption label="&tracking.label;"/>
       <radiogroup id="doNotTrackSelection" orient="vertical"
                   preference="privacy.donottrackheader.value"
                   onsynctopreference="return gPrivacyPane.setTrackingPrefs()"
                   onsyncfrompreference="return gPrivacyPane.getTrackingPrefs()">
         <radio id="dntnotrack" value="1" label="&dntTrackingNotOkay.label2;"
                 accesskey="&dntTrackingNotOkay.accesskey;" />
         <radio id="dntdotrack" value="0" label="&dntTrackingOkay.label2;"
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -483,27 +483,32 @@
           openUILinkIn(submission.uri.spec, aWhere, null, submission.postData);
         ]]></body>
       </method>
     </implementation>
 
     <handlers>
       <handler event="command"><![CDATA[
         const target = event.originalTarget;
-        if (target.classList.contains("addengine-item")) {
+        if (target.engine) {
+          this.currentEngine = target.engine;
+        } else if (target.classList.contains("addengine-item")) {
           var searchService =
             Components.classes["@mozilla.org/browser/search-service;1"]
                       .getService(Components.interfaces.nsIBrowserSearchService);
           // We only detect OpenSearch files
           var type = Components.interfaces.nsISearchEngine.DATA_XML;
+          // Select the installed engine if the installation succeeds
+          var installCallback = {
+            onSuccess: engine => this.currentEngine = engine
+          }
           searchService.addEngine(target.getAttribute("uri"), type,
-                                  target.getAttribute("src"), false);
+                                  target.getAttribute("src"), false,
+                                  installCallback);
         }
-        else if (target.engine)
-          this.currentEngine = target.engine;
         else
           return;
 
         this.focus();
         this.select();
       ]]></handler>
 
       <handler event="popupshowing" action="this.rebuildPopupDynamic();"/>
--- a/browser/components/search/test/browser_426329.js
+++ b/browser/components/search/test/browser_426329.js
@@ -25,18 +25,17 @@ function test() {
 
   let testIterator;
 
   function observer(aSub, aTopic, aData) {
     switch (aData) {
       case "engine-added":
         var engine = ss.getEngineByName("Bug 426329");
         ok(engine, "Engine was added.");
-        //XXX Bug 493051
-        //ss.currentEngine = engine;
+        ss.currentEngine = engine;
         break;
       case "engine-current":
         ok(ss.currentEngine.name == "Bug 426329", "currentEngine set");
         testReturn();
         break;
       case "engine-removed":
         Services.obs.removeObserver(observer, "browser-search-engine-modified");
         finish();
--- a/browser/components/search/test/browser_contextmenu.js
+++ b/browser/components/search/test/browser_contextmenu.js
@@ -11,18 +11,17 @@ function test() {
   const ENGINE_NAME = "Foo";
   var contextMenu;
 
   function observer(aSub, aTopic, aData) {
     switch (aData) {
       case "engine-added":
         var engine = ss.getEngineByName(ENGINE_NAME);
         ok(engine, "Engine was added.");
-        //XXX Bug 493051
-        //ss.currentEngine = engine;
+        ss.currentEngine = engine;
         break;
       case "engine-current":
         is(ss.currentEngine.name, ENGINE_NAME, "currentEngine set");
         startTest();
         break;
       case "engine-removed":
         Services.obs.removeObserver(observer, "browser-search-engine-modified");
         finish();
--- a/browser/components/search/test/browser_healthreport.js
+++ b/browser/components/search/test/browser_healthreport.js
@@ -61,17 +61,17 @@ function test() {
             is(day.get(field), oldCount + 1, "Performing a search increments FHR count by 1.");
 
             let engine = Services.search.getEngineByName("Foo");
             Services.search.removeEngine(engine);
           });
         }
 
         EventUtils.synthesizeKey("VK_RETURN", {});
-        executeSoon(afterSearch);
+        executeSoon(() => executeSoon(afterSearch));
       });
     });
   }
 
   function observer(subject, topic, data) {
     switch (data) {
       case "engine-added":
         let engine = Services.search.getEngineByName("Foo");
--- a/browser/components/search/test/browser_private_search_perwindowpb.js
+++ b/browser/components/search/test/browser_private_search_perwindowpb.js
@@ -37,30 +37,28 @@ function test() {
     onPageLoad(aWin, aCallback);
 
     searchBar.value = aIsPrivate ? "private test" : "public test";
     searchBar.focus();
     EventUtils.synthesizeKey("VK_RETURN", {}, aWin);
   }
 
   function addEngine(aCallback) {
-    function observer(aSub, aTopic, aData) {
-      switch (aData) {
-        case "engine-current":
-          ok(Services.search.currentEngine.name == "Bug 426329",
-             "currentEngine set");
-          aCallback();
-          break;
+    let installCallback = {
+      onSuccess: function (engine) {
+        Services.search.currentEngine = engine;
+        aCallback();
+      },
+      onError: function (errorCode) {
+        ok(false, "failed to install engine: " + errorCode);
       }
-    }
-
-    Services.obs.addObserver(observer, "browser-search-engine-modified", false);
-    Services.search.addEngine(
-      engineURL + "426329.xml", Ci.nsISearchEngine.DATA_XML,
-      "data:image/x-icon,%00", false);
+    };
+    Services.search.addEngine(engineURL + "426329.xml",
+                              Ci.nsISearchEngine.DATA_XML,
+                              "data:image/x-icon,%00", false, installCallback);
   }
 
   function testOnWindow(aIsPrivate, aCallback) {
     let win = OpenBrowserWindow({ private: aIsPrivate });
     waitForFocus(function() {
       windowsToClose.push(win);
       executeSoon(function() aCallback(win));
     }, win);
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -489,33 +489,26 @@ let SessionStoreInternal = {
 
     XPCOMUtils.defineLazyGetter(this, "_max_windows_undo", function () {
       this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true);
       return this._prefBranch.getIntPref("sessionstore.max_windows_undo");
     });
   },
 
   _initWindow: function ssi_initWindow(aWindow) {
-    if (!aWindow || this._loadState == STATE_RUNNING) {
-      // make sure that all browser windows which try to initialize
-      // SessionStore are really tracked by it
-      if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi]))
-        this.onLoad(aWindow);
+    if (aWindow) {
+      this.onLoad(aWindow);
+    } else if (this._loadState == STATE_STOPPED) {
       // If init is being called with a null window, it's possible that we
       // just want to tell sessionstore that a session is live (as is the case
       // with starting Firefox with -private, for example; see bug 568816),
       // so we should mark the load state as running to make sure that
       // things like setBrowserState calls will succeed in restoring the session.
-      if (!aWindow && this._loadState == STATE_STOPPED)
-        this._loadState = STATE_RUNNING;
-      return;
+      this._loadState = STATE_RUNNING;
     }
-
-    // As this is called at delayedStartup, restoration must be initiated here
-    this.onLoad(aWindow);
   },
 
   /**
    * Start tracking a window.
    *
    * This function also initializes the component if it is not
    * initialized yet.
    */
@@ -742,17 +735,17 @@ let SessionStoreInternal = {
     // a single private one. Let's restore the session we actually wanted to
     // restore at startup.
     else if (this._deferredInitialState && !isPrivateWindow &&
              aWindow.toolbar.visible) {
 
       this._deferredInitialState._firstTabs = true;
       this._restoreCount = this._deferredInitialState.windows ?
         this._deferredInitialState.windows.length : 0;
-      this.restoreWindow(aWindow, this._deferredInitialState, true);
+      this.restoreWindow(aWindow, this._deferredInitialState, false);
       this._deferredInitialState = null;
     }
     else if (this._restoreLastWindow && aWindow.toolbar.visible &&
              this._closedWindows.length && !isPrivateWindow) {
 
       // default to the most-recently closed window
       // don't use popup windows
       let closedWindowState = null;
--- a/browser/components/tabview/groupitems.js
+++ b/browser/components/tabview/groupitems.js
@@ -1915,16 +1915,17 @@ let GroupItems = {
   _arrangePaused: false,
   _arrangesPending: [],
   _removingHiddenGroups: false,
   _delayedModUpdates: [],
   _autoclosePaused: false,
   minGroupHeight: 110,
   minGroupWidth: 125,
   _lastActiveList: null,
+  _lastGroupToUpdateTabBar: null,
 
   // ----------
   // Function: toString
   // Prints [GroupItems] for debug use
   toString: function GroupItems_toString() {
     return "[GroupItems count=" + this.groupItems.length + "]";
   },
 
@@ -2280,16 +2281,20 @@ let GroupItems = {
     if (groupItem == this._activeGroupItem)
       this._activeGroupItem = null;
 
     this._arrangesPending = this._arrangesPending.filter(function (pending) {
       return groupItem != pending.groupItem;
     });
 
     this._lastActiveList.remove(groupItem);
+
+    if (this._lastGroupToUpdateTabBar == groupItem)
+      this._lastGroupToUpdateTabBar = null;
+
     UI.updateTabButton();
   },
 
   // ----------
   // Function: groupItem
   // Given some sort of identifier, returns the appropriate groupItem.
   // Currently only supports groupItem ids.
   groupItem: function GroupItems_groupItem(a) {
@@ -2413,18 +2418,23 @@ let GroupItems = {
   // Function: _updateTabBar
   // Hides and shows tabs in the tab bar based on the active groupItem
   _updateTabBar: function GroupItems__updateTabBar() {
     if (!window.UI)
       return; // called too soon
 
     Utils.assert(this._activeGroupItem, "There must be something to show in the tab bar!");
 
+    // Update list of visible tabs only once after switching to another group.
+    if (this._activeGroupItem == this._lastGroupToUpdateTabBar)
+      return;
+
     let tabItems = this._activeGroupItem._children;
     gBrowser.showOnlyTheseTabs(tabItems.map(function(item) item.tab));
+    this._lastGroupToUpdateTabBar = this._activeGroupItem;
   },
 
   // ----------
   // Function: updateActiveGroupItemAndTabBar
   // Sets active TabItem and GroupItem, and updates tab bar appropriately.
   // Parameters:
   // tabItem - the tab item
   // options - is passed to UI.setActive() directly
@@ -2532,30 +2542,30 @@ let GroupItems = {
       return;
 
     Utils.assertThrow(tab._tabViewTabItem, "tab must be linked to a TabItem");
 
     // given tab is already contained in target group
     if (tab._tabViewTabItem.parent && tab._tabViewTabItem.parent.id == groupItemId)
       return;
 
-    let shouldUpdateTabBar = false;
+    let shouldHideTab = false;
     let shouldShowTabView = false;
     let groupItem;
 
     // switch to the appropriate tab first.
     if (tab.selected) {
       if (gBrowser.visibleTabs.length > 1) {
         gBrowser._blurTab(tab);
-        shouldUpdateTabBar = true;
+        shouldHideTab = true;
       } else {
         shouldShowTabView = true;
       }
     } else {
-      shouldUpdateTabBar = true
+      shouldHideTab = true;
     }
 
     // remove tab item from a groupItem
     if (tab._tabViewTabItem.parent)
       tab._tabViewTabItem.parent.remove(tab._tabViewTabItem);
 
     // add tab item to a groupItem
     if (groupItemId) {
@@ -2568,18 +2578,18 @@ let GroupItems = {
 
       let box = new Rect(pageBounds);
       box.width = 250;
       box.height = 200;
 
       new GroupItem([ tab._tabViewTabItem ], { bounds: box, immediately: true });
     }
 
-    if (shouldUpdateTabBar)
-      this._updateTabBar();
+    if (shouldHideTab)
+      gBrowser.hideTab(tab);
     else if (shouldShowTabView)
       UI.showTabView();
   },
 
   // ----------
   // Function: removeHiddenGroups
   // Removes all hidden groups' data and its browser tabs.
   removeHiddenGroups: function GroupItems_removeHiddenGroups() {
--- a/browser/components/tabview/test/browser_tabview_bug624265_perwindowpb.js
+++ b/browser/components/tabview/test/browser_tabview_bug624265_perwindowpb.js
@@ -94,73 +94,37 @@ function test() {
             assertOneSingleGroupItem(aWindow);
             next(aWindow);
           }, aWindow);
         }, aWindow);
       }, aWindow);
     }, aWindow);
   }
 
-  // [624102] check state after return from private browsing
-  let testPrivateBrowsing = function (aWindow) {
-    aWindow.gBrowser.loadOneTab('http://mochi.test:8888/#1', {inBackground: true});
-    aWindow.gBrowser.loadOneTab('http://mochi.test:8888/#2', {inBackground: true});
-
-    let cw = getContentWindow(aWindow);
-    let box = new cw.Rect(20, 20, 250, 200);
-    let groupItem = new cw.GroupItem([], {bounds: box, immediately: true});
-    cw.UI.setActive(groupItem);
-
-    aWindow.gBrowser.selectedTab = aWindow.gBrowser.loadOneTab('http://mochi.test:8888/#3', {inBackground: true});
-    aWindow.gBrowser.loadOneTab('http://mochi.test:8888/#4', {inBackground: true});
-
-    afterAllTabsLoaded(function () {
-      assertNumberOfVisibleTabs(aWindow, 2);
-
-      enterAndLeavePrivateBrowsing(function () {
-        assertNumberOfVisibleTabs(aWindow, 2);
-        aWindow.gBrowser.selectedTab = aWindow.gBrowser.tabs[0];
-        closeGroupItem(cw.GroupItems.groupItems[1], function() {
-          next(aWindow);
-        });
-      });
-    }, aWindow);
-  }
-
-  function testOnWindow(aIsPrivate, aCallback) {
-    let win = OpenBrowserWindow({private: aIsPrivate});
+  function testOnWindow(aCallback) {
+    let win = OpenBrowserWindow({private: false});
     win.addEventListener("load", function onLoad() {
       win.removeEventListener("load", onLoad, false);
       executeSoon(function() { aCallback(win) });
     }, false);
   }
 
-  function enterAndLeavePrivateBrowsing(callback) {
-    testOnWindow(true, function (aWindow) {
-      aWindow.close();
-      callback();
-    });
-  }
-
   waitForExplicitFinish();
 
   // Tests for #624265
   tests.push(testUndoCloseTabs);
 
   // Tests for #623792
   tests.push(testDuplicateTab);
   tests.push(testBackForwardDuplicateTab);
 
-  // Tests for #624102
-  tests.push(testPrivateBrowsing);
-
-  testOnWindow(false, function(aWindow) {
+  testOnWindow(function(aWindow) {
     loadTabView(function() {
       next(aWindow);
     }, aWindow);
   });
 }
 
 function loadTabView(callback, aWindow) {
   showTabView(function () {
     hideTabView(callback, aWindow);
   }, aWindow);
-}
\ No newline at end of file
+}
--- a/browser/config/mozconfigs/linux32/debug
+++ b/browser/config/mozconfigs/linux32/debug
@@ -1,12 +1,11 @@
 ac_add_options --enable-debug
 ac_add_options --enable-trace-malloc
 ac_add_options --enable-signmar
-ENABLE_MARIONETTE=1
 
 . $topsrcdir/build/unix/mozconfig.linux32
 
 # Avoid dependency on libstdc++ 4.5
 ac_add_options --enable-stdcxx-compat
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
--- a/browser/config/mozconfigs/linux64/debug
+++ b/browser/config/mozconfigs/linux64/debug
@@ -1,12 +1,11 @@
 ac_add_options --enable-debug
 ac_add_options --enable-trace-malloc
 ac_add_options --enable-signmar
-ENABLE_MARIONETTE=1
 
 . $topsrcdir/build/unix/mozconfig.linux
 
 # Avoid dependency on libstdc++ 4.5
 ac_add_options --enable-stdcxx-compat
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
--- a/browser/config/mozconfigs/macosx64/debug
+++ b/browser/config/mozconfigs/macosx64/debug
@@ -1,15 +1,14 @@
 . $topsrcdir/build/macosx/mozconfig.common
 
 ac_add_options --enable-debug
 ac_add_options --enable-trace-malloc
 ac_add_options --enable-accessibility
 ac_add_options --enable-signmar
-ENABLE_MARIONETTE=1
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
 ac_add_options --with-macbundlename-prefix=Firefox
 
 # Treat warnings as errors in directories with FAIL_ON_WARNINGS.
 ac_add_options --enable-warnings-as-errors
--- a/browser/config/mozconfigs/win32/debug
+++ b/browser/config/mozconfigs/win32/debug
@@ -1,17 +1,15 @@
 . "$topsrcdir/browser/config/mozconfigs/common"
 
 ac_add_options --enable-debug
 ac_add_options --enable-trace-malloc
 ac_add_options --enable-signmar
 ac_add_options --enable-metro
 
-ENABLE_MARIONETTE=1
-
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
 if test -z "${_PYMAKE}"; then
   mk_add_options MOZ_MAKE_FLAGS=-j1
 fi
 
 if test "$PROCESSOR_ARCHITECTURE" = "AMD64" -o "$PROCESSOR_ARCHITEW6432" = "AMD64"; then
--- a/browser/config/mozconfigs/win64/debug
+++ b/browser/config/mozconfigs/win64/debug
@@ -2,17 +2,16 @@
 
 ac_add_options --target=x86_64-pc-mingw32
 ac_add_options --host=x86_64-pc-mingw32
 
 ac_add_options --enable-debug
 ac_add_options --enable-trace-malloc
 ac_add_options --enable-signmar
 ac_add_options --enable-metro
-ENABLE_MARIONETTE=1
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
 if test -z "${_PYMAKE}"; then
   mk_add_options MOZ_MAKE_FLAGS=-j1
 fi
 
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -19,16 +19,17 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
 Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
 Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
 Cu.import("resource:///modules/source-editor.jsm");
 Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource:///modules/devtools/BreadcrumbsWidget.jsm");
 Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
 Cu.import("resource:///modules/devtools/VariablesView.jsm");
+Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Parser",
   "resource:///modules/devtools/Parser.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper",
   "resource://gre/modules/devtools/NetworkHelper.jsm");
 
@@ -68,16 +69,34 @@ let DebuggerController = {
     this._isInitialized = true;
     window.removeEventListener("DOMContentLoaded", this.startupDebugger, true);
 
     let deferred = this._startup = Promise.defer();
 
     DebuggerView.initialize(() => {
       DebuggerView._isInitialized = true;
 
+      VariablesViewController.attach(DebuggerView.Variables, {
+        getGripClient: aObject => {
+          return this.activeThread.pauseGrip(aObject);
+        }
+      });
+
+      // Relay events from the VariablesView.
+      DebuggerView.Variables.on("fetched", (aEvent, aType) => {
+        switch (aType) {
+          case "variables":
+            window.dispatchEvent(document, "Debugger:FetchedVariables");
+            break;
+          case "properties":
+            window.dispatchEvent(document, "Debugger:FetchedProperties");
+            break;
+        }
+      });
+
       // Chrome debugging needs to initiate the connection by itself.
       if (window._isChromeDebugger) {
         this.connect().then(deferred.resolve);
       } else {
         deferred.resolve();
       }
     });
 
@@ -398,29 +417,27 @@ ThreadState.prototype = {
     DebuggerView.Toolbar.toggleResumeButtonState(this.activeThread.state);
 
     if (DebuggerController._target && (aEvent == "paused" || aEvent == "resumed")) {
       DebuggerController._target.emit("thread-" + aEvent);
     }
   }
 };
 
+
 /**
  * Keeps the stack frame list up-to-date, using the thread client's
  * stack frame cache.
  */
 function StackFrames() {
   this._onPaused = this._onPaused.bind(this);
   this._onResumed = this._onResumed.bind(this);
   this._onFrames = this._onFrames.bind(this);
   this._onFramesCleared = this._onFramesCleared.bind(this);
   this._afterFramesCleared = this._afterFramesCleared.bind(this);
-  this._fetchScopeVariables = this._fetchScopeVariables.bind(this);
-  this._fetchVarProperties = this._fetchVarProperties.bind(this);
-  this._addVarExpander = this._addVarExpander.bind(this);
   this.evaluate = this.evaluate.bind(this);
 }
 
 StackFrames.prototype = {
   get activeThread() DebuggerController.activeThread,
   autoScopeExpand: false,
   currentFrame: null,
   syncedWatchExpressions: null,
@@ -583,17 +600,22 @@ StackFrames.prototype = {
 
     // Make sure the debugger view panes are visible.
     DebuggerView.showInstrumentsPane();
 
     // Make sure all the previous stackframes are removed before re-adding them.
     DebuggerView.StackFrames.empty();
 
     for (let frame of this.activeThread.cachedFrames) {
-      this._addFrame(frame);
+      let depth = frame.depth;
+      let { url, line } = frame.where;
+      let frameLocation = NetworkHelper.convertToUnicode(unescape(url));
+      let frameTitle = StackFrameUtils.getFrameTitle(frame);
+
+      DebuggerView.StackFrames.addFrame(frameTitle, frameLocation, line, depth);
     }
     if (this.currentFrame == null) {
       DebuggerView.StackFrames.selectedDepth = 0;
     }
     if (this.activeThread.moreFrames) {
       DebuggerView.StackFrames.dirty = true;
     }
   },
@@ -656,16 +678,17 @@ StackFrames.prototype = {
     DebuggerView.Sources.highlightBreakpoint(url, line);
     // Don't display the watch expressions textbox inputs in the pane.
     DebuggerView.WatchExpressions.toggleContents(false);
     // Start recording any added variables or properties in any scope.
     DebuggerView.Variables.createHierarchy();
     // Clear existing scopes and create each one dynamically.
     DebuggerView.Variables.empty();
 
+
     // If watch expressions evaluation results are available, create a scope
     // to contain all the values.
     if (this.syncedWatchExpressions && watchExpressionsEvaluation) {
       let label = L10N.getStr("watchExpressionsScopeLabel");
       let scope = DebuggerView.Variables.addScope(label);
 
       // Customize the scope for holding watch expressions evaluations.
       scope.descriptorTooltip = false;
@@ -679,80 +702,39 @@ StackFrames.prototype = {
       this._fetchWatchExpressions(scope, watchExpressionsEvaluation);
       scope.expand();
     }
 
     do {
       // Create a scope to contain all the inspected variables.
       let label = StackFrameUtils.getScopeLabel(environment);
       let scope = DebuggerView.Variables.addScope(label);
+      let innermost = environment == frame.environment;
 
-      // Handle additions to the innermost scope.
-      if (environment == frame.environment) {
+      // Handle special additions to the innermost scope.
+      if (innermost) {
         this._insertScopeFrameReferences(scope, frame);
-        this._addScopeExpander(scope, environment);
-        // Always expand the innermost scope by default.
+      }
+
+      DebuggerView.Variables.controller.addExpander(scope, environment);
+
+      // The innermost scope is always automatically expanded, because it
+      // contains the variables in the current stack frame which are likely to
+      // be inspected.
+      if (innermost || this.autoScopeExpand) {
         scope.expand();
       }
-      // Lazily add nodes for every other environment scope.
-      else {
-        this._addScopeExpander(scope, environment);
-        this.autoScopeExpand && scope.expand();
-      }
     } while ((environment = environment.parent));
 
     // Signal that variables have been fetched.
     window.dispatchEvent(document, "Debugger:FetchedVariables");
     DebuggerView.Variables.commitHierarchy();
   },
 
   /**
-   * Adds an 'onexpand' callback for a scope, lazily handling
-   * the addition of new variables.
-   *
-   * @param Scope aScope
-   *        The scope where the variables will be placed into.
-   * @param object aEnv
-   *        The scope's environment.
-   */
-  _addScopeExpander: function(aScope, aEnv) {
-    aScope._sourceEnvironment = aEnv;
-
-    // It's a good idea to be prepared in case of an expansion.
-    aScope.addEventListener("mouseover", this._fetchScopeVariables, false);
-    // Make sure that variables are always available on expansion.
-    aScope.onexpand = this._fetchScopeVariables;
-  },
-
-  /**
-   * Adds an 'onexpand' callback for a variable, lazily handling
-   * the addition of new properties.
-   *
-   * @param Variable aVar
-   *        The variable where the properties will be placed into.
-   * @param any aGrip
-   *        The grip of the variable.
-   */
-  _addVarExpander: function(aVar, aGrip) {
-    // No need for expansion for primitive values.
-    if (VariablesView.isPrimitive({ value: aGrip })) {
-      return;
-    }
-    aVar._sourceGrip = aGrip;
-
-    // Some variables are likely to contain a very large number of properties.
-    // It's a good idea to be prepared in case of an expansion.
-    if (aVar.name == "window" || aVar.name == "this") {
-      aVar.addEventListener("mouseover", this._fetchVarProperties, false);
-    }
-    // Make sure that properties are always available on expansion.
-    aVar.onexpand = this._fetchVarProperties;
-  },
-
-  /**
    * Adds the watch expressions evaluation results to a scope in the view.
    *
    * @param Scope aScope
    *        The scope where the watch expressions will be placed into.
    * @param object aExp
    *        The grip of the evaluation results.
    */
   _fetchWatchExpressions: function(aScope, aExp) {
@@ -765,234 +747,56 @@ StackFrames.prototype = {
     // Add nodes for every watch expression in scope.
     this.activeThread.pauseGrip(aExp).getPrototypeAndProperties((aResponse) => {
       let ownProperties = aResponse.ownProperties;
       let totalExpressions = DebuggerView.WatchExpressions.itemCount;
 
       for (let i = 0; i < totalExpressions; i++) {
         let name = DebuggerView.WatchExpressions.getExpression(i);
         let expVal = ownProperties[i].value;
-        let expRef = aScope.addVar(name, ownProperties[i]);
-        this._addVarExpander(expRef, expVal);
+        let expRef = aScope.addItem(name, ownProperties[i]);
+        DebuggerView.Variables.controller.addExpander(expRef, expVal);
 
         // Revert some of the custom watch expressions scope presentation flags.
         expRef.switch = null;
         expRef.delete = null;
         expRef.descriptorTooltip = true;
         expRef.separatorStr = L10N.getStr("variablesSeparatorLabel");
       }
 
       // Signal that watch expressions have been fetched.
       window.dispatchEvent(document, "Debugger:FetchedWatchExpressions");
       DebuggerView.Variables.commitHierarchy();
     });
   },
 
   /**
-   * Adds variables to a scope in the view. Triggered when a scope is
-   * expanded or is hovered. It does not expand the scope.
-   *
-   * @param Scope aScope
-   *        The scope where the variables will be placed into.
-   */
-  _fetchScopeVariables: function(aScope) {
-    // Fetch the variables only once.
-    if (aScope._fetched) {
-      return;
-    }
-    aScope._fetched = true;
-    let env = aScope._sourceEnvironment;
-
-    switch (env.type) {
-      case "with":
-      case "object":
-        // Add nodes for every variable in scope.
-        this.activeThread.pauseGrip(env.object).getPrototypeAndProperties((aResponse) => {
-          let { ownProperties, safeGetterValues } = aResponse;
-          this._mergeSafeGetterValues(ownProperties, safeGetterValues);
-          this._insertScopeVariables(ownProperties, aScope);
-
-          // Signal that variables have been fetched.
-          window.dispatchEvent(document, "Debugger:FetchedVariables");
-          DebuggerView.Variables.commitHierarchy();
-        });
-        break;
-      case "block":
-      case "function":
-        // Add nodes for every argument and every other variable in scope.
-        this._insertScopeArguments(env.bindings.arguments, aScope);
-        this._insertScopeVariables(env.bindings.variables, aScope);
-
-        // No need to signal that variables have been fetched, since
-        // the scope arguments and variables are already attached to the
-        // environment bindings, so pausing the active thread is unnecessary.
-        break;
-      default:
-        Cu.reportError("Unknown Debugger.Environment type: " + env.type);
-        break;
-    }
-  },
-
-  /**
    * Add nodes for special frame references in the innermost scope.
    *
    * @param Scope aScope
    *        The scope where the references will be placed into.
    * @param object aFrame
    *        The frame to get some references from.
    */
   _insertScopeFrameReferences: function(aScope, aFrame) {
     // Add any thrown exception.
     if (this.currentException) {
-      let excRef = aScope.addVar("<exception>", { value: this.currentException });
-      this._addVarExpander(excRef, this.currentException);
+      let excRef = aScope.addItem("<exception>", { value: this.currentException });
+      DebuggerView.Variables.controller.addExpander(excRef, this.currentException);
     }
     // Add any returned value.
     if (this.currentReturnedValue) {
-      let retRef = aScope.addVar("<return>", { value: this.currentReturnedValue });
-      this._addVarExpander(retRef, this.currentReturnedValue);
+      let retRef = aScope.addItem("<return>", { value: this.currentReturnedValue });
+      DebuggerView.Variables.controller.addExpander(retRef, this.currentReturnedValue);
     }
     // Add "this".
     if (aFrame.this) {
-      let thisRef = aScope.addVar("this", { value: aFrame.this });
-      this._addVarExpander(thisRef, aFrame.this);
-    }
-  },
-
-  /**
-   * Add nodes for every argument in scope.
-   *
-   * @param object aArguments
-   *        The map of names to arguments, as specified in the protocol.
-   * @param Scope aScope
-   *        The scope where the nodes will be placed into.
-   */
-  _insertScopeArguments: function(aArguments, aScope) {
-    if (!aArguments) {
-      return;
-    }
-    for (let argument of aArguments) {
-      let name = Object.getOwnPropertyNames(argument)[0];
-      let argRef = aScope.addVar(name, argument[name]);
-      let argVal = argument[name].value;
-      this._addVarExpander(argRef, argVal);
-    }
-  },
-
-  /**
-   * Add nodes for every variable in scope.
-   *
-   * @param object aVariables
-   *        The map of names to variables, as specified in the protocol.
-   * @param Scope aScope
-   *        The scope where the nodes will be placed into.
-   */
-  _insertScopeVariables: function(aVariables, aScope) {
-    if (!aVariables) {
-      return;
-    }
-    let variableNames = Object.keys(aVariables);
-
-    // Sort all of the variables before adding them, if preferred.
-    if (Prefs.variablesSortingEnabled) {
-      variableNames.sort();
-    }
-    // Add the variables to the specified scope.
-    for (let name of variableNames) {
-      let varRef = aScope.addVar(name, aVariables[name]);
-      let varVal = aVariables[name].value;
-      this._addVarExpander(varRef, varVal);
-    }
-  },
-
-  /**
-   * Adds properties to a variable in the view. Triggered when a variable is
-   * expanded or certain variables are hovered. It does not expand the variable.
-   *
-   * @param Variable aVar
-   *        The variable where the properties will be placed into.
-   */
-  _fetchVarProperties: function(aVar) {
-    // Fetch the properties only once.
-    if (aVar._fetched) {
-      return;
+      let thisRef = aScope.addItem("this", { value: aFrame.this });
+      DebuggerView.Variables.controller.addExpander(thisRef, aFrame.this);
     }
-    aVar._fetched = true;
-    let grip = aVar._sourceGrip;
-
-    this.activeThread.pauseGrip(grip).getPrototypeAndProperties((aResponse) => {
-      let { ownProperties, prototype, safeGetterValues } = aResponse;
-      let sortable = VariablesView.NON_SORTABLE_CLASSES.indexOf(grip.class) == -1;
-
-      this._mergeSafeGetterValues(ownProperties, safeGetterValues);
-
-      // Add all the variable properties.
-      if (ownProperties) {
-        aVar.addProperties(ownProperties, {
-          // Not all variables need to force sorted properties.
-          sorted: sortable,
-          // Expansion handlers must be set after the properties are added.
-          callback: this._addVarExpander
-        });
-      }
-
-      // Add the variable's __proto__.
-      if (prototype && prototype.type != "null") {
-        aVar.addProperty("__proto__", { value: prototype });
-        // Expansion handlers must be set after the properties are added.
-        this._addVarExpander(aVar.get("__proto__"), prototype);
-      }
-
-      // Mark the variable as having retrieved all its properties.
-      aVar._retrieved = true;
-
-      // Signal that properties have been fetched.
-      window.dispatchEvent(document, "Debugger:FetchedProperties");
-      DebuggerView.Variables.commitHierarchy();
-    });
-  },
-
-  /**
-   * Merge the safe getter values descriptors into the "own properties" object
-   * that comes from a "prototypeAndProperties" response packet. This is needed
-   * for Variables View.
-   *
-   * @private
-   * @param object aOwnProperties
-   *        The |ownProperties| object that will get the new safe getter values.
-   * @param object aSafeGetterValues
-   *        The |safeGetterValues| object.
-   */
-  _mergeSafeGetterValues: function(aOwnProperties, aSafeGetterValues) {
-    // Merge the safe getter values into one object such that we can use it
-    // in VariablesView.
-    for (let name of Object.keys(aSafeGetterValues)) {
-      if (name in aOwnProperties) {
-        aOwnProperties[name].getterValue = aSafeGetterValues[name].getterValue;
-        aOwnProperties[name].getterPrototypeLevel =
-          aSafeGetterValues[name].getterPrototypeLevel;
-      } else {
-        aOwnProperties[name] = aSafeGetterValues[name];
-      }
-    }
-  },
-
-  /**
-   * Adds the specified stack frame to the list.
-   *
-   * @param object aFrame
-   *        The new frame to add.
-   */
-  _addFrame: function(aFrame) {
-    let depth = aFrame.depth;
-    let { url, line } = aFrame.where;
-    let frameLocation = NetworkHelper.convertToUnicode(unescape(url));
-    let frameTitle = StackFrameUtils.getFrameTitle(aFrame);
-
-    DebuggerView.StackFrames.addFrame(frameTitle, frameLocation, line, depth);
   },
 
   /**
    * Loads more stack frames from the debugger server cache.
    */
   addMoreFrames: function() {
     this.activeThread.fillFrames(
       this.activeThread.cachedFrames.length + CALL_STACK_PAGE_SIZE);
--- a/browser/devtools/debugger/test/browser_dbg_bug786070_hide_nonenums.js
+++ b/browser/devtools/debugger/test/browser_dbg_bug786070_hide_nonenums.js
@@ -19,19 +19,19 @@ function test() {
   });
 }
 
 function testNonEnumProperties() {
   gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
     Services.tm.currentThread.dispatch({ run: function() {
 
       let testScope = gDebugger.DebuggerView.Variables.addScope("test-scope");
-      let testVar = testScope.addVar("foo");
+      let testVar = testScope.addItem("foo");
 
-      testVar.addProperties({
+      testVar.addItems({
         foo: {
           value: "bar",
           enumerable: true
         },
         bar: {
           value: "foo",
           enumerable: false
         }
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-03.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-03.js
@@ -19,18 +19,18 @@ function test() {
   });
 }
 
 function testSimpleCall() {
   gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
     Services.tm.currentThread.dispatch({ run: function() {
 
       let testScope = gDebugger.DebuggerView.Variables.addScope("test-scope");
-      let testVar = testScope.addVar("something");
-      let duplVar = testScope.addVar("something");
+      let testVar = testScope.addItem("something");
+      let duplVar = testScope.addItem("something");
 
       info("Scope id: " + testScope.target.id);
       info("Scope name: " + testScope.target.name);
       info("Variable id: " + testVar.target.id);
       info("Variable name: " + testVar.target.name);
 
       ok(testScope,
         "Should have created a scope.");
@@ -56,18 +56,18 @@ function testSimpleCall() {
 
       is(testVar.target.querySelector(".name").getAttribute("value"), "something",
         "Any new variable should have the designated title.");
 
       is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 0,
         "Any new variable should have a details container with no child nodes.");
 
 
-      let properties = testVar.addProperties({ "child": { "value": { "type": "object",
-                                                                     "class": "Object" } } });
+      let properties = testVar.addItems({ "child": { "value": { "type": "object",
+                                                                "class": "Object" } } });
 
 
       ok(!testVar.expanded,
         "Any new created variable should be initially collapsed.");
 
       ok(testVar.visible,
         "Any new created variable should be initially visible.");
 
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-04.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-04.js
@@ -19,36 +19,36 @@ function test() {
   });
 }
 
 function testSimpleCall() {
   gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
     Services.tm.currentThread.dispatch({ run: function() {
 
       let testScope = gDebugger.DebuggerView.Variables.addScope("test");
-      let testVar = testScope.addVar("something");
+      let testVar = testScope.addItem("something");
 
-      let properties = testVar.addProperties({
+      let properties = testVar.addItems({
         "child": {
           "value": {
             "type": "object",
             "class": "Object"
           },
           "enumerable": true
         }
       });
 
       is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 1,
         "A new detail node should have been added in the variable tree.");
 
       ok(testVar.get("child"),
         "The added detail property should be accessible from the variable.");
 
 
-      let properties2 = testVar.get("child").addProperties({
+      let properties2 = testVar.get("child").addItems({
         "grandchild": {
           "value": {
             "type": "object",
             "class": "Object"
           },
           "enumerable": true
         }
       });
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-05.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-05.js
@@ -19,17 +19,17 @@ function test() {
   });
 }
 
 function testSimpleCall() {
   gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
     Services.tm.currentThread.dispatch({ run: function() {
 
       let testScope = gDebugger.DebuggerView.Variables.addScope("test");
-      let testVar = testScope.addVar("something");
+      let testVar = testScope.addItem("something");
 
       testVar.setGrip(1.618);
 
       is(testVar.target.querySelector(".value").getAttribute("value"), "1.618",
         "The grip information for the variable wasn't set correctly.");
 
       is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 0,
         "Adding a value property shouldn't add any new tree nodes.");
@@ -39,42 +39,42 @@ function testSimpleCall() {
 
       is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 0,
         "Adding type and class properties shouldn't add any new tree nodes.");
 
       is(testVar.target.querySelector(".value").getAttribute("value"), "[object Window]",
         "The information for the variable wasn't set correctly.");
 
 
-      testVar.addProperties({ "helloWorld": { "value": "hello world", "enumerable": true } });
+      testVar.addItems({ "helloWorld": { "value": "hello world", "enumerable": true } });
 
       is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 1,
         "A new detail node should have been added in the variable tree.");
 
 
-      testVar.addProperties({ "helloWorld": { "value": "hello jupiter", "enumerable": true } });
+      testVar.addItems({ "helloWorld": { "value": "hello jupiter", "enumerable": true } });
 
       is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 1,
         "Shouldn't be able to duplicate nodes added in the variable tree.");
 
 
-      testVar.addProperties({ "someProp0": { "value": "random string", "enumerable": true },
-                              "someProp1": { "value": "another string", "enumerable": true } });
+      testVar.addItems({ "someProp0": { "value": "random string", "enumerable": true },
+                         "someProp1": { "value": "another string", "enumerable": true } });
 
       is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 3,
         "Two new detail nodes should have been added in the variable tree.");
 
 
-      testVar.addProperties({ "someProp2": { "value": { "type": "null" }, "enumerable": true },
-                              "someProp3": { "value": { "type": "undefined" }, "enumerable": true },
-                              "someProp4": {
-                                "value": { "type": "object", "class": "Object" },
-                                "enumerable": true
-                              }
-                            });
+      testVar.addItems({ "someProp2": { "value": { "type": "null" }, "enumerable": true },
+                         "someProp3": { "value": { "type": "undefined" }, "enumerable": true },
+                         "someProp4": {
+                           "value": { "type": "object", "class": "Object" },
+                           "enumerable": true
+                         }
+                       });
 
       is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 6,
         "Three new detail nodes should have been added in the variable tree.");
 
 
       gDebugger.DebuggerController.activeThread.resume(function() {
         closeDebuggerAndFinish();
       });
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-06.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-06.js
@@ -21,63 +21,63 @@ function test() {
 
 function testSimpleCall() {
   gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
     Services.tm.currentThread.dispatch({ run: function() {
 
       let globalScope = gDebugger.DebuggerView.Variables.addScope("Test-Global");
       let localScope = gDebugger.DebuggerView.Variables.addScope("Test-Local");
 
-      let windowVar = globalScope.addVar("window");
-      let documentVar = globalScope.addVar("document");
-      let localVar0 = localScope.addVar("localVariable");
-      let localVar1 = localScope.addVar("localVar1");
-      let localVar2 = localScope.addVar("localVar2");
-      let localVar3 = localScope.addVar("localVar3");
-      let localVar4 = localScope.addVar("localVar4");
-      let localVar5 = localScope.addVar("localVar5");
+      let windowVar = globalScope.addItem("window");
+      let documentVar = globalScope.addItem("document");
+      let localVar0 = localScope.addItem("localVariable");
+      let localVar1 = localScope.addItem("localVar1");
+      let localVar2 = localScope.addItem("localVar2");
+      let localVar3 = localScope.addItem("localVar3");
+      let localVar4 = localScope.addItem("localVar4");
+      let localVar5 = localScope.addItem("localVar5");
 
       localVar0.setGrip(42);
       localVar1.setGrip(true);
       localVar2.setGrip("nasu");
 
       localVar3.setGrip({ "type": "undefined" });
       localVar4.setGrip({ "type": "null" });
       localVar5.setGrip({ "type": "object", "class": "Object" });
 
-      localVar5.addProperties({ "someProp0": { "value": 42, "enumerable": true },
-                                "someProp1": { "value": true , "enumerable": true},
-                                "someProp2": { "value": "nasu", "enumerable": true},
-                                "someProp3": { "value": { "type": "undefined" }, "enumerable": true},
-                                "someProp4": { "value": { "type": "null" }, "enumerable": true },
-                                "someProp5": {
-                                  "value": { "type": "object", "class": "Object" },
-                                  "enumerable": true
-                                }
-                              });
+      localVar5.addItems({ "someProp0": { "value": 42, "enumerable": true },
+                           "someProp1": { "value": true , "enumerable": true},
+                           "someProp2": { "value": "nasu", "enumerable": true},
+                           "someProp3": { "value": { "type": "undefined" }, "enumerable": true},
+                           "someProp4": { "value": { "type": "null" }, "enumerable": true },
+                           "someProp5": {
+                             "value": { "type": "object", "class": "Object" },
+                             "enumerable": true
+                           }
+                         });
 
-      localVar5.get("someProp5").addProperties({ "someProp0": { "value": 42, "enumerable": true },
-                                                 "someProp1": { "value": true, "enumerable": true },
-                                                 "someProp2": { "value": "nasu", "enumerable": true },
-                                                 "someProp3": { "value": { "type": "undefined" }, "enumerable": true },
-                                                 "someProp4": { "value": { "type": "null" }, "enumerable": true },
-                                                 "someAccessor": { "get": { "type": "object", "class": "Function" },
-                                                                   "set": { "type": "undefined" },
-                                                                   "enumerable": true } });
+      localVar5.get("someProp5").addItems({ "someProp0": { "value": 42, "enumerable": true },
+                                            "someProp1": { "value": true, "enumerable": true },
+                                            "someProp2": { "value": "nasu", "enumerable": true },
+                                            "someProp3": { "value": { "type": "undefined" }, "enumerable": true },
+                                            "someProp4": { "value": { "type": "null" }, "enumerable": true },
+                                            "someAccessor": { "get": { "type": "object", "class": "Function" },
+                                                              "set": { "type": "undefined" }, "enumerable": true }
+                                          });
 
       windowVar.setGrip({ "type": "object", "class": "Window" });
-      windowVar.addProperties({ "helloWorld": { "value": "hello world" } });
+      windowVar.addItems({ "helloWorld": { "value": "hello world" } });
 
       documentVar.setGrip({ "type": "object", "class": "HTMLDocument" });
-      documentVar.addProperties({ "onload": { "value": { "type": "null" } },
-                                  "onunload": { "value": { "type": "null" } },
-                                  "onfocus": { "value": { "type": "null" } },
-                                  "onblur": { "value": { "type": "null" } },
-                                  "onclick": { "value": { "type": "null" } },
-                                  "onkeypress": { "value": { "type": "null" } } });
+      documentVar.addItems({ "onload": { "value": { "type": "null" } },
+                             "onunload": { "value": { "type": "null" } },
+                             "onfocus": { "value": { "type": "null" } },
+                             "onblur": { "value": { "type": "null" } },
+                             "onclick": { "value": { "type": "null" } },
+                             "onkeypress": { "value": { "type": "null" } } });
 
 
       ok(windowVar, "The windowVar hasn't been created correctly.");
       ok(documentVar, "The documentVar hasn't been created correctly.");
       ok(localVar0, "The localVar0 hasn't been created correctly.");
       ok(localVar1, "The localVar1 hasn't been created correctly.");
       ok(localVar2, "The localVar2 hasn't been created correctly.");
       ok(localVar3, "The localVar3 hasn't been created correctly.");
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-data.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-data.js
@@ -70,21 +70,21 @@ function testVariablesView()
   testHierarchy();
   testHeader();
   testFirstLevelContents();
   testSecondLevelContents();
   testThirdLevelContents();
   testIntegrity(arr, obj);
 
   let fooScope = gVariablesView.addScope("foo");
-  let anonymousVar = fooScope.addVar();
+  let anonymousVar = fooScope.addItem();
 
   let anonymousScope = gVariablesView.addScope();
-  let barVar = anonymousScope.addVar("bar");
-  let bazProperty = barVar.addProperty("baz");
+  let barVar = anonymousScope.addItem("bar");
+  let bazProperty = barVar.addItem("baz");
 
   testAnonymousHeaders(fooScope, anonymousVar, anonymousScope, barVar, bazProperty);
   testPropertyInheritance(fooScope, anonymousVar, anonymousScope, barVar, bazProperty);
 
   executeSoon(function() {
     testKeyboardAccessibility(function() {
       testClearHierarchy();
 
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-filter-05.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-05.js
@@ -105,18 +105,19 @@ function testVariablesFiltering()
 
   function test3()
   {
     backspace(3);
 
     is(gSearchBox.value, "*",
       "Searchbox value is incorrect after 3 backspaces");
 
-    is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 3,
-      "There should be 3 variables displayed in the inner scope");
+    // variable count includes `__proto__` for object scopes
+    is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 4,
+      "There should be 4 variables displayed in the inner scope");
     isnot(mathScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be some variables displayed in the math scope");
     isnot(testScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be some variables displayed in the test scope");
     isnot(loadScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be some variables displayed in the load scope");
     isnot(globalScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be some variables displayed in the global scope");
@@ -135,18 +136,19 @@ function testVariablesFiltering()
 
   function test4()
   {
     backspace(1);
 
     is(gSearchBox.value, "",
       "Searchbox value is incorrect after 1 backspace");
 
-    is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 3,
-      "There should be 3 variables displayed in the inner scope");
+    // variable count includes `__proto__` for object scopes
+    is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 4,
+      "There should be 4 variables displayed in the inner scope");
     isnot(mathScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be some variables displayed in the math scope");
     isnot(testScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be some variables displayed in the test scope");
     isnot(loadScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be some variables displayed in the load scope");
     isnot(globalScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be some variables displayed in the global scope");
--- a/browser/devtools/main.js
+++ b/browser/devtools/main.js
@@ -153,17 +153,17 @@ Tools.jsprofiler = {
   accesskey: l10n("profiler.accesskey", profilerStrings),
   key: l10n("profiler2.commandkey", profilerStrings),
   ordinal: 5,
   modifiers: "shift",
   visibilityswitch: "devtools.profiler.enabled",
   icon: "chrome://browser/skin/devtools/tool-profiler.png",
   url: "chrome://browser/content/devtools/profiler.xul",
   label: l10n("profiler.label", profilerStrings),
-  tooltip: l10n("profiler.tooltip", profilerStrings),
+  tooltip: l10n("profiler.tooltip2", profilerStrings),
 
   isTargetSupported: function (target) {
     return true;
   },
 
   build: function (frame, target) {
     let panel = new ProfilerPanel(frame, target);
     return panel.open();
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -1441,17 +1441,17 @@ NetworkDetailsView.prototype = {
   _addHeaders: function(aName, aResponse) {
     let kb = aResponse.headersSize / 1024;
     let size = L10N.numberWithDecimals(kb, HEADERS_SIZE_DECIMALS);
     let text = L10N.getFormatStr("networkMenu.sizeKB", size);
     let headersScope = this._headers.addScope(aName + " (" + text + ")");
     headersScope.expanded = true;
 
     for (let header of aResponse.headers) {
-      let headerVar = headersScope.addVar(header.name, { null: true }, true);
+      let headerVar = headersScope.addItem(header.name, { null: true }, true);
       gNetwork.getString(header.value).then((aString) => headerVar.setGrip(aString));
     }
   },
 
   /**
    * Sets the network request cookies shown in this view.
    *
    * @param object aResponse
@@ -1484,17 +1484,17 @@ NetworkDetailsView.prototype = {
    * @param object aResponse
    *        The message received from the server.
    */
   _addCookies: function(aName, aResponse) {
     let cookiesScope = this._cookies.addScope(aName);
     cookiesScope.expanded = true;
 
     for (let cookie of aResponse.cookies) {
-      let cookieVar = cookiesScope.addVar(cookie.name, { null: true }, true);
+      let cookieVar = cookiesScope.addItem(cookie.name, { null: true }, true);
       gNetwork.getString(cookie.value).then((aString) => cookieVar.setGrip(aString));
 
       // By default the cookie name and value are shown. If this is the only
       // information available, then nothing else is to be displayed.
       let cookieProps = Object.keys(cookie);
       if (cookieProps.length == 2) {
         continue;
       }
@@ -1586,17 +1586,17 @@ NetworkDetailsView.prototype = {
         name: NetworkHelper.convertToUnicode(unescape(param[0])),
         value: NetworkHelper.convertToUnicode(unescape(param[1]))
       });
 
     let paramsScope = this._params.addScope(aName);
     paramsScope.expanded = true;
 
     for (let param of paramsArray) {
-      let headerVar = paramsScope.addVar(param.name, { null: true }, true);
+      let headerVar = paramsScope.addItem(param.name, { null: true }, true);
       headerVar.setGrip(param.value);
     }
   },
 
   /**
    * Sets the network response body shown in this view.
    *
    * @param string aUrl
@@ -1629,17 +1629,17 @@ NetworkDetailsView.prototype = {
         // Valid JSON.
         if (jsonObject) {
           $("#response-content-json-box").hidden = false;
           let jsonScopeName = callbackPadding
             ? L10N.getFormatStr("jsonpScopeName", callbackPadding[0].slice(0, -1))
             : L10N.getStr("jsonScopeName");
 
           let jsonScope = this._json.addScope(jsonScopeName);
-          jsonScope.addVar().populate(jsonObject, { expanded: true });
+          jsonScope.addItem().populate(jsonObject, { expanded: true });
           jsonScope.expanded = true;
         }
         // Malformed JSON.
         else {
           $("#response-content-textarea-box").hidden = false;
           NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
             aEditor.setMode(SourceEditor.MODES.JAVASCRIPT);
             aEditor.setText(aString);
--- a/browser/devtools/profiler/ProfilerPanel.jsm
+++ b/browser/devtools/profiler/ProfilerPanel.jsm
@@ -5,30 +5,36 @@
 "use strict";
 
 const Cu = Components.utils;
 
 Cu.import("resource:///modules/devtools/gDevTools.jsm");
 Cu.import("resource:///modules/devtools/ProfilerController.jsm");
 Cu.import("resource:///modules/devtools/ProfilerHelpers.jsm");
 Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/devtools/Console.jsm");
 
 this.EXPORTED_SYMBOLS = ["ProfilerPanel"];
 
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
     "resource://gre/modules/commonjs/sdk/core/promise.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
   "resource://gre/modules/devtools/dbg-server.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
   "resource://gre/modules/Services.jsm");
 
+const PROFILE_IDLE = 0;
+const PROFILE_RUNNING = 1;
+const PROFILE_COMPLETED = 2;
+
 /**
  * An instance of a profile UI. Profile UI consists of
  * an iframe with Cleopatra loaded in it and some
  * surrounding meta-data (such as uids).
  *
  * Its main function is to talk to the Cleopatra instance
  * inside of the iframe.
  *
@@ -155,28 +161,16 @@ ProfileUI.prototype = {
         onParsed();
       };
 
       poll();
     });
   },
 
   /**
-   * Update profile's label in the sidebar.
-   *
-   * @param string text
-   *   New text for the label.
-   */
-  updateLabel: function PUI_udpateLabel(text) {
-    let doc = this.panel.document;
-    let label = doc.querySelector("li#profile-" + this.uid + "> h1");
-    label.textContent = text;
-  },
-
-  /**
    * Start profiling and, once started, notify the underlying page
    * so that it could update the UI. Also, once started, we add a
    * star to the profile name to indicate which profile is currently
    * running.
    *
    * @param function startFn
    *        A function to use instead of the default
    *        this.panel.startProfiling. Useful when you
@@ -187,17 +181,17 @@ ProfileUI.prototype = {
   start: function PUI_start(startFn) {
     if (this.isStarted || this.isFinished) {
       return;
     }
 
     startFn = startFn || this.panel.startProfiling.bind(this.panel);
     startFn(this.name, () => {
       this.isStarted = true;
-      this.updateLabel(this.name + " *");
+      this.panel.sidebar.setProfileState(this, PROFILE_RUNNING);
       this.panel.broadcast(this.uid, {task: "onStarted"}); // Do we really need this?
       this.emit("started");
     });
   },
 
   /**
    * Stop profiling and, once stopped, notify the underlying page so
    * that it could update the UI and remove a star from the profile
@@ -214,17 +208,17 @@ ProfileUI.prototype = {
     if (!this.isStarted || this.isFinished) {
       return;
     }
 
     stopFn = stopFn || this.panel.stopProfiling.bind(this.panel);
     stopFn(this.name, () => {
       this.isStarted = false;
       this.isFinished = true;
-      this.updateLabel(this.name);
+      this.panel.sidebar.setProfileState(this, PROFILE_COMPLETED);
       this.panel.broadcast(this.uid, {task: "onStopped"});
       this.emit("stopped");
     });
   },
 
   /**
    * Send a message to Cleopatra instance. If a message cannot be
    * sent, this method queues it for later.
@@ -269,16 +263,49 @@ ProfileUI.prototype = {
     this.isReady = null
     this.panel = null;
     this.uid = null;
     this.iframe = null;
     this.messages = null;
   }
 };
 
+function SidebarView(el) {
+  EventEmitter.decorate(this);
+  this.node = new SideMenuWidget(el);
+}
+
+ViewHelpers.create({ constructor: SidebarView, proto: MenuContainer.prototype }, {
+  getItemByProfile: function (profile) {
+    return this.orderedItems.filter((item) => item.attachment.uid === profile.uid)[0];
+  },
+
+  setProfileState: function (profile, state) {
+    let item = this.getItemByProfile(profile);
+    let label = item.target.querySelector(".profiler-sidebar-item > span");
+
+    switch (state) {
+      case PROFILE_IDLE:
+        label.textContent = L10N.getStr("profiler.stateIdle");
+        break;
+      case PROFILE_RUNNING:
+        label.textContent = L10N.getStr("profiler.stateRunning");
+        break;
+      case PROFILE_COMPLETED:
+        label.textContent = L10N.getStr("profiler.stateCompleted");
+        break;
+      default: // Wrong state, do nothing.
+        return;
+    }
+
+    item.attachment.state = state;
+    this.emit("stateChanged", item);
+  }
+});
+
 /**
  * Profiler panel. It is responsible for creating and managing
  * different profile instances (see ProfileUI).
  *
  * ProfilerPanel is an event emitter. It can emit the following
  * events:
  *
  *   - ready:     after the panel is done loading everything,
@@ -315,29 +342,37 @@ function ProfilerPanel(frame, toolbox) {
 
 ProfilerPanel.prototype = {
   isReady:     null,
   window:      null,
   document:    null,
   target:      null,
   controller:  null,
   profiles:    null,
+  sidebar:     null,
 
   _uid:        null,
   _activeUid:  null,
   _runningUid: null,
   _browserWin: null,
   _msgQueue:   null,
 
   get activeProfile() {
     return this.profiles.get(this._activeUid);
   },
 
   set activeProfile(profile) {
+    if (this._activeUid === profile.uid)
+      return;
+
+    if (this.activeProfile)
+      this.activeProfile.hide();
+
     this._activeUid = profile.uid;
+    profile.show();
   },
 
   get browserWindow() {
     if (this._browserWin) {
       return this._browserWin;
     }
 
     let win = this.window.top;
@@ -352,52 +387,69 @@ ProfilerPanel.prototype = {
 
   /**
    * Open a debug connection and, on success, switch to the newly created
    * profile.
    *
    * @return Promise
    */
   open: function PP_open() {
-    let promise;
-
     // Local profiling needs to make the target remote.
-    if (!this.target.isRemote) {
-      promise = this.target.makeRemote();
-    } else {
-      promise = Promise.resolve(this.target);
-    }
+    let target = this.target;
+    let promise = !target.isRemote ? target.makeRemote() : Promise.resolve(target);
 
     return promise
-      .then(function(target) {
+      .then((target) => {
         let deferred = Promise.defer();
+
         this.controller = new ProfilerController(this.target);
+        this.sidebar = new SidebarView(this.document.querySelector("#profiles-list"));
+        this.sidebar.node.addEventListener("select", (ev) => {
+          if (!ev.detail)
+            return;
+
+          let profile = this.profiles.get(ev.detail.attachment.uid);
+          this.activeProfile = profile;
 
-        this.controller.connect(function onConnect() {
+          if (profile.isReady) {
+            profile.flushMessages();
+            return void this.emit("profileSwitched", profile.uid);
+          }
+
+          profile.once("ready", () => {
+            profile.flushMessages();
+            this.emit("profileSwitched", profile.uid);
+          });
+        });
+
+        this.controller.connect(() => {
           let create = this.document.getElementById("profiler-create");
-          create.addEventListener("click", function (ev) {
-            this.createProfile()
-          }.bind(this), false);
+          create.addEventListener("click", () => this.createProfile(), false);
           create.removeAttribute("disabled");
 
           let profile = this.createProfile();
-          this.switchToProfile(profile, function () {
+          let onSwitch = (_, uid) => {
+            if (profile.uid !== uid)
+              return;
+
+            this.off("profileSwitched", onSwitch);
             this.isReady = true;
             this.emit("ready");
 
             deferred.resolve(this);
-          }.bind(this))
-        }.bind(this));
+          };
+
+          this.on("profileSwitched", onSwitch);
+          this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
+        });
 
         return deferred.promise;
-      }.bind(this))
-      .then(null, function onError(reason) {
-        Cu.reportError("ProfilerPanel open failed. " +
-                       reason.error + ": " + reason.message);
-      });
+      })
+      .then(null, (reason) =>
+        Cu.reportError("ProfilePanel open failed: " + reason.message));
   },
 
   /**
    * Creates a new profile instance (see ProfileUI) and
    * adds an appropriate item to the sidebar. Note that
    * this method doesn't automatically switch user to
    * the newly created profile, they have do to switch
    * explicitly.
@@ -422,82 +474,36 @@ ProfilerPanel.prototype = {
     if (!name) {
       name = L10N.getFormatStr("profiler.profileName", [uid]);
       while (this.getProfileByName(name)) {
         uid = ++this._uid;
         name = L10N.getFormatStr("profiler.profileName", [uid]);
       }
     }
 
-    let list = this.document.getElementById("profiles-list");
-    let item = this.document.createElement("li");
-    let wrap = this.document.createElement("h1");
-    name = name || L10N.getFormatStr("profiler.profileName", [uid]);
+    let box = this.document.createElement("vbox");
+    box.className = "profiler-sidebar-item";
+    box.id = "profile-" + uid;
+    let h3 = this.document.createElement("h3");
+    h3.textContent = name;
+    let span = this.document.createElement("span");
+    span.textContent = L10N.getStr("profiler.stateIdle");
+    box.appendChild(h3);
+    box.appendChild(span);
 
-    item.setAttribute("id", "profile-" + uid);
-    item.setAttribute("data-uid", uid);
-    item.addEventListener("click", function (ev) {
-      this.switchToProfile(this.profiles.get(uid));
-    }.bind(this), false);
-
-    wrap.className = "profile-name";
-    wrap.textContent = name;
-
-    item.appendChild(wrap);
-    list.appendChild(item);
+    this.sidebar.push(box, { attachment: { uid: uid, name: name, state: PROFILE_IDLE } });
 
     let profile = new ProfileUI(uid, name, this);
     this.profiles.set(uid, profile);
 
     this.emit("profileCreated", uid);
     return profile;
   },
 
   /**
-   * Switches to a different profile by making its instance an
-   * active one.
-   *
-   * @param ProfileUI profile
-   *   A profile instance to switch to.
-   * @param function onLoad
-   *   A function to call when profile instance is ready.
-   *   If the instance is already loaded, onLoad will be
-   *   called synchronously.
-   */
-  switchToProfile: function PP_switchToProfile(profile, onLoad=function() {}) {
-    let doc = this.document;
-
-    if (this.activeProfile) {
-      this.activeProfile.hide();
-    }
-
-    let active = doc.querySelector("#profiles-list > li.splitview-active");
-    if (active) {
-      active.className = "";
-    }
-
-    doc.getElementById("profile-" + profile.uid).className = "splitview-active";
-    profile.show();
-    this.activeProfile = profile;
-
-    if (profile.isReady) {
-      profile.flushMessages();
-      this.emit("profileSwitched", profile.uid);
-      onLoad();
-      return;
-    }
-
-    profile.once("ready", () => {
-      profile.flushMessages();
-      this.emit("profileSwitched", profile.uid);
-      onLoad();
-    });
-  },
-
-  /**
    * Start collecting profile data.
    *
    * @param function onStart
    *   A function to call once we get the message
    *   that profiling had been successfuly started.
    */
   startProfiling: function PP_startProfiling(name, onStart) {
     this.controller.start(name, (err) => {
--- a/browser/devtools/profiler/cmd-profiler.jsm
+++ b/browser/devtools/profiler/cmd-profiler.jsm
@@ -78,17 +78,25 @@ gcli.addCommand({
       if (profile.isStarted) {
         throw gcli.lookup("profilerAlreadyStarted");
       }
 
       if (profile.isFinished) {
         throw gcli.lookup("profilerAlreadyFinished");
       }
 
-      panel.switchToProfile(profile, function () profile.start());
+      let item = panel.sidebar.getItemByProfile(profile);
+
+      if (panel.sidebar.selectedItem === item) {
+        profile.start();
+      } else {
+        panel.on("profileSwitched", () => profile.start());
+        panel.sidebar.selectedItem = item;
+      }
+
       return gcli.lookup("profilerStarting2");
     }
 
     return gDevTools.showToolbox(context.environment.target, "jsprofiler")
       .then(start);
   }
 });
 
@@ -119,17 +127,25 @@ gcli.addCommand({
       if (profile.isFinished) {
         throw gcli.lookup("profilerAlreadyFinished");
       }
 
       if (!profile.isStarted) {
         throw gcli.lookup("profilerNotStarted2");
       }
 
-      panel.switchToProfile(profile, function () profile.stop());
+      let item = panel.sidebar.getItemByProfile(profile);
+
+      if (panel.sidebar.selectedItem === item) {
+        profile.stop();
+      } else {
+        panel.on("profileSwitched", () => profile.stop());
+        panel.sidebar.selectedItem = item;
+      }
+
       return gcli.lookup("profilerStopping2");
     }
 
     return gDevTools.showToolbox(context.environment.target, "jsprofiler")
       .then(stop);
   }
 });
 
@@ -188,17 +204,17 @@ gcli.addCommand({
       throw gcli.lookup("profilerNotReady");
     }
 
     let profile = panel.getProfileByName(args.name);
     if (!profile) {
       throw gcli.lookup("profilerNotFound");
     }
 
-    panel.switchToProfile(profile);
+    panel.sidebar.selectedItem = panel.sidebar.getItemByProfile(profile);
   }
 });
 
 function getPanel(context, id) {
   if (context == null) {
     return undefined;
   }
 
--- a/browser/devtools/profiler/profiler.xul
+++ b/browser/devtools/profiler/profiler.xul
@@ -1,52 +1,46 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!-- 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/. -->
 
 <?xml-stylesheet href="chrome://global/skin/global.css"?>
 <?xml-stylesheet href="chrome://browser/skin/devtools/common.css"?>
-<?xml-stylesheet href="chrome://browser/skin/devtools/splitview.css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css"?>
 <?xml-stylesheet href="chrome://browser/skin/devtools/profiler.css"?>
-<?xml-stylesheet href="chrome://browser/content/devtools/splitview.css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css"?>
 
 <!DOCTYPE window [
 <!ENTITY % profilerDTD SYSTEM "chrome://browser/locale/devtools/profiler.dtd">
   %profilerDTD;
 ]>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
-  <box flex="1" id="profiler-chrome" class="splitview-root">
-    <box class="splitview-controller" width="180px">
-      <box class="splitview-main"></box>
-
-      <box class="splitview-nav-container">
-        <ol class="splitview-nav" id="profiles-list">
-          <!-- Example:
-          <li class="splitview-active" id="profile-1" data-uid="1">
-            <h1 class="profile-name">Profile 1</h1>
-          </li>
-          -->
-        </ol>
+  <box flex="1" id="profiler-chrome" class="devtools-responsive-container">
+    <vbox class="profiler-sidebar">
+      <toolbar class="devtools-toolbar">
+        <toolbarbutton id="profiler-create"
+                       class="devtools-toolbarbutton"
+                       label="&profilerNew.label;"
+                       disabled="true"/>
+      </toolbar>
 
-        <spacer flex="1"/>
+      <vbox id="profiles-list" flex="1">
+      </vbox>
+    </vbox>
 
-        <toolbar class="devtools-toolbar" mode="full">
-          <toolbarbutton id="profiler-create"
-                         class="devtools-toolbarbutton"
-                         label="&profilerNew.label;"
-                         disabled="true"/>
-        </toolbar>
-      </box> <!-- splitview-nav-container -->
-    </box> <!-- splitview-controller -->
+    <splitter class="devtools-side-splitter"/>
 
-    <box flex="1">
+    <vbox flex="1">
+      <toolbar class="devtools-toolbar">
+      </toolbar>
+
       <vbox flex="1" id="profiler-report">
         <!-- Example:
         <iframe id="profiler-cleo-1"
           src="devtools/cleopatra.html" flex="1"></iframe>
         -->
       </vbox>
-    </box>
+    </vbox>
   </box>
 </window>
--- a/browser/devtools/profiler/test/browser_profiler_bug_830664_multiple_profiles.js
+++ b/browser/devtools/profiler/test/browser_profiler_bug_830664_multiple_profiles.js
@@ -21,59 +21,43 @@ function test() {
       } else {
         profile.once("ready", startProfiling);
       }
     });
     gPanel.createProfile();
   });
 }
 
-function getCleoControls(doc) {
-  return [
-    doc.querySelector("#startWrapper button"),
-    doc.querySelector("#profilerMessage")
-  ];
-}
-
 function startProfiling() {
   gPanel.profiles.get(gPanel.activeProfile.uid).once("started", function () {
     setTimeout(function () {
       sendFromProfile(2, "start");
       gPanel.profiles.get(2).once("started", function () setTimeout(stopProfiling, 50));
     }, 50);
   });
+
   sendFromProfile(gPanel.activeProfile.uid, "start");
 }
 
 function stopProfiling() {
-  let [win, doc] = getProfileInternals(gUid);
-  let [btn, msg] = getCleoControls(doc);
-
-  is(gPanel.document.querySelector("li#profile-1 > h1").textContent,
-    "Profile 1 *", "Profile 1 has a star next to it.");
-  is(gPanel.document.querySelector("li#profile-2 > h1").textContent,
-    "Profile 2 *", "Profile 2 has a star next to it.");
+  is(getSidebarItem(1).attachment.state, PROFILE_RUNNING);
+  is(getSidebarItem(2).attachment.state, PROFILE_RUNNING);
 
   gPanel.profiles.get(gPanel.activeProfile.uid).once("stopped", function () {
-    is(gPanel.document.querySelector("li#profile-1 > h1").textContent,
-      "Profile 1", "Profile 1 doesn't have a star next to it anymore.");
+    is(getSidebarItem(1).attachment.state, PROFILE_COMPLETED);
 
     sendFromProfile(2, "stop");
     gPanel.profiles.get(2).once("stopped", confirmAndFinish);
   });
+
   sendFromProfile(gPanel.activeProfile.uid, "stop");
 }
 
 function confirmAndFinish(ev, data) {
-  let [win, doc] = getProfileInternals(gUid);
-  let [btn, msg] = getCleoControls(doc);
-
-  is(gPanel.document.querySelector("li#profile-1 > h1").textContent,
-    "Profile 1", "Profile 1 doesn't have a star next to it.");
-  is(gPanel.document.querySelector("li#profile-2 > h1").textContent,
-    "Profile 2", "Profile 2 doesn't have a star next to it.");  
+  is(getSidebarItem(1).attachment.state, PROFILE_COMPLETED);
+  is(getSidebarItem(2).attachment.state, PROFILE_COMPLETED);
 
   tearDown(gTab, function onTearDown() {
     gPanel = null;
     gTab = null;
     gUid = null;
   });
 }
--- a/browser/devtools/profiler/test/browser_profiler_console_api.js
+++ b/browser/devtools/profiler/test/browser_profiler_console_api.js
@@ -43,24 +43,22 @@ function testConsoleProfile(hud) {
 
   gPanel.on("profileCreated", profileEnd);
   hud.jsterm.execute("console.profile()");
   hud.jsterm.execute("console.profile()");
 }
 
 function checkProfiles(toolbox) {
   let panel = toolbox.getPanel("jsprofiler");
-  let getTitle = (uid) =>
-    panel.document.querySelector("li#profile-" + uid + " > h1").textContent;
 
-  is(getTitle(1), "Profile 1", "Profile 1 doesn't have a star next to it.");
-  is(getTitle(2), "Profile 2 *", "Profile 2 doesn't have a star next to it.");
-  is(getTitle(3), "Profile 3", "Profile 3 doesn't have a star next to it.");
+  is(getSidebarItem(1, panel).attachment.state, PROFILE_IDLE);
+  is(getSidebarItem(2, panel).attachment.state, PROFILE_RUNNING);
+  is(getSidebarItem(3, panel).attachment.state, PROFILE_COMPLETED);
 
   // Make sure we can still stop profiles via the UI.
 
   gPanel.profiles.get(2).once("stopped", () => {
-    is(getTitle(2), "Profile 2", "Profile 2 doesn't have a star next to it.");
+    is(getSidebarItem(2, panel).attachment.state, PROFILE_COMPLETED);
     tearDown(gTab, () => gTab = gPanel = null);
   });
 
   sendFromProfile(2, "stop");
 }
\ No newline at end of file
--- a/browser/devtools/profiler/test/browser_profiler_console_api_content.js
+++ b/browser/devtools/profiler/test/browser_profiler_console_api_content.js
@@ -12,28 +12,30 @@ function test() {
 
   setUp(URL, (tab, browser, panel) => {
     gTab = tab;
     gPanel = panel;
 
     openProfiler(tab, (toolbox) => {
       gToolbox = toolbox;
       loadUrl(PAGE, tab, () => {
-        gPanel.on("profileCreated", runTests);
+        gPanel.sidebar.on("stateChanged", (_, item) => {
+          if (item.attachment.state !== PROFILE_COMPLETED)
+            return;
+
+          runTests();
+        });
       });
     });
  });
 }
 
 function runTests() {
-  let getTitle = (uid) =>
-    gPanel.document.querySelector("li#profile-" + uid + " > h1").textContent;
-
-  is(getTitle(1), "Profile 1", "Profile 1 doesn't have a star next to it.");
-  is(getTitle(2), "Profile 2", "Profile 2 doesn't have a star next to it.");
+  is(getSidebarItem(1).attachment.state, PROFILE_IDLE);
+  is(getSidebarItem(2).attachment.state, PROFILE_COMPLETED);
 
   gPanel.once("parsed", () => {
     function assertSampleAndFinish() {
       let [win,doc] = getProfileInternals();
       let sample = doc.getElementsByClassName("samplePercentage");
 
       if (sample.length <= 0)
         return void setTimeout(assertSampleAndFinish, 100);
@@ -44,10 +46,11 @@ function runTests() {
         gPanel = null;
         gToolbox = null;
       });
     }
 
     assertSampleAndFinish();
   });
 
-  gPanel.switchToProfile(gPanel.profiles.get(2));
+  let profile = gPanel.profiles.get(2);
+  gPanel.sidebar.selectedItem = gPanel.sidebar.getItemByProfile(profile);
 }
\ No newline at end of file
--- a/browser/devtools/profiler/test/browser_profiler_console_api_mixed.js
+++ b/browser/devtools/profiler/test/browser_profiler_console_api_mixed.js
@@ -13,25 +13,23 @@ function test() {
     gPanel = panel;
 
     openProfiler(tab, runTests);
   });
 }
 
 function runTests(toolbox) {
   let panel = toolbox.getPanel("jsprofiler");
-  let getTitle = (uid) =>
-    panel.document.querySelector("li#profile-" + uid + " > h1").textContent;
 
   panel.profiles.get(1).once("started", () => {
-    is(getTitle(1), "Profile 1 *", "Profile 1 has a start next to it.");
+    is(getSidebarItem(1, panel).attachment.state, PROFILE_RUNNING);
 
     openConsole(gTab, (hud) => {
       panel.profiles.get(1).once("stopped", () => {
-        is(getTitle(1), "Profile 1", "Profile 1 doesn't have a star next to it.");
+        is(getSidebarItem(1, panel).attachment.state, PROFILE_COMPLETED);
         tearDown(gTab, () => gTab = gPanel = null);
       });
 
       hud.jsterm.execute("console.profileEnd()");
     });
   });
 
   sendFromProfile(1, "start");
--- a/browser/devtools/profiler/test/browser_profiler_console_api_named.js
+++ b/browser/devtools/profiler/test/browser_profiler_console_api_named.js
@@ -41,26 +41,26 @@ function testConsoleProfile(hud) {
 
   gPanel.on("profileCreated", profileEnd);
   hud.jsterm.execute("console.profile('Second')");
   hud.jsterm.execute("console.profile('Third')");
 }
 
 function checkProfiles(toolbox) {
   let panel = toolbox.getPanel("jsprofiler");
-  let getTitle = (uid) =>
-    panel.document.querySelector("li#profile-" + uid + " > h1").textContent;
 
-  is(getTitle(1), "Profile 1", "Profile 1 doesn't have a star next to it.");
-  is(getTitle(2), "Second", "Second doesn't have a star next to it.");
-  is(getTitle(3), "Third *", "Third does have a star next to it.");
+  is(getSidebarItem(1, panel).attachment.state, PROFILE_IDLE);
+  is(getSidebarItem(2, panel).attachment.name, "Second");
+  is(getSidebarItem(2, panel).attachment.state, PROFILE_COMPLETED);
+  is(getSidebarItem(3, panel).attachment.name, "Third");
+  is(getSidebarItem(3, panel).attachment.state, PROFILE_RUNNING);
 
   // Make sure we can still stop profiles via the queue pop.
 
   gPanel.profiles.get(3).once("stopped", () => {
     openProfiler(gTab, () => {
-      is(getTitle(3), "Third", "Third doesn't have a star next to it.");
+      is(getSidebarItem(3, panel).attachment.state, PROFILE_COMPLETED);
       tearDown(gTab, () => gTab = gPanel = null);
     });
   });
 
   openConsole(gTab, (hud) => hud.jsterm.execute("console.profileEnd()"));
 }
\ No newline at end of file
--- a/browser/devtools/profiler/test/browser_profiler_profiles.js
+++ b/browser/devtools/profiler/test/browser_profiler_profiles.js
@@ -43,18 +43,21 @@ function onProfileSwitched(name, uid) {
   ok(gPanel.activeProfile.uid === uid, "Switched to a new profile");
   gPanel.createProfile("Custom Profile");
 }
 
 function onNamedProfileCreated(name, uid) {
   is(gPanel.profiles.size, 3, "There are three profiles now");
   is(gPanel.getProfileByUID(uid).name, "Custom Profile", "Name is correct");
 
-  let label = gPanel.document.querySelector("li#profile-" + uid + "> h1");
-  is(label.textContent, "Custom Profile", "Name is correct on the label");
+  let profile = gPanel.profiles.get(uid);
+  let data = gPanel.sidebar.getItemByProfile(profile).attachment;
+
+  is(data.uid, uid, "UID is correct");
+  is(data.name, "Custom Profile", "Name is correct on the label");
 
   let btn = gPanel.document.getElementById("profile-" + uid);
   ok(btn, "Profile item has been added to the sidebar");
   btn.click();
 }
 
 function onNamedProfileSwitched(name, uid) {
   ok(gPanel.activeProfile.uid === uid, "Switched to a new profile");
--- a/browser/devtools/profiler/test/head.js
+++ b/browser/devtools/profiler/test/head.js
@@ -1,14 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 let temp = {};
+
 const PROFILER_ENABLED = "devtools.profiler.enabled";
 const REMOTE_ENABLED = "devtools.debugger.remote-enabled";
+const PROFILE_IDLE = 0;
+const PROFILE_RUNNING = 1;
+const PROFILE_COMPLETED = 2;
 
 Cu.import("resource:///modules/devtools/gDevTools.jsm", temp);
 let gDevTools = temp.gDevTools;
 
 Cu.import("resource://gre/modules/devtools/Loader.jsm", temp);
 let TargetFactory = temp.devtools.TargetFactory;
 
 Cu.import("resource://gre/modules/devtools/dbg-server.jsm", temp);
@@ -31,16 +35,21 @@ registerCleanupFunction(function () {
 function getProfileInternals(uid) {
   let profile = (uid != null) ? gPanel.profiles.get(uid) : gPanel.activeProfile;
   let win = profile.iframe.contentWindow;
   let doc = win.document;
 
   return [win, doc];
 }
 
+function getSidebarItem(uid, panel=gPanel) {
+  let profile = panel.profiles.get(uid);
+  return panel.sidebar.getItemByProfile(profile);
+}
+
 function sendFromProfile(uid, msg) {
   let [win, doc] = getProfileInternals(uid);
   win.parent.postMessage({ uid: uid, status: msg }, "*");
 }
 
 function loadTab(url, callback) {
   let tab = gBrowser.addTab();
   gBrowser.selectedTab = tab;
--- a/browser/devtools/shared/widgets/VariablesView.jsm
+++ b/browser/devtools/shared/widgets/VariablesView.jsm
@@ -15,16 +15,18 @@ const LAZY_APPEND_DELAY = 100; // ms
 const LAZY_APPEND_BATCH = 100; // nodes
 const PAGE_SIZE_SCROLL_HEIGHT_RATIO = 100;
 const PAGE_SIZE_MAX_JUMPS = 30;
 const SEARCH_ACTION_MAX_DELAY = 300; // ms
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper",
   "resource://gre/modules/devtools/NetworkHelper.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils",
   "resource://gre/modules/devtools/WebConsoleUtils.jsm");
 
 this.EXPORTED_SYMBOLS = ["VariablesView"];
@@ -69,29 +71,31 @@ this.VariablesView = function VariablesV
   this._list.setAttribute("orient", "vertical");
   this._list.addEventListener("keypress", this._onViewKeyPress, false);
   this._parent.appendChild(this._list);
   this._boxObject = this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
 
   for (let name in aFlags) {
     this[name] = aFlags[name];
   }
+
+  EventEmitter.decorate(this);
 };
 
 VariablesView.prototype = {
   /**
    * Helper setter for populating this container with a raw object.
    *
    * @param object aData
    *        The raw object to display. You can only provide this object
    *        if you want the variables view to work in sync mode.
    */
   set rawObject(aObject) {
     this.empty();
-    this.addScope().addVar().populate(aObject);
+    this.addScope().addItem().populate(aObject);
   },
 
   /**
    * Adds a scope to contain any inspected variables.
    *
    * @param string aName
    *        The scope's name (e.g. "Local", "Global" etc.).
    * @return Scope
@@ -176,16 +180,21 @@ VariablesView.prototype = {
       if (!this._store.length) {
         this._appendEmptyNotice();
         this._toggleSearchVisibility(false);
       }
     }, aTimeout);
   },
 
   /**
+   * The controller for this VariablesView, if it has one.
+   */
+  controller: null,
+
+  /**
    * The amount of time (in milliseconds) it takes to empty this view lazily.
    */
   lazyEmptyDelay: LAZY_EMPTY_DELAY,
 
   /**
    * Specifies if this view may be emptied lazily.
    * @see VariablesView.prototype.empty
    */
@@ -582,17 +591,18 @@ VariablesView.prototype = {
    *
    * @param nsIDOMNode aNode
    *        The node to search for.
    * @return Scope
    *         The matched scope, or null if nothing is found.
    */
   getScopeForNode: function(aNode) {
     let item = this._itemsByElement.get(aNode);
-    if (item && !(item instanceof Variable) && !(item instanceof Property)) {
+    // Match only Scopes, not Variables or Properties.
+    if (item && !(item instanceof Variable)) {
       return item;
     }
     return null;
   },
 
   /**
    * Recursively searches this container for the scope, variable or property
    * displayed by the specified node.
@@ -785,32 +795,30 @@ VariablesView.prototype = {
         return;
 
       case e.DOM_VK_END:
         this.focusLastVisibleItem();
         return;
 
       case e.DOM_VK_RETURN:
       case e.DOM_VK_ENTER:
-        // Start editing the value or name of the variable or property.
-        if (item instanceof Variable ||
-            item instanceof Property) {
+        // Start editing the value or name of the Variable or Property.
+        if (item instanceof Variable) {
           if (e.metaKey || e.altKey || e.shiftKey) {
             item._activateNameInput();
           } else {
             item._activateValueInput();
           }
         }
         return;
 
       case e.DOM_VK_DELETE:
       case e.DOM_VK_BACK_SPACE:
-        // Delete the variable or property if allowed.
-        if (item instanceof Variable ||
-            item instanceof Property) {
+        // Delete the Variable or Property if allowed.
+        if (item instanceof Variable) {
           item._onDelete(e);
         }
         return;
     }
   },
 
   /**
    * The number of elements in this container to jump when Page Up or Page Down
@@ -897,91 +905,108 @@ VariablesView.prototype = {
   _emptyTextNode: null,
   _emptyTextValue: ""
 };
 
 VariablesView.NON_SORTABLE_CLASSES = [
   "Array",
   "Int8Array",
   "Uint8Array",
+  "Uint8ClampedArray",
   "Int16Array",
   "Uint16Array",
   "Int32Array",
   "Uint32Array",
   "Float32Array",
   "Float64Array"
 ];
 
 /**
+ * Determine whether an object's properties should be sorted based on its class.
+ *
+ * @param string aClassName
+ *        The class of the object.
+ */
+VariablesView.isSortable = function(aClassName) {
+  return VariablesView.NON_SORTABLE_CLASSES.indexOf(aClassName) == -1;
+};
+
+/**
  * Generates the string evaluated when performing simple value changes.
  *
  * @param Variable | Property aItem
  *        The current variable or property.
  * @param string aCurrentString
  *        The trimmed user inputted string.
+ * @param string aPrefix [optional]
+ *        Prefix for the symbolic name.
  * @return string
  *         The string to be evaluated.
  */
-VariablesView.simpleValueEvalMacro = function(aItem, aCurrentString) {
-  return aItem._symbolicName + "=" + aCurrentString;
+VariablesView.simpleValueEvalMacro = function(aItem, aCurrentString, aPrefix = "") {
+  return aPrefix + aItem._symbolicName + "=" + aCurrentString;
 };
 
 /**
  * Generates the string evaluated when overriding getters and setters with
  * plain values.
  *
  * @param Property aItem
  *        The current getter or setter property.
  * @param string aCurrentString
  *        The trimmed user inputted string.
+ * @param string aPrefix [optional]
+ *        Prefix for the symbolic name.
  * @return string
  *         The string to be evaluated.
  */
-VariablesView.overrideValueEvalMacro = function(aItem, aCurrentString) {
+VariablesView.overrideValueEvalMacro = function(aItem, aCurrentString, aPrefix = "") {
   let property = "\"" + aItem._nameString + "\"";
-  let parent = aItem.ownerView._symbolicName || "this";
+  let parent = aPrefix + aItem.ownerView._symbolicName || "this";
 
   return "Object.defineProperty(" + parent + "," + property + "," +
     "{ value: " + aCurrentString +
     ", enumerable: " + parent + ".propertyIsEnumerable(" + property + ")" +
     ", configurable: true" +
     ", writable: true" +
     "})";
 };
 
 /**
  * Generates the string evaluated when performing getters and setters changes.
  *
  * @param Property aItem
  *        The current getter or setter property.
  * @param string aCurrentString
  *        The trimmed user inputted string.
+ * @param string aPrefix [optional]
+ *        Prefix for the symbolic name.
  * @return string
  *         The string to be evaluated.
  */
-VariablesView.getterOrSetterEvalMacro = function(aItem, aCurrentString) {
+VariablesView.getterOrSetterEvalMacro = function(aItem, aCurrentString, aPrefix = "") {
   let type = aItem._nameString;
   let propertyObject = aItem.ownerView;
   let parentObject = propertyObject.ownerView;
   let property = "\"" + propertyObject._nameString + "\"";
-  let parent = parentObject._symbolicName || "this";
+  let parent = aPrefix + parentObject._symbolicName || "this";
 
   switch (aCurrentString) {
     case "":
     case "null":
     case "undefined":
       let mirrorType = type == "get" ? "set" : "get";
       let mirrorLookup = type == "get" ? "__lookupSetter__" : "__lookupGetter__";
 
       // If the parent object will end up without any getter or setter,
       // morph it into a plain value.
       if ((type == "set" && propertyObject.getter.type == "undefined") ||
           (type == "get" && propertyObject.setter.type == "undefined")) {
         // Make sure the right getter/setter to value override macro is applied to the target object.
-        return propertyObject.evaluationMacro(propertyObject, "undefined");
+        return propertyObject.evaluationMacro(propertyObject, "undefined", aPrefix);
       }
 
       // Construct and return the getter/setter removal evaluation string.
       // e.g: Object.defineProperty(foo, "bar", {
       //   get: foo.__lookupGetter__("bar"),
       //   set: undefined,
       //   enumerable: true,
       //   configurable: true
@@ -990,26 +1015,26 @@ VariablesView.getterOrSetterEvalMacro = 
         "{" + mirrorType + ":" + parent + "." + mirrorLookup + "(" + property + ")" +
         "," + type + ":" + undefined +
         ", enumerable: " + parent + ".propertyIsEnumerable(" + property + ")" +
         ", configurable: true" +
         "})";
 
     default:
       // Wrap statements inside a function declaration if not already wrapped.
-      if (aCurrentString.indexOf("function") != 0) {
+      if (!aCurrentString.startsWith("function")) {
         let header = "function(" + (type == "set" ? "value" : "") + ")";
         let body = "";
         // If there's a return statement explicitly written, always use the
         // standard function definition syntax
-        if (aCurrentString.indexOf("return ") != -1) {
+        if (aCurrentString.contains("return ")) {
           body = "{" + aCurrentString + "}";
         }
         // If block syntax is used, use the whole string as the function body.
-        else if (aCurrentString.indexOf("{") == 0) {
+        else if (aCurrentString.startsWith("{")) {
           body = aCurrentString;
         }
         // Prefer an expression closure.
         else {
           body = "(" + aCurrentString + ")";
         }
         aCurrentString = header + body;
       }
@@ -1037,16 +1062,17 @@ VariablesView.getterOrSetterDeleteCallba
 
   // Make sure the right getter/setter to value override macro is applied
   // to the target object.
   aItem.ownerView.eval(aItem.evaluationMacro(aItem, ""));
 
   return true; // Don't hide the element.
 };
 
+
 /**
  * A Scope is an object holding Variable instances.
  * Iterable via "for (let [name, variable] in instance) { }".
  *
  * @param VariablesView aView
  *        The view to contain this scope.
  * @param string aName
  *        The scope's name.
@@ -1078,48 +1104,106 @@ function Scope(aView, aName, aFlags = {}
   this._store = new Map();
   this._enumItems = [];
   this._nonEnumItems = [];
   this._init(aName.trim(), aFlags);
 }
 
 Scope.prototype = {
   /**
-   * Adds a variable to contain any inspected properties.
+   * Whether this Scope should be prefetched when it is remoted.
+   */
+  shouldPrefetch: true,
+
+  /**
+   * Create a new Variable that is a child of this Scope.
    *
    * @param string aName
-   *        The variable's name.
+   *        The name of the new Property.
    * @param object aDescriptor
-   *        Specifies the value and/or type & class of the variable,
+   *        The variable's descriptor.
+   * @return Variable
+   *         The newly created child Variable.
+   */
+  _createChild: function(aName, aDescriptor) {
+    return new Variable(this, aName, aDescriptor);
+  },
+
+  /**
+   * Adds a child to contain any inspected properties.
+   *
+   * @param string aName
+   *        The child's name.
+   * @param object aDescriptor
+   *        Specifies the value and/or type & class of the child,
    *        or 'get' & 'set' accessor properties. If the type is implicit,
    *        it will be inferred from the value.
    *        e.g. - { value: 42 }
    *             - { value: true }
    *             - { value: "nasu" }
    *             - { value: { type: "undefined" } }
    *             - { value: { type: "null" } }
    *             - { value: { type: "object", class: "Object" } }
    *             - { get: { type: "object", class: "Function" },
    *                 set: { type: "undefined" } }
    * @param boolean aRelaxed
    *        True if name duplicates should be allowed.
    * @return Variable
    *         The newly created Variable instance, null if it already exists.
    */
-  addVar: function(aName = "", aDescriptor = {}, aRelaxed = false) {
+  addItem: function(aName = "", aDescriptor = {}, aRelaxed = false) {
     if (this._store.has(aName) && !aRelaxed) {
       return null;
     }
 
-    let variable = new Variable(this, aName, aDescriptor);
-    this._store.set(aName, variable);
-    this._variablesView._itemsByElement.set(variable._target, variable);
-    this._variablesView._currHierarchy.set(variable._absoluteName, variable);
-    variable.header = !!aName;
-    return variable;
+    let child = this._createChild(aName, aDescriptor);
+    this._store.set(aName, child);
+    this._variablesView._itemsByElement.set(child._target, child);
+    this._variablesView._currHierarchy.set(child._absoluteName, child);
+    child.header = !!aName;
+    return child;
+  },
+
+  /**
+   * Adds items for this variable.
+   *
+   * @param object aItems
+   *        An object containing some { name: descriptor } data properties,
+   *        specifying the value and/or type & class of the variable,
+   *        or 'get' & 'set' accessor properties. If the type is implicit,
+   *        it will be inferred from the value.
+   *        e.g. - { someProp0: { value: 42 },
+   *                 someProp1: { value: true },
+   *                 someProp2: { value: "nasu" },
+   *                 someProp3: { value: { type: "undefined" } },
+   *                 someProp4: { value: { type: "null" } },
+   *                 someProp5: { value: { type: "object", class: "Object" } },
+   *                 someProp6: { get: { type: "object", class: "Function" },
+   *                              set: { type: "undefined" } } }
+   * @param object aOptions [optional]
+   *        Additional options for adding the properties. Supported options:
+   *        - sorted: true to sort all the properties before adding them
+   *        - callback: function invoked after each item is added
+   */
+  addItems: function(aItems, aOptions = {}) {
+    let names = Object.keys(aItems);
+
+    // Sort all of the properties before adding them, if preferred.
+    if (aOptions.sorted) {
+      names.sort();
+    }
+    // Add the properties to the current scope.
+    for (let name of names) {
+      let descriptor = aItems[name];
+      let item = this.addItem(name, descriptor);
+
+      if (aOptions.callback) {
+        aOptions.callback(item, descriptor.value);
+      }
+    }
   },
 
   /**
    * Gets the variable in this container having the specified name.
    *
    * @param string aName
    *        The name of the variable to get.
    * @return Variable
@@ -1174,21 +1258,23 @@ Scope.prototype = {
    *        The parent to check.
    * @return boolean
    *         True if the specified item is a descendant, false otherwise.
    */
   isDescendantOf: function(aParent) {
     if (this.isChildOf(aParent)) {
       return true;
     }
-    if (this.ownerView instanceof Scope ||
-        this.ownerView instanceof Variable ||
-        this.ownerView instanceof Property) {
+
+    // Recurse to parent if it is a Scope, Variable, or Property.
+    if (this.ownerView instanceof Scope) {
       return this.ownerView.isDescendantOf(aParent);
     }
+
+    return false;
   },
 
   /**
    * Shows the scope.
    */
   show: function() {
     this._target.hidden = false;
     this._isContentVisible = true;
@@ -1400,20 +1486,19 @@ Scope.prototype = {
     if (!this._nameString ||
         !this._isContentVisible ||
         !this._isHeaderVisible ||
         !this._isMatch) {
       return false;
     }
     // Check if all parent objects are expanded.
     let item = this;
-    while ((item = item.ownerView) &&  /* Parent object exists. */
-           (item instanceof Scope ||
-            item instanceof Variable ||
-            item instanceof Property)) {
+
+    // Recurse while parent is a Scope, Variable, or Property
+    while ((item = item.ownerView) && item instanceof Scope) {
       if (!item._isExpanded) {
         return false;
       }
     }
     return true;
   },
 
   /**
@@ -1717,24 +1802,21 @@ Scope.prototype = {
 
         if (variable._wasToggled && aLowerCaseQuery) {
           variable.expand();
         }
         if (variable._isExpanded && !aLowerCaseQuery) {
           variable._wasToggled = true;
         }
 
-        // If the variable is contained in another scope (variable or property),
+        // If the variable is contained in another Scope, Variable, or Property,
         // the parent may not be a match, thus hidden. It should be visible
         // ("expand upwards").
-
         while ((variable = variable.ownerView) &&  /* Parent object exists. */
-               (variable instanceof Scope ||
-                variable instanceof Variable ||
-                variable instanceof Property)) {
+               variable instanceof Scope) {
 
           // Show and expand the parent, as it is certainly accessible.
           variable._matched = true;
           aLowerCaseQuery && variable.expand();
         }
       }
 
       // Proceed with the search recursively inside this variable or property.
@@ -1966,89 +2048,34 @@ function Variable(aScope, aName, aDescri
   Scope.call(this, aScope, aName, this._initialDescriptor = aDescriptor);
   this.setGrip(aDescriptor.value);
   this._symbolicName = aName;
   this._absoluteName = aScope.name + "[\"" + aName + "\"]";
 }
 
 ViewHelpers.create({ constructor: Variable, proto: Scope.prototype }, {
   /**
-   * Adds a property for this variable.
-   *
-   * @param string aName
-   *        The property's name.
-   * @param object aDescriptor
-   *        Specifies the value and/or type & class of the property,
-   *        or 'get' & 'set' accessor properties. If the type is implicit,
-   *        it will be inferred from the value.
-   *        e.g. - { value: 42 }
-   *             - { value: true }
-   *             - { value: "nasu" }
-   *             - { value: { type: "undefined" } }
-   *             - { value: { type: "null" } }
-   *             - { value: { type: "object", class: "Object" } }
-   *             - { get: { type: "object", class: "Function" },
-   *                 set: { type: "undefined" } }
-   *             - { get: { type "object", class: "Function" },
-   *                 getterValue: "foo", getterPrototypeLevel: 2 }
-   * @param boolean aRelaxed
-   *        True if name duplicates should be allowed.
-   * @return Property
-   *         The newly created Property instance, null if it already exists.
+   * Whether this Scope should be prefetched when it is remoted.
    */
-  addProperty: function(aName = "", aDescriptor = {}, aRelaxed = false) {
-    if (this._store.has(aName) && !aRelaxed) {
-      return null;
-    }
-
-    let property = new Property(this, aName, aDescriptor);
-    this._store.set(aName, property);
-    this._variablesView._itemsByElement.set(property._target, property);
-    this._variablesView._currHierarchy.set(property._absoluteName, property);
-    property.header = !!aName;
-    return property;
+  get shouldPrefetch(){
+    return this.name == "window" || this.name == "this";
   },
 
   /**
-   * Adds properties for this variable.
+   * Create a new Property that is a child of Variable.
    *
-   * @param object aProperties
-   *        An object containing some { name: descriptor } data properties,
-   *        specifying the value and/or type & class of the variable,
-   *        or 'get' & 'set' accessor properties. If the type is implicit,
-   *        it will be inferred from the value.
-   *        e.g. - { someProp0: { value: 42 },
-   *                 someProp1: { value: true },
-   *                 someProp2: { value: "nasu" },
-   *                 someProp3: { value: { type: "undefined" } },
-   *                 someProp4: { value: { type: "null" } },
-   *                 someProp5: { value: { type: "object", class: "Object" } },
-   *                 someProp6: { get: { type: "object", class: "Function" },
-   *                              set: { type: "undefined" } } }
-   * @param object aOptions [optional]
-   *        Additional options for adding the properties. Supported options:
-   *        - sorted: true to sort all the properties before adding them
-   *        - callback: function invoked after each property is added
+   * @param string aName
+   *        The name of the new Property.
+   * @param object aDescriptor
+   *        The property's descriptor.
+   * @return Property
+   *         The newly created child Property.
    */
-  addProperties: function(aProperties, aOptions = {}) {
-    let propertyNames = Object.keys(aProperties);
-
-    // Sort all of the properties before adding them, if preferred.
-    if (aOptions.sorted) {
-      propertyNames.sort();
-    }
-    // Add the properties to the current scope.
-    for (let name of propertyNames) {
-      let descriptor = aProperties[name];
-      let property = this.addProperty(name, descriptor);
-
-      if (aOptions.callback) {
-        aOptions.callback(property, descriptor.value);
-      }
-    }
+  _createChild: function(aName, aDescriptor) {
+    return new Property(this, aName, aDescriptor);
   },
 
   /**
    * Populates this variable to contain all the properties of an object.
    *
    * @param object aObject
    *        The raw object you want to display.
    * @param object aOptions [optional]
@@ -2117,17 +2144,17 @@ ViewHelpers.create({ constructor: Variab
    *        The raw property value you want to display.
    * @return Property
    *         The newly added property instance.
    */
   _addRawValueProperty: function(aName, aDescriptor, aValue) {
     let descriptor = Object.create(aDescriptor);
     descriptor.value = VariablesView.getGrip(aValue);
 
-    let propertyItem = this.addProperty(aName, descriptor);
+    let propertyItem = this.addItem(aName, descriptor);
     propertyItem._sourceValue = aValue;
 
     // Add an 'onexpand' callback for the property, lazily handling
     // the addition of new child properties.
     if (!VariablesView.isPrimitive(descriptor)) {
       propertyItem.onexpand = this._populateTarget;
     }
     return propertyItem;
@@ -2144,17 +2171,17 @@ ViewHelpers.create({ constructor: Variab
    * @return Property
    *         The newly added property instance.
    */
   _addRawNonValueProperty: function(aName, aDescriptor) {
     let descriptor = Object.create(aDescriptor);
     descriptor.get = VariablesView.getGrip(aDescriptor.get);
     descriptor.set = VariablesView.getGrip(aDescriptor.set);
 
-    return this.addProperty(aName, descriptor);
+    return this.addItem(aName, descriptor);
   },
 
   /**
    * Gets this variable's path to the topmost scope.
    * For example, a symbolic name may look like "arguments['0']['foo']['bar']".
    * @return string
    */
   get symbolicName() this._symbolicName,
@@ -2306,18 +2333,18 @@ ViewHelpers.create({ constructor: Variab
       }
       // Deleting getters and setters individually is not allowed if no
       // evaluation method is provided.
       else {
         this.delete = null;
         this.evaluationMacro = null;
       }
 
-      let getter = this.addProperty("get", { value: descriptor.get });
-      let setter = this.addProperty("set", { value: descriptor.set });
+      let getter = this.addItem("get", { value: descriptor.get });
+      let setter = this.addItem("set", { value: descriptor.set });
       getter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
       setter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
 
       getter.hideArrow();
       setter.hideArrow();
       this.expand();
     }
   },
@@ -2847,19 +2874,18 @@ VariablesView.prototype.commitHierarchy 
     let changed = false;
 
     // If the inspected variable existed in a previous hierarchy, check if
     // the displayed value (a representation of the grip) has changed and if
     // it was previously expanded.
     if (prevVariable) {
       expanded = prevVariable._isExpanded;
 
-      // Only analyze variables and properties for displayed value changes.
-      if (currVariable instanceof Variable ||
-          currVariable instanceof Property) {
+      // Only analyze Variables and Properties for displayed value changes.
+      if (currVariable instanceof Variable) {
         changed = prevVariable._valueString != currVariable._valueString;
       }
     }
 
     // Make sure this variable is not handled in ulteror commits for the
     // same hierarchy.
     currVariable._committed = true;
 
@@ -2970,16 +2996,26 @@ VariablesView.isFalsy = function(aDescri
   if (type == "undefined" || type == "null") {
     return true;
   }
 
   return false;
 };
 
 /**
+ * Returns true if the value is an instance of Variable or Property.
+ *
+ * @param any aValue
+ *        The value to test.
+ */
+VariablesView.isVariable = function(aValue) {
+  return aValue instanceof Variable;
+};
+
+/**
  * Returns a standard grip for a value.
  *
  * @param any aValue
  *        The raw value to get a grip for.
  * @return any
  *         The value's grip.
  */
 VariablesView.getGrip = function(aValue) {
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/widgets/VariablesViewController.jsm
@@ -0,0 +1,350 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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/. */
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+Cu.import("resource:///modules/devtools/VariablesView.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "VARIABLES_SORTING_ENABLED", () =>
+  Services.prefs.getBoolPref("devtools.debugger.ui.variables-sorting-enabled")
+);
+
+const MAX_LONG_STRING_LENGTH = 200000;
+
+this.EXPORTED_SYMBOLS = ["VariablesViewController"];
+
+
+/**
+ * Controller for a VariablesView that handles interfacing with the debugger
+ * protocol. Is able to populate scopes and variables via the protocol as well
+ * as manage actor lifespans.
+ *
+ * @param VariablesView aView
+ *        The view to attach to.
+ * @param object aOptions
+ *        Options for configuring the controller. Supported options:
+ *        - getGripClient: callback for creating an object grip client
+ *        - getLongStringClient: callback for creating a long string grip client
+ *        - releaseActor: callback for releasing an actor when it's no longer needed
+ *        - overrideValueEvalMacro: callback for creating an overriding eval macro
+ *        - getterOrSetterEvalMacro: callback for creating a getter/setter eval macro
+ *        - simpleValueEvalMacro: callback for creating a simple value eval macro
+ */
+function VariablesViewController(aView, aOptions) {
+  this.addExpander = this.addExpander.bind(this);
+
+  this._getGripClient = aOptions.getGripClient;
+  this._getLongStringClient = aOptions.getLongStringClient;
+  this._releaseActor = aOptions.releaseActor;
+
+  if (aOptions.overrideValueEvalMacro) {
+    this._overrideValueEvalMacro = aOptions.overrideValueEvalMacro;
+  }
+  if (aOptions.getterOrSetterEvalMacro) {
+    this._getterOrSetterEvalMacro = aOptions.getterOrSetterEvalMacro;
+  }
+  if (aOptions.simpleValueEvalMacro) {
+    this._simpleValueEvalMacro = aOptions.simpleValueEvalMacro;
+  }
+
+  this._actors = new Set();
+  this.view = aView;
+  this.view.controller = this;
+}
+
+VariablesViewController.prototype = {
+  /**
+   * The default getter/setter evaluation macro.
+   */
+  _getterOrSetterEvalMacro: VariablesView.getterOrSetterEvalMacro,
+
+  /**
+   * The default override value evaluation macro.
+   */
+  _overrideValueEvalMacro: VariablesView.overrideValueEvalMacro,
+
+  /**
+   * The default simple value evaluation macro.
+   */
+  _simpleValueEvalMacro: VariablesView.simpleValueEvalMacro,
+
+  /**
+   * Populate a long string into a target using a grip.
+   *
+   * @param Variable aTarget
+   *        The target Variable/Property to put the retrieved string into.
+   * @param LongStringActor aGrip
+   *        The long string grip that use to retrieve the full string.
+   * @return Promise
+   *         The promise that will be resolved when the string is retrieved.
+   */
+  _populateFromLongString: function(aTarget, aGrip){
+    let deferred = Promise.defer();
+
+    let from = aGrip.initial.length;
+    let to = Math.min(aGrip.length, MAX_LONG_STRING_LENGTH);
+
+    this._getLongStringClient(aGrip).substring(from, to, aResponse => {
+      // Stop tracking the actor because it's no longer needed.
+      this.releaseActor(aGrip);
+
+      // Replace the preview with the full string and make it non-expandable.
+      aTarget.onexpand = null;
+      aTarget.setGrip(aGrip.initial + aResponse.substring);
+      aTarget.hideArrow();
+
+      // Mark the string as having retrieved.
+      aTarget._retrieved = true;
+      deferred.resolve();
+    });
+
+    return deferred.promise;
+  },
+
+  /**
+   * Adds properties to a Scope, Variable, or Property in the view. Triggered
+   * when a scope is expanded or certain variables are hovered.
+   *
+   * @param Scope aTarget
+   *        The Scope where the properties will be placed into.
+   * @param object aGrip
+   *        The grip to use to populate the target.
+   */
+  _populateFromObject: function(aTarget, aGrip) {
+    let deferred = Promise.defer();
+
+    this._getGripClient(aGrip).getPrototypeAndProperties(aResponse => {
+      let { ownProperties, prototype, safeGetterValues } = aResponse;
+      let sortable = VariablesView.isSortable(aGrip.class);
+
+      // Merge the safe getter values into one object such that we can use it
+      // in VariablesView.
+      for (let name of Object.keys(safeGetterValues)) {
+        if (name in ownProperties) {
+          ownProperties[name].getterValue = safeGetterValues[name].getterValue;
+          ownProperties[name].getterPrototypeLevel = safeGetterValues[name]
+                                                     .getterPrototypeLevel;
+        } else {
+          ownProperties[name] = safeGetterValues[name];
+        }
+      }
+
+      // Add all the variable properties.
+      if (ownProperties) {
+        aTarget.addItems(ownProperties, {
+          // Not all variables need to force sorted properties.
+          sorted: sortable,
+          // Expansion handlers must be set after the properties are added.
+          callback: this.addExpander
+        });
+      }
+
+      // Add the variable's __proto__.
+      if (prototype && prototype.type != "null") {
+        let proto = aTarget.addItem("__proto__", { value: prototype });
+        // Expansion handlers must be set after the properties are added.
+        this.addExpander(proto, prototype);
+      }
+
+      // Mark the variable as having retrieved all its properties.
+      aTarget._retrieved = true;
+      this.view.commitHierarchy();
+      deferred.resolve();
+    });
+
+    return deferred.promise;
+  },
+
+  /**
+   * Adds an 'onexpand' callback for a variable, lazily handling
+   * the addition of new properties.
+   *
+   * @param Variable aVar
+   *        The variable where the properties will be placed into.
+   * @param any aSource
+   *        The source to use to populate the target.
+   */
+  addExpander: function(aTarget, aSource) {
+    // Attach evaluation macros as necessary.
+    if (aTarget.getter || aTarget.setter) {
+      aTarget.evaluationMacro = this._overrideValueEvalMacro;
+
+      let getter = aTarget.get("get");
+      if (getter) {
+        getter.evaluationMacro = this._getterOrSetterEvalMacro;
+      }
+
+      let setter = aTarget.get("set");
+      if (setter) {
+        setter.evaluationMacro = this._getterOrSetterEvalMacro;
+      }
+    } else {
+      aTarget.evaluationMacro = this._simpleValueEvalMacro;
+    }
+
+    // If the source is primitive then an expander is not needed.
+    if (VariablesView.isPrimitive({ value: aSource })) {
+      return;
+    }
+
+    // If the source is a long string then show the arrow.
+    if (WebConsoleUtils.isActorGrip(aSource) && aSource.type == "longString") {
+      aTarget.showArrow();
+    }
+
+    // Make sure that properties are always available on expansion.
+    aTarget.onexpand = () => this.expand(aTarget, aSource);
+
+    // Some variables are likely to contain a very large number of properties.
+    // It's a good idea to be prepared in case of an expansion.
+    if (aTarget.shouldPrefetch) {
+      aTarget.addEventListener("mouseover", aTarget.onexpand, false);
+    }
+
+    // Register all the actors that this controller now depends on.
+    for (let grip of [aTarget.value, aTarget.getter, aTarget.setter]) {
+      if (WebConsoleUtils.isActorGrip(grip)) {
+        this._actors.add(grip.actor);
+      }
+    }
+  },
+
+  /**
+   * Adds properties to a Scope, Variable, or Property in the view. Triggered
+   * when a scope is expanded or certain variables are hovered.
+   *
+   * @param Scope aTarget
+   *        The Scope to be expanded.
+   * @param object aSource
+   *        The source to use to populate the target.
+   * @return Promise
+   *         The promise that is resolved once the target has been expanded.
+   */
+  expand: function(aTarget, aSource) {
+    // Fetch the variables only once.
+    if (aTarget._fetched) {
+      return aTarget._fetched;
+    }
+
+    let deferred = Promise.defer();
+    aTarget._fetched = deferred.promise;
+
+    if (!aSource) {
+      throw new Error("No actor grip was given for the variable.");
+    }
+
+    // If the target a Variable or Property then we're fetching properties
+    if (VariablesView.isVariable(aTarget)) {
+      this._populateFromObject(aTarget, aSource).then(() => {
+        deferred.resolve();
+        // Signal that properties have been fetched.
+        this.view.emit("fetched", "properties", aTarget);
+      });
+      return deferred.promise;
+    }
+
+    switch (aSource.type) {
+      case "longString":
+        this._populateFromLongString(aTarget, aSource).then(() => {
+          deferred.resolve();
+          // Signal that a long string has been fetched.
+          this.view.emit("fetched", "longString", aTarget);
+        });
+        break;
+      case "with":
+      case "object":
+        this._populateFromObject(aTarget, aSource.object).then(() => {
+          deferred.resolve();
+          // Signal that variables have been fetched.
+          this.view.emit("fetched", "variables", aTarget);
+        });
+        break;
+      case "block":
+      case "function":
+        // Add nodes for every argument and every other variable in scope.
+        let args = aSource.bindings.arguments;
+        if (args) {
+          for (let arg of args) {
+            let name = Object.getOwnPropertyNames(arg)[0];
+            let ref = aTarget.addItem(name, arg[name]);
+            let val = arg[name].value;
+            this.addExpander(ref, val);
+          }
+        }
+
+        aTarget.addItems(aSource.bindings.variables, {
+          // Not all variables need to force sorted properties.
+          sorted: VARIABLES_SORTING_ENABLED,
+          // Expansion handlers must be set after the properties are added.
+          callback: this.addExpander
+        });
+
+        // No need to signal that variables have been fetched, since
+        // the scope arguments and variables are already attached to the
+        // environment bindings, so pausing the active thread is unnecessary.
+
+        deferred.resolve();
+        break;
+      default:
+        let error = "Unknown Debugger.Environment type: " + aSource.type;
+        Cu.reportError(error);
+        deferred.reject(error);
+    }
+
+    return deferred.promise;
+  },
+
+  /**
+   * Release an actor from the controller.
+   *
+   * @param object aActor
+   *        The actor to release.
+   */
+  releaseActor: function(aActor){
+    if (this._releaseActor) {
+      this._releaseActor(aActor);
+    }
+    this._actors.delete(aActor);
+  },
+
+  /**
+   * Release all the actors referenced by the controller, optionally filtered.
+   *
+   * @param function aFilter [optional]
+   *        Callback to filter which actors are released.
+   */
+  releaseActors: function(aFilter) {
+    for (let actor of this._actors) {
+      if (!aFilter || aFilter(actor)) {
+        this.releaseActor(actor);
+      }
+    }
+  },
+};
+
+
+/**
+ * Attaches a VariablesViewController to a VariablesView if it doesn't already
+ * have one.
+ *
+ * @param VariablesView aView
+ *        The view to attach to.
+ * @param object aOptions
+ *        The options to use in creating the controller.
+ * @return VariablesViewController
+ */
+VariablesViewController.attach = function(aView, aOptions) {
+  if (aView.controller) {
+    return aView.controller;
+  }
+  return new VariablesViewController(aView, aOptions);
+};
--- a/browser/devtools/styleeditor/StyleEditorPanel.jsm
+++ b/browser/devtools/styleeditor/StyleEditorPanel.jsm
@@ -86,26 +86,26 @@ StyleEditorPanel.prototype = {
   },
 
   /**
    * Select a stylesheet.
    *
    * @param {string} href
    *        Url of stylesheet to find and select in editor
    * @param {number} line
-   *        Line number to jump to after selecting
+   *        Line number to jump to after selecting. One-indexed
    * @param {number} col
-   *        Column number to jump to after selecting
+   *        Column number to jump to after selecting. One-indexed
    */
   selectStyleSheet: function(href, line, col) {
     if (!this._debuggee || !this.UI) {
       return;
     }
     let stylesheet = this._debuggee.styleSheetFromHref(href);
-    this.UI.selectStyleSheet(href, line, col);
+    this.UI.selectStyleSheet(href, line - 1, col - 1);
   },
 
   /**
    * Destroy the style editor.
    */
   destroy: function() {
     if (!this._destroyed) {
       this._destroyed = true;
--- a/browser/devtools/styleeditor/StyleEditorUI.jsm
+++ b/browser/devtools/styleeditor/StyleEditorUI.jsm
@@ -43,17 +43,17 @@ function StyleEditorUI(debuggee, panelDo
   EventEmitter.decorate(this);
 
   this._debuggee = debuggee;
   this._panelDoc = panelDoc;
   this._window = this._panelDoc.defaultView;
   this._root = this._panelDoc.getElementById("style-editor-chrome");
 
   this.editors = [];
-  this.selectedStyleSheetIndex = -1;
+  this.selectedEditor = null;
 
   this._onStyleSheetCreated = this._onStyleSheetCreated.bind(this);
   this._onStyleSheetsCleared = this._onStyleSheetsCleared.bind(this);
   this._onDocumentLoad = this._onDocumentLoad.bind(this);
   this._onError = this._onError.bind(this);
 
   debuggee.on("document-load", this._onDocumentLoad);
   debuggee.on("stylesheets-cleared", this._onStyleSheetsCleared);
@@ -79,16 +79,24 @@ StyleEditorUI.prototype = {
 
   /*
    * Mark the style editor as having or not having unsaved changes.
    */
   set isDirty(value) {
     this._markedDirty = value;
   },
 
+  /*
+   * Index of selected stylesheet in document.styleSheets
+   */
+  get selectedStyleSheetIndex() {
+    return this.selectedEditor ?
+           this.selectedEditor.styleSheet.styleSheetIndex : -1;
+  },
+
   /**
    * Build the initial UI and wire buttons with event handlers.
    */
   createUI: function() {
     let viewRoot = this._root.parentNode.querySelector(".splitview-root");
 
     this._view = new SplitView(viewRoot);
 
@@ -135,20 +143,27 @@ StyleEditorUI.prototype = {
 
     showFilePicker(file, false, parentWindow, onFileSelected);
   },
 
   /**
    * Handler for debuggee's 'stylesheets-cleared' event. Remove all editors.
    */
   _onStyleSheetsCleared: function() {
-    this._clearStyleSheetEditors();
+    // remember selected sheet and line number for next load
+    if (this.selectedEditor) {
+      let href = this.selectedEditor.styleSheet.href;
+      let {line, col} = this.selectedEditor.sourceEditor.getCaretPosition();
+      this.selectStyleSheet(href, line, col);
+    }
 
+    this._clearStyleSheetEditors();
     this._view.removeAll();
-    this.selectedStyleSheetIndex = -1;
+
+    this.selectedEditor = null;
 
     this._root.classList.add("loading");
   },
 
   /**
    * When a new or imported stylesheet has been added to the document.
    * Add an editor for it.
    */
@@ -161,21 +176,32 @@ StyleEditorUI.prototype = {
    * for all style sheets in the document
    *
    * @param {string} event
    *        Event name
    * @param {StyleSheet} styleSheet
    *        StyleSheet object for new sheet
    */
   _onDocumentLoad: function(event, styleSheets) {
+    if (this._styleSheetToSelect) {
+      // if selected stylesheet from previous load isn't here,
+      // just set first stylesheet to be selected instead
+      let selectedExists = styleSheets.some((sheet) => {
+        return this._styleSheetToSelect.href == sheet.href;
+      })
+      if (!selectedExists) {
+        this._styleSheetToSelect = null;
+      }
+    }
     for (let sheet of styleSheets) {
       this._addStyleSheetEditor(sheet);
     }
-    // this might be the first stylesheet, so remove loading indicator
+
     this._root.classList.remove("loading");
+
     this.emit("document-load");
   },
 
   /**
    * Forward any error from a stylesheet.
    *
    * @param  {string} event
    *         Event name
@@ -290,78 +316,78 @@ StyleEditorUI.prototype = {
           this._selectEditor(editor);
         }
 
         this.emit("editor-added", editor);
       }.bind(this),
 
       onShow: function(summary, details, data) {
         let editor = data.editor;
+        this.selectedEditor = editor;
+        this._styleSheetToSelect = null;
+
         if (!editor.sourceEditor) {
           // only initialize source editor when we switch to this view
           let inputElement = details.querySelector(".stylesheet-editor-input");
           editor.load(inputElement);
         }
         editor.onShow();
-      }
+
+        this.emit("editor-selected", editor);
+      }.bind(this)
     });
   },
 
   /**
    * Switch to the editor that has been marked to be selected.
    */
   switchToSelectedSheet: function() {
     let sheet = this._styleSheetToSelect;
 
     for each (let editor in this.editors) {
       if (editor.styleSheet.href == sheet.href) {
         this._selectEditor(editor, sheet.line, sheet.col);
-        this._styleSheetToSelect = null;
         break;
       }
     }
   },
 
   /**
    * Select an editor in the UI.
    *
    * @param  {StyleSheetEditor} editor
    *         Editor to switch to.
    * @param  {number} line
    *         Line number to jump to
    * @param  {number} col
    *         Column number to jump to
    */
   _selectEditor: function(editor, line, col) {
-    line = line || 1;
-    col = col || 1;
-
-    this.selectedStyleSheetIndex = editor.styleSheet.styleSheetIndex;
+    line = line || 0;
+    col = col || 0;
 
     editor.getSourceEditor().then(() => {
-      editor.sourceEditor.setCaretPosition(line - 1, col - 1);
+      editor.sourceEditor.setCaretPosition(line, col);
     });
 
     this._view.activeSummary = editor.summary;
-
-    this.emit("editor-selected", editor);
   },
 
   /**
    * selects a stylesheet and optionally moves the cursor to a selected line
    *
    * @param {string} [href]
    *        Href of stylesheet that should be selected. If a stylesheet is not passed
    *        and the editor is not initialized we focus the first stylesheet. If
    *        a stylesheet is not passed and the editor is initialized we ignore
    *        the call.
    * @param {Number} [line]
-   *        Line to which the caret should be moved (one-indexed).
+   *        Line to which the caret should be moved (zero-indexed).
    * @param {Number} [col]
-   *        Column to which the caret should be moved (one-indexed).
+   *        Column to which the caret should be moved (zero-indexed).
    */
   selectStyleSheet: function(href, line, col)
   {
     let alreadyCalled = !!this._styleSheetToSelect;
 
     this._styleSheetToSelect = {
       href: href,
       line: line,
--- a/browser/devtools/styleeditor/test/Makefile.in
+++ b/browser/devtools/styleeditor/test/Makefile.in
@@ -23,16 +23,17 @@ include $(topsrcdir)/config/rules.mk
                  browser_styleeditor_new.js \
                  browser_styleeditor_pretty.js \
                  browser_styleeditor_private_perwindowpb.js \
                  browser_styleeditor_sv_keynav.js \
                  browser_styleeditor_sv_resize.js \
                  browser_styleeditor_bug_740541_iframes.js \
                  browser_styleeditor_bug_851132_middle_click.js \
                  browser_styleeditor_nostyle.js \
+                 browser_styleeditor_reload.js \
                  head.js \
                  four.html \
                  head.js \
                  import.css \
                  import.html \
                  import2.css \
                  longload.html \
                  media.html \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_reload.js
@@ -0,0 +1,99 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
+const NEW_URI = TEST_BASE_HTTPS + "media.html";
+
+const LINE_NO = 5;
+const COL_NO = 3;
+
+let gContentWin;
+let gUI;
+
+function test()
+{
+  waitForExplicitFinish();
+
+  addTabAndOpenStyleEditor(function(panel) {
+    gContentWin = gBrowser.selectedTab.linkedBrowser.contentWindow.wrappedJSObject;
+    gUI = panel.UI;
+
+    let count = 0;
+    gUI.on("editor-added", function editorAdded(event, editor) {
+      if (++count == 2) {
+        gUI.off("editor-added", editorAdded);
+        gUI.editors[0].getSourceEditor().then(runTests);
+      }
+    })
+  });
+
+  content.location = TESTCASE_URI;
+}
+
+function runTests()
+{
+  let count = 0;
+  gUI.once("editor-selected", (event, editor) => {
+    editor.getSourceEditor().then(() => {
+      info("selected second editor, about to reload page");
+      reloadPage();
+
+      gUI.on("editor-added", function editorAdded(event, editor) {
+        if (++count == 2) {
+          gUI.off("editor-added", editorAdded);
+          gUI.editors[1].getSourceEditor().then(testRemembered);
+        }
+      })
+    });
+  });
+  gUI.selectStyleSheet(gUI.editors[1].styleSheet.href, LINE_NO, COL_NO);
+}
+
+function testRemembered()
+{
+  is(gUI.selectedEditor, gUI.editors[1], "second editor is selected");
+
+  let {line, col} = gUI.selectedEditor.sourceEditor.getCaretPosition();
+  is(line, LINE_NO, "correct line selected");
+  is(col, COL_NO, "correct column selected");
+
+  testNewPage();
+}
+
+function testNewPage()
+{
+  let count = 0;
+  gUI.on("editor-added", function editorAdded(event, editor) {
+    info("editor added here")
+    if (++count == 2) {
+      gUI.off("editor-added", editorAdded);
+      gUI.editors[0].getSourceEditor().then(testNotRemembered);
+    }
+  })
+
+  info("navigating to a different page");
+  navigatePage();
+}
+
+function testNotRemembered()
+{
+  is(gUI.selectedEditor, gUI.editors[0], "first editor is selected");
+
+  let {line, col} = gUI.selectedEditor.sourceEditor.getCaretPosition();
+  is(line, 0, "first line is selected");
+  is(col, 0, "first column is selected");
+
+  gUI = null;
+  finish();
+}
+
+function reloadPage()
+{
+  gContentWin.location.reload();
+}
+
+function navigatePage()
+{
+  gContentWin.location = NEW_URI;
+}
\ No newline at end of file
--- a/browser/devtools/webconsole/test/Makefile.in
+++ b/browser/devtools/webconsole/test/Makefile.in
@@ -44,16 +44,17 @@ MOCHITEST_BROWSER_FILES = \
 	browser_webconsole_output_order.js \
 	browser_webconsole_property_provider.js \
 	browser_webconsole_bug_587617_output_copy.js \
 	browser_webconsole_bug_585237_line_limit.js \
 	browser_webconsole_bug_582201_duplicate_errors.js \
 	browser_webconsole_bug_580454_timestamp_l10n.js \
 	browser_webconsole_netlogging.js \
 	browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js \
+	browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js \
 	browser_webconsole_bug_594477_clickable_output.js \
 	browser_webconsole_bug_589162_css_filter.js \
 	browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js \
 	browser_webconsole_bug_595350_multiple_windows_and_tabs.js \
 	browser_webconsole_bug_594497_history_arrow_keys.js \
 	browser_webconsole_bug_588342_document_focus.js \
 	browser_webconsole_bug_595934_message_categories.js \
 	browser_webconsole_bug_601352_scroll.js \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js
@@ -0,0 +1,39 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/browser/test-console.html";
+
+function test() {
+  addTab(TEST_URI);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, testInputChange);
+  }, true);
+}
+
+function testInputChange(hud) {
+  var jsterm = hud.jsterm;
+  var input = jsterm.inputNode;
+
+  is(input.getAttribute("focused"), "true", "input has focus");
+  EventUtils.synthesizeKey("VK_TAB", {});
+  is(input.getAttribute("focused"), "", "focus moved away");
+
+  // Test user changed something
+  input.focus();
+  EventUtils.synthesizeKey("A", {});
+  EventUtils.synthesizeKey("VK_TAB", {});
+  is(input.getAttribute("focused"), "true", "input is still focused");
+
+  // Test non empty input but not changed since last focus
+  input.blur();
+  input.focus();
+  EventUtils.synthesizeKey("VK_RIGHT", {});
+  EventUtils.synthesizeKey("VK_TAB", {});
+  is(input.getAttribute("focused"), "", "input moved away");
+
+  jsterm = input = null;
+  finishTest();
+}
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_770099_violation.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_770099_violation.js
@@ -3,17 +3,17 @@
  * Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  *
  * ***** END LICENSE BLOCK ***** */
 
 // Tests that the Web Console CSP messages are displayed
 
 const TEST_VIOLATION = "https://example.com/browser/browser/devtools/webconsole/test/test_bug_770099_violation.html";
-const CSP_VIOLATION_MSG = "CSP WARN:  Directive default-src https://example.com:443 violated by http://some.example.com/test.png"
+const CSP_VIOLATION_MSG = "Content Security Policy: Directive default-src https://example.com:443 violated by http://some.example.com/test.png"
 
 let hud = undefined;
 
 function test() {
   addTab("data:text/html;charset=utf8,Web Console CSP violation test");
   browser.addEventListener("load", function _onLoad() {
     browser.removeEventListener("load", _onLoad, true);
     openConsole(null, loadDocument);
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -32,16 +32,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/devtools/WebConsoleUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/commonjs/sdk/core/promise.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "VariablesView",
                                   "resource:///modules/devtools/VariablesView.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "VariablesViewController",
+                                  "resource:///modules/devtools/VariablesViewController.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
                                   "resource:///modules/devtools/shared/event-emitter.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "devtools",
                                   "resource://gre/modules/devtools/Loader.jsm");
 
 const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
 let l10n = new WebConsoleUtils.l10n(STRINGS_URI);
@@ -2099,23 +2102,19 @@ WebConsoleFrame.prototype = {
     }
     else if (aNode._connectionId &&
              aNode.classList.contains("webconsole-msg-network")) {
       delete this._networkRequests[aNode._connectionId];
       this._releaseObject(aNode._connectionId);
     }
     else if (aNode.classList.contains("webconsole-msg-inspector")) {
       let view = aNode._variablesView;
-      let actors = view ?
-                   this.jsterm._objectActorsInVariablesViews.get(view) :
-                   new Set();
-      for (let actor of actors) {
-        this._releaseObject(actor);
+      if (view) {
+        view.controller.releaseActors();
       }
-      actors.clear();
       aNode._variablesView = null;
     }
 
     if (aNode.parentNode) {
       aNode.parentNode.removeChild(aNode);
     }
   },
 
@@ -2738,16 +2737,45 @@ WebConsoleFrame.prototype = {
     else {
       onDestroy();
     }
 
     return this._destroyer.promise;
   },
 };
 
+
+/**
+ * @see VariablesView.simpleValueEvalMacro
+ */
+function simpleValueEvalMacro(aItem, aCurrentString)
+{
+  return VariablesView.simpleValueEvalMacro(aItem, aCurrentString, "_self");
+};
+
+
+/**
+ * @see VariablesView.overrideValueEvalMacro
+ */
+function overrideValueEvalMacro(aItem, aCurrentString)
+{
+  return VariablesView.overrideValueEvalMacro(aItem, aCurrentString, "_self");
+};
+
+
+/**
+ * @see VariablesView.getterOrSetterEvalMacro
+ */
+function getterOrSetterEvalMacro(aItem, aCurrentString)
+{
+  return VariablesView.getterOrSetterEvalMacro(aItem, aCurrentString, "_self");
+}
+
+
+
 /**
  * Create a JSTerminal (a JavaScript command line). This is attached to an
  * existing HeadsUpDisplay (a Web Console instance). This code is responsible
  * with handling command line input, code evaluation and result output.
  *
  * @constructor
  * @param object aWebConsoleFrame
  *        The WebConsoleFrame object that owns this JSTerm instance.
@@ -2764,20 +2792,19 @@ function JSTerm(aWebConsoleFrame)
   // this.execute().
   this.historyIndex = 0; // incremented on this.execute()
 
   // Holds the index of the history entry that the user is currently viewing.
   // This is reset to this.history.length when this.execute() is invoked.
   this.historyPlaceHolder = 0;
   this._objectActorsInVariablesViews = new Map();
 
-  this._keyPress = this.keyPress.bind(this);
-  this._inputEventHandler = this.inputEventHandler.bind(this);
-  this._fetchVarProperties = this._fetchVarProperties.bind(this);
-  this._fetchVarLongString = this._fetchVarLongString.bind(this);
+  this._keyPress = this._keyPress.bind(this);
+  this._inputEventHandler = this._inputEventHandler.bind(this);
+  this._focusEventHandler = this._focusEventHandler.bind(this);
   this._onKeypressInVariablesView = this._onKeypressInVariablesView.bind(this);
 
   EventEmitter.decorate(this);
 }
 
 JSTerm.prototype = {
   SELECTED_FRAME: -1,
 
@@ -2822,21 +2849,28 @@ JSTerm.prototype = {
 
   /**
    * Last input value.
    * @type string
    */
   lastInputValue: "",
 
   /**
+   * Indicate input node changed since last focus.
+   *
+   * @private
+   * @type boolean
+   */
+  _inputChanged: false,
+
+  /**
    * History of code that was executed.
    * @type array
    */
   history: null,
-
   autocompletePopup: null,
   inputNode: null,
   completeNode: null,
 
   /**
    * Getter for the element that holds the messages we display.
    * @type nsIDOMElement
    */
@@ -2872,16 +2906,17 @@ JSTerm.prototype = {
                                                    autocompleteOptions);
 
     let doc = this.hud.document;
     this.completeNode = doc.querySelector(".jsterm-complete-node");
     this.inputNode = doc.querySelector(".jsterm-input-node");
     this.inputNode.addEventListener("keypress", this._keyPress, false);
     this.inputNode.addEventListener("input", this._inputEventHandler, false);
     this.inputNode.addEventListener("keyup", this._inputEventHandler, false);
+    this.inputNode.addEventListener("focus", this._focusEventHandler, false);
 
     this.lastInputValue && this.setInputValue(this.lastInputValue);
   },
 
   /**
    * The JavaScript evaluation response handler.
    *
    * @private
@@ -3277,17 +3312,37 @@ JSTerm.prototype = {
   _createVariablesView: function JST__createVariablesView(aOptions)
   {
     let view = new VariablesView(aOptions.container);
     view.searchPlaceholder = l10n.getStr("propertiesFilterPlaceholder");
     view.emptyText = l10n.getStr("emptyPropertiesList");
     view.searchEnabled = !aOptions.hideFilterInput;
     view.lazyEmpty = this._lazyVariablesView;
     view.lazyAppend = this._lazyVariablesView;
-    this._objectActorsInVariablesViews.set(view, new Set());
+
+    VariablesViewController.attach(view, {
+      getGripClient: aGrip => {
+        return new GripClient(this.hud.proxy.client, aGrip);
+      },
+      getLongStringClient: aGrip => {
+        return this.webConsoleClient.longString(aGrip);
+      },
+      releaseActor: aActor => {
+        this.hud._releaseObject(aActor);
+      },
+      simpleValueEvalMacro: simpleValueEvalMacro,
+      overrideValueEvalMacro: overrideValueEvalMacro,
+      getterOrSetterEvalMacro: getterOrSetterEvalMacro,
+    });
+
+    // Relay events from the VariablesView.
+    view.on("fetched", (aEvent, aType, aVar) => {
+      this.emit("variablesview-fetched", aVar);
+    });
+
     return view;
   },
 
   /**
    * Update the variables view.
    *
    * @private
    * @param object aOptions
@@ -3299,26 +3354,21 @@ JSTerm.prototype = {
    *        - label: the new label for the inspected object.
    */
   _updateVariablesView: function JST__updateVariablesView(aOptions)
   {
     let view = aOptions.view;
     view.createHierarchy();
     view.empty();
 
-    let actors = this._objectActorsInVariablesViews.get(view);
-    for (let actor of actors) {
-      // We need to avoid pruning the object inspection starting point.
-      // That one is pruned when the console message is removed.
-      if (view._consoleLastObjectActor != actor) {
-        this.hud._releaseObject(actor);
-      }
-    }
-
-    actors.clear();
+    // We need to avoid pruning the object inspection starting point.
+    // That one is pruned when the console message is removed.
+    view.controller.releaseActors(aActor => {
+      return view._consoleLastObjectActor != aActor;
+    });
 
     if (aOptions.objectActor) {
       // Make sure eval works in the correct context.
       view.eval = this._variablesViewEvaluate.bind(this, aOptions);
       view.switch = this._variablesViewSwitch.bind(this, aOptions);
       view.delete = this._variablesViewDelete.bind(this, aOptions);
     }
     else {
@@ -3326,21 +3376,21 @@ JSTerm.prototype = {
       view.switch = null;
       view.delete = null;
     }
 
     let scope = view.addScope(aOptions.label);
     scope.expanded = true;
     scope.locked = true;
 
-    let container = scope.addVar();
-    container.evaluationMacro = this._variablesViewSimpleValueEvalMacro;
+    let container = scope.addItem();
+    container.evaluationMacro = simpleValueEvalMacro;
 
     if (aOptions.objectActor) {
-      this._fetchVarProperties(container, aOptions.objectActor);
+      view.controller.expand(container, aOptions.objectActor);
       view._consoleLastObjectActor = aOptions.objectActor.actor;
     }
     else if (aOptions.rawObject) {
       container.populate(aOptions.rawObject);
       view.commitHierarchy();
       view._consoleLastObjectActor = null;
     }
     else {
@@ -3370,90 +3420,16 @@ JSTerm.prototype = {
       frame: this.SELECTED_FRAME,
       bindObjectActor: aOptions.objectActor.actor,
     };
 
     this.requestEvaluation(aString, evalOptions).then(onEval, onEval);
   },
 
   /**
-   * Generates the string evaluated when performing simple value changes in the
-   * variables view.
-   *
-   * @private
-   * @param Variable | Property aItem
-   *        The current variable or property.
-   * @param string aCurrentString
-   *        The trimmed user inputted string.
-   * @return string
-   *         The string to be evaluated.
-   */
-  _variablesViewSimpleValueEvalMacro:
-  function JST__variablesViewSimpleValueEvalMacro(aItem, aCurrentString)
-  {
-    return "_self" + aItem.symbolicName + "=" + aCurrentString;
-  },
-
-
-  /**
-   * Generates the string evaluated when overriding getters and setters with
-   * plain values in the variables view.
-   *
-   * @private
-   * @param Property aItem
-   *        The current getter or setter property.
-   * @param string aCurrentString
-   *        The trimmed user inputted string.
-   * @return string
-   *         The string to be evaluated.
-   */
-  _variablesViewOverrideValueEvalMacro:
-  function JST__variablesViewOverrideValueEvalMacro(aItem, aCurrentString)
-  {
-    let parent = aItem.ownerView;
-    let symbolicName = parent.symbolicName;
-    if (symbolicName.indexOf("_self") != 0) {
-      parent._symbolicName = "_self" + symbolicName;
-    }
-
-    let result = VariablesView.overrideValueEvalMacro.apply(this, arguments);
-
-    parent._symbolicName = symbolicName;
-
-    return result;
-  },
-
-  /**
-   * Generates the string evaluated when performing getters and setters changes
-   * in the variables view.
-   *
-   * @private
-   * @param Property aItem
-   *        The current getter or setter property.
-   * @param string aCurrentString
-   *        The trimmed user inputted string.
-   * @return string
-   *         The string to be evaluated.
-   */
-  _variablesViewGetterOrSetterEvalMacro:
-  function JST__variablesViewGetterOrSetterEvalMacro(aItem, aCurrentString)
-  {
-    let propertyObject = aItem.ownerView;
-    let parentObject = propertyObject.ownerView;
-    let parent = parentObject.symbolicName;
-    parentObject._symbolicName = "_self" + parent;
-
-    let result = VariablesView.getterOrSetterEvalMacro.apply(this, arguments);
-
-    parentObject._symbolicName = parent;
-
-    return result;
-  },
-
-  /**
    * The property deletion function used by the variables view when a property
    * is deleted.
    *
    * @private
    * @param object aOptions
    *        The options used for |this._updateVariablesView()|.
    * @param object aVar
    *        The Variable object instance for the deleted property.
@@ -3551,154 +3527,17 @@ JSTerm.prototype = {
       if (WebConsoleUtils.isActorGrip(grip)) {
         this.hud._releaseObject(grip.actor);
       }
     }
 
     aCallback && aCallback(aResponse);
   },
 
-  /**
-   * Adds properties to a variable in the view. Triggered when a variable is
-   * expanded. It does not expand the variable.
-   *
-   * @param object aVar
-   *        The VariablseView Variable instance where the properties get added.
-   * @param object [aGrip]
-   *        Optional, the object actor grip of the variable. If the grip is not
-   *        provided, then the aVar.value is used as the object actor grip.
-   */
-  _fetchVarProperties: function JST__fetchVarProperties(aVar, aGrip)
-  {
-    // Retrieve the properties only once.
-    if (aVar._fetched) {
-      return;
-    }
-    aVar._fetched = true;
-
-    let grip = aGrip || aVar.value;
-    if (!grip) {
-      throw new Error("No object actor grip was given for the variable.");
-    }
-
-    let view = aVar._variablesView;
-    let actors = this._objectActorsInVariablesViews.get(view);
-
-    function addActorForDescriptor(aGrip) {
-      if (WebConsoleUtils.isActorGrip(aGrip)) {
-        actors.add(aGrip.actor);
-      }
-    }
-
-    let onNewProperty = (aProperty) => {
-      if (aProperty.getter || aProperty.setter) {
-        aProperty.evaluationMacro = this._variablesViewOverrideValueEvalMacro;
-        let getter = aProperty.get("get");
-        let setter = aProperty.get("set");
-        if (getter) {
-          getter.evaluationMacro = this._variablesViewGetterOrSetterEvalMacro;
-        }
-        if (setter) {
-          setter.evaluationMacro = this._variablesViewGetterOrSetterEvalMacro;
-        }
-      }
-      else {
-        aProperty.evaluationMacro = this._variablesViewSimpleValueEvalMacro;
-      }
-
-      let grips = [aProperty.value, aProperty.getter, aProperty.setter];
-      grips.forEach(addActorForDescriptor);
-
-      let inspectable = !VariablesView.isPrimitive({ value: aProperty.value });
-      let longString = WebConsoleUtils.isActorGrip(aProperty.value) &&
-                       aProperty.value.type == "longString";
-      if (inspectable) {
-        aProperty.onexpand = this._fetchVarProperties;
-      }
-      else if (longString) {
-        aProperty.onexpand = this._fetchVarLongString;
-        aProperty.showArrow();
-      }
-    };
-
-    let client = new GripClient(this.hud.proxy.client, grip);
-    client.getPrototypeAndProperties((aResponse) => {
-      let { ownProperties, prototype, safeGetterValues } = aResponse;
-      let sortable = VariablesView.NON_SORTABLE_CLASSES.indexOf(grip.class) == -1;
-
-      // Merge the safe getter values into one object such that we can use it
-      // in VariablesView.
-      for (let name of Object.keys(safeGetterValues)) {
-        if (name in ownProperties) {
-          ownProperties[name].getterValue = safeGetterValues[name].getterValue;
-          ownProperties[name].getterPrototypeLevel = safeGetterValues[name]
-                                                     .getterPrototypeLevel;
-        }
-        else {
-          ownProperties[name] = safeGetterValues[name];
-        }
-      }
-
-      // Add all the variable properties.
-      if (ownProperties) {
-        aVar.addProperties(ownProperties, {
-          sorted: sortable,
-          callback: onNewProperty,
-        });
-      }
-
-      // Add the variable's __proto__.
-      if (prototype && prototype.type != "null") {
-        let proto = aVar.addProperty("__proto__", { value: prototype });
-        onNewProperty(proto);
-      }
-
-      aVar._retrieved = true;
-      view.commitHierarchy();
-      this.emit("variablesview-fetched", aVar);
-    });
-  },
-
-  /**
-   * Fetch the full string for a given variable that displays a long string.
-   *
-   * @param object aVar
-   *        The VariablesView Variable instance where the properties get added.
-   */
-  _fetchVarLongString: function JST__fetchVarLongString(aVar)
-  {
-    if (aVar._fetched) {
-      return;
-    }
-    aVar._fetched = true;
-
-    let grip = aVar.value;
-    if (!grip) {
-      throw new Error("No long string actor grip was given for the variable.");
-    }
-
-    let client = this.webConsoleClient.longString(grip);
-    let toIndex = Math.min(grip.length, MAX_LONG_STRING_LENGTH);
-    client.substring(grip.initial.length, toIndex, (aResponse) => {
-      if (aResponse.error) {
-        Cu.reportError("JST__fetchVarLongString substring failure: " +
-                       aResponse.error + ": " + aResponse.message);
-        return;
-      }
-
-      aVar.onexpand = null;
-      aVar.setGrip(grip.initial + aResponse.substring);
-      aVar.hideArrow();
-      aVar._retrieved = true;
-
-      if (toIndex != grip.length) {
-        this.hud.logWarningAboutStringTooLong();
-      }
-    });
-  },
+
 
   /**
    * Writes a JS object to the JSTerm outputNode.
    *
    * @param string aOutputMessage
    *        The message to display.
    * @param function [aCallback]
    *        Optional function to invoke when users click the message.
@@ -3825,38 +3664,40 @@ JSTerm.prototype = {
    * @returns void
    */
   setInputValue: function JST_setInputValue(aNewValue)
   {
     this.inputNode.value = aNewValue;
     this.lastInputValue = aNewValue;
     this.completeNode.value = "";
     this.resizeInput();
+    this._inputChanged = true;
   },
 
   /**
    * The inputNode "input" and "keyup" event handler.
-   *
-   * @param nsIDOMEvent aEvent
+   * @private
    */
-  inputEventHandler: function JSTF_inputEventHandler(aEvent)
+  _inputEventHandler: function JST__inputEventHandler()
   {
     if (this.lastInputValue != this.inputNode.value) {
       this.resizeInput();
       this.complete(this.COMPLETE_HINT_ONLY);
       this.lastInputValue = this.inputNode.value;
+      this._inputChanged = true;
     }
   },
 
   /**
    * The inputNode "keypress" event handler.
    *
+   * @private
    * @param nsIDOMEvent aEvent
    */
-  keyPress: function JSTF_keyPress(aEvent)
+  _keyPress: function JST__keyPress(aEvent)
   {
     if (aEvent.ctrlKey) {
       let inputNode = this.inputNode;
       let closePopup = false;
       switch (aEvent.charCode) {
         case 97:
           // control-a
           if (Services.appinfo.OS == "WINNT") {
@@ -3981,28 +3822,36 @@ JSTerm.prototype = {
 
       case Ci.nsIDOMKeyEvent.DOM_VK_TAB:
         // Generate a completion and accept the first proposed value.
         if (this.complete(this.COMPLETE_HINT_ONLY) &&
             this.lastCompletion &&
             this.acceptProposedCompletion()) {
           aEvent.preventDefault();
         }
-        else {
+        else if (this._inputChanged) {
           this.updateCompleteNode(l10n.getStr("Autocomplete.blank"));
           aEvent.preventDefault();
         }
         break;
-
       default:
         break;
     }
   },
 
   /**
+   * The inputNode "focus" event handler.
+   * @private
+   */
+  _focusEventHandler: function JST__focusEventHandler()
+  {
+    this._inputChanged = false;
+  },
+
+  /**
    * Go up/down the history stack of input values.
    *
    * @param number aDirection
    *        History navigation direction: HISTORY_BACK or HISTORY_FORWARD.
    *
    * @returns boolean
    *          True if the input value changed, false otherwise.
    */
@@ -4353,21 +4202,17 @@ JSTerm.prototype = {
 
   /**
    * Destroy the sidebar.
    * @private
    */
   _sidebarDestroy: function JST__sidebarDestroy()
   {
     if (this._variablesView) {
-      let actors = this._objectActorsInVariablesViews.get(this._variablesView);
-      for (let actor of actors) {
-        this.hud._releaseObject(actor);
-      }
-      actors.clear();
+      this._variablesView.controller.releaseActors();
       this._variablesView = null;
     }
 
     if (this.sidebar) {
       this.sidebar.hide();
       this.sidebar.destroy();
       this.sidebar = null;
     }
@@ -4392,16 +4237,17 @@ JSTerm.prototype = {
                 .getElementById("webConsole_autocompletePopup");
     if (popup) {
       popup.parentNode.removeChild(popup);
     }
 
     this.inputNode.removeEventListener("keypress", this._keyPress, false);
     this.inputNode.removeEventListener("input", this._inputEventHandler, false);
     this.inputNode.removeEventListener("keyup", this._inputEventHandler, false);
+    this.inputNode.removeEventListener("focus", this._focusEventHandler, false);
 
     this.hud = null;
   },
 };
 
 /**
  * Utils: a collection of globally used functions.
  */
--- a/browser/fuel/src/Makefile.in
+++ b/browser/fuel/src/Makefile.in
@@ -5,11 +5,11 @@
 DEPTH     = @DEPTH@
 topsrcdir = @top_srcdir@
 srcdir    = @srcdir@
 VPATH     = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 DISABLED_EXTRA_COMPONENTS = fuelApplication.manifest
-EXTRA_PP_COMPONENTS = fuelApplication.js
+DISABLED_EXTRA_PP_COMPONENTS = fuelApplication.js
 
 include $(topsrcdir)/config/rules.mk
--- a/browser/fuel/src/moz.build
+++ b/browser/fuel/src/moz.build
@@ -4,8 +4,12 @@
 # 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/.
 
 MODULE = 'fuel'
 
 EXTRA_COMPONENTS += [
     'fuelApplication.manifest',
 ]
+
+EXTRA_PP_COMPONENTS += [
+    'fuelApplication.js',
+]
--- a/browser/installer/windows/nsis/installer.nsi
+++ b/browser/installer/windows/nsis/installer.nsi
@@ -336,17 +336,17 @@ Section "-Application" APP_IDX
   ${GetLongPath} "$INSTDIR\${FileMainEXE}" $8
   StrCpy $2 "$\"$8$\" -osint -url $\"%1$\""
 
   ; In Win8, the delegate execute handler picks up the value in FirefoxURL and
   ; FirefoxHTML to launch the desktop browser when it needs to.
   ${AddDisabledDDEHandlerValues} "FirefoxHTML" "$2" "$8,1" \
                                  "${AppRegName} Document" ""
   ${AddDisabledDDEHandlerValues} "FirefoxURL" "$2" "$8,1" "${AppRegName} URL" \
-                                 "true"
+                                 "delete"
 
   ; For pre win8, the following keys should only be set if we can write to HKLM.
   ; For post win8, the keys below get set in both HKLM and HKCU.
   ${If} $TmpVal == "HKLM"
     ; Set the Start Menu Internet and Vista Registered App HKLM registry keys.
     ${SetStartMenuInternet} "HKLM"
     ${FixShellIconHandler} "HKLM"
 
--- a/browser/installer/windows/nsis/shared.nsh
+++ b/browser/installer/windows/nsis/shared.nsh
@@ -407,17 +407,17 @@ FunctionEnd
   ${EndIf}
 
   ; An empty string is used for the 5th param because FirefoxHTML is not a
   ; protocol handler
   ${AddDisabledDDEHandlerValues} "FirefoxHTML" "$2" "$8,1" \
                                  "${AppRegName} HTML Document" ""
 
   ${AddDisabledDDEHandlerValues} "FirefoxURL" "$2" "$8,1" "${AppRegName} URL" \
-                                 "true"
+                                 "delete"
   Call RegisterCEH
 
   ; An empty string is used for the 4th & 5th params because the following
   ; protocol handlers already have a display name and the additional keys
   ; required for a protocol handler.
   ${AddDisabledDDEHandlerValues} "ftp" "$2" "$8,1" "" ""
   ${AddDisabledDDEHandlerValues} "http" "$2" "$8,1" "" ""
   ${AddDisabledDDEHandlerValues} "https" "$2" "$8,1" "" ""
@@ -645,17 +645,17 @@ FunctionEnd
     ; protocol handler.
     ${AddDisabledDDEHandlerValues} "FirefoxHTML" "$2" "$8,1" \
                                    "${AppRegName} HTML Document" ""
   ${EndIf}
 
   ${IsHandlerForInstallDir} "FirefoxURL" $R9
   ${If} "$R9" == "true"
     ${AddDisabledDDEHandlerValues} "FirefoxURL" "$2" "$8,1" \
-                                   "${AppRegName} URL" "true"
+                                   "${AppRegName} URL" "delete"
   ${EndIf}
 
   ; An empty string is used for the 4th & 5th params because the following
   ; protocol handlers already have a display name and the additional keys
   ; required for a protocol handler.
   ${IsHandlerForInstallDir} "ftp" $R9
   ${If} "$R9" == "true"
     ${AddDisabledDDEHandlerValues} "ftp" "$2" "$8,1" "" ""
--- a/browser/locales/en-US/chrome/browser/devtools/profiler.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/profiler.properties
@@ -15,20 +15,20 @@
 # displayed inside the developer tools window and in the Developer Tools Menu.
 profiler.label=Profiler
 
 # LOCALIZATION NOTE (profiler2.commandkey, profiler.accesskey)
 # Used for the menuitem in the tool menu
 profiler2.commandkey=VK_F5
 profiler.accesskey=P
 
-# LOCALIZATION NOTE (profiler.tooltip):
+# LOCALIZATION NOTE (profiler.tooltip2):
 # This string is displayed in the tooltip of the tab when the profiler is
 # displayed inside the developer tools window.
-profiler.tooltip=Profiler
+profiler.tooltip2=JavaScript Profiler
 
 # LOCALIZATION NOTE (profiler.profileName):
 # This string is the default name for new profiles. Its parameter is a number.
 # For example: "Profile 1", "Profile 2", etc.
 profiler.profileName=Profile %S
 
 # LOCALIZATION NOTE (profiler.completeProfile):
 # This string is displayed as a tab in the profiler UI. Clicking on it
@@ -77,8 +77,23 @@ profiler.stop=Stop
 # This string is displayed above the progress bar when the profiler
 # is busy loading and parsing the report.
 profiler.loading=Loading profileā€¦
 
 # LOCALIZATION NOTE (profiler.alreadyRunning)
 # This string is displayed in the profiler whenever there is already
 # another running profile. Users can run only one profile at a time.
 profiler.alreadyRunning=Profiler is already running. If you want to run this profile stop Profile %S first.
+
+# LOCALIZATION NOTE (profiler.stateIdle)
+# This string is used to show that the profile in question is in IDLE
+# state meaning that it hasn't been started yet.
+profiler.stateIdle=Idle
+
+# LOCALIZATION NOTE (profiler.stateRunning)
+# This string is used to show that the profile in question is in RUNNING
+# state meaning that it has been started and currently gathering profile data.
+profiler.stateRunning=Running
+
+# LOCALIZATION NOTE (profiler.stateCompleted)
+# This string is used to show that the profile in question is in COMPLETED
+# state meaning that it has been started and stopped already.
+profiler.stateCompleted=Completed
--- a/browser/metro/base/content/bindings/autocomplete.xml
+++ b/browser/metro/base/content/bindings/autocomplete.xml
@@ -17,31 +17,47 @@
   <binding id="autocomplete" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete">
     <implementation>
       <constructor>
           <![CDATA[
             this.minResultsForPopup = 0;
             this.popup._input = this;
           ]]>
       </constructor>
+
       <method name="openPopup">
         <body>
           <![CDATA[
             this.popup.openAutocompletePopup(this, null);
           ]]>
         </body>
       </method>
 
       <method name="closePopup">
         <body>
           <![CDATA[
             this.popup.closePopup(this, null);
           ]]>
         </body>
       </method>
+
+      <method name="formatValue">
+        <body>
+          <![CDATA[
+            BrowserUI.formatURI();
+          ]]>
+        </body>
+      </method>
+
+      <method name="trimValue">
+        <parameter name="aURL"/>
+        <body><![CDATA[
+          return BrowserUI.trimURL(aURL);
+        ]]></body>
+      </method>
     </implementation>
 
     <handlers>
       <handler event="dblclick" phase="capturing">
         <![CDATA[
           let selectAll = Services.prefs.getBoolPref("browser.urlbar.doubleClickSelectsAll");
           if (selectAll)
             this.select();
@@ -65,22 +81,22 @@
       </handler>
     </handlers>
   </binding>
 
   <binding id="autocomplete-popup">
     <content orient="horizontal">
       <xul:vbox id="results-vbox" class="meta-section viewable-height" flex="1">
         <xul:label class="meta-section-title" value="&autocompleteResultsHeader.label;"/>
-        <richgrid id="results-richgrid" anonid="results" seltype="single" flex="1"/>
+        <richgrid id="results-richgrid" deferlayout="true" anonid="results" seltype="single" flex="1"/>
       </xul:vbox>
 
       <xul:vbox id="searches-vbox" class="meta-section viewable-height" flex="1">
         <xul:label class="meta-section-title" value="&autocompleteSearchesHeader.label;"/>
-        <richgrid id="searches-richgrid" anonid="searches" seltype="single" flex="1"/>
+        <richgrid id="searches-richgrid" deferlayout="true" anonid="searches" seltype="single" flex="1"/>
       </xul:vbox>
     </content>
 
     <implementation implements="nsIAutoCompletePopup, nsIObserver">
       <constructor>
         <![CDATA[
           Services.obs.addObserver(this, "browser-search-engine-modified", false);
           this.updateSearchEngines();
@@ -157,16 +173,20 @@
             ContextUI.dismissAppbar();
 
             this._input = aInput;
             this._popupOpen = true;
             this._grid = this._results;
 
             this.clearSelection();
             this.invalidate();
+
+            this._results.arrangeItemsNow();
+            this._searches.arrangeItemsNow();
+
             this._fire("autocompletestart");
           ]]>
         </body>
       </method>
 
       <method name="gridBoundCallback">
         <body>
           <![CDATA[
@@ -390,17 +410,17 @@
           ]]>
         </getter>
       </property>
 
       <method name="_isGridBound">
         <parameter name="aGrid"/>
         <body>
           <![CDATA[
-            return aGrid.itemCount != undefined;
+            return aGrid && aGrid.itemCount != undefined;
           ]]>
         </body>
       </method>
 
       <method name="_fire">
         <parameter name="aName"/>
         <body>
           <![CDATA[
--- a/browser/metro/base/content/bindings/browser.xml
+++ b/browser/metro/base/content/bindings/browser.xml
@@ -121,17 +121,17 @@
         * @param aMessage - message manager message
         * @param aIgnoreScroll ignore root frame scroll.
         * @param aIgnoreScale ignore current scale factor.
         * @return { x: converted x coordinate, y: converted y coordinate }
         *
         * rectBrowserToClient
         *  Converts a rect (left, top, right, bottom).
         *
-        * @param aMessage - message manager message
+        * @param aRect - rect to convert
         * @param aIgnoreScroll ignore root frame scroll.
         * @param aIgnoreScale ignore current scale factor.
         * @return { left:, top:, right:, bottom: }
         *
         * btocx, btocy
         *  Convert individual x and y coordinates.
         *
         * @param aX or aY - client coordinate
--- a/browser/metro/base/content/bindings/grid.xml
+++ b/browser/metro/base/content/bindings/grid.xml
@@ -333,78 +333,115 @@
       <!-- Interface for grid layout management -->
 
       <field name="_rowCount">0</field>
       <property name="rowCount" readonly="true" onget="return this._rowCount;"/>
 
       <field name="_columnCount">0</field>
       <property name="columnCount" readonly="true" onget="return this._columnCount;"/>
 
-      <field name="_scheduledArrangeItemsTries">0</field>
-
       <!--  define a height where we consider an item not yet rendered
             10 is the height of the empty item (padding/border etc. only) -->
       <field name="_itemHeightRenderThreshold">10</field>
 
-      <method name="arrangeItems">
-        <body>
-          <![CDATA[
-            if (this.itemCount <= 0) {
-              return;
-            }
-            let item = this.getItemAtIndex(0);
-            if (item == null) {
-              return;
-            }
-            let gridItemRect = item.getBoundingClientRect();
-
-            // cap the number of times we reschedule calling arrangeItems
-            let maxRetries = 5;
-
-            // delay as necessary until the item has a proper height
-            if (gridItemRect.height <= this._itemHeightRenderThreshold) {
-              if (this._scheduledArrangeItemsTimerId) {
-                // retry of arrangeItems already scheduled
-                return;
-              }
-
-              // track how many times we've attempted arrangeItems
-              this._scheduledArrangeItemsTries++;
-
-              if (maxRetries > this._scheduledArrangeItemsTries) {
-                // schedule re-try of arrangeItems at the next tick
-                this._scheduledArrangeItemsTimerId = setTimeout(this.arrangeItems.bind(this), 0);
-                return;
-              }
-            }
-
-            // items ready to arrange (or retries max exceeded)
-            // reset the flags
-            if (this._scheduledArrangeItemsTimerId) {
-              clearTimeout(this._scheduledArrangeItemsTimerId);
-              delete this._scheduledArrangeItemsTimerId;
-            }
-            if (this._scheduledArrangeItemsTries) {
-              this._scheduledArrangeItemsTries = 0;
-            }
+      <property name="_containerRect">
+        <getter><![CDATA[
+            // return the rect that represents our bounding box
 
             // Autocomplete is a binding within a binding, so we have to step
             // up an additional parentNode.
             let container = null;
             if (this.parentNode.id == "results-vbox" ||
                 this.parentNode.id == "searches-vbox")
               container = this.parentNode.parentNode.getBoundingClientRect();
             else
               container = this.parentNode.getBoundingClientRect();
+            return container;
+        ]]></getter>
+      </property>
 
-            // If we don't have valid dimensions we can't arrange yet
-            if (!container.height || !gridItemRect.height) {
+      <property name="_itemRect">
+        <getter><![CDATA[
+            // return the rect that represents an item in the grid
+
+            // TODO: when we remove the need for DOM item measurement, 0 items will not be a problem
+            let item = this.itemCount ? this.getItemAtIndex(0) : null;
+            if (item) {
+              let gridItemRect = item.getBoundingClientRect();
+              if (gridItemRect.height > this._itemHeightRenderThreshold) {
+                return gridItemRect;
+              }
+            }
+            return null;
+        ]]></getter>
+      </property>
+
+      <!-- do conditions allow layout/arrange of the grid? -->
+      <property name="_canLayout" readonly="true">
+        <getter>
+          <![CDATA[
+            let gridItemRect = this._itemRect;
+            // If we don't have valid item dimensions we can't arrange yet
+            if (!(gridItemRect && gridItemRect.height)) {
+              return false;
+            }
+
+            let container = this._containerRect;
+            // If we don't have valid container dimensions we can't arrange yet
+            if (!(container && container.height)) {
+              return false;
+            }
+            return true;
+          ]]>
+        </getter>
+      </property>
+
+      <field name="_scheduledArrangeItemsTimerId">null</field>
+      <field name="_scheduledArrangeItemsTries">0</field>
+      <field name="_maxArrangeItemsRetries">5</field>
+      <method name="_scheduleArrangeItems">
+        <parameter name="aTime"/>
+        <body>
+          <![CDATA[
+              // cap the number of times we reschedule calling arrangeItems
+              if (
+                  !this._scheduledArrangeItemsTimerId &&
+                  this._maxArrangeItemsRetries > this._scheduledArrangeItemsTries
+              ) {
+                this._scheduledArrangeItemsTimerId = setTimeout(this.arrangeItems.bind(this), aTime || 0);
+                // track how many times we've attempted arrangeItems
+                this._scheduledArrangeItemsTries++;
+              }
+          ]]>
+        </body>
+      </method>
+
+      <method name="arrangeItems">
+        <body>
+          <![CDATA[
+            if (this.hasAttribute("deferlayout")) {
               return;
             }
 
+            if (!this._canLayout) {
+              // try again later
+              this._scheduleArrangeItems();
+              return;
+            }
+
+            let gridItemRect = this._itemRect;
+            let container = this._containerRect;
+
+            // reset the flags
+            if (this._scheduledArrangeItemsTimerId) {
+              clearTimeout(this._scheduledArrangeItemsTimerId);
+              delete this._scheduledArrangeItemsTimerId;
+            }
+            this._scheduledArrangeItemsTries = 0;
+
             // We favor overflowing horizontally, not vertically
             let maxRowCount = Math.floor(container.height / gridItemRect.height) - 1;
 
             this._rowCount = this.getAttribute("rows");
             this._columnCount = this.getAttribute("columns");
 
             if (!this._rowCount) {
               this._rowCount = Math.min(this.itemCount, maxRowCount);
@@ -413,16 +450,31 @@
               this._columnCount = Math.ceil(this.itemCount / this._rowCount);
             }
 
             this._grid.style.width = (this._columnCount * gridItemRect.width) + "px";
 
           ]]>
         </body>
       </method>
+      <method name="arrangeItemsNow">
+        <body>
+          <![CDATA[
+            this.removeAttribute("deferlayout");
+            // cancel any scheduled arrangeItems and reset flags
+            if (this._scheduledArrangeItemsTimerId) {
+              clearTimeout(this._scheduledArrangeItemsTimerId);
+              delete this._scheduledArrangeItemsTimerId;
+            }
+            this._scheduledArrangeItemsTries = 0;
+            // pass over any params
+            return this.arrangeItems.apply(this, arguments);
+          ]]>
+        </body>
+      </method>
 
       <!-- Inteface to suppress selection events -->
 
       <field name="_suppressOnSelect"/>
       <property name="suppressOnSelect"
                   onget="return this.getAttribute('suppressonselect') == 'true';"
                   onset="this.setAttribute('suppressonselect', val);"/>
       <property name="crossSlideBoundary"
--- a/browser/metro/base/content/bindings/selectionoverlay.xml
+++ b/browser/metro/base/content/bindings/selectionoverlay.xml
@@ -5,19 +5,19 @@
     xmlns:xbl="http://www.mozilla.org/xbl"
     xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
     xmlns:html="http://www.w3.org/1999/xhtml">
   <binding id="selection-binding">  
     <content>
       <html:div flex="1" class="selection-overlay-inner window-width window-height" anonid="selection-overlay-inner">
         <xul:stack>
           <html:div anonid="selection-overlay-debug" class="window-width window-height"/>
-          <xul:toolbarbutton id="selectionhandle-mark1" label="^" left="10" top="10" hidden="true"/>
-          <xul:toolbarbutton id="selectionhandle-mark2" label="^" left="10" top="10" hidden="true"/>
-          <xul:toolbarbutton id="selectionhandle-mark3" label="^" left="10" top="10" hidden="true"/>
+          <xul:toolbarbutton anonid="selectionhandle-mark1" class="selectionhandle" label="^" left="10" top="10" hidden="true"/>
+          <xul:toolbarbutton anonid="selectionhandle-mark2" class="selectionhandle" label="^" left="10" top="10" hidden="true"/>
+          <xul:toolbarbutton anonid="selectionhandle-mark3" class="selectionhandle" label="^" left="10" top="10" hidden="true"/>
         </xul:stack>
       </html:div>
     </content>
 
     <implementation implements="nsIDOMEventListener">
       <field name="_selectionOverlay" readonly="true">document.getAnonymousElementByAttribute(this, "anonid", "selection-overlay-inner").parentNode;</field>
       <field name="_selectionDebugOverlay" readonly="true">document.getAnonymousElementByAttribute(this, "anonid", "selection-overlay-debug");</field>
 
@@ -57,28 +57,21 @@
           <![CDATA[
           if (this._debugLayerVisible == "undefined")
             this._debugLayerVisible = false;
           return this._debugLayerVisible;
         ]]>
         </getter>
       </property>
 
-      <method name="init">
+      <method name="getMarker">
+        <parameter name="aMarkerId"/>
         <body>
           <![CDATA[
-          this.enabled = true;
-          ]]>
-        </body>
-      </method>
-
-      <method name="shutdown">
-        <body>
-          <![CDATA[
-          this.enabled = false;
+            return document.getAnonymousElementByAttribute(this, "anonid", aMarkerId);
           ]]>
         </body>
       </method>
 
       <method name="addDebugRect">
         <parameter name="aLeft"/>
         <parameter name="aTop"/>
         <parameter name="aRight"/>
--- a/browser/metro/base/content/browser-scripts.js
+++ b/browser/metro/base/content/browser-scripts.js
@@ -104,16 +104,18 @@ let ScriptContexts = {};
   ["AutofillMenuUI", "chrome://browser/content/helperui/MenuUI.js"],
   ["ContextMenuUI", "chrome://browser/content/helperui/MenuUI.js"],
   ["MenuControlUI", "chrome://browser/content/helperui/MenuUI.js"],
   ["MenuPopup", "chrome://browser/content/helperui/MenuUI.js"],
   ["IndexedDB", "chrome://browser/content/helperui/IndexedDB.js"],
   ["OfflineApps", "chrome://browser/content/helperui/OfflineApps.js"],
   ["SelectHelperUI", "chrome://browser/content/helperui/SelectHelperUI.js"],
   ["SelectionHelperUI", "chrome://browser/content/helperui/SelectionHelperUI.js"],
+  ["SelectionPrototype", "chrome://browser/content/library/SelectionPrototype.js"],
+  ["ChromeSelectionHandler", "chrome://browser/content/helperui/ChromeSelectionHandler.js"],
   ["AnimatedZoom", "chrome://browser/content/AnimatedZoom.js"],
   ["CommandUpdater", "chrome://browser/content/commandUtil.js"],
   ["ContextCommands", "chrome://browser/content/ContextCommands.js"],
   ["Bookmarks", "chrome://browser/content/bookmarks.js"],
   ["Downloads", "chrome://browser/content/downloads.js"],
   ["BookmarksPanelView", "chrome://browser/content/bookmarks.js"],
   ["ConsolePanelView", "chrome://browser/content/console.js"],
   ["DownloadsPanelView", "chrome://browser/content/downloads.js"],
--- a/browser/metro/base/content/browser-ui.js
+++ b/browser/metro/base/content/browser-ui.js
@@ -92,17 +92,21 @@ var BrowserUI = {
 
     // listening escape to dismiss dialog on VK_ESCAPE
     window.addEventListener("keypress", this, true);
 
     window.addEventListener("MozPrecisePointer", this, true);
     window.addEventListener("MozImprecisePointer", this, true);
 
     Services.prefs.addObserver("browser.cache.disk_cache_ssl", this, false);
+    Services.prefs.addObserver("browser.urlbar.formatting.enabled", this, false);
+    Services.prefs.addObserver("browser.urlbar.trimURLs", this, false);
     Services.obs.addObserver(this, "metro_viewstate_changed", false);
+    
+    this._edit.inputField.controllers.insertControllerAt(0, this._copyCutURIController);
 
     // Init core UI modules
     ContextUI.init();
     StartUI.init();
     PanelUI.init();
     FlyoutPanelsUI.init();
     PageThumbs.init();
     SettingsCharm.init();
@@ -234,21 +238,30 @@ var BrowserUI = {
    */
 
   update: function(aState) {
     this._updateToolbar();
   },
 
   getDisplayURI: function(browser) {
     let uri = browser.currentURI;
+    let spec = uri.spec;
+
     try {
-      uri = gURIFixup.createExposableURI(uri);
+      spec = gURIFixup.createExposableURI(uri).spec;
     } catch (ex) {}
 
-    return uri.spec;
+    try {
+      let charset = browser.characterSet;
+      let textToSubURI = Cc["@mozilla.org/intl/texttosuburi;1"].
+                         getService(Ci.nsITextToSubURI);
+      spec = textToSubURI.unEscapeNonAsciiURI(charset, spec);
+    } catch (ex) {}
+
+    return spec;
   },
 
   /**
     * Some prefs that have consequences in both Metro and Desktop such as
     * app-update prefs, are automatically pulled from Desktop here.
     */
   _pullDesktopControlledPrefs: function() {
     function pullDesktopControlledPrefType(prefType, prefFunc) {
@@ -555,16 +568,22 @@ var BrowserUI = {
 
   observe: function BrowserUI_observe(aSubject, aTopic, aData) {
     switch (aTopic) {
       case "nsPref:changed":
         switch (aData) {
           case "browser.cache.disk_cache_ssl":
             this._sslDiskCacheEnabled = Services.prefs.getBoolPref(aData);
             break;
+          case "browser.urlbar.formatting.enabled":
+            this._formattingEnabled = Services.prefs.getBoolPref(aData);
+            break;
+          case "browser.urlbar.trimURLs":
+            this._mayTrimURLs = Services.prefs.getBoolPref(aData);
+            break;
         }
         break;
       case "metro_viewstate_changed":
         this._adjustDOMforViewState();
         let autocomplete = document.getElementById("start-autocomplete");
         if (aData == "snapped") {
           FlyoutPanelsUI.hide();
           // Order matters (need grids to get dimensions, etc), now
@@ -645,42 +664,232 @@ var BrowserUI = {
     let isLoading = Browser.selectedTab.isLoading();
 
     if (isLoading && mode != "loading")
       Elements.urlbarState.setAttribute("mode", "loading");
     else if (!isLoading && mode != "edit")
       Elements.urlbarState.setAttribute("mode", "view");
   },
 
+  _trimURL: function _trimURL(aURL) {
+    // This function must not modify the given URL such that calling
+    // nsIURIFixup::createFixupURI with the result will produce a different URI.
+    return aURL /* remove single trailing slash for http/https/ftp URLs */
+               .replace(/^((?:http|https|ftp):\/\/[^/]+)\/$/, "$1")
+                /* remove http:// unless the host starts with "ftp\d*\." or contains "@" */
+               .replace(/^http:\/\/((?!ftp\d*\.)[^\/@]+(?:\/|$))/, "$1");
+  },
+
+  trimURL: function trimURL(aURL) {
+    return this.mayTrimURLs ? this._trimURL(aURL) : aURL;
+  },
+
   _setURI: function _setURI(aURL) {
     this._edit.value = aURL;
     this.lastKnownGoodURL = aURL;
   },
 
-  _urlbarClicked: function _urlbarClicked() {
+  _getSelectedURIForClipboard: function _getSelectedURIForClipboard() {
+    // Grab the actual input field's value, not our value, which could include moz-action:
+    let inputVal = this._edit.inputField.value;
+    let selectedVal = inputVal.substring(this._edit.selectionStart, this._edit.electionEnd);
+
+    // If the selection doesn't start at the beginning or doesn't span the full domain or
+    // the URL bar is modified, nothing else to do here.
+    if (this._edit.selectionStart > 0 || this._edit.valueIsTyped)
+      return selectedVal;
+    // The selection doesn't span the full domain if it doesn't contain a slash and is
+    // followed by some character other than a slash.
+    if (!selectedVal.contains("/")) {
+      let remainder = inputVal.replace(selectedVal, "");
+      if (remainder != "" && remainder[0] != "/")
+        return selectedVal;
+    }
+
+    let uriFixup = Cc["@mozilla.org/docshell/urifixup;1"].getService(Ci.nsIURIFixup);
+
+    let uri;
+    try {
+      uri = uriFixup.createFixupURI(inputVal, Ci.nsIURIFixup.FIXUP_FLAG_USE_UTF8);
+    } catch (e) {}
+    if (!uri)
+      return selectedVal;
+
+    // Only copy exposable URIs
+    try {
+      uri = uriFixup.createExposableURI(uri);
+    } catch (ex) {}
+
+    // If the entire URL is selected, just use the actual loaded URI.
+    if (inputVal == selectedVal) {
+      // ... but only if  isn't a javascript: or data: URI, since those
+      // are hard to read when encoded
+      if (!uri.schemeIs("javascript") && !uri.schemeIs("data")) {
+        // Parentheses are known to confuse third-party applications (bug 458565).
+        selectedVal = uri.spec.replace(/[()]/g, function (c) escape(c));
+      }
+
+      return selectedVal;
+    }
+
+    // Just the beginning of the URL is selected, check for a trimmed value
+    let spec = uri.spec;
+    let trimmedSpec = this.trimURL(spec);
+    if (spec != trimmedSpec) {
+      // Prepend the portion that trimURL removed from the beginning.
+      // This assumes trimURL will only truncate the URL at
+      // the beginning or end (or both).
+      let trimmedSegments = spec.split(trimmedSpec);
+      selectedVal = trimmedSegments[0] + selectedVal;
+    }
+
+    return selectedVal;
+  },
+
+  _copyCutURIController: {
+    doCommand: function(aCommand) {
+      let urlbar = BrowserUI._edit;
+      let val = BrowserUI._getSelectedURIForClipboard();
+      if (!val)
+        return;
+
+      if (aCommand == "cmd_cut" && this.isCommandEnabled(aCommand)) {
+        let start = urlbar.selectionStart;
+        let end = urlbar.selectionEnd;
+        urlbar.inputField.value = urlbar.inputField.value.substring(0, start) +
+                                  urlbar.inputField.value.substring(end);
+        urlbar.selectionStart = urlbar.selectionEnd = start;
+      }
+
+      Cc["@mozilla.org/widget/clipboardhelper;1"]
+        .getService(Ci.nsIClipboardHelper)
+        .copyString(val, document);
+    },
+
+    supportsCommand: function(aCommand) {
+      switch (aCommand) {
+        case "cmd_copy":
+        case "cmd_cut":
+          return true;
+      }
+      return false;
+    },
+
+    isCommandEnabled: function(aCommand) {
+      let urlbar = BrowserUI._edit;
+      return this.supportsCommand(aCommand) &&
+             (aCommand != "cmd_cut" || !urlbar.readOnly) &&
+             urlbar.selectionStart < urlbar.selectionEnd;
+    },
+
+    onEvent: function(aEventName) {}
+  },
+
+  _urlbarClicked: function _urlbarClicked(aEvent) {
+    let touchEvent = aEvent.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH;
+
     // If the urlbar is not already focused, focus it and select the contents.
-    if (Elements.urlbarState.getAttribute("mode") != "edit")
+    if (Elements.urlbarState.getAttribute("mode") != "edit") {
       this._editURI(true);
+      if (touchEvent) {
+        SelectionHelperUI.attachEditSession(ChromeSelectionHandler,
+                                            aEvent.clientX, aEvent.clientY);
+      }
+      return;
+    }
+
+    // tap caret handling
+    if (touchEvent) {
+      SelectionHelperUI.attachToCaret(ChromeSelectionHandler,
+                                      aEvent.clientX, aEvent.clientY);
+    }
   },
 
   _editURI: function _editURI(aShouldDismiss) {
     this._edit.focus();
     this._edit.select();
 
     Elements.urlbarState.setAttribute("mode", "edit");
     StartUI.show();
-    if (aShouldDismiss)
+    if (aShouldDismiss) {
       ContextUI.dismissTabs();
+    }
+  },
+
+  formatURI: function formatURI() {
+    if (!this.formattingEnabled ||
+        Elements.urlbarState.getAttribute("mode") == "edit")
+      return;
+
+    let controller = this._edit.editor.selectionController;
+    let selection = controller.getSelection(controller.SELECTION_URLSECONDARY);
+    selection.removeAllRanges();
+
+    let textNode = this._edit.editor.rootElement.firstChild;
+    let value = textNode.textContent;
+
+    let protocol = value.match(/^[a-z\d.+\-]+:(?=[^\d])/);
+    if (protocol &&
+        ["http:", "https:", "ftp:"].indexOf(protocol[0]) == -1)
+      return;
+    let matchedURL = value.match(/^((?:[a-z]+:\/\/)?(?:[^\/]+@)?)(.+?)(?::\d+)?(?:\/|$)/);
+    if (!matchedURL)
+      return;
+
+    let [, preDomain, domain] = matchedURL;
+    let baseDomain = domain;
+    let subDomain = "";
+    // getBaseDomainFromHost doesn't recognize IPv6 literals in brackets as IPs (bug 667159)
+    if (domain[0] != "[") {
+      try {
+        baseDomain = Services.eTLD.getBaseDomainFromHost(domain);
+        if (!domain.endsWith(baseDomain)) {
+          // getBaseDomainFromHost converts its resultant to ACE.
+          let IDNService = Cc["@mozilla.org/network/idn-service;1"]
+                           .getService(Ci.nsIIDNService);
+          baseDomain = IDNService.convertACEtoUTF8(baseDomain);
+        }
+      } catch (e) {}
+    }
+    if (baseDomain != domain) {
+      subDomain = domain.slice(0, -baseDomain.length);
+    }
+
+    let rangeLength = preDomain.length + subDomain.length;
+    if (rangeLength) {
+      let range = document.createRange();
+      range.setStart(textNode, 0);
+      range.setEnd(textNode, rangeLength);
+      selection.addRange(range);
+    }
+
+    let startRest = preDomain.length + domain.length;
+    if (startRest < value.length) {
+      let range = document.createRange();
+      range.setStart(textNode, startRest);
+      range.setEnd(textNode, value.length);
+      selection.addRange(range);
+    }
+  },
+
+  _clearURIFormatting: function _clearURIFormatting() {
+    if (!this.formattingEnabled)
+      return;
+
+    let controller = this._edit.editor.selectionController;
+    let selection = controller.getSelection(controller.SELECTION_URLSECONDARY);
+    selection.removeAllRanges();
   },
 
   _urlbarBlurred: function _urlbarBlurred() {
     let state = Elements.urlbarState;
     if (state.getAttribute("mode") == "edit")
       state.removeAttribute("mode");
     this._updateToolbar();
+    this.formatURI();
   },
 
   _closeOrQuit: function _closeOrQuit() {
     // Close active dialog, if we have one. If not then close the application.
     if (!BrowserUI.isContentShowing()) {
       BrowserUI.showContent();
     } else {
       // Check to see if we should really close the window
@@ -954,16 +1163,34 @@ var BrowserUI = {
 
   get sslDiskCacheEnabled() {
     if (this._sslDiskCacheEnabled === null) {
       this._sslDiskCacheEnabled = Services.prefs.getBoolPref("browser.cache.disk_cache_ssl");
     }
     return this._sslDiskCacheEnabled;
   },
 
+  _formattingEnabled: null,
+
+  get formattingEnabled() {
+    if (this._formattingEnabled === null) {
+      this._formattingEnabled = Services.prefs.getBoolPref("browser.urlbar.formatting.enabled");
+    }
+    return this._formattingEnabled;
+  },
+
+  _mayTrimURLs: null,
+
+  get mayTrimURLs() {
+    if (this._mayTrimURLs === null) {
+      this._mayTrimURLs = Services.prefs.getBoolPref("browser.urlbar.trimURLs");
+    }
+    return this._mayTrimURLs;
+  },
+
   supportsCommand : function(cmd) {
     var isSupported = false;
     switch (cmd) {
       case "cmd_back":
       case "cmd_forward":
       case "cmd_reload":
       case "cmd_forceReload":
       case "cmd_stop":
--- a/browser/metro/base/content/browser.css
+++ b/browser/metro/base/content/browser.css
@@ -84,17 +84,18 @@ setting[type="directory"] {
 }
 
 setting[type="radio"],
 setting[type="menulist"] {
   display: -moz-box;
   -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-multi");
 }
 
-#selection-overlay {
+#chrome-selection-overlay,
+#content-selection-overlay {
   -moz-binding: url("chrome://browser/content/bindings/selectionoverlay.xml#selection-binding");
 }
 
 #urlbar-edit {
   -moz-binding: url("chrome://browser/content/bindings/autocomplete.xml#autocomplete");
 }
 
 #start-autocomplete {
--- a/browser/metro/base/content/browser.js
+++ b/browser/metro/base/content/browser.js
@@ -39,16 +39,17 @@ var Browser = {
 
   startup: function startup() {
     var self = this;
 
     try {
       messageManager.loadFrameScript("chrome://browser/content/Util.js", true);
       messageManager.loadFrameScript("chrome://browser/content/contenthandlers/Content.js", true);
       messageManager.loadFrameScript("chrome://browser/content/contenthandlers/FormHelper.js", true);
+      messageManager.loadFrameScript("chrome://browser/content/library/SelectionPrototype.js", true);
       messageManager.loadFrameScript("chrome://browser/content/contenthandlers/SelectionHandler.js", true);
       messageManager.loadFrameScript("chrome://browser/content/contenthandlers/ContextMenuHandler.js", true);
       messageManager.loadFrameScript("chrome://browser/content/contenthandlers/FindHandler.js", true);
       // XXX Viewport resizing disabled because of bug 766142
       //messageManager.loadFrameScript("chrome://browser/content/contenthandlers/ViewportHandler.js", true);
       messageManager.loadFrameScript("chrome://browser/content/contenthandlers/ConsoleAPIObserver.js", true);
       //messageManager.loadFrameScript("chrome://browser/content/contenthandlers/PluginCTPHandler.js", true);
     } catch (e) {
--- a/browser/metro/base/content/browser.xul
+++ b/browser/metro/base/content/browser.xul
@@ -230,17 +230,21 @@
         </hbox>
       </vbox> <!-- end tray -->
 
       <!-- Content viewport -->
       <stack id="content-viewport">
         <deck id="browsers" flex="1" observes="bcast_preciseInput"/>
         <box id="vertical-scroller" class="scroller" orient="vertical" end="0" top="0"/>
         <box id="horizontal-scroller" class="scroller" orient="horizontal" left="0" bottom="0"/>
-      </stack>
+
+        <!-- Content touch selection overlay -->
+        <!-- onclick addresses dom bug 835175 -->
+        <box onclick="false" class="selection-overlay-hidden" id="content-selection-overlay"/>
+  </stack>
     </vbox>
 
     <!-- popup for content navigator helper -->
     <appbar id="content-navigator" class="window-width content-navigator-box" orient="horizontal" pack="start">
       <textbox id="find-helper-textbox" class="search-bar content-navigator-item" oncommand="FindHelperUI.search(this.value)" oninput="FindHelperUI.updateCommands(this.value);" type="search"/>
       <button class="content-navigator-item previous-button" command="cmd_findPrevious"/>
       <button class="content-navigator-item next-button" command="cmd_findNext"/>
       <spacer flex="1"/>
@@ -355,19 +359,19 @@
         <toolbarbutton id="delete-selected-button" hidden="true" fade="true" oncommand="Appbar.dispatchContextualAction('delete')"/>
         <toolbarbutton id="restore-selected-button" hidden="true" fade="true" oncommand="Appbar.dispatchContextualAction('restore')"/>
         <toolbarbutton id="pin-selected-button" hidden="true" fade="true" oncommand="Appbar.dispatchContextualAction('pin')"/>
         <toolbarbutton id="unpin-selected-button" hidden="true" fade="true" oncommand="Appbar.dispatchContextualAction('unpin')"/>
         <toolbarbutton id="clear-selected-button" hidden="true" fade="true" oncommand="Appbar.dispatchContextualAction('clear')"/>
       </toolbar>
     </appbar>
 
-    <!-- Selection overlay - this should be below any content that can have selectable text -->
-    <!-- onclick addresses dom bug 835175, str in bug 832957 -->
-    <box onclick="false" class="selection-overlay-hidden" id="selection-overlay"/>
+    <!-- Chrome touch selection overlay -->
+    <!-- onclick addresses dom bug 835175 -->
+    <box onclick="false" class="selection-overlay-hidden" id="chrome-selection-overlay"/>
 
     <autoscroller class="autoscroller" id="autoscrollerid"/>
 
     <html:div id="overlay-back" class="overlay-button"
               observes="cmd_back" onclick="CommandUpdater.doCommand('cmd_back');"></html:div>
     <html:div id="overlay-plus" class="overlay-button"
               observes="cmd_back" onclick="CommandUpdater.doCommand('cmd_newTab');"></html:div>
 
--- a/browser/metro/base/content/contenthandlers/Content.js
+++ b/browser/metro/base/content/contenthandlers/Content.js
@@ -138,17 +138,17 @@ let Content = {
     addEventListener("DOMContentLoaded", this, false);
     addEventListener("pagehide", this, false);
     // Attach a listener to watch for "click" events bubbling up from error
     // pages and other similar page. This lets us fix bugs like 401575 which
     // require error page UI to do privileged things, without letting error
     // pages have any privilege themselves.
     addEventListener("click", this, false);
 
-    docShell.QueryInterface(Ci.nsIDocShellHistory).useGlobalHistory = true;
+    docShell.useGlobalHistory = true;
   },
 
   /*******************************************
    * Events
    */
 
   handleEvent: function handleEvent(aEvent) {
     if (this._debugEvents) Util.dumpLn("Content:", aEvent.type);
--- a/browser/metro/base/content/contenthandlers/SelectionHandler.js
+++ b/browser/metro/base/content/contenthandlers/SelectionHandler.js
@@ -1,59 +1,18 @@
 /* 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/. */
 
 dump("### SelectionHandler.js loaded\n");
 
-/*
-  http://mxr.mozilla.org/mozilla-central/source/docshell/base/nsIDocShell.idl
-  http://mxr.mozilla.org/mozilla-central/source/content/base/public/nsISelectionDisplay.idl
-  http://mxr.mozilla.org/mozilla-central/source/content/base/public/nsISelectionListener.idl
-  http://mxr.mozilla.org/mozilla-central/source/content/base/public/nsISelectionPrivate.idl
-  http://mxr.mozilla.org/mozilla-central/source/content/base/public/nsISelectionController.idl
-  http://mxr.mozilla.org/mozilla-central/source/content/base/public/nsISelection.idl
-  http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/core/nsIDOMDocument.idl#372
-    rangeCount
-    getRangeAt
-    containsNode
-  http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html
-  http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/range/nsIDOMRange.idl
-  http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/core/nsIDOMDocument.idl#80
-    content.document.createRange()
-    getBoundingClientRect
-    isPointInRange
-  http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/core/nsIDOMNode.idl
-  http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/base/nsIDOMWindowUtils.idl
-    setSelectionAtPoint
-  http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/core/nsIDOMElement.idl
-    getClientRect
-  http://mxr.mozilla.org/mozilla-central/source/layout/generic/nsFrameSelection.h
-  http://mxr.mozilla.org/mozilla-central/source/editor/idl/nsIEditor.idl
-  http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/base/nsIFocusManager.idl
-
-*/
-
-// selection node parameters for various apis
-const kSelectionNodeAnchor = 1;
-const kSelectionNodeFocus = 2;
-
 var SelectionHandler = {
-  _debugEvents: false,
-  _cache: {},
-  _targetElement: null,
-  _targetIsEditable: false,
-  _contentWindow: null,
-  _contentOffset: { x:0, y:0 },
-  _domWinUtils: null,
-  _selectionMoveActive: false,
-  _debugOptions: { dumpRanges: false, displayRanges: false },
-  _snap: true,
-
   init: function init() {
+    this.type = kContentSelector;
+    this.snap = true;
     addMessageListener("Browser:SelectionStart", this);
     addMessageListener("Browser:SelectionAttach", this);
     addMessageListener("Browser:SelectionEnd", this);
     addMessageListener("Browser:SelectionMoveStart", this);
     addMessageListener("Browser:SelectionMove", this);
     addMessageListener("Browser:SelectionMoveEnd", this);
     addMessageListener("Browser:SelectionUpdate", this);
     addMessageListener("Browser:SelectionClose", this);
@@ -81,35 +40,18 @@ var SelectionHandler = {
     removeMessageListener("Browser:CaretAttach", this);
     removeMessageListener("Browser:CaretMove", this);
     removeMessageListener("Browser:CaretUpdate", this);
     removeMessageListener("Browser:SelectionSwitchMode", this);
     removeMessageListener("Browser:RepositionInfoRequest", this);
     removeMessageListener("Browser:SelectionHandlerPing", this);
   },
 
-  /*************************************************
-   * Properties
-   */
-
-  get isActive() {
-    return !!this._targetElement;
-  },
-
-  get targetIsEditable() {
-    return this._targetIsEditable || false;
-  },
-
-  /*
-   * snap - enable or disable word snap for the active marker when a
-   * SelectionMoveEnd event is received. Typically you would disable
-   * snap when zoom is < 1.0 for precision selection.
-   */
-  get snap() {
-    return this._snap;
+  sendAsync: function sendAsync(aMsg, aJson) {
+    sendAsyncMessage(aMsg, aJson);
   },
 
   /*************************************************
    * Browser event handlers
    */
 
   /*
    * Selection start event handler
@@ -129,28 +71,28 @@ var SelectionHandler = {
     let framePoint = this._clientPointToFramePoint({ xPos: aX, yPos: aY });
     if (!this._domWinUtils.selectAtPoint(framePoint.xPos, framePoint.yPos,
                                          Ci.nsIDOMWindowUtils.SELECT_WORDNOSPACE)) {
       this._onFail("failed to set selection at point");
       return;
     }
 
     // Update the position of our selection monocles
-    this._updateSelectionUI(true, true);
+    this._updateSelectionUI("start", true, true);
   },
 
   _onSelectionAttach: function _onSelectionAttach(aX, aY) {
     // Init content window information
     if (!this._initTargetInfo(aX, aY)) {
       this._onFail("failed to get frame offset");
       return;
     }
 
     // Update the position of our selection monocles
-    this._updateSelectionUI(true, true);
+    this._updateSelectionUI("start", true, true);
   },
 
   /*
    * Switch selection modes. Currently we only support switching
    * from "caret" to "selection".
    */
   _onSwitchMode: function _onSwitchMode(aMode, aMarker, aX, aY) {
     if (aMode != "selection") {
@@ -173,17 +115,17 @@ var SelectionHandler = {
       return;
     }
 
     // We bail if things get out of sync here implying we missed a message.
     this._selectionMoveActive = true;
 
     // Update the position of the selection marker that is *not*
     // being dragged.
-    this._updateSelectionUI(aMarker == "end", aMarker == "start");
+    this._updateSelectionUI("update", aMarker == "end", aMarker == "start");
   },
 
   /*
    * Selection monocle start move event handler
    */
   _onSelectionMoveStart: function _onSelectionMoveStart(aMsg) {
     if (!this._contentWindow) {
       this._onFail("_onSelectionMoveStart was called without proper view set up");
@@ -202,17 +144,17 @@ var SelectionHandler = {
       // If we're coming out of an out-of-bounds scroll, the node the user is
       // trying to drag may be hidden (the monocle will be pegged to the edge
       // of the edit). Make sure the node the user wants to move is visible
       // and has focus.
       this._updateInputFocus(aMsg.change);
     }
 
     // Update the position of our selection monocles
-    this._updateSelectionUI(true, true);
+    this._updateSelectionUI("update", true, true);
   },
   
   /*
    * Selection monocle move event handler
    */
   _onSelectionMove: function _onSelectionMove(aMsg) {
     if (!this._contentWindow) {
       this._onFail("_onSelectionMove was called without proper view set up");
@@ -259,17 +201,17 @@ var SelectionHandler = {
     this._handleSelectionPoint(aMsg.change, pos, true);
     this._selectionMoveActive = false;
     
     // _handleSelectionPoint may set a scroll timer, so this must
     // be reset after the last call.
     this._clearTimers();
 
     // Update the position of our selection monocles
-    this._updateSelectionUI(true, true);
+    this._updateSelectionUI("end", true, true);
   },
 
    /*
     * _onCaretAttach - called by SelectionHelperUI when the user taps in a
     * form input. Initializes SelectionHandler, updates the location of the
     * caret, and messages back with current monocle position information.
     *
     * @param aX, aY tap location in client coordinates.
@@ -290,55 +232,17 @@ var SelectionHandler = {
     // Locate and sanity check the caret position
     let selection = this._getSelection();
     if (!selection || !selection.isCollapsed) {
       this._onFail("Unexpected, No selection or selection is not collapsed.");
       return;
     }
 
     // Update the position of our selection monocles
-    this._updateSelectionUI(false, false, true);
-  },
-
-   /*
-    * _onCaretPositionUpdate - sets the current caret location based on
-    * a client coordinates. Messages back with updated monocle position
-    * information.
-    *
-    * @param aX, aY drag location in client coordinates.
-    */
-  _onCaretPositionUpdate: function _onCaretPositionUpdate(aX, aY) {
-    this._onCaretMove(aX, aY);
-
-    // Update the position of our selection monocles
-    this._updateSelectionUI(false, false, true);
-  },
-
-   /*
-    * _onCaretMove - updates the current caret location based on a client
-    * coordinates.
-    *
-    * @param aX, aY drag location in client coordinates.
-    */
-  _onCaretMove: function _onCaretMove(aX, aY) {
-    if (!this._targetIsEditable) {
-      this._onFail("Unexpected, caret position isn't supported with non-inputs.");
-      return;
-    }
-
-    // SelectionHelperUI sends text input tap coordinates and a caret move
-    // event at the start of a monocle drag. caretPositionFromPoint isn't
-    // going to give us correct info if the coord is outside the edit bounds,
-    // so restrict the coordinates before we call cpfp.
-    let containedCoords = this._restrictCoordinateToEditBounds(aX, aY);
-    let cp = this._contentWindow.document.caretPositionFromPoint(containedCoords.xPos,
-                                                                 containedCoords.yPos);
-    let input = cp.offsetNode;
-    let offset = cp.offset;
-    input.selectionStart = input.selectionEnd = offset;
+    this._updateSelectionUI("caret", false, false, true);
   },
 
   /*
    * Selection copy event handler
    *
    * Check to see if the incoming click was on our selection rect.
    * if it was, copy to the clipboard. Incoming coordinates are
    * content values.
@@ -386,72 +290,64 @@ var SelectionHandler = {
    */
   _onSelectionUpdate: function _onSelectionUpdate() {
     if (!this._contentWindow) {
       this._onFail("_onSelectionUpdate was called without proper view set up");
       return;
     }
 
     // Update the position of our selection monocles
-    this._updateSelectionUI(true, true);
+    this._updateSelectionUI("update", true, true);
   },
 
   /*
    * Called if for any reason we fail during the selection
    * process. Cancels the selection.
    */
   _onFail: function _onFail(aDbgMessage) {
     if (aDbgMessage && aDbgMessage.length > 0)
       Util.dumpLn(aDbgMessage);
-    sendAsyncMessage("Content:SelectionFail");
+    this.sendAsync("Content:SelectionFail");
     this._clearSelection();
     this._closeSelection();
   },
 
   /*
-   * Turning on or off various debug featues.
-   */
-  _onSelectionDebug: function _onSelectionDebug(aMsg) {
-    this._debugOptions = aMsg;
-    this._debugEvents = aMsg.dumpEvents;
-  },
-
-  /*
    * _repositionInfoRequest - fired at us by ContentAreaObserver when the
    * soft keyboard is being displayed. CAO wants to make a decision about
    * whether the browser deck needs repositioning.
    */
   _repositionInfoRequest: function _repositionInfoRequest(aJsonMsg) {
     if (!this.isActive) {
       Util.dumpLn("unexpected: repositionInfoRequest but selection isn't active.");
-      sendAsyncMessage("Content:RepositionInfoResponse", { reposition: false });
+      this.sendAsync("Content:RepositionInfoResponse", { reposition: false });
       return;
     }
     
     if (!this.targetIsEditable) {
       Util.dumpLn("unexpected: repositionInfoRequest but targetIsEditable is false.");
-      sendAsyncMessage("Content:RepositionInfoResponse", { reposition: false });
+      this.sendAsync("Content:RepositionInfoResponse", { reposition: false });
     }
     
     let result = this._calcNewContentPosition(aJsonMsg.viewHeight);
 
     // no repositioning needed
     if (result == 0) {
-      sendAsyncMessage("Content:RepositionInfoResponse", { reposition: false });
+      this.sendAsync("Content:RepositionInfoResponse", { reposition: false });
       return;
     }
 
-    sendAsyncMessage("Content:RepositionInfoResponse", {
+    this.sendAsync("Content:RepositionInfoResponse", {
       reposition: true,
       raiseContent: result,
     });
   },
 
   _onPing: function _onPing(aId) {
-    sendAsyncMessage("Content:SelectionHandlerPong", { id: aId });
+    this.sendAsync("Content:SelectionHandlerPong", { id: aId });
   },
 
   /*************************************************
    * Selection helpers
    */
 
   /*
    * _clearSelection
@@ -464,79 +360,36 @@ var SelectionHandler = {
       let selection = this._getSelection();
       if (selection)
         selection.removeAllRanges();
     } else {
       let selection = content.getSelection();
       if (selection)
         selection.removeAllRanges();
     }
-    this.selectedText = "";
   },
 
   /*
    * _closeSelection
    *
    * Shuts SelectionHandler down.
    */
   _closeSelection: function _closeSelection() {
     this._clearTimers();
     this._cache = null;
     this._contentWindow = null;
     this._targetElement = null;
-    this.selectedText = "";
     this._selectionMoveActive = false;
     this._contentOffset = null;
     this._domWinUtils = null;
     this._targetIsEditable = false;
     sendSyncMessage("Content:HandlerShutdown", {});
   },
 
   /*
-   * _updateSelectionUI
-   *
-   * Informs SelectionHelperUI about selection marker position
-   * so that our selection monocles can be positioned properly.
-   *
-   * @param aUpdateStart bool update start marker position
-   * @param aUpdateEnd bool update end marker position
-   * @param aUpdateCaret bool update caret marker position, can be
-   * undefined, defaults to false.
-   */
-  _updateSelectionUI: function _updateSelectionUI(aUpdateStart, aUpdateEnd,
-                                                  aUpdateCaret) {
-    let selection = this._getSelection();
-
-    // If the range didn't have any text, let's bail
-    if (!selection) {
-      this._onFail("no selection was present");
-      return;
-    }
-
-    // Updates this._cache content selection position data which we send over
-    // to SelectionHelperUI. Note updateUIMarkerRects will fail if there isn't
-    // any selection in the page. This can happen when we start a monocle drag
-    // but haven't dragged enough to create selection. Just return.
-    try {
-      this._updateUIMarkerRects(selection);
-    } catch (ex) {
-      Util.dumpLn("_updateUIMarkerRects:", ex.message);
-      return;
-    }
-
-    this._cache.updateStart = aUpdateStart;
-    this._cache.updateEnd = aUpdateEnd;
-    this._cache.updateCaret = aUpdateCaret || false;
-    this._cache.targetIsEditable = this._targetIsEditable;
-
-    // Get monocles positioned correctly
-    sendAsyncMessage("Content:SelectionRange", this._cache);
-  },
-
-  /*
    * Find content within frames - cache the target nsIDOMWindow,
    * client coordinate offset, target element, and dom utils interface.
    */
   _initTargetInfo: function _initTargetInfo(aX, aY) {
     // getCurrentWindowAndOffset takes client coordinates
     let { element: element,
           contentWindow: contentWindow,
           offset: offset,
@@ -549,568 +402,16 @@ var SelectionHandler = {
     this._contentWindow = contentWindow;
     this._contentOffset = offset;
     this._domWinUtils = utils;
     this._targetIsEditable = this._isTextInput(this._targetElement);
     return true;
   },
 
   /*
-   * _updateUIMarkerRects(aSelection)
-   *
-   * Extracts the rects of the current selection, clips them to any text
-   * input bounds, and stores them in the cache table we send over to
-   * SelectionHelperUI.
-   */
-  _updateUIMarkerRects: function _updateUIMarkerRects(aSelection) {
-    this._cache = this._extractUIRects(aSelection.getRangeAt(0));
-    if (this. _debugOptions.dumpRanges)  {
-       Util.dumpLn("start:", "(" + this._cache.start.xPos + "," +
-                   this._cache.start.yPos + ")");
-       Util.dumpLn("end:", "(" + this._cache.end.xPos + "," +
-                   this._cache.end.yPos + ")");
-       Util.dumpLn("caret:", "(" + this._cache.caret.xPos + "," +
-                   this._cache.caret.yPos + ")");
-    }
-    this._restrictSelectionRectToEditBounds();
-  },
-
-  /*
-   * Selection bounds will fall outside the bound of a control if the control
-   * can scroll. Clip UI cache data to the bounds of the target so monocles
-   * don't draw outside the control.
-   */
-  _restrictSelectionRectToEditBounds: function _restrictSelectionRectToEditBounds() {
-    if (!this._targetIsEditable)
-      return;
-
-    let bounds = this._getTargetBrowserRect();
-    if (this._cache.start.xPos < bounds.left)
-      this._cache.start.xPos = bounds.left;
-    if (this._cache.end.xPos < bounds.left)
-      this._cache.end.xPos = bounds.left;
-    if (this._cache.caret.xPos < bounds.left)
-      this._cache.caret.xPos = bounds.left;
-    if (this._cache.start.xPos > bounds.right)
-      this._cache.start.xPos = bounds.right;
-    if (this._cache.end.xPos > bounds.right)
-      this._cache.end.xPos = bounds.right;
-    if (this._cache.caret.xPos > bounds.right)
-      this._cache.caret.xPos = bounds.right;
-
-    if (this._cache.start.yPos < bounds.top)
-      this._cache.start.yPos = bounds.top;
-    if (this._cache.end.yPos < bounds.top)
-      this._cache.end.yPos = bounds.top;
-    if (this._cache.caret.yPos < bounds.top)
-      this._cache.caret.yPos = bounds.top;
-    if (this._cache.start.yPos > bounds.bottom)
-      this._cache.start.yPos = bounds.bottom;
-    if (this._cache.end.yPos > bounds.bottom)
-      this._cache.end.yPos = bounds.bottom;
-    if (this._cache.caret.yPos > bounds.bottom)
-      this._cache.caret.yPos = bounds.bottom;
-  },
-
-  _restrictCoordinateToEditBounds: function _restrictCoordinateToEditBounds(aX, aY) {
-    let result = {
-      xPos: aX,
-      yPos: aY
-    };
-    if (!this._targetIsEditable)
-      return result;
-    let bounds = this._getTargetBrowserRect();
-    if (aX <= bounds.left)
-      result.xPos = bounds.left + 1;
-    if (aX >= bounds.right)
-      result.xPos = bounds.right - 1;
-    if (aY <= bounds.top)
-      result.yPos = bounds.top + 1;
-    if (aY >= bounds.bottom)
-      result.yPos = bounds.bottom - 1;
-  },
-
-  /*
-   * _handleSelectionPoint(aMarker, aPoint, aEndOfSelection) 
-   *
-   * After a monocle moves to a new point in the document, determines
-   * what the target is and acts on its selection accordingly. If the
-   * monocle is within the bounds of the target, adds or subtracts selection
-   * at the monocle coordinates appropriately and then merges selection ranges
-   * into a single continuous selection. If the monocle is outside the bounds
-   * of the target and the underlying target is editable, uses the selection
-   * controller to advance selection and visibility within the control.
-   */
-  _handleSelectionPoint: function _handleSelectionPoint(aMarker, aClientPoint,
-                                                        aEndOfSelection) {
-    let selection = this._getSelection();
-
-    let clientPoint = { xPos: aClientPoint.xPos, yPos: aClientPoint.yPos };
-
-    if (selection.rangeCount == 0) {
-      this._onFail("selection.rangeCount == 0");
-      return;
-    }
-
-    // We expect continuous selection ranges.
-    if (selection.rangeCount > 1) {
-      this._setContinuousSelection();
-    }
-
-    // Adjust our y position up such that we are sending coordinates on
-    // the text line vs. below it where the monocle is positioned.
-    let halfLineHeight = this._queryHalfLineHeight(aMarker, selection);
-    clientPoint.yPos -= halfLineHeight;
-
-    // Modify selection based on monocle movement
-    if (this._targetIsEditable) {
-      this._adjustEditableSelection(aMarker, clientPoint,
-                                    halfLineHeight, aEndOfSelection);
-    } else {
-      this._adjustSelection(aMarker, clientPoint, aEndOfSelection);
-    }
-  },
-
-  /*
-   * _handleSelectionPoint helper methods
-   */
-
-  /*
-   * _adjustEditableSelection
-   *
-   * Based on a monocle marker and position, adds or subtracts from the
-   * existing selection in editable controls. Handles auto-scroll as well.
-   *
-   * @param the marker currently being manipulated
-   * @param aAdjustedClientPoint client point adjusted for line height.
-   * @param aHalfLineHeight half line height in pixels
-   * @param aEndOfSelection indicates if this is the end of a selection
-   * move, in which case we may want to snap to the end of a word or
-   * sentence.
-   */
-  _adjustEditableSelection: function _adjustEditableSelection(aMarker,
-                                                              aAdjustedClientPoint,
-                                                              aHalfLineHeight,
-                                                              aEndOfSelection) {
-    // Test to see if we need to handle auto-scroll in cases where the
-    // monocle is outside the bounds of the control. This also handles
-    // adjusting selection if out-of-bounds is true.
-    let result = this.updateTextEditSelection(aAdjustedClientPoint);
-
-    // If result.trigger is true, the monocle is outside the bounds of the
-    // control.
-    if (result.trigger) {
-      // _handleSelectionPoint is triggered by input movement, so if we've
-      // tested positive for out-of-bounds scrolling here, we need to set a
-      // recurring timer to keep the expected selection behavior going as
-      // long as the user keeps the monocle out of bounds.
-      this._setTextEditUpdateInterval(result.speed);
-
-      // Smooth the selection
-      this._setContinuousSelection();
-
-      // Update the other monocle's position if we've dragged off to one side
-      this._updateSelectionUI(result.start, result.end);
-    } else {
-      // If we aren't out-of-bounds, clear the scroll timer if it exists.
-      this._clearTimers();
-
-      // Restrict the client point to the interior of the control. Prevents
-      // _adjustSelection from accidentally selecting content outside the
-      // control.
-      let constrainedPoint =
-        this._constrainPointWithinControl(aAdjustedClientPoint, aHalfLineHeight);
-
-      // Add or subtract selection
-      this._adjustSelection(aMarker, constrainedPoint, aEndOfSelection);
-    }
-  },
-
-  /*
-   * _adjustSelection
-   *
-   * Based on a monocle marker and position, adds or subtracts from the
-   * existing selection.
-   *
-   * @param the marker currently being manipulated
-   * @param aClientPoint the point designating the new start or end
-   * position for the selection.
-   * @param aEndOfSelection indicates if this is the end of a selection
-   * move, in which case we may want to snap to the end of a word or
-   * sentence.
-   */
-  _adjustSelection: function _adjustSelection(aMarker, aClientPoint,
-                                              aEndOfSelection) {
-    // Make a copy of the existing range, we may need to reset it.
-    this._backupRangeList();
-
-    // shrinkSelectionFromPoint takes sub-frame relative coordinates.
-    let framePoint = this._clientPointToFramePoint(aClientPoint);
-
-    // Tests to see if the user is trying to shrink the selection, and if so
-    // collapses it down to the appropriate side such that our calls below
-    // will reset the selection to the proper range.
-    let shrunk = this._shrinkSelectionFromPoint(aMarker, framePoint);
-
-    let selectResult = false;
-    try {
-      // If we're at the end of a selection (touchend) snap to the word.
-      let type = ((aEndOfSelection && this._snap) ?
-        Ci.nsIDOMWindowUtils.SELECT_WORD :
-        Ci.nsIDOMWindowUtils.SELECT_CHARACTER);
-
-      // Select a character at the point.
-      selectResult = 
-        this._domWinUtils.selectAtPoint(framePoint.xPos,
-                                        framePoint.yPos,
-                                        type);
-    } catch (ex) {
-    }
-
-    // If selectAtPoint failed (which can happen if there's nothing to select)
-    // reset our range back before we shrunk it.
-    if (!selectResult) {
-      this._restoreRangeList();
-    }
-
-    this._freeRangeList();
-
-    // Smooth over the selection between all existing ranges.
-    this._setContinuousSelection();
-
-    // Update the other monocle's position. We do this because the dragging
-    // monocle may reset the static monocle to a new position if the dragging
-    // monocle drags ahead or behind the other.
-    if (aMarker == "start") {
-      this._updateSelectionUI(false, true);
-    } else {
-      this._updateSelectionUI(true, false);
-    }
-  },
-
-  /*
-   * _backupRangeList, _restoreRangeList, and _freeRangeList
-   *
-   * Utilities that manage a cloned copy of the existing selection.
-   */
-
-  _backupRangeList: function _backupRangeList() {
-    this._rangeBackup = new Array();
-    for (let idx = 0; idx < this._getSelection().rangeCount; idx++) {
-      this._rangeBackup.push(this._getSelection().getRangeAt(idx).cloneRange());
-    }
-  },
-
-  _restoreRangeList: function _restoreRangeList() {
-    if (this._rangeBackup == null)
-      return;
-    for (let idx = 0; idx < this._rangeBackup.length; idx++) {
-      this._getSelection().addRange(this._rangeBackup[idx]);
-    }
-    this._freeRangeList();
-  },
-
-  _freeRangeList: function _restoreRangeList() {
-    this._rangeBackup = null;
-  },
-
-  /*
-   * Constrains a selection point within a text input control bounds.
-   *
-   * @param aPoint - client coordinate point
-   * @param aHalfLineHeight - half the line height at the point
-   * @return new constrained point struct
-   */
-  _constrainPointWithinControl: function _cpwc(aPoint, aHalfLineHeight) {
-    let bounds = this._getTargetBrowserRect();
-    let point = { xPos: aPoint.xPos, yPos: aPoint.yPos };
-    if (point.xPos <= bounds.left)
-      point.xPos = bounds.left + 2;
-    if (point.xPos >= bounds.right)
-      point.xPos = bounds.right - 2;
-    if (point.yPos <= (bounds.top + aHalfLineHeight))
-      point.yPos = (bounds.top + aHalfLineHeight);
-    if (point.yPos >= (bounds.bottom - aHalfLineHeight))
-      point.yPos = (bounds.bottom - aHalfLineHeight);
-    return point;
-  },
-
-  /*
-   * _pointOrientationToRect(aPoint, aRect)
-   *
-   * Returns a table representing which sides of target aPoint is offset
-   * from: { left: offset, top: offset, right: offset, bottom: offset }
-   * Works on client coordinates.
-   */
-  _pointOrientationToRect: function _pointOrientationToRect(aPoint) {
-    let bounds = this._getTargetBrowserRect();
-    let result = { left: 0, right: 0, top: 0, bottom: 0 };
-    if (aPoint.xPos <= bounds.left)
-      result.left = bounds.left - aPoint.xPos;
-    if (aPoint.xPos >= bounds.right)
-      result.right = aPoint.xPos - bounds.right;
-    if (aPoint.yPos <= bounds.top)
-      result.top = bounds.top - aPoint.yPos;
-    if (aPoint.yPos >= bounds.bottom)
-      result.bottom = aPoint.yPos - bounds.bottom;
-    return result;
-  },
-
-  /*
-   * updateTextEditSelection(aPoint, aClientPoint)
-   *
-   * Checks to see if the monocle point is outside the bounds of the target
-   * edit. If so, use the selection controller to select and scroll the edit
-   * appropriately.
-   *
-   * @param aClientPoint raw pointer position
-   * @return { speed: 0.0 -> 1.0,
-   *           trigger: true/false if out of bounds,
-   *           start: true/false if updated position,
-   *           end: true/false if updated position }
-   */
-  updateTextEditSelection: function updateTextEditSelection(aClientPoint) {
-    if (aClientPoint == undefined) {
-      aClientPoint = this._rawSelectionPoint;
-    }
-    this._rawSelectionPoint = aClientPoint;
-
-    let orientation = this._pointOrientationToRect(aClientPoint);
-    let result = { speed: 1, trigger: false, start: false, end: false };
-    let ml = Util.isMultilineInput(this._targetElement);
-
-    // This could be improved such that we only select to the beginning of
-    // the line when dragging left but not up.
-    if (orientation.left || (ml && orientation.top)) {
-      this._addEditSelection(kSelectionNodeAnchor);
-      result.speed = orientation.left + orientation.top;
-      result.trigger = true;
-      result.end = true;
-    } else if (orientation.right || (ml && orientation.bottom)) {
-      this._addEditSelection(kSelectionNodeFocus);
-      result.speed = orientation.right + orientation.bottom;
-      result.trigger = true;
-      result.start = true;
-    }
-
-    // 'speed' is just total pixels offset, so clamp it to something
-    // reasonable callers can work with.
-    if (result.speed > 100)
-      result.speed = 100;
-    if (result.speed < 1)
-      result.speed = 1;
-    result.speed /= 100;
-    return result;
-  },
-
-  _setTextEditUpdateInterval: function _setTextEditUpdateInterval(aSpeedValue) {
-    let timeout = (75 - (aSpeedValue * 75));
-    if (!this._scrollTimer)
-      this._scrollTimer = new Util.Timeout();
-    this._scrollTimer.interval(timeout, this.scrollTimerCallback);
-  },
-
-  _clearTimers: function _clearTimers() {
-    if (this._scrollTimer) {
-      this._scrollTimer.clear();
-    }
-  },
-
-  /*
-   * _addEditSelection - selection control call wrapper for text inputs.
-   * Adds selection on the anchor or focus side of selection in a text
-   * input. Scrolls the location into view as well.
-   *
-   * @param const selection node identifier
-   */
-  _addEditSelection: function _addEditSelection(aLocation) {
-    let selCtrl = this._getSelectController();
-    try {
-      if (aLocation == kSelectionNodeAnchor) {
-        let start = Math.max(this._targetElement.selectionStart - 1, 0);
-        this._targetElement.setSelectionRange(start, this._targetElement.selectionEnd,
-                                              "backward");
-      } else {
-        let end = Math.min(this._targetElement.selectionEnd + 1,
-                           this._targetElement.textLength);
-        this._targetElement.setSelectionRange(this._targetElement.selectionStart,
-                                              end,
-                                              "forward");
-      }
-      selCtrl.scrollSelectionIntoView(Ci.nsISelectionController.SELECTION_NORMAL,
-                                      Ci.nsISelectionController.SELECTION_FOCUS_REGION,
-                                      Ci.nsISelectionController.SCROLL_SYNCHRONOUS);
-    } catch (ex) { Util.dumpLn(ex);}
-  },
-
-  _updateInputFocus: function _updateInputFocus(aMarker) {
-    try {
-      let selCtrl = this._getSelectController();
-      this._targetElement.setSelectionRange(this._targetElement.selectionStart,
-                                            this._targetElement.selectionEnd,
-                                            aMarker == "start" ?
-                                              "backward" : "forward");
-      selCtrl.scrollSelectionIntoView(Ci.nsISelectionController.SELECTION_NORMAL,
-                                      Ci.nsISelectionController.SELECTION_FOCUS_REGION,
-                                      Ci.nsISelectionController.SCROLL_SYNCHRONOUS);
-    } catch (ex) {}
-  },
-
-  /*
-   * _queryHalfLineHeight(aMarker, aSelection)
-   *
-   * Y offset applied to the coordinates of the selection position we send
-   * to dom utils. The selection marker sits below text, but we want the
-   * selection position to be on the text above the monocle. Since text
-   * height can vary across the entire selection range, we need the correct
-   * height based on the line the marker in question is moving on.
-   */
-  _queryHalfLineHeight: function _queryHalfLineHeight(aMarker, aSelection) {
-    let rects = aSelection.getRangeAt(0).getClientRects();
-    if (!rects.length) {
-      return 0;
-    }
-
-    // We are assuming here that these rects are ordered correctly.
-    // From looking at the range code it appears they will be.
-    let height = 0;
-    if (aMarker == "start") {
-      // height of the first rect corresponding to the start marker:
-      height = rects[0].bottom - rects[0].top;
-    } else {
-      // height of the last rect corresponding to the end marker:
-      let len = rects.length - 1;
-      height = rects[len].bottom - rects[len].top;
-    }
-    return height / 2;
-  },
-
-  /*
-   * _setContinuousSelection()
-   *
-   * Smooths a selection with multiple ranges into a single
-   * continuous range.
-   */
-  _setContinuousSelection: function _setContinuousSelection() {
-    let selection = this._getSelection();
-    try {
-      if (selection.rangeCount > 1) {
-        let startRange = selection.getRangeAt(0);
-        if (this. _debugOptions.displayRanges) {
-          let clientRect = startRange.getBoundingClientRect();
-          this._setDebugRect(clientRect, "red", false);
-        }
-        let newStartNode = null;
-        let newStartOffset = 0;
-        let newEndNode = null;
-        let newEndOffset = 0;
-        for (let idx = 1; idx < selection.rangeCount; idx++) {
-          let range = selection.getRangeAt(idx);
-          switch (startRange.compareBoundaryPoints(Ci.nsIDOMRange.START_TO_START, range)) {
-            case -1: // startRange is before
-              newStartNode = startRange.startContainer;
-              newStartOffset = startRange.startOffset;
-              break;
-            case 0: // startRange is equal
-              newStartNode = startRange.startContainer;
-              newStartOffset = startRange.startOffset;
-              break;
-            case 1: // startRange is after
-              newStartNode = range.startContainer;
-              newStartOffset = range.startOffset;
-              break;
-          }
-          switch (startRange.compareBoundaryPoints(Ci.nsIDOMRange.END_TO_END, range)) {
-            case -1: // startRange is before
-              newEndNode = range.endContainer;
-              newEndOffset = range.endOffset;
-              break;
-            case 0: // startRange is equal
-              newEndNode = range.endContainer;
-              newEndOffset = range.endOffset;
-              break;
-            case 1: // startRange is after
-              newEndNode = startRange.endContainer;
-              newEndOffset = startRange.endOffset;
-              break;
-          }
-          if (this. _debugOptions.displayRanges) {
-            let clientRect = range.getBoundingClientRect();
-            this._setDebugRect(clientRect, "orange", false);
-          }
-        }
-        let range = content.document.createRange();
-        range.setStart(newStartNode, newStartOffset);
-        range.setEnd(newEndNode, newEndOffset);
-        selection.addRange(range);
-      }
-    } catch (ex) {
-      Util.dumpLn("exception while modifying selection:", ex.message);
-      this._onFail("_handleSelectionPoint failed.");
-      return false;
-    }
-    return true;
-  },
-
-  /*
-   * _shrinkSelectionFromPoint(aMarker, aFramePoint)
-   *
-   * Tests to see if aFramePoint intersects the current selection and if so,
-   * collapses selection down to the opposite start or end point leaving a
-   * character of selection at the collapse point.
-   *
-   * @param aMarker the marker that is being relocated. ("start" or "end")
-   * @param aFramePoint position of the marker.
-   */
-  _shrinkSelectionFromPoint: function _shrinkSelectionFromPoint(aMarker, aFramePoint) {
-    let result = false;
-    try {
-      let selection = this._getSelection();
-      for (let range = 0; range < selection.rangeCount; range++) {
-        // relative to frame
-        let rects = selection.getRangeAt(range).getClientRects();
-        for (let idx = 0; idx < rects.length; idx++) {
-          // Util.dumpLn("[" + idx + "]", aFramePoint.xPos, aFramePoint.yPos, rects[idx].left,
-          // rects[idx].top, rects[idx].right, rects[idx].bottom);
-          if (Util.pointWithinDOMRect(aFramePoint.xPos, aFramePoint.yPos, rects[idx])) {
-            result = true;
-            if (aMarker == "start") {
-              selection.collapseToEnd();
-            } else {
-              selection.collapseToStart();
-            }
-            // collapseToStart and collapseToEnd leave an empty range in the
-            // selection at the collapse point. Therefore we need to add some
-            // selection such that the selection added by selectAtPoint and
-            // the current empty range will get merged properly when we smooth
-            // the selection ranges out.
-            let selCtrl = this._getSelectController();
-            // Expand the collapsed range such that it occupies a little space.
-            if (aMarker == "start") {
-              // State: focus = anchor (collapseToEnd does this)
-              selCtrl.characterMove(false, true);
-              // State: focus = (anchor - 1)
-              selection.collapseToStart();
-              // State: focus = anchor and both are -1 from the original offset
-              selCtrl.characterMove(true, true);
-              // State: focus = anchor + 1, both have been moved back one char
-            } else {
-              selCtrl.characterMove(true, true);
-            }
-            break;
-          }
-        }
-      }
-    } catch (ex) {
-      Util.dumpLn("error shrinking selection:", ex.message);
-    }
-    return result;
-  },
-
-  /*
    * _calcNewContentPosition - calculates the distance the browser should be
    * raised to move the focused form input out of the way of the soft
    * keyboard.
    *
    * @param aNewViewHeight the new content view height
    * @return 0 if no positioning is required or a positive val equal to the
    * distance content should be raised to center the target element.
    */
@@ -1169,32 +470,29 @@ var SelectionHandler = {
     if (caretLocation <= aNewViewHeight) {
       return 0;
     }
 
     // distance from the top of the keyboard down to the caret location
     return caretLocation - aNewViewHeight;
   },
 
-  /*
+  /*************************************************
    * Events
    */
 
   /*
    * Scroll + selection advancement timer when the monocle is
    * outside the bounds of an input control.
    */
   scrollTimerCallback: function scrollTimerCallback() {
     let result = SelectionHandler.updateTextEditSelection();
     // Update monocle position and speed if we've dragged off to one side
     if (result.trigger) {
-      if (result.start)
-        SelectionHandler._updateSelectionUI(true, false);
-      if (result.end)
-        SelectionHandler._updateSelectionUI(false, true);
+      SelectionHandler._updateSelectionUI("update", result.start, result.end);
     }
   },
 
   receiveMessage: function sh_receiveMessage(aMessage) {
     if (this._debugEvents && aMessage.name != "Browser:SelectionMove") {
       Util.dumpLn("SelectionHandler:", aMessage.name);
     }
     let json = aMessage.json;
@@ -1256,108 +554,21 @@ var SelectionHandler = {
         break;
 
       case "Browser:SelectionHandlerPing":
         this._onPing(json.id);
         break;
     }
   },
 
-  /*
+  /*************************************************
    * Utilities
    */
 
   /*
-   * _extractUIRects - Extracts selection and target element information
-   * used by SelectionHelperUI. Returns client relative coordinates.
-   *
-   * @return table containing various ui rects and information
-   */
-  _extractUIRects: function _extractUIRects(aRange) {
-    let seldata = {
-      start: {}, end: {}, caret: {},
-      selection: { left: 0, top: 0, right: 0, bottom: 0 },
-      element: { left: 0, top: 0, right: 0, bottom: 0 }
-    };
-
-    // When in an iframe, aRange coordinates are relative to the frame origin.
-    let rects = aRange.getClientRects();
-
-    if (rects && rects.length) {
-      let startSet = false;
-      for (let idx = 0; idx < rects.length; idx++) {
-        if (this. _debugOptions.dumpRanges) Util.dumpDOMRect(idx, rects[idx]);
-        if (!startSet && !Util.isEmptyDOMRect(rects[idx])) {
-          seldata.start.xPos = rects[idx].left + this._contentOffset.x;
-          seldata.start.yPos = rects[idx].bottom + this._contentOffset.y;
-          seldata.caret = seldata.start;
-          startSet = true;
-          if (this. _debugOptions.dumpRanges) Util.dumpLn("start set");
-        }
-        if (!Util.isEmptyDOMRect(rects[idx])) {
-          seldata.end.xPos = rects[idx].right + this._contentOffset.x;
-          seldata.end.yPos = rects[idx].bottom + this._contentOffset.y;
-          if (this. _debugOptions.dumpRanges) Util.dumpLn("end set");
-        }
-      }
-
-      // Store the client rect of selection
-      let r = aRange.getBoundingClientRect();
-      seldata.selection.left = r.left + this._contentOffset.x;
-      seldata.selection.top = r.top + this._contentOffset.y;
-      seldata.selection.right = r.right + this._contentOffset.x;
-      seldata.selection.bottom = r.bottom + this._contentOffset.y;
-    }
-
-    // Store the client rect of target element
-    r = this._getTargetClientRect();
-    seldata.element.left = r.left + this._contentOffset.x;
-    seldata.element.top = r.top + this._contentOffset.y;
-    seldata.element.right = r.right + this._contentOffset.x;
-    seldata.element.bottom = r.bottom + this._contentOffset.y;
-
-    // If we don't have a range we can attach to let SelectionHelperUI know.
-    seldata.selectionRangeFound = !!rects.length;
-
-    return seldata;
-  },
-
-  /*
-   * Returns bounds of the element relative to the inner sub frame it sits
-   * in.
-   */
-  _getTargetClientRect: function _getTargetClientRect() {
-    return this._targetElement.getBoundingClientRect();
-  },
-
-  /*
-   * Returns bounds of the element relative to the top level browser.
-   */
-  _getTargetBrowserRect: function _getTargetBrowserRect() {
-    let client = this._getTargetClientRect();
-    return {
-      left: client.left +  this._contentOffset.x,
-      top: client.top +  this._contentOffset.y,
-      right: client.right +  this._contentOffset.x,
-      bottom: client.bottom +  this._contentOffset.y
-    };
-  },
-
-   /*
-    * Translate a top level client point to frame relative client point.
-    */
-  _clientPointToFramePoint: function _clientPointToFramePoint(aClientPoint) {
-    let point = {
-      xPos: aClientPoint.xPos - this._contentOffset.x,
-      yPos: aClientPoint.yPos - this._contentOffset.y
-    };
-    return point;
-  },
-
-  /*
    * Retrieve the total offset from the window's origin to the sub frame
    * element including frame and scroll offsets. The resulting offset is
    * such that:
    * sub frame coords + offset = root frame position
    */
   getCurrentWindowAndOffset: function(x, y) {
     // If the element at the given point belongs to another document (such
     // as an iframe's subdocument), the element in the calling document's
@@ -1447,84 +658,13 @@ var SelectionHandler = {
       let docShell = this._getDocShell(this._contentWindow);
       if (docShell == null)
         return null;
       return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                      .getInterface(Ci.nsISelectionDisplay)
                      .QueryInterface(Ci.nsISelectionController);
     }
   },
-
-  /*
-   * Debug routines
-   */
-
-  _debugDumpSelection: function _debugDumpSelection(aNote, aSel) {
-    Util.dumpLn("--" + aNote + "--");
-    Util.dumpLn("anchor:", aSel.anchorNode, aSel.anchorOffset);
-    Util.dumpLn("focus:", aSel.focusNode, aSel.focusOffset);
-  },
-
-  _debugDumpChildNodes: function _dumpChildNodes(aNode, aSpacing) {
-    for (let idx = 0; idx < aNode.childNodes.length; idx++) {
-      let node = aNode.childNodes.item(idx);
-      for (let spaceIdx = 0; spaceIdx < aSpacing; spaceIdx++) dump(" ");
-      Util.dumpLn("[" + idx + "]", node);
-      this._debugDumpChildNodes(node, aSpacing + 1);
-    }
-  },
-
-  _setDebugElementRect: function _setDebugElementRect(e, aScrollOffset, aColor) {
-    try {
-      if (e == null) {
-        Util.dumpLn("SelectionHandler _setDebugElementRect(): passed in null element");
-        return;
-      }
-      if (e.offsetWidth == 0 || e.offsetHeight== 0) {
-        Util.dumpLn("SelectionHandler _setDebugElementRect(): passed in flat rect");
-        return;
-      }
-      // e.offset values are positioned relative to the view.
-      sendAsyncMessage("Content:SelectionDebugRect",
-        { left:e.offsetLeft - aScrollOffset.x,
-          top:e.offsetTop - aScrollOffset.y,
-          right:e.offsetLeft + e.offsetWidth - aScrollOffset.x,
-          bottom:e.offsetTop + e.offsetHeight - aScrollOffset.y,
-          color:aColor, id: e.id });
-    } catch(ex) {
-      Util.dumpLn("SelectionHandler _setDebugElementRect():", ex.message);
-    }
-  },
-
-  /*
-   * Adds a debug rect to the selection overlay, useful in identifying
-   * locations for points and rects. Params are in client coordinates.
-   *
-   * Example:
-   * let rect = { left: aPoint.xPos - 1, top: aPoint.yPos - 1,
-   *              right: aPoint.xPos + 1, bottom: aPoint.yPos + 1 };
-   * this._setDebugRect(rect, "red");
-   *
-   * In SelectionHelperUI, you'll need to turn on displayDebugLayer
-   * in init().
-   */
-  _setDebugRect: function _setDebugRect(aRect, aColor, aFill, aId) {
-    sendAsyncMessage("Content:SelectionDebugRect",
-      { left:aRect.left, top:aRect.top,
-        right:aRect.right, bottom:aRect.bottom,
-        color:aColor, fill: aFill, id: aId });
-  },
-
-  /*
-   * Adds a small debug rect at the point specified. Params are in
-   * client coordinates.
-   *
-   * In SelectionHelperUI, you'll need to turn on displayDebugLayer
-   * in init().
-   */
-  _setDebugPoint: function _setDebugPoint(aX, aY, aColor) {
-    let rect = { left: aX - 2, top: aY - 2,
-                 right: aX + 2, bottom: aY + 2 };
-    this._setDebugRect(rect, aColor, true);
-  },
 };
 
-SelectionHandler.init();
\ No newline at end of file
+SelectionHandler.__proto__ = new SelectionPrototype();
+SelectionHandler.init();
+
new file mode 100644
--- /dev/null
+++ b/browser/metro/base/content/helperui/ChromeSelectionHandler.js
@@ -0,0 +1,364 @@
+/* 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/. */
+
+/*
+ * Selection handler for chrome text inputs
+ */
+
+const kCaretMode = 1;
+const kSelectionMode = 2;
+
+var ChromeSelectionHandler = {
+  _mode: kSelectionMode,
+
+  /*************************************************
+   * Messaging wrapper
+   */
+
+  sendAsync: function sendAsync(aMsg, aJson) {
+    SelectionHelperUI.receiveMessage({ 
+      name: aMsg,
+      json: aJson
+    });
+  },
+
+  /*************************************************
+   * Browser event handlers
+   */
+
+  /*
+   * General selection start method for both caret and selection mode.
+   */
+  _onSelectionAttach: function _onSelectionAttach(aJson) {
+    this._domWinUtils = Util.getWindowUtils(window);
+    this._contentWindow = window;
+    this._targetElement = this._domWinUtils.elementFromPoint(aJson.xPos, aJson.yPos, true, false);
+
+    this._targetIsEditable = this._targetElement instanceof Components.interfaces.nsIDOMXULTextBoxElement;
+    if (!this._targetIsEditable) {
+      this._onFail("not an editable?");
+      return;
+    }
+
+    let selection = this._getSelection();
+    if (!selection) {
+      this._onFail("no selection.");
+      return;
+    }
+
+    if (!selection.isCollapsed) {
+      this._mode = kSelectionMode;
+      this._updateSelectionUI("start", true, true);
+    } else {
+      this._mode = kCaretMode;
+      this._updateSelectionUI("caret", false, false, true);
+    }
+
+  },
+
+  /*
+   * Selection monocle start move event handler
+   */
+  _onSelectionMoveStart: function _onSelectionMoveStart(aMsg) {
+    if (!this.targetIsEditable) {
+      this._onFail("_onSelectionMoveStart with bad targetElement.");
+      return;
+    }
+
+    if (this._selectionMoveActive) {
+      this._onFail("mouse is already down on drag start?");
+      return;
+    }
+
+    // We bail if things get out of sync here implying we missed a message.
+    this._selectionMoveActive = true;
+
+    if (this._targetIsEditable) {
+      // If we're coming out of an out-of-bounds scroll, the node the user is
+      // trying to drag may be hidden (the monocle will be pegged to the edge
+      // of the edit). Make sure the node the user wants to move is visible
+      // and has focus.
+      this._updateInputFocus(aMsg.change);
+    }
+
+    // Update the position of our selection monocles
+    this._updateSelectionUI("update", true, true);
+  },
+  
+  /*
+   * Selection monocle move event handler
+   */
+  _onSelectionMove: function _onSelectionMove(aMsg) {
+    if (!this.targetIsEditable) {
+      this._onFail("_onSelectionMove with bad targetElement.");
+      return;
+    }
+
+    if (!this._selectionMoveActive) {
+      this._onFail("mouse isn't down for drag move?");
+      return;
+    }
+
+    // Update selection in the doc
+    let pos = null;
+    if (aMsg.change == "start") {
+      pos = aMsg.start;
+    } else {
+      pos = aMsg.end;
+    }
+    this._handleSelectionPoint(aMsg.change, pos, false);
+  },
+
+  /*
+   * Selection monocle move finished event handler
+   */
+  _onSelectionMoveEnd: function _onSelectionMoveComplete(aMsg) {
+    if (!this.targetIsEditable) {
+      this._onFail("_onSelectionMoveEnd with bad targetElement.");
+      return;
+    }
+
+    if (!this._selectionMoveActive) {
+      this._onFail("mouse isn't down for drag move?");
+      return;
+    }
+
+    // Update selection in the doc
+    let pos = null;
+    if (aMsg.change == "start") {
+      pos = aMsg.start;
+    } else {
+      pos = aMsg.end;
+    }
+
+    this._handleSelectionPoint(aMsg.change, pos, true);
+    this._selectionMoveActive = false;
+    
+    // Update the position of our selection monocles
+    this._updateSelectionUI("end", true, true);
+  },
+
+  /*
+   * Switch selection modes. Currently we only support switching
+   * from "caret" to "selection".
+   */
+  _onSwitchMode: function _onSwitchMode(aMode, aMarker, aX, aY) {
+    if (aMode != "selection") {
+      this._onFail("unsupported mode switch");
+      return;
+    }
+    
+    // Sanity check to be sure we are initialized
+    if (!this._targetElement) {
+      this._onFail("not initialized");
+      return;
+    }
+
+    // Similar to _onSelectionStart - we need to create initial selection
+    // but without the initialization bits.
+    let framePoint = this._clientPointToFramePoint({ xPos: aX, yPos: aY });
+    if (!this._domWinUtils.selectAtPoint(framePoint.xPos, framePoint.yPos,
+                                         Ci.nsIDOMWindowUtils.SELECT_CHARACTER)) {
+      this._onFail("failed to set selection at point");
+      return;
+    }
+
+    // We bail if things get out of sync here implying we missed a message.
+    this._selectionMoveActive = true;
+    this._mode = kSelectionMode;
+
+    // Update the position of the selection marker that is *not*
+    // being dragged.
+    this._updateSelectionUI("update", aMarker == "end", aMarker == "start");
+  },
+
+  /*
+   * Selection close event handler
+   *
+   * @param aClearSelection requests that selection be cleared.
+   */
+  _onSelectionClose: function _onSelectionClose(aClearSelection) {
+    if (aClearSelection) {
+      this._clearSelection();
+    }
+    this._closeSelection();
+  },
+
+  /*
+   * Called if for any reason we fail during the selection
+   * process. Cancels the selection.
+   */
+  _onFail: function _onFail(aDbgMessage) {
+    if (aDbgMessage && aDbgMessage.length > 0)
+      Util.dumpLn(aDbgMessage);
+    this.sendAsync("Content:SelectionFail");
+    this._clearSelection();
+    this._closeSelection();
+  },
+
+  /*
+   * Empty conversion routines to match those in
+   * browser. Called by SelectionHelperUI when
+   * sending and receiving coordinates in messages.
+   */
+
+  ptClientToBrowser: function ptClientToBrowser(aX, aY, aIgnoreScroll, aIgnoreScale) {
+    return { x: aX, y: aY }
+  },
+
+  rectBrowserToClient: function rectBrowserToClient(aRect, aIgnoreScroll, aIgnoreScale) {
+    return {
+      left: aRect.left,
+      right: aRect.right,
+      top: aRect.top,
+      bottom: aRect.bottom
+    }
+  },
+
+  ptBrowserToClient: function ptBrowserToClient(aX, aY, aIgnoreScroll, aIgnoreScale) {
+    return { x: aX, y: aY }
+  },
+
+  ctobx: function ctobx(aCoord) {
+    return aCoord;
+  },
+
+  ctoby: function ctoby(aCoord) {
+    return aCoord;
+  },
+
+  btocx: function btocx(aCoord) {
+    return aCoord;
+  },
+
+  btocy: function btocy(aCoord) {
+    return aCoord;
+  },
+
+
+  /*************************************************
+   * Selection helpers
+   */
+
+  /*
+   * _clearSelection
+   *
+   * Clear existing selection if it exists and reset our internla state.
+   */
+  _clearSelection: function _clearSelection() {
+    let selection = this._getSelection();
+    if (selection) {
+      selection.removeAllRanges();
+    }
+  },
+
+  /*
+   * _closeSelection
+   *
+   * Shuts SelectionHandler down.
+   */
+  _closeSelection: function _closeSelection() {
+    this._clearTimers();
+    this._cache = null;
+    this._contentWindow = null;
+    this._targetElement = null;
+    this._selectionMoveActive = false;
+    this._domWinUtils = null;
+    this._targetIsEditable = false;
+    this.sendAsync("Content:HandlerShutdown", {});
+  },
+
+  /*************************************************
+   * Events
+   */
+
+  /*
+   * Scroll + selection advancement timer when the monocle is
+   * outside the bounds of an input control.
+   */
+  scrollTimerCallback: function scrollTimerCallback() {
+    let result = ChromeSelectionHandler.updateTextEditSelection();
+    // Update monocle position and speed if we've dragged off to one side
+    if (result.trigger) {
+      ChromeSelectionHandler._updateSelectionUI("update", result.start, result.end);
+    }
+  },
+
+  msgHandler: function msgHandler(aMsg, aJson) {
+    if (this._debugEvents && "Browser:SelectionMove" != aMsg) {
+      Util.dumpLn("ChromeSelectionHandler:", aMsg);
+    }
+    switch(aMsg) {
+      case "Browser:SelectionDebug":
+        this._onSelectionDebug(aJson);
+        break;
+
+      case "Browser:SelectionAttach":
+        this._onSelectionAttach(aJson);
+      break;
+
+      case "Browser:CaretAttach":
+        this._onSelectionAttach(aJson);
+        break;
+
+      case "Browser:SelectionClose":
+        this._onSelectionClose(aJson.clearSelection);
+      break;
+
+      case "Browser:SelectionUpdate":
+        this._updateSelectionUI("update",
+                                this._mode == kSelectionMode,
+                                this._mode == kSelectionMode,
+                                this._mode == kCaretMode);
+      break;
+
+      case "Browser:SelectionMoveStart":
+        this._onSelectionMoveStart(aJson);
+        break;
+
+      case "Browser:SelectionMove":
+        this._onSelectionMove(aJson);
+        break;
+
+      case "Browser:SelectionMoveEnd":
+        this._onSelectionMoveEnd(aJson);
+        break;
+
+      case "Browser:CaretUpdate":
+        this._onCaretPositionUpdate(aJson.caret.xPos, aJson.caret.yPos);
+        break;
+
+      case "Browser:CaretMove":
+        this._onCaretMove(aJson.caret.xPos, aJson.caret.yPos);
+        break;
+
+      case "Browser:SelectionSwitchMode":
+        this._onSwitchMode(aJson.newMode, aJson.change, aJson.xPos, aJson.yPos);
+        break;
+    }
+  },
+
+  /*************************************************
+   * Utilities
+   */
+
+  _getSelection: function _getSelection() {
+    if (this._targetElement instanceof Ci.nsIDOMXULTextBoxElement) {
+      return this._targetElement
+                 .QueryInterface(Components.interfaces.nsIDOMXULTextBoxElement)
+                 .editor.selection;
+    }
+    return null;
+  },
+
+  _getSelectController: function _getSelectController() {
+    return this._targetElement
+                .QueryInterface(Components.interfaces.nsIDOMXULTextBoxElement)
+                .editor.selectionController;
+  },
+};
+
+ChromeSelectionHandler.__proto__ = new SelectionPrototype();
+ChromeSelectionHandler.type = 1; // kChromeSelector
+
--- a/browser/metro/base/content/helperui/SelectionHelperUI.js
+++ b/browser/metro/base/content/helperui/SelectionHelperUI.js
@@ -8,24 +8,26 @@
 
 /*
  * Current monocle image:
  *  dimensions: 32 x 24
  *  circle center: 16 x 14
  *  padding top: 6
  */
 
-XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/commonjs/sdk/core/promise.js");
-
 // Y axis scroll distance that will disable this module and cancel selection
 const kDisableOnScrollDistance = 25;
 
 // Drag hysteresis programmed into monocle drag moves
 const kDragHysteresisDistance = 10;
 
+// selection layer id returned from SelectionHandlerUI's layerMode.
+const kChromeLayer = 1;
+const kContentLayer = 2;
+
 /*
  * Markers
  */
 
 function MarkerDragger(aMarker) {
   this._marker = aMarker;
 }
 
@@ -86,17 +88,17 @@ MarkerDragger.prototype = {
     return true;
   }
 }
 
 function Marker(aParent, aTag, aElementId, xPos, yPos) {
   this._xPos = xPos;
   this._yPos = yPos;
   this._selectionHelperUI = aParent;
-  this._element = document.getElementById(aElementId);
+  this._element = aParent.overlay.getMarker(aElementId);
   this._elementId = aElementId;
   // These get picked in input.js and receives drag input
   this._element.customDragger = new MarkerDragger(this);
   this.tag = aTag;
 }
 
 Marker.prototype = {
   _element: null,
@@ -277,17 +279,24 @@ var SelectionHelperUI = {
   get caretMark() {
     if (this._caretMark == null) {
       this._caretMark = new Marker(this, "caret", this._selectionMarkIds.pop(), 0, 0);
     }
     return this._caretMark;
   },
 
   get overlay() {
-    return document.getElementById("selection-overlay");
+    return document.getElementById(this.layerMode == kChromeLayer ?
+      "chrome-selection-overlay" : "content-selection-overlay");
+  },
+
+  get layerMode() {
+    if (this._msgTarget && this._msgTarget instanceof SelectionPrototype)
+      return kChromeLayer;
+    return kContentLayer;
   },
 
   /*
    * isActive (prop)
    *
    * Determines if a selection edit session is currently active.
    */
   get isActive() {
@@ -370,46 +379,46 @@ var SelectionHelperUI = {
   },
 
   /*
    * openEditSession
    * 
    * Attempts to select underlying text at a point and begins editing
    * the section.
    *
-   * @param aContent - Browser object
+   * @param aMsgTarget - Browser or chrome message target
    * @param aX, aY - Browser relative client coordinates.
    */
-  openEditSession: function openEditSession(aBrowser, aX, aY) {
-    if (!aBrowser || this.isActive)
+  openEditSession: function openEditSession(aMsgTarget, aX, aY) {
+    if (!aMsgTarget || this.isActive)
       return;
-    this._init(aBrowser);
+    this._init(aMsgTarget);
     this._setupDebugOptions();
 
     // Send this over to SelectionHandler in content, they'll message us
     // back with information on the current selection. SelectionStart
     // takes client coordinates.
     this._sendAsyncMessage("Browser:SelectionStart", {
       xPos: aX,
       yPos: aY
     });
   },
 
   /*
    * attachEditSession
    * 
    * Attaches to existing selection and begins editing.
    *
-   * @param aBrowser - Browser object
+   * @param aMsgTarget - Browser or chrome message target
    * @param aX, aY - Browser relative client coordinates.
    */
-  attachEditSession: function attachEditSession(aBrowser, aX, aY) {
-    if (!aBrowser || this.isActive)
+  attachEditSession: function attachEditSession(aMsgTarget, aX, aY) {
+    if (!aMsgTarget || this.isActive)
       return;
-    this._init(aBrowser);
+    this._init(aMsgTarget);
     this._setupDebugOptions();
 
     // Send this over to SelectionHandler in content, they'll message us
     // back with information on the current selection. SelectionAttach
     // takes client coordinates.
     this._sendAsyncMessage("Browser:SelectionAttach", {
       xPos: aX,
       yPos: aY
@@ -422,23 +431,23 @@ var SelectionHelperUI = {
    * Initiates a touch caret selection session for a text input.
    * Can be called multiple times to move the caret marker around.
    *
    * Note the caret marker is pretty limited in functionality. The
    * only thing is can do is be displayed at the caret position.
    * Once the user starts a drag, the caret marker is hidden, and
    * the start and end markers take over.
    *
-   * @param aBrowser - Browser object
+   * @param aMsgTarget - Browser or chrome message target
    * @param aX, aY - Browser relative client coordinates of the tap
    * that initiated the session.
    */
-  attachToCaret: function attachToCaret(aBrowser, aX, aY) {
+  attachToCaret: function attachToCaret(aMsgTarget, aX, aY) {
     if (!this.isActive) {
-      this._init(aBrowser);
+      this._init(aMsgTarget);
       this._setupDebugOptions();
     } else {
       this._hideMonocles();
     }
 
     this._lastPoint = { xPos: aX, yPos: aY };
 
     this._sendAsyncMessage("Browser:CaretAttach", {
@@ -502,52 +511,52 @@ var SelectionHelperUI = {
     messageManager.addMessageListener("Content:HandlerShutdown", this);
     messageManager.addMessageListener("Content:SelectionHandlerPong", this);
 
     window.addEventListener("keypress", this, true);
     window.addEventListener("click", this, false);
     window.addEventListener("touchstart", this, true);
     window.addEventListener("touchend", this, true);
     window.addEventListener("touchmove", this, true);
-    window.addEventListener("MozContextUIShow", this, true);
-    window.addEventListener("MozContextUIDismiss", this, true);
     window.addEventListener("MozPrecisePointer", this, true);
     window.addEventListener("MozDeckOffsetChanging", this, true);
     window.addEventListener("MozDeckOffsetChanged", this, true);
 
     Elements.browsers.addEventListener("URLChanged", this, true);
     Elements.browsers.addEventListener("SizeChanged", this, true);
     Elements.browsers.addEventListener("ZoomChanged", this, true);
 
+    Elements.navbar.addEventListener("transitionend", this, true);
+
     this.overlay.enabled = true;
   },
 
   _shutdown: function _shutdown() {
     messageManager.removeMessageListener("Content:SelectionRange", this);
     messageManager.removeMessageListener("Content:SelectionCopied", this);
     messageManager.removeMessageListener("Content:SelectionFail", this);
     messageManager.removeMessageListener("Content:SelectionDebugRect", this);
     messageManager.removeMessageListener("Content:HandlerShutdown", this);
     messageManager.removeMessageListener("Content:SelectionHandlerPong", this);
 
     window.removeEventListener("keypress", this, true);
     window.removeEventListener("click", this, false);
     window.removeEventListener("touchstart", this, true);
     window.removeEventListener("touchend", this, true);
     window.removeEventListener("touchmove", this, true);
-    window.removeEventListener("MozContextUIShow", this, true);
-    window.removeEventListener("MozContextUIDismiss", this, true);
     window.removeEventListener("MozPrecisePointer", this, true);
     window.removeEventListener("MozDeckOffsetChanging", this, true);
     window.removeEventListener("MozDeckOffsetChanged", this, true);
 
     Elements.browsers.removeEventListener("URLChanged", this, true);
     Elements.browsers.removeEventListener("SizeChanged", this, true);
     Elements.browsers.removeEventListener("ZoomChanged", this, true);
 
+    Elements.navbar.removeEventListener("transitionend", this, true);
+
     this._shutdownAllMarkers();
 
     this._selectionMarkIds = [];
     this._msgTarget = null;
     this._activeSelectionRect = null;
 
     this.overlay.displayDebugLayer = false;
     this.overlay.enabled = false;
@@ -617,19 +626,16 @@ var SelectionHelperUI = {
    * _transitionFromSelectionToCaret
    *
    * Transitions from text selection mode to caret mode.
    *
    * @param aClientX, aClientY - client coordinates of the
    * tap that initiates the change.
    */
   _transitionFromSelectionToCaret: function _transitionFromSelectionToCaret(aClientX, aClientY) {
-    // clear existing selection and shutdown SelectionHandler
-    this.closeEditSession(true);
-
     // Reset some of our state
     this._activeSelectionRect = null;
 
     // Reset the monocles
     this._shutdownAllMarkers();
     this._setupMonocleIdArray();
 
     // Translate to browser relative client coordinates
@@ -686,17 +692,21 @@ var SelectionHelperUI = {
    * Helper for sending a message to SelectionHandler.
    */
   _sendAsyncMessage: function _sendAsyncMessage(aMsg, aJson) {
     if (!this._msgTarget) {
       if (this._debugEvents)
         Util.dumpLn("SelectionHelperUI sendAsyncMessage could not send", aMsg);
       return;
     }
-    this._msgTarget.messageManager.sendAsyncMessage(aMsg, aJson);
+    if (this._msgTarget && this._msgTarget instanceof SelectionPrototype) {
+      this._msgTarget.msgHandler(aMsg, aJson);
+    } else {
+      this._msgTarget.messageManager.sendAsyncMessage(aMsg, aJson);
+    }
   },
 
   _checkForActiveDrag: function _checkForActiveDrag() {
     return (this.startMark.dragging || this.endMark.dragging ||
             this.caretMark.dragging);
   },
 
   _hitTestSelection: function _hitTestSelection(aEvent) {
@@ -709,18 +719,17 @@ var SelectionHelperUI = {
   },
 
   /*
    * _setCaretPositionAtPoint - sets the current caret position.
    *
    * @param aX, aY - browser relative client coordinates
    */
   _setCaretPositionAtPoint: function _setCaretPositionAtPoint(aX, aY) {
-    let json = this._getMarkerBaseMessage();
-    json.change = "caret";
+    let json = this._getMarkerBaseMessage("caret");
     json.caret.xPos = aX;
     json.caret.yPos = aY;
     this._sendAsyncMessage("Browser:CaretUpdate", json);
   },
 
   /*
    * _shutdownAllMarkers
    *
@@ -738,17 +747,17 @@ var SelectionHelperUI = {
     this._startMark = null;
     this._endMark = null;
     this._caretMark = null;
   },
 
   /*
    * _setupMonocleIdArray
    *
-   * Helper for initing the array of monocle ids.
+   * Helper for initing the array of monocle anon ids.
    */
   _setupMonocleIdArray: function _setupMonocleIdArray() {
     this._selectionMarkIds = ["selectionhandle-mark1",
                               "selectionhandle-mark2",
                               "selectionhandle-mark3"];
   },
 
   _hideMonocles: function _hideMonocles() {
@@ -758,16 +767,25 @@ var SelectionHelperUI = {
     if (this._endMark) {
       this.endMark.hide();
     }
     if (this._caretMark) {
       this.caretMark.hide();
     }
   },
 
+  _showMonocles: function _showMonocles(aSelection) {
+    if (!aSelection) {
+      this.caretMark.show();
+    } else {
+      this.endMark.show();
+      this.startMark.show();
+    }
+  },
+
   /*
    * Event handlers for document events
    */
 
   /*
    * _onTap
    *
    * Handles taps that move the current caret around in text edits,
@@ -809,55 +827,45 @@ var SelectionHelperUI = {
       // shutdown but leave focus alone. the event will fall through
       // and the dom will update focus for us. If the user tapped on
       // another input, we'll get a attachToCaret call soonish on the
       // new input.
       this.closeEditSession(false);
       return;
     }
 
-    let selectionTap = this._hitTestSelection(aEvent);
-
-    // If the tap is in the selection, just ignore it. We disallow this
-    // since we always get a single tap before a double, and double tap
-    // copies selected text.
-    if (selectionTap) {
-      aEvent.stopPropagation();
-      aEvent.preventDefault();
+    if (this._hitTestSelection(aEvent) && this._targetIsEditable) {
+      // Attach to the newly placed caret position
+      this._sendAsyncMessage("Browser:CaretAttach", {
+        xPos: aEvent.clientX,
+        yPos: aEvent.clientY
+      });
       return;
     }
 
     // A tap within an editable but outside active selection, clear the
     // selection and flip back to caret mode.
     if (this.startMark.visible && pointInTargetElement &&
         this._targetIsEditable) {
       this._transitionFromSelectionToCaret(clientCoords.x, clientCoords.y);
+      return;
     }
 
-    // If we have active selection in anything else don't let the event get
-    // to content. Prevents random taps from killing active selection.
-    aEvent.stopPropagation();
-    aEvent.preventDefault();
+    // Close when we get a single tap in content.
+    this.closeEditSession(false);
   },
 
   _onKeypress: function _onKeypress() {
     this.closeEditSession();
   },
 
   _onResize: function _onResize() {
     this._sendAsyncMessage("Browser:SelectionUpdate", {});
   },
 
-  _onContextUIVisibilityEvent: function _onContextUIVisibilityEvent(aType) {
-    // Manage display of monocles when the context ui is displayed.
-    if (!this.isActive)
-      return;
-    this.overlay.hidden = (aType == "MozContextUIShow");
-  },
-
   /*
    * _onDeckOffsetChanging - fired by ContentAreaObserver before the browser
    * deck is shifted for form input access in response to a soft keyboard
    * display.
    */
   _onDeckOffsetChanging: function _onDeckOffsetChanging(aEvent) {
     // Hide the monocles temporarily
     this._hideMonocles();
@@ -869,16 +877,34 @@ var SelectionHelperUI = {
    * display.
    */
   _onDeckOffsetChanged: function _onDeckOffsetChanged(aEvent) {
     // Update the monocle position and display
     this.attachToCaret(null, this._lastPoint.xPos, this._lastPoint.yPos);
   },
 
   /*
+   * Detects when the nav bar hides or shows, so we can enable
+   * selection at the appropriate location once the transition is
+   * complete, or shutdown selection down when the nav bar is hidden.
+   */
+  _onNavBarTransitionEvent: function _onNavBarTransitionEvent(aEvent) {
+    if (this.layerMode == kContentLayer) {
+      return;
+    }
+    if (aEvent.propertyName == "bottom" && !Elements.navbar.isShowing) {
+      this.closeEditSession(false);
+      return;
+    }
+    if (aEvent.propertyName == "bottom" && Elements.navbar.isShowing) {
+      this._sendAsyncMessage("Browser:SelectionUpdate", {});
+    }
+  },
+
+  /*
    * Event handlers for message manager
    */
 
   _onDebugRectRequest: function _onDebugRectRequest(aMsg) {
     this.overlay.addDebugRect(aMsg.left, aMsg.top, aMsg.right, aMsg.bottom,
                               aMsg.color, aMsg.fill, aMsg.id);
   },
 
@@ -892,27 +918,25 @@ var SelectionHelperUI = {
 
   _onSelectionCopied: function _onSelectionCopied(json) {
     this.closeEditSession(true);
   },
 
   _onSelectionRangeChange: function _onSelectionRangeChange(json) {
     let haveSelectionRect = true;
 
-    // start and end contain client coordinates.
     if (json.updateStart) {
       this.startMark.position(this._msgTarget.btocx(json.start.xPos, true),
                               this._msgTarget.btocy(json.start.yPos, true));
-      this.startMark.show();
     }
     if (json.updateEnd) {
       this.endMark.position(this._msgTarget.btocx(json.end.xPos, true),
                             this._msgTarget.btocy(json.end.yPos, true));
-      this.endMark.show();
     }
+
     if (json.updateCaret) {
       // If selectionRangeFound is set SelectionHelper found a range we can
       // attach to. If not, there's no text in the control, and hence no caret
       // position information we can use.
       haveSelectionRect = json.selectionRangeFound;
       if (json.selectionRangeFound) {
         this.caretMark.position(this._msgTarget.btocx(json.caret.xPos, true),
                                 this._msgTarget.btocy(json.caret.yPos, true));
@@ -921,16 +945,22 @@ var SelectionHelperUI = {
     }
 
     this._targetIsEditable = json.targetIsEditable;
     this._activeSelectionRect = haveSelectionRect ?
       this._msgTarget.rectBrowserToClient(json.selection, true) :
       this._activeSelectionRect = Util.getCleanRect();
     this._targetElementRect =
       this._msgTarget.rectBrowserToClient(json.element, true);
+
+    // Ifd this is the end of a selection move show the appropriate
+    // monocle images. src=(start, update, end, caret)
+    if (json.src == "start" || json.src == "end") {
+      this._showMonocles(true);
+    }
   },
 
   _onSelectionFail: function _onSelectionFail() {
     Util.dumpLn("failed to get a selection.");
     this.closeEditSession();
   },
 
   /*
@@ -1003,28 +1033,27 @@ var SelectionHelperUI = {
         this._shutdown();
         break;
 
       case "ZoomChanged":
       case "MozPrecisePointer":
         this.closeEditSession(true);
         break;
 
-      case "MozContextUIShow":
-      case "MozContextUIDismiss":
-        this._onContextUIVisibilityEvent(aEvent.type);
-        break;
-
       case "MozDeckOffsetChanging":
         this._onDeckOffsetChanging(aEvent);
         break;
 
       case "MozDeckOffsetChanged":
         this._onDeckOffsetChanged(aEvent);
         break;
+
+      case "transitionend":
+        this._onNavBarTransitionEvent(aEvent);
+        break;
     }
   },
 
   receiveMessage: function sh_receiveMessage(aMessage) {
     if (this._debugEvents) Util.dumpLn("SelectionHelperUI:", aMessage.name);
     let json = aMessage.json;
     switch (aMessage.name) {
       case "Content:SelectionFail":
@@ -1047,46 +1076,45 @@ var SelectionHelperUI = {
         break;
     }
   },
 
   /*
    * Callbacks from markers
    */
 
-  _getMarkerBaseMessage: function _getMarkerBaseMessage() {
+  _getMarkerBaseMessage: function _getMarkerBaseMessage(aMarkerTag) {
     return {
+      change: aMarkerTag,
       start: {
         xPos: this._msgTarget.ctobx(this.startMark.xPos, true),
         yPos: this._msgTarget.ctoby(this.startMark.yPos, true)
       },
       end: {
         xPos: this._msgTarget.ctobx(this.endMark.xPos, true),
         yPos: this._msgTarget.ctoby(this.endMark.yPos, true)
       },
       caret: {
         xPos: this._msgTarget.ctobx(this.caretMark.xPos, true),
         yPos: this._msgTarget.ctoby(this.caretMark.yPos, true)
       },
     };
   },
 
   markerDragStart: function markerDragStart(aMarker) {
-    let json = this._getMarkerBaseMessage();
-    json.change = aMarker.tag;
+    let json = this._getMarkerBaseMessage(aMarker.tag);
     if (aMarker.tag == "caret") {
       this._sendAsyncMessage("Browser:CaretMove", json);
       return;
     }
     this._sendAsyncMessage("Browser:SelectionMoveStart", json);
   },
 
   markerDragStop: function markerDragStop(aMarker) {
-    let json = this._getMarkerBaseMessage();
-    json.change = aMarker.tag;
+    let json = this._getMarkerBaseMessage(aMarker.tag);
     if (aMarker.tag == "caret") {
       this._sendAsyncMessage("Browser:CaretUpdate", json);
       return;
     }
     this._sendAsyncMessage("Browser:SelectionMoveEnd", json);
   },
 
   markerDragMove: function markerDragMove(aMarker, aDirection) {
@@ -1097,14 +1125,17 @@ var SelectionHelperUI = {
         // We are going to transition from caret browsing mode to selection
         // mode on drag. So swap the caret monocle for a start or end monocle
         // depending on the direction of the drag, and start selecting text.
         this._transitionFromCaretToSelection(aDirection);
         return false;
       }
       return true;
     }
-    let json = this._getMarkerBaseMessage();
-    json.change = aMarker.tag;
+
+    // We'll re-display these after the drag is complete.
+    this._hideMonocles();
+
+    let json = this._getMarkerBaseMessage(aMarker.tag);
     this._sendAsyncMessage("Browser:SelectionMove", json);
     return true;
   },
 };
new file mode 100644
--- /dev/null
+++ b/browser/metro/base/content/library/SelectionPrototype.js
@@ -0,0 +1,940 @@
+/* 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/. */
+
+/*
+ * SelectionPrototype - common base class used by both chrome and content selection logic.
+ */
+
+// selection node parameters for various apis
+const kSelectionNodeAnchor = 1;
+const kSelectionNodeFocus = 2;
+
+// selection type property constants
+const kChromeSelector = 1;
+const kContentSelector = 2;
+
+dump("### SelectionPrototype.js loaded\n");
+
+/*
+  http://mxr.mozilla.org/mozilla-central/source/docshell/base/nsIDocShell.idl
+  http://mxr.mozilla.org/mozilla-central/source/content/base/public/nsISelectionDisplay.idl
+  http://mxr.mozilla.org/mozilla-central/source/content/base/public/nsISelectionListener.idl
+  http://mxr.mozilla.org/mozilla-central/source/content/base/public/nsISelectionPrivate.idl
+  http://mxr.mozilla.org/mozilla-central/source/content/base/public/nsISelectionController.idl
+  http://mxr.mozilla.org/mozilla-central/source/content/base/public/nsISelection.idl
+  http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/core/nsIDOMDocument.idl#372
+    rangeCount
+    getRangeAt
+    containsNode
+  http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html
+  http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/range/nsIDOMRange.idl
+  http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/core/nsIDOMDocument.idl#80
+    content.document.createRange()
+    getBoundingClientRect
+    isPointInRange
+  http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/core/nsIDOMNode.idl
+  http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/base/nsIDOMWindowUtils.idl
+    setSelectionAtPoint
+  http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/core/nsIDOMElement.idl
+    getClientRect
+  http://mxr.mozilla.org/mozilla-central/source/layout/generic/nsFrameSelection.h
+  http://mxr.mozilla.org/mozilla-central/source/editor/idl/nsIEditor.idl
+  http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/base/nsIFocusManager.idl
+  http://mxr.mozilla.org/mozilla-central/source/toolkit/content/widgets/textbox.xml
+  http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/xul/nsIDOMXULTextboxElement.idl
+    mEditor
+*/
+
+var SelectionPrototype = function() { }
+
+SelectionPrototype.prototype = {
+  _debugEvents: false,
+  _cache: {},
+  _targetElement: null,
+  _targetIsEditable: true,
+  _contentOffset: { x: 0, y: 0 },
+  _contentWindow: null,
+  _debugOptions: { dumpRanges: false, displayRanges: false },
+  _domWinUtils: null,
+  _selectionMoveActive: false,
+  _snap: false,
+  _type: 0,
+
+  /*************************************************
+   * Properties
+   */
+
+  get isActive() {
+    return !!this._targetElement;
+  },
+
+  get targetIsEditable() {
+    return this._targetIsEditable || false;
+  },
+
+  /*
+   * snap - enable or disable word snap for the active marker when a
+   * SelectionMoveEnd event is received. Typically you would disable
+   * snap when zoom is < 1.0 for precision selection. defaults to off.
+   */
+  get snap() {
+    return this._snap;
+  },
+
+  set snap(aValue) {
+    this._snap = aValue;
+  },
+
+  /*
+   * type - returns a constant indicating which module we're
+   * loaded into - chrome or content.
+   */
+  set type(aValue) {
+    this._type = aValue;
+  },
+
+  get type() {
+    return this._type;
+  },
+
+  /*************************************************
+   * Messaging wrapper
+   */
+
+  /*
+   * sendAsync
+   *
+   * Wrapper over sendAsyncMessage in content, waps direct invocation on
+   * SelectionHelperUI in chrome. Should be overriden by objects that inherit
+   * our behavior. 
+   */
+  sendAsync: function sendAsync(aMsg, aJson) {
+    Util.dumpLn("Base sendAsync called on SelectionPrototype. This is a no-op.");
+  },
+
+  /*************************************************
+   * Common browser event handlers
+   */
+
+   /*
+    * _onCaretPositionUpdate - sets the current caret location based on
+    * a client coordinates. Messages back with updated monocle position
+    * information.
+    *
+    * @param aX, aY drag location in client coordinates.
+    */
+  _onCaretPositionUpdate: function _onCaretPositionUpdate(aX, aY) {
+    this._onCaretMove(aX, aY);
+
+    // Update the position of our selection monocles
+    this._updateSelectionUI("caret", false, false, true);
+  },
+
+   /*
+    * _onCaretMove - updates the current caret location based on a client
+    * coordinates.
+    *
+    * @param aX, aY drag location in client coordinates.
+    */
+  _onCaretMove: function _onCaretMove(aX, aY) {
+    if (!this._targetIsEditable) {
+      this._onFail("Unexpected, caret position isn't supported with non-inputs.");
+      return;
+    }
+
+    // SelectionHelperUI sends text input tap coordinates and a caret move
+    // event at the start of a monocle drag. caretPositionFromPoint isn't
+    // going to give us correct info if the coord is outside the edit bounds,
+    // so restrict the coordinates before we call cpfp.
+    let containedCoords = this._restrictCoordinateToEditBounds(aX, aY);
+    let cp = this._contentWindow.document.caretPositionFromPoint(containedCoords.xPos,
+                                                                 containedCoords.yPos);
+    let input = cp.offsetNode;
+    let offset = cp.offset;
+    input.selectionStart = input.selectionEnd = offset;
+  },
+
+  /*
+   * Turning on or off various debug features.
+   */
+  _onSelectionDebug: function _onSelectionDebug(aMsg) {
+    this._debugOptions = aMsg;
+    this._debugEvents = aMsg.dumpEvents;
+  },
+
+  /*************************************************
+   * Selection api
+   */
+
+  /*
+   * _updateSelectionUI
+   *
+   * Informs SelectionHelperUI about selection marker position
+   * so that our selection monocles can be positioned properly.
+   *
+   * @param aSrcMsg string the message type that triggered this update.
+   * @param aUpdateStart bool update start marker position
+   * @param aUpdateEnd bool update end marker position
+   * @param aUpdateCaret bool update caret marker position, can be
+   * undefined, defaults to false.
+   */
+  _updateSelectionUI: function _updateSelectionUI(aSrcMsg, aUpdateStart, aUpdateEnd,
+                                                  aUpdateCaret) {
+    let selection = this._getSelection();
+
+    // If the range didn't have any text, let's bail
+    if (!selection) {
+      this._onFail("no selection was present");
+      return;
+    }
+
+    // Updates this._cache content selection position data which we send over
+    // to SelectionHelperUI. Note updateUIMarkerRects will fail if there isn't
+    // any selection in the page. This can happen when we start a monocle drag
+    // but haven't dragged enough to create selection. Just return.
+    try {
+      this._updateUIMarkerRects(selection);
+    } catch (ex) {
+      Util.dumpLn("_updateUIMarkerRects:", ex.message);
+      return;
+    }
+
+    this._cache.src = aSrcMsg;
+    this._cache.updateStart = aUpdateStart;
+    this._cache.updateEnd = aUpdateEnd;
+    this._cache.updateCaret = aUpdateCaret || false;
+    this._cache.targetIsEditable = this._targetIsEditable;
+
+    // Get monocles positioned correctly
+    this.sendAsync("Content:SelectionRange", this._cache);
+  },
+
+  /*
+   * _handleSelectionPoint(aMarker, aPoint, aEndOfSelection) 
+   *
+   * After a monocle moves to a new point in the document, determines
+   * what the target is and acts on its selection accordingly. If the
+   * monocle is within the bounds of the target, adds or subtracts selection
+   * at the monocle coordinates appropriately and then merges selection ranges
+   * into a single continuous selection. If the monocle is outside the bounds
+   * of the target and the underlying target is editable, uses the selection
+   * controller to advance selection and visibility within the control.
+   */
+  _handleSelectionPoint: function _handleSelectionPoint(aMarker, aClientPoint,
+                                                        aEndOfSelection) {
+    let selection = this._getSelection();
+
+    let clientPoint = { xPos: aClientPoint.xPos, yPos: aClientPoint.yPos };
+
+    if (selection.rangeCount == 0) {
+      this._onFail("selection.rangeCount == 0");
+      return;
+    }
+
+    // We expect continuous selection ranges.
+    if (selection.rangeCount > 1) {
+      this._setContinuousSelection();
+    }
+
+    // Adjust our y position up such that we are sending coordinates on
+    // the text line vs. below it where the monocle is positioned.
+    let halfLineHeight = this._queryHalfLineHeight(aMarker, selection);
+    clientPoint.yPos -= halfLineHeight;
+
+    // Modify selection based on monocle movement
+    if (this._targetIsEditable) {
+      this._adjustEditableSelection(aMarker, clientPoint, aEndOfSelection);
+    } else {
+      this._adjustSelectionAtPoint(aMarker, clientPoint, aEndOfSelection);
+    }
+  },
+
+  /*
+   * _handleSelectionPoint helper methods
+   */
+
+  /*
+   * _adjustEditableSelection
+   *
+   * Based on a monocle marker and position, adds or subtracts from the
+   * existing selection in editable controls. Handles auto-scroll as well.
+   *
+   * @param the marker currently being manipulated
+   * @param aAdjustedClientPoint client point adjusted for line height.
+   * @param aEndOfSelection indicates if this is the end of a selection
+   * move, in which case we may want to snap to the end of a word or
+   * sentence.
+   */
+  _adjustEditableSelection: function _adjustEditableSelection(aMarker,
+                                                              aAdjustedClientPoint,
+                                                              aEndOfSelection) {
+    // Test to see if we need to handle auto-scroll in cases where the
+    // monocle is outside the bounds of the control. This also handles
+    // adjusting selection if out-of-bounds is true.
+    let result = this.updateTextEditSelection(aAdjustedClientPoint);
+
+    // If result.trigger is true, the monocle is outside the bounds of the
+    // control.
+    if (result.trigger) {
+      // _handleSelectionPoint is triggered by input movement, so if we've
+      // tested positive for out-of-bounds scrolling here, we need to set a
+      // recurring timer to keep the expected selection behavior going as
+      // long as the user keeps the monocle out of bounds.
+      this._setTextEditUpdateInterval(result.speed);
+
+      // Smooth the selection
+      this._setContinuousSelection();
+
+      // Update the other monocle's position if we've dragged off to one side
+      this._updateSelectionUI("update", result.start, result.end);
+    } else {
+      // If we aren't out-of-bounds, clear the scroll timer if it exists.
+      this._clearTimers();
+
+      // Restrict the client point to the interior of the control.
+      let constrainedPoint =
+        this._constrainPointWithinControl(aAdjustedClientPoint);
+
+      // For textareas we fall back on the selectAtPoint logic due to various
+      // issues with caretPositionFromPoint (bug 882149).
+      if (Util.isMultilineInput(this._targetElement)) {
+        this._adjustSelectionAtPoint(aMarker, constrainedPoint, aEndOfSelection);
+        return;
+      }
+
+      //  Add or subtract selection
+      let cp =
+        this._contentWindow.document.caretPositionFromPoint(constrainedPoint.xPos,
+                                                            constrainedPoint.yPos);
+      if (!cp || (cp.offsetNode != this._targetElement &&
+          this._contentWindow.document.getBindingParent(cp.offsetNode) != this._targetElement)) {
+        return;
+      }
+      if (aMarker == "start") {
+        this._targetElement.selectionStart = cp.offset;
+      } else {
+        this._targetElement.selectionEnd = cp.offset;
+      }
+    }
+  },
+
+  /*
+   * _adjustSelectionAtPoint
+   *
+   * Based on a monocle marker and position, adds or subtracts from the
+   * existing selection at a point.
+   *
+   * Note: we are trying to move away from this api due to the overhead. 
+   *
+   * @param the marker currently being manipulated
+   * @param aClientPoint the point designating the new start or end
+   * position for the selection.
+   * @param aEndOfSelection indicates if this is the end of a selection
+   * move, in which case we may want to snap to the end of a word or
+   * sentence.
+   */
+  _adjustSelectionAtPoint: function _adjustSelectionAtPoint(aMarker, aClientPoint,
+                                                            aEndOfSelection) {
+    // Make a copy of the existing range, we may need to reset it.
+    this._backupRangeList();
+
+    // shrinkSelectionFromPoint takes sub-frame relative coordinates.
+    let framePoint = this._clientPointToFramePoint(aClientPoint);
+
+    // Tests to see if the user is trying to shrink the selection, and if so
+    // collapses it down to the appropriate side such that our calls below
+    // will reset the selection to the proper range.
+    let shrunk = this._shrinkSelectionFromPoint(aMarker, framePoint);
+