author | Jim Mathies <jmathies@mozilla.com> |
Fri, 05 Apr 2013 05:33:42 -0500 | |
changeset 127783 | 9a0aa8302b9efccfbe1ea1aa73d6bc9896bf2d1a |
parent 127782 | 8870e8029899c88f1f7fa38162e95f7ded916ea9 |
child 127784 | d33c412d2be05ac950a35262a24c4cbd8444245b |
push id | 24512 |
push user | ryanvm@gmail.com |
push date | Fri, 05 Apr 2013 20:13:49 +0000 |
treeherder | mozilla-central@139b6ba547fa [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | mbrubeck |
bugs | 855362 |
milestone | 23.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
|
--- 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; }