1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/metro/base/content/helperui/ChromeSelectionHandler.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,435 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +/* 1.9 + * Selection handler for chrome text inputs 1.10 + */ 1.11 + 1.12 +let Ci = Components.interfaces; 1.13 + 1.14 +var ChromeSelectionHandler = { 1.15 + _mode: this._SELECTION_MODE, 1.16 + 1.17 + /************************************************* 1.18 + * Messaging wrapper 1.19 + */ 1.20 + 1.21 + sendAsync: function sendAsync(aMsg, aJson) { 1.22 + SelectionHelperUI.receiveMessage({ 1.23 + name: aMsg, 1.24 + json: aJson 1.25 + }); 1.26 + }, 1.27 + 1.28 + /************************************************* 1.29 + * Browser event handlers 1.30 + */ 1.31 + 1.32 + /* 1.33 + * General selection start method for both caret and selection mode. 1.34 + */ 1.35 + _onSelectionAttach: function _onSelectionAttach(aJson) { 1.36 + // Clear previous ChromeSelectionHandler state. 1.37 + this._deactivate(); 1.38 + 1.39 + // Initialize ChromeSelectionHandler state. 1.40 + this._domWinUtils = Util.getWindowUtils(window); 1.41 + this._contentWindow = window; 1.42 + this._targetElement = aJson.target; 1.43 + this._targetIsEditable = Util.isTextInput(this._targetElement) || 1.44 + this._targetElement instanceof Ci.nsIDOMXULTextBoxElement; 1.45 + if (!this._targetIsEditable) { 1.46 + this._onFail("not an editable?", this._targetElement); 1.47 + return; 1.48 + } 1.49 + 1.50 + let selection = this._getSelection(); 1.51 + if (!selection) { 1.52 + this._onFail("no selection."); 1.53 + return; 1.54 + } 1.55 + 1.56 + if (!this._getTargetElementValue()) { 1.57 + this._onFail("Target element does not contain any content to select."); 1.58 + return; 1.59 + } 1.60 + 1.61 + // Add a listener to respond to programmatic selection changes. 1.62 + selection.QueryInterface(Ci.nsISelectionPrivate).addSelectionListener(this); 1.63 + 1.64 + if (!selection.isCollapsed) { 1.65 + this._mode = this._SELECTION_MODE; 1.66 + this._updateSelectionUI("start", true, true); 1.67 + } else { 1.68 + this._mode = this._CARET_MODE; 1.69 + this._updateSelectionUI("caret", false, false, true); 1.70 + } 1.71 + 1.72 + this._targetElement.addEventListener("blur", this, false); 1.73 + }, 1.74 + 1.75 + /* 1.76 + * Selection monocle start move event handler 1.77 + */ 1.78 + _onSelectionMoveStart: function _onSelectionMoveStart(aMsg) { 1.79 + if (!this.targetIsEditable) { 1.80 + this._onFail("_onSelectionMoveStart with bad targetElement."); 1.81 + return; 1.82 + } 1.83 + 1.84 + if (this._selectionMoveActive) { 1.85 + this._onFail("mouse is already down on drag start?"); 1.86 + return; 1.87 + } 1.88 + 1.89 + // We bail if things get out of sync here implying we missed a message. 1.90 + this._selectionMoveActive = true; 1.91 + 1.92 + if (this._targetIsEditable) { 1.93 + // If we're coming out of an out-of-bounds scroll, the node the user is 1.94 + // trying to drag may be hidden (the monocle will be pegged to the edge 1.95 + // of the edit). Make sure the node the user wants to move is visible 1.96 + // and has focus. 1.97 + this._updateInputFocus(aMsg.change); 1.98 + } 1.99 + 1.100 + // Update the position of our selection monocles 1.101 + this._updateSelectionUI("update", true, true); 1.102 + }, 1.103 + 1.104 + /* 1.105 + * Selection monocle move event handler 1.106 + */ 1.107 + _onSelectionMove: function _onSelectionMove(aMsg) { 1.108 + if (!this.targetIsEditable) { 1.109 + this._onFail("_onSelectionMove with bad targetElement."); 1.110 + return; 1.111 + } 1.112 + 1.113 + if (!this._selectionMoveActive) { 1.114 + this._onFail("mouse isn't down for drag move?"); 1.115 + return; 1.116 + } 1.117 + 1.118 + this._handleSelectionPoint(aMsg, false); 1.119 + }, 1.120 + 1.121 + /* 1.122 + * Selection monocle move finished event handler 1.123 + */ 1.124 + _onSelectionMoveEnd: function _onSelectionMoveComplete(aMsg) { 1.125 + if (!this.targetIsEditable) { 1.126 + this._onFail("_onSelectionMoveEnd with bad targetElement."); 1.127 + return; 1.128 + } 1.129 + 1.130 + if (!this._selectionMoveActive) { 1.131 + this._onFail("mouse isn't down for drag move?"); 1.132 + return; 1.133 + } 1.134 + 1.135 + this._handleSelectionPoint(aMsg, true); 1.136 + this._selectionMoveActive = false; 1.137 + 1.138 + // Clear any existing scroll timers 1.139 + this._clearTimers(); 1.140 + 1.141 + // Update the position of our selection monocles 1.142 + this._updateSelectionUI("end", true, true); 1.143 + }, 1.144 + 1.145 + _onSelectionUpdate: function _onSelectionUpdate() { 1.146 + if (!this._targetHasFocus()) { 1.147 + this._closeSelection(); 1.148 + return; 1.149 + } 1.150 + this._updateSelectionUI("update", 1.151 + this._mode == this._SELECTION_MODE, 1.152 + this._mode == this._SELECTION_MODE, 1.153 + this._mode == this._CARET_MODE); 1.154 + }, 1.155 + 1.156 + /* 1.157 + * Switch selection modes. Currently we only support switching 1.158 + * from "caret" to "selection". 1.159 + */ 1.160 + _onSwitchMode: function _onSwitchMode(aMode, aMarker, aX, aY) { 1.161 + if (aMode != "selection") { 1.162 + this._onFail("unsupported mode switch"); 1.163 + return; 1.164 + } 1.165 + 1.166 + // Sanity check to be sure we are initialized 1.167 + if (!this._targetElement) { 1.168 + this._onFail("not initialized"); 1.169 + return; 1.170 + } 1.171 + 1.172 + // Similar to _onSelectionStart - we need to create initial selection 1.173 + // but without the initialization bits. 1.174 + let framePoint = this._clientPointToFramePoint({ xPos: aX, yPos: aY }); 1.175 + if (!this._domWinUtils.selectAtPoint(framePoint.xPos, framePoint.yPos, 1.176 + Ci.nsIDOMWindowUtils.SELECT_CHARACTER)) { 1.177 + this._onFail("failed to set selection at point"); 1.178 + return; 1.179 + } 1.180 + 1.181 + // We bail if things get out of sync here implying we missed a message. 1.182 + this._selectionMoveActive = true; 1.183 + this._mode = this._SELECTION_MODE; 1.184 + 1.185 + // Update the position of the selection marker that is *not* 1.186 + // being dragged. 1.187 + this._updateSelectionUI("update", aMarker == "end", aMarker == "start"); 1.188 + }, 1.189 + 1.190 + /* 1.191 + * Selection close event handler 1.192 + * 1.193 + * @param aClearSelection requests that selection be cleared. 1.194 + */ 1.195 + _onSelectionClose: function _onSelectionClose(aClearSelection) { 1.196 + if (aClearSelection) { 1.197 + this._clearSelection(); 1.198 + } 1.199 + this._closeSelection(); 1.200 + }, 1.201 + 1.202 + /* 1.203 + * Called if for any reason we fail during the selection 1.204 + * process. Cancels the selection. 1.205 + */ 1.206 + _onFail: function _onFail(aDbgMessage) { 1.207 + if (aDbgMessage && aDbgMessage.length > 0) 1.208 + Util.dumpLn(aDbgMessage); 1.209 + this.sendAsync("Content:SelectionFail"); 1.210 + this._clearSelection(); 1.211 + this._closeSelection(); 1.212 + }, 1.213 + 1.214 + /* 1.215 + * Empty conversion routines to match those in 1.216 + * browser. Called by SelectionHelperUI when 1.217 + * sending and receiving coordinates in messages. 1.218 + */ 1.219 + 1.220 + ptClientToBrowser: function ptClientToBrowser(aX, aY, aIgnoreScroll, aIgnoreScale) { 1.221 + return { x: aX, y: aY } 1.222 + }, 1.223 + 1.224 + rectBrowserToClient: function rectBrowserToClient(aRect, aIgnoreScroll, aIgnoreScale) { 1.225 + return { 1.226 + left: aRect.left, 1.227 + right: aRect.right, 1.228 + top: aRect.top, 1.229 + bottom: aRect.bottom 1.230 + } 1.231 + }, 1.232 + 1.233 + ptBrowserToClient: function ptBrowserToClient(aX, aY, aIgnoreScroll, aIgnoreScale) { 1.234 + return { x: aX, y: aY } 1.235 + }, 1.236 + 1.237 + ctobx: function ctobx(aCoord) { 1.238 + return aCoord; 1.239 + }, 1.240 + 1.241 + ctoby: function ctoby(aCoord) { 1.242 + return aCoord; 1.243 + }, 1.244 + 1.245 + btocx: function btocx(aCoord) { 1.246 + return aCoord; 1.247 + }, 1.248 + 1.249 + btocy: function btocy(aCoord) { 1.250 + return aCoord; 1.251 + }, 1.252 + 1.253 + 1.254 + /************************************************* 1.255 + * Selection helpers 1.256 + */ 1.257 + 1.258 + /* 1.259 + * _clearSelection 1.260 + * 1.261 + * Clear existing selection if it exists and reset our internal state. 1.262 + */ 1.263 + _clearSelection: function _clearSelection() { 1.264 + let selection = this._getSelection(); 1.265 + if (selection) { 1.266 + selection.removeAllRanges(); 1.267 + } 1.268 + }, 1.269 + 1.270 + /* 1.271 + * _closeSelection 1.272 + * 1.273 + * Shuts ChromeSelectionHandler and SelectionHelperUI down. 1.274 + */ 1.275 + _closeSelection: function _closeSelection() { 1.276 + this._deactivate(); 1.277 + this.sendAsync("Content:HandlerShutdown", {}); 1.278 + }, 1.279 + 1.280 + /* 1.281 + * _deactivate 1.282 + * 1.283 + * Resets ChromeSelectionHandler state, previously initialized in 1.284 + * general selection start-method |_onSelectionAttach()|. 1.285 + */ 1.286 + _deactivate: function _deactivate() { 1.287 + // Remove our selection notification listener. 1.288 + let selection = this._getSelection(); 1.289 + if (selection) { 1.290 + try { 1.291 + selection.QueryInterface(Ci.nsISelectionPrivate).removeSelectionListener(this); 1.292 + } catch(e) { 1.293 + // Fail safe during multiple _deactivate() calls. 1.294 + } 1.295 + } 1.296 + 1.297 + this._clearTimers(); 1.298 + this._cache = null; 1.299 + this._contentWindow = null; 1.300 + if (this._targetElement) { 1.301 + this._targetElement.removeEventListener("blur", this, true); 1.302 + this._targetElement = null; 1.303 + } 1.304 + this._selectionMoveActive = false; 1.305 + this._domWinUtils = null; 1.306 + this._targetIsEditable = false; 1.307 + this._mode = null; 1.308 + }, 1.309 + 1.310 + get hasSelection() { 1.311 + if (!this._targetElement) { 1.312 + return false; 1.313 + } 1.314 + let selection = this._getSelection(); 1.315 + return (selection && !selection.isCollapsed); 1.316 + }, 1.317 + 1.318 + _targetHasFocus: function() { 1.319 + if (!this._targetElement || !document.commandDispatcher.focusedElement) { 1.320 + return false; 1.321 + } 1.322 + let bindingParent = this._contentWindow.document.getBindingParent(document.commandDispatcher.focusedElement); 1.323 + return (bindingParent && this._targetElement == bindingParent); 1.324 + }, 1.325 + 1.326 + /************************************************* 1.327 + * Events 1.328 + */ 1.329 + 1.330 + /* 1.331 + * Scroll + selection advancement timer when the monocle is 1.332 + * outside the bounds of an input control. 1.333 + */ 1.334 + scrollTimerCallback: function scrollTimerCallback() { 1.335 + let result = ChromeSelectionHandler.updateTextEditSelection(); 1.336 + // Update monocle position and speed if we've dragged off to one side 1.337 + if (result.trigger) { 1.338 + ChromeSelectionHandler._updateSelectionUI("update", result.start, result.end); 1.339 + } 1.340 + }, 1.341 + 1.342 + handleEvent: function handleEvent(aEvent) { 1.343 + if (aEvent.type == "blur" && !this._targetHasFocus()) { 1.344 + this._closeSelection(); 1.345 + } 1.346 + }, 1.347 + 1.348 + msgHandler: function msgHandler(aMsg, aJson) { 1.349 + if (this._debugEvents && "Browser:SelectionMove" != aMsg) { 1.350 + Util.dumpLn("ChromeSelectionHandler:", aMsg); 1.351 + } 1.352 + switch(aMsg) { 1.353 + case "Browser:SelectionDebug": 1.354 + this._onSelectionDebug(aJson); 1.355 + break; 1.356 + 1.357 + case "Browser:SelectionAttach": 1.358 + this._onSelectionAttach(aJson); 1.359 + break; 1.360 + 1.361 + case "Browser:CaretAttach": 1.362 + this._onSelectionAttach(aJson); 1.363 + break; 1.364 + 1.365 + case "Browser:SelectionClose": 1.366 + this._onSelectionClose(aJson.clearSelection); 1.367 + break; 1.368 + 1.369 + case "Browser:SelectionUpdate": 1.370 + this._onSelectionUpdate(); 1.371 + break; 1.372 + 1.373 + case "Browser:SelectionMoveStart": 1.374 + this._onSelectionMoveStart(aJson); 1.375 + break; 1.376 + 1.377 + case "Browser:SelectionMove": 1.378 + this._onSelectionMove(aJson); 1.379 + break; 1.380 + 1.381 + case "Browser:SelectionMoveEnd": 1.382 + this._onSelectionMoveEnd(aJson); 1.383 + break; 1.384 + 1.385 + case "Browser:CaretUpdate": 1.386 + this._onCaretPositionUpdate(aJson.caret.xPos, aJson.caret.yPos); 1.387 + break; 1.388 + 1.389 + case "Browser:CaretMove": 1.390 + this._onCaretMove(aJson.caret.xPos, aJson.caret.yPos); 1.391 + break; 1.392 + 1.393 + case "Browser:SelectionSwitchMode": 1.394 + this._onSwitchMode(aJson.newMode, aJson.change, aJson.xPos, aJson.yPos); 1.395 + break; 1.396 + } 1.397 + }, 1.398 + 1.399 + /************************************************* 1.400 + * Utilities 1.401 + */ 1.402 + 1.403 + _getSelection: function _getSelection() { 1.404 + let targetElementEditor = this._getTargetElementEditor(); 1.405 + 1.406 + return targetElementEditor ? targetElementEditor.selection : null; 1.407 + }, 1.408 + 1.409 + _getTargetElementValue: function _getTargetElementValue() { 1.410 + if (this._targetElement instanceof Ci.nsIDOMXULTextBoxElement) { 1.411 + return this._targetElement.inputField.value; 1.412 + } else if (Util.isTextInput(this._targetElement)) { 1.413 + return this._targetElement.value; 1.414 + } 1.415 + return null; 1.416 + }, 1.417 + 1.418 + _getSelectController: function _getSelectController() { 1.419 + let targetElementEditor = this._getTargetElementEditor(); 1.420 + 1.421 + return targetElementEditor ? targetElementEditor.selectionController : null; 1.422 + }, 1.423 + 1.424 + _getTargetElementEditor: function() { 1.425 + if (this._targetElement instanceof Ci.nsIDOMXULTextBoxElement) { 1.426 + return this._targetElement.QueryInterface(Ci.nsIDOMXULTextBoxElement) 1.427 + .editor; 1.428 + } else if (Util.isTextInput(this._targetElement)) { 1.429 + return this._targetElement.QueryInterface(Ci.nsIDOMNSEditableElement) 1.430 + .editor; 1.431 + } 1.432 + return null; 1.433 + } 1.434 +}; 1.435 + 1.436 +ChromeSelectionHandler.__proto__ = new SelectionPrototype(); 1.437 +ChromeSelectionHandler.type = 1; // kChromeSelector 1.438 +