1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/metro/base/content/helperui/SelectionHelperUI.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1186 @@ 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 management 1.10 + */ 1.11 + 1.12 +/* 1.13 + * Current monocle image: 1.14 + * dimensions: 32 x 24 1.15 + * circle center: 16 x 14 1.16 + * padding top: 6 1.17 + */ 1.18 + 1.19 +// Y axis scroll distance that will disable this module and cancel selection 1.20 +const kDisableOnScrollDistance = 25; 1.21 + 1.22 +// Drag hysteresis programmed into monocle drag moves 1.23 +const kDragHysteresisDistance = 10; 1.24 + 1.25 +// selection layer id returned from SelectionHandlerUI's layerMode. 1.26 +const kChromeLayer = 1; 1.27 +const kContentLayer = 2; 1.28 + 1.29 +/* 1.30 + * Markers 1.31 + */ 1.32 + 1.33 +function MarkerDragger(aMarker) { 1.34 + this._marker = aMarker; 1.35 +} 1.36 + 1.37 +MarkerDragger.prototype = { 1.38 + _selectionHelperUI: null, 1.39 + _marker: null, 1.40 + _shutdown: false, 1.41 + _dragging: false, 1.42 + 1.43 + get marker() { 1.44 + return this._marker; 1.45 + }, 1.46 + 1.47 + set shutdown(aVal) { 1.48 + this._shutdown = aVal; 1.49 + }, 1.50 + 1.51 + get shutdown() { 1.52 + return this._shutdown; 1.53 + }, 1.54 + 1.55 + get dragging() { 1.56 + return this._dragging; 1.57 + }, 1.58 + 1.59 + freeDrag: function freeDrag() { 1.60 + return true; 1.61 + }, 1.62 + 1.63 + isDraggable: function isDraggable(aTarget, aContent) { 1.64 + return { x: true, y: true }; 1.65 + }, 1.66 + 1.67 + dragStart: function dragStart(aX, aY, aTarget, aScroller) { 1.68 + if (this._shutdown) 1.69 + return false; 1.70 + this._dragging = true; 1.71 + this.marker.dragStart(aX, aY); 1.72 + return true; 1.73 + }, 1.74 + 1.75 + dragStop: function dragStop(aDx, aDy, aScroller) { 1.76 + if (this._shutdown) 1.77 + return false; 1.78 + this._dragging = false; 1.79 + this.marker.dragStop(aDx, aDy); 1.80 + return true; 1.81 + }, 1.82 + 1.83 + dragMove: function dragMove(aDx, aDy, aScroller, aIsKenetic, aClientX, aClientY) { 1.84 + // Note if aIsKenetic is true this is synthetic movement, 1.85 + // we don't want that so return false. 1.86 + if (this._shutdown || aIsKenetic) 1.87 + return false; 1.88 + this.marker.moveBy(aDx, aDy, aClientX, aClientY); 1.89 + // return true if we moved, false otherwise. The result 1.90 + // is used in deciding if we should repaint between drags. 1.91 + return true; 1.92 + } 1.93 +} 1.94 + 1.95 +function Marker(aParent, aTag, aElementId, xPos, yPos) { 1.96 + this._xPos = xPos; 1.97 + this._yPos = yPos; 1.98 + this._selectionHelperUI = aParent; 1.99 + this._element = aParent.overlay.getMarker(aElementId); 1.100 + this._elementId = aElementId; 1.101 + // These get picked in input.js and receives drag input 1.102 + this._element.customDragger = new MarkerDragger(this); 1.103 + this.tag = aTag; 1.104 +} 1.105 + 1.106 +Marker.prototype = { 1.107 + _element: null, 1.108 + _elementId: "", 1.109 + _selectionHelperUI: null, 1.110 + _xPos: 0, 1.111 + _yPos: 0, 1.112 + _xDrag: 0, 1.113 + _yDrag: 0, 1.114 + _tag: "", 1.115 + _hPlane: 0, 1.116 + _vPlane: 0, 1.117 + _restrictedToBounds: false, 1.118 + 1.119 + // Tweak me if the monocle graphics change in any way 1.120 + _monocleRadius: 8, 1.121 + _monocleXHitTextAdjust: -2, 1.122 + _monocleYHitTextAdjust: -10, 1.123 + 1.124 + get xPos() { 1.125 + return this._xPos; 1.126 + }, 1.127 + 1.128 + get yPos() { 1.129 + return this._yPos; 1.130 + }, 1.131 + 1.132 + get tag() { 1.133 + return this._tag; 1.134 + }, 1.135 + 1.136 + set tag(aVal) { 1.137 + this._tag = aVal; 1.138 + }, 1.139 + 1.140 + get dragging() { 1.141 + return this._element.customDragger.dragging; 1.142 + }, 1.143 + 1.144 + // Indicates that marker's position doesn't reflect real selection boundary 1.145 + // but rather boundary of input control while actual selection boundaries are 1.146 + // not visible (ex. due scrolled content). 1.147 + get restrictedToBounds() { 1.148 + return this._restrictedToBounds; 1.149 + }, 1.150 + 1.151 + shutdown: function shutdown() { 1.152 + this._element.hidden = true; 1.153 + this._element.customDragger.shutdown = true; 1.154 + delete this._element.customDragger; 1.155 + this._selectionHelperUI = null; 1.156 + this._element = null; 1.157 + }, 1.158 + 1.159 + setTrackBounds: function setTrackBounds(aVerticalPlane, aHorizontalPlane) { 1.160 + // monocle boundaries 1.161 + this._hPlane = aHorizontalPlane; 1.162 + this._vPlane = aVerticalPlane; 1.163 + }, 1.164 + 1.165 + show: function show() { 1.166 + this._element.hidden = false; 1.167 + }, 1.168 + 1.169 + hide: function hide() { 1.170 + this._element.hidden = true; 1.171 + }, 1.172 + 1.173 + get visible() { 1.174 + return this._element.hidden == false; 1.175 + }, 1.176 + 1.177 + position: function position(aX, aY, aRestrictedToBounds) { 1.178 + this._xPos = aX; 1.179 + this._yPos = aY; 1.180 + this._restrictedToBounds = !!aRestrictedToBounds; 1.181 + this._setPosition(); 1.182 + }, 1.183 + 1.184 + _setPosition: function _setPosition() { 1.185 + this._element.left = this._xPos + "px"; 1.186 + this._element.top = this._yPos + "px"; 1.187 + }, 1.188 + 1.189 + dragStart: function dragStart(aX, aY) { 1.190 + this._xDrag = 0; 1.191 + this._yDrag = 0; 1.192 + this._selectionHelperUI.markerDragStart(this); 1.193 + }, 1.194 + 1.195 + dragStop: function dragStop(aDx, aDy) { 1.196 + this._selectionHelperUI.markerDragStop(this); 1.197 + }, 1.198 + 1.199 + moveBy: function moveBy(aDx, aDy, aClientX, aClientY) { 1.200 + this._xPos -= aDx; 1.201 + this._yPos -= aDy; 1.202 + this._xDrag -= aDx; 1.203 + this._yDrag -= aDy; 1.204 + // Add a bit of hysteresis to our directional detection so "big fingers" 1.205 + // are detected accurately. 1.206 + let direction = "tbd"; 1.207 + if (Math.abs(this._xDrag) > kDragHysteresisDistance || 1.208 + Math.abs(this._yDrag) > kDragHysteresisDistance) { 1.209 + direction = (this._xDrag <= 0 && this._yDrag <= 0 ? "start" : "end"); 1.210 + } 1.211 + // We may swap markers in markerDragMove. If markerDragMove 1.212 + // returns true keep processing, otherwise get out of here. 1.213 + if (this._selectionHelperUI.markerDragMove(this, direction)) { 1.214 + this._setPosition(); 1.215 + } 1.216 + }, 1.217 + 1.218 + hitTest: function hitTest(aX, aY) { 1.219 + // Gets the pointer of the arrow right in the middle of the 1.220 + // monocle. 1.221 + aY += this._monocleYHitTextAdjust; 1.222 + aX += this._monocleXHitTextAdjust; 1.223 + if (aX >= (this._xPos - this._monocleRadius) && 1.224 + aX <= (this._xPos + this._monocleRadius) && 1.225 + aY >= (this._yPos - this._monocleRadius) && 1.226 + aY <= (this._yPos + this._monocleRadius)) 1.227 + return true; 1.228 + return false; 1.229 + }, 1.230 + 1.231 + swapMonocle: function swapMonocle(aCaret) { 1.232 + let targetElement = aCaret._element; 1.233 + let targetElementId = aCaret._elementId; 1.234 + 1.235 + aCaret._element = this._element; 1.236 + aCaret._element.customDragger._marker = aCaret; 1.237 + aCaret._elementId = this._elementId; 1.238 + 1.239 + this._xPos = aCaret._xPos; 1.240 + this._yPos = aCaret._yPos; 1.241 + this._element = targetElement; 1.242 + this._element.customDragger._marker = this; 1.243 + this._elementId = targetElementId; 1.244 + this._element.visible = true; 1.245 + }, 1.246 + 1.247 +}; 1.248 + 1.249 +/* 1.250 + * SelectionHelperUI 1.251 + */ 1.252 + 1.253 +var SelectionHelperUI = { 1.254 + _debugEvents: false, 1.255 + _msgTarget: null, 1.256 + _startMark: null, 1.257 + _endMark: null, 1.258 + _caretMark: null, 1.259 + _target: null, 1.260 + _showAfterUpdate: false, 1.261 + _activeSelectionRect: null, 1.262 + _selectionMarkIds: [], 1.263 + _targetIsEditable: false, 1.264 + 1.265 + /* 1.266 + * Properties 1.267 + */ 1.268 + 1.269 + get startMark() { 1.270 + if (this._startMark == null) { 1.271 + this._startMark = new Marker(this, "start", this._selectionMarkIds.pop(), 0, 0); 1.272 + } 1.273 + return this._startMark; 1.274 + }, 1.275 + 1.276 + get endMark() { 1.277 + if (this._endMark == null) { 1.278 + this._endMark = new Marker(this, "end", this._selectionMarkIds.pop(), 0, 0); 1.279 + } 1.280 + return this._endMark; 1.281 + }, 1.282 + 1.283 + get caretMark() { 1.284 + if (this._caretMark == null) { 1.285 + this._caretMark = new Marker(this, "caret", this._selectionMarkIds.pop(), 0, 0); 1.286 + } 1.287 + return this._caretMark; 1.288 + }, 1.289 + 1.290 + get overlay() { 1.291 + return document.getElementById(this.layerMode == kChromeLayer ? 1.292 + "chrome-selection-overlay" : "content-selection-overlay"); 1.293 + }, 1.294 + 1.295 + get layerMode() { 1.296 + if (this._msgTarget && this._msgTarget instanceof SelectionPrototype) 1.297 + return kChromeLayer; 1.298 + return kContentLayer; 1.299 + }, 1.300 + 1.301 + /* 1.302 + * isActive (prop) 1.303 + * 1.304 + * Determines if a selection edit session is currently active. 1.305 + */ 1.306 + get isActive() { 1.307 + return this._msgTarget ? true : false; 1.308 + }, 1.309 + 1.310 + /* 1.311 + * isSelectionUIVisible (prop) 1.312 + * 1.313 + * Determines if edit session monocles are visible. Useful 1.314 + * in checking if selection handler is setup for tests. 1.315 + */ 1.316 + get isSelectionUIVisible() { 1.317 + if (!this._msgTarget || !this._startMark) 1.318 + return false; 1.319 + return this._startMark.visible; 1.320 + }, 1.321 + 1.322 + /* 1.323 + * isCaretUIVisible (prop) 1.324 + * 1.325 + * Determines if caret browsing monocle is visible. Useful 1.326 + * in checking if selection handler is setup for tests. 1.327 + */ 1.328 + get isCaretUIVisible() { 1.329 + if (!this._msgTarget || !this._caretMark) 1.330 + return false; 1.331 + return this._caretMark.visible; 1.332 + }, 1.333 + 1.334 + /* 1.335 + * hasActiveDrag (prop) 1.336 + * 1.337 + * Determines if a marker is actively being dragged (missing call 1.338 + * to markerDragStop). Useful in checking if selection handler is 1.339 + * setup for tests. 1.340 + */ 1.341 + get hasActiveDrag() { 1.342 + if (!this._msgTarget) 1.343 + return false; 1.344 + if ((this._caretMark && this._caretMark.dragging) || 1.345 + (this._startMark && this._startMark.dragging) || 1.346 + (this._endMark && this._endMark.dragging)) 1.347 + return true; 1.348 + return false; 1.349 + }, 1.350 + 1.351 + 1.352 + /* 1.353 + * Observers 1.354 + */ 1.355 + 1.356 + observe: function (aSubject, aTopic, aData) { 1.357 + switch (aTopic) { 1.358 + case "attach_edit_session_to_content": 1.359 + // We receive this from text input bindings when this module 1.360 + // isn't accessible. 1.361 + this.chromeTextboxClick(aSubject); 1.362 + break; 1.363 + 1.364 + case "apzc-transform-begin": 1.365 + if (this.isActive && this.layerMode == kContentLayer) { 1.366 + this._hideMonocles(); 1.367 + } 1.368 + break; 1.369 + 1.370 + case "apzc-transform-end": 1.371 + // The selection range callback will check to see if the new 1.372 + // position is off the screen, in which case it shuts down and 1.373 + // clears the selection. 1.374 + if (this.isActive && this.layerMode == kContentLayer) { 1.375 + this._showAfterUpdate = true; 1.376 + this._sendAsyncMessage("Browser:SelectionUpdate", { 1.377 + isInitiatedByAPZC: true 1.378 + }); 1.379 + } 1.380 + break; 1.381 + } 1.382 + }, 1.383 + 1.384 + /* 1.385 + * Public apis 1.386 + */ 1.387 + 1.388 + /* 1.389 + * pingSelectionHandler 1.390 + * 1.391 + * Ping the SelectionHandler and wait for the right response. Insures 1.392 + * all previous messages have been received. Useful in checking if 1.393 + * selection handler is setup for tests. 1.394 + * 1.395 + * @return a promise 1.396 + */ 1.397 + pingSelectionHandler: function pingSelectionHandler() { 1.398 + if (!this.isActive) 1.399 + return null; 1.400 + 1.401 + if (this._pingCount == undefined) { 1.402 + this._pingCount = 0; 1.403 + this._pingArray = []; 1.404 + } 1.405 + 1.406 + this._pingCount++; 1.407 + 1.408 + let deferred = Promise.defer(); 1.409 + this._pingArray.push({ 1.410 + id: this._pingCount, 1.411 + deferred: deferred 1.412 + }); 1.413 + 1.414 + this._sendAsyncMessage("Browser:SelectionHandlerPing", { id: this._pingCount }); 1.415 + return deferred.promise; 1.416 + }, 1.417 + 1.418 + /* 1.419 + * openEditSession 1.420 + * 1.421 + * Attempts to select underlying text at a point and begins editing 1.422 + * the section. 1.423 + * 1.424 + * @param aMsgTarget - Browser or chrome message target 1.425 + * @param aX, aY - Browser relative client coordinates. 1.426 + * @param aSetFocus - (optional) For form inputs, requests that the focus 1.427 + * be set to the element. 1.428 + */ 1.429 + openEditSession: function openEditSession(aMsgTarget, aX, aY, aSetFocus) { 1.430 + if (!aMsgTarget || this.isActive) 1.431 + return; 1.432 + this._init(aMsgTarget); 1.433 + this._setupDebugOptions(); 1.434 + let setFocus = aSetFocus || false; 1.435 + // Send this over to SelectionHandler in content, they'll message us 1.436 + // back with information on the current selection. SelectionStart 1.437 + // takes client coordinates. 1.438 + this._sendAsyncMessage("Browser:SelectionStart", { 1.439 + setFocus: setFocus, 1.440 + xPos: aX, 1.441 + yPos: aY 1.442 + }); 1.443 + }, 1.444 + 1.445 + /* 1.446 + * attachEditSession 1.447 + * 1.448 + * Attaches to existing selection and begins editing. 1.449 + * 1.450 + * @param aMsgTarget - Browser or chrome message target. 1.451 + * @param aX Tap browser relative client X coordinate. 1.452 + * @param aY Tap browser relative client Y coordinate. 1.453 + * @param aTarget Actual tap target (optional). 1.454 + */ 1.455 + attachEditSession: function attachEditSession(aMsgTarget, aX, aY, aTarget) { 1.456 + if (!aMsgTarget || this.isActive) 1.457 + return; 1.458 + this._init(aMsgTarget); 1.459 + this._setupDebugOptions(); 1.460 + 1.461 + // Send this over to SelectionHandler in content, they'll message us 1.462 + // back with information on the current selection. SelectionAttach 1.463 + // takes client coordinates. 1.464 + this._sendAsyncMessage("Browser:SelectionAttach", { 1.465 + target: aTarget, 1.466 + xPos: aX, 1.467 + yPos: aY 1.468 + }); 1.469 + }, 1.470 + 1.471 + /* 1.472 + * attachToCaret 1.473 + * 1.474 + * Initiates a touch caret selection session for a text input. 1.475 + * Can be called multiple times to move the caret marker around. 1.476 + * 1.477 + * Note the caret marker is pretty limited in functionality. The 1.478 + * only thing is can do is be displayed at the caret position. 1.479 + * Once the user starts a drag, the caret marker is hidden, and 1.480 + * the start and end markers take over. 1.481 + * 1.482 + * @param aMsgTarget - Browser or chrome message target. 1.483 + * @param aX Tap browser relative client X coordinate. 1.484 + * @param aY Tap browser relative client Y coordinate. 1.485 + * @param aTarget Actual tap target (optional). 1.486 + */ 1.487 + attachToCaret: function attachToCaret(aMsgTarget, aX, aY, aTarget) { 1.488 + if (!this.isActive) { 1.489 + this._init(aMsgTarget); 1.490 + this._setupDebugOptions(); 1.491 + } else { 1.492 + this._hideMonocles(); 1.493 + } 1.494 + 1.495 + this._lastCaretAttachment = { 1.496 + target: aTarget, 1.497 + xPos: aX, 1.498 + yPos: aY 1.499 + }; 1.500 + 1.501 + this._sendAsyncMessage("Browser:CaretAttach", { 1.502 + target: aTarget, 1.503 + xPos: aX, 1.504 + yPos: aY 1.505 + }); 1.506 + }, 1.507 + 1.508 + /* 1.509 + * canHandleContextMenuMsg 1.510 + * 1.511 + * Determines if we can handle a ContextMenuHandler message. 1.512 + */ 1.513 + canHandleContextMenuMsg: function canHandleContextMenuMsg(aMessage) { 1.514 + if (aMessage.json.types.indexOf("content-text") != -1) 1.515 + return true; 1.516 + return false; 1.517 + }, 1.518 + 1.519 + /* 1.520 + * closeEditSession(aClearSelection) 1.521 + * 1.522 + * Closes an active edit session and shuts down. Does not clear existing 1.523 + * selection regions if they exist. 1.524 + * 1.525 + * @param aClearSelection bool indicating if the selection handler should also 1.526 + * clear any selection. optional, the default is false. 1.527 + */ 1.528 + closeEditSession: function closeEditSession(aClearSelection) { 1.529 + if (!this.isActive) { 1.530 + return; 1.531 + } 1.532 + // This will callback in _selectionHandlerShutdown in 1.533 + // which we will call _shutdown(). 1.534 + let clearSelection = aClearSelection || false; 1.535 + this._sendAsyncMessage("Browser:SelectionClose", { 1.536 + clearSelection: clearSelection 1.537 + }); 1.538 + }, 1.539 + 1.540 + /* 1.541 + * Click handler for chrome pages loaded into the browser (about:config). 1.542 + * Called from the text input bindings via the attach_edit_session_to_content 1.543 + * observer. 1.544 + */ 1.545 + chromeTextboxClick: function (aEvent) { 1.546 + this.attachEditSession(Browser.selectedTab.browser, aEvent.clientX, 1.547 + aEvent.clientY, aEvent.target); 1.548 + }, 1.549 + 1.550 + /* 1.551 + * Handy debug routines that work independent of selection. They 1.552 + * make use of the selection overlay for drawing points. 1.553 + */ 1.554 + 1.555 + debugDisplayDebugPoint: function (aLeft, aTop, aSize, aCssColorStr, aFill) { 1.556 + this.overlay.enabled = true; 1.557 + this.overlay.displayDebugLayer = true; 1.558 + this.overlay.addDebugRect(aLeft, aTop, aLeft + aSize, aTop + aSize, 1.559 + aCssColorStr, aFill); 1.560 + }, 1.561 + 1.562 + debugClearDebugPoints: function () { 1.563 + this.overlay.displayDebugLayer = false; 1.564 + if (!this._msgTarget) { 1.565 + this.overlay.enabled = false; 1.566 + } 1.567 + }, 1.568 + 1.569 + /* 1.570 + * Init and shutdown 1.571 + */ 1.572 + 1.573 + init: function () { 1.574 + let os = Services.obs; 1.575 + os.addObserver(this, "attach_edit_session_to_content", false); 1.576 + os.addObserver(this, "apzc-transform-begin", false); 1.577 + os.addObserver(this, "apzc-transform-end", false); 1.578 + }, 1.579 + 1.580 + _init: function _init(aMsgTarget) { 1.581 + // store the target message manager 1.582 + this._msgTarget = aMsgTarget; 1.583 + 1.584 + // Init our list of available monocle ids 1.585 + this._setupMonocleIdArray(); 1.586 + 1.587 + // Init selection rect info 1.588 + this._activeSelectionRect = Util.getCleanRect(); 1.589 + this._targetElementRect = Util.getCleanRect(); 1.590 + 1.591 + // SelectionHandler messages 1.592 + messageManager.addMessageListener("Content:SelectionRange", this); 1.593 + messageManager.addMessageListener("Content:SelectionCopied", this); 1.594 + messageManager.addMessageListener("Content:SelectionFail", this); 1.595 + messageManager.addMessageListener("Content:SelectionDebugRect", this); 1.596 + messageManager.addMessageListener("Content:HandlerShutdown", this); 1.597 + messageManager.addMessageListener("Content:SelectionHandlerPong", this); 1.598 + messageManager.addMessageListener("Content:SelectionSwap", this); 1.599 + 1.600 + // capture phase 1.601 + window.addEventListener("keypress", this, true); 1.602 + window.addEventListener("MozPrecisePointer", this, true); 1.603 + window.addEventListener("MozDeckOffsetChanging", this, true); 1.604 + window.addEventListener("MozDeckOffsetChanged", this, true); 1.605 + window.addEventListener("KeyboardChanged", this, true); 1.606 + 1.607 + // bubble phase 1.608 + window.addEventListener("click", this, false); 1.609 + window.addEventListener("touchstart", this, false); 1.610 + 1.611 + Elements.browsers.addEventListener("URLChanged", this, true); 1.612 + Elements.browsers.addEventListener("SizeChanged", this, true); 1.613 + 1.614 + Elements.tabList.addEventListener("TabSelect", this, true); 1.615 + 1.616 + Elements.navbar.addEventListener("transitionend", this, true); 1.617 + 1.618 + this.overlay.enabled = true; 1.619 + }, 1.620 + 1.621 + _shutdown: function _shutdown() { 1.622 + messageManager.removeMessageListener("Content:SelectionRange", this); 1.623 + messageManager.removeMessageListener("Content:SelectionCopied", this); 1.624 + messageManager.removeMessageListener("Content:SelectionFail", this); 1.625 + messageManager.removeMessageListener("Content:SelectionDebugRect", this); 1.626 + messageManager.removeMessageListener("Content:HandlerShutdown", this); 1.627 + messageManager.removeMessageListener("Content:SelectionHandlerPong", this); 1.628 + messageManager.removeMessageListener("Content:SelectionSwap", this); 1.629 + 1.630 + window.removeEventListener("keypress", this, true); 1.631 + window.removeEventListener("MozPrecisePointer", this, true); 1.632 + window.removeEventListener("MozDeckOffsetChanging", this, true); 1.633 + window.removeEventListener("MozDeckOffsetChanged", this, true); 1.634 + window.removeEventListener("KeyboardChanged", this, true); 1.635 + 1.636 + window.removeEventListener("click", this, false); 1.637 + window.removeEventListener("touchstart", this, false); 1.638 + 1.639 + Elements.browsers.removeEventListener("URLChanged", this, true); 1.640 + Elements.browsers.removeEventListener("SizeChanged", this, true); 1.641 + 1.642 + Elements.tabList.removeEventListener("TabSelect", this, true); 1.643 + 1.644 + Elements.navbar.removeEventListener("transitionend", this, true); 1.645 + 1.646 + this._shutdownAllMarkers(); 1.647 + 1.648 + this._selectionMarkIds = []; 1.649 + this._msgTarget = null; 1.650 + this._activeSelectionRect = null; 1.651 + 1.652 + this.overlay.displayDebugLayer = false; 1.653 + this.overlay.enabled = false; 1.654 + }, 1.655 + 1.656 + /* 1.657 + * Utilities 1.658 + */ 1.659 + 1.660 + /* 1.661 + * _swapCaretMarker 1.662 + * 1.663 + * Swap two drag markers - used when transitioning from caret mode 1.664 + * to selection mode. We take the current caret marker (which is in a 1.665 + * drag state) and swap it out with one of the selection markers. 1.666 + */ 1.667 + _swapCaretMarker: function _swapCaretMarker(aDirection) { 1.668 + let targetMark = null; 1.669 + if (aDirection == "start") 1.670 + targetMark = this.startMark; 1.671 + else 1.672 + targetMark = this.endMark; 1.673 + let caret = this.caretMark; 1.674 + targetMark.swapMonocle(caret); 1.675 + let id = caret._elementId; 1.676 + caret.shutdown(); 1.677 + this._caretMark = null; 1.678 + this._selectionMarkIds.push(id); 1.679 + }, 1.680 + 1.681 + /* 1.682 + * _transitionFromCaretToSelection 1.683 + * 1.684 + * Transitions from caret mode to text selection mode. 1.685 + */ 1.686 + _transitionFromCaretToSelection: function _transitionFromCaretToSelection(aDirection) { 1.687 + // Get selection markers initialized if they aren't already 1.688 + { let mark = this.startMark; mark = this.endMark; } 1.689 + 1.690 + // Swap the caret marker out for the start or end marker depending 1.691 + // on direction. 1.692 + this._swapCaretMarker(aDirection); 1.693 + 1.694 + let targetMark = null; 1.695 + if (aDirection == "start") 1.696 + targetMark = this.startMark; 1.697 + else 1.698 + targetMark = this.endMark; 1.699 + 1.700 + // Position both in the same starting location. 1.701 + this.startMark.position(targetMark.xPos, targetMark.yPos); 1.702 + this.endMark.position(targetMark.xPos, targetMark.yPos); 1.703 + 1.704 + // We delay transitioning until we know which direction the user is dragging 1.705 + // based on a hysteresis value in the drag marker code. Down in our caller, we 1.706 + // cache the first drag position in _cachedCaretPos so we can select from the 1.707 + // initial caret drag position. Use those values if we have them. (Note 1.708 + // _cachedCaretPos has already been translated in _getMarkerBaseMessage.) 1.709 + let xpos = this._cachedCaretPos ? this._cachedCaretPos.xPos : 1.710 + this._msgTarget.ctobx(targetMark.xPos, true); 1.711 + let ypos = this._cachedCaretPos ? this._cachedCaretPos.yPos : 1.712 + this._msgTarget.ctoby(targetMark.yPos, true); 1.713 + 1.714 + // Start the selection monocle drag. SelectionHandler relies on this 1.715 + // for getting initialized. This will also trigger a message back for 1.716 + // monocle positioning. Note, markerDragMove is still on the stack in 1.717 + // this call! 1.718 + this._sendAsyncMessage("Browser:SelectionSwitchMode", { 1.719 + newMode: "selection", 1.720 + change: targetMark.tag, 1.721 + xPos: xpos, 1.722 + yPos: ypos, 1.723 + }); 1.724 + }, 1.725 + 1.726 + /* 1.727 + * _setupDebugOptions 1.728 + * 1.729 + * Sends a message over to content instructing it to 1.730 + * turn on various debug features. 1.731 + */ 1.732 + _setupDebugOptions: function _setupDebugOptions() { 1.733 + // Debug options for selection 1.734 + let debugOpts = { dumpRanges: false, displayRanges: false, dumpEvents: false }; 1.735 + try { 1.736 + if (Services.prefs.getBoolPref(kDebugSelectionDumpPref)) 1.737 + debugOpts.displayRanges = true; 1.738 + } catch (ex) {} 1.739 + try { 1.740 + if (Services.prefs.getBoolPref(kDebugSelectionDisplayPref)) 1.741 + debugOpts.displayRanges = true; 1.742 + } catch (ex) {} 1.743 + try { 1.744 + if (Services.prefs.getBoolPref(kDebugSelectionDumpEvents)) { 1.745 + debugOpts.dumpEvents = true; 1.746 + this._debugEvents = true; 1.747 + } 1.748 + } catch (ex) {} 1.749 + 1.750 + if (debugOpts.displayRanges || debugOpts.dumpRanges || debugOpts.dumpEvents) { 1.751 + // Turn on the debug layer 1.752 + this.overlay.displayDebugLayer = true; 1.753 + // Tell SelectionHandler what to do 1.754 + this._sendAsyncMessage("Browser:SelectionDebug", debugOpts); 1.755 + } 1.756 + }, 1.757 + 1.758 + /* 1.759 + * _sendAsyncMessage 1.760 + * 1.761 + * Helper for sending a message to SelectionHandler. 1.762 + */ 1.763 + _sendAsyncMessage: function _sendAsyncMessage(aMsg, aJson) { 1.764 + if (!this._msgTarget) { 1.765 + if (this._debugEvents) 1.766 + Util.dumpLn("SelectionHelperUI sendAsyncMessage could not send", aMsg); 1.767 + return; 1.768 + } 1.769 + if (this._msgTarget && this._msgTarget instanceof SelectionPrototype) { 1.770 + this._msgTarget.msgHandler(aMsg, aJson); 1.771 + } else { 1.772 + this._msgTarget.messageManager.sendAsyncMessage(aMsg, aJson); 1.773 + } 1.774 + }, 1.775 + 1.776 + _checkForActiveDrag: function _checkForActiveDrag() { 1.777 + return (this.startMark.dragging || this.endMark.dragging || 1.778 + this.caretMark.dragging); 1.779 + }, 1.780 + 1.781 + _hitTestSelection: function _hitTestSelection(aEvent) { 1.782 + // Ignore if the double tap isn't on our active selection rect. 1.783 + if (this._activeSelectionRect && 1.784 + Util.pointWithinRect(aEvent.clientX, aEvent.clientY, this._activeSelectionRect)) { 1.785 + return true; 1.786 + } 1.787 + return false; 1.788 + }, 1.789 + 1.790 + /* 1.791 + * _setCaretPositionAtPoint - sets the current caret position. 1.792 + * 1.793 + * @param aX, aY - browser relative client coordinates 1.794 + */ 1.795 + _setCaretPositionAtPoint: function _setCaretPositionAtPoint(aX, aY) { 1.796 + let json = this._getMarkerBaseMessage("caret"); 1.797 + json.caret.xPos = aX; 1.798 + json.caret.yPos = aY; 1.799 + this._sendAsyncMessage("Browser:CaretUpdate", json); 1.800 + }, 1.801 + 1.802 + /* 1.803 + * _shutdownAllMarkers 1.804 + * 1.805 + * Helper for shutting down all markers and 1.806 + * freeing the objects associated with them. 1.807 + */ 1.808 + _shutdownAllMarkers: function _shutdownAllMarkers() { 1.809 + if (this._startMark) 1.810 + this._startMark.shutdown(); 1.811 + if (this._endMark) 1.812 + this._endMark.shutdown(); 1.813 + if (this._caretMark) 1.814 + this._caretMark.shutdown(); 1.815 + 1.816 + this._startMark = null; 1.817 + this._endMark = null; 1.818 + this._caretMark = null; 1.819 + }, 1.820 + 1.821 + /* 1.822 + * _setupMonocleIdArray 1.823 + * 1.824 + * Helper for initing the array of monocle anon ids. 1.825 + */ 1.826 + _setupMonocleIdArray: function _setupMonocleIdArray() { 1.827 + this._selectionMarkIds = ["selectionhandle-mark1", 1.828 + "selectionhandle-mark2", 1.829 + "selectionhandle-mark3"]; 1.830 + }, 1.831 + 1.832 + _hideMonocles: function _hideMonocles() { 1.833 + if (this._startMark) { 1.834 + this.startMark.hide(); 1.835 + } 1.836 + if (this._endMark) { 1.837 + this.endMark.hide(); 1.838 + } 1.839 + if (this._caretMark) { 1.840 + this.caretMark.hide(); 1.841 + } 1.842 + }, 1.843 + 1.844 + _showMonocles: function _showMonocles(aSelection) { 1.845 + if (!aSelection) { 1.846 + if (this._checkMonocleVisibility(this.caretMark.xPos, this.caretMark.yPos)) { 1.847 + this.caretMark.show(); 1.848 + } 1.849 + } else { 1.850 + if (this._checkMonocleVisibility(this.endMark.xPos, this.endMark.yPos)) { 1.851 + this.endMark.show(); 1.852 + } 1.853 + if (this._checkMonocleVisibility(this.startMark.xPos, this.startMark.yPos)) { 1.854 + this.startMark.show(); 1.855 + } 1.856 + } 1.857 + }, 1.858 + 1.859 + _checkMonocleVisibility: function(aX, aY) { 1.860 + let viewport = Browser.selectedBrowser.contentViewportBounds; 1.861 + aX = this._msgTarget.ctobx(aX); 1.862 + aY = this._msgTarget.ctoby(aY); 1.863 + if (aX < viewport.x || aY < viewport.y || 1.864 + aX > (viewport.x + viewport.width) || 1.865 + aY > (viewport.y + viewport.height)) { 1.866 + return false; 1.867 + } 1.868 + return true; 1.869 + }, 1.870 + 1.871 + /* 1.872 + * Event handlers for document events 1.873 + */ 1.874 + 1.875 + /* 1.876 + * Handles taps that move the current caret around in text edits, 1.877 + * clear active selection and focus when necessary, or change 1.878 + * modes. Only active after SelectionHandlerUI is initialized. 1.879 + */ 1.880 + _onClick: function(aEvent) { 1.881 + if (this.layerMode == kChromeLayer && this._targetIsEditable) { 1.882 + this.attachToCaret(this._msgTarget, aEvent.clientX, aEvent.clientY, 1.883 + aEvent.target); 1.884 + } 1.885 + }, 1.886 + 1.887 + _onKeypress: function _onKeypress() { 1.888 + this.closeEditSession(); 1.889 + }, 1.890 + 1.891 + _onResize: function _onResize() { 1.892 + this._sendAsyncMessage("Browser:SelectionUpdate", {}); 1.893 + }, 1.894 + 1.895 + /* 1.896 + * _onDeckOffsetChanging - fired by ContentAreaObserver before the browser 1.897 + * deck is shifted for form input access in response to a soft keyboard 1.898 + * display. 1.899 + */ 1.900 + _onDeckOffsetChanging: function _onDeckOffsetChanging(aEvent) { 1.901 + // Hide the monocles temporarily 1.902 + this._hideMonocles(); 1.903 + }, 1.904 + 1.905 + /* 1.906 + * _onDeckOffsetChanged - fired by ContentAreaObserver after the browser 1.907 + * deck is shifted for form input access in response to a soft keyboard 1.908 + * display. 1.909 + */ 1.910 + _onDeckOffsetChanged: function _onDeckOffsetChanged(aEvent) { 1.911 + // Update the monocle position and display 1.912 + this.attachToCaret(null, this._lastCaretAttachment.xPos, 1.913 + this._lastCaretAttachment.yPos, this._lastCaretAttachment.target); 1.914 + }, 1.915 + 1.916 + /* 1.917 + * Detects when the nav bar transitions, so we can enable selection at the 1.918 + * appropriate location once the transition is complete, or shutdown 1.919 + * selection down when the nav bar is hidden. 1.920 + */ 1.921 + _onNavBarTransitionEvent: function _onNavBarTransitionEvent(aEvent) { 1.922 + // Ignore when selection is in content 1.923 + if (this.layerMode == kContentLayer) { 1.924 + return; 1.925 + } 1.926 + 1.927 + // After tansitioning up, show the monocles 1.928 + if (Elements.navbar.isShowing) { 1.929 + this._showAfterUpdate = true; 1.930 + this._sendAsyncMessage("Browser:SelectionUpdate", {}); 1.931 + } 1.932 + }, 1.933 + 1.934 + _onKeyboardChangedEvent: function _onKeyboardChangedEvent() { 1.935 + if (!this.isActive || this.layerMode == kContentLayer) { 1.936 + return; 1.937 + } 1.938 + this._sendAsyncMessage("Browser:SelectionUpdate", {}); 1.939 + }, 1.940 + 1.941 + /* 1.942 + * Event handlers for message manager 1.943 + */ 1.944 + 1.945 + _onDebugRectRequest: function _onDebugRectRequest(aMsg) { 1.946 + this.overlay.addDebugRect(aMsg.left, aMsg.top, aMsg.right, aMsg.bottom, 1.947 + aMsg.color, aMsg.fill, aMsg.id); 1.948 + }, 1.949 + 1.950 + _selectionHandlerShutdown: function _selectionHandlerShutdown() { 1.951 + this._shutdown(); 1.952 + }, 1.953 + 1.954 + _selectionSwap: function _selectionSwap() { 1.955 + [this.startMark.tag, this.endMark.tag] = [this.endMark.tag, 1.956 + this.startMark.tag]; 1.957 + [this._startMark, this._endMark] = [this.endMark, this.startMark]; 1.958 + }, 1.959 + 1.960 + /* 1.961 + * Message handlers 1.962 + */ 1.963 + 1.964 + _onSelectionCopied: function _onSelectionCopied(json) { 1.965 + this.closeEditSession(true); 1.966 + }, 1.967 + 1.968 + _onSelectionRangeChange: function _onSelectionRangeChange(json) { 1.969 + let haveSelectionRect = true; 1.970 + 1.971 + if (json.updateStart) { 1.972 + let x = this._msgTarget.btocx(json.start.xPos, true); 1.973 + let y = this._msgTarget.btocy(json.start.yPos, true); 1.974 + this.startMark.position(x, y, json.start.restrictedToBounds); 1.975 + } 1.976 + 1.977 + if (json.updateEnd) { 1.978 + let x = this._msgTarget.btocx(json.end.xPos, true); 1.979 + let y = this._msgTarget.btocy(json.end.yPos, true); 1.980 + this.endMark.position(x, y, json.end.restrictedToBounds); 1.981 + } 1.982 + 1.983 + if (json.updateCaret) { 1.984 + let x = this._msgTarget.btocx(json.caret.xPos, true); 1.985 + let y = this._msgTarget.btocy(json.caret.yPos, true); 1.986 + // If selectionRangeFound is set SelectionHelper found a range we can 1.987 + // attach to. If not, there's no text in the control, and hence no caret 1.988 + // position information we can use. 1.989 + haveSelectionRect = json.selectionRangeFound; 1.990 + if (json.selectionRangeFound) { 1.991 + this.caretMark.position(x, y); 1.992 + this._showMonocles(false); 1.993 + } 1.994 + } 1.995 + 1.996 + if (this._showAfterUpdate) { 1.997 + this._showAfterUpdate = false; 1.998 + this._showMonocles(!json.updateCaret); 1.999 + } 1.1000 + 1.1001 + this._targetIsEditable = json.targetIsEditable; 1.1002 + this._activeSelectionRect = haveSelectionRect ? 1.1003 + this._msgTarget.rectBrowserToClient(json.selection, true) : 1.1004 + this._activeSelectionRect = Util.getCleanRect(); 1.1005 + this._targetElementRect = 1.1006 + this._msgTarget.rectBrowserToClient(json.element, true); 1.1007 + 1.1008 + // If this is the end of a selection move show the appropriate 1.1009 + // monocle images. src=(start, update, end, caret) 1.1010 + if (json.src == "start" || json.src == "end") { 1.1011 + this._showMonocles(true); 1.1012 + } 1.1013 + }, 1.1014 + 1.1015 + _onSelectionFail: function _onSelectionFail() { 1.1016 + Util.dumpLn("failed to get a selection."); 1.1017 + this.closeEditSession(); 1.1018 + }, 1.1019 + 1.1020 + /* 1.1021 + * _onPong 1.1022 + * 1.1023 + * Handles the closure of promise we return when we send a ping 1.1024 + * to SelectionHandler in pingSelectionHandler. Testing use. 1.1025 + */ 1.1026 + _onPong: function _onPong(aId) { 1.1027 + let ping = this._pingArray.pop(); 1.1028 + if (ping.id != aId) { 1.1029 + ping.deferred.reject( 1.1030 + new Error("Selection module's pong doesn't match our last ping.")); 1.1031 + } 1.1032 + ping.deferred.resolve(); 1.1033 + }, 1.1034 + 1.1035 + /* 1.1036 + * Events 1.1037 + */ 1.1038 + 1.1039 + handleEvent: function handleEvent(aEvent) { 1.1040 + if (this._debugEvents && aEvent.type != "touchmove") { 1.1041 + Util.dumpLn("SelectionHelperUI:", aEvent.type); 1.1042 + } 1.1043 + switch (aEvent.type) { 1.1044 + case "click": 1.1045 + this._onClick(aEvent); 1.1046 + break; 1.1047 + 1.1048 + case "touchstart": { 1.1049 + if (aEvent.touches.length != 1) 1.1050 + break; 1.1051 + // Only prevent default if we're dragging so that 1.1052 + // APZC doesn't scroll. 1.1053 + if (this._checkForActiveDrag()) { 1.1054 + aEvent.preventDefault(); 1.1055 + } 1.1056 + break; 1.1057 + } 1.1058 + 1.1059 + case "keypress": 1.1060 + this._onKeypress(aEvent); 1.1061 + break; 1.1062 + 1.1063 + case "SizeChanged": 1.1064 + this._onResize(aEvent); 1.1065 + break; 1.1066 + 1.1067 + case "URLChanged": 1.1068 + case "TabSelect": 1.1069 + this._shutdown(); 1.1070 + break; 1.1071 + 1.1072 + case "MozPrecisePointer": 1.1073 + this.closeEditSession(true); 1.1074 + break; 1.1075 + 1.1076 + case "MozDeckOffsetChanging": 1.1077 + this._onDeckOffsetChanging(aEvent); 1.1078 + break; 1.1079 + 1.1080 + case "MozDeckOffsetChanged": 1.1081 + this._onDeckOffsetChanged(aEvent); 1.1082 + break; 1.1083 + 1.1084 + case "transitionend": 1.1085 + this._onNavBarTransitionEvent(aEvent); 1.1086 + break; 1.1087 + 1.1088 + case "KeyboardChanged": 1.1089 + this._onKeyboardChangedEvent(); 1.1090 + break; 1.1091 + } 1.1092 + }, 1.1093 + 1.1094 + receiveMessage: function sh_receiveMessage(aMessage) { 1.1095 + if (this._debugEvents) Util.dumpLn("SelectionHelperUI:", aMessage.name); 1.1096 + let json = aMessage.json; 1.1097 + switch (aMessage.name) { 1.1098 + case "Content:SelectionFail": 1.1099 + this._onSelectionFail(); 1.1100 + break; 1.1101 + case "Content:SelectionRange": 1.1102 + this._onSelectionRangeChange(json); 1.1103 + break; 1.1104 + case "Content:SelectionCopied": 1.1105 + this._onSelectionCopied(json); 1.1106 + break; 1.1107 + case "Content:SelectionDebugRect": 1.1108 + this._onDebugRectRequest(json); 1.1109 + break; 1.1110 + case "Content:HandlerShutdown": 1.1111 + this._selectionHandlerShutdown(); 1.1112 + break; 1.1113 + case "Content:SelectionSwap": 1.1114 + this._selectionSwap(); 1.1115 + break; 1.1116 + case "Content:SelectionHandlerPong": 1.1117 + this._onPong(json.id); 1.1118 + break; 1.1119 + } 1.1120 + }, 1.1121 + 1.1122 + /* 1.1123 + * Callbacks from markers 1.1124 + */ 1.1125 + 1.1126 + _getMarkerBaseMessage: function _getMarkerBaseMessage(aMarkerTag) { 1.1127 + return { 1.1128 + change: aMarkerTag, 1.1129 + start: { 1.1130 + xPos: this._msgTarget.ctobx(this.startMark.xPos, true), 1.1131 + yPos: this._msgTarget.ctoby(this.startMark.yPos, true), 1.1132 + restrictedToBounds: this.startMark.restrictedToBounds 1.1133 + }, 1.1134 + end: { 1.1135 + xPos: this._msgTarget.ctobx(this.endMark.xPos, true), 1.1136 + yPos: this._msgTarget.ctoby(this.endMark.yPos, true), 1.1137 + restrictedToBounds: this.endMark.restrictedToBounds 1.1138 + }, 1.1139 + caret: { 1.1140 + xPos: this._msgTarget.ctobx(this.caretMark.xPos, true), 1.1141 + yPos: this._msgTarget.ctoby(this.caretMark.yPos, true) 1.1142 + }, 1.1143 + }; 1.1144 + }, 1.1145 + 1.1146 + markerDragStart: function markerDragStart(aMarker) { 1.1147 + let json = this._getMarkerBaseMessage(aMarker.tag); 1.1148 + if (aMarker.tag == "caret") { 1.1149 + // Cache for when we start the drag in _transitionFromCaretToSelection. 1.1150 + if (!this._cachedCaretPos) { 1.1151 + this._cachedCaretPos = this._getMarkerBaseMessage(aMarker.tag).caret; 1.1152 + } 1.1153 + return; 1.1154 + } 1.1155 + this._sendAsyncMessage("Browser:SelectionMoveStart", json); 1.1156 + }, 1.1157 + 1.1158 + markerDragStop: function markerDragStop(aMarker) { 1.1159 + let json = this._getMarkerBaseMessage(aMarker.tag); 1.1160 + if (aMarker.tag == "caret") { 1.1161 + this._cachedCaretPos = null; 1.1162 + return; 1.1163 + } 1.1164 + this._sendAsyncMessage("Browser:SelectionMoveEnd", json); 1.1165 + }, 1.1166 + 1.1167 + markerDragMove: function markerDragMove(aMarker, aDirection) { 1.1168 + if (aMarker.tag == "caret") { 1.1169 + // If direction is "tbd" the drag monocle hasn't determined which 1.1170 + // direction the user is dragging. 1.1171 + if (aDirection != "tbd") { 1.1172 + // We are going to transition from caret browsing mode to selection 1.1173 + // mode on drag. So swap the caret monocle for a start or end monocle 1.1174 + // depending on the direction of the drag, and start selecting text. 1.1175 + this._transitionFromCaretToSelection(aDirection); 1.1176 + return false; 1.1177 + } 1.1178 + return true; 1.1179 + } 1.1180 + this._cachedCaretPos = null; 1.1181 + 1.1182 + // We'll re-display these after the drag is complete. 1.1183 + this._hideMonocles(); 1.1184 + 1.1185 + let json = this._getMarkerBaseMessage(aMarker.tag); 1.1186 + this._sendAsyncMessage("Browser:SelectionMove", json); 1.1187 + return true; 1.1188 + }, 1.1189 +};