Bug 897113 - add noContext/contextmenu toggle for richgrid, disable selection for snapped view and awesomebar results. New selectNone richgrid method. r=rsilveira
authorSam Foster <sfoster@mozilla.com>
Tue, 22 Oct 2013 16:32:42 -0700
changeset 165581 e0598dfe8de11270a3064d84febb6959d6b1a278
parent 165580 8f8964ee7d123c167081ed66fd77f310ee40160b
child 165582 13132285f1c93178515acdfbf134d69a2fa3bcb0
push id3066
push userakeybl@mozilla.com
push dateMon, 09 Dec 2013 19:58:46 +0000
treeherdermozilla-beta@a31a0dce83aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrsilveira
bugs897113
milestone27.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
Bug 897113 - add noContext/contextmenu toggle for richgrid, disable selection for snapped view and awesomebar results. New selectNone richgrid method. r=rsilveira
browser/metro/base/content/bindings/grid.xml
browser/metro/base/content/bindings/urlbar.xml
browser/metro/base/tests/mochitest/browser_snappedState.js
browser/metro/base/tests/mochitest/browser_tilegrid.xul
browser/metro/base/tests/mochitest/browser_tiles.js
browser/metro/modules/View.jsm
--- a/browser/metro/base/content/bindings/grid.xml
+++ b/browser/metro/base/content/bindings/grid.xml
@@ -86,16 +86,28 @@
               this._fireEvent("select");
             } else {
               this._fireEvent("selectionchange");
             }
           ]]>
         </body>
       </method>
 
+      <method name="selectNone">
+        <body>
+          <![CDATA[
+            let selectedCount = this.selectedItems.length;
+            this.clearSelection();
+            if (selectedCount && "single" != this.getAttribute("seltype")) {
+              this._fireEvent("selectionchange");
+            }
+          ]]>
+        </body>
+      </method>
+
       <method name="handleItemClick">
         <parameter name="aItem"/>
         <parameter name="aEvent"/>
         <body>
           <![CDATA[
             if (!this.isBound)
               return;
 
@@ -111,17 +123,17 @@
         </body>
       </method>
 
       <method name="handleItemContextMenu">
         <parameter name="aItem"/>
         <parameter name="aEvent"/>
         <body>
           <![CDATA[
-            if (!this.isBound || this.suppressOnSelect)
+            if (!this.isBound || this.noContext)
               return;
             // we'll republish this as a selectionchange event on the grid
             aEvent.stopPropagation();
             this.toggleItemSelection(aItem);
           ]]>
         </body>
       </method>
 
@@ -187,17 +199,17 @@
           ]]>
         </getter>
         <setter>
           <![CDATA[
             if (val >= 0) {
               let selected = this.getItemAtIndex(val);
               this.selectItem(selected);
             } else {
-              this.clearSelection();
+              this.selectNone();
             }
           ]]>
         </setter>
       </property>
 
       <method name="appendItem">
         <parameter name="aLabel"/>
         <parameter name="aValue"/>
@@ -605,16 +617,19 @@
           ]]>
         </body>
       </method>
 
       <!-- Inteface to suppress selection events -->
       <property name="suppressOnSelect"
                   onget="return this.getAttribute('suppressonselect') == 'true';"
                   onset="this.setAttribute('suppressonselect', val);"/>
+      <property name="noContext"
+                  onget="return this.hasAttribute('nocontext');"
+                  onset="if (val) this.setAttribute('nocontext', true); else this.removeAttribute('nocontext');"/>
       <property name="crossSlideBoundary"
           onget="return this.hasAttribute('crossslideboundary')? this.getAttribute('crossslideboundary') : Infinity;"/>
 
     <!-- Internal methods -->
       <field name="_xslideHandler"/>
       <constructor>
         <![CDATA[
           // create our quota of item slots
@@ -622,17 +637,17 @@
               count < slotCount; count++) {
             this.appendChild( this._createItemElement() );
           }
 
           if (this.controller && this.controller.gridBoundCallback != undefined)
             this.controller.gridBoundCallback();
 
           // set up cross-slide gesture handling for multiple-selection grids
-          if ("undefined" !== typeof CrossSlide && "multiple" == this.getAttribute("seltype")) {
+          if ("undefined" !== typeof CrossSlide && !this.noContext) {
             this._xslideHandler = new CrossSlide.Handler(this, {
                   REARRANGESTART: this.crossSlideBoundary
             });
           }
 
           // XXX This event was never actually implemented (bug 223411).
           let event = document.createEvent("Events");
           event.initEvent("contentgenerated", true, true);
@@ -831,17 +846,17 @@
       <!--  /item bend effect handler -->
 
       <handler event="context-action">
         <![CDATA[
           // context-action is an event fired by the appbar typically
           // which directs us to do something to the selected tiles
           switch (event.action) {
             case "clear":
-              this.clearSelection();
+              this.selectNone();
               break;
             default:
               if (this.controller && this.controller.doActionOnSelectedTiles) {
                 this.controller.doActionOnSelectedTiles(event.action, event);
               }
           }
         ]]>
       </handler>
@@ -891,17 +906,17 @@
               event.target.removeAttribute('crosssliding');
               event.target.style.removeProperty('transform');
               break;
           }
         ]]>
       </handler>
       <handler event="MozCrossSlideSelect">
         <![CDATA[
-          if (this.suppressOnSelect)
+          if (this.noContext)
             return;
           this.toggleItemSelection(event.target);
         ]]>
       </handler>
     </handlers>
   </binding>
 
   <binding id="richgrid-item">
--- a/browser/metro/base/content/bindings/urlbar.xml
+++ b/browser/metro/base/content/bindings/urlbar.xml
@@ -500,31 +500,30 @@
   </binding>
 
   <binding id="urlbar-autocomplete">
     <content class="meta-section-container">
       <xul:vbox class="meta-section" anonid="results-container" flex="1">
         <xul:label class="meta-section-title"
                    value="&autocompleteResultsHeader.label;"/>
         <richgrid anonid="results" rows="3" flex="1"
-                  seltype="single" deferlayout="true"/>
+                  seltype="single" nocontext="true" deferlayout="true"/>
       </xul:vbox>
 
       <xul:vbox class="meta-section" flex="1">
         <xul:label anonid="searches-header" class="meta-section-title"/>
         <richgrid anonid="searches" rows="3" flex="1"
-                  seltype="single" deferlayout="true"/>
+                  seltype="single" nocontext="true" deferlayout="true"/>
       </xul:vbox>
     </content>
 
     <implementation implements="nsIAutoCompletePopup, nsIObserver">
       <constructor>
         <![CDATA[
           this.hidden = true;
-
           Services.obs.addObserver(this, "browser-search-engine-modified", false);
 
           this._results.controller = this;
           this._searches.controller = this;
         ]]>
       </constructor>
 
       <destructor>
--- a/browser/metro/base/tests/mochitest/browser_snappedState.js
+++ b/browser/metro/base/tests/mochitest/browser_snappedState.js
@@ -109,25 +109,64 @@ gTests.push({
     ok(Browser.selectedBrowser.contentWindow.scrollMaxY !== 0, "Snapped scrolls vertically");
     ok(Browser.selectedBrowser.contentWindow.scrollMaxX === 0, "Snapped does not scroll horizontally");
   },
   tearDown: function() {
     BookmarksTestHelper.restore();
     yield restoreViewstate();
   }
 });
+gTests.push({
+  desc: "Test tile selection is cleared and disabled",
+  setUp: function() {
+    BookmarksTestHelper.setup();
+    HistoryTestHelper.setup();
+    showStartUI();
+  },
+  run: function() {
+    // minimal event mocking to trigger context-click handlers
+    function makeMockEvent(item) {
+      return {
+        stopPropagation: function() {},
+        target: item
+      };
+    }
+    let startWin = Browser.selectedBrowser.contentWindow;
+    // make sure the bookmarks grid is showing
+    startWin.StartUI.onNarrowTitleClick("start-bookmarks");
+    let bookmarksGrid = startWin.document.querySelector("#start-bookmarks-grid");
+    // sanity check
+    ok(bookmarksGrid, "matched bookmarks grid");
+    ok(bookmarksGrid.children[0], "bookmarks grid has items");
+    // select a tile (balancing implementation leakage with test simplicity)
+    let mockEvent = makeMockEvent(bookmarksGrid.children[0]);
+    bookmarksGrid.handleItemContextMenu(bookmarksGrid.children[0], mockEvent);
+    // check tile was selected
+    is(bookmarksGrid.selectedItems.length, 1, "Tile got selected in landscape view");
+    // switch to snapped view
+    yield setSnappedViewstate();
+    is(bookmarksGrid.selectedItems.length, 0, "grid items selection cleared in snapped view");
+    // attempt to select a tile in snapped view
+    mockEvent = makeMockEvent(bookmarksGrid.children[0]);
+    bookmarksGrid.handleItemContextMenu(bookmarksGrid.children[0], mockEvent);
+    is(bookmarksGrid.selectedItems.length, 0, "no grid item selections possible in snapped view");
+  },
+  tearDown: function() {
+    BookmarksTestHelper.restore();
+    HistoryTestHelper.restore();
+    yield restoreViewstate();
+  }
+});
 
 gTests.push({
   desc: "Navbar contextual buttons are not shown in snapped",
   setUp: setUpSnapped,
   run: function() {
     let toolbarContextual = document.getElementById("toolbar-contextual");
-
     let visibility = getComputedStyle(toolbarContextual).getPropertyValue("visibility");
-
     ok(visibility === "collapse" || visibility === "hidden", "Contextual buttons not shown in navbar");
   },
   tearDown: restoreViewstate
 });
 
 gTests.push({
   desc: "Test Portrait titles",
   setUp: setUpPortrait,
--- a/browser/metro/base/tests/mochitest/browser_tilegrid.xul
+++ b/browser/metro/base/tests/mochitest/browser_tilegrid.xul
@@ -3,31 +3,28 @@
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <?xml-stylesheet href="chrome://browser/skin/platform.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/browser.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/tiles.css" type="text/css"?>
-
 <!DOCTYPE window []>
-
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
-
   <vbox id="alayout">
-      <richgrid id="grid_layout" seltype="single" flex="1">
+      <richgrid id="grid_layout" seltype="single" nocontext="true" flex="1">
       </richgrid>
   </vbox>
   <vbox>
       <richgrid id="slots_grid" seltype="single" minSlots="6" flex="1"/>
   </vbox>
   <vbox style="height:600px">
     <hbox>
-      <richgrid id="clearGrid" seltype="single" flex="1" rows="2">
+      <richgrid id="clearGrid" seltype="single" nocontext="true" flex="1" rows="2">
         <richgriditem value="about:blank" id="clearGrid_item1" label="First item"/>
         <richgriditem value="about:blank" id="clearGrid_item2" label="2nd item"/>
         <richgriditem value="about:blank" id="clearGrid_item1" label="First item"/>
       </richgrid>
     </hbox>
     <hbox>
       <richgrid id="emptyGrid" seltype="single" flex="1" rows="2" minSlots="6">
       </richgrid>
--- a/browser/metro/base/tests/mochitest/browser_tiles.js
+++ b/browser/metro/base/tests/mochitest/browser_tiles.js
@@ -350,33 +350,35 @@ gTests.push({
     is(grid.selectedIndex, -1, "selectedIndex reports correctly with nothing selected");
 
     // item selection
     grid.selectItem(grid.items[1]);
     ok(grid.items[1].selected, "Item selected property is truthy after grid.selectItem");
     ok(grid.items[1].getAttribute("selected"), "Item selected attribute is truthy after grid.selectItem");
     ok(grid.selectedItems.length, "There are selectedItems after grid.selectItem");
 
-    // clearSelection
-    grid.selectItem(grid.items[0]);
-    grid.selectItem(grid.items[1]);
-    grid.clearSelection();
-    is(grid.selectedItems.length, 0, "Nothing selected when we clearSelection");
-    is(grid.selectedIndex, -1, "selectedIndex resets after clearSelection");
-
     // select events
     // in seltype=single mode, select is like the default action for the tile
     // (think <a>, not <select multiple>)
     let handler = {
       handleEvent: function(aEvent) {}
     };
     let handlerStub = stubMethod(handler, "handleEvent");
+
+    grid.items[1].selected = true;
+
     doc.defaultView.addEventListener("select", handler, false);
     info("select listener added");
 
+    // clearSelection
+    grid.clearSelection();
+    is(grid.selectedItems.length, 0, "Nothing selected when we clearSelection");
+    is(grid.selectedIndex, -1, "selectedIndex resets after clearSelection");
+    is(handlerStub.callCount, 0, "clearSelection should not fire a selectionchange event");
+
     info("calling selectItem, currently it is:" + grid.items[0].selected);
     // Note: A richgrid in seltype=single mode fires "select" events from selectItem
     grid.selectItem(grid.items[0]);
     info("calling selectItem, now it is:" + grid.items[0].selected);
     yield waitForMs(0);
 
     is(handlerStub.callCount, 1, "select event handler was called when we selected an item");
     is(handlerStub.calledWith[0].type, "select", "handler got a select event");
@@ -407,48 +409,82 @@ gTests.push({
     ok(grid.items[1].selected, "toggleItemSelection sets truthy selected prop on previously-unselected item");
     is(grid.selectedItems.length, 1, "1 item selected when we first toggleItemSelection");
     is(grid.selectedItems[0], grid.items[1], "the right item is selected");
     is(grid.selectedIndex, 1, "selectedIndex is correct");
 
     grid.toggleItemSelection(grid.items[1]);
     is(grid.selectedItems.length, 0, "Nothing selected when we toggleItemSelection again");
 
-    // clearSelection
-    grid.items[0].selected=true;
-    grid.items[1].selected=true;
-    is(grid.selectedItems.length, 2, "Both items are selected before calling clearSelection");
-    grid.clearSelection();
-    is(grid.selectedItems.length, 0, "Nothing selected when we clearSelection");
-    ok(!(grid.items[0].selected || grid.items[1].selected), "selected properties all falsy when we clearSelection");
-
     // selectionchange events
     // in seltype=multiple mode, we track selected state on all items
     // (think <select multiple> not <a>)
     let handler = {
       handleEvent: function(aEvent) {}
     };
     let handlerStub = stubMethod(handler, "handleEvent");
     doc.defaultView.addEventListener("selectionchange", handler, false);
     info("selectionchange listener added");
 
+    // clearSelection
+    grid.items[0].selected=true;
+    grid.items[1].selected=true;
+    is(grid.selectedItems.length, 2, "Both items are selected before calling clearSelection");
+    grid.clearSelection();
+    is(grid.selectedItems.length, 0, "Nothing selected when we clearSelection");
+    ok(!(grid.items[0].selected || grid.items[1].selected), "selected properties all falsy when we clearSelection");
+    is(handlerStub.callCount, 0, "clearSelection should not fire a selectionchange event");
+
     info("calling toggleItemSelection, currently it is:" + grid.items[0].selected);
     // Note: A richgrid in seltype=single mode fires "select" events from selectItem
     grid.toggleItemSelection(grid.items[0]);
     info("/calling toggleItemSelection, now it is:" + grid.items[0].selected);
     yield waitForMs(0);
 
     is(handlerStub.callCount, 1, "selectionchange event handler was called when we selected an item");
     is(handlerStub.calledWith[0].type, "selectionchange", "handler got a selectionchange event");
     is(handlerStub.calledWith[0].target, grid, "select event had the originating grid as the target");
     handlerStub.restore();
     doc.defaultView.removeEventListener("selectionchange", handler, false);
   }
 });
 
+gTests.push({
+  desc: "selectNone",
+  run: function() {
+    let grid = doc.querySelector("#grid-select2");
+
+    is(typeof grid.selectNone, "function", "selectNone is a function on the grid");
+
+    is(grid.itemCount, 2, "2 items initially");
+
+    // selectNone should fire a selectionchange event
+    let handler = {
+      handleEvent: function(aEvent) {}
+    };
+    let handlerStub = stubMethod(handler, "handleEvent");
+    doc.defaultView.addEventListener("selectionchange", handler, false);
+    info("selectionchange listener added");
+
+    grid.items[0].selected=true;
+    grid.items[1].selected=true;
+    is(grid.selectedItems.length, 2, "Both items are selected before calling selectNone");
+    grid.selectNone();
+
+    is(grid.selectedItems.length, 0, "Nothing selected when we selectNone");
+    ok(!(grid.items[0].selected || grid.items[1].selected), "selected properties all falsy when we selectNone");
+
+    is(handlerStub.callCount, 1, "selectionchange event handler was called when we selectNone");
+    is(handlerStub.calledWith[0].type, "selectionchange", "handler got a selectionchange event");
+    is(handlerStub.calledWith[0].target, grid, "selectionchange event had the originating grid as the target");
+    handlerStub.restore();
+    doc.defaultView.removeEventListener("selectionchange", handler, false);
+  }
+});
+
 function gridSlotsSetup() {
     let grid = this.grid = doc.createElement("richgrid");
     grid.setAttribute("minSlots", 6);
     doc.documentElement.appendChild(grid);
     is(grid.ownerDocument, doc, "created grid in the expected document");
 }
 function gridSlotsTearDown() {
     this.grid && this.grid.parentNode.removeChild(this.grid);
--- a/browser/metro/modules/View.jsm
+++ b/browser/metro/modules/View.jsm
@@ -36,29 +36,38 @@ function View(aSet) {
 }
 
 View.prototype = {
   destruct: function () {
     Services.obs.removeObserver(this.viewStateObserver, "metro_viewstate_changed");
   },
 
   _adjustDOMforViewState: function _adjustDOMforViewState(aState) {
-    if (this._set) {
-      if (undefined == aState)
-        aState = this._set.getAttribute("viewstate");
-
-      this._set.setAttribute("suppressonselect", (aState == "snapped"));
-
-      if (aState == "portrait") {
-        this._set.setAttribute("vertical", true);
-      } else {
-        this._set.removeAttribute("vertical");
-      }
-
-      this._set.arrangeItems();
+    let grid = this._set;
+    if (!grid) {
+      return;
+    }
+    if (!aState) {
+      aState = grid.getAttribute("viewstate");
+    }
+    switch (aState) {
+      case "snapped":
+        grid.setAttribute("nocontext", true);
+        grid.selectNone();
+        break;
+      case "portrait":
+        grid.removeAttribute("nocontext");
+        grid.setAttribute("vertical", true);
+        break;
+      default:
+        grid.removeAttribute("nocontext");
+        grid.removeAttribute("vertical");
+    }
+    if ("arrangeItems" in grid) {
+      grid.arrangeItems();
     }
   },
 
   _updateFavicon: function pv__updateFavicon(aItem, aUri) {
     if ("string" == typeof aUri) {
       aUri = makeURI(aUri);
     }
     PlacesUtils.favicons.getFaviconURLForPage(aUri, this._gotIcon.bind(this, aItem));