Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | /* |
michael@0 | 6 | * selection management |
michael@0 | 7 | */ |
michael@0 | 8 | |
michael@0 | 9 | /* |
michael@0 | 10 | * Current monocle image: |
michael@0 | 11 | * dimensions: 32 x 24 |
michael@0 | 12 | * circle center: 16 x 14 |
michael@0 | 13 | * padding top: 6 |
michael@0 | 14 | */ |
michael@0 | 15 | |
michael@0 | 16 | // Y axis scroll distance that will disable this module and cancel selection |
michael@0 | 17 | const kDisableOnScrollDistance = 25; |
michael@0 | 18 | |
michael@0 | 19 | // Drag hysteresis programmed into monocle drag moves |
michael@0 | 20 | const kDragHysteresisDistance = 10; |
michael@0 | 21 | |
michael@0 | 22 | // selection layer id returned from SelectionHandlerUI's layerMode. |
michael@0 | 23 | const kChromeLayer = 1; |
michael@0 | 24 | const kContentLayer = 2; |
michael@0 | 25 | |
michael@0 | 26 | /* |
michael@0 | 27 | * Markers |
michael@0 | 28 | */ |
michael@0 | 29 | |
michael@0 | 30 | function MarkerDragger(aMarker) { |
michael@0 | 31 | this._marker = aMarker; |
michael@0 | 32 | } |
michael@0 | 33 | |
michael@0 | 34 | MarkerDragger.prototype = { |
michael@0 | 35 | _selectionHelperUI: null, |
michael@0 | 36 | _marker: null, |
michael@0 | 37 | _shutdown: false, |
michael@0 | 38 | _dragging: false, |
michael@0 | 39 | |
michael@0 | 40 | get marker() { |
michael@0 | 41 | return this._marker; |
michael@0 | 42 | }, |
michael@0 | 43 | |
michael@0 | 44 | set shutdown(aVal) { |
michael@0 | 45 | this._shutdown = aVal; |
michael@0 | 46 | }, |
michael@0 | 47 | |
michael@0 | 48 | get shutdown() { |
michael@0 | 49 | return this._shutdown; |
michael@0 | 50 | }, |
michael@0 | 51 | |
michael@0 | 52 | get dragging() { |
michael@0 | 53 | return this._dragging; |
michael@0 | 54 | }, |
michael@0 | 55 | |
michael@0 | 56 | freeDrag: function freeDrag() { |
michael@0 | 57 | return true; |
michael@0 | 58 | }, |
michael@0 | 59 | |
michael@0 | 60 | isDraggable: function isDraggable(aTarget, aContent) { |
michael@0 | 61 | return { x: true, y: true }; |
michael@0 | 62 | }, |
michael@0 | 63 | |
michael@0 | 64 | dragStart: function dragStart(aX, aY, aTarget, aScroller) { |
michael@0 | 65 | if (this._shutdown) |
michael@0 | 66 | return false; |
michael@0 | 67 | this._dragging = true; |
michael@0 | 68 | this.marker.dragStart(aX, aY); |
michael@0 | 69 | return true; |
michael@0 | 70 | }, |
michael@0 | 71 | |
michael@0 | 72 | dragStop: function dragStop(aDx, aDy, aScroller) { |
michael@0 | 73 | if (this._shutdown) |
michael@0 | 74 | return false; |
michael@0 | 75 | this._dragging = false; |
michael@0 | 76 | this.marker.dragStop(aDx, aDy); |
michael@0 | 77 | return true; |
michael@0 | 78 | }, |
michael@0 | 79 | |
michael@0 | 80 | dragMove: function dragMove(aDx, aDy, aScroller, aIsKenetic, aClientX, aClientY) { |
michael@0 | 81 | // Note if aIsKenetic is true this is synthetic movement, |
michael@0 | 82 | // we don't want that so return false. |
michael@0 | 83 | if (this._shutdown || aIsKenetic) |
michael@0 | 84 | return false; |
michael@0 | 85 | this.marker.moveBy(aDx, aDy, aClientX, aClientY); |
michael@0 | 86 | // return true if we moved, false otherwise. The result |
michael@0 | 87 | // is used in deciding if we should repaint between drags. |
michael@0 | 88 | return true; |
michael@0 | 89 | } |
michael@0 | 90 | } |
michael@0 | 91 | |
michael@0 | 92 | function Marker(aParent, aTag, aElementId, xPos, yPos) { |
michael@0 | 93 | this._xPos = xPos; |
michael@0 | 94 | this._yPos = yPos; |
michael@0 | 95 | this._selectionHelperUI = aParent; |
michael@0 | 96 | this._element = aParent.overlay.getMarker(aElementId); |
michael@0 | 97 | this._elementId = aElementId; |
michael@0 | 98 | // These get picked in input.js and receives drag input |
michael@0 | 99 | this._element.customDragger = new MarkerDragger(this); |
michael@0 | 100 | this.tag = aTag; |
michael@0 | 101 | } |
michael@0 | 102 | |
michael@0 | 103 | Marker.prototype = { |
michael@0 | 104 | _element: null, |
michael@0 | 105 | _elementId: "", |
michael@0 | 106 | _selectionHelperUI: null, |
michael@0 | 107 | _xPos: 0, |
michael@0 | 108 | _yPos: 0, |
michael@0 | 109 | _xDrag: 0, |
michael@0 | 110 | _yDrag: 0, |
michael@0 | 111 | _tag: "", |
michael@0 | 112 | _hPlane: 0, |
michael@0 | 113 | _vPlane: 0, |
michael@0 | 114 | _restrictedToBounds: false, |
michael@0 | 115 | |
michael@0 | 116 | // Tweak me if the monocle graphics change in any way |
michael@0 | 117 | _monocleRadius: 8, |
michael@0 | 118 | _monocleXHitTextAdjust: -2, |
michael@0 | 119 | _monocleYHitTextAdjust: -10, |
michael@0 | 120 | |
michael@0 | 121 | get xPos() { |
michael@0 | 122 | return this._xPos; |
michael@0 | 123 | }, |
michael@0 | 124 | |
michael@0 | 125 | get yPos() { |
michael@0 | 126 | return this._yPos; |
michael@0 | 127 | }, |
michael@0 | 128 | |
michael@0 | 129 | get tag() { |
michael@0 | 130 | return this._tag; |
michael@0 | 131 | }, |
michael@0 | 132 | |
michael@0 | 133 | set tag(aVal) { |
michael@0 | 134 | this._tag = aVal; |
michael@0 | 135 | }, |
michael@0 | 136 | |
michael@0 | 137 | get dragging() { |
michael@0 | 138 | return this._element.customDragger.dragging; |
michael@0 | 139 | }, |
michael@0 | 140 | |
michael@0 | 141 | // Indicates that marker's position doesn't reflect real selection boundary |
michael@0 | 142 | // but rather boundary of input control while actual selection boundaries are |
michael@0 | 143 | // not visible (ex. due scrolled content). |
michael@0 | 144 | get restrictedToBounds() { |
michael@0 | 145 | return this._restrictedToBounds; |
michael@0 | 146 | }, |
michael@0 | 147 | |
michael@0 | 148 | shutdown: function shutdown() { |
michael@0 | 149 | this._element.hidden = true; |
michael@0 | 150 | this._element.customDragger.shutdown = true; |
michael@0 | 151 | delete this._element.customDragger; |
michael@0 | 152 | this._selectionHelperUI = null; |
michael@0 | 153 | this._element = null; |
michael@0 | 154 | }, |
michael@0 | 155 | |
michael@0 | 156 | setTrackBounds: function setTrackBounds(aVerticalPlane, aHorizontalPlane) { |
michael@0 | 157 | // monocle boundaries |
michael@0 | 158 | this._hPlane = aHorizontalPlane; |
michael@0 | 159 | this._vPlane = aVerticalPlane; |
michael@0 | 160 | }, |
michael@0 | 161 | |
michael@0 | 162 | show: function show() { |
michael@0 | 163 | this._element.hidden = false; |
michael@0 | 164 | }, |
michael@0 | 165 | |
michael@0 | 166 | hide: function hide() { |
michael@0 | 167 | this._element.hidden = true; |
michael@0 | 168 | }, |
michael@0 | 169 | |
michael@0 | 170 | get visible() { |
michael@0 | 171 | return this._element.hidden == false; |
michael@0 | 172 | }, |
michael@0 | 173 | |
michael@0 | 174 | position: function position(aX, aY, aRestrictedToBounds) { |
michael@0 | 175 | this._xPos = aX; |
michael@0 | 176 | this._yPos = aY; |
michael@0 | 177 | this._restrictedToBounds = !!aRestrictedToBounds; |
michael@0 | 178 | this._setPosition(); |
michael@0 | 179 | }, |
michael@0 | 180 | |
michael@0 | 181 | _setPosition: function _setPosition() { |
michael@0 | 182 | this._element.left = this._xPos + "px"; |
michael@0 | 183 | this._element.top = this._yPos + "px"; |
michael@0 | 184 | }, |
michael@0 | 185 | |
michael@0 | 186 | dragStart: function dragStart(aX, aY) { |
michael@0 | 187 | this._xDrag = 0; |
michael@0 | 188 | this._yDrag = 0; |
michael@0 | 189 | this._selectionHelperUI.markerDragStart(this); |
michael@0 | 190 | }, |
michael@0 | 191 | |
michael@0 | 192 | dragStop: function dragStop(aDx, aDy) { |
michael@0 | 193 | this._selectionHelperUI.markerDragStop(this); |
michael@0 | 194 | }, |
michael@0 | 195 | |
michael@0 | 196 | moveBy: function moveBy(aDx, aDy, aClientX, aClientY) { |
michael@0 | 197 | this._xPos -= aDx; |
michael@0 | 198 | this._yPos -= aDy; |
michael@0 | 199 | this._xDrag -= aDx; |
michael@0 | 200 | this._yDrag -= aDy; |
michael@0 | 201 | // Add a bit of hysteresis to our directional detection so "big fingers" |
michael@0 | 202 | // are detected accurately. |
michael@0 | 203 | let direction = "tbd"; |
michael@0 | 204 | if (Math.abs(this._xDrag) > kDragHysteresisDistance || |
michael@0 | 205 | Math.abs(this._yDrag) > kDragHysteresisDistance) { |
michael@0 | 206 | direction = (this._xDrag <= 0 && this._yDrag <= 0 ? "start" : "end"); |
michael@0 | 207 | } |
michael@0 | 208 | // We may swap markers in markerDragMove. If markerDragMove |
michael@0 | 209 | // returns true keep processing, otherwise get out of here. |
michael@0 | 210 | if (this._selectionHelperUI.markerDragMove(this, direction)) { |
michael@0 | 211 | this._setPosition(); |
michael@0 | 212 | } |
michael@0 | 213 | }, |
michael@0 | 214 | |
michael@0 | 215 | hitTest: function hitTest(aX, aY) { |
michael@0 | 216 | // Gets the pointer of the arrow right in the middle of the |
michael@0 | 217 | // monocle. |
michael@0 | 218 | aY += this._monocleYHitTextAdjust; |
michael@0 | 219 | aX += this._monocleXHitTextAdjust; |
michael@0 | 220 | if (aX >= (this._xPos - this._monocleRadius) && |
michael@0 | 221 | aX <= (this._xPos + this._monocleRadius) && |
michael@0 | 222 | aY >= (this._yPos - this._monocleRadius) && |
michael@0 | 223 | aY <= (this._yPos + this._monocleRadius)) |
michael@0 | 224 | return true; |
michael@0 | 225 | return false; |
michael@0 | 226 | }, |
michael@0 | 227 | |
michael@0 | 228 | swapMonocle: function swapMonocle(aCaret) { |
michael@0 | 229 | let targetElement = aCaret._element; |
michael@0 | 230 | let targetElementId = aCaret._elementId; |
michael@0 | 231 | |
michael@0 | 232 | aCaret._element = this._element; |
michael@0 | 233 | aCaret._element.customDragger._marker = aCaret; |
michael@0 | 234 | aCaret._elementId = this._elementId; |
michael@0 | 235 | |
michael@0 | 236 | this._xPos = aCaret._xPos; |
michael@0 | 237 | this._yPos = aCaret._yPos; |
michael@0 | 238 | this._element = targetElement; |
michael@0 | 239 | this._element.customDragger._marker = this; |
michael@0 | 240 | this._elementId = targetElementId; |
michael@0 | 241 | this._element.visible = true; |
michael@0 | 242 | }, |
michael@0 | 243 | |
michael@0 | 244 | }; |
michael@0 | 245 | |
michael@0 | 246 | /* |
michael@0 | 247 | * SelectionHelperUI |
michael@0 | 248 | */ |
michael@0 | 249 | |
michael@0 | 250 | var SelectionHelperUI = { |
michael@0 | 251 | _debugEvents: false, |
michael@0 | 252 | _msgTarget: null, |
michael@0 | 253 | _startMark: null, |
michael@0 | 254 | _endMark: null, |
michael@0 | 255 | _caretMark: null, |
michael@0 | 256 | _target: null, |
michael@0 | 257 | _showAfterUpdate: false, |
michael@0 | 258 | _activeSelectionRect: null, |
michael@0 | 259 | _selectionMarkIds: [], |
michael@0 | 260 | _targetIsEditable: false, |
michael@0 | 261 | |
michael@0 | 262 | /* |
michael@0 | 263 | * Properties |
michael@0 | 264 | */ |
michael@0 | 265 | |
michael@0 | 266 | get startMark() { |
michael@0 | 267 | if (this._startMark == null) { |
michael@0 | 268 | this._startMark = new Marker(this, "start", this._selectionMarkIds.pop(), 0, 0); |
michael@0 | 269 | } |
michael@0 | 270 | return this._startMark; |
michael@0 | 271 | }, |
michael@0 | 272 | |
michael@0 | 273 | get endMark() { |
michael@0 | 274 | if (this._endMark == null) { |
michael@0 | 275 | this._endMark = new Marker(this, "end", this._selectionMarkIds.pop(), 0, 0); |
michael@0 | 276 | } |
michael@0 | 277 | return this._endMark; |
michael@0 | 278 | }, |
michael@0 | 279 | |
michael@0 | 280 | get caretMark() { |
michael@0 | 281 | if (this._caretMark == null) { |
michael@0 | 282 | this._caretMark = new Marker(this, "caret", this._selectionMarkIds.pop(), 0, 0); |
michael@0 | 283 | } |
michael@0 | 284 | return this._caretMark; |
michael@0 | 285 | }, |
michael@0 | 286 | |
michael@0 | 287 | get overlay() { |
michael@0 | 288 | return document.getElementById(this.layerMode == kChromeLayer ? |
michael@0 | 289 | "chrome-selection-overlay" : "content-selection-overlay"); |
michael@0 | 290 | }, |
michael@0 | 291 | |
michael@0 | 292 | get layerMode() { |
michael@0 | 293 | if (this._msgTarget && this._msgTarget instanceof SelectionPrototype) |
michael@0 | 294 | return kChromeLayer; |
michael@0 | 295 | return kContentLayer; |
michael@0 | 296 | }, |
michael@0 | 297 | |
michael@0 | 298 | /* |
michael@0 | 299 | * isActive (prop) |
michael@0 | 300 | * |
michael@0 | 301 | * Determines if a selection edit session is currently active. |
michael@0 | 302 | */ |
michael@0 | 303 | get isActive() { |
michael@0 | 304 | return this._msgTarget ? true : false; |
michael@0 | 305 | }, |
michael@0 | 306 | |
michael@0 | 307 | /* |
michael@0 | 308 | * isSelectionUIVisible (prop) |
michael@0 | 309 | * |
michael@0 | 310 | * Determines if edit session monocles are visible. Useful |
michael@0 | 311 | * in checking if selection handler is setup for tests. |
michael@0 | 312 | */ |
michael@0 | 313 | get isSelectionUIVisible() { |
michael@0 | 314 | if (!this._msgTarget || !this._startMark) |
michael@0 | 315 | return false; |
michael@0 | 316 | return this._startMark.visible; |
michael@0 | 317 | }, |
michael@0 | 318 | |
michael@0 | 319 | /* |
michael@0 | 320 | * isCaretUIVisible (prop) |
michael@0 | 321 | * |
michael@0 | 322 | * Determines if caret browsing monocle is visible. Useful |
michael@0 | 323 | * in checking if selection handler is setup for tests. |
michael@0 | 324 | */ |
michael@0 | 325 | get isCaretUIVisible() { |
michael@0 | 326 | if (!this._msgTarget || !this._caretMark) |
michael@0 | 327 | return false; |
michael@0 | 328 | return this._caretMark.visible; |
michael@0 | 329 | }, |
michael@0 | 330 | |
michael@0 | 331 | /* |
michael@0 | 332 | * hasActiveDrag (prop) |
michael@0 | 333 | * |
michael@0 | 334 | * Determines if a marker is actively being dragged (missing call |
michael@0 | 335 | * to markerDragStop). Useful in checking if selection handler is |
michael@0 | 336 | * setup for tests. |
michael@0 | 337 | */ |
michael@0 | 338 | get hasActiveDrag() { |
michael@0 | 339 | if (!this._msgTarget) |
michael@0 | 340 | return false; |
michael@0 | 341 | if ((this._caretMark && this._caretMark.dragging) || |
michael@0 | 342 | (this._startMark && this._startMark.dragging) || |
michael@0 | 343 | (this._endMark && this._endMark.dragging)) |
michael@0 | 344 | return true; |
michael@0 | 345 | return false; |
michael@0 | 346 | }, |
michael@0 | 347 | |
michael@0 | 348 | |
michael@0 | 349 | /* |
michael@0 | 350 | * Observers |
michael@0 | 351 | */ |
michael@0 | 352 | |
michael@0 | 353 | observe: function (aSubject, aTopic, aData) { |
michael@0 | 354 | switch (aTopic) { |
michael@0 | 355 | case "attach_edit_session_to_content": |
michael@0 | 356 | // We receive this from text input bindings when this module |
michael@0 | 357 | // isn't accessible. |
michael@0 | 358 | this.chromeTextboxClick(aSubject); |
michael@0 | 359 | break; |
michael@0 | 360 | |
michael@0 | 361 | case "apzc-transform-begin": |
michael@0 | 362 | if (this.isActive && this.layerMode == kContentLayer) { |
michael@0 | 363 | this._hideMonocles(); |
michael@0 | 364 | } |
michael@0 | 365 | break; |
michael@0 | 366 | |
michael@0 | 367 | case "apzc-transform-end": |
michael@0 | 368 | // The selection range callback will check to see if the new |
michael@0 | 369 | // position is off the screen, in which case it shuts down and |
michael@0 | 370 | // clears the selection. |
michael@0 | 371 | if (this.isActive && this.layerMode == kContentLayer) { |
michael@0 | 372 | this._showAfterUpdate = true; |
michael@0 | 373 | this._sendAsyncMessage("Browser:SelectionUpdate", { |
michael@0 | 374 | isInitiatedByAPZC: true |
michael@0 | 375 | }); |
michael@0 | 376 | } |
michael@0 | 377 | break; |
michael@0 | 378 | } |
michael@0 | 379 | }, |
michael@0 | 380 | |
michael@0 | 381 | /* |
michael@0 | 382 | * Public apis |
michael@0 | 383 | */ |
michael@0 | 384 | |
michael@0 | 385 | /* |
michael@0 | 386 | * pingSelectionHandler |
michael@0 | 387 | * |
michael@0 | 388 | * Ping the SelectionHandler and wait for the right response. Insures |
michael@0 | 389 | * all previous messages have been received. Useful in checking if |
michael@0 | 390 | * selection handler is setup for tests. |
michael@0 | 391 | * |
michael@0 | 392 | * @return a promise |
michael@0 | 393 | */ |
michael@0 | 394 | pingSelectionHandler: function pingSelectionHandler() { |
michael@0 | 395 | if (!this.isActive) |
michael@0 | 396 | return null; |
michael@0 | 397 | |
michael@0 | 398 | if (this._pingCount == undefined) { |
michael@0 | 399 | this._pingCount = 0; |
michael@0 | 400 | this._pingArray = []; |
michael@0 | 401 | } |
michael@0 | 402 | |
michael@0 | 403 | this._pingCount++; |
michael@0 | 404 | |
michael@0 | 405 | let deferred = Promise.defer(); |
michael@0 | 406 | this._pingArray.push({ |
michael@0 | 407 | id: this._pingCount, |
michael@0 | 408 | deferred: deferred |
michael@0 | 409 | }); |
michael@0 | 410 | |
michael@0 | 411 | this._sendAsyncMessage("Browser:SelectionHandlerPing", { id: this._pingCount }); |
michael@0 | 412 | return deferred.promise; |
michael@0 | 413 | }, |
michael@0 | 414 | |
michael@0 | 415 | /* |
michael@0 | 416 | * openEditSession |
michael@0 | 417 | * |
michael@0 | 418 | * Attempts to select underlying text at a point and begins editing |
michael@0 | 419 | * the section. |
michael@0 | 420 | * |
michael@0 | 421 | * @param aMsgTarget - Browser or chrome message target |
michael@0 | 422 | * @param aX, aY - Browser relative client coordinates. |
michael@0 | 423 | * @param aSetFocus - (optional) For form inputs, requests that the focus |
michael@0 | 424 | * be set to the element. |
michael@0 | 425 | */ |
michael@0 | 426 | openEditSession: function openEditSession(aMsgTarget, aX, aY, aSetFocus) { |
michael@0 | 427 | if (!aMsgTarget || this.isActive) |
michael@0 | 428 | return; |
michael@0 | 429 | this._init(aMsgTarget); |
michael@0 | 430 | this._setupDebugOptions(); |
michael@0 | 431 | let setFocus = aSetFocus || false; |
michael@0 | 432 | // Send this over to SelectionHandler in content, they'll message us |
michael@0 | 433 | // back with information on the current selection. SelectionStart |
michael@0 | 434 | // takes client coordinates. |
michael@0 | 435 | this._sendAsyncMessage("Browser:SelectionStart", { |
michael@0 | 436 | setFocus: setFocus, |
michael@0 | 437 | xPos: aX, |
michael@0 | 438 | yPos: aY |
michael@0 | 439 | }); |
michael@0 | 440 | }, |
michael@0 | 441 | |
michael@0 | 442 | /* |
michael@0 | 443 | * attachEditSession |
michael@0 | 444 | * |
michael@0 | 445 | * Attaches to existing selection and begins editing. |
michael@0 | 446 | * |
michael@0 | 447 | * @param aMsgTarget - Browser or chrome message target. |
michael@0 | 448 | * @param aX Tap browser relative client X coordinate. |
michael@0 | 449 | * @param aY Tap browser relative client Y coordinate. |
michael@0 | 450 | * @param aTarget Actual tap target (optional). |
michael@0 | 451 | */ |
michael@0 | 452 | attachEditSession: function attachEditSession(aMsgTarget, aX, aY, aTarget) { |
michael@0 | 453 | if (!aMsgTarget || this.isActive) |
michael@0 | 454 | return; |
michael@0 | 455 | this._init(aMsgTarget); |
michael@0 | 456 | this._setupDebugOptions(); |
michael@0 | 457 | |
michael@0 | 458 | // Send this over to SelectionHandler in content, they'll message us |
michael@0 | 459 | // back with information on the current selection. SelectionAttach |
michael@0 | 460 | // takes client coordinates. |
michael@0 | 461 | this._sendAsyncMessage("Browser:SelectionAttach", { |
michael@0 | 462 | target: aTarget, |
michael@0 | 463 | xPos: aX, |
michael@0 | 464 | yPos: aY |
michael@0 | 465 | }); |
michael@0 | 466 | }, |
michael@0 | 467 | |
michael@0 | 468 | /* |
michael@0 | 469 | * attachToCaret |
michael@0 | 470 | * |
michael@0 | 471 | * Initiates a touch caret selection session for a text input. |
michael@0 | 472 | * Can be called multiple times to move the caret marker around. |
michael@0 | 473 | * |
michael@0 | 474 | * Note the caret marker is pretty limited in functionality. The |
michael@0 | 475 | * only thing is can do is be displayed at the caret position. |
michael@0 | 476 | * Once the user starts a drag, the caret marker is hidden, and |
michael@0 | 477 | * the start and end markers take over. |
michael@0 | 478 | * |
michael@0 | 479 | * @param aMsgTarget - Browser or chrome message target. |
michael@0 | 480 | * @param aX Tap browser relative client X coordinate. |
michael@0 | 481 | * @param aY Tap browser relative client Y coordinate. |
michael@0 | 482 | * @param aTarget Actual tap target (optional). |
michael@0 | 483 | */ |
michael@0 | 484 | attachToCaret: function attachToCaret(aMsgTarget, aX, aY, aTarget) { |
michael@0 | 485 | if (!this.isActive) { |
michael@0 | 486 | this._init(aMsgTarget); |
michael@0 | 487 | this._setupDebugOptions(); |
michael@0 | 488 | } else { |
michael@0 | 489 | this._hideMonocles(); |
michael@0 | 490 | } |
michael@0 | 491 | |
michael@0 | 492 | this._lastCaretAttachment = { |
michael@0 | 493 | target: aTarget, |
michael@0 | 494 | xPos: aX, |
michael@0 | 495 | yPos: aY |
michael@0 | 496 | }; |
michael@0 | 497 | |
michael@0 | 498 | this._sendAsyncMessage("Browser:CaretAttach", { |
michael@0 | 499 | target: aTarget, |
michael@0 | 500 | xPos: aX, |
michael@0 | 501 | yPos: aY |
michael@0 | 502 | }); |
michael@0 | 503 | }, |
michael@0 | 504 | |
michael@0 | 505 | /* |
michael@0 | 506 | * canHandleContextMenuMsg |
michael@0 | 507 | * |
michael@0 | 508 | * Determines if we can handle a ContextMenuHandler message. |
michael@0 | 509 | */ |
michael@0 | 510 | canHandleContextMenuMsg: function canHandleContextMenuMsg(aMessage) { |
michael@0 | 511 | if (aMessage.json.types.indexOf("content-text") != -1) |
michael@0 | 512 | return true; |
michael@0 | 513 | return false; |
michael@0 | 514 | }, |
michael@0 | 515 | |
michael@0 | 516 | /* |
michael@0 | 517 | * closeEditSession(aClearSelection) |
michael@0 | 518 | * |
michael@0 | 519 | * Closes an active edit session and shuts down. Does not clear existing |
michael@0 | 520 | * selection regions if they exist. |
michael@0 | 521 | * |
michael@0 | 522 | * @param aClearSelection bool indicating if the selection handler should also |
michael@0 | 523 | * clear any selection. optional, the default is false. |
michael@0 | 524 | */ |
michael@0 | 525 | closeEditSession: function closeEditSession(aClearSelection) { |
michael@0 | 526 | if (!this.isActive) { |
michael@0 | 527 | return; |
michael@0 | 528 | } |
michael@0 | 529 | // This will callback in _selectionHandlerShutdown in |
michael@0 | 530 | // which we will call _shutdown(). |
michael@0 | 531 | let clearSelection = aClearSelection || false; |
michael@0 | 532 | this._sendAsyncMessage("Browser:SelectionClose", { |
michael@0 | 533 | clearSelection: clearSelection |
michael@0 | 534 | }); |
michael@0 | 535 | }, |
michael@0 | 536 | |
michael@0 | 537 | /* |
michael@0 | 538 | * Click handler for chrome pages loaded into the browser (about:config). |
michael@0 | 539 | * Called from the text input bindings via the attach_edit_session_to_content |
michael@0 | 540 | * observer. |
michael@0 | 541 | */ |
michael@0 | 542 | chromeTextboxClick: function (aEvent) { |
michael@0 | 543 | this.attachEditSession(Browser.selectedTab.browser, aEvent.clientX, |
michael@0 | 544 | aEvent.clientY, aEvent.target); |
michael@0 | 545 | }, |
michael@0 | 546 | |
michael@0 | 547 | /* |
michael@0 | 548 | * Handy debug routines that work independent of selection. They |
michael@0 | 549 | * make use of the selection overlay for drawing points. |
michael@0 | 550 | */ |
michael@0 | 551 | |
michael@0 | 552 | debugDisplayDebugPoint: function (aLeft, aTop, aSize, aCssColorStr, aFill) { |
michael@0 | 553 | this.overlay.enabled = true; |
michael@0 | 554 | this.overlay.displayDebugLayer = true; |
michael@0 | 555 | this.overlay.addDebugRect(aLeft, aTop, aLeft + aSize, aTop + aSize, |
michael@0 | 556 | aCssColorStr, aFill); |
michael@0 | 557 | }, |
michael@0 | 558 | |
michael@0 | 559 | debugClearDebugPoints: function () { |
michael@0 | 560 | this.overlay.displayDebugLayer = false; |
michael@0 | 561 | if (!this._msgTarget) { |
michael@0 | 562 | this.overlay.enabled = false; |
michael@0 | 563 | } |
michael@0 | 564 | }, |
michael@0 | 565 | |
michael@0 | 566 | /* |
michael@0 | 567 | * Init and shutdown |
michael@0 | 568 | */ |
michael@0 | 569 | |
michael@0 | 570 | init: function () { |
michael@0 | 571 | let os = Services.obs; |
michael@0 | 572 | os.addObserver(this, "attach_edit_session_to_content", false); |
michael@0 | 573 | os.addObserver(this, "apzc-transform-begin", false); |
michael@0 | 574 | os.addObserver(this, "apzc-transform-end", false); |
michael@0 | 575 | }, |
michael@0 | 576 | |
michael@0 | 577 | _init: function _init(aMsgTarget) { |
michael@0 | 578 | // store the target message manager |
michael@0 | 579 | this._msgTarget = aMsgTarget; |
michael@0 | 580 | |
michael@0 | 581 | // Init our list of available monocle ids |
michael@0 | 582 | this._setupMonocleIdArray(); |
michael@0 | 583 | |
michael@0 | 584 | // Init selection rect info |
michael@0 | 585 | this._activeSelectionRect = Util.getCleanRect(); |
michael@0 | 586 | this._targetElementRect = Util.getCleanRect(); |
michael@0 | 587 | |
michael@0 | 588 | // SelectionHandler messages |
michael@0 | 589 | messageManager.addMessageListener("Content:SelectionRange", this); |
michael@0 | 590 | messageManager.addMessageListener("Content:SelectionCopied", this); |
michael@0 | 591 | messageManager.addMessageListener("Content:SelectionFail", this); |
michael@0 | 592 | messageManager.addMessageListener("Content:SelectionDebugRect", this); |
michael@0 | 593 | messageManager.addMessageListener("Content:HandlerShutdown", this); |
michael@0 | 594 | messageManager.addMessageListener("Content:SelectionHandlerPong", this); |
michael@0 | 595 | messageManager.addMessageListener("Content:SelectionSwap", this); |
michael@0 | 596 | |
michael@0 | 597 | // capture phase |
michael@0 | 598 | window.addEventListener("keypress", this, true); |
michael@0 | 599 | window.addEventListener("MozPrecisePointer", this, true); |
michael@0 | 600 | window.addEventListener("MozDeckOffsetChanging", this, true); |
michael@0 | 601 | window.addEventListener("MozDeckOffsetChanged", this, true); |
michael@0 | 602 | window.addEventListener("KeyboardChanged", this, true); |
michael@0 | 603 | |
michael@0 | 604 | // bubble phase |
michael@0 | 605 | window.addEventListener("click", this, false); |
michael@0 | 606 | window.addEventListener("touchstart", this, false); |
michael@0 | 607 | |
michael@0 | 608 | Elements.browsers.addEventListener("URLChanged", this, true); |
michael@0 | 609 | Elements.browsers.addEventListener("SizeChanged", this, true); |
michael@0 | 610 | |
michael@0 | 611 | Elements.tabList.addEventListener("TabSelect", this, true); |
michael@0 | 612 | |
michael@0 | 613 | Elements.navbar.addEventListener("transitionend", this, true); |
michael@0 | 614 | |
michael@0 | 615 | this.overlay.enabled = true; |
michael@0 | 616 | }, |
michael@0 | 617 | |
michael@0 | 618 | _shutdown: function _shutdown() { |
michael@0 | 619 | messageManager.removeMessageListener("Content:SelectionRange", this); |
michael@0 | 620 | messageManager.removeMessageListener("Content:SelectionCopied", this); |
michael@0 | 621 | messageManager.removeMessageListener("Content:SelectionFail", this); |
michael@0 | 622 | messageManager.removeMessageListener("Content:SelectionDebugRect", this); |
michael@0 | 623 | messageManager.removeMessageListener("Content:HandlerShutdown", this); |
michael@0 | 624 | messageManager.removeMessageListener("Content:SelectionHandlerPong", this); |
michael@0 | 625 | messageManager.removeMessageListener("Content:SelectionSwap", this); |
michael@0 | 626 | |
michael@0 | 627 | window.removeEventListener("keypress", this, true); |
michael@0 | 628 | window.removeEventListener("MozPrecisePointer", this, true); |
michael@0 | 629 | window.removeEventListener("MozDeckOffsetChanging", this, true); |
michael@0 | 630 | window.removeEventListener("MozDeckOffsetChanged", this, true); |
michael@0 | 631 | window.removeEventListener("KeyboardChanged", this, true); |
michael@0 | 632 | |
michael@0 | 633 | window.removeEventListener("click", this, false); |
michael@0 | 634 | window.removeEventListener("touchstart", this, false); |
michael@0 | 635 | |
michael@0 | 636 | Elements.browsers.removeEventListener("URLChanged", this, true); |
michael@0 | 637 | Elements.browsers.removeEventListener("SizeChanged", this, true); |
michael@0 | 638 | |
michael@0 | 639 | Elements.tabList.removeEventListener("TabSelect", this, true); |
michael@0 | 640 | |
michael@0 | 641 | Elements.navbar.removeEventListener("transitionend", this, true); |
michael@0 | 642 | |
michael@0 | 643 | this._shutdownAllMarkers(); |
michael@0 | 644 | |
michael@0 | 645 | this._selectionMarkIds = []; |
michael@0 | 646 | this._msgTarget = null; |
michael@0 | 647 | this._activeSelectionRect = null; |
michael@0 | 648 | |
michael@0 | 649 | this.overlay.displayDebugLayer = false; |
michael@0 | 650 | this.overlay.enabled = false; |
michael@0 | 651 | }, |
michael@0 | 652 | |
michael@0 | 653 | /* |
michael@0 | 654 | * Utilities |
michael@0 | 655 | */ |
michael@0 | 656 | |
michael@0 | 657 | /* |
michael@0 | 658 | * _swapCaretMarker |
michael@0 | 659 | * |
michael@0 | 660 | * Swap two drag markers - used when transitioning from caret mode |
michael@0 | 661 | * to selection mode. We take the current caret marker (which is in a |
michael@0 | 662 | * drag state) and swap it out with one of the selection markers. |
michael@0 | 663 | */ |
michael@0 | 664 | _swapCaretMarker: function _swapCaretMarker(aDirection) { |
michael@0 | 665 | let targetMark = null; |
michael@0 | 666 | if (aDirection == "start") |
michael@0 | 667 | targetMark = this.startMark; |
michael@0 | 668 | else |
michael@0 | 669 | targetMark = this.endMark; |
michael@0 | 670 | let caret = this.caretMark; |
michael@0 | 671 | targetMark.swapMonocle(caret); |
michael@0 | 672 | let id = caret._elementId; |
michael@0 | 673 | caret.shutdown(); |
michael@0 | 674 | this._caretMark = null; |
michael@0 | 675 | this._selectionMarkIds.push(id); |
michael@0 | 676 | }, |
michael@0 | 677 | |
michael@0 | 678 | /* |
michael@0 | 679 | * _transitionFromCaretToSelection |
michael@0 | 680 | * |
michael@0 | 681 | * Transitions from caret mode to text selection mode. |
michael@0 | 682 | */ |
michael@0 | 683 | _transitionFromCaretToSelection: function _transitionFromCaretToSelection(aDirection) { |
michael@0 | 684 | // Get selection markers initialized if they aren't already |
michael@0 | 685 | { let mark = this.startMark; mark = this.endMark; } |
michael@0 | 686 | |
michael@0 | 687 | // Swap the caret marker out for the start or end marker depending |
michael@0 | 688 | // on direction. |
michael@0 | 689 | this._swapCaretMarker(aDirection); |
michael@0 | 690 | |
michael@0 | 691 | let targetMark = null; |
michael@0 | 692 | if (aDirection == "start") |
michael@0 | 693 | targetMark = this.startMark; |
michael@0 | 694 | else |
michael@0 | 695 | targetMark = this.endMark; |
michael@0 | 696 | |
michael@0 | 697 | // Position both in the same starting location. |
michael@0 | 698 | this.startMark.position(targetMark.xPos, targetMark.yPos); |
michael@0 | 699 | this.endMark.position(targetMark.xPos, targetMark.yPos); |
michael@0 | 700 | |
michael@0 | 701 | // We delay transitioning until we know which direction the user is dragging |
michael@0 | 702 | // based on a hysteresis value in the drag marker code. Down in our caller, we |
michael@0 | 703 | // cache the first drag position in _cachedCaretPos so we can select from the |
michael@0 | 704 | // initial caret drag position. Use those values if we have them. (Note |
michael@0 | 705 | // _cachedCaretPos has already been translated in _getMarkerBaseMessage.) |
michael@0 | 706 | let xpos = this._cachedCaretPos ? this._cachedCaretPos.xPos : |
michael@0 | 707 | this._msgTarget.ctobx(targetMark.xPos, true); |
michael@0 | 708 | let ypos = this._cachedCaretPos ? this._cachedCaretPos.yPos : |
michael@0 | 709 | this._msgTarget.ctoby(targetMark.yPos, true); |
michael@0 | 710 | |
michael@0 | 711 | // Start the selection monocle drag. SelectionHandler relies on this |
michael@0 | 712 | // for getting initialized. This will also trigger a message back for |
michael@0 | 713 | // monocle positioning. Note, markerDragMove is still on the stack in |
michael@0 | 714 | // this call! |
michael@0 | 715 | this._sendAsyncMessage("Browser:SelectionSwitchMode", { |
michael@0 | 716 | newMode: "selection", |
michael@0 | 717 | change: targetMark.tag, |
michael@0 | 718 | xPos: xpos, |
michael@0 | 719 | yPos: ypos, |
michael@0 | 720 | }); |
michael@0 | 721 | }, |
michael@0 | 722 | |
michael@0 | 723 | /* |
michael@0 | 724 | * _setupDebugOptions |
michael@0 | 725 | * |
michael@0 | 726 | * Sends a message over to content instructing it to |
michael@0 | 727 | * turn on various debug features. |
michael@0 | 728 | */ |
michael@0 | 729 | _setupDebugOptions: function _setupDebugOptions() { |
michael@0 | 730 | // Debug options for selection |
michael@0 | 731 | let debugOpts = { dumpRanges: false, displayRanges: false, dumpEvents: false }; |
michael@0 | 732 | try { |
michael@0 | 733 | if (Services.prefs.getBoolPref(kDebugSelectionDumpPref)) |
michael@0 | 734 | debugOpts.displayRanges = true; |
michael@0 | 735 | } catch (ex) {} |
michael@0 | 736 | try { |
michael@0 | 737 | if (Services.prefs.getBoolPref(kDebugSelectionDisplayPref)) |
michael@0 | 738 | debugOpts.displayRanges = true; |
michael@0 | 739 | } catch (ex) {} |
michael@0 | 740 | try { |
michael@0 | 741 | if (Services.prefs.getBoolPref(kDebugSelectionDumpEvents)) { |
michael@0 | 742 | debugOpts.dumpEvents = true; |
michael@0 | 743 | this._debugEvents = true; |
michael@0 | 744 | } |
michael@0 | 745 | } catch (ex) {} |
michael@0 | 746 | |
michael@0 | 747 | if (debugOpts.displayRanges || debugOpts.dumpRanges || debugOpts.dumpEvents) { |
michael@0 | 748 | // Turn on the debug layer |
michael@0 | 749 | this.overlay.displayDebugLayer = true; |
michael@0 | 750 | // Tell SelectionHandler what to do |
michael@0 | 751 | this._sendAsyncMessage("Browser:SelectionDebug", debugOpts); |
michael@0 | 752 | } |
michael@0 | 753 | }, |
michael@0 | 754 | |
michael@0 | 755 | /* |
michael@0 | 756 | * _sendAsyncMessage |
michael@0 | 757 | * |
michael@0 | 758 | * Helper for sending a message to SelectionHandler. |
michael@0 | 759 | */ |
michael@0 | 760 | _sendAsyncMessage: function _sendAsyncMessage(aMsg, aJson) { |
michael@0 | 761 | if (!this._msgTarget) { |
michael@0 | 762 | if (this._debugEvents) |
michael@0 | 763 | Util.dumpLn("SelectionHelperUI sendAsyncMessage could not send", aMsg); |
michael@0 | 764 | return; |
michael@0 | 765 | } |
michael@0 | 766 | if (this._msgTarget && this._msgTarget instanceof SelectionPrototype) { |
michael@0 | 767 | this._msgTarget.msgHandler(aMsg, aJson); |
michael@0 | 768 | } else { |
michael@0 | 769 | this._msgTarget.messageManager.sendAsyncMessage(aMsg, aJson); |
michael@0 | 770 | } |
michael@0 | 771 | }, |
michael@0 | 772 | |
michael@0 | 773 | _checkForActiveDrag: function _checkForActiveDrag() { |
michael@0 | 774 | return (this.startMark.dragging || this.endMark.dragging || |
michael@0 | 775 | this.caretMark.dragging); |
michael@0 | 776 | }, |
michael@0 | 777 | |
michael@0 | 778 | _hitTestSelection: function _hitTestSelection(aEvent) { |
michael@0 | 779 | // Ignore if the double tap isn't on our active selection rect. |
michael@0 | 780 | if (this._activeSelectionRect && |
michael@0 | 781 | Util.pointWithinRect(aEvent.clientX, aEvent.clientY, this._activeSelectionRect)) { |
michael@0 | 782 | return true; |
michael@0 | 783 | } |
michael@0 | 784 | return false; |
michael@0 | 785 | }, |
michael@0 | 786 | |
michael@0 | 787 | /* |
michael@0 | 788 | * _setCaretPositionAtPoint - sets the current caret position. |
michael@0 | 789 | * |
michael@0 | 790 | * @param aX, aY - browser relative client coordinates |
michael@0 | 791 | */ |
michael@0 | 792 | _setCaretPositionAtPoint: function _setCaretPositionAtPoint(aX, aY) { |
michael@0 | 793 | let json = this._getMarkerBaseMessage("caret"); |
michael@0 | 794 | json.caret.xPos = aX; |
michael@0 | 795 | json.caret.yPos = aY; |
michael@0 | 796 | this._sendAsyncMessage("Browser:CaretUpdate", json); |
michael@0 | 797 | }, |
michael@0 | 798 | |
michael@0 | 799 | /* |
michael@0 | 800 | * _shutdownAllMarkers |
michael@0 | 801 | * |
michael@0 | 802 | * Helper for shutting down all markers and |
michael@0 | 803 | * freeing the objects associated with them. |
michael@0 | 804 | */ |
michael@0 | 805 | _shutdownAllMarkers: function _shutdownAllMarkers() { |
michael@0 | 806 | if (this._startMark) |
michael@0 | 807 | this._startMark.shutdown(); |
michael@0 | 808 | if (this._endMark) |
michael@0 | 809 | this._endMark.shutdown(); |
michael@0 | 810 | if (this._caretMark) |
michael@0 | 811 | this._caretMark.shutdown(); |
michael@0 | 812 | |
michael@0 | 813 | this._startMark = null; |
michael@0 | 814 | this._endMark = null; |
michael@0 | 815 | this._caretMark = null; |
michael@0 | 816 | }, |
michael@0 | 817 | |
michael@0 | 818 | /* |
michael@0 | 819 | * _setupMonocleIdArray |
michael@0 | 820 | * |
michael@0 | 821 | * Helper for initing the array of monocle anon ids. |
michael@0 | 822 | */ |
michael@0 | 823 | _setupMonocleIdArray: function _setupMonocleIdArray() { |
michael@0 | 824 | this._selectionMarkIds = ["selectionhandle-mark1", |
michael@0 | 825 | "selectionhandle-mark2", |
michael@0 | 826 | "selectionhandle-mark3"]; |
michael@0 | 827 | }, |
michael@0 | 828 | |
michael@0 | 829 | _hideMonocles: function _hideMonocles() { |
michael@0 | 830 | if (this._startMark) { |
michael@0 | 831 | this.startMark.hide(); |
michael@0 | 832 | } |
michael@0 | 833 | if (this._endMark) { |
michael@0 | 834 | this.endMark.hide(); |
michael@0 | 835 | } |
michael@0 | 836 | if (this._caretMark) { |
michael@0 | 837 | this.caretMark.hide(); |
michael@0 | 838 | } |
michael@0 | 839 | }, |
michael@0 | 840 | |
michael@0 | 841 | _showMonocles: function _showMonocles(aSelection) { |
michael@0 | 842 | if (!aSelection) { |
michael@0 | 843 | if (this._checkMonocleVisibility(this.caretMark.xPos, this.caretMark.yPos)) { |
michael@0 | 844 | this.caretMark.show(); |
michael@0 | 845 | } |
michael@0 | 846 | } else { |
michael@0 | 847 | if (this._checkMonocleVisibility(this.endMark.xPos, this.endMark.yPos)) { |
michael@0 | 848 | this.endMark.show(); |
michael@0 | 849 | } |
michael@0 | 850 | if (this._checkMonocleVisibility(this.startMark.xPos, this.startMark.yPos)) { |
michael@0 | 851 | this.startMark.show(); |
michael@0 | 852 | } |
michael@0 | 853 | } |
michael@0 | 854 | }, |
michael@0 | 855 | |
michael@0 | 856 | _checkMonocleVisibility: function(aX, aY) { |
michael@0 | 857 | let viewport = Browser.selectedBrowser.contentViewportBounds; |
michael@0 | 858 | aX = this._msgTarget.ctobx(aX); |
michael@0 | 859 | aY = this._msgTarget.ctoby(aY); |
michael@0 | 860 | if (aX < viewport.x || aY < viewport.y || |
michael@0 | 861 | aX > (viewport.x + viewport.width) || |
michael@0 | 862 | aY > (viewport.y + viewport.height)) { |
michael@0 | 863 | return false; |
michael@0 | 864 | } |
michael@0 | 865 | return true; |
michael@0 | 866 | }, |
michael@0 | 867 | |
michael@0 | 868 | /* |
michael@0 | 869 | * Event handlers for document events |
michael@0 | 870 | */ |
michael@0 | 871 | |
michael@0 | 872 | /* |
michael@0 | 873 | * Handles taps that move the current caret around in text edits, |
michael@0 | 874 | * clear active selection and focus when necessary, or change |
michael@0 | 875 | * modes. Only active after SelectionHandlerUI is initialized. |
michael@0 | 876 | */ |
michael@0 | 877 | _onClick: function(aEvent) { |
michael@0 | 878 | if (this.layerMode == kChromeLayer && this._targetIsEditable) { |
michael@0 | 879 | this.attachToCaret(this._msgTarget, aEvent.clientX, aEvent.clientY, |
michael@0 | 880 | aEvent.target); |
michael@0 | 881 | } |
michael@0 | 882 | }, |
michael@0 | 883 | |
michael@0 | 884 | _onKeypress: function _onKeypress() { |
michael@0 | 885 | this.closeEditSession(); |
michael@0 | 886 | }, |
michael@0 | 887 | |
michael@0 | 888 | _onResize: function _onResize() { |
michael@0 | 889 | this._sendAsyncMessage("Browser:SelectionUpdate", {}); |
michael@0 | 890 | }, |
michael@0 | 891 | |
michael@0 | 892 | /* |
michael@0 | 893 | * _onDeckOffsetChanging - fired by ContentAreaObserver before the browser |
michael@0 | 894 | * deck is shifted for form input access in response to a soft keyboard |
michael@0 | 895 | * display. |
michael@0 | 896 | */ |
michael@0 | 897 | _onDeckOffsetChanging: function _onDeckOffsetChanging(aEvent) { |
michael@0 | 898 | // Hide the monocles temporarily |
michael@0 | 899 | this._hideMonocles(); |
michael@0 | 900 | }, |
michael@0 | 901 | |
michael@0 | 902 | /* |
michael@0 | 903 | * _onDeckOffsetChanged - fired by ContentAreaObserver after the browser |
michael@0 | 904 | * deck is shifted for form input access in response to a soft keyboard |
michael@0 | 905 | * display. |
michael@0 | 906 | */ |
michael@0 | 907 | _onDeckOffsetChanged: function _onDeckOffsetChanged(aEvent) { |
michael@0 | 908 | // Update the monocle position and display |
michael@0 | 909 | this.attachToCaret(null, this._lastCaretAttachment.xPos, |
michael@0 | 910 | this._lastCaretAttachment.yPos, this._lastCaretAttachment.target); |
michael@0 | 911 | }, |
michael@0 | 912 | |
michael@0 | 913 | /* |
michael@0 | 914 | * Detects when the nav bar transitions, so we can enable selection at the |
michael@0 | 915 | * appropriate location once the transition is complete, or shutdown |
michael@0 | 916 | * selection down when the nav bar is hidden. |
michael@0 | 917 | */ |
michael@0 | 918 | _onNavBarTransitionEvent: function _onNavBarTransitionEvent(aEvent) { |
michael@0 | 919 | // Ignore when selection is in content |
michael@0 | 920 | if (this.layerMode == kContentLayer) { |
michael@0 | 921 | return; |
michael@0 | 922 | } |
michael@0 | 923 | |
michael@0 | 924 | // After tansitioning up, show the monocles |
michael@0 | 925 | if (Elements.navbar.isShowing) { |
michael@0 | 926 | this._showAfterUpdate = true; |
michael@0 | 927 | this._sendAsyncMessage("Browser:SelectionUpdate", {}); |
michael@0 | 928 | } |
michael@0 | 929 | }, |
michael@0 | 930 | |
michael@0 | 931 | _onKeyboardChangedEvent: function _onKeyboardChangedEvent() { |
michael@0 | 932 | if (!this.isActive || this.layerMode == kContentLayer) { |
michael@0 | 933 | return; |
michael@0 | 934 | } |
michael@0 | 935 | this._sendAsyncMessage("Browser:SelectionUpdate", {}); |
michael@0 | 936 | }, |
michael@0 | 937 | |
michael@0 | 938 | /* |
michael@0 | 939 | * Event handlers for message manager |
michael@0 | 940 | */ |
michael@0 | 941 | |
michael@0 | 942 | _onDebugRectRequest: function _onDebugRectRequest(aMsg) { |
michael@0 | 943 | this.overlay.addDebugRect(aMsg.left, aMsg.top, aMsg.right, aMsg.bottom, |
michael@0 | 944 | aMsg.color, aMsg.fill, aMsg.id); |
michael@0 | 945 | }, |
michael@0 | 946 | |
michael@0 | 947 | _selectionHandlerShutdown: function _selectionHandlerShutdown() { |
michael@0 | 948 | this._shutdown(); |
michael@0 | 949 | }, |
michael@0 | 950 | |
michael@0 | 951 | _selectionSwap: function _selectionSwap() { |
michael@0 | 952 | [this.startMark.tag, this.endMark.tag] = [this.endMark.tag, |
michael@0 | 953 | this.startMark.tag]; |
michael@0 | 954 | [this._startMark, this._endMark] = [this.endMark, this.startMark]; |
michael@0 | 955 | }, |
michael@0 | 956 | |
michael@0 | 957 | /* |
michael@0 | 958 | * Message handlers |
michael@0 | 959 | */ |
michael@0 | 960 | |
michael@0 | 961 | _onSelectionCopied: function _onSelectionCopied(json) { |
michael@0 | 962 | this.closeEditSession(true); |
michael@0 | 963 | }, |
michael@0 | 964 | |
michael@0 | 965 | _onSelectionRangeChange: function _onSelectionRangeChange(json) { |
michael@0 | 966 | let haveSelectionRect = true; |
michael@0 | 967 | |
michael@0 | 968 | if (json.updateStart) { |
michael@0 | 969 | let x = this._msgTarget.btocx(json.start.xPos, true); |
michael@0 | 970 | let y = this._msgTarget.btocy(json.start.yPos, true); |
michael@0 | 971 | this.startMark.position(x, y, json.start.restrictedToBounds); |
michael@0 | 972 | } |
michael@0 | 973 | |
michael@0 | 974 | if (json.updateEnd) { |
michael@0 | 975 | let x = this._msgTarget.btocx(json.end.xPos, true); |
michael@0 | 976 | let y = this._msgTarget.btocy(json.end.yPos, true); |
michael@0 | 977 | this.endMark.position(x, y, json.end.restrictedToBounds); |
michael@0 | 978 | } |
michael@0 | 979 | |
michael@0 | 980 | if (json.updateCaret) { |
michael@0 | 981 | let x = this._msgTarget.btocx(json.caret.xPos, true); |
michael@0 | 982 | let y = this._msgTarget.btocy(json.caret.yPos, true); |
michael@0 | 983 | // If selectionRangeFound is set SelectionHelper found a range we can |
michael@0 | 984 | // attach to. If not, there's no text in the control, and hence no caret |
michael@0 | 985 | // position information we can use. |
michael@0 | 986 | haveSelectionRect = json.selectionRangeFound; |
michael@0 | 987 | if (json.selectionRangeFound) { |
michael@0 | 988 | this.caretMark.position(x, y); |
michael@0 | 989 | this._showMonocles(false); |
michael@0 | 990 | } |
michael@0 | 991 | } |
michael@0 | 992 | |
michael@0 | 993 | if (this._showAfterUpdate) { |
michael@0 | 994 | this._showAfterUpdate = false; |
michael@0 | 995 | this._showMonocles(!json.updateCaret); |
michael@0 | 996 | } |
michael@0 | 997 | |
michael@0 | 998 | this._targetIsEditable = json.targetIsEditable; |
michael@0 | 999 | this._activeSelectionRect = haveSelectionRect ? |
michael@0 | 1000 | this._msgTarget.rectBrowserToClient(json.selection, true) : |
michael@0 | 1001 | this._activeSelectionRect = Util.getCleanRect(); |
michael@0 | 1002 | this._targetElementRect = |
michael@0 | 1003 | this._msgTarget.rectBrowserToClient(json.element, true); |
michael@0 | 1004 | |
michael@0 | 1005 | // If this is the end of a selection move show the appropriate |
michael@0 | 1006 | // monocle images. src=(start, update, end, caret) |
michael@0 | 1007 | if (json.src == "start" || json.src == "end") { |
michael@0 | 1008 | this._showMonocles(true); |
michael@0 | 1009 | } |
michael@0 | 1010 | }, |
michael@0 | 1011 | |
michael@0 | 1012 | _onSelectionFail: function _onSelectionFail() { |
michael@0 | 1013 | Util.dumpLn("failed to get a selection."); |
michael@0 | 1014 | this.closeEditSession(); |
michael@0 | 1015 | }, |
michael@0 | 1016 | |
michael@0 | 1017 | /* |
michael@0 | 1018 | * _onPong |
michael@0 | 1019 | * |
michael@0 | 1020 | * Handles the closure of promise we return when we send a ping |
michael@0 | 1021 | * to SelectionHandler in pingSelectionHandler. Testing use. |
michael@0 | 1022 | */ |
michael@0 | 1023 | _onPong: function _onPong(aId) { |
michael@0 | 1024 | let ping = this._pingArray.pop(); |
michael@0 | 1025 | if (ping.id != aId) { |
michael@0 | 1026 | ping.deferred.reject( |
michael@0 | 1027 | new Error("Selection module's pong doesn't match our last ping.")); |
michael@0 | 1028 | } |
michael@0 | 1029 | ping.deferred.resolve(); |
michael@0 | 1030 | }, |
michael@0 | 1031 | |
michael@0 | 1032 | /* |
michael@0 | 1033 | * Events |
michael@0 | 1034 | */ |
michael@0 | 1035 | |
michael@0 | 1036 | handleEvent: function handleEvent(aEvent) { |
michael@0 | 1037 | if (this._debugEvents && aEvent.type != "touchmove") { |
michael@0 | 1038 | Util.dumpLn("SelectionHelperUI:", aEvent.type); |
michael@0 | 1039 | } |
michael@0 | 1040 | switch (aEvent.type) { |
michael@0 | 1041 | case "click": |
michael@0 | 1042 | this._onClick(aEvent); |
michael@0 | 1043 | break; |
michael@0 | 1044 | |
michael@0 | 1045 | case "touchstart": { |
michael@0 | 1046 | if (aEvent.touches.length != 1) |
michael@0 | 1047 | break; |
michael@0 | 1048 | // Only prevent default if we're dragging so that |
michael@0 | 1049 | // APZC doesn't scroll. |
michael@0 | 1050 | if (this._checkForActiveDrag()) { |
michael@0 | 1051 | aEvent.preventDefault(); |
michael@0 | 1052 | } |
michael@0 | 1053 | break; |
michael@0 | 1054 | } |
michael@0 | 1055 | |
michael@0 | 1056 | case "keypress": |
michael@0 | 1057 | this._onKeypress(aEvent); |
michael@0 | 1058 | break; |
michael@0 | 1059 | |
michael@0 | 1060 | case "SizeChanged": |
michael@0 | 1061 | this._onResize(aEvent); |
michael@0 | 1062 | break; |
michael@0 | 1063 | |
michael@0 | 1064 | case "URLChanged": |
michael@0 | 1065 | case "TabSelect": |
michael@0 | 1066 | this._shutdown(); |
michael@0 | 1067 | break; |
michael@0 | 1068 | |
michael@0 | 1069 | case "MozPrecisePointer": |
michael@0 | 1070 | this.closeEditSession(true); |
michael@0 | 1071 | break; |
michael@0 | 1072 | |
michael@0 | 1073 | case "MozDeckOffsetChanging": |
michael@0 | 1074 | this._onDeckOffsetChanging(aEvent); |
michael@0 | 1075 | break; |
michael@0 | 1076 | |
michael@0 | 1077 | case "MozDeckOffsetChanged": |
michael@0 | 1078 | this._onDeckOffsetChanged(aEvent); |
michael@0 | 1079 | break; |
michael@0 | 1080 | |
michael@0 | 1081 | case "transitionend": |
michael@0 | 1082 | this._onNavBarTransitionEvent(aEvent); |
michael@0 | 1083 | break; |
michael@0 | 1084 | |
michael@0 | 1085 | case "KeyboardChanged": |
michael@0 | 1086 | this._onKeyboardChangedEvent(); |
michael@0 | 1087 | break; |
michael@0 | 1088 | } |
michael@0 | 1089 | }, |
michael@0 | 1090 | |
michael@0 | 1091 | receiveMessage: function sh_receiveMessage(aMessage) { |
michael@0 | 1092 | if (this._debugEvents) Util.dumpLn("SelectionHelperUI:", aMessage.name); |
michael@0 | 1093 | let json = aMessage.json; |
michael@0 | 1094 | switch (aMessage.name) { |
michael@0 | 1095 | case "Content:SelectionFail": |
michael@0 | 1096 | this._onSelectionFail(); |
michael@0 | 1097 | break; |
michael@0 | 1098 | case "Content:SelectionRange": |
michael@0 | 1099 | this._onSelectionRangeChange(json); |
michael@0 | 1100 | break; |
michael@0 | 1101 | case "Content:SelectionCopied": |
michael@0 | 1102 | this._onSelectionCopied(json); |
michael@0 | 1103 | break; |
michael@0 | 1104 | case "Content:SelectionDebugRect": |
michael@0 | 1105 | this._onDebugRectRequest(json); |
michael@0 | 1106 | break; |
michael@0 | 1107 | case "Content:HandlerShutdown": |
michael@0 | 1108 | this._selectionHandlerShutdown(); |
michael@0 | 1109 | break; |
michael@0 | 1110 | case "Content:SelectionSwap": |
michael@0 | 1111 | this._selectionSwap(); |
michael@0 | 1112 | break; |
michael@0 | 1113 | case "Content:SelectionHandlerPong": |
michael@0 | 1114 | this._onPong(json.id); |
michael@0 | 1115 | break; |
michael@0 | 1116 | } |
michael@0 | 1117 | }, |
michael@0 | 1118 | |
michael@0 | 1119 | /* |
michael@0 | 1120 | * Callbacks from markers |
michael@0 | 1121 | */ |
michael@0 | 1122 | |
michael@0 | 1123 | _getMarkerBaseMessage: function _getMarkerBaseMessage(aMarkerTag) { |
michael@0 | 1124 | return { |
michael@0 | 1125 | change: aMarkerTag, |
michael@0 | 1126 | start: { |
michael@0 | 1127 | xPos: this._msgTarget.ctobx(this.startMark.xPos, true), |
michael@0 | 1128 | yPos: this._msgTarget.ctoby(this.startMark.yPos, true), |
michael@0 | 1129 | restrictedToBounds: this.startMark.restrictedToBounds |
michael@0 | 1130 | }, |
michael@0 | 1131 | end: { |
michael@0 | 1132 | xPos: this._msgTarget.ctobx(this.endMark.xPos, true), |
michael@0 | 1133 | yPos: this._msgTarget.ctoby(this.endMark.yPos, true), |
michael@0 | 1134 | restrictedToBounds: this.endMark.restrictedToBounds |
michael@0 | 1135 | }, |
michael@0 | 1136 | caret: { |
michael@0 | 1137 | xPos: this._msgTarget.ctobx(this.caretMark.xPos, true), |
michael@0 | 1138 | yPos: this._msgTarget.ctoby(this.caretMark.yPos, true) |
michael@0 | 1139 | }, |
michael@0 | 1140 | }; |
michael@0 | 1141 | }, |
michael@0 | 1142 | |
michael@0 | 1143 | markerDragStart: function markerDragStart(aMarker) { |
michael@0 | 1144 | let json = this._getMarkerBaseMessage(aMarker.tag); |
michael@0 | 1145 | if (aMarker.tag == "caret") { |
michael@0 | 1146 | // Cache for when we start the drag in _transitionFromCaretToSelection. |
michael@0 | 1147 | if (!this._cachedCaretPos) { |
michael@0 | 1148 | this._cachedCaretPos = this._getMarkerBaseMessage(aMarker.tag).caret; |
michael@0 | 1149 | } |
michael@0 | 1150 | return; |
michael@0 | 1151 | } |
michael@0 | 1152 | this._sendAsyncMessage("Browser:SelectionMoveStart", json); |
michael@0 | 1153 | }, |
michael@0 | 1154 | |
michael@0 | 1155 | markerDragStop: function markerDragStop(aMarker) { |
michael@0 | 1156 | let json = this._getMarkerBaseMessage(aMarker.tag); |
michael@0 | 1157 | if (aMarker.tag == "caret") { |
michael@0 | 1158 | this._cachedCaretPos = null; |
michael@0 | 1159 | return; |
michael@0 | 1160 | } |
michael@0 | 1161 | this._sendAsyncMessage("Browser:SelectionMoveEnd", json); |
michael@0 | 1162 | }, |
michael@0 | 1163 | |
michael@0 | 1164 | markerDragMove: function markerDragMove(aMarker, aDirection) { |
michael@0 | 1165 | if (aMarker.tag == "caret") { |
michael@0 | 1166 | // If direction is "tbd" the drag monocle hasn't determined which |
michael@0 | 1167 | // direction the user is dragging. |
michael@0 | 1168 | if (aDirection != "tbd") { |
michael@0 | 1169 | // We are going to transition from caret browsing mode to selection |
michael@0 | 1170 | // mode on drag. So swap the caret monocle for a start or end monocle |
michael@0 | 1171 | // depending on the direction of the drag, and start selecting text. |
michael@0 | 1172 | this._transitionFromCaretToSelection(aDirection); |
michael@0 | 1173 | return false; |
michael@0 | 1174 | } |
michael@0 | 1175 | return true; |
michael@0 | 1176 | } |
michael@0 | 1177 | this._cachedCaretPos = null; |
michael@0 | 1178 | |
michael@0 | 1179 | // We'll re-display these after the drag is complete. |
michael@0 | 1180 | this._hideMonocles(); |
michael@0 | 1181 | |
michael@0 | 1182 | let json = this._getMarkerBaseMessage(aMarker.tag); |
michael@0 | 1183 | this._sendAsyncMessage("Browser:SelectionMove", json); |
michael@0 | 1184 | return true; |
michael@0 | 1185 | }, |
michael@0 | 1186 | }; |