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