Bug 855362 - support querying content for the distance focused elements should be raised when the skb is displayed. r=mbrubeck
authorJim Mathies <jmathies@mozilla.com>
Fri, 05 Apr 2013 05:33:42 -0500
changeset 127783 9a0aa8302b9efccfbe1ea1aa73d6bc9896bf2d1a
parent 127782 8870e8029899c88f1f7fa38162e95f7ded916ea9
child 127784 d33c412d2be05ac950a35262a24c4cbd8444245b
push id24512
push userryanvm@gmail.com
push dateFri, 05 Apr 2013 20:13:49 +0000
treeherdermozilla-central@139b6ba547fa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmbrubeck
bugs855362
milestone23.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 855362 - support querying content for the distance focused elements should be raised when the skb is displayed. r=mbrubeck
browser/metro/base/content/ContentAreaObserver.js
browser/metro/base/content/contenthandlers/SelectionHandler.js
browser/metro/theme/browser.css
--- a/browser/metro/base/content/ContentAreaObserver.js
+++ b/browser/metro/base/content/ContentAreaObserver.js
@@ -27,16 +27,17 @@
   *
   *  The portion of the content area that is not obscured by the on-screen
   *  keyboard.
   */
 
 var ContentAreaObserver = {
   styles: {},
   _keyboardState: false,
+  _shiftAmount: 0,
 
   /*
    * Properties
    */
 
   get width() {
     return window.innerWidth || 1366;
   },
@@ -63,28 +64,35 @@ var ContentAreaObserver = {
 
   /*
    * Public apis
    */
 
   init: function init() {
     window.addEventListener("resize", this, false);
 
+    // Message manager msgs we listen for
+    messageManager.addMessageListener("Content:RepositionInfoResponse", this);
+
     // Observer msgs we listen for
     Services.obs.addObserver(this, "metro_softkeyboard_shown", false);
     Services.obs.addObserver(this, "metro_softkeyboard_hidden", false);
 
+    // setup initial values for browser form repositioning
+    this._shiftBrowserDeck(0);
+
     // initialize our custom width and height styles
     this._initStyles();
 
     // apply default styling
     this.update();
   },
 
   shutdown: function shutdown() {
+    messageManager.removeMessageListener("Content:RepositionInfoResponse", this);
     Services.obs.removeObserver(this, "metro_softkeyboard_shown");
     Services.obs.removeObserver(this, "metro_softkeyboard_hidden");
   },
 
   update: function cao_update (width, height) {
     let oldWidth = parseInt(this.styles["window-width"].width);
     let oldHeight = parseInt(this.styles["window-height"].height);
 
@@ -150,16 +158,35 @@ var ContentAreaObserver = {
    */
 
   _onKeyboardDisplayChanging: function _onKeyboardDisplayChanging(aNewState) {
     this._keyboardState = aNewState;
 
     this._dispatchWindowEvent("KeyboardChanged", aNewState);
 
     this.updateViewableArea();
+
+    if (!aNewState) {
+      this._shiftBrowserDeck(0);
+      return;
+    }
+
+    // Request info about the target form element to see if we
+    // need to reposition the browser above the keyboard.
+    Browser.selectedBrowser.messageManager.sendAsyncMessage("Browser:RepositionInfoRequest", {
+      viewHeight: this.viewableHeight,
+    });
+  },
+
+  _onRepositionResponse: function _onRepositionResponse(aJsonMsg) {
+    if (!aJsonMsg.reposition || !this._keyboardState) {
+      this._shiftBrowserDeck(0);
+      return;
+    }
+    this._shiftBrowserDeck(aJsonMsg.raiseContent);
   },
 
   observe: function cao_observe(aSubject, aTopic, aData) {
     // Note these are fired before the transition starts. Also per MS specs
     // we should not do anything "heavy" here. We have about 100ms before
     // windows just ignores the event and starts the animation.
     switch (aTopic) {
       case "metro_softkeyboard_hidden":
@@ -181,20 +208,47 @@ var ContentAreaObserver = {
       case 'resize':
         if (aEvent.target != window)
           return;
         ContentAreaObserver.update();
         break;
     }
   },
 
+  receiveMessage: function sh_receiveMessage(aMessage) {
+    switch (aMessage.name) {
+      case "Content:RepositionInfoResponse":
+        this._onRepositionResponse(aMessage.json);
+        break;
+    }
+  },
+
   /*
    * Internal helpers
    */
 
+  _shiftBrowserDeck: function _shiftBrowserDeck(aAmount) {
+    if (this._shiftAmount == aAmount)
+      return;
+
+    this._shiftAmount = aAmount;
+    this._dispatchWindowEvent("MozDeckOffsetChanging", aAmount);
+
+    // Elements.browsers is the deck all browsers sit in
+    let self = this;
+    Elements.browsers.addEventListener("transitionend", function () {
+      Elements.browsers.removeEventListener("transitionend", arguments.callee, true);
+      self._dispatchWindowEvent("MozDeckOffsetChanged", aAmount);
+    }, true);
+
+    // selectAtPoint bug 858471
+    //Elements.browsers.style.transform = "translateY(" + (-1 * aAmount) + "px)";
+    Elements.browsers.style.marginTop = "" + (-1 * aAmount) + "px";
+  },
+
   _dispatchWindowEvent: function _dispatchWindowEvent(aEventName, aDetail) {
     let event = document.createEvent("UIEvents");
     event.initUIEvent(aEventName, true, false, window, aDetail);
     window.dispatchEvent(event);
   },
 
   _disatchBrowserEvent: function (aName, aDetail) {
     setTimeout(function() {
--- a/browser/metro/base/content/contenthandlers/SelectionHandler.js
+++ b/browser/metro/base/content/contenthandlers/SelectionHandler.js
@@ -59,16 +59,17 @@ var SelectionHandler = {
     addMessageListener("Browser:SelectionClose", this);
     addMessageListener("Browser:SelectionClear", this);
     addMessageListener("Browser:SelectionCopy", this);
     addMessageListener("Browser:SelectionDebug", this);
     addMessageListener("Browser:CaretAttach", this);
     addMessageListener("Browser:CaretMove", this);
     addMessageListener("Browser:CaretUpdate", this);
     addMessageListener("Browser:SelectionSwitchMode", this);
+    addMessageListener("Browser:RepositionInfoRequest", this);
   },
 
   shutdown: function shutdown() {
     removeMessageListener("Browser:SelectionStart", this);
     removeMessageListener("Browser:SelectionAttach", this);
     removeMessageListener("Browser:SelectionEnd", this);
     removeMessageListener("Browser:SelectionMoveStart", this);
     removeMessageListener("Browser:SelectionMove", this);
@@ -77,22 +78,31 @@ var SelectionHandler = {
     removeMessageListener("Browser:SelectionClose", this);
     removeMessageListener("Browser:SelectionClear", this);
     removeMessageListener("Browser:SelectionCopy", this);
     removeMessageListener("Browser:SelectionDebug", this);
     removeMessageListener("Browser:CaretAttach", this);
     removeMessageListener("Browser:CaretMove", this);
     removeMessageListener("Browser:CaretUpdate", this);
     removeMessageListener("Browser:SelectionSwitchMode", this);
+    removeMessageListener("Browser:RepositionInfoRequest", this);
   },
 
   /*************************************************
    * Properties
    */
 
+  get isActive() {
+    return !!this._targetElement;
+  },
+
+  get targetIsEditable() {
+    return this._targetIsEditable || false;
+  },
+
   /*
    * snap - enable or disable word snap for the active marker when a
    * SelectionMoveEnd event is received. Typically you would disable
    * snap when zoom is < 1.0 for precision selection.
    */
   get snap() {
     return this._snap;
   },
@@ -399,16 +409,47 @@ var SelectionHandler = {
   /*
    * Turning on or off various debug featues.
    */
   _onSelectionDebug: function _onSelectionDebug(aMsg) {
     this._debugOptions = aMsg;
     this._debugEvents = aMsg.dumpEvents;
   },
 
+  /*
+   * _repositionInfoRequest - fired at us by ContentAreaObserver when the
+   * soft keyboard is being displayed. CAO wants to make a decision about
+   * whether the browser deck needs repositioning.
+   */
+  _repositionInfoRequest: function _repositionInfoRequest(aJsonMsg) {
+    if (!this.isActive) {
+      Util.dumpLn("unexpected: repositionInfoRequest but selection isn't active.");
+      sendAsyncMessage("Content:RepositionInfoResponse", { reposition: false });
+      return;
+    }
+    
+    if (!this.targetIsEditable) {
+      Util.dumpLn("unexpected: repositionInfoRequest but targetIsEditable is false.");
+      sendAsyncMessage("Content:RepositionInfoResponse", { reposition: false });
+    }
+    
+    let result = this._calcNewContentPosition(aJsonMsg.viewHeight);
+
+    // no repositioning needed
+    if (result == 0) {
+      sendAsyncMessage("Content:RepositionInfoResponse", { reposition: false });
+      return;
+    }
+
+    sendAsyncMessage("Content:RepositionInfoResponse", {
+      reposition: true,
+      raiseContent: result,
+    });
+  },
+
   /*************************************************
    * Selection helpers
    */
 
   /*
    * _clearSelection
    *
    * Clear existing selection if it exists and reset our internla state.
@@ -1043,16 +1084,85 @@ var SelectionHandler = {
       }
     } catch (ex) {
       Util.dumpLn("error shrinking selection:", ex.message);
     }
     return result;
   },
 
   /*
+   * _calcNewContentPosition - calculates the distance the browser should be
+   * raised to move the focused form input out of the way of the soft
+   * keyboard.
+   *
+   * @param aNewViewHeight the new content view height
+   * @return 0 if no positioning is required or a positive val equal to the
+   * distance content should be raised to center the target element.
+   */
+  _calcNewContentPosition: function _calcNewContentPosition(aNewViewHeight) {
+    // We don't support this on non-editable elements
+    if (!this._targetIsEditable) {
+      return 0;
+    }
+
+    // If the bottom of the target bounds is higher than the new height,
+    // there's no need to adjust. It will be above the keyboard.
+    if (this._cache.element.bottom <= aNewViewHeight) {
+      return 0;
+    }
+    
+    // height of the target element
+    let targetHeight = this._cache.element.bottom - this._cache.element.top;
+    // height of the browser view.
+    let viewBottom = this._targetElement.ownerDocument.defaultView.innerHeight;
+
+    // If the target is shorter than the new content height, we can go ahead
+    // and center it.
+    if (targetHeight <= aNewViewHeight) {
+      // Try to center the element vertically in the new content area, but
+      // don't position such that the bottom of the browser view moves above
+      // the top of the chrome. We purposely do not resize the browser window
+      // by making it taller when trying to center elements that are near the
+      // lower bounds. This would trigger reflow which can cause content to
+      // shift around. 
+      let splitMargin = Math.round((aNewViewHeight - targetHeight) * .5);
+      let distanceToPageBounds = viewBottom - this._cache.element.bottom;
+      let distanceFromChromeTop = this._cache.element.bottom - aNewViewHeight;
+      let distanceToCenter =
+        distanceFromChromeTop + Math.min(distanceToPageBounds, splitMargin);
+      return distanceToCenter;
+    }
+
+    // Special case: we are dealing with an input that is taller than the
+    // desired height of content. We need to center on the caret location.
+    let rect =
+      this._domWinUtils.sendQueryContentEvent(this._domWinUtils.QUERY_CARET_RECT,
+                                              this._targetElement.selectionEnd,
+                                              0, 0, 0);
+    if (!rect || !rect.succeeded) {
+      Util.dumpLn("no caret was present, unexpected.");
+      return 0;
+    }
+
+    // Note sendQueryContentEvent with QUERY_CARET_RECT is really buggy. If it
+    // can't find the exact location of the caret position it will "guess".
+    // Sometimes this can put the result in unexpected locations.
+    let caretLocation = Math.max(Math.min(Math.round(rect.top + (rect.height * .5)),
+                                          viewBottom), 0);
+
+    // Caret is above the bottom of the new view bounds, no need to shift.
+    if (caretLocation <= aNewViewHeight) {
+      return 0;
+    }
+
+    // distance from the top of the keyboard down to the caret location
+    return caretLocation - aNewViewHeight;
+  },
+
+  /*
    * Events
    */
 
   /*
    * Scroll + selection advancement timer when the monocle is
    * outside the bounds of an input control.
    */
   scrollTimerCallback: function scrollTimerCallback() {
@@ -1122,16 +1232,20 @@ var SelectionHandler = {
 
       case "Browser:SelectionDebug":
         this._onSelectionDebug(json);
         break;
 
       case "Browser:SelectionUpdate":
         this._onSelectionUpdate();
         break;
+
+      case "Browser:RepositionInfoRequest":
+        this._repositionInfoRequest(json);
+        break;
     }
   },
 
   /*
    * Utilities
    */
 
   /*
--- a/browser/metro/theme/browser.css
+++ b/browser/metro/theme/browser.css
@@ -818,18 +818,22 @@ setting[type="directory"] > .preferences
 /* Browser Content Areas ----------------------------------------------------- */
 
 /* Hide the browser while the start UI is visible */
 #content-viewport[startpage],
 #content-viewport[filtering] {
   visibility: collapse;
 }
 
+/* a 'margin-top' is applied dynamically in ContentAreaObserver */
 #browsers {
   background: white;
+  transition-property: margin-top;
+  transition-duration: .3s;
+  transition-timing-function: ease-in-out;
 }
 
 /* Panel UI ---------------------------------------------------------------- */
 
 #panel-container {
   padding: 60px 40px;
 }