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