browser/metro/base/content/helperui/SelectionHelperUI.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

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);
  1010   },
  1012   _onSelectionFail: function _onSelectionFail() {
  1013     Util.dumpLn("failed to get a selection.");
  1014     this.closeEditSession();
  1015   },
  1017   /*
  1018    * _onPong
  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."));
  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);
  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();
  1053         break;
  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;
  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;
  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;
  1150       return;
  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;
  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;
  1175       return true;
  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 };

mercurial