Bug 892229 - Ctrl+F / Cmd+F should search/filter requests. r=vporof
authorAaron Graham <agraham19@gmail.com>
Mon, 06 Apr 2015 15:11:00 -0400
changeset 257377 30eef7948c54753470230f8d49aeec7759a115a7
parent 257376 6f59be32c4895456303770d2a6b755878a73ae97
child 257378 76a06f930f3b790f69e6b3d13e0e6edd4d5bc856
push id8007
push userraliiev@mozilla.com
push dateMon, 11 May 2015 19:23:16 +0000
treeherdermozilla-aurora@e2ce1aac996e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvporof
bugs892229
milestone40.0a1
Bug 892229 - Ctrl+F / Cmd+F should search/filter requests. r=vporof
browser/devtools/netmonitor/netmonitor-view.js
browser/devtools/netmonitor/netmonitor.xul
browser/devtools/netmonitor/test/browser_net_filter-01.js
browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd
browser/themes/shared/devtools/netmonitor.inc.css
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -55,16 +55,17 @@ const GENERIC_VARIABLES_VIEW_SETTINGS = 
   searchEnabled: true,
   editableValueTooltip: "",
   editableNameTooltip: "",
   preventDisableOnChange: true,
   preventDescriptorModifiers: true,
   eval: () => {}
 };
 const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200; // px
+const FREETEXT_FILTER_SEARCH_DELAY = 200; // ms
 
 /**
  * Object defining the network monitor view components.
  */
 let NetMonitorView = {
   /**
    * Initializes the network monitor view.
    */
@@ -345,16 +346,17 @@ RequestsMenuView.prototype = Heritage.ex
    */
   initialize: function() {
     dumpn("Initializing the RequestsMenuView");
 
     this.widget = new SideMenuWidget($("#requests-menu-contents"));
     this._splitter = $("#network-inspector-view-splitter");
     this._summary = $("#requests-menu-network-summary-label");
     this._summary.setAttribute("value", L10N.getStr("networkMenu.empty"));
+    this.userInputTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 
     Prefs.filters.forEach(type => this.filterOn(type));
     this.sortContents(this._byTiming);
 
     this.allowFocusOnRightClick = true;
     this.maintainSelectionVisible = true;
     this.widget.autoscrollWithAppendedItems = true;
 
@@ -375,16 +377,23 @@ RequestsMenuView.prototype = Heritage.ex
     this._onContextPerfCommand = () => NetMonitorView.toggleFrontendMode();
     this._onReloadCommand = () => NetMonitorView.reloadPage();
 
     this.sendCustomRequestEvent = this.sendCustomRequest.bind(this);
     this.closeCustomRequestEvent = this.closeCustomRequest.bind(this);
     this.cloneSelectedRequestEvent = this.cloneSelectedRequest.bind(this);
     this.toggleRawHeadersEvent = this.toggleRawHeaders.bind(this);
 
+    this.requestsFreetextFilterEvent = this.requestsFreetextFilterEvent.bind(this);
+    this.reFilterRequests = this.reFilterRequests.bind(this);
+
+    this.freetextFilterBox = $("#requests-menu-filter-freetext-text");
+    this.freetextFilterBox.addEventListener("input", this.requestsFreetextFilterEvent, false);
+    this.freetextFilterBox.addEventListener("command", this.requestsFreetextFilterEvent, false);
+
     $("#toolbar-labels").addEventListener("click", this.requestsMenuSortEvent, false);
     $("#requests-menu-footer").addEventListener("click", this.requestsMenuFilterEvent, false);
     $("#requests-menu-clear-button").addEventListener("click", this.reqeustsMenuClearEvent, false);
     $("#network-request-popup").addEventListener("popupshowing", this._onContextShowing, false);
     $("#request-menu-context-newtab").addEventListener("command", this._onContextNewTabCommand, false);
     $("#request-menu-context-copy-url").addEventListener("command", this._onContextCopyUrlCommand, false);
     $("#request-menu-context-copy-image-as-data-uri").addEventListener("command", this._onContextCopyImageAsDataUriCommand, false);
     $("#toggle-raw-headers").addEventListener("click", this.toggleRawHeadersEvent, false);
@@ -435,16 +444,19 @@ RequestsMenuView.prototype = Heritage.ex
     this.widget.removeEventListener("select", this._onSelect, false);
     this.widget.removeEventListener("swap", this._onSwap, false);
     this._splitter.removeEventListener("mousemove", this._onResize, false);
     window.removeEventListener("resize", this._onResize, false);
 
     $("#toolbar-labels").removeEventListener("click", this.requestsMenuSortEvent, false);
     $("#requests-menu-footer").removeEventListener("click", this.requestsMenuFilterEvent, false);
     $("#requests-menu-clear-button").removeEventListener("click", this.reqeustsMenuClearEvent, false);
+    this.freetextFilterBox.removeEventListener("input", this.requestsFreetextFilterEvent, false);
+    this.freetextFilterBox.removeEventListener("command", this.requestsFreetextFilterEvent, false);
+    this.userInputTimer.cancel();
     $("#network-request-popup").removeEventListener("popupshowing", this._onContextShowing, false);
     $("#request-menu-context-newtab").removeEventListener("command", this._onContextNewTabCommand, false);
     $("#request-menu-context-copy-url").removeEventListener("command", this._onContextCopyUrlCommand, false);
     $("#request-menu-context-copy-image-as-data-uri").removeEventListener("command", this._onContextCopyImageAsDataUriCommand, false);
     $("#request-menu-context-resend").removeEventListener("command", this._onContextResendCommand, false);
     $("#request-menu-context-perf").removeEventListener("command", this._onContextPerfCommand, false);
 
     $("#requests-menu-reload-notice-button").removeEventListener("command", this._onReloadCommand, false);
@@ -666,16 +678,41 @@ RequestsMenuView.prototype = Heritage.ex
     } else {
       requestTextarea.value = null;
       responseTextare.value = null;
       $("#raw-headers").hidden = true;
     }
   },
 
   /**
+   * Handles the timeout on the freetext filter textbox
+   */
+  requestsFreetextFilterEvent: function() {
+    this.userInputTimer.cancel();
+    this._currentFreetextFilter = this.freetextFilterBox.value || "";
+
+    if (this._currentFreetextFilter.length === 0) {
+      this.freetextFilterBox.removeAttribute("filled");
+    } else {
+      this.freetextFilterBox.setAttribute("filled", true);
+    }
+
+    this.userInputTimer.initWithCallback(this.reFilterRequests, FREETEXT_FILTER_SEARCH_DELAY, Ci.nsITimer.TYPE_ONE_SHOT);
+  },
+
+  /**
+   * Refreshes the view contents with the newly selected filters
+   */
+  reFilterRequests: function() {
+    this.filterContents(this._filterPredicate);
+    this.refreshSummary();
+    this.refreshZebra();
+  },
+
+  /**
    * Filters all network requests in this container by a specified type.
    *
    * @param string aType
    *        Either "all", "html", "css", "js", "xhr", "fonts", "images", "media"
    *        "flash" or "other".
    */
   filterOn: function(aType = "all") {
     if (aType === "all") {
@@ -695,19 +732,17 @@ RequestsMenuView.prototype = Heritage.ex
     }
     else if (this._activeFilters.indexOf(aType) === -1) {
       this._enableFilter(aType);
     }
     else {
       this._disableFilter(aType);
     }
 
-    this.filterContents(this._filterPredicate);
-    this.refreshSummary();
-    this.refreshZebra();
+    this.reFilterRequests();
   },
 
   /**
    * Same as `filterOn`, except that it only allows a single type exclusively.
    *
    * @param string aType
    *        @see RequestsMenuView.prototype.fitlerOn
    */
@@ -766,44 +801,41 @@ RequestsMenuView.prototype = Heritage.ex
   },
 
   /**
    * Returns a predicate that can be used to test if a request matches any of
    * the active filters.
    */
   get _filterPredicate() {
     let filterPredicates = this._allFilterPredicates;
-
-     if (this._activeFilters.length === 1) {
-       // The simplest case: only one filter active.
-       return filterPredicates[this._activeFilters[0]].bind(this);
-     } else {
-       // Multiple filters active.
-       return requestItem => {
-         return this._activeFilters.some(filterName => {
-           return filterPredicates[filterName].call(this, requestItem);
-         });
-       };
-     }
+    let currentFreetextFilter = this._currentFreetextFilter;
+
+    return requestItem => {
+      return this._activeFilters.some(filterName => {
+        return filterPredicates[filterName].call(this, requestItem) &&
+                filterPredicates["freetext"].call(this, requestItem, currentFreetextFilter);
+      });
+    };
   },
 
   /**
    * Returns an object with all the filter predicates as [key: function] pairs.
    */
   get _allFilterPredicates() ({
     all: () => true,
     html: this.isHtml,
     css: this.isCss,
     js: this.isJs,
     xhr: this.isXHR,
     fonts: this.isFont,
     images: this.isImage,
     media: this.isMedia,
     flash: this.isFlash,
-    other: this.isOther
+    other: this.isOther,
+    freetext: this.isFreetextMatch
   }),
 
   /**
    * Sorts all network requests in this container by a specified detail.
    *
    * @param string aType
    *        Either "status", "method", "file", "domain", "type", "transferred",
    *        "size" or "waterfall".
@@ -952,16 +984,19 @@ RequestsMenuView.prototype = Heritage.ex
       mimeType.contains("/x-shockwave-flash"))) ||
     url.contains(".swf") ||
     url.contains(".flv"),
 
   isOther: function(e)
     !this.isHtml(e) && !this.isCss(e) && !this.isJs(e) && !this.isXHR(e) &&
     !this.isFont(e) && !this.isImage(e) && !this.isMedia(e) && !this.isFlash(e),
 
+  isFreetextMatch: function({ attachment: { url } }, text) //no text is a positive match
+    !text || url.contains(text),
+
   /**
    * Predicates used when sorting items.
    *
    * @param object aFirst
    *        The first item used in the comparison.
    * @param object aSecond
    *        The second item used in the comparison.
    * @return number
@@ -1860,17 +1895,18 @@ RequestsMenuView.prototype = Heritage.ex
   _canvas: null,
   _ctx: null,
   _cachedWaterfallWidth: 0,
   _firstRequestStartedMillis: -1,
   _lastRequestEndedMillis: -1,
   _updateQueue: [],
   _updateTimeout: null,
   _resizeTimeout: null,
-  _activeFilters: ["all"]
+  _activeFilters: ["all"],
+  _currentFreetextFilter: ""
 });
 
 /**
  * Functions handling the sidebar details view.
  */
 function SidebarView() {
   dumpn("SidebarView was instantiated");
 }
--- a/browser/devtools/netmonitor/netmonitor.xul
+++ b/browser/devtools/netmonitor/netmonitor.xul
@@ -42,16 +42,28 @@
                 accesskey="&netmonitorUI.summary.editAndResend.accesskey;"/>
       <menuseparator id="request-menu-context-separator"/>
       <menuitem id="request-menu-context-perf"
                 label="&netmonitorUI.context.perfTools;"
                 accesskey="&netmonitorUI.context.perfTools.accesskey;"/>
     </menupopup>
   </popupset>
 
+  <commandset>
+    <command id="freeTextFilterCommand"
+             oncommand="NetMonitorView.RequestsMenu.freetextFilterBox.focus()"/>
+  </commandset>
+
+  <keyset>
+    <key id="freeTextFilterKey"
+         key="&netmonitorUI.footer.filterFreetext.key;"
+         modifiers="accel"
+         command="freeTextFilterCommand"/>
+  </keyset>
+
   <deck id="body" class="theme-sidebar" flex="1">
 
     <vbox id="network-inspector-view" flex="1">
       <hbox id="network-table-and-sidebar"
             class="devtools-responsive-container"
             flex="1">
         <vbox id="network-table" flex="1" class="devtools-main-content">
           <toolbar id="requests-menu-toolbar"
@@ -747,16 +759,24 @@
                 data-key="flash"
                 label="&netmonitorUI.footer.filterFlash;">
         </button>
         <button id="requests-menu-filter-other-button"
                 class="requests-menu-filter-button requests-menu-footer-button"
                 data-key="other"
                 label="&netmonitorUI.footer.filterOther;">
         </button>
+        <spacer id="requests-menu-spacer-textbox"
+                class="requests-menu-footer-spacer"
+                flex="0"/>
+        <textbox id="requests-menu-filter-freetext-text"
+                 class="requests-menu-footer-textbox devtools-searchinput"
+                 type="search"
+                 required="true"
+                 placeholder="&netmonitorUI.footer.filterFreetext.label;"/>
         <spacer id="requests-menu-spacer"
                 class="requests-menu-footer-spacer"
                 flex="100"/>
         <button id="requests-menu-network-summary-button"
                 class="requests-menu-footer-button"
                 tooltiptext="&netmonitorUI.footer.perf;"/>
         <label id="requests-menu-network-summary-label"
                class="plain requests-menu-footer-label"
--- a/browser/devtools/netmonitor/test/browser_net_filter-01.js
+++ b/browser/devtools/netmonitor/test/browser_net_filter-01.js
@@ -1,33 +1,42 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Test if filtering items in the network table works correctly.
  */
 const BASIC_REQUESTS = [
-  { url: "sjs_content-type-test-server.sjs?fmt=html&res=undefined" },
-  { url: "sjs_content-type-test-server.sjs?fmt=css" },
-  { url: "sjs_content-type-test-server.sjs?fmt=js" },
+  { url: "sjs_content-type-test-server.sjs?fmt=html&res=undefined&text=sample" },
+  { url: "sjs_content-type-test-server.sjs?fmt=css&text=sample" },
+  { url: "sjs_content-type-test-server.sjs?fmt=js&text=sample" },
 ];
 
 const REQUESTS_WITH_MEDIA = BASIC_REQUESTS.concat([
   { url: "sjs_content-type-test-server.sjs?fmt=font" },
   { url: "sjs_content-type-test-server.sjs?fmt=image" },
   { url: "sjs_content-type-test-server.sjs?fmt=audio" },
   { url: "sjs_content-type-test-server.sjs?fmt=video" },
 ]);
 
 const REQUESTS_WITH_MEDIA_AND_FLASH = REQUESTS_WITH_MEDIA.concat([
   { url: "sjs_content-type-test-server.sjs?fmt=flash" },
 ]);
 
 function test() {
   initNetMonitor(FILTERING_URL).then(([aTab, aDebuggee, aMonitor]) => {
+
+    function setFreetextFilter(value) {
+      // Set the text and manually call all callbacks synchronously to avoid the timeout
+      RequestsMenu.freetextFilterBox.value = value;
+      RequestsMenu.requestsFreetextFilterEvent();
+      RequestsMenu.userInputTimer.cancel();
+      RequestsMenu.reFilterRequests();
+    }
+
     info("Starting test... ");
 
     let { $, NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
 
     waitForNetworkEvents(aMonitor, 8).then(() => {
@@ -91,26 +100,47 @@ function test() {
           testFilterButtons(aMonitor, "flash");
           return testContents([0, 0, 0, 0, 0, 0, 0, 1]);
         })
         .then(() => {
           EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
           testFilterButtons(aMonitor, "all");
           return testContents([1, 1, 1, 1, 1, 1, 1, 1]);
         })
+        .then(() => {
+          // Text in filter box that matches nothing should hide all.
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+          setFreetextFilter("foobar");
+          return testContents([0, 0, 0, 0, 0, 0, 0, 0]);
+        })
+        .then(() => {
+          // Text in filter box that matches should filter out everything else.
+          EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-all-button"));
+          setFreetextFilter("sample");
+          return testContents([1, 1, 1, 0, 0, 0, 0, 0]);
+        })
         // ...then combine multiple filters together.
         .then(() => {
           // Enable filtering for html and css; should show request of both type.
+          setFreetextFilter("");
           EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-html-button"));
           EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-css-button"));
           testFilterButtonsCustom(aMonitor, [0, 1, 1, 0, 0, 0, 0, 0, 0, 0]);
           return testContents([1, 1, 0, 0, 0, 0, 0, 0]);
         })
         .then(() => {
+          // Html and css filter enabled and text filter should show just the html and css match.
+          // Should not show both the items that match the button plus the items that match the text.
+          setFreetextFilter("sample");
+          return testContents([1, 1, 0, 0, 0, 0, 0, 0]);
+        })
+
+        .then(() => {
           EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-flash-button"));
+          setFreetextFilter("");
           testFilterButtonsCustom(aMonitor, [0, 1, 1, 0, 0, 0, 0, 0, 1, 0]);
           return testContents([1, 1, 0, 0, 0, 0, 0, 1]);
         })
         .then(() => {
           // Disable some filters. Only one left active.
           EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-css-button"));
           EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-flash-button"));
           testFilterButtons(aMonitor, "html");
--- a/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd
@@ -119,16 +119,21 @@
 <!-- LOCALIZATION NOTE (netmonitorUI.footer.filterFlash): This is the label displayed
   -  in the network details footer for the "Flash" filtering button. -->
 <!ENTITY netmonitorUI.footer.filterFlash  "Flash">
 
 <!-- LOCALIZATION NOTE (netmonitorUI.footer.filterOther): This is the label displayed
   -  in the network details footer for the "Other" filtering button. -->
 <!ENTITY netmonitorUI.footer.filterOther  "Other">
 
+<!-- LOCALIZATION NOTE (netmonitorUI.footer.filterFreetext): This is the label displayed
+  -  in the network details footer for the url filtering textbox. -->
+<!ENTITY netmonitorUI.footer.filterFreetext.label  "Filter URLs">
+<!ENTITY netmonitorUI.footer.filterFreetext.key  "F">
+
 <!-- LOCALIZATION NOTE (netmonitorUI.footer.clear): This is the label displayed
   -  in the network details footer for the "Clear" button. -->
 <!ENTITY netmonitorUI.footer.clear  "Clear">
 
 <!-- LOCALIZATION NOTE (netmonitorUI.footer.clear): This is the label displayed
   -  in the network details footer for the performance analysis button. -->
 <!ENTITY netmonitorUI.footer.perf   "Toggle performance analysis…">
 
--- a/browser/themes/shared/devtools/netmonitor.inc.css
+++ b/browser/themes/shared/devtools/netmonitor.inc.css
@@ -692,16 +692,38 @@ label.requests-menu-status-code {
   color: rgba(245,247,250,1); /* Light foreground text */
 }
 
 .requests-menu-footer-label {
   padding-top: 3px;
   font-weight: 600;
 }
 
+#requests-menu-filter-freetext-text {
+  transition-property: max-width, -moz-padding-end, -moz-padding-start;
+  transition-duration: 250ms;
+  transition-timing-function: ease;
+}
+
+#requests-menu-filter-freetext-text:not([focused]):not([filled]) > .textbox-input-box {
+  overflow: hidden;
+}
+
+#requests-menu-filter-freetext-text:not([focused]):not([filled]) {
+  max-width: 20px !important;
+  -moz-padding-end: 5px;
+  -moz-padding-start: 22px;
+  background-position: 8px center, top left, top left;
+}
+
+#requests-menu-filter-freetext-text[focused],
+#requests-menu-filter-freetext-text[filled] {
+  max-width: 200px !important;
+}
+
 /* Performance analysis buttons */
 
 #requests-menu-network-summary-button {
   background: none;
   box-shadow: none;
   border-color: transparent;
   list-style-image: url(profiler-stopwatch.svg);
   -moz-padding-end: 0;