Bug 580400 - Console should display dividers to separate messages, r+a=dietrich
authorPatrick Walton <pwalton@mozilla.com>
Mon, 09 Aug 2010 15:35:33 -0300
changeset 49222 d729c1359ae025c24a2e964e84416b6b28d881ca
parent 49221 371d935c6f1ad2570b60578fdb97857c9c172807
child 49223 dcff266c39b1edd3c8137529f2bdb0479d840bb9
push id14951
push userrcampbell@mozilla.com
push dateMon, 09 Aug 2010 18:36:02 +0000
treeherderautoland@dcff266c39b1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs580400
milestone2.0b4pre
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 580400 - Console should display dividers to separate messages, r+a=dietrich
toolkit/components/console/hudservice/HUDService.jsm
toolkit/components/console/hudservice/tests/browser/browser_HUDServiceTestsAll.js
toolkit/themes/pinstripe/global/headsUpDisplay.css
toolkit/themes/winstripe/global/headsUpDisplay.css
--- a/toolkit/components/console/hudservice/HUDService.jsm
+++ b/toolkit/components/console/hudservice/HUDService.jsm
@@ -76,16 +76,20 @@ const ELEMENT_NS_URI = "http://www.w3.or
 const ELEMENT_NS = "html:";
 const HUD_STYLESHEET_URI = "chrome://global/skin/headsUpDisplay.css";
 const HUD_STRINGS_URI = "chrome://global/locale/headsUpDisplay.properties";
 
 XPCOMUtils.defineLazyGetter(this, "stringBundle", function () {
   return Services.strings.createBundle(HUD_STRINGS_URI);
 });
 
+// The amount of time in milliseconds that must pass between messages to
+// trigger the display of a new group.
+const NEW_GROUP_DELAY = 5000;
+
 const ERRORS = { LOG_MESSAGE_MISSING_ARGS:
                  "Missing arguments: aMessage, aConsoleNode and aMessageNode are required.",
                  CANNOT_GET_HUD: "Cannot getHeads Up Display with provided ID",
                  MISSING_ARGS: "Missing arguments",
                  LOG_OUTPUT_FAILED: "Log Failure: Could not append messageNode to outputNode",
 };
 
 function HUD_SERVICE()
@@ -308,16 +312,18 @@ HUD_SERVICE.prototype =
   clearDisplay: function HS_clearDisplay(aId)
   {
     var displayNode = this.getOutputNodeById(aId);
     var outputNode = displayNode.querySelectorAll(".hud-output-node")[0];
 
     while (outputNode.firstChild) {
       outputNode.removeChild(outputNode.firstChild);
     }
+
+    outputNode.lastTimestamp = 0;
   },
 
   /**
    * get a unique ID from the sequence generator
    *
    * @returns integer
    */
   sequenceId: function HS_sequencerId()
@@ -684,33 +690,35 @@ HUD_SERVICE.prototype =
       // do not log anything
       return;
     }
 
     if (!aMessage) {
       throw new Error(ERRORS.MISSING_ARGS);
     }
 
+    let lastGroupNode = this.appendGroupIfNecessary(aConsoleNode,
+                                                    aMessage.timestamp);
     if (aFilterString) {
       var filtered = this.filterLogMessage(aFilterString, aMessageNode);
       if (filtered) {
         // we have successfully filtered a message, we need to log it
-        aConsoleNode.appendChild(aMessageNode);
+        lastGroupNode.appendChild(aMessageNode);
         aMessageNode.scrollIntoView(false);
       }
       else {
         // we need to ignore this message by changing its css class - we are
         // still logging this, it is just hidden
         var hiddenMessage = ConsoleUtils.hideLogMessage(aMessageNode);
-        aConsoleNode.appendChild(hiddenMessage);
+        lastGroupNode.appendChild(hiddenMessage);
       }
     }
     else {
       // log everything
-      aConsoleNode.appendChild(aMessageNode);
+      lastGroupNode.appendChild(aMessageNode);
       aMessageNode.scrollIntoView(false);
     }
     // store this message in the storage module:
     this.storage.recordEntry(aMessage.hudId, aMessage);
   },
 
   /**
    * logs a message to the Heads Up Display that originates
@@ -1192,16 +1200,61 @@ HUD_SERVICE.prototype =
       var result = this.logNetActivity(aType, aURI, aActivityObject);
     }
     else if (aType == "console-listener") {
       this.logConsoleActivity(aURI, aActivityObject);
     }
   },
 
   /**
+   * Builds and appends a group to the console if enough time has passed since
+   * the last message.
+   *
+   * @param nsIDOMNode aConsoleNode
+   *        The DOM node that holds the output of the console (NB: not the HUD
+   *        node itself).
+   * @param number aTimestamp
+   *        The timestamp of the newest message in milliseconds.
+   * @returns nsIDOMNode
+   *          The group into which the next message should be written.
+   */
+  appendGroupIfNecessary:
+  function HS_appendGroupIfNecessary(aConsoleNode, aTimestamp)
+  {
+    let hudBox = aConsoleNode;
+    while (hudBox != null && hudBox.getAttribute("class") !== "hud-box") {
+      hudBox = hudBox.parentNode;
+    }
+
+    let lastTimestamp = hudBox.lastTimestamp;
+    let delta = aTimestamp - lastTimestamp;
+    hudBox.lastTimestamp = aTimestamp;
+    if (delta < NEW_GROUP_DELAY) {
+      // No new group needed. Return the most recently-added group, if there is
+      // one.
+      let lastGroupNode = aConsoleNode.querySelector(".hud-group:last-child");
+      if (lastGroupNode != null) {
+        return lastGroupNode;
+      }
+    }
+
+    let chromeDocument = aConsoleNode.ownerDocument;
+    let groupNode = chromeDocument.createElement("vbox");
+    groupNode.setAttribute("class", "hud-group");
+
+    let separatorNode = chromeDocument.createElement("separator");
+    separatorNode.setAttribute("class", "groove hud-divider");
+    separatorNode.setAttribute("orient", "horizontal");
+    groupNode.appendChild(separatorNode);
+
+    aConsoleNode.appendChild(groupNode);
+    return groupNode;
+  },
+
+  /**
    * update loadgroup when the window object is re-created
    *
    * @param string aId
    * @param nsILoadGroup aLoadGroup
    * @returns void
    */
   updateLoadGroup: function HS_updateLoadGroup(aId, aLoadGroup)
   {
@@ -1622,16 +1675,18 @@ function HeadsUpDisplay(aConfig)
   let splitter = this.chromeDocument.createElement("splitter");
   splitter.setAttribute("class", "hud-splitter");
 
   this.notificationBox.insertBefore(splitter,
                                     this.notificationBox.childNodes[1]);
 
   let console = this.createConsole();
 
+  this.HUDBox.lastTimestamp = 0;
+
   this.contentWindow.wrappedJSObject.console = console;
 
   // create the JSTerm input element
   try {
     this.createConsoleInput(this.contentWindow, this.consoleWrap, this.outputNode);
     this.HUDBox.querySelectorAll(".jsterm-input-node")[0].focus();
   }
   catch (ex) {
@@ -1831,16 +1886,18 @@ HeadsUpDisplay.prototype = {
     let consoleFilterToolbar = this.makeFilterToolbar();
     consoleFilterToolbar.setAttribute("id", "viewGroup");
     this.consoleFilterToolbar = consoleFilterToolbar;
     consoleWrap.appendChild(consoleFilterToolbar);
 
     consoleWrap.appendChild(this.outputNode);
     outerWrap.appendChild(consoleWrap);
 
+    this.HUDBox.lastTimestamp = 0;
+
     this.jsTermParentNode = outerWrap;
     this.HUDBox.appendChild(outerWrap);
     return this.HUDBox;
   },
 
 
   /**
    * sets the click events for all binary toggle filter buttons
@@ -2460,16 +2517,19 @@ JSTerm.prototype = {
    *        The message to display.
    * @param boolean aIsInput
    *        True if the message is the user's input, false if the message is
    *        the result of the expression the user typed.
    * @returns void
    */
   writeOutput: function JST_writeOutput(aOutputMessage, aIsInput)
   {
+    let lastGroupNode = HUDService.appendGroupIfNecessary(this.outputNode,
+                                                          Date.now());
+
     var node = this.elementFactory("div");
     if (aIsInput) {
       node.setAttribute("class", "jsterm-input-line");
       aOutputMessage = "> " + aOutputMessage;
     }
     else {
       node.setAttribute("class", "jsterm-output-line");
     }
@@ -2478,27 +2538,30 @@ JSTerm.prototype = {
       let classes = this.cssClassOverride.split(" ");
       for (let i = 0; i < classes.length; i++) {
         node.classList.add(classes[i]);
       }
     }
 
     var textNode = this.textFactory(aOutputMessage);
     node.appendChild(textNode);
-    this.outputNode.appendChild(node);
+
+    lastGroupNode.appendChild(node);
     node.scrollIntoView(false);
   },
 
   clearOutput: function JST_clearOutput()
   {
     let outputNode = this.outputNode;
 
     while (outputNode.firstChild) {
       outputNode.removeChild(outputNode.firstChild);
     }
+
+    outputNode.lastTimestamp = 0;
   },
 
   keyDown: function JSTF_keyDown(aEvent)
   {
     var self = this;
     function handleKeyDown(aEvent) {
       // ctrl-a
       var setTimeout = aEvent.target.ownerDocument.defaultView.setTimeout;
--- a/toolkit/components/console/hudservice/tests/browser/browser_HUDServiceTestsAll.js
+++ b/toolkit/components/console/hudservice/tests/browser/browser_HUDServiceTestsAll.js
@@ -111,17 +111,18 @@ function introspectLogNodes() {
   ok(outputNode.childElementCount > 0, "more than 1 child node");
 
   let domLogEntries =
     outputNode.childNodes;
 
   let count = outputNode.childNodes.length;
   ok(count > 0, "LogCount: " + count);
 
-  let klasses = ["hud-msg-node hud-log",
+  let klasses = ["hud-group",
+                 "hud-msg-node hud-log",
                  "hud-msg-node hud-warn",
                  "hud-msg-node hud-info",
                  "hud-msg-node hud-error",
                  "hud-msg-node hud-exception",
                  "hud-msg-node hud-network"];
 
   function verifyClass(klass) {
     let len = klasses.length;
@@ -242,25 +243,25 @@ function testConsoleLoggingAPI(aMethod)
 
   // test for multiple arguments.
   HUDService.clearDisplay(hudId);
   HUDService.setFilterState(hudId, aMethod, true);
   browser.contentWindow.wrappedJSObject.console[aMethod]("foo", "bar");
 
   let HUD = HUDService.hudWeakReferences[hudId].get();
   let jsterm = HUD.jsterm;
-  let outputLogNode = jsterm.outputNode;
-  ok(/foo bar/.test(outputLogNode.childNodes[0].childNodes[0].nodeValue),
+  let group = jsterm.outputNode.querySelector(".hud-group");
+  ok(/foo bar/.test(group.childNodes[1].childNodes[0].nodeValue),
     "Emitted both console arguments");
 }
 
 function testLogEntry(aOutputNode, aMatchString, aSuccessErrObj)
 {
-  var msgs = aOutputNode.childNodes;
-  for (var i = 0; i < msgs.length; i++) {
+  var msgs = aOutputNode.querySelector(".hud-group").childNodes;
+  for (var i = 1; i < msgs.length; i++) {
     var message = msgs[i].innerHTML.indexOf(aMatchString);
     if (message > -1) {
       ok(true, aSuccessErrObj.success);
       return;
     }
   }
   throw new Error(aSuccessErrObj.err);
 }
@@ -294,67 +295,97 @@ function testOutputOrder()
 {
   let HUD = HUDService.hudWeakReferences[hudId].get();
   let jsterm = HUD.jsterm;
   let outputNode = jsterm.outputNode;
 
   jsterm.clearOutput();
   jsterm.execute("console.log('foo', 'bar');");
 
-  is(outputNode.childNodes.length, 3, "Three children in output");
-  let outputChildren = outputNode.childNodes;
+  let group = outputNode.querySelector(".hud-group");
+  is(group.childNodes.length, 4, "Four children in output");
+  let outputChildren = group.childNodes;
 
   let executedStringFirst =
-    /console\.log\('foo', 'bar'\);/.test(outputChildren[0].childNodes[0].nodeValue);
+    /console\.log\('foo', 'bar'\);/.test(outputChildren[1].childNodes[0].nodeValue);
 
   let outputSecond =
-    /foo bar/.test(outputChildren[1].childNodes[0].nodeValue);
+    /foo bar/.test(outputChildren[2].childNodes[0].nodeValue);
 
   ok(executedStringFirst && outputSecond, "executed string comes first");
 }
 
+function testGroups()
+{
+  let HUD = HUDService.hudWeakReferences[hudId].get();
+  let jsterm = HUD.jsterm;
+  let outputNode = jsterm.outputNode;
+
+  jsterm.clearOutput();
+
+  let timestamp0 = Date.now();
+  jsterm.execute("0");
+  is(outputNode.querySelectorAll(".hud-group").length, 1,
+    "one group exists after the first console message");
+
+  jsterm.execute("1");
+  let timestamp1 = Date.now();
+  if (timestamp1 - timestamp0 < 5000) {
+    is(outputNode.querySelectorAll(".hud-group").length, 1,
+      "only one group still exists after the second console message");
+  }
+
+  HUD.HUDBox.lastTimestamp = 0;   // a "far past" value
+  jsterm.execute("2");
+  is(outputNode.querySelectorAll(".hud-group").length, 2,
+    "two groups exist after the third console message");
+}
+
 function testNullUndefinedOutput()
 {
   let HUD = HUDService.hudWeakReferences[hudId].get();
   let jsterm = HUD.jsterm;
   let outputNode = jsterm.outputNode;
 
   jsterm.clearOutput();
   jsterm.execute("null;");
 
-  is(outputNode.childNodes.length, 2, "Two children in output");
-  let outputChildren = outputNode.childNodes;
+  let group = outputNode.querySelector(".hud-group");
+  is(group.childNodes.length, 3, "Three children in output");
+  let outputChildren = group.childNodes;
 
-  is (outputChildren[1].childNodes[0].nodeValue, "null",
+  is (outputChildren[2].childNodes[0].nodeValue, "null",
       "'null' printed to output");
 
   jsterm.clearOutput();
   jsterm.execute("undefined;");
 
-  is(outputNode.childNodes.length, 2, "Two children in output");
-  outputChildren = outputNode.childNodes;
+  group = outputNode.querySelector(".hud-group");
+  is(group.childNodes.length, 3, "Three children in output");
+  outputChildren = group.childNodes;
 
-  is (outputChildren[1].childNodes[0].nodeValue, "undefined",
+  is (outputChildren[2].childNodes[0].nodeValue, "undefined",
       "'undefined' printed to output");
 }
 
 function testJSInputAndOutputStyling() {
   let jsterm = HUDService.hudWeakReferences[hudId].get().jsterm;
 
   jsterm.clearOutput();
   jsterm.execute("2 + 2");
 
-  let outputChildren = jsterm.outputNode.childNodes;
-  let jsInputNode = outputChildren[0];
+  let group = jsterm.outputNode.querySelector(".hud-group");
+  let outputChildren = group.childNodes;
+  let jsInputNode = outputChildren[1];
   isnot(jsInputNode.childNodes[0].nodeValue.indexOf("2 + 2"), -1,
     "JS input node contains '2 + 2'");
   isnot(jsInputNode.getAttribute("class").indexOf("jsterm-input-line"), -1,
     "JS input node is of the CSS class 'jsterm-input-line'");
 
-  let jsOutputNode = outputChildren[1];
+  let jsOutputNode = outputChildren[2];
   isnot(jsOutputNode.childNodes[0].nodeValue.indexOf("4"), -1,
     "JS output node contains '4'");
   isnot(jsOutputNode.getAttribute("class").indexOf("jsterm-output-line"), -1,
     "JS output node is of the CSS class 'jsterm-output-line'");
 }
 
 function testCreateDisplay() {
   ok(typeof cs.consoleDisplays == "object",
@@ -539,28 +570,29 @@ function testCompletion()
 }
 
 function testExecutionScope()
 {
   content.location.href = TEST_URI;
 
   let HUD = HUDService.hudWeakReferences[hudId].get();
   let jsterm = HUD.jsterm;
-  let outputNode = jsterm.outputNode;
 
   jsterm.clearOutput();
   jsterm.execute("location;");
 
-  is(outputNode.childNodes.length, 2, "Two children in output");
-  let outputChildren = outputNode.childNodes;
+  let group = jsterm.outputNode.querySelector(".hud-group");
 
-  is(/location;/.test(outputChildren[0].childNodes[0].nodeValue), true,
+  is(group.childNodes.length, 3, "Three children in output");
+  let outputChildren = group.childNodes;
+
+  is(/location;/.test(outputChildren[1].childNodes[0].nodeValue), true,
     "'location;' written to output");
 
-  isnot(outputChildren[1].childNodes[0].nodeValue.indexOf(TEST_URI), -1,
+  isnot(outputChildren[2].childNodes[0].nodeValue.indexOf(TEST_URI), -1,
     "command was executed in the window scope");
 }
 
 function testIteration() {
   var id = "foo";
   var it = cs.displayStore(id);
   var entry = it.next();
   var entry2 = it.next();
@@ -712,16 +744,17 @@ function test() {
 
       // ConsoleStorageTests
       testCreateDisplay();
       testRecordEntry();
       testRecordManyEntries();
       testIteration();
       testConsoleHistory();
       testOutputOrder();
+      testGroups();
       testNullUndefinedOutput();
       testJSInputAndOutputStyling();
       testExecutionScope();
       testCompletion();
       testPropertyProvider();
       testNet();
     });
   }, false);
--- a/toolkit/themes/pinstripe/global/headsUpDisplay.css
+++ b/toolkit/themes/pinstripe/global/headsUpDisplay.css
@@ -103,16 +103,20 @@
 .hud-output-node {
     border-bottom: 1px solid #ddd; 
     border-top: 1px solid #ddd; 
     overflow-x: auto; overflow: auto;
     font: 1em monospace; background-color: white;
     width: 100%;
 }
 
+.hud-group:first-child .hud-divider {
+    display: none;
+}
+
 /* JSTerm Styles */
 
 .jsterm-wrapper-node {
     font-family: monospace; 
     font-size: 1em;
     background-color: #000; 
     border: 1px solid #333; 
     padding: 0.1em;
--- a/toolkit/themes/winstripe/global/headsUpDisplay.css
+++ b/toolkit/themes/winstripe/global/headsUpDisplay.css
@@ -15,16 +15,17 @@
  *
  * The Initial Developer of the Original Code is
  *   Mozilla Corporation
  * Portions created by the Initial Developer are Copyright (C) 2010
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *   David Dahl <ddahl@mozilla.com>
+ *   Patrick Walton <pcwalton@mozilla.com>
 
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -99,16 +100,20 @@
 .hud-output-node {
     border-bottom: 1px solid #ddd; 
     border-top: 1px solid #ddd; 
     overflow-x: auto; overflow: auto;
     font: 1em monospace; background-color: white;
     width: 100%;
 }
 
+.hud-group:first-child .hud-divider {
+    display: none;
+}
+
 /* JSTerm Styles */
 
 .jsterm-wrapper-node {
     font-family: monospace; 
     font-size: 1em;
     background-color: #000; 
     border: 1px solid #333; 
     padding: 0.1em;