Bug 891056 - Support "snapped view" in Windows 8.1. a=lsblakk
authorMatt Brubeck <mbrubeck@mozilla.com>
Mon, 07 Oct 2013 08:36:00 -0400
changeset 160572 0b375eef59f9e0ccea4f3608fa4eb58cc44dc4a0
parent 160571 b20e12690dcba81d9d608a5ce59bd31bc5acf072
child 160573 6d32335e66e7af1165a625a0ca9ff20073555672
push id2961
push userlsblakk@mozilla.com
push dateMon, 28 Oct 2013 21:59:28 +0000
treeherdermozilla-beta@73ef4f13486f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslsblakk
bugs891056
milestone26.0a2
Bug 891056 - Support "snapped view" in Windows 8.1. a=lsblakk Roll-up of patches 2-4.
browser/metro/base/content/ContentAreaObserver.js
browser/metro/base/content/browser-ui.js
browser/metro/base/content/browser.xul
browser/metro/base/content/flyoutpanels/FlyoutPanelsUI.js
browser/metro/base/content/startui/BookmarksView.js
browser/metro/base/content/startui/HistoryView.js
browser/metro/base/content/startui/RemoteTabsView.js
browser/metro/base/content/startui/StartUI.js
browser/metro/base/content/startui/TopSitesView.js
browser/metro/base/tests/mochitest/browser_snappedState.js
browser/metro/base/tests/mochitest/head.js
browser/metro/base/tests/mochitest/helpers/ViewStateHelper.js
browser/metro/modules/View.jsm
browser/metro/profile/metro.js
browser/metro/theme/browser.css
--- a/browser/metro/base/content/ContentAreaObserver.js
+++ b/browser/metro/base/content/ContentAreaObserver.js
@@ -72,16 +72,23 @@ var ContentAreaObserver = {
   get isKeyboardOpened() {
     return Services.metro.keyboardVisible;
   },
 
   get isKeyboardTransitioning() {
     return this._deckTransitioning;
   },
 
+  get viewstate() {
+    if (this.width < Services.prefs.getIntPref("browser.ui.snapped.maxWidth")) {
+      return "snapped";
+    }
+    return (this.height > this.width) ? "portrait" : "landscape";
+  },
+
   /*
    * Public apis
    */
 
   init: function init() {
     window.addEventListener("resize", this, false);
 
     // Message manager msgs we listen for
@@ -117,16 +124,18 @@ var ContentAreaObserver = {
     if (newHeight == oldHeight && newWidth == oldWidth)
       return;
 
     this.styles["window-width"].width = newWidth + "px";
     this.styles["window-width"].maxWidth = newWidth + "px";
     this.styles["window-height"].height = newHeight + "px";
     this.styles["window-height"].maxHeight = newHeight + "px";
 
+    this._updateViewState();
+
     this.updateContentArea(newWidth, this._getContentHeightForWindow(newHeight));
     this._disatchBrowserEvent("SizeChanged");
   },
 
   updateContentArea: function cao_updateContentArea (width, height) {
     let oldHeight = parseInt(this.styles["content-height"].height);
     let oldWidth = parseInt(this.styles["content-width"].width);
 
@@ -275,16 +284,25 @@ var ContentAreaObserver = {
         break;
     }
   },
 
   /*
    * Internal helpers
    */
 
+  _updateViewState: function (aState) {
+    let oldViewstate = Elements.windowState.getAttribute("viewstate");
+    let viewstate = aState || this.viewstate;
+    if (viewstate != oldViewstate) {
+      Elements.windowState.setAttribute("viewstate", viewstate);
+      Services.obs.notifyObservers(null, "metro_viewstate_changed", viewstate);
+    }
+  },
+
   _shiftBrowserDeck: function _shiftBrowserDeck(aAmount) {
     if (aAmount == 0) {
       this._deckTransitioning = false;
       this._dispatchWindowEvent("KeyboardChanged", this.isKeyboardOpened);
     }
 
     if (this._shiftAmount == aAmount)
       return;
--- a/browser/metro/base/content/browser-ui.js
+++ b/browser/metro/base/content/browser-ui.js
@@ -97,29 +97,25 @@ var BrowserUI = {
 
     // listening escape to dismiss dialog on VK_ESCAPE
     window.addEventListener("keypress", this, true);
 
     window.addEventListener("MozPrecisePointer", this, true);
     window.addEventListener("MozImprecisePointer", this, true);
 
     Services.prefs.addObserver("browser.cache.disk_cache_ssl", this, false);
-    Services.obs.addObserver(this, "metro_viewstate_changed", false);
 
     // Init core UI modules
     ContextUI.init();
     PanelUI.init();
     FlyoutPanelsUI.init();
     PageThumbs.init();
     SettingsCharm.init();
     NavButtonSlider.init();
 
-    // show the right toolbars, awesomescreen, etc for the os viewstate
-    BrowserUI._adjustDOMforViewState();
-
     // We can delay some initialization until after startup.  We wait until
     // the first page is shown, then dispatch a UIReadyDelayed event.
     messageManager.addMessageListener("pageshow", function onPageShow() {
       if (getBrowser().currentURI.spec == "about:blank")
         return;
 
       messageManager.removeMessageListener("pageshow", onPageShow);
 
@@ -173,16 +169,17 @@ var BrowserUI = {
     }, 3000);
 #endif
   },
 
   uninit: function() {
     messageManager.removeMessageListener("Browser:MozApplicationManifest", OfflineApps);
 
     PanelUI.uninit();
+    FlyoutPanelsUI.uninit();
     Downloads.uninit();
     SettingsCharm.uninit();
     messageManager.removeMessageListener("Content:StateChange", this);
     PageThumbs.uninit();
     this.stopDebugServer();
   },
 
   /************************************
@@ -588,23 +585,16 @@ var BrowserUI = {
               this.stopDebugServer();
             }
             break;
           case debugServerPortChanged:
             this.changeDebugPort(Services.prefs.getIntPref(aData));
             break;
         }
         break;
-      case "metro_viewstate_changed":
-        this._adjustDOMforViewState(aData);
-        if (aData == "snapped") {
-          FlyoutPanelsUI.hide();
-        }
-
-        break;
     }
   },
 
   /*********************************
    * Internal utils
    */
 
   /**
@@ -633,38 +623,16 @@ var BrowserUI = {
     }
     let registry = Cc["@mozilla.org/windows-registry-key;1"].
                    createInstance(Ci.nsIWindowsRegKey);
     pullDesktopControlledPrefType(Ci.nsIPrefBranch.PREF_INT, "setIntPref");
     pullDesktopControlledPrefType(Ci.nsIPrefBranch.PREF_BOOL, "setBoolPref");
     pullDesktopControlledPrefType(Ci.nsIPrefBranch.PREF_STRING, "setCharPref");
   },
 
-  _adjustDOMforViewState: function(aState) {
-    let currViewState = aState;
-    if (!currViewState && Services.metro.immersive) {
-      switch (Services.metro.snappedState) {
-        case Ci.nsIWinMetroUtils.fullScreenLandscape:
-          currViewState = "landscape";
-          break;
-        case Ci.nsIWinMetroUtils.fullScreenPortrait:
-          currViewState = "portrait";
-          break;
-        case Ci.nsIWinMetroUtils.filled:
-          currViewState = "filled";
-          break;
-        case Ci.nsIWinMetroUtils.snapped:
-          currViewState = "snapped";
-          break;
-      }
-    }
-
-    Elements.windowState.setAttribute("viewstate", currViewState);
-  },
-
   _titleChanged: function(aBrowser) {
     let url = this.getDisplayURI(aBrowser);
 
     let tabCaption;
     if (aBrowser.contentTitle) {
       tabCaption = aBrowser.contentTitle;
     } else if (!Util.isURLEmpty(aBrowser.userTypedValue)) {
       tabCaption = aBrowser.userTypedValue;
--- a/browser/metro/base/content/browser.xul
+++ b/browser/metro/base/content/browser.xul
@@ -167,16 +167,17 @@
     <key id="key_selectTab6" oncommand="BrowserUI.selectTabAtIndex(5);" key="6" modifiers="accel"/>
     <key id="key_selectTab7" oncommand="BrowserUI.selectTabAtIndex(6);" key="7" modifiers="accel"/>
     <key id="key_selectTab8" oncommand="BrowserUI.selectTabAtIndex(7);" key="8" modifiers="accel"/>
     <key id="key_selectLastTab" oncommand="BrowserUI.selectTabAtIndex(-1);" key="9" modifiers="accel"/>
   </keyset>
 
   <stack id="stack" flex="1">
     <observes element="bcast_urlbarState" attribute="*"/>
+    <observes element="bcast_windowState" attribute="*"/>
     <!-- Page Area -->
     <vbox id="page">
       <vbox id="tray" class="tray-toolbar" observes="bcast_windowState" >
         <!-- Tabs -->
         <hbox id="tabs-container" observes="bcast_windowState">
           <box id="tabs" flex="1"
                 observes="bcast_preciseInput"
                 onselect="BrowserUI.selectTabAndDismiss(this);"
--- a/browser/metro/base/content/flyoutpanels/FlyoutPanelsUI.js
+++ b/browser/metro/base/content/flyoutpanels/FlyoutPanelsUI.js
@@ -31,16 +31,22 @@ let FlyoutPanelsUI = {
       let [name, script] = aScript;
       XPCOMUtils.defineLazyGetter(FlyoutPanelsUI, name, function() {
         let sandbox = {};
         Services.scriptloader.loadSubScript(script, sandbox);
         sandbox[name].init();
         return sandbox[name];
       });
     });
+
+    Services.obs.addObserver(this, "metro_viewstate_changed", false);
+  },
+
+  uninit: function () {
+    Services.obs.removeObserver(this, "metro_viewstate_changed");
   },
 
   show: function(aToShow) {
     if (!this[aToShow]) {
       throw("FlyoutPanelsUI asked to show '" + aToShow + "' which does not exist");
     }
 
     if (this._currentFlyout) {
@@ -68,16 +74,26 @@ let FlyoutPanelsUI = {
       Services.metro.showSettingsFlyout();
     }
   },
 
   get isVisible() {
     return this._currentFlyout ? true : false;
   },
 
+  observe: function (aSubject, aTopic, aData) {
+    switch (aTopic) {
+      case "metro_viewstate_changed":
+        if (aData == "snapped") {
+          this.hide();
+        }
+        break;
+    }
+  },
+
   dispatchEvent: function(aEvent) {
     if (this._currentFlyout) {
       this._currentFlyout._topmostElement.dispatchEvent(aEvent);
     }
   },
 
   hide: function() {
     if (this._currentFlyout) {
--- a/browser/metro/base/content/startui/BookmarksView.js
+++ b/browser/metro/base/content/startui/BookmarksView.js
@@ -8,34 +8,32 @@
  * Wraps a list/grid control implementing nsIDOMXULSelectControlElement and
  * fills it with the user's bookmarks.
  *
  * @param           aSet    Control implementing nsIDOMXULSelectControlElement.
  * @param {Number}  aLimit  Maximum number of items to show in the view.
  * @param           aRoot   Bookmark root to show in the view.
  */
 function BookmarksView(aSet, aLimit, aRoot, aFilterUnpinned) {
-  this._set = aSet;
-  this._set.controller = this;
+  View.call(this, aSet);
+
   this._inBatch = false; // batch up grid updates to avoid redundant arrangeItems calls
 
   this._limit = aLimit;
   this._filterUnpinned = aFilterUnpinned;
   this._bookmarkService = PlacesUtils.bookmarks;
   this._navHistoryService = gHistSvc;
 
   this._changes = new BookmarkChangeListener(this);
   this._pinHelper = new ItemPinHelper("metro.bookmarks.unpinned");
   this._bookmarkService.addObserver(this._changes, false);
-  Services.obs.addObserver(this, "metro_viewstate_changed", false);
   StartUI.chromeWin.addEventListener('MozAppbarDismissing', this, false);
   StartUI.chromeWin.addEventListener('BookmarksNeedsRefresh', this, false);
   window.addEventListener("TabClose", this, true);
 
-  this._adjustDOMforViewState();
   this.root = aRoot;
 }
 
 BookmarksView.prototype = Util.extend(Object.create(View.prototype), {
   _limit: null,
   _set: null,
   _changes: null,
   _root: null,
@@ -57,21 +55,21 @@ BookmarksView.prototype = Util.extend(Ob
   },
 
   set root(aRoot) {
     this._root = aRoot;
   },
 
   destruct: function bv_destruct() {
     this._bookmarkService.removeObserver(this._changes);
-    Services.obs.removeObserver(this, "metro_viewstate_changed");
     if (StartUI.chromeWin) {
       StartUI.chromeWin.removeEventListener('MozAppbarDismissing', this, false);
       StartUI.chromeWin.removeEventListener('BookmarksNeedsRefresh', this, false);
     }
+    View.prototype.destruct.call(this);
   },
 
   handleItemClick: function bv_handleItemClick(aItem) {
     let url = aItem.getAttribute("value");
     StartUI.goToURI(url);
   },
 
   _getItemForBookmarkId: function bv__getItemForBookmark(aBookmarkId) {
@@ -270,25 +268,16 @@ BookmarksView.prototype = Util.extend(Ob
       default:
         return;
     }
 
     // Send refresh event so all view are in sync.
     this._sendNeedsRefresh();
   },
 
-  // nsIObservers
-  observe: function (aSubject, aTopic, aState) {
-    switch(aTopic) {
-      case "metro_viewstate_changed":
-        this.onViewStateChange(aState);
-        break;
-    }
-  },
-
   handleEvent: function bv_handleEvent(aEvent) {
     switch (aEvent.type){
       case "MozAppbarDismissing":
         // If undo wasn't pressed, time to do definitive actions.
         if (this._toRemove) {
           for (let bookmarkId of this._toRemove) {
             this._bookmarkService.removeItem(bookmarkId);
           }
--- a/browser/metro/base/content/startui/HistoryView.js
+++ b/browser/metro/base/content/startui/HistoryView.js
@@ -1,45 +1,42 @@
 /* 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';
 
 function HistoryView(aSet, aLimit, aFilterUnpinned) {
-  this._set = aSet;
-  this._set.controller = this;
+  View.call(this, aSet);
+
   this._inBatch = 0;
 
   this._limit = aLimit;
   this._filterUnpinned = aFilterUnpinned;
   this._historyService = PlacesUtils.history;
   this._navHistoryService = gHistSvc;
 
   this._pinHelper = new ItemPinHelper("metro.history.unpinned");
   this._historyService.addObserver(this, false);
-  Services.obs.addObserver(this, "metro_viewstate_changed", false);
   StartUI.chromeWin.addEventListener('MozAppbarDismissing', this, false);
   StartUI.chromeWin.addEventListener('HistoryNeedsRefresh', this, false);
   window.addEventListener("TabClose", this, true);
-
-  this._adjustDOMforViewState();
 }
 
 HistoryView.prototype = Util.extend(Object.create(View.prototype), {
   _set: null,
   _toRemove: null,
 
   destruct: function destruct() {
     this._historyService.removeObserver(this);
-    Services.obs.removeObserver(this, "metro_viewstate_changed");
     if (StartUI.chromeWin) {
       StartUI.chromeWin.removeEventListener('MozAppbarDismissing', this, false);
       StartUI.chromeWin.removeEventListener('HistoryNeedsRefresh', this, false);
     }
+    View.prototype.destruct.call(this);
   },
 
   handleItemClick: function tabview_handleItemClick(aItem) {
     let url = aItem.getAttribute("value");
     StartUI.goToURI(url);
   },
 
   populateGrid: function populateGrid(aRefresh) {
@@ -218,25 +215,16 @@ HistoryView.prototype = Util.extend(Obje
       case "TabClose":
         // Flush any pending actions - appbar will call us back
         // before this returns with 'MozAppbarDismissing' above.
         StartUI.chromeWin.ContextUI.dismissContextAppbar();
       break;
     }
   },
 
-  // nsIObservers
-  observe: function (aSubject, aTopic, aState) {
-    switch(aTopic) {
-      case "metro_viewstate_changed":
-        this.onViewStateChange(aState);
-        break;
-    }
-  },
-
   // nsINavHistoryObserver & helpers
 
   onBeginUpdateBatch: function() {
     // Avoid heavy grid redraws while a batch is in process
     this._inBatch++;
   },
 
   onEndUpdateBatch: function() {
--- a/browser/metro/base/content/startui/RemoteTabsView.js
+++ b/browser/metro/base/content/startui/RemoteTabsView.js
@@ -15,50 +15,44 @@ Components.utils.import("resource://serv
  * early during startup.
  *
  * @param    aSet         Control implementing nsIDOMXULSelectControlElement.
  * @param    aSetUIAccess The UI element that should be hidden when Sync is
  *                          disabled. Must sanely support 'hidden' attribute.
  *                          You may only have one UI access point at this time.
  */
 function RemoteTabsView(aSet, aSetUIAccessList) {
-  this._set = aSet;
-  this._set.controller = this;
+  View.call(this, aSet);
+
   this._uiAccessElements = aSetUIAccessList;
 
   // Sync uses special voodoo observers.
   // If you want to change this code, talk to the fx-si team
   Weave.Svc.Obs.add("weave:service:sync:finish", this);
   Weave.Svc.Obs.add("weave:service:start-over", this);
 
-  Services.obs.addObserver(this, "metro_viewstate_changed", false);
-
   if (this.isSyncEnabled() ) {
     this.populateGrid();
   }
   else {
     this.setUIAccessVisible(false);
   }
-  this._adjustDOMforViewState();
 }
 
 RemoteTabsView.prototype = Util.extend(Object.create(View.prototype), {
   _set: null,
   _uiAccessElements: [],
 
   handleItemClick: function tabview_handleItemClick(aItem) {
     let url = aItem.getAttribute("value");
     StartUI.goToURI(url);
   },
 
   observe: function(subject, topic, data) {
     switch (topic) {
-      case "metro_viewstate_changed":
-        this.onViewStateChange(data);
-        break;
       case "weave:service:sync:finish":
         this.populateGrid();
         break;
       case "weave:service:start-over":
         this.setUIAccessVisible(false);
         break;
     }
   },
@@ -97,19 +91,19 @@ RemoteTabsView.prototype = Util.extend(O
 
       }, this);
     }
     this.setUIAccessVisible(show);
     this._set.arrangeItems();
   },
 
   destruct: function destruct() {
-    Services.obs.removeObserver(this, "metro_viewstate_changed");
     Weave.Svc.Obs.remove("weave:engine:sync:finish", this);
     Weave.Svc.Obs.remove("weave:service:logout:start-over", this);
+    View.prototype.destruct.call(this);
   },
 
   isSyncEnabled: function isSyncEnabled() {
     return (Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED);
   }
 
 });
 
--- a/browser/metro/base/content/startui/StartUI.js
+++ b/browser/metro/base/content/startui/StartUI.js
@@ -21,17 +21,17 @@ var StartUI = {
   init: function init() {
     this.startUI.addEventListener("click", this, false);
     this.startUI.addEventListener("MozMousePixelScroll", this, false);
 
     // Update the input type on our local broadcaster
     document.getElementById("bcast_preciseInput").setAttribute("input",
       this.chromeWin.InputSourceHelper.isPrecise ? "precise" : "imprecise");
 
-    this._adjustDOMforViewState();
+    this._adjustDOMforViewState(this.chromeWin.ContentAreaObserver.viewstate);
 
     TopSitesStartView.init();
     BookmarksStartView.init();
     HistoryStartView.init();
     RemoteTabsStartView.init();
 
     this.chromeWin.addEventListener("MozPrecisePointer", this, true);
     this.chromeWin.addEventListener("MozImprecisePointer", this, true);
@@ -76,16 +76,20 @@ var StartUI = {
       return;
 
     for (let expandedSection of this.startUI.querySelectorAll(".meta-section[expanded]"))
       expandedSection.removeAttribute("expanded")
 
     section.setAttribute("expanded", "true");
   },
 
+  _adjustDOMforViewState: function(aState) {
+    document.getElementById("bcast_windowState").setAttribute("viewstate", aState);
+  },
+
   handleEvent: function handleEvent(aEvent) {
     switch (aEvent.type) {
       case "MozPrecisePointer":
         document.getElementById("bcast_preciseInput").setAttribute("input", "precise");
         break;
       case "MozImprecisePointer":
         document.getElementById("bcast_preciseInput").setAttribute("input", "imprecise");
         break;
@@ -101,43 +105,16 @@ var StartUI = {
         }
 
         aEvent.preventDefault();
         aEvent.stopPropagation();
         break;
     }
   },
 
-  _adjustDOMforViewState: function(aState) {
-    let currViewState = aState;
-    if (!currViewState && Services.metro.immersive) {
-      switch (Services.metro.snappedState) {
-        case Ci.nsIWinMetroUtils.fullScreenLandscape:
-          currViewState = "landscape";
-          break;
-        case Ci.nsIWinMetroUtils.fullScreenPortrait:
-          currViewState = "portrait";
-          break;
-        case Ci.nsIWinMetroUtils.filled:
-          currViewState = "filled";
-          break;
-        case Ci.nsIWinMetroUtils.snapped:
-          currViewState = "snapped";
-          break;
-      }
-    }
-
-    document.getElementById("bcast_windowState").setAttribute("viewstate", currViewState);
-    if (currViewState == "snapped") {
-      document.getElementById("start-topsites-grid").removeAttribute("tiletype");
-    } else {
-      document.getElementById("start-topsites-grid").setAttribute("tiletype", "thumbnail");
-    }
-  },
-
   observe: function (aSubject, aTopic, aData) {
     switch (aTopic) {
       case "metro_viewstate_changed":
         this._adjustDOMforViewState(aData);
         break;
     }
   }
 };
--- a/browser/metro/base/content/startui/TopSitesView.js
+++ b/browser/metro/base/content/startui/TopSitesView.js
@@ -6,31 +6,28 @@
 
 let prefs = Components.classes["@mozilla.org/preferences-service;1"].
       getService(Components.interfaces.nsIPrefBranch);
 
 Cu.import("resource://gre/modules/PageThumbs.jsm");
 Cu.import("resource:///modules/colorUtils.jsm");
 
 function TopSitesView(aGrid, aMaxSites) {
-  this._set = aGrid;
-  this._set.controller = this;
+  View.call(this, aGrid);
+
   this._topSitesMax = aMaxSites;
 
   // clean up state when the appbar closes
   StartUI.chromeWin.addEventListener('MozAppbarDismissing', this, false);
   let history = Cc["@mozilla.org/browser/nav-history-service;1"].
                 getService(Ci.nsINavHistoryService);
   history.addObserver(this, false);
 
   PageThumbs.addExpirationFilter(this);
   Services.obs.addObserver(this, "Metro:RefreshTopsiteThumbnail", false);
-  Services.obs.addObserver(this, "metro_viewstate_changed", false);
-
-  this._adjustDOMforViewState();
 
   NewTabUtils.allPages.register(this);
   TopSites.prepareCache().then(function(){
     this.populateGrid();
   }.bind(this));
 }
 
 TopSitesView.prototype = Util.extend(Object.create(View.prototype), {
@@ -38,22 +35,22 @@ TopSitesView.prototype = Util.extend(Obj
   _topSitesMax: null,
   // _lastSelectedSites used to temporarily store blocked/removed sites for undo/restore-ing
   _lastSelectedSites: null,
   // isUpdating used only for testing currently
   isUpdating: false,
 
   destruct: function destruct() {
     Services.obs.removeObserver(this, "Metro:RefreshTopsiteThumbnail");
-    Services.obs.removeObserver(this, "metro_viewstate_changed");
     PageThumbs.removeExpirationFilter(this);
     NewTabUtils.allPages.unregister(this);
     if (StartUI.chromeWin) {
       StartUI.chromeWin.removeEventListener('MozAppbarDismissing', this, false);
     }
+    View.prototype.destruct.call(this);
   },
 
   handleItemClick: function tabview_handleItemClick(aItem) {
     let url = aItem.getAttribute("value");
     StartUI.goToURI(url);
   },
 
   doActionOnSelectedTiles: function(aActionName, aEvent) {
@@ -132,16 +129,17 @@ TopSitesView.prototype = Util.extend(Obj
     }
   },
 
   handleEvent: function(aEvent) {
     switch (aEvent.type){
       case "MozAppbarDismissing":
         // clean up when the context appbar is dismissed - we don't remember selections
         this._lastSelectedSites = null;
+        break;
     }
   },
 
   update: function() {
     // called by the NewTabUtils.allPages.update, notifying us of data-change in topsites
     let grid = this._set,
         dirtySites = TopSites.dirty();
 
@@ -229,16 +227,23 @@ TopSitesView.prototype = Util.extend(Obj
   _adjustDOMforViewState: function _adjustDOMforViewState(aState) {
     if (!this._set)
       return;
     if (!aState)
       aState = this._set.getAttribute("viewstate");
 
     View.prototype._adjustDOMforViewState.call(this, aState);
 
+    // Don't show thumbnails in snapped view.
+    if (aState == "snapped") {
+      document.getElementById("start-topsites-grid").removeAttribute("tiletype");
+    } else {
+      document.getElementById("start-topsites-grid").setAttribute("tiletype", "thumbnail");
+    }
+
     // propogate tiletype changes down to tile children
     let tileType = this._set.getAttribute("tiletype");
     for (let item of this._set.children) {
       if (tileType) {
         item.setAttribute("tiletype", tileType);
       } else {
         item.removeAttribute("tiletype");
       }
@@ -246,19 +251,16 @@ TopSitesView.prototype = Util.extend(Obj
   },
 
   // nsIObservers
   observe: function (aSubject, aTopic, aState) {
     switch(aTopic) {
       case "Metro:RefreshTopsiteThumbnail":
         this.forceReloadOfThumbnail(aState);
         break;
-      case "metro_viewstate_changed":
-        this.onViewStateChange(aState);
-        break;
     }
   },
 
   // nsINavHistoryObserver
   onBeginUpdateBatch: function() {
   },
 
   onEndUpdateBatch: function() {
--- a/browser/metro/base/tests/mochitest/browser_snappedState.js
+++ b/browser/metro/base/tests/mochitest/browser_snappedState.js
@@ -106,17 +106,17 @@ gTests.push({
     yield setUpSnapped();
   },
   run: function() {
     ok(Browser.selectedBrowser.contentWindow.scrollMaxY !== 0, "Snapped scrolls vertically");
     ok(Browser.selectedBrowser.contentWindow.scrollMaxX === 0, "Snapped does not scroll horizontally");
   },
   tearDown: function() {
     BookmarksTestHelper.restore();
-    restoreViewstate();
+    yield restoreViewstate();
   }
 });
 
 gTests.push({
   desc: "Navbar contextual buttons are not shown in snapped",
   setUp: setUpSnapped,
   run: function() {
     let toolbarContextual = document.getElementById("toolbar-contextual");
@@ -155,11 +155,11 @@ gTests.push({
   },
   run: function() {
     ok(Browser.selectedBrowser.contentWindow.scrollMaxY !== 0, "Portrait scrolls vertically");
     ok(Browser.selectedBrowser.contentWindow.scrollMaxX === 0, "Portrait does not scroll horizontally");
   },
   tearDown: function() {
     BookmarksTestHelper.restore();
     HistoryTestHelper.restore();
-    restoreViewstate();
+    yield restoreViewstate();
   }
 });
--- a/browser/metro/base/tests/mochitest/head.js
+++ b/browser/metro/base/tests/mochitest/head.js
@@ -42,17 +42,17 @@ const mochitestPath = splitPath.join('/'
 }, this);
 
 /*=============================================================================
   Metro ui helpers
 =============================================================================*/
 
 function isLandscapeMode()
 {
-  return (Services.metro.snappedState == Ci.nsIWinMetroUtils.fullScreenLandscape);
+  return Elements.windowState.getAttribute("viewstate") == "landscape";
 }
 
 function setDevPixelEqualToPx()
 {
   todo(false, "test depends on devPixelsPerPx set to 1.0 - see bugs 886624 and 859742");
   SpecialPowers.setCharPref("layout.css.devPixelsPerPx", "1.0");
   registerCleanupFunction(function () {
     SpecialPowers.clearUserPref("layout.css.devPixelsPerPx");
--- a/browser/metro/base/tests/mochitest/helpers/ViewStateHelper.js
+++ b/browser/metro/base/tests/mochitest/helpers/ViewStateHelper.js
@@ -15,39 +15,38 @@ function setSnappedViewstate() {
 
   // Reduce browser width to simulate small screen size.
   let fullWidth = browser.clientWidth;
   let padding = fullWidth - snappedSize;
 
   browser.style.borderRight = padding + "px solid gray";
 
   // Communicate viewstate change
-  Services.obs.notifyObservers(null, 'metro_viewstate_changed', 'snapped');
+  ContentAreaObserver._updateViewState("snapped");
 
   // Make sure it renders the new mode properly
   yield waitForMs(0);
 }
 
 function setPortraitViewstate() {
   ok(isLandscapeMode(), "setPortraitViewstate expects landscape mode to work.");
 
   let browser = Browser.selectedBrowser;
 
   let fullWidth = browser.clientWidth;
   let padding = fullWidth - portraitSize;
 
   browser.style.borderRight = padding + "px solid gray";
 
-  Services.obs.notifyObservers(null, 'metro_viewstate_changed', 'portrait');
+  ContentAreaObserver._updateViewState("portrait");
 
   // Make sure it renders the new mode properly
   yield waitForMs(0);
 }
 
 function restoreViewstate() {
-  ok(isLandscapeMode(), "restoreViewstate expects landscape mode to work.");
-
-  Services.obs.notifyObservers(null, 'metro_viewstate_changed', 'landscape');
+  ContentAreaObserver._updateViewState("landscape");
+  ok(isLandscapeMode(), "restoreViewstate should restore landscape mode.");
 
   Browser.selectedBrowser.style.removeProperty("border-right");
 
   yield waitForMs(0);
 }
--- a/browser/metro/modules/View.jsm
+++ b/browser/metro/modules/View.jsm
@@ -18,20 +18,33 @@ function makeURI(aURL, aOriginCharset, a
 }
 
 // --------------------------------
 
 
 // --------------------------------
 // View prototype for shared functionality
 
-function View() {
+function View(aSet) {
+  this._set = aSet;
+  this._set.controller = this;
+
+  this.viewStateObserver = {
+    observe: (aSubject, aTopic, aData) => this._adjustDOMforViewState(aData)
+  };
+  Services.obs.addObserver(this.viewStateObserver, "metro_viewstate_changed", false);
+
+  this._adjustDOMforViewState();
 }
 
 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") {
@@ -39,20 +52,16 @@ View.prototype = {
       } else {
         this._set.removeAttribute("vertical");
       }
 
       this._set.arrangeItems();
     }
   },
 
-  onViewStateChange: function (aState) {
-    this._adjustDOMforViewState(aState);
-  },
-
   _updateFavicon: function pv__updateFavicon(aItem, aUri) {
     if ("string" == typeof aUri) {
       aUri = makeURI(aUri);
     }
     PlacesUtils.favicons.getFaviconURLForPage(aUri, this._gotIcon.bind(this, aItem));
   },
 
   _gotIcon: function pv__gotIcon(aItem, aIconUri) {
--- a/browser/metro/profile/metro.js
+++ b/browser/metro/profile/metro.js
@@ -372,16 +372,19 @@ pref("privacy.sanitize.migrateFx3Prefs",
 
 // enable geo
 pref("geo.enabled", true);
 pref("geo.wifi.uri", "https://www.googleapis.com/geolocation/v1/geolocate?key=%GOOGLE_API_KEY%");
 
 // JS error console
 pref("devtools.errorconsole.enabled", false);
 
+// snapped view
+pref("browser.ui.snapped.maxWidth", 600);
+
 // kinetic tweakables
 pref("browser.ui.kinetic.updateInterval", 16);
 pref("browser.ui.kinetic.exponentialC", 1400);
 pref("browser.ui.kinetic.polynomialC", 100);
 pref("browser.ui.kinetic.swipeLength", 160);
 pref("browser.ui.zoom.animationDuration", 200); // ms duration of double-tap zoom animation
 
 // pinch gesture
--- a/browser/metro/theme/browser.css
+++ b/browser/metro/theme/browser.css
@@ -370,16 +370,17 @@ documenttab[selected] .documenttab-selec
 }
 
 #overlay-plus:-moz-locale-dir(ltr),
 #overlay-back:-moz-locale-dir(rtl) {
   right: -70px;
   background-position: left 6px center;
 }
 
+#stack[viewstate="snapped"] > .overlay-button,
 #stack[keyboardVisible] > .overlay-button,
 #stack[autocomplete] > .overlay-button,
 #stack[fullscreen] > .overlay-button,
 #appbar[visible] ~ .overlay-button,
 .overlay-button[disabled] {
   box-shadow: none;
   visibility: collapse;
 }