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: let Ci = Components.interfaces; michael@0: let Cc = Components.classes; michael@0: michael@0: dump("### SelectionHandler.js loaded\n"); michael@0: michael@0: var SelectionHandler = { michael@0: init: function init() { michael@0: this.type = kContentSelector; michael@0: this.snap = true; michael@0: this.lastYPos = this.lastXPos = null; michael@0: addMessageListener("Browser:SelectionStart", this); michael@0: addMessageListener("Browser:SelectionAttach", this); michael@0: addMessageListener("Browser:SelectionEnd", this); michael@0: addMessageListener("Browser:SelectionMoveStart", this); michael@0: addMessageListener("Browser:SelectionMove", this); michael@0: addMessageListener("Browser:SelectionMoveEnd", this); michael@0: addMessageListener("Browser:SelectionUpdate", this); michael@0: addMessageListener("Browser:SelectionClose", this); michael@0: addMessageListener("Browser:SelectionCopy", this); michael@0: addMessageListener("Browser:SelectionDebug", this); michael@0: addMessageListener("Browser:CaretAttach", this); michael@0: addMessageListener("Browser:CaretMove", this); michael@0: addMessageListener("Browser:CaretUpdate", this); michael@0: addMessageListener("Browser:SelectionSwitchMode", this); michael@0: addMessageListener("Browser:RepositionInfoRequest", this); michael@0: addMessageListener("Browser:SelectionHandlerPing", this); michael@0: addMessageListener("Browser:ResetLastPos", this); michael@0: }, michael@0: michael@0: shutdown: function shutdown() { michael@0: removeMessageListener("Browser:SelectionStart", this); michael@0: removeMessageListener("Browser:SelectionAttach", this); michael@0: removeMessageListener("Browser:SelectionEnd", this); michael@0: removeMessageListener("Browser:SelectionMoveStart", this); michael@0: removeMessageListener("Browser:SelectionMove", this); michael@0: removeMessageListener("Browser:SelectionMoveEnd", this); michael@0: removeMessageListener("Browser:SelectionUpdate", this); michael@0: removeMessageListener("Browser:SelectionClose", this); michael@0: removeMessageListener("Browser:SelectionCopy", this); michael@0: removeMessageListener("Browser:SelectionDebug", this); michael@0: removeMessageListener("Browser:CaretAttach", this); michael@0: removeMessageListener("Browser:CaretMove", this); michael@0: removeMessageListener("Browser:CaretUpdate", this); michael@0: removeMessageListener("Browser:SelectionSwitchMode", this); michael@0: removeMessageListener("Browser:RepositionInfoRequest", this); michael@0: removeMessageListener("Browser:SelectionHandlerPing", this); michael@0: removeMessageListener("Browser:ResetLastPos", this); michael@0: }, michael@0: michael@0: sendAsync: function sendAsync(aMsg, aJson) { michael@0: sendAsyncMessage(aMsg, aJson); michael@0: }, michael@0: michael@0: /************************************************* michael@0: * Browser event handlers michael@0: */ michael@0: michael@0: /* michael@0: * Selection start event handler michael@0: */ michael@0: _onSelectionStart: function _onSelectionStart(aJson) { michael@0: // Init content window information michael@0: if (!this._initTargetInfo(aJson.xPos, aJson.yPos)) { michael@0: this._onFail("failed to get target information"); michael@0: return; michael@0: } michael@0: michael@0: // for context menu select command, which doesn't trigger michael@0: // form input focus changes. michael@0: if (aJson.setFocus && this._targetIsEditable) { michael@0: this._targetElement.focus(); michael@0: } michael@0: michael@0: // Clear any existing selection from the document michael@0: let selection = this._contentWindow.getSelection(); michael@0: selection.removeAllRanges(); michael@0: michael@0: // Set our initial selection, aX and aY should be in client coordinates. michael@0: let framePoint = this._clientPointToFramePoint({ xPos: aJson.xPos, yPos: aJson.yPos }); michael@0: if (!this._domWinUtils.selectAtPoint(framePoint.xPos, framePoint.yPos, michael@0: Ci.nsIDOMWindowUtils.SELECT_WORDNOSPACE)) { michael@0: this._onFail("failed to set selection at point"); michael@0: return; michael@0: } michael@0: michael@0: // Update the position of our selection monocles michael@0: this._updateSelectionUI("start", true, true); michael@0: }, michael@0: michael@0: _onSelectionAttach: function _onSelectionAttach(aX, aY) { michael@0: // Init content window information michael@0: if (!this._initTargetInfo(aX, aY)) { michael@0: this._onFail("failed to get frame offset"); michael@0: return; michael@0: } michael@0: michael@0: // Update the position of our selection monocles michael@0: this._updateSelectionUI("start", true, true); michael@0: }, michael@0: michael@0: /* michael@0: * Switch selection modes. Currently we only support switching michael@0: * from "caret" to "selection". michael@0: */ michael@0: _onSwitchMode: function _onSwitchMode(aMode, aMarker, aX, aY) { michael@0: if (aMode != "selection") { michael@0: this._onFail("unsupported mode switch"); michael@0: return; michael@0: } michael@0: michael@0: // Sanity check to be sure we are initialized michael@0: if (!this._targetElement) { michael@0: this._onFail("not initialized"); michael@0: return; michael@0: } michael@0: michael@0: // Only use selectAtPoint for editable content and avoid that for inputs, michael@0: // as we can expand caret to selection manually more precisely. We can use michael@0: // selectAtPoint for inputs too though, but only once bug 881938 is fully michael@0: // resolved. michael@0: if(Util.isEditableContent(this._targetElement)) { michael@0: // Similar to _onSelectionStart - we need to create initial selection michael@0: // but without the initialization bits. michael@0: let framePoint = this._clientPointToFramePoint({ xPos: aX, yPos: aY }); michael@0: if (!this._domWinUtils.selectAtPoint(framePoint.xPos, framePoint.yPos, michael@0: Ci.nsIDOMWindowUtils.SELECT_CHARACTER)) { michael@0: this._onFail("failed to set selection at point"); michael@0: return; michael@0: } michael@0: } else if (this._targetElement.selectionStart == 0 || aMarker == "end") { michael@0: // Expand caret forward or backward depending on direction michael@0: this._targetElement.selectionEnd++; michael@0: } else { michael@0: this._targetElement.selectionStart--; michael@0: } michael@0: michael@0: // We bail if things get out of sync here implying we missed a message. michael@0: this._selectionMoveActive = true; michael@0: michael@0: // Update the position of the selection marker that is *not* michael@0: // being dragged. michael@0: this._updateSelectionUI("update", aMarker == "end", aMarker == "start"); michael@0: }, michael@0: michael@0: /* michael@0: * Selection monocle start move event handler michael@0: */ michael@0: _onSelectionMoveStart: function _onSelectionMoveStart(aMsg) { michael@0: if (!this._contentWindow) { michael@0: this._onFail("_onSelectionMoveStart was called without proper view set up"); michael@0: return; michael@0: } michael@0: michael@0: if (this._selectionMoveActive) { michael@0: this._onFail("mouse is already down on drag start?"); michael@0: return; michael@0: } michael@0: michael@0: // We bail if things get out of sync here implying we missed a message. michael@0: this._selectionMoveActive = true; michael@0: michael@0: if (this._targetIsEditable) { michael@0: // If we're coming out of an out-of-bounds scroll, the node the user is michael@0: // trying to drag may be hidden (the monocle will be pegged to the edge michael@0: // of the edit). Make sure the node the user wants to move is visible michael@0: // and has focus. michael@0: this._updateInputFocus(aMsg.change); michael@0: } michael@0: michael@0: // Update the position of our selection monocles michael@0: this._updateSelectionUI("update", true, true); michael@0: }, michael@0: michael@0: /* michael@0: * Selection monocle move event handler michael@0: */ michael@0: _onSelectionMove: function _onSelectionMove(aMsg) { michael@0: if (!this._contentWindow) { michael@0: this._onFail("_onSelectionMove was called without proper view set up"); michael@0: return; michael@0: } michael@0: michael@0: if (!this._selectionMoveActive) { michael@0: this._onFail("mouse isn't down for drag move?"); michael@0: return; michael@0: } michael@0: michael@0: this._handleSelectionPoint(aMsg, false); michael@0: }, michael@0: michael@0: /* michael@0: * Selection monocle move finished event handler michael@0: */ michael@0: _onSelectionMoveEnd: function _onSelectionMoveComplete(aMsg) { michael@0: if (!this._contentWindow) { michael@0: this._onFail("_onSelectionMove was called without proper view set up"); michael@0: return; michael@0: } michael@0: michael@0: if (!this._selectionMoveActive) { michael@0: this._onFail("mouse isn't down for drag move?"); michael@0: return; michael@0: } michael@0: michael@0: this._handleSelectionPoint(aMsg, true); michael@0: this._selectionMoveActive = false; michael@0: michael@0: // _handleSelectionPoint may set a scroll timer, so this must michael@0: // be reset after the last call. michael@0: this._clearTimers(); michael@0: michael@0: // Update the position of our selection monocles michael@0: this._updateSelectionUI("end", true, true); michael@0: }, michael@0: michael@0: /* michael@0: * _onCaretAttach - called by SelectionHelperUI when the user taps in a michael@0: * form input. Initializes SelectionHandler, updates the location of the michael@0: * caret, and messages back with current monocle position information. michael@0: * michael@0: * @param aX, aY tap location in client coordinates. michael@0: */ michael@0: _onCaretAttach: function _onCaretAttach(aX, aY) { michael@0: // Init content window information michael@0: if (!this._initTargetInfo(aX, aY)) { michael@0: this._onFail("failed to get target information"); michael@0: return; michael@0: } michael@0: michael@0: // This should never happen, but we check to make sure michael@0: if (!this._targetIsEditable) { michael@0: this._onFail("Coordiates didn't find a text input element."); michael@0: return; michael@0: } michael@0: michael@0: // Locate and sanity check the caret position michael@0: let selection = this._getSelection(); michael@0: if (!selection || !selection.isCollapsed) { michael@0: this._onFail("No selection or selection is not collapsed."); michael@0: return; michael@0: } michael@0: michael@0: // Update the position of our selection monocles michael@0: this._updateSelectionUI("caret", false, false, true); michael@0: }, michael@0: michael@0: /* michael@0: * Selection copy event handler michael@0: * michael@0: * Check to see if the incoming click was on our selection rect. michael@0: * if it was, copy to the clipboard. Incoming coordinates are michael@0: * content values. michael@0: */ michael@0: _onSelectionCopy: function _onSelectionCopy(aMsg) { michael@0: let tap = { michael@0: xPos: aMsg.xPos, michael@0: yPos: aMsg.yPos, michael@0: }; michael@0: michael@0: let tapInSelection = (tap.xPos > this._cache.selection.left && michael@0: tap.xPos < this._cache.selection.right) && michael@0: (tap.yPos > this._cache.selection.top && michael@0: tap.yPos < this._cache.selection.bottom); michael@0: // Util.dumpLn(tapInSelection, michael@0: // tap.xPos, tap.yPos, "|", this._cache.selection.left, michael@0: // this._cache.selection.right, this._cache.selection.top, michael@0: // this._cache.selection.bottom); michael@0: let success = false; michael@0: let selectedText = this._getSelectedText(); michael@0: if (tapInSelection && selectedText.length) { michael@0: let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"] michael@0: .getService(Ci.nsIClipboardHelper); michael@0: clipboard.copyString(selectedText, this._contentWindow.document); michael@0: success = true; michael@0: } michael@0: sendSyncMessage("Content:SelectionCopied", { succeeded: success }); michael@0: }, michael@0: michael@0: /* michael@0: * Selection close event handler michael@0: * michael@0: * @param aClearSelection requests that selection be cleared. michael@0: */ michael@0: _onSelectionClose: function _onSelectionClose(aClearSelection) { michael@0: if (aClearSelection) { michael@0: this._clearSelection(); michael@0: } michael@0: this.closeSelection(); michael@0: }, michael@0: michael@0: /* michael@0: * Called any time SelectionHelperUI would like us to michael@0: * recalculate the selection bounds. michael@0: */ michael@0: _onSelectionUpdate: function _onSelectionUpdate(aMsg) { michael@0: if (!this._contentWindow) { michael@0: this._onFail("_onSelectionUpdate was called without proper view set up"); michael@0: return; michael@0: } michael@0: michael@0: if (aMsg && aMsg.isInitiatedByAPZC) { michael@0: let {offset: offset} = Content.getCurrentWindowAndOffset( michael@0: this._targetCoordinates.x, this._targetCoordinates.y); michael@0: this._contentOffset = offset; michael@0: } michael@0: michael@0: // Update the position of our selection monocles michael@0: this._updateSelectionUI("update", true, true); michael@0: }, michael@0: michael@0: /* michael@0: * Called if for any reason we fail during the selection michael@0: * process. Cancels the selection. michael@0: */ michael@0: _onFail: function _onFail(aDbgMessage) { michael@0: if (aDbgMessage && aDbgMessage.length > 0) michael@0: Util.dumpLn(aDbgMessage); michael@0: this.sendAsync("Content:SelectionFail"); michael@0: this._clearSelection(); michael@0: this.closeSelection(); michael@0: }, michael@0: michael@0: /* michael@0: * _repositionInfoRequest - fired at us by ContentAreaObserver when the michael@0: * soft keyboard is being displayed. CAO wants to make a decision about michael@0: * whether the browser deck needs repositioning. michael@0: */ michael@0: _repositionInfoRequest: function _repositionInfoRequest(aJsonMsg) { michael@0: let result = this._calcNewContentPosition(aJsonMsg.viewHeight); michael@0: michael@0: // no repositioning needed michael@0: if (result == 0) { michael@0: this.sendAsync("Content:RepositionInfoResponse", { reposition: false }); michael@0: return; michael@0: } michael@0: michael@0: this.sendAsync("Content:RepositionInfoResponse", { michael@0: reposition: true, michael@0: raiseContent: result, michael@0: }); michael@0: }, michael@0: michael@0: _onPing: function _onPing(aId) { michael@0: this.sendAsync("Content:SelectionHandlerPong", { id: aId }); michael@0: }, michael@0: michael@0: onClickCoords: function (xPos, yPos) { michael@0: this.lastXPos = xPos; michael@0: this.lastYPos = yPos; michael@0: }, michael@0: michael@0: /************************************************* michael@0: * Selection helpers michael@0: */ michael@0: michael@0: /* michael@0: * _clearSelection michael@0: * michael@0: * Clear existing selection if it exists and reset our internla state. michael@0: */ michael@0: _clearSelection: function _clearSelection() { michael@0: this._clearTimers(); michael@0: if (this._contentWindow) { michael@0: let selection = this._getSelection(); michael@0: if (selection) michael@0: selection.removeAllRanges(); michael@0: } else { michael@0: let selection = content.getSelection(); michael@0: if (selection) michael@0: selection.removeAllRanges(); michael@0: } michael@0: }, michael@0: michael@0: /* michael@0: * closeSelection michael@0: * michael@0: * Shuts SelectionHandler down. michael@0: */ michael@0: closeSelection: function closeSelection() { michael@0: this._clearTimers(); michael@0: this._cache = null; michael@0: this._contentWindow = null; michael@0: this._targetElement = null; michael@0: this._selectionMoveActive = false; michael@0: this._contentOffset = null; michael@0: this._domWinUtils = null; michael@0: this._targetIsEditable = false; michael@0: this._targetCoordinates = null; michael@0: sendSyncMessage("Content:HandlerShutdown", {}); michael@0: }, michael@0: michael@0: /* michael@0: * Find content within frames - cache the target nsIDOMWindow, michael@0: * client coordinate offset, target element, and dom utils interface. michael@0: */ michael@0: _initTargetInfo: function _initTargetInfo(aX, aY) { michael@0: // getCurrentWindowAndOffset takes client coordinates michael@0: let { element: element, michael@0: contentWindow: contentWindow, michael@0: offset: offset, michael@0: utils: utils } = michael@0: Content.getCurrentWindowAndOffset(aX, aY); michael@0: if (!contentWindow) { michael@0: return false; michael@0: } michael@0: this._targetElement = element; michael@0: this._contentWindow = contentWindow; michael@0: this._contentOffset = offset; michael@0: this._domWinUtils = utils; michael@0: this._targetIsEditable = Util.isEditable(this._targetElement); michael@0: this._targetCoordinates = { michael@0: x: aX, michael@0: y: aY michael@0: }; michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: /* michael@0: * _calcNewContentPosition - calculates the distance the browser should be michael@0: * raised to move the focused form input out of the way of the soft michael@0: * keyboard. michael@0: * michael@0: * @param aNewViewHeight the new content view height michael@0: * @return 0 if no positioning is required or a positive val equal to the michael@0: * distance content should be raised to center the target element. michael@0: */ michael@0: _calcNewContentPosition: function _calcNewContentPosition(aNewViewHeight) { michael@0: // We have no target element but the keyboard is up michael@0: // so lets not cover content that is below the keyboard michael@0: if (!this._cache || !this._cache.element) { michael@0: if (this.lastYPos != null && this.lastYPos > aNewViewHeight) { michael@0: return Services.metro.keyboardHeight; michael@0: } michael@0: return 0; michael@0: } michael@0: michael@0: let position = Util.centerElementInView(aNewViewHeight, this._cache.element); michael@0: if (position !== undefined) { michael@0: return position; michael@0: } michael@0: michael@0: // Special case: we are dealing with an input that is taller than the michael@0: // desired height of content. We need to center on the caret location. michael@0: let rect = michael@0: this._domWinUtils.sendQueryContentEvent( michael@0: this._domWinUtils.QUERY_CARET_RECT, michael@0: this._targetElement.selectionEnd, michael@0: 0, 0, 0, michael@0: this._domWinUtils.QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK); michael@0: if (!rect || !rect.succeeded) { michael@0: Util.dumpLn("no caret was present, unexpected."); michael@0: return 0; michael@0: } michael@0: michael@0: // Note sendQueryContentEvent with QUERY_CARET_RECT is really buggy. If it michael@0: // can't find the exact location of the caret position it will "guess". michael@0: // Sometimes this can put the result in unexpected locations. michael@0: let caretLocation = Math.max(Math.min(Math.round(rect.top + (rect.height * .5)), michael@0: viewBottom), 0); michael@0: michael@0: // Caret is above the bottom of the new view bounds, no need to shift. michael@0: if (caretLocation <= aNewViewHeight) { michael@0: return 0; michael@0: } michael@0: michael@0: // distance from the top of the keyboard down to the caret location michael@0: return caretLocation - aNewViewHeight; michael@0: }, michael@0: michael@0: /************************************************* michael@0: * Events michael@0: */ michael@0: michael@0: /* michael@0: * Scroll + selection advancement timer when the monocle is michael@0: * outside the bounds of an input control. michael@0: */ michael@0: scrollTimerCallback: function scrollTimerCallback() { michael@0: let result = SelectionHandler.updateTextEditSelection(); michael@0: // Update monocle position and speed if we've dragged off to one side michael@0: if (result.trigger) { michael@0: SelectionHandler._updateSelectionUI("update", result.start, result.end); michael@0: } michael@0: }, michael@0: michael@0: receiveMessage: function sh_receiveMessage(aMessage) { michael@0: if (this._debugEvents && aMessage.name != "Browser:SelectionMove") { michael@0: Util.dumpLn("SelectionHandler:", aMessage.name); michael@0: } michael@0: let json = aMessage.json; michael@0: switch (aMessage.name) { michael@0: case "Browser:SelectionStart": michael@0: this._onSelectionStart(json); michael@0: break; michael@0: michael@0: case "Browser:SelectionAttach": michael@0: this._onSelectionAttach(json.xPos, json.yPos); michael@0: break; michael@0: michael@0: case "Browser:CaretAttach": michael@0: this._onCaretAttach(json.xPos, json.yPos); michael@0: break; michael@0: michael@0: case "Browser:CaretMove": michael@0: this._onCaretMove(json.caret.xPos, json.caret.yPos); michael@0: break; michael@0: michael@0: case "Browser:CaretUpdate": michael@0: this._onCaretPositionUpdate(json.caret.xPos, json.caret.yPos); michael@0: break; michael@0: michael@0: case "Browser:SelectionSwitchMode": michael@0: this._onSwitchMode(json.newMode, json.change, json.xPos, json.yPos); michael@0: break; michael@0: michael@0: case "Browser:SelectionClose": michael@0: this._onSelectionClose(json.clearSelection); michael@0: break; michael@0: michael@0: case "Browser:SelectionMoveStart": michael@0: this._onSelectionMoveStart(json); michael@0: break; michael@0: michael@0: case "Browser:SelectionMove": michael@0: this._onSelectionMove(json); michael@0: break; michael@0: michael@0: case "Browser:SelectionMoveEnd": michael@0: this._onSelectionMoveEnd(json); michael@0: break; michael@0: michael@0: case "Browser:SelectionCopy": michael@0: this._onSelectionCopy(json); michael@0: break; michael@0: michael@0: case "Browser:SelectionDebug": michael@0: this._onSelectionDebug(json); michael@0: break; michael@0: michael@0: case "Browser:SelectionUpdate": michael@0: this._onSelectionUpdate(json); michael@0: break; michael@0: michael@0: case "Browser:RepositionInfoRequest": michael@0: // This message is sent simultaneously with a tap event. michael@0: // Wait a bit to make sure we have the most up-to-date tap co-ordinates michael@0: // before a call to _calcNewContentPosition() which accesses them. michael@0: content.setTimeout (function () { michael@0: SelectionHandler._repositionInfoRequest(json); michael@0: }, 50); michael@0: break; michael@0: michael@0: case "Browser:SelectionHandlerPing": michael@0: this._onPing(json.id); michael@0: break; michael@0: michael@0: case "Browser:ResetLastPos": michael@0: this.onClickCoords(json.xPos, json.yPos); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: /************************************************* michael@0: * Utilities michael@0: */ michael@0: michael@0: _getDocShell: function _getDocShell(aWindow) { michael@0: if (aWindow == null) michael@0: return null; michael@0: return aWindow.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebNavigation) michael@0: .QueryInterface(Ci.nsIDocShell); michael@0: }, michael@0: michael@0: _getSelectedText: function _getSelectedText() { michael@0: let selection = this._getSelection(); michael@0: if (selection) michael@0: return selection.toString(); michael@0: return ""; michael@0: }, michael@0: michael@0: _getSelection: function _getSelection() { michael@0: if (this._targetElement instanceof Ci.nsIDOMNSEditableElement) { michael@0: return this._targetElement michael@0: .QueryInterface(Ci.nsIDOMNSEditableElement) michael@0: .editor.selection; michael@0: } else if (this._contentWindow) michael@0: return this._contentWindow.getSelection(); michael@0: return null; michael@0: }, michael@0: michael@0: _getSelectController: function _getSelectController() { michael@0: if (this._targetElement instanceof Ci.nsIDOMNSEditableElement) { michael@0: return this._targetElement michael@0: .QueryInterface(Ci.nsIDOMNSEditableElement) michael@0: .editor.selectionController; michael@0: } else { michael@0: let docShell = this._getDocShell(this._contentWindow); michael@0: if (docShell == null) michael@0: return null; michael@0: return docShell.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsISelectionDisplay) michael@0: .QueryInterface(Ci.nsISelectionController); michael@0: } michael@0: }, michael@0: }; michael@0: this.SelectionHandler = SelectionHandler; michael@0: michael@0: SelectionHandler.__proto__ = new SelectionPrototype(); michael@0: SelectionHandler.init(); michael@0: