Bug 1290842 - reduce the amount calls to the highlighter upon the first find action and improve the behavior when modal highlighting is not enabled now that we have a FinderHighlighter class we can use. r?jaws draft
authorMike de Boer <mdeboer@mozilla.com>
Mon, 01 Aug 2016 13:37:14 +0200
changeset 394967 9473d69094f113a110fcbcc553d3187d235d1731
parent 394965 c94403616cdd5e67861c73b87e7eaec63939c375
child 526915 aba9fba29e8fbad79600a4714730f4fb33c8db0a
push id24682
push usermdeboer@mozilla.com
push dateMon, 01 Aug 2016 11:37:41 +0000
reviewersjaws
bugs1290842
milestone50.0a1
Bug 1290842 - reduce the amount calls to the highlighter upon the first find action and improve the behavior when modal highlighting is not enabled now that we have a FinderHighlighter class we can use. r?jaws MozReview-Commit-ID: 7QD7PibpEiI
toolkit/content/widgets/findbar.xml
toolkit/modules/Finder.jsm
toolkit/modules/FinderHighlighter.jsm
toolkit/modules/FinderIterator.jsm
--- a/toolkit/content/widgets/findbar.xml
+++ b/toolkit/content/widgets/findbar.xml
@@ -582,16 +582,33 @@
             this._prefsvc.setBoolPref("findbar.highlightAll", aHighlight);
           }
           this._highlightAll = aHighlight;
           let checkbox = this.getElement("highlight");
           checkbox.checked = this._highlightAll;
         ]]></body>
       </method>
 
+      <method name="_setHighlightTimeout">
+        <body><![CDATA[
+          if (this._highlightTimeout)
+            clearTimeout(this._highlightTimeout);
+
+          let word = this._findField.value;
+          // Bug 429723. Don't attempt to highlight ""
+          if (!this._highlightAll || !word)
+            return;
+
+          this._highlightTimeout = setTimeout(() => {
+            this.browser.finder.highlight(true, word,
+              this._findMode == this.FIND_LINKS);
+          }, 500);
+        ]]></body>
+      </method>
+
       <!--
         - Updates the case-sensitivity mode of the findbar and its UI.
         - @param [optional] aString
         -        The string for which case sensitivity might be turned on.
         -        This only used when case-sensitivity is in auto mode,
         -        @see _shouldBeCaseSensitive. The default value for this
         -        parameter is the find-field value.
         -->
@@ -676,18 +693,17 @@
         <body><![CDATA[
           let prefsvc =
             Components.classes["@mozilla.org/preferences-service;1"]
                       .getService(Components.interfaces.nsIPrefBranch);
 
           // Just set the pref; our observer will change the find bar behavior.
           prefsvc.setBoolPref("findbar.entireword", aEntireWord);
 
-          if (this.getElement("highlight").checked)
-            this._setHighlightTimeout();
+          this._setHighlightTimeout();
         ]]></body>
       </method>
 
       <field name="_strBundle">null</field>
       <property name="strBundle">
         <getter><![CDATA[
           if (!this._strBundle) {
             this._strBundle =
@@ -1021,18 +1037,17 @@
             // Getting here means the user commanded a find op. Make sure any
             // initial prefilling is ignored if it hasn't happened yet.
             if (this._startFindDeferred) {
               this._startFindDeferred.resolve();
               this._startFindDeferred = null;
             }
 
             this._enableFindButtons(val);
-            if (this.getElement("highlight").checked)
-              this._setHighlightTimeout();
+            this._setHighlightTimeout();
 
             this._updateCaseSensitivity(val);
             this._updateEntireWord();
 
             this.browser.finder.fastFind(val, this._findMode == this.FIND_LINKS,
                                          this._findMode != this.FIND_NORMAL);
           }
 
@@ -1065,28 +1080,16 @@
           }
 
           this.setAttribute("flash",
                             (this._flashFindBarCount % 2 == 0) ?
                             "false" : "true");
         ]]></body>
       </method>
 
-      <method name="_setHighlightTimeout">
-        <body><![CDATA[
-          if (this._highlightTimeout)
-            clearTimeout(this._highlightTimeout);
-          this._highlightTimeout =
-            setTimeout(function(aSelf) {
-                         aSelf.toggleHighlight(false);
-                         aSelf.toggleHighlight(true);
-                       }, 500, this);
-        ]]></body>
-      </method>
-
       <method name="_findAgain">
         <parameter name="aFindPrevious"/>
         <body><![CDATA[
           this.browser.finder.findAgain(aFindPrevious,
                                         this._findMode == this.FIND_LINKS,
                                         this._findMode != this.FIND_NORMAL);
         ]]></body>
       </method>
--- a/toolkit/modules/Finder.jsm
+++ b/toolkit/modules/Finder.jsm
@@ -129,20 +129,22 @@ Finder.prototype = {
       return;
 
     ClipboardHelper.copyStringToClipboard(aSearchString,
                                           Ci.nsIClipboard.kFindClipboard);
   },
 
   set caseSensitive(aSensitive) {
     this._fastFind.caseSensitive = aSensitive;
+    this.iterator.reset();
   },
 
   set entireWord(aEntireWord) {
     this._fastFind.entireWord = aEntireWord;
+    this.iterator.reset();
   },
 
   get highlighter() {
     if (this._highlighter)
       return this._highlighter;
 
     const {FinderHighlighter} = Cu.import("resource://gre/modules/FinderHighlighter.jsm", {});
     return this._highlighter = new FinderHighlighter(this);
--- a/toolkit/modules/FinderHighlighter.jsm
+++ b/toolkit/modules/FinderHighlighter.jsm
@@ -14,16 +14,17 @@ Cu.import("resource://gre/modules/XPCOMU
 
 XPCOMUtils.defineLazyModuleGetter(this, "Color", "resource://gre/modules/Color.jsm");
 XPCOMUtils.defineLazyGetter(this, "kDebug", () => {
   const kDebugPref = "findbar.modalHighlight.debug";
   return Services.prefs.getPrefType(kDebugPref) && Services.prefs.getBoolPref(kDebugPref);
 });
 
 const kModalHighlightRepaintFreqMs = 10;
+const kHighlightAllPref = "findbar.highlightAll";
 const kModalHighlightPref = "findbar.modalHighlight";
 const kFontPropsCSS = ["color", "font-family", "font-kerning", "font-size",
   "font-size-adjust", "font-stretch", "font-variant", "font-weight", "letter-spacing",
   "text-emphasis", "text-orientation", "text-transform", "word-spacing"];
 const kFontPropsCamelCase = kFontPropsCSS.map(prop => {
   let parts = prop.split("-");
   return parts.shift() + parts.map(part => part.charAt(0).toUpperCase() + part.slice(1)).join("");
 });
@@ -95,17 +96,16 @@ const kModalStyle = `
   background: #fff;
   border: 1px solid #666;
   position: absolute;
 }
 
 .findbar-modalHighlight-outlineMask[brighttext] > .findbar-modalHighlight-rect {
   background: #000;
 }`;
-const kXULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 function mockAnonymousContentNode(domNode) {
   return {
     setTextContentForElement(id, text) {
       (domNode.querySelector("#" + id) || domNode).textContent = text;
     },
     getAttributeForElement(id, attrName) {
       let node = domNode.querySelector("#" + id) || domNode;
@@ -132,18 +132,21 @@ function mockAnonymousContentNode(domNod
 
 /**
  * FinderHighlighter class that is used by Finder.jsm to take care of the
  * 'Highlight All' feature, which can highlight all find occurrences in a page.
  *
  * @param {Finder} finder Finder.jsm instance
  */
 function FinderHighlighter(finder) {
+  this._currentFoundRange = null;
+  this._modal = Services.prefs.getBoolPref(kModalHighlightPref);
+  this._highlightAll = Services.prefs.getBoolPref(kHighlightAllPref);
   this.finder = finder;
-  this._modal = Services.prefs.getBoolPref(kModalHighlightPref);
+  this.visible = false;
 }
 
 FinderHighlighter.prototype = {
   get iterator() {
     if (this._iterator)
       return this._iterator;
     this._iterator = Cu.import("resource://gre/modules/FinderIterator.jsm", null).FinderIterator;
     return this._iterator;
@@ -254,68 +257,67 @@ FinderHighlighter.prototype = {
       this._addEditorListeners(editableNode.editor);
     }
   },
 
   /**
    * If modal highlighting is enabled, show the dimmed background that will overlay
    * the page.
    *
-   * @param  {nsIDOMWindow} window The dimmed background will overlay this window.
-   *                               Optional, defaults to the finder window.
-   * @return {AnonymousContent}    Reference to the node inserted into the
-   *                               CanvasFrame. It'll also be stored in the
-   *                               `_modalHighlightOutline` member variable.
+   * @param {nsIDOMWindow} window The dimmed background will overlay this window.
+   *                              Optional, defaults to the finder window.
    */
   show(window = null) {
-    if (!this._modal)
-      return null;
+    if (!this._modal || this.visible)
+      return;
 
+    this.visible = true;
     window = window || this.finder._getWindow();
-    let anonNode = this._maybeCreateModalHighlightNodes(window);
+    this._maybeCreateModalHighlightNodes(window);
     this._addModalHighlightListeners(window);
-
-    return anonNode;
   },
 
   /**
    * Clear all highlighted matches. If modal highlighting is enabled and
    * the outline + dimmed background is currently visible, both will be hidden.
+   *
+   * @param {nsIDOMWindow} window    The dimmed background will overlay this window.
+   *                                 Optional, defaults to the finder window.
+   * @param {nsIDOMRange}  skipRange A range that should not be removed from the
+   *                                 find selection.
    */
-  hide(window = null) {
+  hide(window = null, skipRange = null) {
     window = window || this.finder._getWindow();
 
     let doc = window.document;
-    let controller = this.finder._getSelectionController(window);
-    let sel = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
-    sel.removeAllRanges();
+    this._clearSelection(this.finder._getSelectionController(window), skipRange);
 
     // Next, check our editor cache, for editors belonging to this
     // document
     if (this._editors) {
       for (let x = this._editors.length - 1; x >= 0; --x) {
         if (this._editors[x].document == doc) {
-          sel = this._editors[x].selectionController
-                                .getSelection(Ci.nsISelectionController.SELECTION_FIND);
-          sel.removeAllRanges();
+          this._clearSelection(this._editors[x].selectionController, skipRange);
           // We don't need to listen to this editor any more
           this._unhookListenersAtIndex(x);
         }
       }
     }
 
-    if (!this._modal)
+    if (!this._modal || !this.visible)
       return;
 
     if (this._modalHighlightOutline)
       this._modalHighlightOutline.setAttributeForElement(kModalOutlineId, "hidden", "true");
 
     this._removeHighlightAllMask(window);
     this._removeModalHighlightListeners(window);
     delete this._brightText;
+
+    this.visible = false;
   },
 
   /**
    * Called by the Finder after a find result comes in; update the position and
    * content of the outline to the newly found occurrence.
    * To make sure that the outline covers the found range completely, all the
    * CSS styles that influence the text are copied and applied to the outline.
    *
@@ -330,62 +332,79 @@ FinderHighlighter.prototype = {
    *   {String}  linkURL       If a link was hit, this will contain a URL string.
    *   {Rect}    rect          An object with top, left, width and height
    *                           coordinates of the current selection.
    *   {String}  searchString  The string the search was performed with.
    *   {Boolean} storeResult   Indicator if the search string should be stored
    *                           by the consumer of the Finder.
    */
   update(data) {
-    if (!this._modal)
+    let window = this.finder._getWindow();
+    let foundRange = this.finder._fastFind.getFoundRange();
+    if (!this._modal) {
+      if (this._highlightAll) {
+        this.hide(window, foundRange);
+        let params = this.iterator.params;
+        if (params.word)
+          this.highlight(true, params.word, params.linksOnly);
+      }
       return;
+    }
 
     // Place the match placeholder on top of the current found range.
-    let foundRange = this.finder._fastFind.getFoundRange();
     if (data.result == Ci.nsITypeAheadFind.FIND_NOTFOUND || !foundRange) {
       this.hide();
       return;
     }
 
-    let window = this.finder._getWindow();
-    let textContent = this._getRangeContentArray(foundRange);
-    if (!textContent.length) {
-      this.hide(window);
-      return;
-    }
+    let outlineNode;
+    if (foundRange !== this._currentFoundRange || data.findAgain) {
+      this._currentFoundRange = foundRange;
+
+      let textContent = this._getRangeContentArray(foundRange);
+      if (!textContent.length) {
+        this.hide(window);
+        return;
+      }
+
+      let rect = foundRange.getBoundingClientRect();
+      let fontStyle = this._getRangeFontStyle(foundRange);
+      if (typeof this._brightText == "undefined") {
+        this._brightText = this._isColorBright(fontStyle.color);
+      }
 
-    let rect = foundRange.getBoundingClientRect();
-    let fontStyle = this._getRangeFontStyle(foundRange);
-    if (typeof this._brightText == "undefined") {
-      this._brightText = this._isColorBright(fontStyle.color);
+      // Text color in the outline is determined by our stylesheet.
+      delete fontStyle.color;
+
+      if (!this.visible)
+        this.show(window);
+      else
+        this._maybeCreateModalHighlightNodes(window);
+
+      outlineNode = this._modalHighlightOutline;
+      outlineNode.setTextContentForElement(kModalOutlineId + "-text", textContent.join(" "));
+      outlineNode.setAttributeForElement(kModalOutlineId + "-text", "style",
+        this._getHTMLFontStyle(fontStyle));
+
+      if (typeof outlineNode.getAttributeForElement(kModalOutlineId, "hidden") == "string")
+        outlineNode.removeAttributeForElement(kModalOutlineId, "hidden");
+      let { scrollX, scrollY } = this._getScrollPosition(window);
+      outlineNode.setAttributeForElement(kModalOutlineId, "style",
+        `top: ${scrollY + rect.top}px; left: ${scrollX + rect.left}px`);
     }
 
-    // Text color in the outline is determined by our stylesheet.
-    delete fontStyle.color;
-
-    let anonNode = this.show(window);
-
-    anonNode.setTextContentForElement(kModalOutlineId + "-text", textContent.join(" "));
-    anonNode.setAttributeForElement(kModalOutlineId + "-text", "style",
-      this._getHTMLFontStyle(fontStyle));
-
-    if (typeof anonNode.getAttributeForElement(kModalOutlineId, "hidden") == "string")
-      anonNode.removeAttributeForElement(kModalOutlineId, "hidden");
-    let { scrollX, scrollY } = this._getScrollPosition(window);
-    anonNode.setAttributeForElement(kModalOutlineId, "style",
-      `top: ${scrollY + rect.top}px; left: ${scrollX + rect.left}px`);
-
-    if (typeof anonNode.getAttributeForElement(kModalOutlineId, "grow") == "string")
+    outlineNode = this._modalHighlightOutline;
+    if (typeof outlineNode.getAttributeForElement(kModalOutlineId, "grow") == "string")
       return;
 
     window.requestAnimationFrame(() => {
-      anonNode.setAttributeForElement(kModalOutlineId, "grow", true);
+      outlineNode.setAttributeForElement(kModalOutlineId, "grow", true);
       this._listenForOutlineEvent(kModalOutlineId, "transitionend", () => {
         try {
-          anonNode.removeAttributeForElement(kModalOutlineId, "grow");
+          outlineNode.removeAttributeForElement(kModalOutlineId, "grow");
         } catch (ex) {}
       });
     });
   },
 
   /**
    * Invalidates the list by clearing the map of highglighted ranges that we
    * keep to build the mask for.
@@ -436,23 +455,44 @@ FinderHighlighter.prototype = {
 
   /**
    * When 'Highlight All' is toggled during a session, this callback is invoked
    * and when it's turned off, the found occurrences will be removed from the mask.
    *
    * @param {Boolean} highlightAll
    */
   onHighlightAllChange(highlightAll) {
+    this._highlightAll = highlightAll;
     if (this._modal && !highlightAll) {
       this.clear();
       this._scheduleRepaintOfMask(this.finder._getWindow());
     }
   },
 
   /**
+   * Utility; removes all ranges from the find selection that belongs to a
+   * controller. Optionally skips a specific range.
+   *
+   * @param  {nsISelectionController} controller
+   * @param  {nsIDOMRange}            skipRange
+   */
+  _clearSelection(controller, skipRange = null) {
+    let sel = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
+    if (!skipRange) {
+      sel.removeAllRanges();
+    } else {
+      for (let i = sel.rangeCount - 1; i >= 0; --i) {
+        let range = sel.getRangeAt(i);
+        if (range !== skipRange)
+          sel.removeRange(range);
+      }
+    }
+  },
+
+  /**
    * Utility; get the nsIDOMWindowUtils for a window.
    *
    * @param  {nsIDOMWindow} window Optional, defaults to the finder window.
    * @return {nsIDOMWindowUtils}
    */
   _getDWU(window = null) {
     return (window || this.finder._getWindow())
       .QueryInterface(Ci.nsIInterfaceRequestor)
@@ -600,35 +640,37 @@ FinderHighlighter.prototype = {
     // sorts.
     this._scheduleRepaintOfMask(window);
   },
 
   /**
    * Lazily insert the nodes we need as anonymous content into the CanvasFrame
    * of a window.
    *
-   * @param  {nsIDOMWindow} window Window to draw in.
-   * @return {AnonymousContent} The reference to the outline node, NOT the mask.
+   * @param {nsIDOMWindow} window Window to draw in.
    */
   _maybeCreateModalHighlightNodes(window) {
     if (this._modalHighlightOutline) {
-      if (!this._modalHighlightAllMask)
-        this._repaintHighlightAllMask(window);
-      return this._modalHighlightOutline;
+      if (!this._modalHighlightAllMask) {
+        // Make sure to at least show the dimmed background.
+        this._repaintHighlightAllMask(window, false);
+        this._scheduleRepaintOfMask(window);
+      }
+      return;
     }
 
     let document = window.document;
     // A hidden document doesn't accept insertAnonymousContent calls yet.
     if (document.hidden) {
       let onVisibilityChange = () => {
         document.removeEventListener("visibilitychange", onVisibilityChange);
         this._maybeCreateModalHighlightNodes(window);
       };
       document.addEventListener("visibilitychange", onVisibilityChange);
-      return null;
+      return;
     }
 
     this._maybeInstallStyleSheet(window);
 
     // The outline needs to be sitting inside a container, otherwise the anonymous
     // content API won't find it by its ID later...
     let container = document.createElement("div");
 
@@ -636,58 +678,60 @@ FinderHighlighter.prototype = {
     let outlineBox = document.createElement("div");
     outlineBox.setAttribute("id", kModalOutlineId);
     outlineBox.className = kModalOutlineId + (kDebug ? ` ${kModalIdPrefix}-findbar-debug` : "");
     let outlineBoxText = document.createElement("span");
     outlineBoxText.setAttribute("id", kModalOutlineId + "-text");
     outlineBox.appendChild(outlineBoxText);
 
     container.appendChild(outlineBox);
-
-    this._repaintHighlightAllMask(window);
-
     this._modalHighlightOutline = kDebug ?
       mockAnonymousContentNode(document.body.appendChild(container.firstChild)) :
       document.insertAnonymousContent(container);
-    return this._modalHighlightOutline;
+
+    // Make sure to at least show the dimmed background.
+    this._repaintHighlightAllMask(window, false);
   },
 
   /**
    * Build and draw the mask that takes care of the dimmed background that
    * overlays the current page and the mask that cuts out all the rectangles of
    * the ranges that were found.
    *
    * @param {nsIDOMWindow} window Window to draw in.
+   * @param {Boolean} [paintContent]
    */
-  _repaintHighlightAllMask(window) {
+  _repaintHighlightAllMask(window, paintContent = true) {
     let document = window.document;
 
     const kMaskId = kModalIdPrefix + "-findbar-modalHighlight-outlineMask";
     let maskNode = document.createElement("div");
 
     // Make sure the dimmed mask node takes the full width and height that's available.
     let {width, height} = this._getWindowDimensions(window);
     maskNode.setAttribute("id", kMaskId);
     maskNode.setAttribute("class", kMaskId + (kDebug ? ` ${kModalIdPrefix}-findbar-debug` : ""));
     maskNode.setAttribute("style", `width: ${width}px; height: ${height}px;`);
     if (this._brightText)
       maskNode.setAttribute("brighttext", "true");
 
-    // Create a DOM node for each rectangle representing the ranges we found.
-    let maskContent = [];
-    const kRectClassName = kModalIdPrefix + "-findbar-modalHighlight-rect";
-    if (this._modalHighlightRectsMap) {
-      for (let rects of this._modalHighlightRectsMap.values()) {
-        for (let rect of rects) {
-          maskContent.push(`<div class="${kRectClassName}" style="top: ${rect.y}px;
-            left: ${rect.x}px; height: ${rect.height}px; width: ${rect.width}px;"></div>`);
+    if (paintContent) {
+      // Create a DOM node for each rectangle representing the ranges we found.
+      let maskContent = [];
+      const kRectClassName = kModalIdPrefix + "-findbar-modalHighlight-rect";
+      if (this._modalHighlightRectsMap) {
+        for (let rects of this._modalHighlightRectsMap.values()) {
+          for (let rect of rects) {
+            maskContent.push(`<div class="${kRectClassName}" style="top: ${rect.y}px;
+              left: ${rect.x}px; height: ${rect.height}px; width: ${rect.width}px;"></div>`);
+          }
         }
       }
+      maskNode.innerHTML = maskContent.join("");
     }
-    maskNode.innerHTML = maskContent.join("");
 
     // Always remove the current mask and insert it a-fresh, because we're not
     // free to alter DOM nodes inside the CanvasFrame.
     this._removeHighlightAllMask(window);
 
     this._modalHighlightAllMask = kDebug ?
       mockAnonymousContentNode(document.body.appendChild(maskNode)) :
       document.insertAnonymousContent(maskNode);
--- a/toolkit/modules/FinderIterator.jsm
+++ b/toolkit/modules/FinderIterator.jsm
@@ -24,16 +24,20 @@ this.FinderIterator = {
   _previousRanges: [],
   _spawnId: 0,
   ranges: [],
   running: false,
 
   // Expose `kIterationSizeMax` to the outside world for unit tests to use.
   get kIterationSizeMax() { return kIterationSizeMax },
 
+  get params() {
+    return Object.assign({}, this._currentParams || this._previousParams);
+  },
+
   /**
    * Start iterating the active Finder docShell, using the options below. When
    * it already started at the request of another consumer, we first yield the
    * results we already collected before continuing onward to yield fresh results.
    * We make sure to pause every `kIterationSizeMax` iterations to make sure we
    * don't block the host process too long. In the case of a break like this, we
    * yield `undefined`, instead of a range.
    * Upon re-entrance after a break, we check if `stop()` was called during the