browser/metro/base/content/contenthandlers/SelectionHandler.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     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 let Ci = Components.interfaces;
     6 let Cc = Components.classes;
     8 dump("### SelectionHandler.js loaded\n");
    10 var SelectionHandler = {
    11   init: function init() {
    12     this.type = kContentSelector;
    13     this.snap = true;
    14     this.lastYPos = this.lastXPos = null;
    15     addMessageListener("Browser:SelectionStart", this);
    16     addMessageListener("Browser:SelectionAttach", this);
    17     addMessageListener("Browser:SelectionEnd", this);
    18     addMessageListener("Browser:SelectionMoveStart", this);
    19     addMessageListener("Browser:SelectionMove", this);
    20     addMessageListener("Browser:SelectionMoveEnd", this);
    21     addMessageListener("Browser:SelectionUpdate", this);
    22     addMessageListener("Browser:SelectionClose", this);
    23     addMessageListener("Browser:SelectionCopy", this);
    24     addMessageListener("Browser:SelectionDebug", this);
    25     addMessageListener("Browser:CaretAttach", this);
    26     addMessageListener("Browser:CaretMove", this);
    27     addMessageListener("Browser:CaretUpdate", this);
    28     addMessageListener("Browser:SelectionSwitchMode", this);
    29     addMessageListener("Browser:RepositionInfoRequest", this);
    30     addMessageListener("Browser:SelectionHandlerPing", this);
    31     addMessageListener("Browser:ResetLastPos", this);
    32   },
    34   shutdown: function shutdown() {
    35     removeMessageListener("Browser:SelectionStart", this);
    36     removeMessageListener("Browser:SelectionAttach", this);
    37     removeMessageListener("Browser:SelectionEnd", this);
    38     removeMessageListener("Browser:SelectionMoveStart", this);
    39     removeMessageListener("Browser:SelectionMove", this);
    40     removeMessageListener("Browser:SelectionMoveEnd", this);
    41     removeMessageListener("Browser:SelectionUpdate", this);
    42     removeMessageListener("Browser:SelectionClose", this);
    43     removeMessageListener("Browser:SelectionCopy", this);
    44     removeMessageListener("Browser:SelectionDebug", this);
    45     removeMessageListener("Browser:CaretAttach", this);
    46     removeMessageListener("Browser:CaretMove", this);
    47     removeMessageListener("Browser:CaretUpdate", this);
    48     removeMessageListener("Browser:SelectionSwitchMode", this);
    49     removeMessageListener("Browser:RepositionInfoRequest", this);
    50     removeMessageListener("Browser:SelectionHandlerPing", this);
    51     removeMessageListener("Browser:ResetLastPos", this);
    52   },
    54   sendAsync: function sendAsync(aMsg, aJson) {
    55     sendAsyncMessage(aMsg, aJson);
    56   },
    58   /*************************************************
    59    * Browser event handlers
    60    */
    62   /*
    63    * Selection start event handler
    64    */
    65   _onSelectionStart: function _onSelectionStart(aJson) {
    66     // Init content window information
    67     if (!this._initTargetInfo(aJson.xPos, aJson.yPos)) {
    68       this._onFail("failed to get target information");
    69       return;
    70     }
    72     // for context menu select command, which doesn't trigger
    73     // form input focus changes.
    74     if (aJson.setFocus && this._targetIsEditable) {
    75       this._targetElement.focus();
    76     }
    78     // Clear any existing selection from the document
    79     let selection = this._contentWindow.getSelection();
    80     selection.removeAllRanges();
    82     // Set our initial selection, aX and aY should be in client coordinates.
    83     let framePoint = this._clientPointToFramePoint({ xPos: aJson.xPos, yPos: aJson.yPos });
    84     if (!this._domWinUtils.selectAtPoint(framePoint.xPos, framePoint.yPos,
    85                                          Ci.nsIDOMWindowUtils.SELECT_WORDNOSPACE)) {
    86       this._onFail("failed to set selection at point");
    87       return;
    88     }
    90     // Update the position of our selection monocles
    91     this._updateSelectionUI("start", true, true);
    92   },
    94   _onSelectionAttach: function _onSelectionAttach(aX, aY) {
    95     // Init content window information
    96     if (!this._initTargetInfo(aX, aY)) {
    97       this._onFail("failed to get frame offset");
    98       return;
    99     }
   101     // Update the position of our selection monocles
   102     this._updateSelectionUI("start", true, true);
   103   },
   105   /*
   106    * Switch selection modes. Currently we only support switching
   107    * from "caret" to "selection".
   108    */
   109   _onSwitchMode: function _onSwitchMode(aMode, aMarker, aX, aY) {
   110     if (aMode != "selection") {
   111       this._onFail("unsupported mode switch");
   112       return;
   113     }
   115     // Sanity check to be sure we are initialized
   116     if (!this._targetElement) {
   117       this._onFail("not initialized");
   118       return;
   119     }
   121     // Only use selectAtPoint for editable content and avoid that for inputs,
   122     // as we can expand caret to selection manually more precisely. We can use
   123     // selectAtPoint for inputs too though, but only once bug 881938 is fully
   124     // resolved.
   125     if(Util.isEditableContent(this._targetElement)) {
   126       // Similar to _onSelectionStart - we need to create initial selection
   127       // but without the initialization bits.
   128       let framePoint = this._clientPointToFramePoint({ xPos: aX, yPos: aY });
   129       if (!this._domWinUtils.selectAtPoint(framePoint.xPos, framePoint.yPos,
   130                                            Ci.nsIDOMWindowUtils.SELECT_CHARACTER)) {
   131         this._onFail("failed to set selection at point");
   132         return;
   133       }
   134     } else if (this._targetElement.selectionStart == 0 || aMarker == "end") {
   135       // Expand caret forward or backward depending on direction
   136       this._targetElement.selectionEnd++;
   137     } else {
   138       this._targetElement.selectionStart--;
   139     }
   141     // We bail if things get out of sync here implying we missed a message.
   142     this._selectionMoveActive = true;
   144     // Update the position of the selection marker that is *not*
   145     // being dragged.
   146     this._updateSelectionUI("update", aMarker == "end", aMarker == "start");
   147   },
   149   /*
   150    * Selection monocle start move event handler
   151    */
   152   _onSelectionMoveStart: function _onSelectionMoveStart(aMsg) {
   153     if (!this._contentWindow) {
   154       this._onFail("_onSelectionMoveStart was called without proper view set up");
   155       return;
   156     }
   158     if (this._selectionMoveActive) {
   159       this._onFail("mouse is already down on drag start?");
   160       return;
   161     }
   163     // We bail if things get out of sync here implying we missed a message.
   164     this._selectionMoveActive = true;
   166     if (this._targetIsEditable) {
   167       // If we're coming out of an out-of-bounds scroll, the node the user is
   168       // trying to drag may be hidden (the monocle will be pegged to the edge
   169       // of the edit). Make sure the node the user wants to move is visible
   170       // and has focus.
   171       this._updateInputFocus(aMsg.change);
   172     }
   174     // Update the position of our selection monocles
   175     this._updateSelectionUI("update", true, true);
   176   },
   178   /*
   179    * Selection monocle move event handler
   180    */
   181   _onSelectionMove: function _onSelectionMove(aMsg) {
   182     if (!this._contentWindow) {
   183       this._onFail("_onSelectionMove was called without proper view set up");
   184       return;
   185     }
   187     if (!this._selectionMoveActive) {
   188       this._onFail("mouse isn't down for drag move?");
   189       return;
   190     }
   192     this._handleSelectionPoint(aMsg, false);
   193   },
   195   /*
   196    * Selection monocle move finished event handler
   197    */
   198   _onSelectionMoveEnd: function _onSelectionMoveComplete(aMsg) {
   199     if (!this._contentWindow) {
   200       this._onFail("_onSelectionMove was called without proper view set up");
   201       return;
   202     }
   204     if (!this._selectionMoveActive) {
   205       this._onFail("mouse isn't down for drag move?");
   206       return;
   207     }
   209     this._handleSelectionPoint(aMsg, true);
   210     this._selectionMoveActive = false;
   212     // _handleSelectionPoint may set a scroll timer, so this must
   213     // be reset after the last call.
   214     this._clearTimers();
   216     // Update the position of our selection monocles
   217     this._updateSelectionUI("end", true, true);
   218   },
   220    /*
   221     * _onCaretAttach - called by SelectionHelperUI when the user taps in a
   222     * form input. Initializes SelectionHandler, updates the location of the
   223     * caret, and messages back with current monocle position information.
   224     *
   225     * @param aX, aY tap location in client coordinates.
   226     */
   227   _onCaretAttach: function _onCaretAttach(aX, aY) {
   228     // Init content window information
   229     if (!this._initTargetInfo(aX, aY)) {
   230       this._onFail("failed to get target information");
   231       return;
   232     }
   234     // This should never happen, but we check to make sure
   235     if (!this._targetIsEditable) {
   236       this._onFail("Coordiates didn't find a text input element.");
   237       return;
   238     }
   240     // Locate and sanity check the caret position
   241     let selection = this._getSelection();
   242     if (!selection || !selection.isCollapsed) {
   243       this._onFail("No selection or selection is not collapsed.");
   244       return;
   245     }
   247     // Update the position of our selection monocles
   248     this._updateSelectionUI("caret", false, false, true);
   249   },
   251   /*
   252    * Selection copy event handler
   253    *
   254    * Check to see if the incoming click was on our selection rect.
   255    * if it was, copy to the clipboard. Incoming coordinates are
   256    * content values.
   257    */
   258   _onSelectionCopy: function _onSelectionCopy(aMsg) {
   259     let tap = {
   260       xPos: aMsg.xPos,
   261       yPos: aMsg.yPos,
   262     };
   264     let tapInSelection = (tap.xPos > this._cache.selection.left &&
   265                           tap.xPos < this._cache.selection.right) &&
   266                          (tap.yPos > this._cache.selection.top &&
   267                           tap.yPos < this._cache.selection.bottom);
   268     // Util.dumpLn(tapInSelection,
   269     //             tap.xPos, tap.yPos, "|", this._cache.selection.left,
   270     //             this._cache.selection.right, this._cache.selection.top,
   271     //             this._cache.selection.bottom);
   272     let success = false;
   273     let selectedText = this._getSelectedText();
   274     if (tapInSelection && selectedText.length) {
   275       let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
   276                         .getService(Ci.nsIClipboardHelper);
   277       clipboard.copyString(selectedText, this._contentWindow.document);
   278       success = true;
   279     }
   280     sendSyncMessage("Content:SelectionCopied", { succeeded: success });
   281   },
   283   /*
   284    * Selection close event handler
   285    *
   286    * @param aClearSelection requests that selection be cleared.
   287    */
   288   _onSelectionClose: function _onSelectionClose(aClearSelection) {
   289     if (aClearSelection) {
   290       this._clearSelection();
   291     }
   292     this.closeSelection();
   293   },
   295   /*
   296    * Called any time SelectionHelperUI would like us to
   297    * recalculate the selection bounds.
   298    */
   299   _onSelectionUpdate: function _onSelectionUpdate(aMsg) {
   300     if (!this._contentWindow) {
   301       this._onFail("_onSelectionUpdate was called without proper view set up");
   302       return;
   303     }
   305     if (aMsg && aMsg.isInitiatedByAPZC) {
   306       let {offset: offset} = Content.getCurrentWindowAndOffset(
   307         this._targetCoordinates.x, this._targetCoordinates.y);
   308       this._contentOffset = offset;
   309     }
   311     // Update the position of our selection monocles
   312     this._updateSelectionUI("update", true, true);
   313   },
   315   /*
   316    * Called if for any reason we fail during the selection
   317    * process. Cancels the selection.
   318    */
   319   _onFail: function _onFail(aDbgMessage) {
   320     if (aDbgMessage && aDbgMessage.length > 0)
   321       Util.dumpLn(aDbgMessage);
   322     this.sendAsync("Content:SelectionFail");
   323     this._clearSelection();
   324     this.closeSelection();
   325   },
   327   /*
   328    * _repositionInfoRequest - fired at us by ContentAreaObserver when the
   329    * soft keyboard is being displayed. CAO wants to make a decision about
   330    * whether the browser deck needs repositioning.
   331    */
   332   _repositionInfoRequest: function _repositionInfoRequest(aJsonMsg) {
   333     let result = this._calcNewContentPosition(aJsonMsg.viewHeight);
   335     // no repositioning needed
   336     if (result == 0) {
   337       this.sendAsync("Content:RepositionInfoResponse", { reposition: false });
   338       return;
   339     }
   341     this.sendAsync("Content:RepositionInfoResponse", {
   342       reposition: true,
   343       raiseContent: result,
   344     });
   345   },
   347   _onPing: function _onPing(aId) {
   348     this.sendAsync("Content:SelectionHandlerPong", { id: aId });
   349   },
   351   onClickCoords: function (xPos, yPos) {
   352     this.lastXPos = xPos;
   353     this.lastYPos = yPos;
   354   },
   356   /*************************************************
   357    * Selection helpers
   358    */
   360   /*
   361    * _clearSelection
   362    *
   363    * Clear existing selection if it exists and reset our internla state.
   364    */
   365   _clearSelection: function _clearSelection() {
   366     this._clearTimers();
   367     if (this._contentWindow) {
   368       let selection = this._getSelection();
   369       if (selection)
   370         selection.removeAllRanges();
   371     } else {
   372       let selection = content.getSelection();
   373       if (selection)
   374         selection.removeAllRanges();
   375     }
   376   },
   378   /*
   379    * closeSelection
   380    *
   381    * Shuts SelectionHandler down.
   382    */
   383   closeSelection: function closeSelection() {
   384     this._clearTimers();
   385     this._cache = null;
   386     this._contentWindow = null;
   387     this._targetElement = null;
   388     this._selectionMoveActive = false;
   389     this._contentOffset = null;
   390     this._domWinUtils = null;
   391     this._targetIsEditable = false;
   392     this._targetCoordinates = null;
   393     sendSyncMessage("Content:HandlerShutdown", {});
   394   },
   396   /*
   397    * Find content within frames - cache the target nsIDOMWindow,
   398    * client coordinate offset, target element, and dom utils interface.
   399    */
   400   _initTargetInfo: function _initTargetInfo(aX, aY) {
   401     // getCurrentWindowAndOffset takes client coordinates
   402     let { element: element,
   403           contentWindow: contentWindow,
   404           offset: offset,
   405           utils: utils } =
   406       Content.getCurrentWindowAndOffset(aX, aY);
   407     if (!contentWindow) {
   408       return false;
   409     }
   410     this._targetElement = element;
   411     this._contentWindow = contentWindow;
   412     this._contentOffset = offset;
   413     this._domWinUtils = utils;
   414     this._targetIsEditable = Util.isEditable(this._targetElement);
   415     this._targetCoordinates = {
   416       x: aX,
   417       y: aY
   418     };
   420     return true;
   421   },
   423   /*
   424    * _calcNewContentPosition - calculates the distance the browser should be
   425    * raised to move the focused form input out of the way of the soft
   426    * keyboard.
   427    *
   428    * @param aNewViewHeight the new content view height
   429    * @return 0 if no positioning is required or a positive val equal to the
   430    * distance content should be raised to center the target element.
   431    */
   432   _calcNewContentPosition: function _calcNewContentPosition(aNewViewHeight) {
   433     // We have no target element but the keyboard is up
   434     // so lets not cover content that is below the keyboard
   435     if (!this._cache || !this._cache.element) {
   436       if (this.lastYPos != null && this.lastYPos > aNewViewHeight) {
   437         return Services.metro.keyboardHeight;
   438       }
   439       return 0;
   440     }
   442     let position = Util.centerElementInView(aNewViewHeight, this._cache.element);
   443     if (position !== undefined) {
   444       return position;
   445     }
   447     // Special case: we are dealing with an input that is taller than the
   448     // desired height of content. We need to center on the caret location.
   449     let rect =
   450       this._domWinUtils.sendQueryContentEvent(
   451         this._domWinUtils.QUERY_CARET_RECT,
   452         this._targetElement.selectionEnd,
   453         0, 0, 0,
   454         this._domWinUtils.QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK);
   455     if (!rect || !rect.succeeded) {
   456       Util.dumpLn("no caret was present, unexpected.");
   457       return 0;
   458     }
   460     // Note sendQueryContentEvent with QUERY_CARET_RECT is really buggy. If it
   461     // can't find the exact location of the caret position it will "guess".
   462     // Sometimes this can put the result in unexpected locations.
   463     let caretLocation = Math.max(Math.min(Math.round(rect.top + (rect.height * .5)),
   464                                           viewBottom), 0);
   466     // Caret is above the bottom of the new view bounds, no need to shift.
   467     if (caretLocation <= aNewViewHeight) {
   468       return 0;
   469     }
   471     // distance from the top of the keyboard down to the caret location
   472     return caretLocation - aNewViewHeight;
   473   },
   475   /*************************************************
   476    * Events
   477    */
   479   /*
   480    * Scroll + selection advancement timer when the monocle is
   481    * outside the bounds of an input control.
   482    */
   483   scrollTimerCallback: function scrollTimerCallback() {
   484     let result = SelectionHandler.updateTextEditSelection();
   485     // Update monocle position and speed if we've dragged off to one side
   486     if (result.trigger) {
   487       SelectionHandler._updateSelectionUI("update", result.start, result.end);
   488     }
   489   },
   491   receiveMessage: function sh_receiveMessage(aMessage) {
   492     if (this._debugEvents && aMessage.name != "Browser:SelectionMove") {
   493       Util.dumpLn("SelectionHandler:", aMessage.name);
   494     }
   495     let json = aMessage.json;
   496     switch (aMessage.name) {
   497       case "Browser:SelectionStart":
   498         this._onSelectionStart(json);
   499         break;
   501       case "Browser:SelectionAttach":
   502         this._onSelectionAttach(json.xPos, json.yPos);
   503         break;
   505       case "Browser:CaretAttach":
   506         this._onCaretAttach(json.xPos, json.yPos);
   507         break;
   509       case "Browser:CaretMove":
   510         this._onCaretMove(json.caret.xPos, json.caret.yPos);
   511         break;
   513       case "Browser:CaretUpdate":
   514         this._onCaretPositionUpdate(json.caret.xPos, json.caret.yPos);
   515         break;
   517       case "Browser:SelectionSwitchMode":
   518         this._onSwitchMode(json.newMode, json.change, json.xPos, json.yPos);
   519         break;
   521       case "Browser:SelectionClose":
   522         this._onSelectionClose(json.clearSelection);
   523         break;
   525       case "Browser:SelectionMoveStart":
   526         this._onSelectionMoveStart(json);
   527         break;
   529       case "Browser:SelectionMove":
   530         this._onSelectionMove(json);
   531         break;
   533       case "Browser:SelectionMoveEnd":
   534         this._onSelectionMoveEnd(json);
   535         break;
   537       case "Browser:SelectionCopy":
   538         this._onSelectionCopy(json);
   539         break;
   541       case "Browser:SelectionDebug":
   542         this._onSelectionDebug(json);
   543         break;
   545       case "Browser:SelectionUpdate":
   546         this._onSelectionUpdate(json);
   547         break;
   549       case "Browser:RepositionInfoRequest":
   550         // This message is sent simultaneously with a tap event.
   551         // Wait a bit to make sure we have the most up-to-date tap co-ordinates
   552         // before a call to _calcNewContentPosition() which accesses them.
   553         content.setTimeout (function () {
   554           SelectionHandler._repositionInfoRequest(json);
   555         }, 50);
   556         break;
   558       case "Browser:SelectionHandlerPing":
   559         this._onPing(json.id);
   560         break;
   562       case "Browser:ResetLastPos":
   563         this.onClickCoords(json.xPos, json.yPos);
   564         break;
   565     }
   566   },
   568   /*************************************************
   569    * Utilities
   570    */
   572   _getDocShell: function _getDocShell(aWindow) {
   573     if (aWindow == null)
   574       return null;
   575     return aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
   576                   .getInterface(Ci.nsIWebNavigation)
   577                   .QueryInterface(Ci.nsIDocShell);
   578   },
   580   _getSelectedText: function _getSelectedText() {
   581     let selection = this._getSelection();
   582     if (selection)
   583       return selection.toString();
   584     return "";
   585   },
   587   _getSelection: function _getSelection() {
   588     if (this._targetElement instanceof Ci.nsIDOMNSEditableElement) {
   589       return this._targetElement
   590                  .QueryInterface(Ci.nsIDOMNSEditableElement)
   591                  .editor.selection;
   592     } else if (this._contentWindow)
   593       return this._contentWindow.getSelection();
   594     return null;
   595   },
   597   _getSelectController: function _getSelectController() {
   598     if (this._targetElement instanceof Ci.nsIDOMNSEditableElement) {
   599       return this._targetElement
   600                  .QueryInterface(Ci.nsIDOMNSEditableElement)
   601                  .editor.selectionController;
   602     } else {
   603       let docShell = this._getDocShell(this._contentWindow);
   604       if (docShell == null)
   605         return null;
   606       return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
   607                      .getInterface(Ci.nsISelectionDisplay)
   608                      .QueryInterface(Ci.nsISelectionController);
   609     }
   610   },
   611 };
   612 this.SelectionHandler = SelectionHandler;
   614 SelectionHandler.__proto__ = new SelectionPrototype();
   615 SelectionHandler.init();

mercurial