diff -r 000000000000 -r 6474c204b198 browser/metro/base/content/helperui/SelectionHelperUI.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/metro/base/content/helperui/SelectionHelperUI.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,1186 @@ +/* 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/. */ + +/* + * selection management + */ + +/* + * Current monocle image: + * dimensions: 32 x 24 + * circle center: 16 x 14 + * padding top: 6 + */ + +// Y axis scroll distance that will disable this module and cancel selection +const kDisableOnScrollDistance = 25; + +// Drag hysteresis programmed into monocle drag moves +const kDragHysteresisDistance = 10; + +// selection layer id returned from SelectionHandlerUI's layerMode. +const kChromeLayer = 1; +const kContentLayer = 2; + +/* + * Markers + */ + +function MarkerDragger(aMarker) { + this._marker = aMarker; +} + +MarkerDragger.prototype = { + _selectionHelperUI: null, + _marker: null, + _shutdown: false, + _dragging: false, + + get marker() { + return this._marker; + }, + + set shutdown(aVal) { + this._shutdown = aVal; + }, + + get shutdown() { + return this._shutdown; + }, + + get dragging() { + return this._dragging; + }, + + freeDrag: function freeDrag() { + return true; + }, + + isDraggable: function isDraggable(aTarget, aContent) { + return { x: true, y: true }; + }, + + dragStart: function dragStart(aX, aY, aTarget, aScroller) { + if (this._shutdown) + return false; + this._dragging = true; + this.marker.dragStart(aX, aY); + return true; + }, + + dragStop: function dragStop(aDx, aDy, aScroller) { + if (this._shutdown) + return false; + this._dragging = false; + this.marker.dragStop(aDx, aDy); + return true; + }, + + dragMove: function dragMove(aDx, aDy, aScroller, aIsKenetic, aClientX, aClientY) { + // Note if aIsKenetic is true this is synthetic movement, + // we don't want that so return false. + if (this._shutdown || aIsKenetic) + return false; + this.marker.moveBy(aDx, aDy, aClientX, aClientY); + // return true if we moved, false otherwise. The result + // is used in deciding if we should repaint between drags. + return true; + } +} + +function Marker(aParent, aTag, aElementId, xPos, yPos) { + this._xPos = xPos; + this._yPos = yPos; + this._selectionHelperUI = aParent; + this._element = aParent.overlay.getMarker(aElementId); + this._elementId = aElementId; + // These get picked in input.js and receives drag input + this._element.customDragger = new MarkerDragger(this); + this.tag = aTag; +} + +Marker.prototype = { + _element: null, + _elementId: "", + _selectionHelperUI: null, + _xPos: 0, + _yPos: 0, + _xDrag: 0, + _yDrag: 0, + _tag: "", + _hPlane: 0, + _vPlane: 0, + _restrictedToBounds: false, + + // Tweak me if the monocle graphics change in any way + _monocleRadius: 8, + _monocleXHitTextAdjust: -2, + _monocleYHitTextAdjust: -10, + + get xPos() { + return this._xPos; + }, + + get yPos() { + return this._yPos; + }, + + get tag() { + return this._tag; + }, + + set tag(aVal) { + this._tag = aVal; + }, + + get dragging() { + return this._element.customDragger.dragging; + }, + + // Indicates that marker's position doesn't reflect real selection boundary + // but rather boundary of input control while actual selection boundaries are + // not visible (ex. due scrolled content). + get restrictedToBounds() { + return this._restrictedToBounds; + }, + + shutdown: function shutdown() { + this._element.hidden = true; + this._element.customDragger.shutdown = true; + delete this._element.customDragger; + this._selectionHelperUI = null; + this._element = null; + }, + + setTrackBounds: function setTrackBounds(aVerticalPlane, aHorizontalPlane) { + // monocle boundaries + this._hPlane = aHorizontalPlane; + this._vPlane = aVerticalPlane; + }, + + show: function show() { + this._element.hidden = false; + }, + + hide: function hide() { + this._element.hidden = true; + }, + + get visible() { + return this._element.hidden == false; + }, + + position: function position(aX, aY, aRestrictedToBounds) { + this._xPos = aX; + this._yPos = aY; + this._restrictedToBounds = !!aRestrictedToBounds; + this._setPosition(); + }, + + _setPosition: function _setPosition() { + this._element.left = this._xPos + "px"; + this._element.top = this._yPos + "px"; + }, + + dragStart: function dragStart(aX, aY) { + this._xDrag = 0; + this._yDrag = 0; + this._selectionHelperUI.markerDragStart(this); + }, + + dragStop: function dragStop(aDx, aDy) { + this._selectionHelperUI.markerDragStop(this); + }, + + moveBy: function moveBy(aDx, aDy, aClientX, aClientY) { + this._xPos -= aDx; + this._yPos -= aDy; + this._xDrag -= aDx; + this._yDrag -= aDy; + // Add a bit of hysteresis to our directional detection so "big fingers" + // are detected accurately. + let direction = "tbd"; + if (Math.abs(this._xDrag) > kDragHysteresisDistance || + Math.abs(this._yDrag) > kDragHysteresisDistance) { + direction = (this._xDrag <= 0 && this._yDrag <= 0 ? "start" : "end"); + } + // We may swap markers in markerDragMove. If markerDragMove + // returns true keep processing, otherwise get out of here. + if (this._selectionHelperUI.markerDragMove(this, direction)) { + this._setPosition(); + } + }, + + hitTest: function hitTest(aX, aY) { + // Gets the pointer of the arrow right in the middle of the + // monocle. + aY += this._monocleYHitTextAdjust; + aX += this._monocleXHitTextAdjust; + if (aX >= (this._xPos - this._monocleRadius) && + aX <= (this._xPos + this._monocleRadius) && + aY >= (this._yPos - this._monocleRadius) && + aY <= (this._yPos + this._monocleRadius)) + return true; + return false; + }, + + swapMonocle: function swapMonocle(aCaret) { + let targetElement = aCaret._element; + let targetElementId = aCaret._elementId; + + aCaret._element = this._element; + aCaret._element.customDragger._marker = aCaret; + aCaret._elementId = this._elementId; + + this._xPos = aCaret._xPos; + this._yPos = aCaret._yPos; + this._element = targetElement; + this._element.customDragger._marker = this; + this._elementId = targetElementId; + this._element.visible = true; + }, + +}; + +/* + * SelectionHelperUI + */ + +var SelectionHelperUI = { + _debugEvents: false, + _msgTarget: null, + _startMark: null, + _endMark: null, + _caretMark: null, + _target: null, + _showAfterUpdate: false, + _activeSelectionRect: null, + _selectionMarkIds: [], + _targetIsEditable: false, + + /* + * Properties + */ + + get startMark() { + if (this._startMark == null) { + this._startMark = new Marker(this, "start", this._selectionMarkIds.pop(), 0, 0); + } + return this._startMark; + }, + + get endMark() { + if (this._endMark == null) { + this._endMark = new Marker(this, "end", this._selectionMarkIds.pop(), 0, 0); + } + return this._endMark; + }, + + get caretMark() { + if (this._caretMark == null) { + this._caretMark = new Marker(this, "caret", this._selectionMarkIds.pop(), 0, 0); + } + return this._caretMark; + }, + + get overlay() { + return document.getElementById(this.layerMode == kChromeLayer ? + "chrome-selection-overlay" : "content-selection-overlay"); + }, + + get layerMode() { + if (this._msgTarget && this._msgTarget instanceof SelectionPrototype) + return kChromeLayer; + return kContentLayer; + }, + + /* + * isActive (prop) + * + * Determines if a selection edit session is currently active. + */ + get isActive() { + return this._msgTarget ? true : false; + }, + + /* + * isSelectionUIVisible (prop) + * + * Determines if edit session monocles are visible. Useful + * in checking if selection handler is setup for tests. + */ + get isSelectionUIVisible() { + if (!this._msgTarget || !this._startMark) + return false; + return this._startMark.visible; + }, + + /* + * isCaretUIVisible (prop) + * + * Determines if caret browsing monocle is visible. Useful + * in checking if selection handler is setup for tests. + */ + get isCaretUIVisible() { + if (!this._msgTarget || !this._caretMark) + return false; + return this._caretMark.visible; + }, + + /* + * hasActiveDrag (prop) + * + * Determines if a marker is actively being dragged (missing call + * to markerDragStop). Useful in checking if selection handler is + * setup for tests. + */ + get hasActiveDrag() { + if (!this._msgTarget) + return false; + if ((this._caretMark && this._caretMark.dragging) || + (this._startMark && this._startMark.dragging) || + (this._endMark && this._endMark.dragging)) + return true; + return false; + }, + + + /* + * Observers + */ + + observe: function (aSubject, aTopic, aData) { + switch (aTopic) { + case "attach_edit_session_to_content": + // We receive this from text input bindings when this module + // isn't accessible. + this.chromeTextboxClick(aSubject); + break; + + case "apzc-transform-begin": + if (this.isActive && this.layerMode == kContentLayer) { + this._hideMonocles(); + } + break; + + case "apzc-transform-end": + // The selection range callback will check to see if the new + // position is off the screen, in which case it shuts down and + // clears the selection. + if (this.isActive && this.layerMode == kContentLayer) { + this._showAfterUpdate = true; + this._sendAsyncMessage("Browser:SelectionUpdate", { + isInitiatedByAPZC: true + }); + } + break; + } + }, + + /* + * Public apis + */ + + /* + * pingSelectionHandler + * + * Ping the SelectionHandler and wait for the right response. Insures + * all previous messages have been received. Useful in checking if + * selection handler is setup for tests. + * + * @return a promise + */ + pingSelectionHandler: function pingSelectionHandler() { + if (!this.isActive) + return null; + + if (this._pingCount == undefined) { + this._pingCount = 0; + this._pingArray = []; + } + + this._pingCount++; + + let deferred = Promise.defer(); + this._pingArray.push({ + id: this._pingCount, + deferred: deferred + }); + + this._sendAsyncMessage("Browser:SelectionHandlerPing", { id: this._pingCount }); + return deferred.promise; + }, + + /* + * openEditSession + * + * Attempts to select underlying text at a point and begins editing + * the section. + * + * @param aMsgTarget - Browser or chrome message target + * @param aX, aY - Browser relative client coordinates. + * @param aSetFocus - (optional) For form inputs, requests that the focus + * be set to the element. + */ + openEditSession: function openEditSession(aMsgTarget, aX, aY, aSetFocus) { + if (!aMsgTarget || this.isActive) + return; + this._init(aMsgTarget); + this._setupDebugOptions(); + let setFocus = aSetFocus || false; + // Send this over to SelectionHandler in content, they'll message us + // back with information on the current selection. SelectionStart + // takes client coordinates. + this._sendAsyncMessage("Browser:SelectionStart", { + setFocus: setFocus, + xPos: aX, + yPos: aY + }); + }, + + /* + * attachEditSession + * + * Attaches to existing selection and begins editing. + * + * @param aMsgTarget - Browser or chrome message target. + * @param aX Tap browser relative client X coordinate. + * @param aY Tap browser relative client Y coordinate. + * @param aTarget Actual tap target (optional). + */ + attachEditSession: function attachEditSession(aMsgTarget, aX, aY, aTarget) { + if (!aMsgTarget || this.isActive) + return; + this._init(aMsgTarget); + this._setupDebugOptions(); + + // Send this over to SelectionHandler in content, they'll message us + // back with information on the current selection. SelectionAttach + // takes client coordinates. + this._sendAsyncMessage("Browser:SelectionAttach", { + target: aTarget, + xPos: aX, + yPos: aY + }); + }, + + /* + * attachToCaret + * + * Initiates a touch caret selection session for a text input. + * Can be called multiple times to move the caret marker around. + * + * Note the caret marker is pretty limited in functionality. The + * only thing is can do is be displayed at the caret position. + * Once the user starts a drag, the caret marker is hidden, and + * the start and end markers take over. + * + * @param aMsgTarget - Browser or chrome message target. + * @param aX Tap browser relative client X coordinate. + * @param aY Tap browser relative client Y coordinate. + * @param aTarget Actual tap target (optional). + */ + attachToCaret: function attachToCaret(aMsgTarget, aX, aY, aTarget) { + if (!this.isActive) { + this._init(aMsgTarget); + this._setupDebugOptions(); + } else { + this._hideMonocles(); + } + + this._lastCaretAttachment = { + target: aTarget, + xPos: aX, + yPos: aY + }; + + this._sendAsyncMessage("Browser:CaretAttach", { + target: aTarget, + xPos: aX, + yPos: aY + }); + }, + + /* + * canHandleContextMenuMsg + * + * Determines if we can handle a ContextMenuHandler message. + */ + canHandleContextMenuMsg: function canHandleContextMenuMsg(aMessage) { + if (aMessage.json.types.indexOf("content-text") != -1) + return true; + return false; + }, + + /* + * closeEditSession(aClearSelection) + * + * Closes an active edit session and shuts down. Does not clear existing + * selection regions if they exist. + * + * @param aClearSelection bool indicating if the selection handler should also + * clear any selection. optional, the default is false. + */ + closeEditSession: function closeEditSession(aClearSelection) { + if (!this.isActive) { + return; + } + // This will callback in _selectionHandlerShutdown in + // which we will call _shutdown(). + let clearSelection = aClearSelection || false; + this._sendAsyncMessage("Browser:SelectionClose", { + clearSelection: clearSelection + }); + }, + + /* + * Click handler for chrome pages loaded into the browser (about:config). + * Called from the text input bindings via the attach_edit_session_to_content + * observer. + */ + chromeTextboxClick: function (aEvent) { + this.attachEditSession(Browser.selectedTab.browser, aEvent.clientX, + aEvent.clientY, aEvent.target); + }, + + /* + * Handy debug routines that work independent of selection. They + * make use of the selection overlay for drawing points. + */ + + debugDisplayDebugPoint: function (aLeft, aTop, aSize, aCssColorStr, aFill) { + this.overlay.enabled = true; + this.overlay.displayDebugLayer = true; + this.overlay.addDebugRect(aLeft, aTop, aLeft + aSize, aTop + aSize, + aCssColorStr, aFill); + }, + + debugClearDebugPoints: function () { + this.overlay.displayDebugLayer = false; + if (!this._msgTarget) { + this.overlay.enabled = false; + } + }, + + /* + * Init and shutdown + */ + + init: function () { + let os = Services.obs; + os.addObserver(this, "attach_edit_session_to_content", false); + os.addObserver(this, "apzc-transform-begin", false); + os.addObserver(this, "apzc-transform-end", false); + }, + + _init: function _init(aMsgTarget) { + // store the target message manager + this._msgTarget = aMsgTarget; + + // Init our list of available monocle ids + this._setupMonocleIdArray(); + + // Init selection rect info + this._activeSelectionRect = Util.getCleanRect(); + this._targetElementRect = Util.getCleanRect(); + + // SelectionHandler messages + messageManager.addMessageListener("Content:SelectionRange", this); + messageManager.addMessageListener("Content:SelectionCopied", this); + messageManager.addMessageListener("Content:SelectionFail", this); + messageManager.addMessageListener("Content:SelectionDebugRect", this); + messageManager.addMessageListener("Content:HandlerShutdown", this); + messageManager.addMessageListener("Content:SelectionHandlerPong", this); + messageManager.addMessageListener("Content:SelectionSwap", this); + + // capture phase + window.addEventListener("keypress", this, true); + window.addEventListener("MozPrecisePointer", this, true); + window.addEventListener("MozDeckOffsetChanging", this, true); + window.addEventListener("MozDeckOffsetChanged", this, true); + window.addEventListener("KeyboardChanged", this, true); + + // bubble phase + window.addEventListener("click", this, false); + window.addEventListener("touchstart", this, false); + + Elements.browsers.addEventListener("URLChanged", this, true); + Elements.browsers.addEventListener("SizeChanged", this, true); + + Elements.tabList.addEventListener("TabSelect", this, true); + + Elements.navbar.addEventListener("transitionend", this, true); + + this.overlay.enabled = true; + }, + + _shutdown: function _shutdown() { + messageManager.removeMessageListener("Content:SelectionRange", this); + messageManager.removeMessageListener("Content:SelectionCopied", this); + messageManager.removeMessageListener("Content:SelectionFail", this); + messageManager.removeMessageListener("Content:SelectionDebugRect", this); + messageManager.removeMessageListener("Content:HandlerShutdown", this); + messageManager.removeMessageListener("Content:SelectionHandlerPong", this); + messageManager.removeMessageListener("Content:SelectionSwap", this); + + window.removeEventListener("keypress", this, true); + window.removeEventListener("MozPrecisePointer", this, true); + window.removeEventListener("MozDeckOffsetChanging", this, true); + window.removeEventListener("MozDeckOffsetChanged", this, true); + window.removeEventListener("KeyboardChanged", this, true); + + window.removeEventListener("click", this, false); + window.removeEventListener("touchstart", this, false); + + Elements.browsers.removeEventListener("URLChanged", this, true); + Elements.browsers.removeEventListener("SizeChanged", this, true); + + Elements.tabList.removeEventListener("TabSelect", this, true); + + Elements.navbar.removeEventListener("transitionend", this, true); + + this._shutdownAllMarkers(); + + this._selectionMarkIds = []; + this._msgTarget = null; + this._activeSelectionRect = null; + + this.overlay.displayDebugLayer = false; + this.overlay.enabled = false; + }, + + /* + * Utilities + */ + + /* + * _swapCaretMarker + * + * Swap two drag markers - used when transitioning from caret mode + * to selection mode. We take the current caret marker (which is in a + * drag state) and swap it out with one of the selection markers. + */ + _swapCaretMarker: function _swapCaretMarker(aDirection) { + let targetMark = null; + if (aDirection == "start") + targetMark = this.startMark; + else + targetMark = this.endMark; + let caret = this.caretMark; + targetMark.swapMonocle(caret); + let id = caret._elementId; + caret.shutdown(); + this._caretMark = null; + this._selectionMarkIds.push(id); + }, + + /* + * _transitionFromCaretToSelection + * + * Transitions from caret mode to text selection mode. + */ + _transitionFromCaretToSelection: function _transitionFromCaretToSelection(aDirection) { + // Get selection markers initialized if they aren't already + { let mark = this.startMark; mark = this.endMark; } + + // Swap the caret marker out for the start or end marker depending + // on direction. + this._swapCaretMarker(aDirection); + + let targetMark = null; + if (aDirection == "start") + targetMark = this.startMark; + else + targetMark = this.endMark; + + // Position both in the same starting location. + this.startMark.position(targetMark.xPos, targetMark.yPos); + this.endMark.position(targetMark.xPos, targetMark.yPos); + + // We delay transitioning until we know which direction the user is dragging + // based on a hysteresis value in the drag marker code. Down in our caller, we + // cache the first drag position in _cachedCaretPos so we can select from the + // initial caret drag position. Use those values if we have them. (Note + // _cachedCaretPos has already been translated in _getMarkerBaseMessage.) + let xpos = this._cachedCaretPos ? this._cachedCaretPos.xPos : + this._msgTarget.ctobx(targetMark.xPos, true); + let ypos = this._cachedCaretPos ? this._cachedCaretPos.yPos : + this._msgTarget.ctoby(targetMark.yPos, true); + + // Start the selection monocle drag. SelectionHandler relies on this + // for getting initialized. This will also trigger a message back for + // monocle positioning. Note, markerDragMove is still on the stack in + // this call! + this._sendAsyncMessage("Browser:SelectionSwitchMode", { + newMode: "selection", + change: targetMark.tag, + xPos: xpos, + yPos: ypos, + }); + }, + + /* + * _setupDebugOptions + * + * Sends a message over to content instructing it to + * turn on various debug features. + */ + _setupDebugOptions: function _setupDebugOptions() { + // Debug options for selection + let debugOpts = { dumpRanges: false, displayRanges: false, dumpEvents: false }; + try { + if (Services.prefs.getBoolPref(kDebugSelectionDumpPref)) + debugOpts.displayRanges = true; + } catch (ex) {} + try { + if (Services.prefs.getBoolPref(kDebugSelectionDisplayPref)) + debugOpts.displayRanges = true; + } catch (ex) {} + try { + if (Services.prefs.getBoolPref(kDebugSelectionDumpEvents)) { + debugOpts.dumpEvents = true; + this._debugEvents = true; + } + } catch (ex) {} + + if (debugOpts.displayRanges || debugOpts.dumpRanges || debugOpts.dumpEvents) { + // Turn on the debug layer + this.overlay.displayDebugLayer = true; + // Tell SelectionHandler what to do + this._sendAsyncMessage("Browser:SelectionDebug", debugOpts); + } + }, + + /* + * _sendAsyncMessage + * + * Helper for sending a message to SelectionHandler. + */ + _sendAsyncMessage: function _sendAsyncMessage(aMsg, aJson) { + if (!this._msgTarget) { + if (this._debugEvents) + Util.dumpLn("SelectionHelperUI sendAsyncMessage could not send", aMsg); + return; + } + if (this._msgTarget && this._msgTarget instanceof SelectionPrototype) { + this._msgTarget.msgHandler(aMsg, aJson); + } else { + this._msgTarget.messageManager.sendAsyncMessage(aMsg, aJson); + } + }, + + _checkForActiveDrag: function _checkForActiveDrag() { + return (this.startMark.dragging || this.endMark.dragging || + this.caretMark.dragging); + }, + + _hitTestSelection: function _hitTestSelection(aEvent) { + // Ignore if the double tap isn't on our active selection rect. + if (this._activeSelectionRect && + Util.pointWithinRect(aEvent.clientX, aEvent.clientY, this._activeSelectionRect)) { + return true; + } + return false; + }, + + /* + * _setCaretPositionAtPoint - sets the current caret position. + * + * @param aX, aY - browser relative client coordinates + */ + _setCaretPositionAtPoint: function _setCaretPositionAtPoint(aX, aY) { + let json = this._getMarkerBaseMessage("caret"); + json.caret.xPos = aX; + json.caret.yPos = aY; + this._sendAsyncMessage("Browser:CaretUpdate", json); + }, + + /* + * _shutdownAllMarkers + * + * Helper for shutting down all markers and + * freeing the objects associated with them. + */ + _shutdownAllMarkers: function _shutdownAllMarkers() { + if (this._startMark) + this._startMark.shutdown(); + if (this._endMark) + this._endMark.shutdown(); + if (this._caretMark) + this._caretMark.shutdown(); + + this._startMark = null; + this._endMark = null; + this._caretMark = null; + }, + + /* + * _setupMonocleIdArray + * + * Helper for initing the array of monocle anon ids. + */ + _setupMonocleIdArray: function _setupMonocleIdArray() { + this._selectionMarkIds = ["selectionhandle-mark1", + "selectionhandle-mark2", + "selectionhandle-mark3"]; + }, + + _hideMonocles: function _hideMonocles() { + if (this._startMark) { + this.startMark.hide(); + } + if (this._endMark) { + this.endMark.hide(); + } + if (this._caretMark) { + this.caretMark.hide(); + } + }, + + _showMonocles: function _showMonocles(aSelection) { + if (!aSelection) { + if (this._checkMonocleVisibility(this.caretMark.xPos, this.caretMark.yPos)) { + this.caretMark.show(); + } + } else { + if (this._checkMonocleVisibility(this.endMark.xPos, this.endMark.yPos)) { + this.endMark.show(); + } + if (this._checkMonocleVisibility(this.startMark.xPos, this.startMark.yPos)) { + this.startMark.show(); + } + } + }, + + _checkMonocleVisibility: function(aX, aY) { + let viewport = Browser.selectedBrowser.contentViewportBounds; + aX = this._msgTarget.ctobx(aX); + aY = this._msgTarget.ctoby(aY); + if (aX < viewport.x || aY < viewport.y || + aX > (viewport.x + viewport.width) || + aY > (viewport.y + viewport.height)) { + return false; + } + return true; + }, + + /* + * Event handlers for document events + */ + + /* + * Handles taps that move the current caret around in text edits, + * clear active selection and focus when necessary, or change + * modes. Only active after SelectionHandlerUI is initialized. + */ + _onClick: function(aEvent) { + if (this.layerMode == kChromeLayer && this._targetIsEditable) { + this.attachToCaret(this._msgTarget, aEvent.clientX, aEvent.clientY, + aEvent.target); + } + }, + + _onKeypress: function _onKeypress() { + this.closeEditSession(); + }, + + _onResize: function _onResize() { + this._sendAsyncMessage("Browser:SelectionUpdate", {}); + }, + + /* + * _onDeckOffsetChanging - fired by ContentAreaObserver before the browser + * deck is shifted for form input access in response to a soft keyboard + * display. + */ + _onDeckOffsetChanging: function _onDeckOffsetChanging(aEvent) { + // Hide the monocles temporarily + this._hideMonocles(); + }, + + /* + * _onDeckOffsetChanged - fired by ContentAreaObserver after the browser + * deck is shifted for form input access in response to a soft keyboard + * display. + */ + _onDeckOffsetChanged: function _onDeckOffsetChanged(aEvent) { + // Update the monocle position and display + this.attachToCaret(null, this._lastCaretAttachment.xPos, + this._lastCaretAttachment.yPos, this._lastCaretAttachment.target); + }, + + /* + * Detects when the nav bar transitions, so we can enable selection at the + * appropriate location once the transition is complete, or shutdown + * selection down when the nav bar is hidden. + */ + _onNavBarTransitionEvent: function _onNavBarTransitionEvent(aEvent) { + // Ignore when selection is in content + if (this.layerMode == kContentLayer) { + return; + } + + // After tansitioning up, show the monocles + if (Elements.navbar.isShowing) { + this._showAfterUpdate = true; + this._sendAsyncMessage("Browser:SelectionUpdate", {}); + } + }, + + _onKeyboardChangedEvent: function _onKeyboardChangedEvent() { + if (!this.isActive || this.layerMode == kContentLayer) { + return; + } + this._sendAsyncMessage("Browser:SelectionUpdate", {}); + }, + + /* + * Event handlers for message manager + */ + + _onDebugRectRequest: function _onDebugRectRequest(aMsg) { + this.overlay.addDebugRect(aMsg.left, aMsg.top, aMsg.right, aMsg.bottom, + aMsg.color, aMsg.fill, aMsg.id); + }, + + _selectionHandlerShutdown: function _selectionHandlerShutdown() { + this._shutdown(); + }, + + _selectionSwap: function _selectionSwap() { + [this.startMark.tag, this.endMark.tag] = [this.endMark.tag, + this.startMark.tag]; + [this._startMark, this._endMark] = [this.endMark, this.startMark]; + }, + + /* + * Message handlers + */ + + _onSelectionCopied: function _onSelectionCopied(json) { + this.closeEditSession(true); + }, + + _onSelectionRangeChange: function _onSelectionRangeChange(json) { + let haveSelectionRect = true; + + if (json.updateStart) { + let x = this._msgTarget.btocx(json.start.xPos, true); + let y = this._msgTarget.btocy(json.start.yPos, true); + this.startMark.position(x, y, json.start.restrictedToBounds); + } + + if (json.updateEnd) { + let x = this._msgTarget.btocx(json.end.xPos, true); + let y = this._msgTarget.btocy(json.end.yPos, true); + this.endMark.position(x, y, json.end.restrictedToBounds); + } + + if (json.updateCaret) { + let x = this._msgTarget.btocx(json.caret.xPos, true); + let y = this._msgTarget.btocy(json.caret.yPos, true); + // If selectionRangeFound is set SelectionHelper found a range we can + // attach to. If not, there's no text in the control, and hence no caret + // position information we can use. + haveSelectionRect = json.selectionRangeFound; + if (json.selectionRangeFound) { + this.caretMark.position(x, y); + this._showMonocles(false); + } + } + + if (this._showAfterUpdate) { + this._showAfterUpdate = false; + this._showMonocles(!json.updateCaret); + } + + this._targetIsEditable = json.targetIsEditable; + this._activeSelectionRect = haveSelectionRect ? + this._msgTarget.rectBrowserToClient(json.selection, true) : + this._activeSelectionRect = Util.getCleanRect(); + this._targetElementRect = + this._msgTarget.rectBrowserToClient(json.element, true); + + // If this is the end of a selection move show the appropriate + // monocle images. src=(start, update, end, caret) + if (json.src == "start" || json.src == "end") { + this._showMonocles(true); + } + }, + + _onSelectionFail: function _onSelectionFail() { + Util.dumpLn("failed to get a selection."); + this.closeEditSession(); + }, + + /* + * _onPong + * + * Handles the closure of promise we return when we send a ping + * to SelectionHandler in pingSelectionHandler. Testing use. + */ + _onPong: function _onPong(aId) { + let ping = this._pingArray.pop(); + if (ping.id != aId) { + ping.deferred.reject( + new Error("Selection module's pong doesn't match our last ping.")); + } + ping.deferred.resolve(); + }, + + /* + * Events + */ + + handleEvent: function handleEvent(aEvent) { + if (this._debugEvents && aEvent.type != "touchmove") { + Util.dumpLn("SelectionHelperUI:", aEvent.type); + } + switch (aEvent.type) { + case "click": + this._onClick(aEvent); + break; + + case "touchstart": { + if (aEvent.touches.length != 1) + break; + // Only prevent default if we're dragging so that + // APZC doesn't scroll. + if (this._checkForActiveDrag()) { + aEvent.preventDefault(); + } + break; + } + + case "keypress": + this._onKeypress(aEvent); + break; + + case "SizeChanged": + this._onResize(aEvent); + break; + + case "URLChanged": + case "TabSelect": + this._shutdown(); + break; + + case "MozPrecisePointer": + this.closeEditSession(true); + break; + + case "MozDeckOffsetChanging": + this._onDeckOffsetChanging(aEvent); + break; + + case "MozDeckOffsetChanged": + this._onDeckOffsetChanged(aEvent); + break; + + case "transitionend": + this._onNavBarTransitionEvent(aEvent); + break; + + case "KeyboardChanged": + this._onKeyboardChangedEvent(); + break; + } + }, + + receiveMessage: function sh_receiveMessage(aMessage) { + if (this._debugEvents) Util.dumpLn("SelectionHelperUI:", aMessage.name); + let json = aMessage.json; + switch (aMessage.name) { + case "Content:SelectionFail": + this._onSelectionFail(); + break; + case "Content:SelectionRange": + this._onSelectionRangeChange(json); + break; + case "Content:SelectionCopied": + this._onSelectionCopied(json); + break; + case "Content:SelectionDebugRect": + this._onDebugRectRequest(json); + break; + case "Content:HandlerShutdown": + this._selectionHandlerShutdown(); + break; + case "Content:SelectionSwap": + this._selectionSwap(); + break; + case "Content:SelectionHandlerPong": + this._onPong(json.id); + break; + } + }, + + /* + * Callbacks from markers + */ + + _getMarkerBaseMessage: function _getMarkerBaseMessage(aMarkerTag) { + return { + change: aMarkerTag, + start: { + xPos: this._msgTarget.ctobx(this.startMark.xPos, true), + yPos: this._msgTarget.ctoby(this.startMark.yPos, true), + restrictedToBounds: this.startMark.restrictedToBounds + }, + end: { + xPos: this._msgTarget.ctobx(this.endMark.xPos, true), + yPos: this._msgTarget.ctoby(this.endMark.yPos, true), + restrictedToBounds: this.endMark.restrictedToBounds + }, + caret: { + xPos: this._msgTarget.ctobx(this.caretMark.xPos, true), + yPos: this._msgTarget.ctoby(this.caretMark.yPos, true) + }, + }; + }, + + markerDragStart: function markerDragStart(aMarker) { + let json = this._getMarkerBaseMessage(aMarker.tag); + if (aMarker.tag == "caret") { + // Cache for when we start the drag in _transitionFromCaretToSelection. + if (!this._cachedCaretPos) { + this._cachedCaretPos = this._getMarkerBaseMessage(aMarker.tag).caret; + } + return; + } + this._sendAsyncMessage("Browser:SelectionMoveStart", json); + }, + + markerDragStop: function markerDragStop(aMarker) { + let json = this._getMarkerBaseMessage(aMarker.tag); + if (aMarker.tag == "caret") { + this._cachedCaretPos = null; + return; + } + this._sendAsyncMessage("Browser:SelectionMoveEnd", json); + }, + + markerDragMove: function markerDragMove(aMarker, aDirection) { + if (aMarker.tag == "caret") { + // If direction is "tbd" the drag monocle hasn't determined which + // direction the user is dragging. + if (aDirection != "tbd") { + // We are going to transition from caret browsing mode to selection + // mode on drag. So swap the caret monocle for a start or end monocle + // depending on the direction of the drag, and start selecting text. + this._transitionFromCaretToSelection(aDirection); + return false; + } + return true; + } + this._cachedCaretPos = null; + + // We'll re-display these after the drag is complete. + this._hideMonocles(); + + let json = this._getMarkerBaseMessage(aMarker.tag); + this._sendAsyncMessage("Browser:SelectionMove", json); + return true; + }, +};