Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Mon, 04 May 2015 15:53:32 -0400
changeset 273593 642aa49f22cff497d834e0a4066d1473497ecfa4
parent 273572 5712dd72bd3ad5e63c0ec4c7beff6056ac55336e (current diff)
parent 273592 b4ddf5fe16f69c5ebaf63680aa8ab1549d01b169 (diff)
child 273628 102d0e9aa9e1629e2b448b45ac5224c2aef87ec7
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone40.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 fx-team to m-c. a=merge
--- a/browser/base/content/pageinfo/permissions.js
+++ b/browser/base/content/pageinfo/permissions.js
@@ -25,17 +25,17 @@ var permissionObserver = {
           setPluginsRadioState();
       }
     }
   }
 };
 
 function onLoadPermission()
 {
-  var uri = gDocument.documentURIObject;
+  var uri = BrowserUtils.makeURIFromCPOW(gDocument.documentURIObject);
   var permTab = document.getElementById("permTab");
   if (SitePermissions.isSupportedURI(uri)) {
     gPermURI = uri;
     var hostText = document.getElementById("hostText");
     hostText.value = gPermURI.host;
 
     for (var i of gPermissions)
       initRow(i);
--- a/browser/base/content/pageinfo/security.js
+++ b/browser/base/content/pageinfo/security.js
@@ -1,13 +1,15 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* 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/. */
 
+Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
+
 var security = {
   // Display the server certificate (static)
   viewCert : function () {
     var cert = security._cert;
     viewCertHelper(window, cert);
   },
 
   _getSecurityInfo : function() {
@@ -130,17 +132,17 @@ var security = {
   {
     var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
                        .getService(Components.interfaces.nsIWindowMediator);
     var win = wm.getMostRecentWindow("Browser:Cookies");
     var eTLDService = Components.classes["@mozilla.org/network/effective-tld-service;1"].
                       getService(Components.interfaces.nsIEffectiveTLDService);
 
     var eTLD;
-    var uri = gDocument.documentURIObject;
+    var uri = BrowserUtils.makeURIFromCPOW(gDocument.documentURIObject);
     try {
       eTLD = eTLDService.getBaseDomain(uri);
     }
     catch (e) {
       // getBaseDomain will fail if the host is an IP address or is empty
       eTLD = uri.asciiHost;
     }
 
@@ -227,17 +229,17 @@ function securityOnLoad() {
   }
   else
     viewCert.collapsed = true;
 
   /* Set Privacy & History section text */
   var yesStr = pageInfoBundle.getString("yes");
   var noStr = pageInfoBundle.getString("no");
 
-  var uri = gDocument.documentURIObject;
+  var uri = BrowserUtils.makeURIFromCPOW(gDocument.documentURIObject);
   setText("security-privacy-cookies-value",
           hostHasCookies(uri) ? yesStr : noStr);
   setText("security-privacy-passwords-value",
           realmHasPasswords(uri) ? yesStr : noStr);
   
   var visitCount = previousVisitCount(info.hostName);
   if(visitCount > 1) {
     setText("security-privacy-history-value",
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -169,30 +169,28 @@ skip-if = true # bug 428712
 [browser_bug432599.js]
 [browser_bug435035.js]
 [browser_bug435325.js]
 skip-if = buildapp == 'mulet' || e10s # Bug 1099156 - test directly manipulates content
 [browser_bug441778.js]
 skip-if = buildapp == 'mulet'
 [browser_bug455852.js]
 [browser_bug460146.js]
-skip-if = e10s # Bug 866413 - PageInfo doesn't work in e10s
 [browser_bug462289.js]
 skip-if = toolkit == "cocoa" || e10s # Bug 1102017 - middle-button mousedown on selected tab2 does not activate tab - Didn't expect [object XULElement], but got it
 [browser_bug462673.js]
 [browser_bug477014.js]
 [browser_bug479408.js]
 skip-if = buildapp == 'mulet'
 [browser_bug481560.js]
 [browser_bug484315.js]
 [browser_bug491431.js]
 skip-if = buildapp == 'mulet'
 [browser_bug495058.js]
 [browser_bug517902.js]
-skip-if = e10s # Bug 866413 - PageInfo doesn't work in e10s
 [browser_bug519216.js]
 [browser_bug520538.js]
 [browser_bug521216.js]
 [browser_bug533232.js]
 [browser_bug537013.js]
 skip-if = buildapp == 'mulet' || e10s # Bug 1134458 - Find bar doesn't work correctly in a detached tab
 [browser_bug537474.js]
 skip-if = e10s # Bug 1102020 - test tries to use browserDOMWindow.openURI to open a link, and gets a null rv where it expects a window
@@ -330,17 +328,17 @@ skip-if = os != "win" # The Fitts Law me
 skip-if = e10s # Bug 1100664 - test directly access content docShells (TypeError: gBrowser.docShell is null)
 [browser_mixedcontent_securityflags.js]
 [browser_notification_tab_switching.js]
 skip-if = buildapp == 'mulet' || e10s # Bug 1100662 - content access causing uncaught exception - Error: cannot ipc non-cpow object at chrome://mochitests/content/browser/browser/base/content/test/general/browser_notification_tab_switching.js:32 (or in RemoteAddonsChild.jsm)
 [browser_offlineQuotaNotification.js]
 skip-if = buildapp == 'mulet' || e10s # Bug 1093603 - test breaks with PopupNotifications.panel.firstElementChild is null
 [browser_overflowScroll.js]
 [browser_pageInfo.js]
-skip-if = buildapp == 'mulet' || e10s # Bug 866413 - PageInfo doesn't work in e10s
+skip-if = buildapp == 'mulet'
 [browser_page_style_menu.js]
 
 [browser_parsable_css.js]
 skip-if = e10s
 [browser_parsable_script.js]
 skip-if = asan # Disabled because it takes a long time (see test for more information)
 
 [browser_pinnedTabs.js]
--- a/browser/base/content/test/general/browser_bug427559.js
+++ b/browser/base/content/test/general/browser_bug427559.js
@@ -1,49 +1,53 @@
-/* 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";
 
 /*
  * Test bug 427559 to make sure focused elements that are no longer on the page
  * will have focus transferred to the window when changing tabs back to that
  * tab with the now-gone element.
  */
 
-// Default focus on a button and have it kill itself on blur
-let testPage = 'data:text/html,<body><button onblur="this.parentNode.removeChild(this);"><script>document.body.firstChild.focus();</script></body>';
-
-function test() {
-  waitForExplicitFinish();
-
-  gBrowser.selectedTab = gBrowser.addTab();
-  var browser = gBrowser.selectedBrowser;
+// Default focus on a button and have it kill itself on blur.
+const URL = 'data:text/html;charset=utf-8,' +
+            '<body><button onblur="this.remove()">' +
+            '<script>document.body.firstChild.focus()</script></body>';
 
-  browser.addEventListener("load", function () {
-    browser.removeEventListener("load", arguments.callee, true);
-    executeSoon(function () {
-      var testPageWin = content;
+function getFocusedLocalName(browser) {
+  return ContentTask.spawn(browser, null, function* () {
+    return content.document.activeElement.localName;
+  });
+}
 
-      is(browser.contentDocumentAsCPOW.activeElement.localName, "button", "button is focused");
+add_task(function* () {
+  gBrowser.selectedTab = gBrowser.addTab(URL);
+  let browser = gBrowser.selectedBrowser;
+  yield BrowserTestUtils.browserLoaded(browser);
 
-      addEventListener("focus", function focusedWindow(event) {
-        if (!String(event.target.location).startsWith("data:"))
-          return;
+  is((yield getFocusedLocalName(browser)), "button", "button is focused");
 
-        removeEventListener("focus", focusedWindow, true);
-
-        // Make sure focus is given to the window because the element is now gone
-        is(browser.contentDocumentAsCPOW.activeElement.localName, "body", "body is focused");
-
-        gBrowser.removeCurrentTab();
-        finish();
-      }, true);
+  let promiseFocused = ContentTask.spawn(browser, null, function* () {
+    return new Promise(resolve => {
+      content.addEventListener("focus", function onFocus({target}) {
+        if (String(target.location).startsWith("data:")) {
+          content.removeEventListener("focus", onFocus);
+          resolve();
+        }
+      });
+    });
+  });
 
-      // The test page loaded, so open an empty tab, select it, then restore
-      // the test tab. This causes the test page's focused element to be removed
-      // from its document.
-      gBrowser.selectedTab = gBrowser.addTab();
-      gBrowser.removeCurrentTab();
-    });
-  }, true);
+  // The test page loaded, so open an empty tab, select it, then restore
+  // the test tab. This causes the test page's focused element to be removed
+  // from its document.
+  gBrowser.selectedTab = gBrowser.addTab("about:blank");
+  yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+  gBrowser.removeCurrentTab();
 
-  content.location = testPage;
-}
+  // Wait until the original tab is focused again.
+  yield promiseFocused;
+
+  // Make sure focus is given to the window because the element is now gone.
+  is((yield getFocusedLocalName(browser)), "body", "body is focused");
+
+  // Cleanup.
+  gBrowser.removeCurrentTab();
+});
--- a/browser/base/content/test/plugins/browser.ini
+++ b/browser/base/content/test/plugins/browser.ini
@@ -74,17 +74,16 @@ skip-if = e10s # bug 1160166
 [browser_plugin_reloading.js]
 [browser_blocklist_content.js]
 skip-if = !e10s
 [browser_globalplugin_crashinfobar.js]
 skip-if = !crashreporter
 [browser_pluginCrashCommentAndURL.js]
 skip-if = !crashreporter
 [browser_pageInfo_plugins.js]
-skip-if = e10s # Bug 866413
 [browser_pluginplaypreview.js]
 skip-if = e10s # bug 1148827
 [browser_pluginplaypreview2.js]
 skip-if = e10s # bug 1148827
 [browser_pluginplaypreview3.js]
 skip-if = e10s # bug 1148827
 [browser_pluginCrashReportNonDeterminism.js]
 skip-if = !crashreporter || os == 'linux' # Bug 1152811
--- a/browser/components/places/content/editBookmarkOverlay.js
+++ b/browser/components/places/content/editBookmarkOverlay.js
@@ -463,20 +463,24 @@ let gEditItemOverlay = {
                  this._paneInfo.uris : [this._paneInfo.uri];
     let currentTags = this._paneInfo.bulkTagging ?
                         yield this._getCommonTags() :
                         PlacesUtils.tagging.getTagsForURI(uris[0]);
     let anyChanges = yield this._setTagsFromTagsInputField(currentTags, uris);
     if (!anyChanges)
       return false;
 
+    // The panel could have been closed in the meanwhile.
+    if (!this._paneInfo)
+      return false;
+
     // Ensure the tagsField is in sync, clean it up from empty tags
     currentTags = this._paneInfo.bulkTagging ?
                     this._getCommonTags() :
-                    PlacesUtils.tagging.getTagsForURI(this._uri);
+                    PlacesUtils.tagging.getTagsForURI(this._paneInfo.uri);
     this._initTextField(this._tagsField, currentTags.join(", "), false);
     return true;
   }),
 
   /**
    * Stores the first-edit field for this dialog, if the passed-in field
    * is indeed the first edited field
    * @param aNewField
@@ -500,17 +504,17 @@ let gEditItemOverlay = {
 
   onNamePickerChange() {
     if (this.readOnly || !this._paneInfo.isItem)
       return;
 
     // Here we update either the item title or its cached static title
     let newTitle = this._namePicker.value;
     if (!newTitle &&
-        PlacesUtils.bookmarks.getFolderIdForItem(itemId) == PlacesUtils.tagsFolderId) {
+        PlacesUtils.bookmarks.getFolderIdForItem(this._paneInfo.itemId) == PlacesUtils.tagsFolderId) {
       // We don't allow setting an empty title for a tag, restore the old one.
       this._initNamePicker();
     }
     else {
       this._mayUpdateFirstEditField("namePicker");
       if (!PlacesUIUtils.useAsyncTransactions) {
         let txn = new PlacesEditItemTitleTransaction(this._paneInfo.itemId,
                                                      newTitle);
@@ -651,27 +655,27 @@ let gEditItemOverlay = {
    * Get the corresponding menu-item in the folder-menu-list for a bookmarks
    * folder if such an item exists. Otherwise, this creates a menu-item for the
    * folder. If the items-count limit (see MAX_FOLDERS_IN_MENU_LIST) is reached,
    * the new item replaces the last menu-item.
    * @param aFolderId
    *        The identifier of the bookmarks folder.
    */
   _getFolderMenuItem(aFolderId) {
-    let menuPopup = this._folderMenuList.menupopup;
+    let menupopup = this._folderMenuList.menupopup;
     let menuItem = Array.prototype.find.call(
-      menuPopup.childNodes, menuItem => menuItem.folderId === aFolderId);
+      menupopup.childNodes, menuItem => menuItem.folderId === aFolderId);
     if (menuItem !== undefined)
       return menuItem;
 
     // 3 special folders + separator + folder-items-count limit
     if (menupopup.childNodes.length == 4 + MAX_FOLDER_ITEM_IN_MENU_LIST)
-      menupopup.removeChild(menuPopup.lastChild);
+      menupopup.removeChild(menupopup.lastChild);
 
-    return this._appendFolderItemToMenupopup(menuPopup, aFolderId);
+    return this._appendFolderItemToMenupopup(menupopup, aFolderId);
   },
 
   onFolderMenuListCommand(aEvent) {
     // Set a selectedIndex attribute to show special icons
     this._folderMenuList.setAttribute("selectedIndex",
                                       this._folderMenuList.selectedIndex);
 
     if (aEvent.target.id == "editBMPanel_chooseFolderMenuItem") {
@@ -683,17 +687,18 @@ let gEditItemOverlay = {
       // XXXmano HACK: setTimeout 100, otherwise focus goes back to the
       // menulist right away
       setTimeout(function(self) self.toggleFolderTreeVisibility(), 100, this);
       return;
     }
 
     // Move the item
     let containerId = this._getFolderIdFromMenuList();
-    if (PlacesUtils.bookmarks.getFolderIdForItem(this._paneInfo.itemId) != containerId) {
+    if (PlacesUtils.bookmarks.getFolderIdForItem(this._paneInfo.itemId) != containerId &&
+        this._paneInfo.itemId != containerId) {
       if (PlacesUIUtils.useAsyncTransactions) {
         Task.spawn(function* () {
           let newParentGuid = yield PlacesUtils.promiseItemGuid(containerId);
           let guid = this._paneInfo.itemGuid;
           yield PlacesTransactions.Move({ guid, newParentGuid }).transact();
         }.bind(this));
       }
       else {
@@ -703,28 +708,28 @@ let gEditItemOverlay = {
         PlacesUtils.transactionManager.doTransaction(txn);
       }
 
       // Mark the containing folder as recently-used if it isn't in the
       // static list
       if (containerId != PlacesUtils.unfiledBookmarksFolderId &&
           containerId != PlacesUtils.toolbarFolderId &&
           containerId != PlacesUtils.bookmarksMenuFolderId) {
-        this._markFolderAsRecentlyUsed(container)
+        this._markFolderAsRecentlyUsed(containerId)
             .catch(Components.utils.reportError);
       }
     }
 
     // Update folder-tree selection
     var folderTreeRow = this._element("folderTreeRow");
     if (!folderTreeRow.collapsed) {
       var selectedNode = this._folderTree.selectedNode;
       if (!selectedNode ||
-          PlacesUtils.getConcreteItemId(selectedNode) != container)
-        this._folderTree.selectItems([container]);
+          PlacesUtils.getConcreteItemId(selectedNode) != containerId)
+        this._folderTree.selectItems([containerId]);
     }
   },
 
   onFolderTreeSelect() {
     var selectedNode = this._folderTree.selectedNode;
 
     // Disable the "New Folder" button if we cannot create a new folder
     this._element("newFolderButton")
@@ -745,23 +750,25 @@ let gEditItemOverlay = {
   _markFolderAsRecentlyUsed: Task.async(function* (aFolderId) {
     if (!PlacesUIUtils.useAsyncTransactions) {
       let txns = [];
 
       // Expire old unused recent folders.
       let annotation = this._getLastUsedAnnotationObject(false);
       while (this._recentFolders.length > MAX_FOLDER_ITEM_IN_MENU_LIST) {
         let folderId = this._recentFolders.pop().folderId;
-        let annoTxn = new PlacesSetItemAnnotationTransaction(folderId, anno);
+        let annoTxn = new PlacesSetItemAnnotationTransaction(folderId,
+                                                             annotation);
         txns.push(annoTxn);
       }
 
       // Mark folder as recently used
       annotation = this._getLastUsedAnnotationObject(true);
-      let annoTxn = new PlacesSetItemAnnotationTransaction(aFolderId, anno);
+      let annoTxn = new PlacesSetItemAnnotationTransaction(aFolderId,
+                                                           annotation);
       txns.push(annoTxn);
 
       let aggregate =
         new PlacesAggregatedTransaction("Update last used folders", txns);
       PlacesUtils.transactionManager.doTransaction(aggregate);
       return;
     }
 
@@ -978,45 +985,47 @@ let gEditItemOverlay = {
       this._rebuildTagsSelectorList().catch(Components.utils.reportError);
   },
 
   _onItemTitleChange(aItemId, aNewTitle) {
     if (!this._paneInfo.isBookmark)
       return;
     if (aItemId == this._paneInfo.itemId) {
       this._paneInfo.title = aNewTitle;
-      this._initTextField(this._namePicker);
+      this._initTextField(this._namePicker, aNewTitle);
     }
     else if (this._paneInfo.visibleRows.has("folderRow")) {
       // If the title of a folder which is listed within the folders
       // menulist has been changed, we need to update the label of its
       // representing element.
       let menupopup = this._folderMenuList.menupopup;
       for (menuitem of menupopup.childNodes) {
-        if ("folderId" in menuItem && menuItem.folderId == aItemId) {
+        if ("folderId" in menuitem && menuitem.folderId == aItemId) {
           menuitem.label = aNewTitle;
           break;
         }
       }
     }
   },
 
   // nsINavBookmarkObserver
   onItemChanged(aItemId, aProperty, aIsAnnotationProperty, aValue,
                 aLastModified, aItemType) {
-    if (aProperty == "tags" && this._paneInfo.visibleRows.has("tagsRow"))
+    if (aProperty == "tags" && this._paneInfo.visibleRows.has("tagsRow")) {
       this._onTagsChange(aItemId);
-    else if (!this._paneInfo.isItem || this._paneInfo.itemId != aItemId)
+    }
+    else if (aProperty == "title" && this._paneInfo.isItem) {
+      // This also updates titles of folders in the folder menu list.
+      this._onItemTitleChange(aItemId, aValue);
+    }
+    else if (!this._paneInfo.isItem || this._paneInfo.itemId != aItemId) {
       return;
+    }
 
     switch (aProperty) {
-    case "title":
-      if (this._paneInfo.isItem)
-        this._onItemTitleChange(aItemId, aValue);
-      break;
     case "uri":
       let newURI = NetUtil.newURI(aValue);
       if (!newURI.equals(this._paneInfo.uri)) {
         this._paneInfo.uri = newURI;
         if (this._paneInfo.visibleRows.has("locationRow"))
           this._initLocationField();
 
         if (this._paneInfo.visibleRows.has("tagsRow")) {
--- a/browser/components/places/content/places.js
+++ b/browser/components/places/content/places.js
@@ -1220,17 +1220,16 @@ var ViewMenu = {
     //   dir:  Default sort direction to use if none has been specified
     //   anno: The annotation to sort by, if key is "ANNOTATION"
     var colLookupTable = {
       title:        { key: "TITLE",        dir: "ascending"  },
       tags:         { key: "TAGS",         dir: "ascending"  },
       url:          { key: "URI",          dir: "ascending"  },
       date:         { key: "DATE",         dir: "descending" },
       visitCount:   { key: "VISITCOUNT",   dir: "descending" },
-      keyword:      { key: "KEYWORD",      dir: "ascending"  },
       dateAdded:    { key: "DATEADDED",    dir: "descending" },
       lastModified: { key: "LASTMODIFIED", dir: "descending" },
       description:  { key: "ANNOTATION",
                       dir: "ascending",
                       anno: PlacesUIUtils.DESCRIPTION_ANNO }
     };
 
     // Make sure we have a valid column.
--- a/browser/components/places/content/places.xul
+++ b/browser/components/places/content/places.xul
@@ -385,19 +385,16 @@
                       persist="width hidden ordinal sortActive sortDirection"/>
             <splitter class="tree-splitter"/>
             <treecol label="&col.mostrecentvisit.label;" id="placesContentDate" anonid="date" flex="1" hidden="true"
                       persist="width hidden ordinal sortActive sortDirection"/>
             <splitter class="tree-splitter"/>
             <treecol label="&col.visitcount.label;" id="placesContentVisitCount" anonid="visitCount" flex="1" hidden="true"
                       persist="width hidden ordinal sortActive sortDirection"/>
             <splitter class="tree-splitter"/>
-            <treecol label="&col.keyword.label;" id="placesContentKeyword" anonid="keyword" flex="1" hidden="true"
-                      persist="width hidden ordinal sortActive sortDirection"/>
-            <splitter class="tree-splitter"/>
             <treecol label="&col.description.label;" id="placesContentDescription" anonid="description" flex="1" hidden="true"
                       persist="width hidden ordinal sortActive sortDirection"/>
             <splitter class="tree-splitter"/>
             <treecol label="&col.dateadded.label;" id="placesContentDateAdded" anonid="dateAdded" flex="1" hidden="true"
                       persist="width hidden ordinal sortActive sortDirection"/>
             <splitter class="tree-splitter"/>
             <treecol label="&col.lastmodified.label;" id="placesContentLastModified" anonid="lastModified" flex="1" hidden="true"
                       persist="width hidden ordinal sortActive sortDirection"/>
--- a/browser/components/places/content/treeView.js
+++ b/browser/components/places/content/treeView.js
@@ -513,36 +513,33 @@ PlacesTreeView.prototype = {
       timeObj.getMinutes(), timeObj.getSeconds()));
   },
 
   COLUMN_TYPE_UNKNOWN: 0,
   COLUMN_TYPE_TITLE: 1,
   COLUMN_TYPE_URI: 2,
   COLUMN_TYPE_DATE: 3,
   COLUMN_TYPE_VISITCOUNT: 4,
-  COLUMN_TYPE_KEYWORD: 5,
-  COLUMN_TYPE_DESCRIPTION: 6,
-  COLUMN_TYPE_DATEADDED: 7,
-  COLUMN_TYPE_LASTMODIFIED: 8,
-  COLUMN_TYPE_TAGS: 9,
+  COLUMN_TYPE_DESCRIPTION: 5,
+  COLUMN_TYPE_DATEADDED: 6,
+  COLUMN_TYPE_LASTMODIFIED: 7,
+  COLUMN_TYPE_TAGS: 8,
 
   _getColumnType: function PTV__getColumnType(aColumn) {
     let columnType = aColumn.element.getAttribute("anonid") || aColumn.id;
 
     switch (columnType) {
       case "title":
         return this.COLUMN_TYPE_TITLE;
       case "url":
         return this.COLUMN_TYPE_URI;
       case "date":
         return this.COLUMN_TYPE_DATE;
       case "visitCount":
         return this.COLUMN_TYPE_VISITCOUNT;
-      case "keyword":
-        return this.COLUMN_TYPE_KEYWORD;
       case "description":
         return this.COLUMN_TYPE_DESCRIPTION;
       case "dateAdded":
         return this.COLUMN_TYPE_DATEADDED;
       case "lastModified":
         return this.COLUMN_TYPE_LASTMODIFIED;
       case "tags":
         return this.COLUMN_TYPE_TAGS;
@@ -563,20 +560,16 @@ PlacesTreeView.prototype = {
       case Ci.nsINavHistoryQueryOptions.SORT_BY_URI_ASCENDING:
         return [this.COLUMN_TYPE_URI, false];
       case Ci.nsINavHistoryQueryOptions.SORT_BY_URI_DESCENDING:
         return [this.COLUMN_TYPE_URI, true];
       case Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_ASCENDING:
         return [this.COLUMN_TYPE_VISITCOUNT, false];
       case Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING:
         return [this.COLUMN_TYPE_VISITCOUNT, true];
-      case Ci.nsINavHistoryQueryOptions.SORT_BY_KEYWORD_ASCENDING:
-        return [this.COLUMN_TYPE_KEYWORD, false];
-      case Ci.nsINavHistoryQueryOptions.SORT_BY_KEYWORD_DESCENDING:
-        return [this.COLUMN_TYPE_KEYWORD, true];
       case Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_ASCENDING:
         if (this._result.sortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO)
           return [this.COLUMN_TYPE_DESCRIPTION, false];
         break;
       case Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_DESCENDING:
         if (this._result.sortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO)
           return [this.COLUMN_TYPE_DESCRIPTION, true];
       case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING:
@@ -844,19 +837,17 @@ PlacesTreeView.prototype = {
     this._invalidateCellValue(aNode, this.COLUMN_TYPE_DATE);
     this._invalidateCellValue(aNode, this.COLUMN_TYPE_VISITCOUNT);
   },
 
   nodeTagsChanged: function PTV_nodeTagsChanged(aNode) {
     this._invalidateCellValue(aNode, this.COLUMN_TYPE_TAGS);
   },
 
-  nodeKeywordChanged: function PTV_nodeKeywordChanged(aNode, aNewKeyword) {
-    this._invalidateCellValue(aNode, this.COLUMN_TYPE_KEYWORD);
-  },
+  nodeKeywordChanged(aNode, aNewKeyword) {},
 
   nodeAnnotationChanged: function PTV_nodeAnnotationChanged(aNode, aAnno) {
     if (aAnno == PlacesUIUtils.DESCRIPTION_ANNO) {
       this._invalidateCellValue(aNode, this.COLUMN_TYPE_DESCRIPTION);
     }
     else if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
       PlacesUtils.livemarks.getLivemark({ id: aNode.itemId })
         .then(aLivemark => {
@@ -1439,20 +1430,16 @@ PlacesTreeView.prototype = {
           // I expect, and gives me no information I know how to use.
           // Only show this for URI-based items.
           return "";
         }
 
         return this._convertPRTimeToString(nodeTime);
       case this.COLUMN_TYPE_VISITCOUNT:
         return node.accessCount;
-      case this.COLUMN_TYPE_KEYWORD:
-        if (PlacesUtils.nodeIsBookmark(node))
-          return PlacesUtils.bookmarks.getKeywordForBookmark(node.itemId);
-        return "";
       case this.COLUMN_TYPE_DESCRIPTION:
         if (node.itemId != -1) {
           try {
             return PlacesUtils.annotations.
                                getItemAnnotation(node.itemId, PlacesUIUtils.DESCRIPTION_ANNO);
           }
           catch (ex) { /* has no description */ }
         }
@@ -1578,25 +1565,16 @@ PlacesTreeView.prototype = {
         if (oldSort == NHQO.SORT_BY_VISITCOUNT_DESCENDING)
           newSort = NHQO.SORT_BY_VISITCOUNT_ASCENDING;
         else if (allowTriState && oldSort == NHQO.SORT_BY_VISITCOUNT_ASCENDING)
           newSort = NHQO.SORT_BY_NONE;
         else
           newSort = NHQO.SORT_BY_VISITCOUNT_DESCENDING;
 
         break;
-      case this.COLUMN_TYPE_KEYWORD:
-        if (oldSort == NHQO.SORT_BY_KEYWORD_ASCENDING)
-          newSort = NHQO.SORT_BY_KEYWORD_DESCENDING;
-        else if (allowTriState && oldSort == NHQO.SORT_BY_KEYWORD_DESCENDING)
-          newSort = NHQO.SORT_BY_NONE;
-        else
-          newSort = NHQO.SORT_BY_KEYWORD_ASCENDING;
-
-        break;
       case this.COLUMN_TYPE_DESCRIPTION:
         if (oldSort == NHQO.SORT_BY_ANNOTATION_ASCENDING &&
             oldSortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO) {
           newSort = NHQO.SORT_BY_ANNOTATION_DESCENDING;
           newSortingAnnotation = PlacesUIUtils.DESCRIPTION_ANNO;
         }
         else if (allowTriState &&
                  oldSort == NHQO.SORT_BY_ANNOTATION_DESCENDING &&
--- a/browser/components/places/tests/browser/browser_sort_in_library.js
+++ b/browser/components/places/tests/browser/browser_sort_in_library.js
@@ -30,17 +30,16 @@
 // SORT_BY_<key>_<dir>) and sortingAnnotation is checked against anno.  anno
 // may be undefined if key is not "ANNOTATION".
 const SORT_LOOKUP_TABLE = {
   title:        { key: "TITLE",        dir: "ASCENDING"  },
   tags:         { key: "TAGS",         dir: "ASCENDING"  },
   url:          { key: "URI",          dir: "ASCENDING"  },
   date:         { key: "DATE",         dir: "DESCENDING" },
   visitCount:   { key: "VISITCOUNT",   dir: "DESCENDING" },
-  keyword:      { key: "KEYWORD",      dir: "ASCENDING"  },
   dateAdded:    { key: "DATEADDED",    dir: "DESCENDING" },
   lastModified: { key: "LASTMODIFIED", dir: "DESCENDING" },
   description:  { key:  "ANNOTATION",
                   dir:  "ASCENDING",
                   anno: "bookmarkProperties/description" }
 };
 
 // This is the column that's sorted if one is not specified and the tree is
--- a/browser/devtools/framework/gDevTools.jsm
+++ b/browser/devtools/framework/gDevTools.jsm
@@ -567,17 +567,21 @@ let gDevToolsBrowser = {
    * This function is for the benefit of Tools:DevToolbox in
    * browser/base/content/browser-sets.inc and should not be used outside
    * of there
    */
   toggleToolboxCommand: function(gBrowser) {
     let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
     let toolbox = gDevTools.getToolbox(target);
 
-    toolbox ? toolbox.destroy() : gDevTools.showToolbox(target);
+    // If a toolbox exists, using toggle from the Main window :
+    // - should close a docked toolbox
+    // - should focus a windowed toolbox
+    let isDocked = toolbox && toolbox.hostType != devtools.Toolbox.HostType.WINDOW;
+    isDocked ? toolbox.destroy() : gDevTools.showToolbox(target);
   },
 
   toggleBrowserToolboxCommand: function(gBrowser) {
     let target = devtools.TargetFactory.forWindow(gBrowser.ownerDocument.defaultView);
     let toolbox = gDevTools.getToolbox(target);
 
     toolbox ? toolbox.destroy()
      : gDevTools.showToolbox(target, "inspector", Toolbox.HostType.WINDOW);
--- a/browser/devtools/framework/test/browser.ini
+++ b/browser/devtools/framework/test/browser.ini
@@ -42,16 +42,17 @@ skip-if = e10s # Bug 1030318
 [browser_toolbox_select_event.js]
 skip-if = e10s # Bug 1069044 - destroyInspector may hang during shutdown
 [browser_toolbox_sidebar.js]
 [browser_toolbox_sidebar_events.js]
 [browser_toolbox_sidebar_existing_tabs.js]
 [browser_toolbox_sidebar_overflow_menu.js]
 [browser_toolbox_tabsswitch_shortcuts.js]
 [browser_toolbox_textbox_context_menu.js]
+[browser_toolbox_toggle.js]
 [browser_toolbox_tool_ready.js]
 [browser_toolbox_tool_remote_reopen.js]
 [browser_toolbox_transport_events.js]
 [browser_toolbox_view_source_01.js]
 [browser_toolbox_view_source_02.js]
 [browser_toolbox_view_source_03.js]
 [browser_toolbox_view_source_04.js]
 [browser_toolbox_window_reload_target.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_toggle.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+const URL = "data:text/html;charset=utf-8,Test toggling devtools using keyboard shortcuts";
+
+add_task(function*() {
+  // Test with ACCEL+SHIFT+I / ACCEL+ALT+I (MacOSX) ; modifiers should match :
+  // - toolbox-key-toggle in browser/devtools/framework/toolbox-window.xul
+  // - key_devToolboxMenuItem in browser/base/content/browser.xul
+  info('Test toggle using CTRL+SHIFT+I/CMD+ALT+I');
+  yield testToggle('I', {
+    accelKey : true,
+    shiftKey : !navigator.userAgent.match(/Mac/),
+    altKey : navigator.userAgent.match(/Mac/),
+  });
+  // Test with F12 ; no modifiers
+  info('Test toggle using F12');
+  yield testToggle('VK_F12', {});
+});
+
+function* testToggle(key, modifiers) {
+  let tab = yield addTab(URL + " ; key : '" + key + "'");
+  yield gDevTools.showToolbox(TargetFactory.forTab(tab));
+
+  yield testToggleDockedToolbox(tab, key, modifiers);
+
+  yield testToggleDetachedToolbox(tab, key, modifiers);
+
+  yield cleanup();
+}
+
+function* testToggleDockedToolbox (tab, key, modifiers) {
+  let toolbox = getToolboxForTab(tab);
+
+  isnot(toolbox.hostType, devtools.Toolbox.HostType.WINDOW, "Toolbox is docked in the main window");
+
+  info('verify docked toolbox is destroyed when using toggle key');
+  let onToolboxDestroyed = once(gDevTools, "toolbox-destroyed");
+  EventUtils.synthesizeKey(key, modifiers);
+  yield onToolboxDestroyed;
+  ok(true, "Docked toolbox is destroyed when using a toggle key");
+
+  info('verify new toolbox is created when using toggle key');
+  let onToolboxReady = once(gDevTools, "toolbox-ready");
+  EventUtils.synthesizeKey(key, modifiers);
+  yield onToolboxReady;
+  ok(true, "Toolbox is created by using when toggle key");
+}
+
+function* testToggleDetachedToolbox (tab, key, modifiers) {
+  let toolbox = getToolboxForTab(tab);
+
+  info('change the toolbox hostType to WINDOW');
+  yield toolbox.switchHost(devtools.Toolbox.HostType.WINDOW);
+  is(toolbox.hostType, devtools.Toolbox.HostType.WINDOW, "Toolbox opened on separate window");
+
+  let toolboxWindow = toolbox._host._window;
+  info('Wait for focus on the toolbox window')
+  yield new Promise(resolve => waitForFocus(resolve, toolboxWindow));
+
+  info('Focus main window')
+  let onMainWindowFocus = once(window, "focus");
+  window.focus();
+  yield onMainWindowFocus;
+  ok(true, "Main window focused");
+
+  info('verify windowed toolbox is focused when using toggle key from the main window')
+  let onToolboxWindowFocus = once(toolboxWindow, "focus");
+  EventUtils.synthesizeKey(key, modifiers);
+  yield onToolboxWindowFocus;
+  ok(true, "Toolbox focused and not destroyed");
+
+  info('verify windowed toolbox is destroyed when using toggle key from its own window')
+  let onToolboxDestroyed = once(gDevTools, "toolbox-destroyed");
+  EventUtils.synthesizeKey(key, modifiers, toolboxWindow);
+  yield onToolboxDestroyed;
+  ok(true, "Toolbox destroyed");
+}
+
+function getToolboxForTab(tab) {
+  return gDevTools.getToolbox(TargetFactory.forTab(tab));
+}
+
+function* cleanup(toolbox) {
+  Services.prefs.setCharPref("devtools.toolbox.host", devtools.Toolbox.HostType.BOTTOM);
+  gBrowser.removeCurrentTab();
+}
--- a/browser/devtools/framework/toolbox-window.xul
+++ b/browser/devtools/framework/toolbox-window.xul
@@ -21,12 +21,25 @@
     <command id="toolbox-cmd-close" oncommand="window.close();"/>
   </commandset>
 
   <keyset id="toolbox-keyset">
     <key id="toolbox-key-close"
          key="&closeCmd.key;"
          command="toolbox-cmd-close"
          modifiers="accel"/>
+    <key id="toolbox-key-toggle"
+         key="&toggleToolbox.key;"
+         command="toolbox-cmd-close"
+#ifdef XP_MACOSX
+         modifiers="accel,alt"
+#else
+         modifiers="accel,shift"
+#endif
+        />
+    <key id="toolbox-key-toggle-F12"
+         keycode="&toggleToolboxF12.keycode;"
+         keytext="&toggleToolboxF12.keytext;"
+         command="toolbox-cmd-close"/>
   </keyset>
 
   <iframe id="toolbox-iframe" flex="1" forceOwnRefreshDriver=""></iframe>
 </window>
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -769,33 +769,40 @@ InspectorPanel.prototype = {
 
     // Get information about the right-clicked node.
     let popupNode = this.panelDoc.popupNode;
     if (!popupNode || !popupNode.classList.contains("link")) {
       return;
     }
 
     let type = popupNode.dataset.type;
-    // Bug 1158822 will make "resource" type URLs open in devtools, but for now
-    // they're considered like "uri".
-    if (type === "uri" || type === "resource") {
+    if (type === "uri" || type === "cssresource" || type === "jsresource") {
       // First make sure the target can resolve relative URLs.
       this.target.actorHasMethod("inspector", "resolveRelativeURL").then(canResolve => {
         if (!canResolve) {
           return;
         }
 
         linkSeparator.removeAttribute("hidden");
 
         // Links can't be opened in new tabs in the browser toolbox.
-        if (!this.target.chrome) {
+        if (type === "uri" && !this.target.chrome) {
           linkFollow.removeAttribute("hidden");
           linkFollow.setAttribute("label", this.strings.GetStringFromName(
             "inspector.menu.openUrlInNewTab.label"));
+        } else if (type === "cssresource") {
+          linkFollow.removeAttribute("hidden");
+          linkFollow.setAttribute("label", this.toolboxStrings.GetStringFromName(
+            "toolbox.viewCssSourceInStyleEditor.label"));
+        } else if (type === "jsresource") {
+          linkFollow.removeAttribute("hidden");
+          linkFollow.setAttribute("label", this.toolboxStrings.GetStringFromName(
+            "toolbox.viewJsSourceInDebugger.label"));
         }
+
         linkCopy.removeAttribute("hidden");
         linkCopy.setAttribute("label", this.strings.GetStringFromName(
           "inspector.menu.copyUrlToClipboard.label"));
       }, console.error);
     } else if (type === "idref") {
       linkSeparator.removeAttribute("hidden");
       linkFollow.removeAttribute("hidden");
       linkFollow.setAttribute("label", this.strings.formatStringFromName(
@@ -1094,36 +1101,41 @@ InspectorPanel.prototype = {
    * This method is here for the benefit of the node-menu-link-follow menu item
    * in the inspector contextual-menu. It's behavior depends on which node was
    * right-clicked when the menu was opened.
    */
   followAttributeLink: function InspectorPanel_followLink(e) {
     let type = this.panelDoc.popupNode.dataset.type;
     let link = this.panelDoc.popupNode.dataset.link;
 
-    // "resource" type links should open appropriate tool instead (bug 1158822).
-    if (type === "uri" || type === "resource") {
+    if (type === "uri" || type === "cssresource" || type === "jsresource") {
       // Open link in a new tab.
       // When the inspector menu was setup on click (see _setupNodeLinkMenu), we
       // already checked that resolveRelativeURL existed.
       this.inspector.resolveRelativeURL(link, this.selection.nodeFront).then(url => {
-        let browserWin = this.target.tab.ownerDocument.defaultView;
-        browserWin.openUILinkIn(url, "tab");
-      }, console.error);
+        if (type === "uri") {
+          let browserWin = this.target.tab.ownerDocument.defaultView;
+          browserWin.openUILinkIn(url, "tab");
+        } else if (type === "cssresource") {
+          return this.toolbox.viewSourceInStyleEditor(url);
+        } else if (type === "jsresource") {
+          return this.toolbox.viewSourceInDebugger(url);
+        }
+      }).catch(e => console.error(e));
     } else if (type == "idref") {
       // Select the node in the same document.
       this.walker.document(this.selection.nodeFront).then(doc => {
-        this.walker.querySelector(doc, "#" + CSS.escape(link)).then(node => {
+        return this.walker.querySelector(doc, "#" + CSS.escape(link)).then(node => {
           if (!node) {
             this.emit("idref-attribute-link-failed");
             return;
           }
           this.selection.setNodeFront(node);
-        }, console.error);
-      }, console.error);
+        });
+      }).catch(e => console.error(e));
     }
   },
 
   /**
    * This method is here for the benefit of the node-menu-link-copy menu item
    * in the inspector contextual-menu. It's behavior depends on which node was
    * right-clicked when the menu was opened.
    */
@@ -1174,23 +1186,26 @@ InspectorPanel.prototype = {
       delete this._timer;
     }
   }
 };
 
 /////////////////////////////////////////////////////////////////////////
 //// Initializers
 
-loader.lazyGetter(InspectorPanel.prototype, "strings",
-  function () {
-    return Services.strings.createBundle(
-            "chrome://browser/locale/devtools/inspector.properties");
-  });
+loader.lazyGetter(InspectorPanel.prototype, "strings", function () {
+  return Services.strings.createBundle(
+    "chrome://browser/locale/devtools/inspector.properties");
+});
+
+loader.lazyGetter(InspectorPanel.prototype, "toolboxStrings", function () {
+  return Services.strings.createBundle(
+    "chrome://browser/locale/devtools/toolbox.properties");
+});
 
 loader.lazyGetter(this, "clipboardHelper", function() {
   return Cc["@mozilla.org/widget/clipboardhelper;1"].
     getService(Ci.nsIClipboardHelper);
 });
 
-
 loader.lazyGetter(this, "DOMUtils", function () {
   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 });
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -113,17 +113,17 @@ browser.jar:
     content/browser/devtools/performance/views/details-memory-flamegraph.js (performance/views/details-memory-flamegraph.js)
     content/browser/devtools/performance/views/recordings.js           (performance/views/recordings.js)
     content/browser/devtools/performance/views/jit-optimizations.js    (performance/views/jit-optimizations.js)
     content/browser/devtools/responsivedesign/resize-commands.js       (responsivedesign/resize-commands.js)
     content/browser/devtools/commandline.css                           (commandline/commandline.css)
     content/browser/devtools/commandlineoutput.xhtml                   (commandline/commandlineoutput.xhtml)
     content/browser/devtools/commandlinetooltip.xhtml                  (commandline/commandlinetooltip.xhtml)
     content/browser/devtools/commandline/commands-index.js             (commandline/commands-index.js)
-    content/browser/devtools/framework/toolbox-window.xul              (framework/toolbox-window.xul)
+*   content/browser/devtools/framework/toolbox-window.xul              (framework/toolbox-window.xul)
     content/browser/devtools/framework/toolbox-options.xul             (framework/toolbox-options.xul)
     content/browser/devtools/framework/toolbox-options.js              (framework/toolbox-options.js)
     content/browser/devtools/framework/toolbox.xul                     (framework/toolbox.xul)
     content/browser/devtools/framework/options-panel.css               (framework/options-panel.css)
     content/browser/devtools/framework/toolbox-process-window.xul      (framework/toolbox-process-window.xul)
 *   content/browser/devtools/framework/toolbox-process-window.js       (framework/toolbox-process-window.js)
     content/browser/devtools/framework/dev-edition-promo.xul           (framework/dev-edition-promo/dev-edition-promo.xul)
 *   content/browser/devtools/framework/dev-edition-promo.css           (framework/dev-edition-promo/dev-edition-promo.css)
--- a/browser/devtools/markupview/markup-view.xhtml
+++ b/browser/devtools/markupview/markup-view.xhtml
@@ -80,17 +80,17 @@
      --><span save="${name}" class="attr-name theme-fg-color2"></span><!--
      -->=&quot;<!--
      --><span save="${val}" class="attr-value theme-fg-color6"></span><!--
      -->&quot;<!--
    --></span><!--
  --></span>
 
     <span id="template-text" save="${elt}" class="editor text">
-      <pre save="${value}" style="display:inline-block;" tabindex="0"></pre>
+      <pre save="${value}" style="display:inline-block; white-space: normal;" tabindex="0"></pre>
     </span>
 
     <span id="template-comment"
           save="${elt}"
           class="editor comment theme-comment"><!--
    --><span>&lt;!--</span><!--
    --><pre save="${value}" style="display:inline-block;" tabindex="0"></pre><!--
    --><span>--&gt;</span><!--
--- a/browser/devtools/markupview/test/browser.ini
+++ b/browser/devtools/markupview/test/browser.ini
@@ -69,16 +69,17 @@ skip-if = e10s # Bug 1040751 - CodeMirro
 skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible
 [browser_markupview_events_jquery_2.1.1.js]
 skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible
 [browser_markupview_links_01.js]
 [browser_markupview_links_02.js]
 [browser_markupview_links_03.js]
 [browser_markupview_links_04.js]
 [browser_markupview_links_05.js]
+[browser_markupview_links_06.js]
 [browser_markupview_load_01.js]
 [browser_markupview_html_edit_01.js]
 [browser_markupview_html_edit_02.js]
 [browser_markupview_html_edit_03.js]
 [browser_markupview_image_tooltip.js]
 [browser_markupview_keybindings_01.js]
 [browser_markupview_keybindings_02.js]
 [browser_markupview_keybindings_03.js]
--- a/browser/devtools/markupview/test/browser_markupview_links_01.js
+++ b/browser/devtools/markupview/test/browser_markupview_links_01.js
@@ -8,17 +8,17 @@
 // values) are URIs or pointers to IDs.
 
 const TEST_URL = TEST_URL_ROOT + "doc_markup_links.html";
 
 const TEST_DATA = [{
   selector: "link",
   attributes: [{
     attributeName: "href",
-    links: [{type: "resource", value: "style.css"}]
+    links: [{type: "cssresource", value: "style.css"}]
   }]
 }, {
   selector: "link[rel=icon]",
   attributes: [{
     attributeName: "href",
     links: [{type: "uri", value: "/media/img/firefox/favicon-196.223e1bcaf067.png"}]
   }]
 }, {
@@ -90,17 +90,17 @@ const TEST_DATA = [{
   }, {
     attributeName: "src",
     links: [{type: "uri", value: "code-rush.mp4"}]
   }]
 }, {
   selector: "script",
   attributes: [{
     attributeName: "src",
-    links: [{type: "resource", value: "lib_jquery_1.0.js"}]
+    links: [{type: "jsresource", value: "lib_jquery_1.0.js"}]
   }]
 }];
 
 add_task(function*() {
   let {inspector} = yield addTab(TEST_URL).then(openInspector);
 
   for (let {selector, attributes} of TEST_DATA) {
     info("Testing attributes on node " + selector);
--- a/browser/devtools/markupview/test/browser_markupview_links_04.js
+++ b/browser/devtools/markupview/test/browser_markupview_links_04.js
@@ -5,33 +5,35 @@
 "use strict";
 
 // Tests that the contextual menu shows the right items when clicking on a link
 // in an attribute.
 
 const TEST_URL = TEST_URL_ROOT + "doc_markup_links.html";
 const STRINGS = Services.strings
   .createBundle("chrome://browser/locale/devtools/inspector.properties");
+const TOOLBOX_STRINGS = Services.strings
+  .createBundle("chrome://browser/locale/devtools/toolbox.properties");
 
 // The test case array contains objects with the following properties:
 // - selector: css selector for the node to select in the inspector
 // - attributeName: name of the attribute to test
 // - popupNodeSelector: css selector for the element inside the attribute
 //   element to use as the contextual menu anchor
 // - isLinkFollowItemVisible: is the follow-link item expected to be displayed
 // - isLinkCopyItemVisible: is the copy-link item expected to be displayed
 // - linkFollowItemLabel: the expected label of the follow-link item
 // - linkCopyItemLabel: the expected label of the copy-link item
 const TEST_DATA = [{
   selector: "link",
   attributeName: "href",
   popupNodeSelector: ".link",
   isLinkFollowItemVisible: true,
   isLinkCopyItemVisible: true,
-  linkFollowItemLabel: STRINGS.GetStringFromName("inspector.menu.openUrlInNewTab.label"),
+  linkFollowItemLabel: TOOLBOX_STRINGS.GetStringFromName("toolbox.viewCssSourceInStyleEditor.label"),
   linkCopyItemLabel: STRINGS.GetStringFromName("inspector.menu.copyUrlToClipboard.label")
 }, {
   selector: "link[rel=icon]",
   attributeName: "href",
   popupNodeSelector: ".link",
   isLinkFollowItemVisible: true,
   isLinkCopyItemVisible: true,
   linkFollowItemLabel: STRINGS.GetStringFromName("inspector.menu.openUrlInNewTab.label"),
@@ -51,17 +53,17 @@ const TEST_DATA = [{
   linkFollowItemLabel: STRINGS.formatStringFromName(
     "inspector.menu.selectElement.label", ["name"], 1)
 }, {
   selector: "script",
   attributeName: "src",
   popupNodeSelector: ".link",
   isLinkFollowItemVisible: true,
   isLinkCopyItemVisible: true,
-  linkFollowItemLabel: STRINGS.GetStringFromName("inspector.menu.openUrlInNewTab.label"),
+  linkFollowItemLabel: TOOLBOX_STRINGS.GetStringFromName("toolbox.viewJsSourceInDebugger.label"),
   linkCopyItemLabel: STRINGS.GetStringFromName("inspector.menu.copyUrlToClipboard.label")
 }, {
   selector: "p[for]",
   attributeName: "for",
   popupNodeSelector: ".attr-value",
   isLinkFollowItemVisible: false,
   isLinkCopyItemVisible: false
 }];
--- a/browser/devtools/markupview/test/browser_markupview_links_05.js
+++ b/browser/devtools/markupview/test/browser_markupview_links_05.js
@@ -7,19 +7,16 @@
 // Tests that the contextual menu items shown when clicking on links in
 // attributes actually do the right things.
 
 const TEST_URL = TEST_URL_ROOT + "doc_markup_links.html";
 
 add_task(function*() {
   let {inspector} = yield addTab(TEST_URL).then(openInspector);
 
-  let linkFollow = inspector.panelDoc.getElementById("node-menu-link-follow");
-  let linkCopy = inspector.panelDoc.getElementById("node-menu-link-copy");
-
   info("Select a node with a URI attribute");
   yield selectNode("video", inspector);
 
   info("Set the popupNode to the node that contains the uri");
   let {editor} = yield getContainerForSelector("video", inspector);
   let popupNode = editor.attrElements.get("poster").querySelector(".link");
   inspector.panelDoc.popupNode = popupNode;
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_markupview_links_06.js
@@ -0,0 +1,51 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the contextual menu items shown when clicking on linked attributes
+// for <script> and <link> tags actually open the right tools.
+
+const TEST_URL = TEST_URL_ROOT + "doc_markup_links.html";
+
+add_task(function*() {
+  let {toolbox, inspector} = yield addTab(TEST_URL).then(openInspector);
+
+  info("Select a node with a cssresource attribute");
+  yield selectNode("link", inspector);
+
+  info("Set the popupNode to the node that contains the uri");
+  let {editor} = yield getContainerForSelector("link", inspector);
+  let popupNode = editor.attrElements.get("href").querySelector(".link");
+  inspector.panelDoc.popupNode = popupNode;
+
+  info("Follow the link and wait for the style-editor to open");
+  let onStyleEditorReady = toolbox.once("styleeditor-ready");
+  inspector.followAttributeLink();
+  yield onStyleEditorReady;
+
+  // No real need to test that the editor opened on the right file here as this
+  // is already tested in /framework/test/browser_toolbox_view_source_*
+  ok(true, "The style-editor was open");
+
+  info("Switch back to the inspector");
+  yield toolbox.selectTool("inspector");
+
+  info("Select a node with a jsresource attribute");
+  yield selectNode("script", inspector);
+
+  info("Set the popupNode to the node that contains the uri");
+  ({editor}) = yield getContainerForSelector("script", inspector);
+  popupNode = editor.attrElements.get("src").querySelector(".link");
+  inspector.panelDoc.popupNode = popupNode;
+
+  info("Follow the link and wait for the debugger to open");
+  let onDebuggerReady = toolbox.once("jsdebugger-ready");
+  inspector.followAttributeLink();
+  yield onDebuggerReady;
+
+  // No real need to test that the debugger opened on the right file here as
+  // this is already tested in /framework/test/browser_toolbox_view_source_*
+  ok(true, "The debugger was open");
+});
--- a/browser/devtools/shared/inplace-editor.js
+++ b/browser/devtools/shared/inplace-editor.js
@@ -242,16 +242,18 @@ function InplaceEditor(aOptions, aEvent)
     (e) => { e.stopPropagation(); }, false);
 
   this.validate = aOptions.validate;
 
   if (this.validate) {
     this.input.addEventListener("keyup", this._onKeyup, false);
   }
 
+  this._updateSize();
+
   if (aOptions.start) {
     aOptions.start(this, aEvent);
   }
 
   EventEmitter.decorate(this);
 }
 
 exports.InplaceEditor = InplaceEditor;
@@ -359,17 +361,16 @@ InplaceEditor.prototype = {
     // we get a chance to resize.  Yuck.
     let width = this._measurement.offsetWidth + 10;
 
     if (this.multiline) {
       // Make sure there's some content in the current line.  This is a hack to
       // account for the fact that after adding a newline the <pre> doesn't grow
       // unless there's text content on the line.
       width += 15;
-      this._measurement.textContent += "M";
       this.input.style.height = this._measurement.offsetHeight + "px";
     }
 
     this.input.style.width = width + "px";
   },
 
   /**
    * Get the width of a single character in the input to properly position the
--- a/browser/devtools/shared/node-attribute-parser.js
+++ b/browser/devtools/shared/node-attribute-parser.js
@@ -13,28 +13,31 @@
  *
  * There are several types of linkable attribute values:
  * - TYPE_URI: a URI (e.g. <a href="uri">).
  * - TYPE_URI_LIST: a space separated list of URIs (e.g. <a ping="uri1 uri2">).
  * - TYPE_IDREF: a reference to an other element in the same document via its id
  *   (e.g. <label for="input-id"> or <key command="command-id">).
  * - TYPE_IDREF_LIST: a space separated list of IDREFs (e.g.
  *   <output for="id1 id2">).
- * - TYPE_RESOURCE_URI: a URI to a javascript or css resource that can be opened
- *   in the devtools (e.g. <script src="uri">).
+ * - TYPE_JS_RESOURCE_URI: a URI to a javascript resource that can be opened in
+ *   the devtools (e.g. <script src="uri">).
+ * - TYPE_CSS_RESOURCE_URI: a URI to a css resource that can be opened in the
+ *   devtools (e.g. <link href="uri">).
  *
  * parseAttribute is the parser entry function, exported on this module.
  */
 
 const TYPE_STRING = "string";
 const TYPE_URI = "uri";
 const TYPE_URI_LIST = "uriList";
 const TYPE_IDREF = "idref";
 const TYPE_IDREF_LIST = "idrefList";
-const TYPE_RESOURCE_URI = "resource";
+const TYPE_JS_RESOURCE_URI = "jsresource";
+const TYPE_CSS_RESOURCE_URI = "cssresource";
 
 const SVG_NS = "http://www.w3.org/2000/svg";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
 const ATTRIBUTE_TYPES = [
   {namespaceURI: HTML_NS, attributeName: "action", tagName: "form", type: TYPE_URI},
   {namespaceURI: HTML_NS, attributeName: "background", tagName: "body", type: TYPE_URI},
@@ -60,34 +63,34 @@ const ATTRIBUTE_TYPES = [
   {namespaceURI: HTML_NS, attributeName: "form", tagName: "select", type: TYPE_IDREF},
   {namespaceURI: HTML_NS, attributeName: "form", tagName: "textarea", type: TYPE_IDREF},
   {namespaceURI: HTML_NS, attributeName: "formaction", tagName: "button", type: TYPE_URI},
   {namespaceURI: HTML_NS, attributeName: "formaction", tagName: "input", type: TYPE_URI},
   {namespaceURI: HTML_NS, attributeName: "headers", tagName: "td", type: TYPE_IDREF_LIST},
   {namespaceURI: HTML_NS, attributeName: "headers", tagName: "th", type: TYPE_IDREF_LIST},
   {namespaceURI: HTML_NS, attributeName: "href", tagName: "a", type: TYPE_URI},
   {namespaceURI: HTML_NS, attributeName: "href", tagName: "area", type: TYPE_URI},
-  {namespaceURI: "*", attributeName: "href", tagName: "link", type: TYPE_RESOURCE_URI,
+  {namespaceURI: "*", attributeName: "href", tagName: "link", type: TYPE_CSS_RESOURCE_URI,
    isValid: (namespaceURI, tagName, attributes) => {
     return getAttribute(attributes, "rel") === "stylesheet";
    }},
   {namespaceURI: "*", attributeName: "href", tagName: "link", type: TYPE_URI},
   {namespaceURI: HTML_NS, attributeName: "href", tagName: "base", type: TYPE_URI},
   {namespaceURI: HTML_NS, attributeName: "icon", tagName: "menuitem", type: TYPE_URI},
   {namespaceURI: HTML_NS, attributeName: "list", tagName: "input", type: TYPE_IDREF},
   {namespaceURI: HTML_NS, attributeName: "longdesc", tagName: "img", type: TYPE_URI},
   {namespaceURI: HTML_NS, attributeName: "longdesc", tagName: "frame", type: TYPE_URI},
   {namespaceURI: HTML_NS, attributeName: "longdesc", tagName: "iframe", type: TYPE_URI},
   {namespaceURI: HTML_NS, attributeName: "manifest", tagName: "html", type: TYPE_URI},
   {namespaceURI: HTML_NS, attributeName: "menu", tagName: "button", type: TYPE_IDREF},
   {namespaceURI: HTML_NS, attributeName: "ping", tagName: "a", type: TYPE_URI_LIST},
   {namespaceURI: HTML_NS, attributeName: "ping", tagName: "area", type: TYPE_URI_LIST},
   {namespaceURI: HTML_NS, attributeName: "poster", tagName: "video", type: TYPE_URI},
   {namespaceURI: HTML_NS, attributeName: "profile", tagName: "head", type: TYPE_URI},
-  {namespaceURI: "*", attributeName: "src", tagName: "script", type: TYPE_RESOURCE_URI},
+  {namespaceURI: "*", attributeName: "src", tagName: "script", type: TYPE_JS_RESOURCE_URI},
   {namespaceURI: HTML_NS, attributeName: "src", tagName: "input", type: TYPE_URI},
   {namespaceURI: HTML_NS, attributeName: "src", tagName: "frame", type: TYPE_URI},
   {namespaceURI: HTML_NS, attributeName: "src", tagName: "iframe", type: TYPE_URI},
   {namespaceURI: HTML_NS, attributeName: "src", tagName: "img", type: TYPE_URI},
   {namespaceURI: HTML_NS, attributeName: "src", tagName: "audio", type: TYPE_URI},
   {namespaceURI: HTML_NS, attributeName: "src", tagName: "embed", type: TYPE_URI},
   {namespaceURI: HTML_NS, attributeName: "src", tagName: "source", type: TYPE_URI},
   {namespaceURI: HTML_NS, attributeName: "src", tagName: "track", type: TYPE_URI},
@@ -132,19 +135,25 @@ let parsers = {
     let data = splitBy(attributeValue, " ");
     for (let token of data) {
       if (!token.type) {
         token.type = TYPE_URI;
       }
     }
     return data;
   },
-  [TYPE_RESOURCE_URI]: function(attributeValue) {
+  [TYPE_JS_RESOURCE_URI]: function(attributeValue) {
     return [{
-      type: TYPE_RESOURCE_URI,
+      type: TYPE_JS_RESOURCE_URI,
+      value: attributeValue
+    }];
+  },
+  [TYPE_CSS_RESOURCE_URI]: function(attributeValue) {
+    return [{
+      type: TYPE_CSS_RESOURCE_URI,
       value: attributeValue
     }];
   },
   [TYPE_IDREF]: function(attributeValue) {
     return [{
       type: TYPE_IDREF,
       value: attributeValue
     }];
@@ -164,17 +173,17 @@ let parsers = {
  * Parse an attribute value.
  * @param {String} namespaceURI The namespaceURI of the node that has the
  * attribute.
  * @param {String} tagName The tagName of the node that has the attribute.
  * @param {Array} attributes The list of all attributes of the node. This should
  * be an array of {name, value} objects.
  * @param {String} attributeName The name of the attribute to parse.
  * @return {Array} An array of tokens that represents the value. Each token is
- * an object {type: [string|uri|resource|idref], value}.
+ * an object {type: [string|uri|jsresource|cssresource|idref], value}.
  * For instance parsing the ping attribute in <a ping="uri1 uri2"> returns:
  * [
  *   {type: "uri", value: "uri2"},
  *   {type: "string", value: " "},
  *   {type: "uri", value: "uri1"}
  * ]
  */
 function parseAttribute(namespaceURI, tagName, attributes, attributeName) {
--- a/browser/devtools/shared/test/unit/test_attribute-parsing-02.js
+++ b/browser/devtools/shared/test/unit/test_attribute-parsing-02.js
@@ -46,17 +46,17 @@ const TEST_DATA = [{
   ]
 }, {
   tagName: "link",
   namespaceURI: "http://www.w3.org/1999/xhtml",
   attributeName: "href",
   attributeValue: "styles.css",
   otherAttributes: [{name: "rel", value: "stylesheet"}],
   expected: [
-    {value: "styles.css", type: "resource"}
+    {value: "styles.css", type: "cssresource"}
   ]
 }, {
   tagName: "link",
   namespaceURI: "http://www.w3.org/1999/xhtml",
   attributeName: "href",
   attributeValue: "styles.css",
   expected: [
     {value: "styles.css", type: "uri"}
@@ -98,17 +98,17 @@ const TEST_DATA = [{
     {value: "some_command_id", type: "idref"}
   ]
 }, {
   tagName: "script",
   namespaceURI: "whatever",
   attributeName: "src",
   attributeValue: "script.js",
   expected: [
-    {value: "script.js", type: "resource"}
+    {value: "script.js", type: "jsresource"}
   ]
 }];
 
 function run_test() {
   for (let {tagName, namespaceURI, attributeName,
             otherAttributes, attributeValue, expected} of TEST_DATA) {
     do_print("Testing <" + tagName + " " + attributeName + "='" + attributeValue + "'>");
 
--- a/browser/devtools/webconsole/console-output.js
+++ b/browser/devtools/webconsole/console-output.js
@@ -88,16 +88,17 @@ const CONSOLE_API_LEVELS_TO_SEVERITIES =
   assert: "error",
   warn: "warning",
   info: "info",
   log: "log",
   trace: "log",
   table: "log",
   debug: "log",
   dir: "log",
+  dirxml: "log",
   group: "log",
   groupCollapsed: "log",
   groupEnd: "log",
   time: "log",
   timeEnd: "log",
   count: "log"
 };
 
@@ -2977,17 +2978,18 @@ Widgets.ObjectRenderers.add({
         break;
       default:
         throw new Error("Unsupported nodeType: " + preview.nodeType);
     }
   },
 
   _renderDocumentNode: function()
   {
-    let fn = Widgets.ObjectRenderers.byKind.ObjectWithURL.prototype._renderElement;
+    let fn =
+      Widgets.ObjectRenderers.byKind.ObjectWithURL.prototype._renderElement;
     this.element = fn.call(this, this.objectActor,
                            this.objectActor.preview.location);
     this.element.classList.add("documentNode");
   },
 
   _renderAttributeNode: function(nodeName, nodeValue, addLink)
   {
     let value = VariablesView.getString(nodeValue, { noStringQuotes: true });
--- a/browser/devtools/webconsole/test/browser.ini
+++ b/browser/devtools/webconsole/test/browser.ini
@@ -379,8 +379,9 @@ skip-if = e10s # Bug 1042253 - webconsol
 [browser_webconsole_start_netmon_first.js]
 [browser_webconsole_console_trace_duplicates.js]
 [browser_webconsole_cd_iframe.js]
 [browser_webconsole_autocomplete_crossdomain_iframe.js]
 [browser_webconsole_console_custom_styles.js]
 [browser_webconsole_console_api_stackframe.js]
 [browser_webconsole_column_numbers.js]
 [browser_console_open_or_focus.js]
+[browser_webconsole_bug_922212_console_dirxml.js]
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js
@@ -4,17 +4,17 @@
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 // Tests that console.dir works as intended.
 
 "use strict";
 
 const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 659907: " +
-  "Expand console object with a dir method"
+  "Expand console object with a dir method";
 
 let test = asyncTest(function*() {
   yield loadTab(TEST_URI);
   let hud = yield openConsole();
   hud.jsterm.clearOutput();
 
   hud.jsterm.execute("console.dir(document)");
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_922212_console_dirxml.js
@@ -0,0 +1,49 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that console.dirxml works as intended.
+
+"use strict";
+
+const TEST_URI = `data:text/html;charset=utf-8,Web Console test for bug 922212:
+  Add console.dirxml`;
+
+let test = asyncTest(function*() {
+  yield loadTab(TEST_URI);
+  let hud = yield openConsole();
+  hud.jsterm.clearOutput();
+
+  // Should work like console.log(window)
+  hud.jsterm.execute("console.dirxml(window)");
+
+  let [result] = yield waitForMessages({
+    webconsole: hud,
+    messages: [{
+      name: "console.dirxml(window) output:",
+      text: /Window \u2192/,
+      category: CATEGORY_WEBDEV,
+      severity: SEVERITY_LOG,
+    }],
+  });
+
+  hud.jsterm.clearOutput();
+
+  hud.jsterm.execute("console.dirxml(document.body)");
+
+  // Should work like console.log(document.body);
+  [result] = yield waitForMessages({
+    webconsole: hud,
+    messages: [{
+      name: "console.dirxml(document.body) output:",
+      text: "<body>",
+      category: CATEGORY_WEBDEV,
+      severity: SEVERITY_LOG,
+    }],
+  });
+  let msg = [...result.matched][0];
+  yield checkLinkToInspector(true, msg);
+});
+
--- a/browser/devtools/webconsole/test/head.js
+++ b/browser/devtools/webconsole/test/head.js
@@ -1469,17 +1469,18 @@ function checkOutputForInputs(hud, input
         text: entry.output,
         category: CATEGORY_WEBDEV,
         severity: SEVERITY_LOG,
       }],
     });
 
     if (typeof entry.inspectorIcon == "boolean") {
       let msg = [...result.matched][0];
-      yield checkLinkToInspector(entry, msg);
+      info("Checking Inspector Link: " + entry.input);
+      yield checkLinkToInspector(entry.inspectorIcon, msg);
     }
   }
 
   function checkPrintOutput(entry)
   {
     info("Printing: " + entry.input);
     hud.jsterm.clearOutput();
     hud.jsterm.execute("print(" + entry.input + ")");
@@ -1511,17 +1512,18 @@ function checkOutputForInputs(hud, input
       }],
     });
 
     let msg = [...result.matched][0];
     if (!entry.noClick) {
       yield checkObjectClick(entry, msg);
     }
     if (typeof entry.inspectorIcon == "boolean") {
-      yield checkLinkToInspector(entry, msg);
+      info("Checking Inspector Link: " + entry.input);
+      yield checkLinkToInspector(entry.inspectorIcon, msg);
     }
   }
 
   function* checkObjectClick(entry, msg)
   {
     info("Clicking: " + entry.input);
     let body = msg.querySelector(".message-body a") ||
                msg.querySelector(".message-body");
@@ -1551,40 +1553,16 @@ function checkOutputForInputs(hud, input
     } else {
       container.removeEventListener("TabOpen", entry._onTabOpen, true);
       entry._onTabOpen = null;
     }
 
     yield promise.resolve(null);
   }
 
-  function checkLinkToInspector(entry, msg)
-  {
-    info("Checking Inspector Link: " + entry.input);
-    let elementNodeWidget = [...msg._messageObject.widgets][0];
-    if (!elementNodeWidget) {
-      ok(!entry.inspectorIcon, "The message has no ElementNode widget");
-      return;
-    }
-
-    return elementNodeWidget.linkToInspector().then(() => {
-      // linkToInspector resolved, check for the .open-inspector element
-      if (entry.inspectorIcon) {
-        ok(msg.querySelectorAll(".open-inspector").length,
-          "The ElementNode widget is linked to the inspector");
-      } else {
-        ok(!msg.querySelectorAll(".open-inspector").length,
-          "The ElementNode widget isn't linked to the inspector");
-      }
-    }, () => {
-      // linkToInspector promise rejected, node not linked to inspector
-      ok(!entry.inspectorIcon, "The ElementNode widget isn't linked to the inspector");
-    });
-  }
-
   function onVariablesViewOpen(entry, {resolve, reject}, event, view, options)
   {
     info("Variables view opened: " + entry.input);
     let label = entry.variablesViewLabel || entry.output;
     if (typeof label == "string" && options.label != label) {
       return;
     }
     if (label instanceof RegExp && !label.test(options.label)) {
@@ -1641,16 +1619,46 @@ function once(target, eventName, useCapt
       }, useCapture);
       break;
     }
   }
 
   return deferred.promise;
 }
 
+/**
+ * Checks a link to the inspector
+ *
+ * @param {boolean} hasLinkToInspector Set to true if the message should
+ *  link to the inspector panel.
+ * @param {element} msg The message to test.
+ */
+function checkLinkToInspector(hasLinkToInspector, msg)
+{
+  let elementNodeWidget = [...msg._messageObject.widgets][0];
+  if (!elementNodeWidget) {
+    ok(!hasLinkToInspector, "The message has no ElementNode widget");
+    return;
+  }
+
+  return elementNodeWidget.linkToInspector().then(() => {
+    // linkToInspector resolved, check for the .open-inspector element
+    if (hasLinkToInspector) {
+      ok(msg.querySelectorAll(".open-inspector").length,
+        "The ElementNode widget is linked to the inspector");
+    } else {
+      ok(!msg.querySelectorAll(".open-inspector").length,
+        "The ElementNode widget isn't linked to the inspector");
+    }
+  }, () => {
+    // linkToInspector promise rejected, node not linked to inspector
+    ok(!hasLinkToInspector, "The ElementNode widget isn't linked to the inspector");
+  });
+}
+
 function getSourceActor(aSources, aURL) {
   let item = aSources.getItemForAttachment(a => a.source.url === aURL);
   return item && item.value;
 }
 
 /**
  * Verify that clicking on a link from a popup notification message tries to
  * open the expected URL.
--- a/browser/devtools/webconsole/test/test-console-extras.html
+++ b/browser/devtools/webconsole/test/test-console-extras.html
@@ -1,17 +1,17 @@
 <!DOCTYPE HTML>
 <html dir="ltr" xml:lang="en-US" lang="en-US"><head>
     <meta charset="utf-8">
     <title>Console extended API test</title>
     <script type="text/javascript">
       function test() {
         console.log("start");
         console.clear()
-        console.dirxml()
+        console.timeStamp()
         console.log("end");
       }
     </script>
   </head>
   <body>
     <h1 id="header">Heads Up Display Demo</h1>
     <button onclick="test();">Test Extended API</button>
     <div id="myDiv"></div>
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -125,16 +125,17 @@ const LEVELS = {
   assert: SEVERITY_ERROR,
   warn: SEVERITY_WARNING,
   info: SEVERITY_INFO,
   log: SEVERITY_LOG,
   trace: SEVERITY_LOG,
   table: SEVERITY_LOG,
   debug: SEVERITY_LOG,
   dir: SEVERITY_LOG,
+  dirxml: SEVERITY_LOG,
   group: SEVERITY_LOG,
   groupCollapsed: SEVERITY_LOG,
   groupEnd: SEVERITY_LOG,
   time: SEVERITY_LOG,
   timeEnd: SEVERITY_LOG,
   count: SEVERITY_LOG
 };
 
@@ -1280,17 +1281,21 @@ WebConsoleFrame.prototype = {
         body = { arguments: args };
         let clipboardArray = [];
         args.forEach((aValue) => {
           clipboardArray.push(VariablesView.getString(aValue));
         });
         clipboardText = clipboardArray.join(" ");
         break;
       }
-
+      case "dirxml": {
+        // We just alias console.dirxml() with console.log().
+        aMessage.level = "log";
+        return WCF_logConsoleAPIMessage.call(this, aMessage);
+      }
       case "group":
       case "groupCollapsed":
         clipboardText = body = aMessage.groupName;
         this.groupDepth++;
         break;
 
       case "groupEnd":
         if (this.groupDepth > 0) {
--- a/browser/locales/en-US/chrome/browser/devtools/inspector.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/inspector.properties
@@ -80,28 +80,14 @@ inspector.expandPane=Expand pane
 inspector.menu.openUrlInNewTab.label=Open Link in New Tab
 
 # LOCALIZATION NOTE (inspector.menu.copyUrlToClipboard.label): This is the label
 # of a menu item in the inspector contextual-menu that appears when the user
 # right-clicks on the attribute of a node in the inspector that is a URL, and
 # that allows to copy that URL in the clipboard.
 inspector.menu.copyUrlToClipboard.label=Copy Link Address
 
-# LOCALIZATION NOTE (inspector.menu.openFileInDebugger.label): This is the label
-# of a menu item in the inspector contextual-menu that appears when the user
-# right-clicks on the attribute of a node in the inspector that is a URL to a
-# javascript filename, and that allows to open the corresponding file in the
-# debugger.
-inspector.menu.openFileInDebugger.label=Open File in Debugger
-
-# LOCALIZATION NOTE (inspector.menu.openFileInStyleEditor.label): This is the
-# label of a menu item in the inspector contextual-menu that appears when the
-# user right-clicks on the attribute of a node in the inspector that is a URL to
-# a css filename, and that allows to open the corresponding file in the style
-# editor.
-inspector.menu.openFileInStyleEditor.label=Open File in Style-Editor
-
 # LOCALIZATION NOTE (inspector.menu.selectElement.label): This is the label of a
 # menu item in the inspector contextual-menu that appears when the user right-
 # clicks on the attribute of a node in the inspector that is the ID of another
 # element in the DOM (like with <label for="input-id">), and that allows to
 # select that element in the inspector.
 inspector.menu.selectElement.label=Select Element #%S
--- a/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd
@@ -1,16 +1,19 @@
 <!-- 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/. -->
 
 <!-- LOCALIZATION NOTE : FILE This file contains the Toolbox strings -->
 <!-- LOCALIZATION NOTE : FILE Do not translate key -->
 
 <!ENTITY closeCmd.key  "W">
+<!ENTITY toggleToolbox.key  "I">
+<!ENTITY toggleToolboxF12.keycode          "VK_F12">
+<!ENTITY toggleToolboxF12.keytext          "F12">
 
 <!ENTITY toolboxCloseButton.tooltip    "Close Developer Tools">
 <!ENTITY toolboxOptionsButton.key      "O">
 <!ENTITY toolboxNextTool.key           "]">
 <!ENTITY toolboxPreviousTool.key       "[">
 
 <!ENTITY toolboxZoomIn.key             "+">
 <!ENTITY toolboxZoomIn.key2            "="> <!-- + is above this key on many keyboards -->
--- a/browser/locales/en-US/chrome/browser/devtools/toolbox.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/toolbox.properties
@@ -83,8 +83,20 @@ options.darkTheme.label=Dark theme
 # LOCALIZATION NOTE (options.lightTheme.label)
 # Used as a label for light theme
 options.lightTheme.label=Light theme
 
 # LOCALIZATION NOTE (toolbox.noContentProcess.message)
 # Used as a message in the alert displayed when trying to open a browser
 # content toolbox and there is no content process running
 toolbox.noContentProcess.message=No content process running.
+
+# LOCALIZATION NOTE (toolbox.viewCssSourceInStyleEditor.label)
+# Used as a message in either tooltips or contextual menu items to open the
+# corresponding URL as a css file in the Style-Editor tool.
+# DEV NOTE: Mostly used wherever toolbox.viewSourceInStyleEditor is used.
+toolbox.viewCssSourceInStyleEditor.label=Open File in Style-Editor
+
+# LOCALIZATION NOTE (toolbox.viewJsSourceInDebugger.label)
+# Used as a message in either tooltips or contextual menu items to open the
+# corresponding URL as a js file in the Debugger tool.
+# DEV NOTE: Mostly used wherever toolbox.viewSourceInDebugger is used.
+toolbox.viewJsSourceInDebugger.label=Open File in Debugger
--- a/browser/locales/en-US/chrome/browser/places/places.dtd
+++ b/browser/locales/en-US/chrome/browser/places/places.dtd
@@ -80,17 +80,16 @@
 <!ENTITY cmd.moveBookmarks.label                  "Move…">
 <!ENTITY cmd.moveBookmarks.accesskey              "M">
 
 <!ENTITY col.name.label          "Name">
 <!ENTITY col.tags.label          "Tags">
 <!ENTITY col.url.label           "Location">
 <!ENTITY col.mostrecentvisit.label "Most Recent Visit">
 <!ENTITY col.visitcount.label    "Visit Count">
-<!ENTITY col.keyword.label       "Keyword">
 <!ENTITY col.description.label   "Description">
 <!ENTITY col.dateadded.label     "Added">
 <!ENTITY col.lastmodified.label  "Last Modified">
 
 <!ENTITY search.label                              "Search:">
 <!ENTITY search.accesskey                          "S">
 
 <!ENTITY cmd.find.key  "f">
--- a/browser/locales/en-US/chrome/browser/places/places.properties
+++ b/browser/locales/en-US/chrome/browser/places/places.properties
@@ -31,18 +31,16 @@ sortByNameGeneric=Sort by Name
 view.sortBy.1.name.label=Sort by Name
 view.sortBy.1.name.accesskey=N
 view.sortBy.1.url.label=Sort by Location
 view.sortBy.1.url.accesskey=L
 view.sortBy.1.date.label=Sort by Most Recent Visit
 view.sortBy.1.date.accesskey=V
 view.sortBy.1.visitCount.label=Sort by Visit Count
 view.sortBy.1.visitCount.accesskey=C
-view.sortBy.1.keyword.label=Sort by Keyword
-view.sortBy.1.keyword.accesskey=K
 view.sortBy.1.description.label=Sort by Description
 view.sortBy.1.description.accesskey=D
 view.sortBy.1.dateAdded.label=Sort by Added
 view.sortBy.1.dateAdded.accesskey=e
 view.sortBy.1.lastModified.label=Sort by Last Modified
 view.sortBy.1.lastModified.accesskey=M
 view.sortBy.1.tags.label=Sort by Tags
 view.sortBy.1.tags.accesskey=T
--- a/browser/modules/Feeds.jsm
+++ b/browser/modules/Feeds.jsm
@@ -2,16 +2,17 @@
  * 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";
 
 this.EXPORTED_SYMBOLS = [ "Feeds" ];
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
                                   "resource://gre/modules/BrowserUtils.jsm");
 
 const Ci = Components.interfaces;
 
 this.Feeds = {
 
@@ -32,18 +33,21 @@ this.Feeds = {
 
     var type = aLink.type.toLowerCase().replace(/^\s+|\s*(?:;.*)?$/g, "");
     if (!aIsFeed) {
       aIsFeed = (type == "application/rss+xml" ||
                  type == "application/atom+xml");
     }
 
     if (aIsFeed) {
+      // re-create the principal as it may be a CPOW.
+      let principalURI = BrowserUtils.makeURIFromCPOW(aPrincipal.URI);
+      let principalToCheck = Services.scriptSecurityManager.getNoAppCodebasePrincipal(principalURI);
       try {
-        BrowserUtils.urlSecurityCheck(aLink.href, aPrincipal,
+        BrowserUtils.urlSecurityCheck(aLink.href, principalToCheck,
                                       Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
         return type || "application/rss+xml";
       }
       catch(ex) {
       }
     }
 
     return null;
--- a/dom/base/Console.cpp
+++ b/dom/base/Console.cpp
@@ -809,16 +809,17 @@ void
 Console::Trace(JSContext* aCx)
 {
   const Sequence<JS::Value> data;
   Method(aCx, MethodTrace, NS_LITERAL_STRING("trace"), data);
 }
 
 // Displays an interactive listing of all the properties of an object.
 METHOD(Dir, "dir");
+METHOD(Dirxml, "dirxml");
 
 METHOD(Group, "group")
 METHOD(GroupCollapsed, "groupCollapsed")
 METHOD(GroupEnd, "groupEnd")
 
 void
 Console::Time(JSContext* aCx, const JS::Handle<JS::Value> aTime)
 {
--- a/dom/base/Console.h
+++ b/dom/base/Console.h
@@ -70,16 +70,19 @@ public:
 
   void
   Trace(JSContext* aCx);
 
   void
   Dir(JSContext* aCx, const Sequence<JS::Value>& aData);
 
   void
+  Dirxml(JSContext* aCx, const Sequence<JS::Value>& aData);
+
+  void
   Group(JSContext* aCx, const Sequence<JS::Value>& aData);
 
   void
   GroupCollapsed(JSContext* aCx, const Sequence<JS::Value>& aData);
 
   void
   GroupEnd(JSContext* aCx, const Sequence<JS::Value>& aData);
 
@@ -111,16 +114,17 @@ private:
     MethodInfo,
     MethodWarn,
     MethodError,
     MethodException,
     MethodDebug,
     MethodTable,
     MethodTrace,
     MethodDir,
+    MethodDirxml,
     MethodGroup,
     MethodGroupCollapsed,
     MethodGroupEnd,
     MethodTime,
     MethodTimeEnd,
     MethodAssert,
     MethodCount
   };
--- a/dom/webidl/Console.webidl
+++ b/dom/webidl/Console.webidl
@@ -11,34 +11,33 @@ interface Console {
   void info(any... data);
   void warn(any... data);
   void error(any... data);
   void _exception(any... data);
   void debug(any... data);
   void table(any... data);
   void trace();
   void dir(any... data);
+  void dirxml(any... data);
   void group(any... data);
   void groupCollapsed(any... data);
   void groupEnd(any... data);
   void time(optional any time);
   void timeEnd(optional any time);
 
   void profile(any... data);
   void profileEnd(any... data);
 
   void assert(boolean condition, any... data);
   void count(any... data);
 
   // No-op methods for compatibility with other browsers.
   [BinaryName="noopMethod"]
   void clear();
   [BinaryName="noopMethod"]
-  void dirxml();
-  [BinaryName="noopMethod"]
   void markTimeline();
   [BinaryName="noopMethod"]
   void timeline();
   [BinaryName="noopMethod"]
   void timelineEnd();
   [BinaryName="noopMethod"]
   void timeStamp();
 };
--- a/mobile/android/base/ZoomedView.java
+++ b/mobile/android/base/ZoomedView.java
@@ -12,63 +12,113 @@ import org.mozilla.gecko.gfx.PointUtils;
 import org.mozilla.gecko.mozglue.DirectBufferAllocator;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.content.res.Configuration;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.Matrix;
+import android.graphics.Paint;
 import android.graphics.Point;
 import android.graphics.PointF;
+import android.graphics.RectF;
+import android.graphics.Shader;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewTreeObserver;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.RelativeLayout;
+import android.widget.TextView;
 
 import java.nio.ByteBuffer;
 import java.text.DecimalFormat;
 
 public class ZoomedView extends FrameLayout implements LayerView.OnMetricsChangedListener,
         LayerView.ZoomedViewListener, GeckoEventListener {
     private static final String LOGTAG = "Gecko" + ZoomedView.class.getSimpleName();
 
-    private static final int DEFAULT_ZOOM_FACTOR = 3;
-    private static final int W_CAPTURED_VIEW_IN_PERCENT = 80;
+    private static final float[] ZOOM_FACTORS_LIST = {2.0f, 3.0f, 1.5f};
+    private static final int W_CAPTURED_VIEW_IN_PERCENT = 50;
     private static final int H_CAPTURED_VIEW_IN_PERCENT = 50;
     private static final int MINIMUM_DELAY_BETWEEN_TWO_RENDER_CALLS_NS = 1000000;
     private static final int DELAY_BEFORE_NEXT_RENDER_REQUEST_MS = 2000;
 
-    private int zoomFactor;
+    private float zoomFactor;
+    private int currentZoomFactorIndex;
     private ImageView zoomedImageView;
     private LayerView layerView;
     private int viewWidth;
-    private int viewHeight;
+    private int viewHeight; // Only the zoomed view height, no toolbar, no shadow ...
+    private int viewContainerWidth;
+    private int viewContainerHeight; // Zoomed view height with toolbar and other elements like shadow, ...
+    private int containterSize; // shadow, margin, ...
     private Point lastPosition;
     private boolean shouldSetVisibleOnUpdate;
     private PointF returnValue;
+    private ImageView closeButton;
+    private TextView changeZoomFactorButton;
+    private boolean toolbarOnTop;
+    private float offsetDueToToolBarPosition;
+    private int toolbarHeight;
+    private int cornerRadius;
 
     private boolean stopUpdateView;
 
     private int lastOrientation;
 
     private ByteBuffer buffer;
     private Runnable requestRenderRunnable;
     private long startTimeReRender;
     private long lastStartTimeReRender;
 
+    private class RoundedBitmapDrawable extends BitmapDrawable {
+        private Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
+        final float cornerRadius;
+        final boolean squareOnTopOfDrawable;
+
+        RoundedBitmapDrawable(Resources res, Bitmap bitmap, boolean squareOnTop, int radius) {
+            super(res, bitmap);
+            squareOnTopOfDrawable = squareOnTop;
+            final BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP,
+                Shader.TileMode.CLAMP);
+            paint.setAntiAlias(true);
+            paint.setShader(shader);
+            cornerRadius = radius;
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            int height = getBounds().height();
+            int width = getBounds().width();
+            RectF rect = new RectF(0.0f, 0.0f, width, height);
+            canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint);
+
+            //draw rectangles over the corners we want to be square
+            if (squareOnTopOfDrawable) {
+                canvas.drawRect(0, 0, cornerRadius, cornerRadius, paint);
+                canvas.drawRect(width - cornerRadius, 0, width, cornerRadius, paint);
+            } else {
+                canvas.drawRect(0, height - cornerRadius, cornerRadius, height, paint);
+                canvas.drawRect(width - cornerRadius, height - cornerRadius, width, height, paint);
+            }
+        }
+    }
+
     private class ZoomedViewTouchListener implements View.OnTouchListener {
         private float originRawX;
         private float originRawY;
         private boolean dragged;
         private MotionEvent actionDownEvent;
 
         @Override
         public boolean onTouch(View view, MotionEvent event) {
@@ -82,42 +132,49 @@ public class ZoomedView extends FrameLay
                     dragged = true;
                 }
                 break;
 
             case MotionEvent.ACTION_UP:
                 if (dragged) {
                     dragged = false;
                 } else {
-                    GeckoEvent eClickInZoomedView = GeckoEvent.createBroadcastEvent("Gesture:ClickInZoomedView", "");
-                    GeckoAppShell.sendEventToGecko(eClickInZoomedView);
-                    layerView.dispatchTouchEvent(actionDownEvent);
-                    actionDownEvent.recycle();
-                    PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(event.getX(), event.getY());
-                    MotionEvent e = MotionEvent.obtain(event.getDownTime(), event.getEventTime(),
-                            MotionEvent.ACTION_UP, convertedPosition.x, convertedPosition.y,
-                            event.getMetaState());
-                    layerView.dispatchTouchEvent(e);
-                    e.recycle();
+                    if (isClickInZoomedView(event.getY())) {
+                        GeckoEvent eClickInZoomedView = GeckoEvent.createBroadcastEvent("Gesture:ClickInZoomedView", "");
+                        GeckoAppShell.sendEventToGecko(eClickInZoomedView);
+                        layerView.dispatchTouchEvent(actionDownEvent);
+                        actionDownEvent.recycle();
+                        PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(event.getX(), event.getY());
+                        MotionEvent e = MotionEvent.obtain(event.getDownTime(), event.getEventTime(),
+                                MotionEvent.ACTION_UP, convertedPosition.x, convertedPosition.y,
+                                event.getMetaState());
+                        layerView.dispatchTouchEvent(e);
+                        e.recycle();
+                    }
                 }
                 break;
 
             case MotionEvent.ACTION_DOWN:
                 dragged = false;
                 originRawX = event.getRawX();
                 originRawY = event.getRawY();
                 PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(event.getX(), event.getY());
                 actionDownEvent = MotionEvent.obtain(event.getDownTime(), event.getEventTime(),
                         MotionEvent.ACTION_DOWN, convertedPosition.x, convertedPosition.y,
                         event.getMetaState());
                 break;
             }
             return true;
         }
 
+        private boolean isClickInZoomedView(float y) {
+            return ((toolbarOnTop && y > toolbarHeight) ||
+                (!toolbarOnTop && y < ZoomedView.this.viewHeight));
+        }
+
         private boolean moveZoomedView(MotionEvent event) {
             RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) ZoomedView.this.getLayoutParams();
             if ((!dragged) && (Math.abs((int) (event.getRawX() - originRawX)) < PanZoomController.CLICK_THRESHOLD)
                     && (Math.abs((int) (event.getRawY() - originRawY)) < PanZoomController.CLICK_THRESHOLD)) {
                 // When the user just touches the screen ACTION_MOVE can be detected for a very small delta on position.
                 // In this case, the move is ignored if the delta is lower than 1 unit.
                 return false;
             }
@@ -138,54 +195,74 @@ public class ZoomedView extends FrameLay
 
     public ZoomedView(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
 
     public ZoomedView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
         returnValue = new PointF();
+        currentZoomFactorIndex = 0;
+        zoomFactor = ZOOM_FACTORS_LIST[currentZoomFactorIndex];
         requestRenderRunnable = new Runnable() {
             @Override
             public void run() {
                 requestZoomedViewRender();
             }
         };
-        EventDispatcher.getInstance().registerGeckoThreadListener(this, "Gesture:nothingDoneOnLongPress",
+        EventDispatcher.getInstance().registerGeckoThreadListener(this,
                 "Gesture:clusteredLinksClicked", "Window:Resize", "Content:LocationChange");
     }
 
     void destroy() {
         ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable);
-        EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "Gesture:nothingDoneOnLongPress",
+        EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
                 "Gesture:clusteredLinksClicked", "Window:Resize", "Content:LocationChange");
     }
 
     // This method (onFinishInflate) is called only when the zoomed view class is used inside
     // an xml structure <org.mozilla.gecko.ZoomedView ...
     // It won't be called if the class is used from java code like "new  ZoomedView(context);"
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        ImageView closeButton = (ImageView) findViewById(R.id.dialog_close);
+        closeButton = (ImageView) findViewById(R.id.dialog_close);
         closeButton.setOnClickListener(new View.OnClickListener() {
             public void onClick(View view) {
                 stopZoomDisplay();
             }
         });
 
+        changeZoomFactorButton = (TextView) findViewById(R.id.change_zoom_factor);
+        changeZoomFactorButton.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View view) {
+                changeZoomFactor();
+            }
+        });
+        setTextInZoomFactorButton(ZOOM_FACTORS_LIST[0]);
+
         zoomedImageView = (ImageView) findViewById(R.id.zoomed_image_view);
-        zoomedImageView.setOnTouchListener(new ZoomedViewTouchListener());
+        this.setOnTouchListener(new ZoomedViewTouchListener());
+
+        toolbarHeight = getResources().getDimensionPixelSize(R.dimen.zoomed_view_toolbar_height);
+        containterSize = getResources().getDimensionPixelSize(R.dimen.drawable_dropshadow_size);
+        cornerRadius = getResources().getDimensionPixelSize(R.dimen.button_corner_radius);
+
+        moveToolbar(true);
     }
 
     /*
      * Convert a click from ZoomedView. Return the position of the click in the
      * LayerView
      */
     private PointF getUnzoomedPositionFromPointInZoomedView(float x, float y) {
+        if (toolbarOnTop && y > toolbarHeight) {
+           y = y - toolbarHeight;
+        }
+
         ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
         PointF offset = metrics.getMarginOffset();
         final float parentWidth = metrics.getWidth();
         final float parentHeight = metrics.getHeight();
         RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams();
 
         returnValue.x = (int) ((x / zoomFactor) +     // Conversion of the x offset inside the zoomed view (using the scale factor)
 
@@ -194,24 +271,25 @@ public class ZoomedView extends FrameLay
                         /* Conversion of the left side position of the zoomed view
                          *   Minimum value for the left side of the zoomed view is 0
                          *     and we return 0 after conversion
                          *   Maximum value for the left side of the zoomed view is (parentWidth - offset.x - viewWidth)
                          *     and we return (parentWidth - offset.x - (viewWidth / zoomFactor)) after conversion.
                          */
                         (((float) params.leftMargin) - offset.x) *
                             ((parentWidth - offset.x - (viewWidth / zoomFactor)) /
-                            (parentWidth - offset.x - viewWidth)));
+                            (parentWidth - offset.x - viewContainerWidth)));
 
         // Same comments here vertically
         returnValue.y = (int) ((y / zoomFactor) +
-                        offset.y +
+                        offset.y -
+                        offsetDueToToolBarPosition +
                         (((float) params.topMargin) - offset.y) *
-                            ((parentHeight - offset.y - (viewHeight / zoomFactor)) /
-                            (parentHeight - offset.y - viewHeight)));
+                            ((parentHeight - offset.y + offsetDueToToolBarPosition - (viewHeight / zoomFactor)) /
+                            (parentHeight - offset.y - viewContainerHeight)));
 
         return returnValue;
     }
 
     /*
      * A touch point (x,y) occurs in LayerView, this point should be displayed
      * in the center of the zoomed view. The returned point is the position of
      * the Top-Left zoomed view point on the screen device
@@ -227,24 +305,24 @@ public class ZoomedView extends FrameLay
 
                         /* Conversion of the left side position of the zoomed view.
                          * See the comment in getUnzoomedPositionFromPointInZoomedView.
                          * The proportional factor is the same. It is used in a division
                          * and not in a multiplication to convert the position from
                          * the LayerView to the ZoomedView.
                          */
                         ((parentWidth - offset.x - (viewWidth / zoomFactor)) /
-                        (parentWidth - offset.x - viewWidth)))
+                        (parentWidth - offset.x - viewContainerWidth)))
 
                 + offset.x);     // The offset of the layerView
 
         // Same comments here vertically
-        returnValue.y = (int) ((((y - (viewHeight / (2 * zoomFactor)))) /
-                        ((parentHeight - offset.y - (viewHeight / zoomFactor)) /
-                        (parentHeight - offset.y - viewHeight)))
+        returnValue.y = (int) ((((y + offsetDueToToolBarPosition - (viewHeight / (2 * zoomFactor)))) /
+                        ((parentHeight - offset.y + offsetDueToToolBarPosition - (viewHeight / zoomFactor)) /
+                        (parentHeight - offset.y - viewContainerHeight)))
                 + offset.y);
 
         return returnValue;
     }
 
     private void moveZoomedView(ImmutableViewportMetrics metrics, float newLeftMargin, float newTopMargin) {
         final float parentWidth = metrics.getWidth();
         final float parentHeight = metrics.getHeight();
@@ -254,32 +332,69 @@ public class ZoomedView extends FrameLay
         int topMarginMin;
         int leftMarginMin;
         PointF offset = metrics.getMarginOffset();
         topMarginMin = (int) offset.y;
         leftMarginMin = (int) offset.x;
 
         if (newTopMargin < topMarginMin) {
             newLayoutParams.topMargin = topMarginMin;
-        } else if (newTopMargin + viewHeight > parentHeight) {
-            newLayoutParams.topMargin = (int) (parentHeight - viewHeight);
+        } else if (newTopMargin + viewContainerHeight > parentHeight) {
+            newLayoutParams.topMargin = (int) (parentHeight - viewContainerHeight);
         }
 
         if (newLeftMargin < leftMarginMin) {
             newLayoutParams.leftMargin = leftMarginMin;
-        } else if (newLeftMargin + viewWidth > parentWidth) {
-            newLayoutParams.leftMargin = (int) (parentWidth - viewWidth);
+        } else if (newLeftMargin + viewContainerWidth > parentWidth) {
+            newLayoutParams.leftMargin = (int) (parentWidth - viewContainerWidth);
+        }
+
+        if (newLayoutParams.topMargin < topMarginMin + 1) {
+            moveToolbar(false);
+        } else if (newLayoutParams.topMargin + viewContainerHeight > parentHeight - 1) {
+            moveToolbar(true);
         }
 
         setLayoutParams(newLayoutParams);
         PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(0, 0);
         lastPosition = PointUtils.round(convertedPosition);
         requestZoomedViewRender();
     }
 
+    private void moveToolbar(boolean moveTop) {
+        if (toolbarOnTop == moveTop) {
+            return;
+        }
+        toolbarOnTop = moveTop;
+        if (toolbarOnTop) {
+            offsetDueToToolBarPosition = toolbarHeight;
+        } else {
+            offsetDueToToolBarPosition = 0;
+        }
+
+        RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) zoomedImageView.getLayoutParams();
+        RelativeLayout.LayoutParams pChangeZoomFactorButton = (RelativeLayout.LayoutParams) changeZoomFactorButton.getLayoutParams();
+        RelativeLayout.LayoutParams pCloseButton = (RelativeLayout.LayoutParams) closeButton.getLayoutParams();
+
+        if (moveTop) {
+            p.addRule(RelativeLayout.BELOW, R.id.change_zoom_factor);
+            pChangeZoomFactorButton.addRule(RelativeLayout.BELOW, 0);
+            pCloseButton.addRule(RelativeLayout.BELOW, 0);
+        } else {
+            p.addRule(RelativeLayout.BELOW, 0);
+            pChangeZoomFactorButton.addRule(RelativeLayout.BELOW, R.id.zoomed_image_view);
+            pCloseButton.addRule(RelativeLayout.BELOW, R.id.zoomed_image_view);
+        }
+        pChangeZoomFactorButton.addRule(RelativeLayout.ALIGN_LEFT, R.id.zoomed_image_view);
+        pCloseButton.addRule(RelativeLayout.ALIGN_RIGHT, R.id.zoomed_image_view);
+        zoomedImageView.setLayoutParams(p);
+        changeZoomFactorButton.setLayoutParams(pChangeZoomFactorButton);
+        closeButton.setLayoutParams(pCloseButton);
+    }
+
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
         // In case of orientation change, the zoomed view update is stopped until the orientation change
         // is completed. At this time, the function onMetricsChanged is called and the
         // zoomed view update is restarted again.
         if (lastOrientation != newConfig.orientation) {
             shouldBlockUpdate(true);
@@ -294,23 +409,22 @@ public class ZoomedView extends FrameLay
 
         RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams();
         setCapturedSize(viewport);
         moveZoomedView(viewport, params.leftMargin, params.topMargin);
     }
 
     private void setCapturedSize(ImmutableViewportMetrics metrics) {
         float parentMinSize = Math.min(metrics.getWidth(), metrics.getHeight());
-        // For metrics.zoomFactor lower than 1, the zoom factor of the zoomed view is calculated
-        // to get always the same size for the content in the zoomed view.
-        // For metrics.zoomFactor greater than 1, the zoom factor is always set to the default
-        // value DEFAULT_ZOOM_FACTOR, thus the zoomed view is always a zoom of the normal view.
-        zoomFactor = Math.max(DEFAULT_ZOOM_FACTOR, (int) (DEFAULT_ZOOM_FACTOR / metrics.zoomFactor));
-        viewWidth = (int) (parentMinSize * W_CAPTURED_VIEW_IN_PERCENT / (zoomFactor * 100.0)) * zoomFactor;
-        viewHeight = (int) (parentMinSize * H_CAPTURED_VIEW_IN_PERCENT / (zoomFactor * 100.0)) * zoomFactor;
+        viewWidth = (int) ((parentMinSize * W_CAPTURED_VIEW_IN_PERCENT / (zoomFactor * 100.0)) * zoomFactor);
+        viewHeight = (int) ((parentMinSize * H_CAPTURED_VIEW_IN_PERCENT / (zoomFactor * 100.0)) * zoomFactor);
+        viewContainerHeight = viewHeight + toolbarHeight +
+                2 * containterSize; // Top and bottom shadows
+        viewContainerWidth = viewWidth +
+                2 * containterSize; // Right and left shadows
         // Display in zoomedview is corrupted when width is an odd number
         // More details about this issue here: bug 776906 comment 11
         viewWidth &= ~0x1;
     }
 
     private void shouldBlockUpdate(boolean shouldBlockUpdate) {
         stopUpdateView = shouldBlockUpdate;
     }
@@ -338,23 +452,41 @@ public class ZoomedView extends FrameLay
         ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable);
         if (layerView != null) {
             layerView.setOnMetricsChangedZoomedViewportListener(null);
             layerView.removeZoomedViewListener(this);
             layerView = null;
         }
     }
 
+    private void changeZoomFactor() {
+        if (currentZoomFactorIndex < ZOOM_FACTORS_LIST.length - 1) {
+            currentZoomFactorIndex++;
+        } else {
+            currentZoomFactorIndex = 0;
+        }
+        zoomFactor = ZOOM_FACTORS_LIST[currentZoomFactorIndex];
+
+        ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
+        refreshZoomedViewSize(metrics);
+        setTextInZoomFactorButton(zoomFactor);
+    }
+
+    private void setTextInZoomFactorButton(float zoom) {
+        final String percentageValue = Integer.toString((int) (100*zoom));
+        changeZoomFactorButton.setText(getResources().getString(R.string.percent, percentageValue));
+    }
+
     @Override
     public void handleMessage(final String event, final JSONObject message) {
         ThreadUtils.postToUiThread(new Runnable() {
             @Override
             public void run() {
                 try {
-                    if (event.equals("Gesture:nothingDoneOnLongPress") || event.equals("Gesture:clusteredLinksClicked")) {
+                    if (event.equals("Gesture:clusteredLinksClicked")) {
                         final JSONObject clickPosition = message.getJSONObject("clickPosition");
                         int left = clickPosition.getInt("x");
                         int top = clickPosition.getInt("y");
                         // Start to display inside the zoomedView
                         LayerView geckoAppLayerView = GeckoAppShell.getLayerView();
                         if (geckoAppLayerView != null) {
                             startZoomDisplay(geckoAppLayerView, left, top);
                         }
@@ -368,16 +500,20 @@ public class ZoomedView extends FrameLay
                     Log.e(LOGTAG, "JSON exception", e);
                 }
             }
         });
     }
 
     private void moveUsingGeckoPosition(int leftFromGecko, int topFromGecko) {
         ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
+        final float parentHeight = metrics.getHeight();
+        // moveToolbar is called before getZoomedViewTopLeftPositionFromTouchPosition in order to
+        // correctly center vertically the zoomed area
+        moveToolbar((topFromGecko * metrics.zoomFactor > parentHeight / 2));
         PointF convertedPosition = getZoomedViewTopLeftPositionFromTouchPosition((leftFromGecko * metrics.zoomFactor),
                 (topFromGecko * metrics.zoomFactor));
         moveZoomedView(metrics, convertedPosition.x, convertedPosition.y);
     }
 
     @Override
     public void onMetricsChanged(final ImmutableViewportMetrics viewport) {
         // It can be called from a Gecko thread (forceViewportMetrics in GeckoLayerClient).
@@ -401,18 +537,18 @@ public class ZoomedView extends FrameLay
         final Bitmap sb3 = Bitmap.createBitmap(viewWidth, viewHeight, getBitmapConfig());
         if (sb3 != null) {
             data.rewind();
             try {
                 sb3.copyPixelsFromBuffer(data);
             } catch (Exception iae) {
                 Log.w(LOGTAG, iae.toString());
             }
-            BitmapDrawable ob3 = new BitmapDrawable(getResources(), sb3);
             if (zoomedImageView != null) {
+                RoundedBitmapDrawable ob3 = new RoundedBitmapDrawable(getResources(), sb3, toolbarOnTop, cornerRadius);
                 zoomedImageView.setImageDrawable(ob3);
             }
         }
         if (shouldSetVisibleOnUpdate) {
             this.setVisibility(View.VISIBLE);
             shouldSetVisibleOnUpdate = false;
         }
         lastStartTimeReRender = startTimeReRender;
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -609,16 +609,24 @@ just addresses the organization to follo
 <!-- Miscellaneous -->
 <!-- LOCALIZATION NOTE (ellipsis): This text is appended to a piece of text that does not fit in the
      designated space. Use the unicode ellipsis char, \u2026, or use "..." if \u2026 doesn't suit
      traditions in your locale. -->
 <!ENTITY ellipsis "…">
 
 <!ENTITY colon ":">
 
+<!-- LOCALIZATION NOTE (percent): The percent sign is appended after a number to
+     display a percentage value. formatS is the number, #37 is the code to display a percent sign.
+     This format string is typically used by getString method, in such method the percent sign
+     is a reserved caracter. In order to display one percent sign in the result of getString,
+     double percent signs must be inserted in the format string.
+     This entity is used in the zoomed view to display the zoom factor-->
+<!ENTITY percent "&formatS;&#37;&#37;">
+
 <!-- These are only used for accessibility for the done and overflow-menu buttons in the actionbar.
      They are never shown to users -->
 <!ENTITY actionbar_menu "Menu">
 <!ENTITY actionbar_done "Done">
 
 <!-- Voice search in the awesome bar -->
 <!ENTITY voicesearch_prompt "Speak now">
 <!ENTITY voicesearch_failed_title "&brandShortName; Voice Search">
--- a/mobile/android/base/resources/layout/zoomed_view.xml
+++ b/mobile/android/base/resources/layout/zoomed_view.xml
@@ -5,29 +5,42 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/.
 -->
 
 <org.mozilla.gecko.ZoomedView xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:gecko="http://schemas.android.com/apk/res-auto"
     android:id="@+id/zoomed_view_container"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:layout_alignParentLeft="true"
-    android:layout_alignParentTop="true"
-    android:background="@android:color/white"
-    android:visibility="gone" >
+    android:background="@drawable/dropshadow"
+    android:padding="@dimen/drawable_dropshadow_size"
+    android:visibility="gone">
 
-
-    <ImageView
-        android:id="@+id/zoomed_image_view"
+    <RelativeLayout
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:background="#000000"
-        android:padding="1dip" />
-
-    <ImageView
-        android:id="@+id/dialog_close"
-        android:background="@drawable/close"
-        android:layout_height="20dp"
-        android:layout_width="20dp"
-        android:layout_gravity ="top|right" />
+        android:layout_alignParentLeft="true"
+        android:layout_alignParentTop="true"
+        android:background="@drawable/toolbar_grey_round">
+        <TextView
+            android:id="@+id/change_zoom_factor"
+            android:layout_width="wrap_content"
+            android:layout_height="@dimen/zoomed_view_toolbar_height"
+            android:background="@android:color/transparent"
+            android:padding="12dip"
+            android:layout_alignLeft="@+id/zoomed_image_view"
+            android:textSize="16sp"
+            android:textColor="@color/text_and_tabs_tray_grey"/>
+        <ImageView
+            android:id="@+id/dialog_close"
+            android:scaleType="center"
+            android:layout_width="@dimen/zoomed_view_toolbar_height"
+            android:layout_height="@dimen/zoomed_view_toolbar_height"
+            android:layout_alignRight="@id/zoomed_image_view"
+            android:src="@drawable/close_edit_mode_selector"/>
+        <ImageView
+            android:id="@id/zoomed_image_view"
+            android:layout_below="@id/change_zoom_factor"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
+    </RelativeLayout>
 
 </org.mozilla.gecko.ZoomedView>
\ No newline at end of file
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -200,16 +200,20 @@
     <dimen name="tab_history_title_fading_width">50dp</dimen>
     <dimen name="tab_history_title_margin_right">15dp</dimen>
     <dimen name="tab_history_title_text_size">14sp</dimen>
     <dimen name="tab_history_bg_width">2dp</dimen>
     <dimen name="tab_history_border_padding">2dp</dimen>
 
     <dimen name="horizontal_drag_area">256dp</dimen>
 
+    <!-- ZoomedView dimensions. -->
+    <dimen name="zoomed_view_toolbar_height">44dp</dimen>
+    <dimen name="drawable_dropshadow_size">3dp</dimen>
+
     <!-- Find-In-Page dialog dimensions. -->
     <dimen name="find_in_page_text_margin_left">5dip</dimen>
     <dimen name="find_in_page_text_margin_right">12dip</dimen>
     <dimen name="find_in_page_text_padding_left">10dip</dimen>
     <dimen name="find_in_page_text_padding_right">10dip</dimen>
     <dimen name="find_in_page_status_margin_right">10dip</dimen>
     <dimen name="find_in_page_matchcase_padding">10dip</dimen>
     <dimen name="find_in_page_control_margin_top">2dip</dimen>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -523,10 +523,12 @@
   <string name="voicesearch_failed_message_recoverable">&voicesearch_failed_message_recoverable;</string>
   <string name="voicesearch_failed_retry">&voicesearch_failed_retry;</string>
 
   <!-- Miscellaneous -->
   <string name="ellipsis">&ellipsis;</string>
 
   <string name="colon">&colon;</string>
 
+  <string name="percent">&percent;</string>
+
   <string name="remote_tabs_last_synced">&remote_tabs_last_synced;</string>
 </resources>
--- a/toolkit/devtools/server/actors/root.js
+++ b/toolkit/devtools/server/actors/root.js
@@ -152,16 +152,19 @@ RootActor.prototype = {
     // that returns the font faces used on a node
     getUsedFontFaces: true,
     // Trait added in Gecko 38, indicating that all features necessary for
     // grabbing allocations from the MemoryActor are available for the performance tool
     memoryActorAllocations: true,
     // Added in Gecko 40, indicating that the backend isn't stupid about
     // sending resumption packets on tab navigation.
     noNeedToFakeResumptionOnNavigation: true,
+    // Added in Firefox 40. Indicates that the backend supports registering custom
+    // commands through the WebConsoleCommands API.
+    webConsoleCommands: true,
     // Whether root actor exposes tab actors
     // if allowChromeProcess is true, you can fetch a ChromeActor instance
     // to debug chrome and any non-content ressource via getProcess request
     // if allocChromeProcess is defined, but not true, it means that root actor
     // no longer expose tab actors, but also that getProcess forbids
     // exposing actors for security reasons
     get allowChromeProcess() {
       return DebuggerServer.allowChromeProcess;
--- a/toolkit/devtools/server/actors/webbrowser.js
+++ b/toolkit/devtools/server/actors/webbrowser.js
@@ -627,28 +627,113 @@ BrowserTabList.prototype.onCloseWindow =
       }
     }
   }, "BrowserTabList.prototype.onCloseWindow's delayed body"), 0);
 }, "BrowserTabList.prototype.onCloseWindow");
 
 exports.BrowserTabList = BrowserTabList;
 
 /**
- * Creates a tab actor for handling requests to a browser tab, like
- * attaching and detaching. TabActor respects the actor factories
- * registered with DebuggerServer.addTabActor.
+ * Creates a TabActor whose main goal is to manage lifetime and
+ * expose the tab actors being registered via DebuggerServer.registerModule.
+ * But also track the lifetime of the document being tracked.
+ *
+ * ### Main requests:
+ *
+ * `attach`/`detach` requests:
+ *  - start/stop document watching:
+ *    Starts watching for new documents and emits `tabNavigated` and
+ *    `frameUpdate` over RDP.
+ *  - retrieve the thread actor:
+ *    Instantiates a ThreadActor that can be later attached to in order to
+ *    debug JS sources in the document.
+ * `switchToFrame`:
+ *  Change the targeted document of the whole TabActor, and its child tab actors
+ *  to an iframe or back to its original document.
+ *
+ * Most of the TabActor properties (like `chromeEventHandler` or `docShells`)
+ * are meant to be used by the various child tab actors.
+ *
+ * ### RDP events:
+ *
+ *  - `tabNavigated`:
+ *    Sent when the tab is about to navigate or has just navigated to
+ *    a different document.
+ *    This event contains the following attributes:
+ *     * url (string) The new URI being loaded.
+ *     * nativeConsoleAPI (boolean) `false` if the console API of the page has been
+ *                                          overridden (e.g. by Firebug),
+ *                                  `true`  if the Gecko implementation is used.
+ *     * state (string) `start` if we just start requesting the new URL,
+ *                      `stop`  if the new URL is done loading.
+ *     * isFrameSwitching (boolean) Indicates the event is dispatched when
+ *                                  switching the TabActor context to
+ *                                  a different frame. When we switch to
+ *                                  an iframe, there is no document load.
+ *                                  The targeted document is most likely
+ *                                  going to be already done loading.
+ *     * title (string) The document title being loaded.
+ *                      (sent only on state=stop)
+ *
+ *  - `frameUpdate`:
+ *    Sent when there was a change in the child frames contained in the document
+ *    or when the tab's context was switched to another frame.
+ *    This event can have four different forms depending on the type of incident:
+ *    * One or many frames are updated:
+ *      { frames: [{ id, url, title, parentID }, ...] }
+ *    * One frame got destroyed:
+ *      { frames: [{ id, destroy: true }]}
+ *    * All frames got destroyed:
+ *      { destroyAll: true }
+ *    * We switched the context of the TabActor to a specific frame:
+ *      { selected: #id }
+ *
+ * ### Internal, non-rdp events:
+ * Various events are also dispatched on the TabActor itself that are not
+ * related to RDP, so, not sent to the client. They all relate to the documents
+ * tracked by the TabActor (its main targeted document, but also any of its iframes).
+ *  - will-navigate
+ *    This event fires once navigation starts.
+ *    All pending user prompts are dealt with,
+ *    but it is fired before the first request starts.
+ *  - navigate
+ *    This event is fired once the document's readyState is "complete".
+ *  - window-ready
+ *    This event is fired on three distinct scenarios:
+ *     * When a new Window object is crafted, equivalent of `DOMWindowCreated`.
+ *       It is dispatched before any page script is executed.
+ *     * We will have already received a window-ready event for this window
+ *       when it was created, but we received a window-destroyed event when
+ *       it was frozen into the bfcache, and now the user navigated back to
+ *       this page, so it's now live again and we should resume handling it.
+ *     * For each existing document, when an `attach` request is received.
+ *       At this point scripts in the page will be already loaded.
+ *  - window-destroyed
+ *    This event is fired in two cases:
+ *     * When the window object is destroyed, i.e. when the related document
+ *       is garbage collected. This can happen when the tab is closed or the
+ *       iframe is removed from the DOM.
+ *       It is equivalent of `inner-window-destroyed` event.
+ *     * When the page goes into the bfcache and gets frozen.
+ *       The equivalent of `pagehide`.
+ *  - changed-toplevel-document
+ *    This event fires when we switch the TabActor targeted document
+ *    to one of its iframes, or back to its original top document.
+ *    It is dispatched between window-destroyed and window-ready.
+ *
+ * Note that *all* these events are dispatched in the following order
+ * when we switch the context of the TabActor to a given iframe:
+ *   will-navigate, window-destroyed, changed-toplevel-document, window-ready, navigate
  *
  * This class is subclassed by BrowserTabActor and
  * ContentActor. Subclasses are expected to implement a getter
- * the docShell properties.
+ * for the docShell property.
  *
  * @param aConnection DebuggerServerConnection
  *        The conection to the client.
- * @param aChromeEventHandler
- *        An object on which listen for DOMWindowCreated and pageshow events.
  */
 function TabActor(aConnection)
 {
   this.conn = aConnection;
   this._tabActorPool = null;
   // A map of actor names to actor instances provided by extensions.
   this._extraActors = {};
   this._exited = false;
--- a/toolkit/devtools/webconsole/test/test_commands_registration.html
+++ b/toolkit/devtools/webconsole/test/test_commands_registration.html
@@ -20,16 +20,21 @@ let tests;
 
 let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 let {WebConsoleCommands} = devtools.require("devtools/toolkit/webconsole/utils");
 
 function evaluateJS(input) {
   return new Promise((resolve) => gState.client.evaluateJS(input, resolve));
 }
 
+function* evaluateJSAndCheckResult(input, result) {
+  let response = yield evaluateJS(input);
+  checkObject(response, {result});
+}
+
 function startTest()
 {
   removeEventListener("load", startTest);
 
   attachConsole(["PageError"], onAttach, true);
 }
 
 function onAttach(aState, aResponse)
@@ -133,16 +138,44 @@ tests = [
       input: command,
       result: ">o_/"
     });
     is(document.getElementById("quack").textContent, ">o_/",
         "#foo textContent should equal to \">o_/\"");
     WebConsoleCommands.unregister("$foo");
     ok(!WebConsoleCommands.hasCommand("$foo"), "$foo should be unregistered");
     nextTest();
+  }),
+
+  Task.async(function* unregisterAfterOverridingTwice() {
+    WebConsoleCommands.register("keys", (owner, obj) => "command 1");
+    info("checking the value of the first override");
+    yield evaluateJSAndCheckResult("keys('foo');", "command 1");
+
+    let orig = WebConsoleCommands.getCommand("keys");
+    WebConsoleCommands.register("keys", (owner, obj) => {
+      if (obj === "quack")
+        return "bang!";
+      return orig(owner, obj);
+    });
+
+    info("checking the values after the second override");
+    yield evaluateJSAndCheckResult("keys({});", "command 1");
+    yield evaluateJSAndCheckResult("keys('quack');", "bang!");
+
+    WebConsoleCommands.unregister("keys");
+
+    info("checking the value after unregistration (should restore " +
+      "the original command)");
+    yield evaluateJSAndCheckResult("keys({});", {
+      class: "Array",
+      preview: {items: []}
+    });
+    nextTest();
+
   })
 ];
 
 function testEnd()
 {
   // If this is the first run, reload the page and do it again.
   // Otherwise, end the test.
   delete top.foo;
--- a/toolkit/devtools/webconsole/utils.js
+++ b/toolkit/devtools/webconsole/utils.js
@@ -1524,16 +1524,29 @@ ConsoleAPIListener.prototype =
  * WebConsole commands manager.
  *
  * Defines a set of functions /variables ("commands") that are available from
  * the Web Console but not from the web page.
  *
  */
 let WebConsoleCommands = {
   _registeredCommands: new Map(),
+  _originalCommands: new Map(),
+
+  /**
+   * @private
+   * Reserved for built-in commands. To register a command from the code of an
+   * add-on, see WebConsoleCommands.register instead.
+   *
+   * @see WebConsoleCommands.register
+   */
+  _registerOriginal: function (name, command) {
+    this.register(name, command);
+    this._originalCommands.set(name, this.getCommand(name));
+  },
 
   /**
    * Register a new command.
    * @param {string} name The command name (exemple: "$")
    * @param {(function|object)} command The command to register.
    *  It can be a function so the command is a function (like "$()"),
    *  or it can also be a property descriptor to describe a getter / value (like
    *  "$0").
@@ -1559,20 +1572,26 @@ let WebConsoleCommands = {
    */
   register: function(name, command) {
     this._registeredCommands.set(name, command);
   },
 
   /**
    * Unregister a command.
    *
+   * If the command being unregister overrode a built-in command,
+   * the latter is restored.
+   *
    * @param {string} name The name of the command
    */
   unregister: function(name) {
     this._registeredCommands.delete(name);
+    if (this._originalCommands.has(name)) {
+      this.register(name, this._originalCommands.get(name));
+    }
   },
 
   /**
    * Returns a command by its name.
    *
    * @param {string} name The name of the command.
    *
    * @return {(function|object)} The command.
@@ -1606,57 +1625,57 @@ exports.WebConsoleCommands = WebConsoleC
 /**
  * Find a node by ID.
  *
  * @param string aId
  *        The ID of the element you want.
  * @return nsIDOMNode or null
  *         The result of calling document.querySelector(aSelector).
  */
-WebConsoleCommands.register("$", function JSTH_$(aOwner, aSelector)
+WebConsoleCommands._registerOriginal("$", function JSTH_$(aOwner, aSelector)
 {
   return aOwner.window.document.querySelector(aSelector);
 });
 
 /**
  * Find the nodes matching a CSS selector.
  *
  * @param string aSelector
  *        A string that is passed to window.document.querySelectorAll.
  * @return nsIDOMNodeList
  *         Returns the result of document.querySelectorAll(aSelector).
  */
-WebConsoleCommands.register("$$", function JSTH_$$(aOwner, aSelector)
+WebConsoleCommands._registerOriginal("$$", function JSTH_$$(aOwner, aSelector)
 {
   return aOwner.window.document.querySelectorAll(aSelector);
 });
 
 /**
  * Returns the result of the last console input evaluation
  *
  * @return object|undefined
  * Returns last console evaluation or undefined
  */
-WebConsoleCommands.register("$_", {
+WebConsoleCommands._registerOriginal("$_", {
   get: function(aOwner) {
     return aOwner.consoleActor.getLastConsoleInputEvaluation();
   }
 });
 
 
 /**
  * Runs an xPath query and returns all matched nodes.
  *
  * @param string aXPath
  *        xPath search query to execute.
  * @param [optional] nsIDOMNode aContext
  *        Context to run the xPath query on. Uses window.document if not set.
  * @return array of nsIDOMNode
  */
-WebConsoleCommands.register("$x", function JSTH_$x(aOwner, aXPath, aContext)
+WebConsoleCommands._registerOriginal("$x", function JSTH_$x(aOwner, aXPath, aContext)
 {
   let nodes = new aOwner.window.wrappedJSObject.Array();
   let doc = aOwner.window.document;
   aContext = aContext || doc;
 
   let results = doc.evaluate(aXPath, aContext, null,
                              Ci.nsIDOMXPathResult.ANY_TYPE, null);
   let node;
@@ -1668,93 +1687,93 @@ WebConsoleCommands.register("$x", functi
 });
 
 /**
  * Returns the currently selected object in the highlighter.
  *
  * @return Object representing the current selection in the
  *         Inspector, or null if no selection exists.
  */
-WebConsoleCommands.register("$0", {
+WebConsoleCommands._registerOriginal("$0", {
   get: function(aOwner) {
     return aOwner.makeDebuggeeValue(aOwner.selectedNode);
   }
 });
 
 /**
  * Clears the output of the WebConsole.
  */
-WebConsoleCommands.register("clear", function JSTH_clear(aOwner)
+WebConsoleCommands._registerOriginal("clear", function JSTH_clear(aOwner)
 {
   aOwner.helperResult = {
     type: "clearOutput",
   };
 });
 
 /**
  * Clears the input history of the WebConsole.
  */
-WebConsoleCommands.register("clearHistory", function JSTH_clearHistory(aOwner)
+WebConsoleCommands._registerOriginal("clearHistory", function JSTH_clearHistory(aOwner)
 {
   aOwner.helperResult = {
     type: "clearHistory",
   };
 });
 
 /**
  * Returns the result of Object.keys(aObject).
  *
  * @param object aObject
  *        Object to return the property names from.
  * @return array of strings
  */
-WebConsoleCommands.register("keys", function JSTH_keys(aOwner, aObject)
+WebConsoleCommands._registerOriginal("keys", function JSTH_keys(aOwner, aObject)
 {
   return aOwner.window.wrappedJSObject.Object.keys(WebConsoleUtils.unwrap(aObject));
 });
 
 /**
  * Returns the values of all properties on aObject.
  *
  * @param object aObject
  *        Object to display the values from.
  * @return array of string
  */
-WebConsoleCommands.register("values", function JSTH_values(aOwner, aObject)
+WebConsoleCommands._registerOriginal("values", function JSTH_values(aOwner, aObject)
 {
   let arrValues = new aOwner.window.wrappedJSObject.Array();
   let obj = WebConsoleUtils.unwrap(aObject);
 
   for (let prop in obj) {
     arrValues.push(obj[prop]);
   }
 
   return arrValues;
 });
 
 /**
  * Opens a help window in MDN.
  */
-WebConsoleCommands.register("help", function JSTH_help(aOwner)
+WebConsoleCommands._registerOriginal("help", function JSTH_help(aOwner)
 {
   aOwner.helperResult = { type: "help" };
 });
 
 /**
  * Change the JS evaluation scope.
  *
  * @param DOMElement|string|window aWindow
  *        The window object to use for eval scope. This can be a string that
  *        is used to perform document.querySelector(), to find the iframe that
  *        you want to cd() to. A DOMElement can be given as well, the
  *        .contentWindow property is used. Lastly, you can directly pass
  *        a window object. If you call cd() with no arguments, the current
  *        eval scope is cleared back to its default (the top window).
  */
-WebConsoleCommands.register("cd", function JSTH_cd(aOwner, aWindow)
+WebConsoleCommands._registerOriginal("cd", function JSTH_cd(aOwner, aWindow)
 {
   if (!aWindow) {
     aOwner.consoleActor.evalWindow = null;
     aOwner.helperResult = { type: "cd" };
     return;
   }
 
   if (typeof aWindow == "string") {
@@ -1773,17 +1792,17 @@ WebConsoleCommands.register("cd", functi
 });
 
 /**
  * Inspects the passed aObject. This is done by opening the PropertyPanel.
  *
  * @param object aObject
  *        Object to inspect.
  */
-WebConsoleCommands.register("inspect", function JSTH_inspect(aOwner, aObject)
+WebConsoleCommands._registerOriginal("inspect", function JSTH_inspect(aOwner, aObject)
 {
   let dbgObj = aOwner.makeDebuggeeValue(aObject);
   let grip = aOwner.createValueGrip(dbgObj);
   aOwner.helperResult = {
     type: "inspectObject",
     input: aOwner.evalInput,
     object: grip,
   };
@@ -1791,17 +1810,17 @@ WebConsoleCommands.register("inspect", f
 
 /**
  * Prints aObject to the output.
  *
  * @param object aObject
  *        Object to print to the output.
  * @return string
  */
-WebConsoleCommands.register("pprint", function JSTH_pprint(aOwner, aObject)
+WebConsoleCommands._registerOriginal("pprint", function JSTH_pprint(aOwner, aObject)
 {
   if (aObject === null || aObject === undefined || aObject === true ||
       aObject === false) {
     aOwner.helperResult = {
       type: "error",
       message: "helperFuncUnsupportedTypeError",
     };
     return null;
@@ -1838,17 +1857,17 @@ WebConsoleCommands.register("pprint", fu
 
 /**
  * Print the String representation of a value to the output, as-is.
  *
  * @param any aValue
  *        A value you want to output as a string.
  * @return void
  */
-WebConsoleCommands.register("print", function JSTH_print(aOwner, aValue)
+WebConsoleCommands._registerOriginal("print", function JSTH_print(aOwner, aValue)
 {
   aOwner.helperResult = { rawOutput: true };
   if (typeof aValue === "symbol") {
     return Symbol.prototype.toString.call(aValue);
   }
   // Waiving Xrays here allows us to see a closer representation of the
   // underlying object. This may execute arbitrary content code, but that
   // code will run with content privileges, and the result will be rendered
@@ -1858,17 +1877,17 @@ WebConsoleCommands.register("print", fun
 
 /**
  * Copy the String representation of a value to the clipboard.
  *
  * @param any aValue
  *        A value you want to copy as a string.
  * @return void
  */
-WebConsoleCommands.register("copy", function JSTH_copy(aOwner, aValue)
+WebConsoleCommands._registerOriginal("copy", function JSTH_copy(aOwner, aValue)
 {
   let payload;
   try {
     if (aValue instanceof Ci.nsIDOMElement) {
       payload = aValue.outerHTML;
     } else if (typeof aValue == "string") {
       payload = aValue;
     } else {