Bug 625465 - Items in list view should launch description view with a single click on an add-on entry. r=dtownsend, a=beltzner
☠☠ backed out by f523472a47e6 ☠ ☠
authorBlair McBride <bmcbride@mozilla.com>
Mon, 07 Feb 2011 16:53:53 +1300
changeset 62837 8c2aa133200aaa0fc7c312e5631f49da08bc9b60
parent 62836 a63e2fb788992138dfd919574a96513c8745cc96
child 62838 447f9ec41036b53d280e9f577ad867af0e05b24a
push id1
push userroot
push dateTue, 10 Dec 2013 15:46:25 +0000
reviewersdtownsend, beltzner
bugs625465
milestone2.0b12pre
Bug 625465 - Items in list view should launch description view with a single click on an add-on entry. r=dtownsend, a=beltzner
toolkit/mozapps/extensions/content/extensions.css
toolkit/mozapps/extensions/content/extensions.xml
toolkit/mozapps/extensions/test/browser/Makefile.in
toolkit/mozapps/extensions/test/browser/browser_bug562854.js
toolkit/mozapps/extensions/test/browser/browser_bug591465.js
toolkit/mozapps/extensions/test/browser/browser_bug625465.js
toolkit/mozapps/extensions/test/browser/browser_list.js
toolkit/themes/gnomestripe/mozapps/extensions/extensions.css
toolkit/themes/pinstripe/mozapps/extensions/detail-btn.png
toolkit/themes/pinstripe/mozapps/extensions/extensions.css
toolkit/themes/pinstripe/mozapps/jar.mn
toolkit/themes/winstripe/mozapps/extensions/detail-btn.png
toolkit/themes/winstripe/mozapps/extensions/extensions.css
toolkit/themes/winstripe/mozapps/jar.mn
--- a/toolkit/mozapps/extensions/content/extensions.css
+++ b/toolkit/mozapps/extensions/content/extensions.css
@@ -49,16 +49,20 @@ xhtml|link {
 .category {
   -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#category");
 }
 
 .sort-controls {
   -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#sorters");
 }
 
+.list {
+  -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#addons-richlistbox");  
+}
+
 .addon[status="installed"] {
   -moz-box-orient: vertical;
   -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#addon-generic");
 }
 
 .addon[status="installing"] {
   -moz-box-orient: vertical;
   -moz-binding: url("chrome://mozapps/content/extensions/extensions.xml#addon-installing");
--- a/toolkit/mozapps/extensions/content/extensions.xml
+++ b/toolkit/mozapps/extensions/content/extensions.xml
@@ -516,16 +516,71 @@
           this._creatorLink.hidden = !showLink;
           this._creatorName.hidden = showLink;
         ]]></body>
       </method>
     </implementation>
   </binding>
 
 
+  <binding id="addons-richlistbox" extends="chrome://global/content/bindings/richlistbox.xml#richlistbox">
+    <implementation>
+      <field name="mLastMoveTime">null</field>
+      <field name="mLastMousePos">null</field>
+    </implementation>
+    <handlers>
+      <!-- Automatically select the item under the mouse cursor -->
+      <handler event="mousemove"><![CDATA[
+        // Save latest mouse position incase we want to scroll and keep the item
+        // under the mouse selected (the scroll event doesn't include this information).
+        this.mLastMousePos = { x: event.clientX,
+                               y: event.clientY };
+
+        // Updating every mousemove can kill performance, so make sure
+        // at least 30ms has passed.
+        var now = Date.now();
+
+        if ((!this.mLastMoveTime) || (now - this.mLastMoveTime > 30)) {
+          var item = event.target;
+
+          while (item && item.localName != "richlistitem")
+            item = item == this ? null : item.parentNode;
+
+          if (!item)
+            return;
+
+          var index = this.getIndexOfItem(item);
+          if (index != this.selectedIndex)
+            this.selectedIndex = index;
+
+          this.mLastMoveTime = now;
+        }
+      ]]></handler>
+      <!-- Automatically select the item under the mouse cursor when scrolling -->
+      <handler event="scroll"><![CDATA[
+        if (!this.mLastMousePos)
+          return;
+
+        var item = document.elementFromPoint(this.mLastMousePos.x,
+                                             this.mLastMousePos.y);
+
+        while (item && item.localName != "richlistitem")
+          item = item == this ? null : item.parentNode;
+
+        if (!item)
+          return;
+
+        var index = this.getIndexOfItem(item);
+        if (index != this.selectedIndex)
+          this.selectedIndex = index;
+      ]]></handler>
+    </handlers>
+  </binding>
+
+
   <!-- Install status - Displays the status of an install/upgrade. -->
   <binding id="install-status">
     <content>
       <xul:label anonid="message"/>
       <xul:progressmeter anonid="progress" class="download-progress"/>
       <xul:button anonid="purchase-remote-btn" hidden="true"
                   class="addon-control"
                   oncommand="document.getBindingParent(this).purchaseRemote();"/>
@@ -840,18 +895,17 @@
             <xul:label anonid="date-updated" class="date-updated"
                        unknown="&addon.unknownDate;"/>
           </xul:hbox>
 
           <xul:hbox flex="1" align="end">
             <xul:vbox flex="1">
               <xul:hbox align="center" class="description-container">
                 <xul:label flex="1" anonid="description" class="description" crop="end"/>
-                <xul:button anonid="details-btn" class="details button-link"
-                            label="&addon.details.label;"
+                <xul:button anonid="details-btn" class="details"
                             tooltiptext="&addon.details.tooltip;"
                             oncommand="document.getBindingParent(this).showInDetailView();"/>
                 <xul:spacer flex="5000"/> <!-- Necessary to make the description crop -->
               </xul:hbox>
               <xul:vbox anonid="relnotes-container" class="relnotes-container">
                 <xul:label class="relnotes-header" value="&addon.releaseNotes.label;"/>
                 <xul:label anonid="relnotes-loading" value="&addon.loadingReleaseNotes.label;"/>
                 <xul:label anonid="relnotes-error" hidden="true"
@@ -1622,30 +1676,43 @@
         <body><![CDATA[
             this._updateState();
         ]]></body>
       </method>
     </implementation>
 
     <handlers>
       <handler event="click" button="0"><![CDATA[
-        switch (event.detail) {
-        case 1:
-          // Prevent double-click where the UI changes on the first click
-          this._lastClickTarget = event.originalTarget;
-          break;
-        case 2:
-          if (event.originalTarget.localName != 'button' &&
-              !event.originalTarget.classList.contains('text-link') &&
-              event.originalTarget == this._lastClickTarget) {
-            this.showInDetailView();
-          }
-          break;
+       if (event.originalTarget.localName != "button" &&
+           event.originalTarget.localName != "checkbox" &&
+           !event.originalTarget.classList.contains("text-link")) {
+          this.showInDetailView();
         }
       ]]></handler>
+      <!-- CSS's :active pseudo-class will match if a button is clicked,
+           so we need to track it manually via mousedown. -->
+      <handler event="mousedown" button="0"><![CDATA[
+       if (event.originalTarget.localName != "button" &&
+           event.originalTarget.localName != "checkbox" &&
+           !event.originalTarget.classList.contains("text-link")) {
+          this.setAttribute("mousedown", true);
+        }
+      ]]></handler>
+      <handler event="mouseup" button="0"><![CDATA[
+        this.removeAttribute("mousedown");
+      ]]></handler>
+      <handler event="mouseout"><![CDATA[
+        var el = event.relatedTarget;
+        while (el != null && el != this.parentNode) {
+          if (el == this)
+            return;
+          el = el.parentNode;
+        }
+        this.removeAttribute("mousedown");
+      ]]></handler>
     </handlers>
   </binding>
 
 
   <!-- Addon - uninstalled - An uninstalled addon that can be re-installed. -->
   <binding id="addon-uninstalled"
            extends="chrome://mozapps/content/extensions/extensions.xml#addon-base">
     <content>
--- a/toolkit/mozapps/extensions/test/browser/Makefile.in
+++ b/toolkit/mozapps/extensions/test/browser/Makefile.in
@@ -65,16 +65,17 @@ include $(DEPTH)/config/autoconf.mk
   browser_bug587970.js \
   browser_bug591465.js \
   browser_bug591663.js \
   browser_bug593535.js \
   browser_bug596336.js \
   browser_bug608316.js \
   browser_bug610764.js \
   browser_bug618502.js \
+  browser_bug625465.js \
   browser_details.js \
   browser_discovery.js \
   browser_dragdrop.js \
   browser_list.js \
   browser_searching.js \
   browser_sorting.js \
   browser_uninstalling.js \
   browser_install.js \
--- a/toolkit/mozapps/extensions/test/browser/browser_bug562854.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug562854.js
@@ -1,14 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 /**
- * Tests that double-click does not go to detail view if the target is a link or button.
+ * Tests that clicking does not go to detail view if the target is a link or button,
+ * but clicking anywhere else does.
  */
 
 function test() {
   requestLongerTimeout(2);
 
   waitForExplicitFinish();
 
   var gProvider = new MockProvider();
@@ -35,26 +36,25 @@ function is_in_list(aManager, view) {
 
 function is_in_detail(aManager, view) {
   var doc = aManager.document;
 
   is(doc.getElementById("categories").selectedItem.value, view, "Should be on the right category");
   is(doc.getElementById("view-port").selectedPanel.id, "detail-view", "Should be on the right view");
 }
 
-// Check that double-click does something.
+// Check that clicking on the addon item does something.
 add_test(function() {
   open_manager("addons://list/extension", function(aManager) {
     info("Part 1");
     is_in_list(aManager, "addons://list/extension");
 
     var addon = get_addon_element(aManager, "test1@tests.mozilla.org");
     addon.parentNode.ensureElementIsVisible(addon);
-    EventUtils.synthesizeMouseAtCenter(addon, { clickCount: 1 }, aManager);
-    EventUtils.synthesizeMouseAtCenter(addon, { clickCount: 2 }, aManager);
+    EventUtils.synthesizeMouseAtCenter(addon, { }, aManager);
 
     wait_for_view_load(aManager, function(aManager) {
       info("Part 2");
       is_in_detail(aManager, "addons://list/extension");
 
       close_manager(aManager, run_next_test);
     });
   });
@@ -105,17 +105,16 @@ add_test(function() {
 
     // The undo button is removed when clicked on.
     // We need to wait for the UI to catch up.
     setTimeout(function() {
       var target = aManager.document.getAnonymousElementByAttribute(addon, "anonid", "undo-btn");
       var rect = target.getBoundingClientRect();
       var addonRect = addon.getBoundingClientRect();
 
-      EventUtils.synthesizeMouse(target, rect.width / 2, rect.height / 2, { clickCount: 1 }, aManager);
       EventUtils.synthesizeMouse(addon,
         rect.left - addonRect.left + rect.width / 2,
         rect.top - addonRect.top + rect.height / 2,
         { clickCount: 2 },
         aManager
       );
 
       wait_for_view_load(aManager, function(aManager) {
--- a/toolkit/mozapps/extensions/test/browser/browser_bug591465.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug591465.js
@@ -123,17 +123,17 @@ add_test(function() {
 
     check_contextmenu(false, true, false, false, false);
 
     gContextMenu.hidePopup();
     run_next_test();
   }, false);
 
   info("Opening context menu on enabled extension item");
-  EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+  el.parentNode.selectedItem = el;
   EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
 });
 
 add_test(function() {
   var el = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
   isnot(el, null, "Should have found addon element");
   el.mAddon.userDisabled = true;
 
@@ -142,17 +142,17 @@ add_test(function() {
 
     check_contextmenu(false, false, false, false, false);
 
     gContextMenu.hidePopup();
     run_next_test();
   }, false);
 
   info("Opening context menu on newly disabled extension item");
-  EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+  el.parentNode.selectedItem = el;
   EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
 });
 
 add_test(function() {
   var el = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
   isnot(el, null, "Should have found addon element");
   el.mAddon.userDisabled = false;
 
@@ -161,34 +161,34 @@ add_test(function() {
 
     check_contextmenu(false, true, false, false, false);
 
     gContextMenu.hidePopup();
     run_next_test();
   }, false);
 
   info("Opening context menu on newly enabled extension item");
-  EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+  el.parentNode.selectedItem = el;
   EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
 });
 
 add_test(function() {
   var el = get_addon_element(gManagerWindow, "addon2@tests.mozilla.org");
 
   gContextMenu.addEventListener("popupshown", function() {
     gContextMenu.removeEventListener("popupshown", arguments.callee, false);
 
     check_contextmenu(false, false, false, false, false);
 
     gContextMenu.hidePopup();
     run_next_test();
   }, false);
 
   info("Opening context menu on disabled extension item");
-  EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+  el.parentNode.selectedItem = el;
   EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
 });
 
 
 add_test(function() {
   gManagerWindow.loadView("addons://list/theme");
   wait_for_view_load(gManagerWindow, function() {
     var el = get_addon_element(gManagerWindow, "theme1@tests.mozilla.org");
@@ -198,17 +198,17 @@ add_test(function() {
 
     check_contextmenu(true, true, false, false, false);
 
       gContextMenu.hidePopup();
       run_next_test();
     }, false);
 
     info("Opening context menu on enabled theme item");
-    EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+    el.parentNode.selectedItem = el;
     EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
   });
 });
 
 
 add_test(function() {
   var el = get_addon_element(gManagerWindow, "theme2@tests.mozilla.org");
 
@@ -217,17 +217,17 @@ add_test(function() {
 
     check_contextmenu(true, false, false, false, false);
 
     gContextMenu.hidePopup();
     run_next_test();
   }, false);
 
   info("Opening context menu on disabled theme item");
-  EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+  el.parentNode.selectedItem = el;
   EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
 });
 
 
 add_test(function() {
   gManagerWindow.loadView("addons://detail/addon1@tests.mozilla.org");
   wait_for_view_load(gManagerWindow, function() {
 
@@ -237,17 +237,17 @@ add_test(function() {
       check_contextmenu(false, true, false, true, false);
 
       gContextMenu.hidePopup();
       run_next_test();
     }, false);
 
     info("Opening context menu on enabled extension, in detail view");
     var el = gManagerWindow.document.querySelector("#detail-view .detail-view-container");
-    EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+    el.parentNode.selectedItem = el;
     EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
   });
 });
 
 
 add_test(function() {
   gManagerWindow.loadView("addons://detail/addon2@tests.mozilla.org");
   wait_for_view_load(gManagerWindow, function() {
@@ -258,17 +258,17 @@ add_test(function() {
       check_contextmenu(false, false, false, true, false);
 
       gContextMenu.hidePopup();
       run_next_test();
     }, false);
 
     info("Opening context menu on disabled extension, in detail view");
     var el = gManagerWindow.document.querySelector("#detail-view .detail-view-container");
-    EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+    el.parentNode.selectedItem = el;
     EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
   });
 });
 
 
 add_test(function() {
   gManagerWindow.loadView("addons://detail/theme1@tests.mozilla.org");
   wait_for_view_load(gManagerWindow, function() {
@@ -279,17 +279,17 @@ add_test(function() {
       check_contextmenu(true, true, false, true, false);
 
       gContextMenu.hidePopup();
       run_next_test();
     }, false);
 
     info("Opening context menu on enabled theme, in detail view");
     var el = gManagerWindow.document.querySelector("#detail-view .detail-view-container");
-    EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+    el.parentNode.selectedItem = el;
     EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
   });
 });
 
 
 add_test(function() {
   gManagerWindow.loadView("addons://detail/theme2@tests.mozilla.org");
   wait_for_view_load(gManagerWindow, function() {
@@ -300,17 +300,17 @@ add_test(function() {
       check_contextmenu(true, false, false, true, false);
 
       gContextMenu.hidePopup();
       run_next_test();
     }, false);
 
     info("Opening context menu on disabled theme, in detail view");
     var el = gManagerWindow.document.querySelector("#detail-view .detail-view-container");
-    EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+    el.parentNode.selectedItem = el;
     EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
   });
 });
 
 add_test(function() {
   gManagerWindow.loadView("addons://detail/theme3@tests.mozilla.org");
   wait_for_view_load(gManagerWindow, function() {
 
@@ -320,17 +320,17 @@ add_test(function() {
       check_contextmenu(true, true, false, true, true);
 
       gContextMenu.hidePopup();
       run_next_test();
     }, false);
 
     info("Opening context menu with single menu item on enabled theme, in detail view");
     var el = gManagerWindow.document.querySelector("#detail-view .detail-view-container");
-    EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+    el.parentNode.selectedItem = el;
     EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
   });
 });
 
 add_test(function() {
   info("Searching for remote addons");
 
   Services.prefs.setCharPref(PREF_GETADDONS_GETSEARCHRESULTS, SEARCH_URL);
@@ -354,17 +354,17 @@ add_test(function() {
 
         check_contextmenu(false, false, true, false, false);
 
         gContextMenu.hidePopup();
         run_next_test();
       }, false);
 
       info("Opening context menu on remote extension item");
-      EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+      el.parentNode.selectedItem = el;
       EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
 
     });
   });
 });
 
 
 add_test(function() {
@@ -384,12 +384,12 @@ add_test(function() {
         aInstalls[0].cancel();
 
         run_next_test();
       });
     }, false);
 
     info("Opening context menu on remote extension, in detail view");
     var el = gManagerWindow.document.querySelector("#detail-view .detail-view-container");
-    EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+    el.parentNode.selectedItem = el;
     EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
   });
 });
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug625465.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Bug 625465 - Items in list view should launch description view with a single click on an add-on entry
+
+var gManagerWindow;
+var gList;
+
+function test() {
+  waitForExplicitFinish();
+
+  var gProvider = new MockProvider();
+  for (let i = 1; i <= 30; i++) {
+    gProvider.createAddons([{
+      id: "test" + i + "@tests.mozilla.org",
+      name: "Test add-on " + i,
+      description: "foo"
+    }]);
+  }
+
+  open_manager("addons://list/extension", function(aManager) {
+    gManagerWindow = aManager;
+    gList = gManagerWindow.document.getElementById("addon-list");
+
+    run_next_test();
+  });
+}
+
+function end_test() {
+  close_manager(gManagerWindow, finish);
+}
+
+
+function get_item_content_pos(aItem) {
+  return {
+    x: (aItem.boxObject.x - gList.boxObject.x) + 10,
+    y: (aItem.boxObject.y - gList.boxObject.y - gList._scrollbox.scrollTop) + 10
+  };
+}
+
+function get_item_index(aItem) {
+  for (let i = 0; i < gList.childElementCount; i++) {
+    if (gList.childNodes[i] == aItem)
+      return i;
+  }
+  return -1;
+}
+
+function mouseover_item(aItemNum, aCallback) {
+  var item = get_addon_element(gManagerWindow, "test" + aItemNum + "@tests.mozilla.org");
+  var itemIndex = get_item_index(item);
+  gList.ensureElementIsVisible(item);
+  if (aItemNum == 1)
+    is(gList.selectedIndex, -1, "Should not initially have an item selected");
+
+  info("Moving mouse over item: " + item.value);
+  var pos = get_item_content_pos(item);
+  EventUtils.synthesizeMouse(gList, pos.x, pos.y, {type: "mousemove"}, gManagerWindow);
+  executeSoon(function() {
+    is(gManagerWindow.gViewController.currentViewId, "addons://list/extension", "Should still be in list view");
+    is(gList.selectedIndex, itemIndex, "Correct item should be selected");
+  
+    aCallback();
+  });
+}
+
+add_test(function() {
+  var i = 1;
+  function test_next_item() {
+    if (i < 10) {
+      mouseover_item(i, function() {
+        i++
+        test_next_item();
+      });
+    } else {
+      run_next_test();
+    }
+  }
+  test_next_item();
+});
+
+
+add_test(function() {
+  gList.selectedIndex = 1;
+  var item = gList.selectedItem;
+  gList.ensureElementIsVisible(item);
+  var pos = get_item_content_pos(item);
+  EventUtils.synthesizeMouse(gList, pos.x, pos.y, {type: "mousemove"}, gManagerWindow);
+  executeSoon(function() {
+    var scrollDelta = item.boxObject.height * 2;
+    info("Scrolling by " + scrollDelta + "px (2 items)");
+    EventUtils.synthesizeMouseScroll(gList, pos.x, pos.y, {type: "MozMousePixelScroll", delta: scrollDelta}, gManagerWindow);
+    setTimeout(function() {
+      var item = gList.selectedItem;
+      var itemIndex = get_item_index(item);
+      is(itemIndex, 3, "Correct item should be selected");
+      run_next_test();
+    }, 100);
+  });
+});
--- a/toolkit/mozapps/extensions/test/browser/browser_list.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_list.js
@@ -589,17 +589,18 @@ add_test(function() {
   Services.prefs.setBoolPref("accessibility.tabfocus_applies_to_xul", false);
 
   let items = get_test_items();
 
   var fm = Cc["@mozilla.org/focus-manager;1"].
            getService(Ci.nsIFocusManager);
 
   let addon = items["Test add-on 6"];
-  EventUtils.synthesizeMouseAtCenter(addon, { }, gManagerWindow);
+  addon.parentNode.selectedItem = addon;
+  addon.focus();
   is(fm.focusedElement, addon.parentNode, "Focus should have moved to the list");
 
   EventUtils.synthesizeKey("VK_TAB", { }, gManagerWindow);
   is(fm.focusedElement, get_node(addon, "details-btn"), "Focus should have moved to the more button");
 
   EventUtils.synthesizeKey("VK_TAB", { }, gManagerWindow);
   is(fm.focusedElement, get_node(addon, "disable-btn"), "Focus should have moved to the disable button");
 
--- a/toolkit/themes/gnomestripe/mozapps/extensions/extensions.css
+++ b/toolkit/themes/gnomestripe/mozapps/extensions/extensions.css
@@ -409,19 +409,25 @@
 }
 
 .addon[selected] .text-link,
 .addon[selected] .button-link {
   color: inherit;
 }
 
 .details {
+  -moz-appearance: none;
+  border: none;
   cursor: pointer;
+  padding: 0;
   margin: 0;
-  -moz-margin-start: 10px;
+  background: transparent;
+  min-width: 13px;
+  -moz-margin-start: 6px;
+  list-style-image: url("moz-icon://stock/gtk-go-forward?size=16");
 }
 
 .icon-container {
   width: 48px;
   height: 48px;
   margin: 3px 7px;
 }
 
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..2a422483b10835cad161e5cde4a6f081b85bf380
GIT binary patch
literal 1152
zc$@)%1b_R9P)<h;3K|Lk000e1NJLTq001Wd000dL1^@s6DwKC+00001b5ch_0Itp)
z=>Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02y>eSaefwW^{L9
za%BKeVQFr3E>1;MAa*k@H7+*LgRNQs00ZzzL_t(YOSM&9NSjv}{;;B(h$xCEnP~l&
zxh9=9$r5Y3X{#|!OHH%+JG+>zYqX=r*(pZTRGpLv3smM5WeTFh8$oaMMg`FeQBV*;
zK~NC9RzXoO-1CI*V;_>%i*132@0|CX=e+NE-}gKbgnxbk7+wc-fKdd;v~kVf?;4D1
z;3?1|f@9ja_MdP*!YOyUyStxnZf?5H&(D7&IHoN!GBQ6aD=XxZr!dTatgo-{Tv%A>
zIy*b_5**VO8XEc{At52$AE&0K#xprN`EGZ2_YZ<&+Wc{tl{=%Oqcs;77oET_<KyFR
z@cmANeQ<EF8jcDV7Z*PkoiQuVJ3Bku-^X#eT+XPdD96Oagmft%r?s{9?cUy=cYAx=
z-P6<KSzB9sL2PVnG&md%_a%;S-Q)2vkS3tKudmOMk&!Wjja`_VB{ViRnw?JPT`@Sd
z-EMbX#VIK%sjIB4v|t|-ptjj;?Z{O7>gwt<x7+PZNlBTvTCLqnOG{5Bm`tWuGLE2M
zUthQ5fQP_+qPDiSEiW(c_qMh+2H>;J&dwHQXJ;3Q!C7Z!W?o&z86F;fl9-rShkXk0
ztYc$itq7uGetzCUAc!uVPWLAg@m8{RbaYh7ID+x?^z<7Xpp&Dyxw*k$FucKmKhS=B
ze5?-+4mOFwwP0Ry73c8qFeflDkQcnk$FjV<Y@42*Hpqbryw1(d^?1Er2kmHsA{j?8
z!jT3CWXOT^+K?&d+}zyvuz#_?zyE1SNXP>zxPyZO?NuD~)n`FLK`q#q%i!R+0KKh~
z1A)EF%*_0CWMrfc)i9tBZ_7AB&d|`%4IGdp0jajf#>NidR^P%_24wa3_h+h9Dygj1
zXn1D&gB)aEi-*Lr-bQfMRaI350|Nuu5|m11Lv(cXa9LSd&DPe|v#F`6ugc5I@5wkq
zqF%3O7Cs@cF%Qx)R9aeEg*lr!hjy)2t3eOm7c-|#OH1S02RX^f$xrYIDzNW!@U-Zq
zN|f&gk)NOcI6OT3FQd^|3(2xVf*#NwA0J;K;|KxNF|}H)#34}xG$$Y5N5C!4xt5xm
zT6A)9BE8mY(4?P|l9CGF$KhH`Ow7+mM@L`s5)@@};^N|po0^(Z3A5S!7!p!}$99i%
zu3h2?0tJ>jrcfwU#l^+UMko+xu~=YIs2JGJ&JLa2l0=mPOHGQ1h%h0m>{lNLRY;?a
zPF7Y{3d9Tg{$V6eSXh_^XKGNT_}<=LWqNwLnl^tNpFavLwCt`%qhVk1RhlRrEG080
zIti95J_Q!#{~G0c3Hxh;GQhHk8Dr-EHOJ=@2L*_Iq{5d05jRDf-~ZSDlk+#Uqi9uk
S@a5|O0000<MNUMnLSTZ42Nz8M
--- a/toolkit/themes/pinstripe/mozapps/extensions/extensions.css
+++ b/toolkit/themes/pinstripe/mozapps/extensions/extensions.css
@@ -435,19 +435,34 @@
 
 .addon {
   border-bottom: 1px solid #B6B1B9;
   padding: 5px;
   color: #373D48;
 }
 
 .details {
+  -moz-appearance: none;
+  border: none;
   cursor: pointer;
+  padding: 3px 0 0 0;
   margin: 0;
-  -moz-margin-start: 10px;
+  background: transparent;
+  min-width: 13px;
+  -moz-margin-start: 6px;
+  list-style-image: url("chrome://mozapps/skin/extensions/detail-btn.png");
+  -moz-image-region: rect(0px, 13px, 13px, 0px);
+}
+
+.details:hover {
+  -moz-image-region: rect(0px, 26px, 13px, 13px);
+}
+
+.details:active {
+  -moz-image-region: rect(0px, 39px, 13px, 26px);
 }
 
 .icon-container {
   width: 48px;
   height: 48px;
   margin: 3px 7px;
 }
 
@@ -622,28 +637,33 @@
 .addon-view[pending="uninstall"] {
   background-image: url("chrome://mozapps/skin/extensions/stripes-info-negative.png"),
                     -moz-linear-gradient(rgba(128, 128, 128, 0.04),
                                          rgba(128, 128, 128, 0));
   background-repeat: repeat-x;
 }
 
 .addon[selected] {
-  background-color: rgba(105, 125, 149, 0.39);
+  background-color: rgba(255, 255, 255, 0.45);
   color: black;
 }
 
 .addon[selected] .name-container {
   text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.7);
 }
 
 .addon[active="false"][selected] .name-container {
   color: #3F3F3F;
 }
 
+.addon[mousedown] {
+  background-color: rgba(255, 255, 255, 0.3);
+  box-shadow: inset 1px 1px 4px rgba(0, 0, 0, 0.15);
+}
+
 
 /*** search view ***/
 
 #search-filter {
   padding: 5px 20px;
   font-size: 120%;
   overflow-x: hidden;
   border-bottom: 1px solid rgba(50, 65, 92, 0.4);
--- a/toolkit/themes/pinstripe/mozapps/jar.mn
+++ b/toolkit/themes/pinstripe/mozapps/jar.mn
@@ -30,16 +30,17 @@ toolkit.jar:
   skin/classic/mozapps/extensions/stripes-error.png               (extensions/stripes-error.png)
   skin/classic/mozapps/extensions/stripes-info-positive.png       (extensions/stripes-info-positive.png)
   skin/classic/mozapps/extensions/stripes-info-negative.png       (extensions/stripes-info-negative.png)
   skin/classic/mozapps/extensions/alerticon-warning.png           (extensions/alerticon-warning.png)
   skin/classic/mozapps/extensions/alerticon-error.png             (extensions/alerticon-error.png)
   skin/classic/mozapps/extensions/alerticon-info-positive.png     (extensions/alerticon-info-positive.png)
   skin/classic/mozapps/extensions/alerticon-info-negative.png     (extensions/alerticon-info-negative.png)
   skin/classic/mozapps/extensions/background-texture.png          (extensions/background-texture.png)
+  skin/classic/mozapps/extensions/detail-btn.png                  (extensions/detail-btn.png)
   skin/classic/mozapps/extensions/about.css                       (extensions/about.css)
 * skin/classic/mozapps/extensions/extensions.css                  (extensions/extensions.css)
   skin/classic/mozapps/extensions/extensions.svg                  (extensions/extensions.svg)
   skin/classic/mozapps/extensions/update.css                      (extensions/update.css)
   skin/classic/mozapps/extensions/eula.css                        (extensions/eula.css)
   skin/classic/mozapps/extensions/blocklist.css                   (extensions/blocklist.css)
   skin/classic/mozapps/passwordmgr/key.png                        (passwordmgr/key.png)
   skin/classic/mozapps/passwordmgr/key-16.png                     (passwordmgr/key-16.png)
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..2a422483b10835cad161e5cde4a6f081b85bf380
GIT binary patch
literal 1152
zc$@)%1b_R9P)<h;3K|Lk000e1NJLTq001Wd000dL1^@s6DwKC+00001b5ch_0Itp)
z=>Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02y>eSaefwW^{L9
za%BKeVQFr3E>1;MAa*k@H7+*LgRNQs00ZzzL_t(YOSM&9NSjv}{;;B(h$xCEnP~l&
zxh9=9$r5Y3X{#|!OHH%+JG+>zYqX=r*(pZTRGpLv3smM5WeTFh8$oaMMg`FeQBV*;
zK~NC9RzXoO-1CI*V;_>%i*132@0|CX=e+NE-}gKbgnxbk7+wc-fKdd;v~kVf?;4D1
z;3?1|f@9ja_MdP*!YOyUyStxnZf?5H&(D7&IHoN!GBQ6aD=XxZr!dTatgo-{Tv%A>
zIy*b_5**VO8XEc{At52$AE&0K#xprN`EGZ2_YZ<&+Wc{tl{=%Oqcs;77oET_<KyFR
z@cmANeQ<EF8jcDV7Z*PkoiQuVJ3Bku-^X#eT+XPdD96Oagmft%r?s{9?cUy=cYAx=
z-P6<KSzB9sL2PVnG&md%_a%;S-Q)2vkS3tKudmOMk&!Wjja`_VB{ViRnw?JPT`@Sd
z-EMbX#VIK%sjIB4v|t|-ptjj;?Z{O7>gwt<x7+PZNlBTvTCLqnOG{5Bm`tWuGLE2M
zUthQ5fQP_+qPDiSEiW(c_qMh+2H>;J&dwHQXJ;3Q!C7Z!W?o&z86F;fl9-rShkXk0
ztYc$itq7uGetzCUAc!uVPWLAg@m8{RbaYh7ID+x?^z<7Xpp&Dyxw*k$FucKmKhS=B
ze5?-+4mOFwwP0Ry73c8qFeflDkQcnk$FjV<Y@42*Hpqbryw1(d^?1Er2kmHsA{j?8
z!jT3CWXOT^+K?&d+}zyvuz#_?zyE1SNXP>zxPyZO?NuD~)n`FLK`q#q%i!R+0KKh~
z1A)EF%*_0CWMrfc)i9tBZ_7AB&d|`%4IGdp0jajf#>NidR^P%_24wa3_h+h9Dygj1
zXn1D&gB)aEi-*Lr-bQfMRaI350|Nuu5|m11Lv(cXa9LSd&DPe|v#F`6ugc5I@5wkq
zqF%3O7Cs@cF%Qx)R9aeEg*lr!hjy)2t3eOm7c-|#OH1S02RX^f$xrYIDzNW!@U-Zq
zN|f&gk)NOcI6OT3FQd^|3(2xVf*#NwA0J;K;|KxNF|}H)#34}xG$$Y5N5C!4xt5xm
zT6A)9BE8mY(4?P|l9CGF$KhH`Ow7+mM@L`s5)@@};^N|po0^(Z3A5S!7!p!}$99i%
zu3h2?0tJ>jrcfwU#l^+UMko+xu~=YIs2JGJ&JLa2l0=mPOHGQ1h%h0m>{lNLRY;?a
zPF7Y{3d9Tg{$V6eSXh_^XKGNT_}<=LWqNwLnl^tNpFavLwCt`%qhVk1RhlRrEG080
zIti95J_Q!#{~G0c3Hxh;GQhHk8Dr-EHOJ=@2L*_Iq{5d05jRDf-~ZSDlk+#Uqi9uk
S@a5|O0000<MNUMnLSTZ42Nz8M
--- a/toolkit/themes/winstripe/mozapps/extensions/extensions.css
+++ b/toolkit/themes/winstripe/mozapps/extensions/extensions.css
@@ -508,19 +508,38 @@
 
 .view-pane:not(#search-view) .addon:last-of-type,
 #search-view .addon[last] {
   border-bottom-width: 2px;
   -moz-border-bottom-colors: rgba(0, 0, 0, 0.1) rgba(255, 255, 255, 0.1);
 }
 
 .details {
+  -moz-appearance: none;
+  border: none;
   cursor: pointer;
+  padding: 3px 0 0 0;
   margin: 0;
-  -moz-margin-start: 10px;
+  background: transparent;
+  min-width: 13px;
+  -moz-margin-start: 6px;
+  list-style-image: url("chrome://mozapps/skin/extensions/detail-btn.png");
+  -moz-image-region: rect(0px, 13px, 13px, 0px);
+}
+
+.details:hover {
+  -moz-image-region: rect(0px, 26px, 13px, 13px);
+}
+
+.details:active {
+  -moz-image-region: rect(0px, 39px, 13px, 26px);
+}
+
+.details:-moz-focusring > .button-box {
+  border-color: transparent;
 }
 
 .icon-container {
   width: 48px;
   height: 48px;
   margin: 3px 7px;
 }
 
@@ -674,28 +693,47 @@
 .addon-view[pending="uninstall"] {
   background-image: url("chrome://mozapps/skin/extensions/stripes-info-negative.png"),
                     -moz-linear-gradient(rgba(128, 128, 128, 0.04),
                                          rgba(128, 128, 128, 0));
   background-repeat: repeat-x;
 }
 
 .addon[selected] {
-  background-color: rgba(148, 172, 204, 0.39);
+  background-color: rgba(255, 255, 255, 0.65);
   color: black;
 }
 
 .addon[selected] .name-container {
   text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.7);  
 }
 
 .addon[active="false"][selected] .name-container {
   color: #3F3F3F;
 }
 
+.addon[mousedown] {
+  background-color: rgba(255, 255, 255, 0.5);
+  box-shadow: inset 1px 1px 4px rgba(0, 0, 0, 0.20);
+  border-top-width: 1px;
+  border-bottom-width: 0px;
+  padding-top: 6px;
+  padding-bottom: 6px;
+}
+
+.view-pane:not(#search-view) .addon[mousedown]:first-of-type,
+#search-view .addon[first][mousedown] {
+  border-top-width: 0px;
+}
+
+.view-pane:not(#search-view) .addon[mousedown]:last-of-type,
+#search-view .addon[last][mousedown] {
+  border-bottom-width: 1px;
+}
+
 
 /*** item - uninstalled ***/
 
 .addon[status="uninstalled"] {
   border: none;
 }
 
 .addon[status="uninstalled"] > .container {
--- a/toolkit/themes/winstripe/mozapps/jar.mn
+++ b/toolkit/themes/winstripe/mozapps/jar.mn
@@ -37,16 +37,17 @@ toolkit.jar:
         skin/classic/mozapps/extensions/stripes-error.png          (extensions/stripes-error.png)
         skin/classic/mozapps/extensions/stripes-info-positive.png  (extensions/stripes-info-positive.png)
         skin/classic/mozapps/extensions/stripes-info-negative.png  (extensions/stripes-info-negative.png)
         skin/classic/mozapps/extensions/alerticon-warning.png      (extensions/alerticon-warning.png)
         skin/classic/mozapps/extensions/alerticon-error.png        (extensions/alerticon-error.png)
         skin/classic/mozapps/extensions/alerticon-info-positive.png (extensions/alerticon-info-positive.png)
         skin/classic/mozapps/extensions/alerticon-info-negative.png (extensions/alerticon-info-negative.png)
         skin/classic/mozapps/extensions/background-texture.png     (extensions/background-texture.png)
+        skin/classic/mozapps/extensions/detail-btn.png             (extensions/detail-btn.png)
         skin/classic/mozapps/extensions/eula.css                   (extensions/eula.css)
         skin/classic/mozapps/handling/handling.css                 (handling/handling.css)
         skin/classic/mozapps/passwordmgr/key.png                   (passwordmgr/key.png)
         skin/classic/mozapps/passwordmgr/key-16.png                (passwordmgr/key-16.png)
         skin/classic/mozapps/passwordmgr/key-64.png                (passwordmgr/key-64.png)
 #ifdef MOZ_PLACES
         skin/classic/mozapps/places/defaultFavicon.png             (places/defaultFavicon.png)
         skin/classic/mozapps/places/tagContainerIcon.png           (places/tagContainerIcon.png)
@@ -112,16 +113,17 @@ toolkit.jar:
         skin/classic/aero/mozapps/extensions/stripes-error.png             (extensions/stripes-error.png)
         skin/classic/aero/mozapps/extensions/stripes-info-positive.png     (extensions/stripes-info-positive.png)
         skin/classic/aero/mozapps/extensions/stripes-info-negative.png     (extensions/stripes-info-negative.png)
         skin/classic/aero/mozapps/extensions/alerticon-warning.png         (extensions/alerticon-warning.png)
         skin/classic/aero/mozapps/extensions/alerticon-error.png           (extensions/alerticon-error.png)
         skin/classic/aero/mozapps/extensions/alerticon-info-positive.png   (extensions/alerticon-info-positive.png)
         skin/classic/aero/mozapps/extensions/alerticon-info-negative.png   (extensions/alerticon-info-negative.png)
         skin/classic/aero/mozapps/extensions/background-texture.png        (extensions/background-texture.png)
+        skin/classic/aero/mozapps/extensions/detail-btn.png                     (extensions/detail-btn.png)
         skin/classic/aero/mozapps/extensions/eula.css                      (extensions/eula.css)
         skin/classic/aero/mozapps/handling/handling.css                    (handling/handling.css)
         skin/classic/aero/mozapps/passwordmgr/key.png                      (passwordmgr/key-aero.png)
         skin/classic/aero/mozapps/passwordmgr/key-16.png                   (passwordmgr/key-aero-16.png)
         skin/classic/aero/mozapps/passwordmgr/key-64.png                   (passwordmgr/key-aero-64.png)
 #ifdef MOZ_PLACES
         skin/classic/aero/mozapps/places/defaultFavicon.png                (places/defaultFavicon-aero.png)
         skin/classic/aero/mozapps/places/tagContainerIcon.png              (places/tagContainerIcon-aero.png)