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 handler for chrome text inputs michael@0: */ michael@0: michael@0: let Ci = Components.interfaces; michael@0: michael@0: var ChromeSelectionHandler = { michael@0: _mode: this._SELECTION_MODE, michael@0: michael@0: /************************************************* michael@0: * Messaging wrapper michael@0: */ michael@0: michael@0: sendAsync: function sendAsync(aMsg, aJson) { michael@0: SelectionHelperUI.receiveMessage({ michael@0: name: aMsg, michael@0: json: aJson michael@0: }); michael@0: }, michael@0: michael@0: /************************************************* michael@0: * Browser event handlers michael@0: */ michael@0: michael@0: /* michael@0: * General selection start method for both caret and selection mode. michael@0: */ michael@0: _onSelectionAttach: function _onSelectionAttach(aJson) { michael@0: // Clear previous ChromeSelectionHandler state. michael@0: this._deactivate(); michael@0: michael@0: // Initialize ChromeSelectionHandler state. michael@0: this._domWinUtils = Util.getWindowUtils(window); michael@0: this._contentWindow = window; michael@0: this._targetElement = aJson.target; michael@0: this._targetIsEditable = Util.isTextInput(this._targetElement) || michael@0: this._targetElement instanceof Ci.nsIDOMXULTextBoxElement; michael@0: if (!this._targetIsEditable) { michael@0: this._onFail("not an editable?", this._targetElement); michael@0: return; michael@0: } michael@0: michael@0: let selection = this._getSelection(); michael@0: if (!selection) { michael@0: this._onFail("no selection."); michael@0: return; michael@0: } michael@0: michael@0: if (!this._getTargetElementValue()) { michael@0: this._onFail("Target element does not contain any content to select."); michael@0: return; michael@0: } michael@0: michael@0: // Add a listener to respond to programmatic selection changes. michael@0: selection.QueryInterface(Ci.nsISelectionPrivate).addSelectionListener(this); michael@0: michael@0: if (!selection.isCollapsed) { michael@0: this._mode = this._SELECTION_MODE; michael@0: this._updateSelectionUI("start", true, true); michael@0: } else { michael@0: this._mode = this._CARET_MODE; michael@0: this._updateSelectionUI("caret", false, false, true); michael@0: } michael@0: michael@0: this._targetElement.addEventListener("blur", this, false); 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.targetIsEditable) { michael@0: this._onFail("_onSelectionMoveStart with bad targetElement."); 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.targetIsEditable) { michael@0: this._onFail("_onSelectionMove with bad targetElement."); 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.targetIsEditable) { michael@0: this._onFail("_onSelectionMoveEnd with bad targetElement."); 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: // Clear any existing scroll timers 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: _onSelectionUpdate: function _onSelectionUpdate() { michael@0: if (!this._targetHasFocus()) { michael@0: this._closeSelection(); michael@0: return; michael@0: } michael@0: this._updateSelectionUI("update", michael@0: this._mode == this._SELECTION_MODE, michael@0: this._mode == this._SELECTION_MODE, michael@0: this._mode == this._CARET_MODE); 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: // 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: michael@0: // We bail if things get out of sync here implying we missed a message. michael@0: this._selectionMoveActive = true; michael@0: this._mode = this._SELECTION_MODE; 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 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 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: * Empty conversion routines to match those in michael@0: * browser. Called by SelectionHelperUI when michael@0: * sending and receiving coordinates in messages. michael@0: */ michael@0: michael@0: ptClientToBrowser: function ptClientToBrowser(aX, aY, aIgnoreScroll, aIgnoreScale) { michael@0: return { x: aX, y: aY } michael@0: }, michael@0: michael@0: rectBrowserToClient: function rectBrowserToClient(aRect, aIgnoreScroll, aIgnoreScale) { michael@0: return { michael@0: left: aRect.left, michael@0: right: aRect.right, michael@0: top: aRect.top, michael@0: bottom: aRect.bottom michael@0: } michael@0: }, michael@0: michael@0: ptBrowserToClient: function ptBrowserToClient(aX, aY, aIgnoreScroll, aIgnoreScale) { michael@0: return { x: aX, y: aY } michael@0: }, michael@0: michael@0: ctobx: function ctobx(aCoord) { michael@0: return aCoord; michael@0: }, michael@0: michael@0: ctoby: function ctoby(aCoord) { michael@0: return aCoord; michael@0: }, michael@0: michael@0: btocx: function btocx(aCoord) { michael@0: return aCoord; michael@0: }, michael@0: michael@0: btocy: function btocy(aCoord) { michael@0: return aCoord; michael@0: }, 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 internal state. michael@0: */ michael@0: _clearSelection: function _clearSelection() { michael@0: let selection = this._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 ChromeSelectionHandler and SelectionHelperUI down. michael@0: */ michael@0: _closeSelection: function _closeSelection() { michael@0: this._deactivate(); michael@0: this.sendAsync("Content:HandlerShutdown", {}); michael@0: }, michael@0: michael@0: /* michael@0: * _deactivate michael@0: * michael@0: * Resets ChromeSelectionHandler state, previously initialized in michael@0: * general selection start-method |_onSelectionAttach()|. michael@0: */ michael@0: _deactivate: function _deactivate() { michael@0: // Remove our selection notification listener. michael@0: let selection = this._getSelection(); michael@0: if (selection) { michael@0: try { michael@0: selection.QueryInterface(Ci.nsISelectionPrivate).removeSelectionListener(this); michael@0: } catch(e) { michael@0: // Fail safe during multiple _deactivate() calls. michael@0: } michael@0: } michael@0: michael@0: this._clearTimers(); michael@0: this._cache = null; michael@0: this._contentWindow = null; michael@0: if (this._targetElement) { michael@0: this._targetElement.removeEventListener("blur", this, true); michael@0: this._targetElement = null; michael@0: } michael@0: this._selectionMoveActive = false; michael@0: this._domWinUtils = null; michael@0: this._targetIsEditable = false; michael@0: this._mode = null; michael@0: }, michael@0: michael@0: get hasSelection() { michael@0: if (!this._targetElement) { michael@0: return false; michael@0: } michael@0: let selection = this._getSelection(); michael@0: return (selection && !selection.isCollapsed); michael@0: }, michael@0: michael@0: _targetHasFocus: function() { michael@0: if (!this._targetElement || !document.commandDispatcher.focusedElement) { michael@0: return false; michael@0: } michael@0: let bindingParent = this._contentWindow.document.getBindingParent(document.commandDispatcher.focusedElement); michael@0: return (bindingParent && this._targetElement == bindingParent); 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 = ChromeSelectionHandler.updateTextEditSelection(); michael@0: // Update monocle position and speed if we've dragged off to one side michael@0: if (result.trigger) { michael@0: ChromeSelectionHandler._updateSelectionUI("update", result.start, result.end); michael@0: } michael@0: }, michael@0: michael@0: handleEvent: function handleEvent(aEvent) { michael@0: if (aEvent.type == "blur" && !this._targetHasFocus()) { michael@0: this._closeSelection(); michael@0: } michael@0: }, michael@0: michael@0: msgHandler: function msgHandler(aMsg, aJson) { michael@0: if (this._debugEvents && "Browser:SelectionMove" != aMsg) { michael@0: Util.dumpLn("ChromeSelectionHandler:", aMsg); michael@0: } michael@0: switch(aMsg) { michael@0: case "Browser:SelectionDebug": michael@0: this._onSelectionDebug(aJson); michael@0: break; michael@0: michael@0: case "Browser:SelectionAttach": michael@0: this._onSelectionAttach(aJson); michael@0: break; michael@0: michael@0: case "Browser:CaretAttach": michael@0: this._onSelectionAttach(aJson); michael@0: break; michael@0: michael@0: case "Browser:SelectionClose": michael@0: this._onSelectionClose(aJson.clearSelection); michael@0: break; michael@0: michael@0: case "Browser:SelectionUpdate": michael@0: this._onSelectionUpdate(); michael@0: break; michael@0: michael@0: case "Browser:SelectionMoveStart": michael@0: this._onSelectionMoveStart(aJson); michael@0: break; michael@0: michael@0: case "Browser:SelectionMove": michael@0: this._onSelectionMove(aJson); michael@0: break; michael@0: michael@0: case "Browser:SelectionMoveEnd": michael@0: this._onSelectionMoveEnd(aJson); michael@0: break; michael@0: michael@0: case "Browser:CaretUpdate": michael@0: this._onCaretPositionUpdate(aJson.caret.xPos, aJson.caret.yPos); michael@0: break; michael@0: michael@0: case "Browser:CaretMove": michael@0: this._onCaretMove(aJson.caret.xPos, aJson.caret.yPos); michael@0: break; michael@0: michael@0: case "Browser:SelectionSwitchMode": michael@0: this._onSwitchMode(aJson.newMode, aJson.change, aJson.xPos, aJson.yPos); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: /************************************************* michael@0: * Utilities michael@0: */ michael@0: michael@0: _getSelection: function _getSelection() { michael@0: let targetElementEditor = this._getTargetElementEditor(); michael@0: michael@0: return targetElementEditor ? targetElementEditor.selection : null; michael@0: }, michael@0: michael@0: _getTargetElementValue: function _getTargetElementValue() { michael@0: if (this._targetElement instanceof Ci.nsIDOMXULTextBoxElement) { michael@0: return this._targetElement.inputField.value; michael@0: } else if (Util.isTextInput(this._targetElement)) { michael@0: return this._targetElement.value; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: _getSelectController: function _getSelectController() { michael@0: let targetElementEditor = this._getTargetElementEditor(); michael@0: michael@0: return targetElementEditor ? targetElementEditor.selectionController : null; michael@0: }, michael@0: michael@0: _getTargetElementEditor: function() { michael@0: if (this._targetElement instanceof Ci.nsIDOMXULTextBoxElement) { michael@0: return this._targetElement.QueryInterface(Ci.nsIDOMXULTextBoxElement) michael@0: .editor; michael@0: } else if (Util.isTextInput(this._targetElement)) { michael@0: return this._targetElement.QueryInterface(Ci.nsIDOMNSEditableElement) michael@0: .editor; michael@0: } michael@0: return null; michael@0: } michael@0: }; michael@0: michael@0: ChromeSelectionHandler.__proto__ = new SelectionPrototype(); michael@0: ChromeSelectionHandler.type = 1; // kChromeSelector michael@0: