<?xml version="1.0"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
   - License, v. 2.0. If a copy of the MPL was not distributed with this
   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->


<!DOCTYPE bindings SYSTEM "chrome://global/locale/console.dtd">

<bindings id="consoleBindings"
          xmlns="http://www.mozilla.org/xbl"
          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
          xmlns:xbl="http://www.mozilla.org/xbl">

  <binding id="console-box" extends="xul:box">
    <content>  
      <xul:stringbundle src="chrome://global/locale/console.properties" role="string-bundle"/>
      <xul:vbox class="console-box-internal">
        <xul:vbox class="console-rows" role="console-rows" xbl:inherits="dir=sortOrder"/>
      </xul:vbox>
    </content>

    <implementation>
      <field name="limit" readonly="true">
        250
      </field>

      <field name="fieldMaxLength" readonly="true">
        <!-- Limit displayed string lengths to avoid performance issues. (Bug 796179 and 831020) -->
        200
      </field>

      <field name="showChromeErrors" readonly="true">
        Services.prefs.getBoolPref("javascript.options.showInConsole");
      </field>

      <property name="count" readonly="true">
        <getter>return this.mCount</getter>
      </property>

      <property name="mode">
        <getter>return this.mMode;</getter>
        <setter><![CDATA[
          if (this.mode != val) {
            this.mMode = val || "All";
            this.setAttribute("mode", this.mMode);
            this.selectedItem = null;
          }
          return val;
        ]]></setter>
      </property>

      <property name="filter">
        <getter>return this.mFilter;</getter>
        <setter><![CDATA[
          val = val.toLowerCase();
          if (this.mFilter != val) {
            this.mFilter = val;
            for (let aRow of this.mConsoleRowBox.children) {
              this.filterElement(aRow);
            }
          }
          return val;
        ]]></setter>
      </property>

      <property name="sortOrder">
        <getter>return this.getAttribute("sortOrder");</getter>
        <setter>this.setAttribute("sortOrder", val); return val;</setter>
      </property>
      <field name="mSelectedItem">null</field>
      <property name="selectedItem">
        <getter>return this.mSelectedItem</getter>
        <setter><![CDATA[
          if (this.mSelectedItem)
            this.mSelectedItem.removeAttribute("selected");

          this.mSelectedItem = val;
          if (val)
            val.setAttribute("selected", "true");

          // Update edit commands
          window.updateCommands("focus");
          return val;
        ]]></setter>
      </property>

      <method name="init">
        <body><![CDATA[
          this.mCount = 0;

          this.mConsoleListener = {
            console: this, 
            observe : function(aObject) {
              // The message can arrive a little bit after the xbl binding has been
              // unbind. So node.appendItem will not be available anymore.
              if ('appendItem' in this.console)
                this.console.appendItem(aObject);
            }
          };

          this.mConsoleRowBox = document.getAnonymousElementByAttribute(this, "role", "console-rows");
          this.mStrBundle = document.getAnonymousElementByAttribute(this, "role", "string-bundle");

          try {
            Services.console.registerListener(this.mConsoleListener);
          } catch (ex) {
            appendItem(
              "Unable to display errors - couldn't get Console Service component. " +
              "(Missing @mozilla.org/consoleservice;1)");
            return;
          }

          this.mMode = this.getAttribute("mode") || "All";
          this.mFilter = "";

          this.appendInitialItems();
          window.controllers.insertControllerAt(0, this._controller);
        ]]></body>
      </method>

      <method name="destroy">
        <body><![CDATA[
          Services.console.unregisterListener(this.mConsoleListener);
          window.controllers.removeController(this._controller);
        ]]></body>
      </method>

      <method name="appendInitialItems">
        <body><![CDATA[
          var messages = Services.console.getMessageArray();

          // In case getMessageArray returns 0-length array as null
          if (!messages)
            messages = [];

          var limit = messages.length - this.limit;
          if (limit < 0) limit = 0;

          // Checks if console ever been cleared
          for (var i = messages.length - 1; i >= limit; --i)
            if (!messages[i].message)
              break;

          // Populate with messages after latest "clear"
          while (++i < messages.length)
            this.appendItem(messages[i]);
        ]]></body>
      </method>

      <method name="appendItem">
        <parameter name="aObject"/>
        <body><![CDATA[
          try {
            // Try to QI it to a script error to get more info
            var scriptError = aObject.QueryInterface(Components.interfaces.nsIScriptError);

            // filter chrome urls
            if (!this.showChromeErrors && scriptError.sourceName.substr(0, 9) == "chrome://")
              return;

            // filter private windows
            if (scriptError.isFromPrivateWindow)
              return;

            this.appendError(scriptError);
          } catch (ex) {
            try {
              // Try to QI it to a console message
              var msg = aObject.QueryInterface(Components.interfaces.nsIConsoleMessage);
              if (msg.message)
                this.appendMessage(msg.message);
              else // observed a null/"clear" message
                this.clearConsole();
            } catch (ex2) {
              // Give up and append the object itself as a string
              this.appendMessage(aObject);
            }
          }
        ]]></body>
      </method>

      <method name="_truncateIfNecessary">
        <parameter name="aString"/>
        <parameter name="aMiddleCharacter"/>
        <body><![CDATA[
          if (!aString || aString.length <= this.fieldMaxLength)
            return {string: aString, column: aMiddleCharacter};
          let halfLimit = this.fieldMaxLength / 2;
          if (!aMiddleCharacter || aMiddleCharacter < 0 || aMiddleCharacter > aString.length)
            aMiddleCharacter = halfLimit;

          let startPosition = 0;
          let endPosition = aString.length;
          if (aMiddleCharacter - halfLimit >= 0)
            startPosition = aMiddleCharacter - halfLimit;
          if (aMiddleCharacter + halfLimit <= aString.length)
            endPosition = aMiddleCharacter + halfLimit;
          if (endPosition - startPosition < this.fieldMaxLength)
            endPosition += this.fieldMaxLength - (endPosition - startPosition);
          let truncatedString = aString.substring(startPosition, endPosition);
          let Ci = Components.interfaces;
          let ellipsis = Services.prefs.getComplexValue("intl.ellipsis",
                                                        Ci.nsIPrefLocalizedString).data;
          if (startPosition > 0) {
            truncatedString = ellipsis + truncatedString;
            aMiddleCharacter += ellipsis.length;
          }
          if (endPosition < aString.length)
            truncatedString = truncatedString + ellipsis;

          return {
            string: truncatedString,
            column: aMiddleCharacter - startPosition
          };
        ]]></body>
      </method>

      <method name="appendError">
        <parameter name="aObject"/>
        <body><![CDATA[
          var row = this.createConsoleRow();
          var nsIScriptError = Components.interfaces.nsIScriptError;

          // Is this error actually just a non-fatal warning?
          var warning = aObject.flags & nsIScriptError.warningFlag != 0;

          var typetext = warning ? "typeWarning" : "typeError";
          row.setAttribute("typetext", this.mStrBundle.getString(typetext));
          row.setAttribute("type", warning ? "warning" : "error");
          row.setAttribute("msg", aObject.errorMessage);
          row.setAttribute("category", aObject.category);
          row.setAttribute("time", this.properFormatTime(aObject.timeStamp));
          if (aObject.lineNumber || aObject.sourceName) {
            row.setAttribute("href", this._truncateIfNecessary(aObject.sourceName).string);
            row.mSourceName = aObject.sourceName;
            row.setAttribute("line", aObject.lineNumber);
          } else {
            row.setAttribute("hideSource", "true");
          }
          if (aObject.sourceLine) {
            let sourceLine = aObject.sourceLine.replace(/\s/g, " ");
            let truncatedLineObj = this._truncateIfNecessary(sourceLine, aObject.columnNumber);
            row.setAttribute("code", truncatedLineObj.string);
            row.mSourceLine = sourceLine;
            if (aObject.columnNumber) {
              row.setAttribute("col", aObject.columnNumber);
              row.setAttribute("errorDots", this.repeatChar(" ", truncatedLineObj.column));
              row.setAttribute("errorCaret", " ");
            } else {
              row.setAttribute("hideCaret", "true");
            }
          } else {
            row.setAttribute("hideCode", "true");
          }

          this.appendConsoleRow(row);
        ]]></body>
      </method>

      <method name="appendMessage">
        <parameter name="aMessage"/>
        <parameter name="aType"/>
        <body><![CDATA[
          var row = this.createConsoleRow();
          row.setAttribute("type", aType || "message");
          row.setAttribute("msg", aMessage);
          this.appendConsoleRow(row);
        ]]></body>
      </method>

      <method name="clear">
        <body><![CDATA[
          // add a "clear" message (mainly for other listeners)
          Services.console.logStringMessage(null);
          Services.console.reset();
        ]]></body>
      </method>

      <method name="properFormatTime">
        <parameter name="aTime"/>
        <body><![CDATA[
          const dateServ = Components.classes["@mozilla.org/intl/scriptabledateformat;1"]
                                     .getService(Components.interfaces.nsIScriptableDateFormat);
          let errorTime = new Date(aTime);
          return dateServ.FormatDateTime("", dateServ.dateFormatShort, dateServ.timeFormatSeconds,
              errorTime.getFullYear(), errorTime.getMonth() + 1, errorTime.getDate(),
              errorTime.getHours(), errorTime.getMinutes(), errorTime.getSeconds());
        ]]></body>
      </method>

      <method name="copySelectedItem">
        <body><![CDATA[
          if (this.mSelectedItem) try {
            const clipURI = "@mozilla.org/widget/clipboardhelper;1";
            const clipI = Components.interfaces.nsIClipboardHelper;
            var clipboard = Components.classes[clipURI].getService(clipI);

            clipboard.copyString(this.mSelectedItem.toString(), document);
          } catch (ex) {
            // Unable to copy anything, die quietly
          }
        ]]></body>
      </method>

      <method name="createConsoleRow">
        <body><![CDATA[
          var row = document.createElement("box");
          row.setAttribute("class", "console-row");
          row._IsConsoleRow = true;
          row._ConsoleBox = this;
          return row;
        ]]></body>
      </method>

      <method name="appendConsoleRow">
        <parameter name="aRow"/>
        <body><![CDATA[
          this.filterElement(aRow);
          this.mConsoleRowBox.appendChild(aRow);
          if (++this.mCount > this.limit) this.deleteFirst();
        ]]></body>
      </method>

      <method name="deleteFirst">
        <body><![CDATA[
          var node = this.mConsoleRowBox.firstChild;
          this.mConsoleRowBox.removeChild(node);
          --this.mCount;
        ]]></body>
      </method>

      <method name="clearConsole">
        <body><![CDATA[
          if (this.mCount == 0) // already clear
            return;
          this.mCount = 0;

          var newRows = this.mConsoleRowBox.cloneNode(false);
          this.mConsoleRowBox.parentNode.replaceChild(newRows, this.mConsoleRowBox);
          this.mConsoleRowBox = newRows;
          this.selectedItem = null;
        ]]></body>
      </method>

      <method name="filterElement">
        <parameter name="aRow" />
        <body><![CDATA[
          let anyMatch = ["msg", "line", "code"].some(function (key) {
            return (aRow.hasAttribute(key) &&
                    this.stringMatchesFilters(aRow.getAttribute(key), this.mFilter));
          }, this) || (aRow.mSourceName &&
                       this.stringMatchesFilters(aRow.mSourceName, this.mFilter));

          if (anyMatch) {
            aRow.classList.remove("filtered-by-string")
          } else {
            aRow.classList.add("filtered-by-string")
          }
        ]]></body>
      </method>

      <!-- UTILITY FUNCTIONS -->
      
      <method name="repeatChar">
        <parameter name="aChar"/>
        <parameter name="aCol"/>
        <body><![CDATA[
          if (--aCol <= 0)
            return "";

          for (var i = 2; i < aCol; i += i)
            aChar += aChar;

          return aChar + aChar.slice(0, aCol - aChar.length);
        ]]></body>
      </method>

      <method name="stringMatchesFilters">
        <parameter name="aString"/>
        <parameter name="aFilter"/>
        <body><![CDATA[
          if (!aString || !aFilter) {
            return true;
          }

          let searchStr = aString.toLowerCase();
          let filterStrings = aFilter.split(/\s+/);
          return !filterStrings.some(function (f) {
            return searchStr.indexOf(f) == -1;
          });
        ]]></body>
      </method>
          
      <constructor> this.init(); </constructor>
      <destructor> this.destroy(); </destructor>

      <!-- Command controller for the copy command -->
      <field name="_controller"><![CDATA[({
        _outer: this,

        QueryInterface: function(aIID) {
          if (aIID.equals(Components.interfaces.nsIController) ||
              aIID.equals(Components.interfaces.nsISupports))
            return this;
          throw Components.results.NS_NOINTERFACE;
        },

        supportsCommand: function(aCommand) {
          return aCommand == "cmd_copy";
        },

        isCommandEnabled: function(aCommand) {
          return aCommand == "cmd_copy" && this._outer.selectedItem;
        },

        doCommand: function(aCommand) {
          if (aCommand == "cmd_copy")
            this._outer.copySelectedItem();
        },

        onEvent: function() { }
      });]]></field>
    </implementation>

    <handlers>
      <handler event="mousedown"><![CDATA[
        if (event.button == 0 || event.button == 2) {
          var target = event.originalTarget;

          while (target && !("_IsConsoleRow" in target))
            target = target.parentNode;

          if (target)
            this.selectedItem = target;
        }
      ]]></handler>
    </handlers>
  </binding>

  <binding id="error" extends="xul:box">
    <content>
      <xul:box class="console-row-internal-box" flex="1">
        <xul:box class="console-row-icon" align="center" xbl:inherits="selected">
          <xul:image class="console-icon" xbl:inherits="src,type"/>
        </xul:box>
        <xul:vbox class="console-row-content" xbl:inherits="selected" flex="1">
          <xul:box class="console-row-msg" align="start">
            <xul:label class="label" xbl:inherits="value=typetext"/>
            <xul:description class="console-error-msg" xbl:inherits="xbl:text=msg" flex="1"/>
            <xul:label class="label console-time" xbl:inherits="value=time"/>
          </xul:box>
          <xul:box class="console-row-file" xbl:inherits="hidden=hideSource">
            <xul:label class="label" value="&errFile.label;"/>
            <xul:box class="console-error-source" xbl:inherits="href,line"/>
            <xul:spacer flex="1"/>
            <xul:hbox class="lineNumberRow" xbl:inherits="line">
              <xul:label class="label" value="&errLine.label;"/>
              <xul:label class="label" xbl:inherits="value=line"/>
            </xul:hbox>
          </xul:box>
          <xul:vbox class="console-row-code" xbl:inherits="selected,hidden=hideCode">
            <xul:label class="monospace console-code" xbl:inherits="value=code" crop="end"/>
            <xul:box xbl:inherits="hidden=hideCaret">
              <xul:label class="monospace console-dots" xbl:inherits="value=errorDots"/>
              <xul:label class="monospace console-caret" xbl:inherits="value=errorCaret"/>
              <xul:spacer flex="1"/>
            </xul:box>
          </xul:vbox>
        </xul:vbox>
      </xul:box>
    </content>

    <implementation>
      <field name="mSourceName">null</field>
      <field name="mSourceLine">null</field>

      <method name="toString">
        <body><![CDATA[
          let msg = "";
          let strBundle = this._ConsoleBox.mStrBundle;

          if (this.hasAttribute("time"))
            msg += strBundle.getFormattedString("errTime", [this.getAttribute("time")]) + "\n";

          msg += this.getAttribute("typetext") + " " + this.getAttribute("msg");

          if (this.hasAttribute("line") && this.mSourceName) {
            msg += "\n" + strBundle.getFormattedString("errFile",
                                        [this.mSourceName]) + "\n";
            if (this.hasAttribute("col")) {
              msg += strBundle.getFormattedString("errLineCol",
                         [this.getAttribute("line"), this.getAttribute("col")]);
            } else
              msg += strBundle.getFormattedString("errLine", [this.getAttribute("line")]);
          }

          if (this.hasAttribute("code"))
            msg += "\n" + strBundle.getString("errCode") + "\n" + this.mSourceLine;

          return msg;
        ]]></body>
      </method>
    </implementation>

  </binding>

  <binding id="message" extends="xul:box">
    <content>
      <xul:box class="console-internal-box" flex="1">
        <xul:box class="console-row-icon" align="center">
          <xul:image class="console-icon" xbl:inherits="src,type"/>
        </xul:box>
        <xul:vbox class="console-row-content" xbl:inherits="selected" flex="1">
          <xul:vbox class="console-row-msg" flex="1">
            <xul:description class="console-msg-text" xbl:inherits="xbl:text=msg"/>
          </xul:vbox>
        </xul:vbox>
      </xul:box>
    </content>

    <implementation>
      <method name="toString">
        <body><![CDATA[
          return this.getAttribute("msg");
        ]]></body>
      </method>
    </implementation>
  </binding>

  <binding id="console-error-source" extends="xul:box">
    <content>
      <xul:label class="text-link" xbl:inherits="value=href" crop="right"/>
    </content>

    <handlers>
      <handler event="click" phase="capturing" button="0" preventdefault="true">
        <![CDATA[
          var url = document.getBindingParent(this).mSourceName;
          url = url.substring(url.lastIndexOf(" ") + 1);
          var line = getAttribute("line");
          gViewSourceUtils.viewSource(url, null, null, line);
        ]]>
      </handler>
    </handlers>
  </binding>

</bindings>
