mail/base/content/mailWidgets.xml
author Ben Campbell <benc@thunderbird.net>
Mon, 21 Jan 2019 17:57:59 +1300
changeset 34279 a770f9f6ca8155aeae822b10f0e21df94910db9e
parent 34203 96b12a150fe1d827b9be239705e6e94492d36c54
child 34403 b4291fd243157744625ee9da830c2204416968c9
permissions -rw-r--r--
Bug 453908 - remove some left-over direct RDF uses from JavaScript code. r=aceman

<?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/. -->

<!-- import-globals-from ../../../mailnews/base/content/dateFormat.js -->
<!-- import-globals-from mailWindow.js -->
<!-- import-globals-from SearchDialog.js -->

<!DOCTYPE bindings [
<!ENTITY % msgHdrViewOverlayDTD SYSTEM "chrome://messenger/locale/msgHdrViewOverlay.dtd" >
%msgHdrViewOverlayDTD;
<!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd" >
%messengerDTD;
]>

<bindings id="mailBindings"
          xmlns="http://www.mozilla.org/xbl"
          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
          xmlns:nc="http://home.netscape.com/NC-rdf#"
          xmlns:xbl="http://www.mozilla.org/xbl">

  <binding id="attachmentlist-base" extends="chrome://messenger/content/richlistbox.xml#xbl-richlistbox">
    <implementation>
      <constructor><![CDATA[
        let children = Array.from(this._childNodes);

        children.filter(aChild => aChild.getAttribute("selected") == "true")
                .forEach(this.selectedItems.append, this.selectedItems);

        children.filter(aChild => !aChild.hasAttribute("context"))
                .forEach(aChild => aChild.setAttribute("context",
                           this.getAttribute("itemcontext")));

        this.sizes = {small: 16, large: 32, tile: 32};
        this.messenger = Cc["@mozilla.org/messenger;1"]
                           .createInstance(Ci.nsIMessenger);

      ]]></constructor>

      <!-- ///////////////// public members ///////////////// -->

      <property name="view">
        <getter><![CDATA[
          return this.getAttribute("view");
        ]]></getter>
        <setter><![CDATA[
          this.setAttribute("view", val);
          this._setImageSize();
          return val;
        ]]></setter>
      </property>

      <property name="orient">
        <getter><![CDATA[
          return this.getAttribute("orient");
        ]]></getter>
        <setter><![CDATA[
          // The current item can get messed up when changing orientation.
          let curr = this.currentItem;
          this.currentItem = null;

          this.setAttribute("orient", val);
          this.currentItem = curr;
          return val;
        ]]></setter>
      </property>

      <property name="itemCount" readonly="true"
                onget="return this._childNodes.length;"/>

      <method name="getIndexOfItem">
        <parameter name="item"/>
        <body><![CDATA[
          for (let i = 0; i < this._childNodes.length; i++) {
            if (this._childNodes[i] === item)
              return i;
          }
          return -1;
        ]]></body>
      </method>
      <method name="getItemAtIndex">
        <parameter name="index"/>
        <body><![CDATA[
          if (index >= 0 && index < this._childNodes.length)
            return this._childNodes[index];
          return null;
        ]]></body>
      </method>
      <method name="getRowCount">
        <body><![CDATA[
          return this._childNodes.length;
        ]]></body>
      </method>
      <method name="getIndexOfFirstVisibleRow">
        <body><![CDATA[
          if (this._childNodes.length == 0)
            return -1;

          // First try to estimate which row is visible, assuming they're all
          // the same height.
          let box = this.scrollbox;
          let estimatedRow = Math.floor(box.scrollTop /
                                        this._childNodes[0].boxObject.height);
          let estimatedIndex = estimatedRow * this._itemsPerRow();
          let offset = this._childNodes[estimatedIndex].boxObject.screenY -
                       box.boxObject.screenY;

          if (offset > 0) {
            // We went too far! Go back until we find an item totally off-
            // screen, then return the one after that.
            for (let i = estimatedIndex - 1; i >= 0; i--) {
              let childBoxObj = this._childNodes[i].boxObject;
              if (childBoxObj.screenY + childBoxObj.height <=
                  box.boxObject.screenY)
                return i + 1;
            }

            // If we get here, we must have gone back to the beginning of the
            // list, so just return 0.
            return 0;
          }
          // We didn't go far enough! Keep going until we find an item at
          // least partially on-screen.
          for (let i = estimatedIndex; i < this._childNodes.length; i++) {
            let childBoxObj = this._childNodes[i].boxObject;
            if (childBoxObj.screenY + childBoxObj.height >
                box.boxObject.screenY > 0)
              return i;
          }

          // If we get here, something is very wrong.
          Cu.reportError(
            "Couldn't get index of first visible row for attachmentlist!\n");
          return -1;
        ]]></body>
      </method>
      <method name="ensureIndexIsVisible">
        <parameter name="index"/>
        <body><![CDATA[
          this.ensureElementIsVisible(this.getItemAtIndex(index));
        ]]></body>
      </method>
      <method name="ensureElementIsVisible">
        <parameter name="item"/>
        <body><![CDATA[
          let box = this.scrollbox;

          // Are we too far down?
          if (item.boxObject.screenY < box.boxObject.screenY)
            box.scrollTop = item.boxObject.y - box.boxObject.y;
          // ... or not far enough?
          else if (item.boxObject.screenY + item.boxObject.height >
                   box.boxObject.screenY + box.boxObject.height)
            box.scrollTop = item.boxObject.y + item.boxObject.height -
                            box.boxObject.y - box.boxObject.height;
        ]]></body>
      </method>
      <method name="scrollToIndex">
        <parameter name="index"/>
        <body><![CDATA[
          let box = this.scrollbox;
          let item = this.getItemAtIndex(index);
          if (!item)
            return;
          box.scrollTop = item.boxObject.y - box.boxObject.y;
        ]]></body>
      </method>
      <method name="appendItem">
        <parameter name="attachment"/>
        <parameter name="name"/>
        <body><![CDATA[
          // -1 appends due to the way getItemAtIndex is implemented.
          return this.insertItemAt(-1, attachment, name);
        ]]></body>
      </method>
      <method name="insertItemAt">
        <parameter name="index"/>
        <parameter name="attachment"/>
        <parameter name="name"/>
        <body><![CDATA[
          const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
          let item = this.ownerDocument.createElementNS(XULNS, "richlistitem");
          item.className = "attachmentItem";
          item.setAttribute("name", name || attachment.name);
          item.setAttribute("role", "option");

          let size;
          if (attachment.size != null && attachment.size != -1)
            size = this.messenger.formatFileSize(attachment.size);
          else // Use a zero-width space so the size label has the right height.
            size = "\u200b";
          item.setAttribute("size", size);

          // Pick out some nice icons (small and large) for the attachment
          if (attachment.contentType == "text/x-moz-deleted") {
            let base = "chrome://messenger/skin/icons/";
            item.setAttribute("image16", base + "attachment-deleted.png");
            item.setAttribute("image32", base + "attachment-deleted-large.png");
          } else {
            item.setAttribute("image16", "moz-icon://" + attachment.name +
                              "?size=16&amp;contentType=" +
                              attachment.contentType);
            item.setAttribute("image32", "moz-icon://" + attachment.name +
                              "?size=32&amp;contentType=" +
                              attachment.contentType);
          }

          item.setAttribute("imagesize", this.sizes[this.getAttribute("view")] || 16);
          item.setAttribute("context", this.getAttribute("itemcontext"));
          item.attachment = attachment;

          this.insertBefore(item, this.getItemAtIndex(index));
          return item;
        ]]></body>
      </method>

      <!-- Get the preferred height (the height that would allow us to fit
           everything without scrollbars) of the attachmentlist's boxObject. -->
      <property name="preferredHeight" readonly="true"
                onget="return this.scrollbox.scrollHeight - this.scrollbox.clientHeight + this.boxObject.height;"/>

      <!-- Find the attachmentitem node for the specified nsIMsgAttachment. -->
      <method name="findItemForAttachment">
        <parameter name="aAttachment"/>
        <body><![CDATA[
          for (let i = 0; i < this.itemCount; i++) {
            let item = this.getItemAtIndex(i);
            if (item.attachment == aAttachment)
              return item;
          }
          return null;
        ]]></body>
      </method>

      <!-- ///////////////// private members ///////////////// -->

      <property name="_childNodes" readonly="true">
        <getter><![CDATA[
          return this.querySelectorAll("richlistitem.attachmentItem");
        ]]></getter>
      </property>
      <property name="scrollbox" readonly="true">
        <getter><![CDATA[
          return document.getAnonymousElementByAttribute(this, "anonid", "scrollbox");
        ]]></getter>
      </property>

      <method name="_fireOnSelect">
        <body><![CDATA[
          if (!this._suppressOnSelect && !this.suppressOnSelect) {
            this.dispatchEvent(new Event("select",
                                         { bubbles: false, cancelable: true }));
          }
        ]]></body>
      </method>

      <method name="_itemsPerRow">
        <body><![CDATA[
          // For 0 or 1 children, we can assume that they all fit in one row.
          if (this._childNodes.length < 2)
            return this._childNodes.length;

          let itemWidth = this._childNodes[1].boxObject.x -
                          this._childNodes[0].boxObject.x;

          if (itemWidth == 0) // Each item takes up a full row
            return 1;
          return Math.floor(this.scrollbox.clientWidth / itemWidth);
        ]]></body>
      </method>

      <method name="_itemsPerCol">
        <parameter name="aItemsPerRow"/>
        <body><![CDATA[
          let itemsPerRow = aItemsPerRow || this._itemsPerRow();

          if (this._childNodes.length == 0)
            return 0;
          else if (this._childNodes.length <= itemsPerRow)
            return 1;

          let itemHeight = this._childNodes[itemsPerRow].boxObject.y -
                           this._childNodes[0].boxObject.y;

          return Math.floor(this.scrollbox.clientHeight / itemHeight);
        ]]></body>
      </method>

      <method name="_setImageSize">
        <body><![CDATA[
          let size = this.sizes[this.view] || 16;

          for (let i = 0; i < this._childNodes.length; i++)
            this._childNodes[i].imageSize = size;
        ]]></body>
      </method>
    </implementation>

    <handlers>
      <!-- The spacebar should work just like the arrow keys, except that the
           focused element doesn't change, so use moveByOffset here. -->
      <handler event="keypress" key=" " modifiers="control shift any"
               action="this.moveByOffset(0, !event.ctrlKey, event.shiftKey);"
               phase="target" preventdefault="true"/>
      <handler event="keypress" keycode="VK_RETURN"><![CDATA[
        if (this.currentItem) {
          this.addItemToSelection(this.currentItem);
          let evt = document.createEvent("XULCommandEvent");
          evt.initCommandEvent("command", true, true, window, 0, event.ctrlKey,
                               event.altKey, event.shiftKey, event.metaKey, null);
          this.currentItem.dispatchEvent(evt);
        }
      ]]></handler>
      <handler event="click" button="0" phase="target"><![CDATA[
        if (this.selType != "multiple" || (!event.ctrlKey && !event.shiftKey &&
                                           !event.metaKey))
          this.clearSelection();
      ]]></handler>
      <!-- make sure we keep the focus... -->
      <handler event="mousedown" button="0"
               action="if (document.commandDispatcher.focusedElement != this) this.focus();"/>
    </handlers>
  </binding>

  <binding id="attachmentlist-horizontal" extends="chrome://messenger/content/mailWidgets.xml#attachmentlist-base">
    <content>
      <xul:scrollbox flex="1" anonid="scrollbox" style="overflow: auto;">
        <xul:hbox flex="1" class="attachmentlist-wrapper">
          <children includes="richlistitem"/>
        </xul:hbox>
      </xul:scrollbox>
    </content>
    <implementation>
      <method name="setOptimumWidth">
        <body><![CDATA[
          if (this._childNodes.length == 0)
            return;

          let width = 0;
          let border = this._childNodes[0].boxObject.width -
                       this._childNodes[0].clientWidth;

          for (let child of this._childNodes)
            width = Math.max(width, child.scrollWidth);
          for (let child of this._childNodes)
            child.width = width + border;
        ]]></body>
      </method>
    </implementation>
    <handlers>
      <handler event="keypress" keycode="VK_LEFT" modifiers="control shift any"
               action="this.moveByOffset(-1, !event.ctrlKey, event.shiftKey);"
               phase="target" preventdefault="true"/>
      <handler event="keypress" keycode="VK_RIGHT" modifiers="control shift any"
               action="this.moveByOffset(1, !event.ctrlKey, event.shiftKey);"
               phase="target" preventdefault="true"/>
      <handler event="keypress" keycode="VK_UP" modifiers="control shift any"
               action="this.moveByOffset(-this._itemsPerRow(), !event.ctrlKey, event.shiftKey);"
               phase="target" preventdefault="true"/>
      <handler event="keypress" keycode="VK_DOWN" modifiers="control shift any"
               action="this.moveByOffset(this._itemsPerRow(), !event.ctrlKey, event.shiftKey);"
               phase="target" preventdefault="true"/>
    </handlers>
  </binding>

  <binding id="attachmentlist-vertical" extends="chrome://messenger/content/mailWidgets.xml#attachmentlist-base">
    <implementation>
      <method name="_itemsPerRow">
        <body><![CDATA[
          // Vertical attachment lists have one item per row by definition.
          return 1;
        ]]></body>
      </method>
    </implementation>
  </binding>

  <binding id="attachmentitem" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
    <implementation>
      <constructor><![CDATA[
        this._updateImage();
      ]]></constructor>

      <property name="imageSize">
        <getter><![CDATA[
          return this.getAttribute("imagesize");
        ]]></getter>
        <setter><![CDATA[
          this.setAttribute("imagesize", val);
          this._updateImage();
          return val;
        ]]></setter>
      </property>

      <property name="image">
        <getter><![CDATA[
          return this.getAttribute("image");
        ]]></getter>
        <setter><![CDATA[
          if (val)
            this.setAttribute("image", val);
          else
            this.removeAttribute("image");
          this._updateImage();
          return val;
        ]]></setter>
      </property>

      <method name="_updateImage">
        <body><![CDATA[
          if (!this.hasAttribute("image")) {
            let icon = document.getAnonymousElementByAttribute(this, "anonid",
                                                               "icon");
            let attr = "image" + this.imageSize;
            if (this.hasAttribute(attr))
              icon.setAttribute("src", this.getAttribute(attr));
          }
        ]]></body>
      </method>
    </implementation>
    <!-- Below, we want the name label to flex but not be any bigger than
         necessary, so add a spacer with a huge flex value. -->
    <content>
      <xul:hbox class="attachmentcell-content" flex="1">
        <xul:hbox align="center">
          <xul:image class="attachmentcell-icon" anonid="icon"
                     xbl:inherits="src=image"/>
        </xul:hbox>
        <xul:hbox class="attachmentcell-text" flex="1">
          <xul:hbox class="attachmentcell-nameselection" flex="1">
              <xul:label class="attachmentcell-name" xbl:inherits="value=name"
                         flex="1" crop="center"/>
          </xul:hbox>
          <xul:spacer flex="99999"/>
          <xul:label class="attachmentcell-size" xbl:inherits="value=size"/>
        </xul:hbox>
      </xul:hbox>
    </content>
    <handlers>
      <handler event="click" button="0" clickcount="2"><![CDATA[
        let evt = document.createEvent("XULCommandEvent");
        evt.initCommandEvent("command", true, true, window, 0, event.ctrlKey,
                             event.altKey, event.shiftKey, event.metaKey, null);
        this.dispatchEvent(evt);
      ]]></handler>
    </handlers>
  </binding>

  <!-- multi-emailHeaderField: presents multiple emailheaderfields with a toggle -->
  <binding id="mail-multi-emailHeaderField">
    <content>
      <xul:hbox class="headerValueBox" anonid="longEmailAddresses" flex="1"
                singleline="true"
                align="baseline">
        <xul:description class="headerValue" containsEmail="true"
                         anonid="emailAddresses" flex="1"
                         orient="vertical" pack="start" />
      </xul:hbox>
      <xul:label class="moreIndicator" onclick="this.parentNode.toggleWrap()"
                 collapsed="true" anonid="more"/>
    </content>

    <implementation>
      <constructor>
        <![CDATA[
          this.mAddresses = [];
          ChromeUtils.import("resource://gre/modules/Services.jsm");
        ]]>
      </constructor>

      <field name="mAddresses"/>

      <!-- addAddressView: a public method used to add an address to this widget.
           aAddresses is an object with 3 properties: displayName, emailAddress and fullAddress
      -->
      <method name="addAddressView">
        <parameter name="aAddress"/>
        <body>
          <![CDATA[
            this.mAddresses.push(aAddress);
          ]]>
        </body>
      </method>

      <!-- resetAddressView: a public method used to reset addresses shown by
           this widget
      -->
      <method name="resetAddressView">
        <body>
          <![CDATA[
            this.mAddresses.length = 0;
          ]]>
        </body>
      </method>

      <!-- updateEmailAddressNode: private method used to set properties on an address node -->
      <method name="updateEmailAddressNode">
        <parameter name="aEmailNode"/>
        <parameter name="aAddress"/>
        <body>
          <![CDATA[
            aEmailNode.setAttribute("label", aAddress.fullAddress || aAddress.displayName || "");
            aEmailNode.removeAttribute("tooltiptext");
            aEmailNode.setAttribute("emailAddress", aAddress.emailAddress || "");
            aEmailNode.setAttribute("fullAddress", aAddress.fullAddress || "");
            aEmailNode.setAttribute("displayName", aAddress.displayName || "");

            // Since the control attribute points to the
            // <mail-multi-emailHeaderField> element rather than the XUL
            // <mail-emailaddress>, screen readers don't know to automagically
            // prepend the label when reading the tag, so we force this.
            // Furthermore, at least on Mac, there is no JS labelElement
            // property at all, so we skip in that case.  We get away with it
            // because there's no screen reader support on the Mac.
            if ("labelElement" in this) {
                let ariaLabel = this.labelElement.value + ": " +
                                (aAddress.fullAddress || aAddress.displayName || "");
                aEmailNode.setAttribute("aria-label", ariaLabel);
            }

            try {
              if (("UpdateEmailNodeDetails" in top) && aAddress.emailAddress)
                UpdateEmailNodeDetails(aAddress.emailAddress, aEmailNode);
            } catch (ex) {
              dump("UpdateEmailNodeDetails failed: " + ex + "\n");
            }
          ]]>
        </body>
      </method>

      <!-- This field is used to buffer the width of the comma node so that
           it only has to be determined once during the lifetime of this
           widget. Otherwise it would cause an expensive reflow every time. -->
      <field name="commaNodeWidth">0</field>

      <!-- fillAddressesNode: private method used to create email address
           nodes for either our short or long view.
           @param aAddressesNode {DOMElement}: the div to add addresses too
           @param all {Boolean}: If false, show only a few addresses + "more"
           @return {Integer} number of addresses we have put into the list
      -->
      <method name="fillAddressesNode">
        <parameter name="aAddressesNode"/>
        <parameter name="all"/>
        <body>
          <![CDATA[
            // try to leverage any cached nodes before creating new ones
            // XXX look for possible perf win using heuristic for the 2nd
            // param instead of hardcoding 1
            var cached = aAddressesNode.childNodes.length;

            // XXXdmose one or more of the ancestor nodes could be collapsed,
            // so this hack just undoes that for all ancestors.  We should do
            // better.  Observed causes include the message header pane being
            // collapsed before the first message has been read, as well as
            // (more common), the <row> containing this widget being collapsed
            // because the previously displayed message didn't have this header.
            for (let node = aAddressesNode; node; node = node.parentNode)
              node.collapsed = false;

            // this ensures that the worst-case "n more" width is considered
            this.addNMore(this.mAddresses.length);
            var availableWidth = aAddressesNode.clientWidth;
            this.more.collapsed = true;

            // add addresses until we're done, or we overflow the allowed lines
            var i = 0;
            for (let curLine = 0, curLineWidth = 0;
                 i < this.mAddresses.length && (all || curLine < this.maxLinesBeforeMore);
                 i++) {
              let newAddressNode;

              // First, add a comma as long as this isn't the first address.
              if (i > 0) {
                if (cached-- > 0) {
                  aAddressesNode.childNodes[i * 2 - 1].hidden = false;
                } else {
                  this.appendComma();
                  if (this.commaNodeWidth == 0)
                    this.commaNodeWidth = this.emailAddresses.lastChild.clientWidth;
                }
              }

              // Now add an email address.
              if (cached-- > 0) {
                newAddressNode = aAddressesNode.childNodes[i * 2];
                newAddressNode.hidden = false;
              } else {
                newAddressNode = document.createElement("mail-emailaddress");

                // Stash the headerName somewhere that UpdateEmailNodeDetails
                // will be able to find it.
                newAddressNode.setAttribute("headerName", this.headerName);

                newAddressNode = aAddressesNode.appendChild(newAddressNode);
              }
              this.updateEmailAddressNode(newAddressNode, this.mAddresses[i]);

              // Reading .clientWidth triggers an expensive reflow, so only
              // do it when necessary for possible early loop exit to display
              // (X more).
              if (!all) {

                // Calculate width and lines, consider the i+1 comma node if we have to
                // <http://www.w3.org/TR/cssom-view/#client-attributes>
                // <https://developer.mozilla.org/en/Determining_the_dimensions_of_elements>
                let newLineWidth = i + 1 < this.mAddresses.length ?
                                   newAddressNode.clientWidth + this.commaNodeWidth :
                                   newAddressNode.clientWidth;
                curLineWidth += newLineWidth;

                let overLineWidth = curLineWidth - availableWidth;
                if (overLineWidth > 0 && i > 0) {
                  curLine++;
                  curLineWidth = newLineWidth;
                }

                // hide the last node spanning into the additional line (n>1)
                // also hide it if <30px left after sliding the address (n=1)
                // or if the last address would be truncated without "more"
                if (curLine >= this.maxLinesBeforeMore &&
                    (this.maxLinesBeforeMore > 1 ||
                     (i + 1 == this.mAddresses.length && overLineWidth > 30) ||
                     newLineWidth - overLineWidth < 30)) {
                  aAddressesNode.lastChild.hidden = true;
                  i--;
                }
              }
            }

            // Update maxAddressesBeforeMore if we exceed the current cache
            // estimate, but only if we aren't supposed to show all addresses.
            if (!all && this.maxAddressesBeforeMore < i)
              this.maxAddressesBeforeMore = i;

            // Hide any extra nodes but keep them around for later.
            cached = aAddressesNode.childNodes.length;
            for (let j = Math.max(i * 2 - 1, 0); j < cached; j++) {
              aAddressesNode.childNodes[j].hidden = true;
            }

            // If we're not required to show all addresses, and there are still
            // addresses remaining, add an (N more) widget.
            if (!all) {
              let remainingAddresses = this.mAddresses.length - i;
              if (remainingAddresses > 0) {
                if (aAddressesNode.childNodes.length % 2 == 0)
                  aAddressesNode.lastChild.hidden = false;
                else
                  this.appendComma();

                this.addNMore(remainingAddresses);
                this.setNMoreTooltiptext(this.mAddresses.slice(-remainingAddresses));
              }
            }

            return i; // number of addresses shown
          ]]>
        </body>
      </method>

      <property name="emailAddresses" readonly="true">
        <getter><![CDATA[
          return document.getAnonymousElementByAttribute(this, "anonid", "emailAddresses");
        ]]></getter>
      </property>
      <property name="longEmailAddresses" readonly="true">
        <getter><![CDATA[
          return document.getAnonymousElementByAttribute(this, "anonid", "longEmailAddresses");
        ]]></getter>
      </property>
      <property name="more" readonly="true">
        <getter><![CDATA[
          return document.getAnonymousElementByAttribute(this, "anonid", "more");
        ]]></getter>
      </property>

      <!-- The number of lines of addresses we will display before adding a
           (more) indicator to the widget. This can be increased using the
           preference mailnews.headers.show_n_lines_before_more . -->
      <field name="maxLinesBeforeMore">1</field>

      <!-- The number addresses which did fit up to now before the (more)
           indicator became necessary to be added. This determines how many
           address elements are cached for the lifetime of the widget. -->
      <field name="maxAddressesBeforeMore">1</field>

      <!-- Public method to build the DOM nodes for display, to be called
           after all the addresses have been added to the widget. It uses
           fillAddressesNode to display at most maxLinesBeforeMore lines of
           addresses plus the (more) widget which can be clicked to reveal
           the rest. The "singleline" attribute is set for one line only. -->
      <method name="buildViews">
        <body>
          <![CDATA[
            this.maxLinesBeforeMore = Services.prefs.getIntPref(
              "mailnews.headers.show_n_lines_before_more");
            const dt = Ci.nsMimeHeaderDisplayTypes;
            let headerchoice = Services.prefs.getIntPref("mail.show_headers");
            if (this.maxLinesBeforeMore < 1 ||
                headerchoice == dt.AllHeaders) {
              this.fillAddressesNode(this.emailAddresses, true);
              this.longEmailAddresses.removeAttribute("singleline");
            } else {
              this.fillAddressesNode(this.emailAddresses, false);
              // force a single line only in the default n=1 case
              if (this.maxLinesBeforeMore > 1)
                this.longEmailAddresses.removeAttribute("singleline");
            }
          ]]>
        </body>
      </method>

      <!-- Append a comma after the (currently) final (email address, we hope!)
           node of this.emailAddresses -->
      <method name="appendComma">
        <body>
          <![CDATA[
            // Create and append a comma.
            let commaNode = document.createElement("text");
            commaNode.setAttribute("value", ",");
            commaNode.setAttribute("class", "emailSeparator");
            this.emailAddresses.appendChild(commaNode);
           ]]>
        </body>
      </method>

      <!-- Add a (N more) widget which can be clicked to reveal the rest. -->
      <method name="addNMore">
        <parameter name="aNumber"/>
        <body>
          <![CDATA[
            // figure out the right plural for the language we're using
            let words = document.getElementById("bundle_messenger")
                                .getString("headerMoreAddrs");
            let moreForm = PluralForm.get(aNumber, words).replace("#1",
                                                                  aNumber);

            // set the "n more" text node
            this.more.setAttribute("value", moreForm);
            // remove the tooltip text of the more widget
            this.more.removeAttribute("tooltiptext");

            this.more.collapsed = false;
          ]]>
        </body>
      </method>

      <!-- This field is used to specify the maximum number of addresses in the more
           button tooltip text. -->
      <field name="tooltipLength">20</field>
      <property name="maxAddressesInMoreTooltipValue"
                onset="return this.tooltipLength=val;"
                onget="return this.tooltipLength;"/>

      <!-- Populate the tooltiptext of the (N more) widget with hidden email
           addresses. -->
      <method name="setNMoreTooltiptext">
        <parameter name="aAddresses"/>
        <body>
          <![CDATA[
            if (aAddresses.length == 0)
              return;

            let tttArray = [];
            for (let i = 0; (i < aAddresses.length) && (i < this.tooltipLength); i++) {
              tttArray.push(aAddresses[i].fullAddress);
            }
            let ttText = tttArray.join(", ");

            let remainingAddresses = aAddresses.length - tttArray.length;
            // not all missing addresses fit in the tooltip
            if (remainingAddresses > 0) {
              // figure out the right plural for the language we're using
              let words = document.getElementById("bundle_messenger")
                                  .getString("headerMoreAddrsTooltip");
              let moreForm = PluralForm.get(remainingAddresses, words)
                                       .replace("#1", remainingAddresses);
              ttText += moreForm;
            }
            this.more.setAttribute("tooltiptext", ttText);
          ]]>
        </body>
      </method>

      <!-- Updates the nodes of this field with a call to
           UpdateExtraAddressProcessing. The parameters are optional fields
           that can contain extra information to be passed to
           UpdateExtraAddressProcessing, the implementation of that function
           should be checked to determine what it requires -->
      <method name="updateExtraAddressProcessing">
        <parameter name="aParam1"/>
        <parameter name="aParam2"/>
        <parameter name="aParam3"/>
        <body>
          <![CDATA[
            if (UpdateExtraAddressProcessing) {
              var childNodes = this.emailAddresses.childNodes;
              for (let i = 0; i < this.mAddresses.length; i++) {
                UpdateExtraAddressProcessing(this.mAddresses[i],
                                             childNodes[i * 2],
                                             aParam1, aParam2, aParam3);
              }
            }
          ]]>
        </body>
      </method>

      <!-- Called when the (more) indicator has been clicked on; re-renders
           the widget with all the addresses. -->
      <method name="toggleWrap">
        <body>
          <![CDATA[
            // Workaround the fact that XUL line-wrapping and "overflow: auto"
            // don't interact properly (bug 492645), without which we
            // would be inadvertently occluding too much of the message header
            // text and forcing the user to scroll unnecessarily (bug 525225).
            //
            // Fake the "All Headers" mode, so that we get a scroll bar
            // Will be reset when a new message loads
            document.getElementById("expandedHeaderView").setAttribute("show_header_mode", "all");

            // Causes different CSS selectors to be used, which allows all
            // of the addresses to be properly displayed and wrapped.
            this.longEmailAddresses.removeAttribute("singleline");

            this.clearChildNodes(this.emailAddresses);

            // Re-render the node, this time with all the addresses.
            this.fillAddressesNode(this.emailAddresses, true);
            // Compute height of 'expandedHeaderView' from 'expandedHeadersBox'.
            let expandedHeaderView = document.getElementById("expandedHeaderView");
            expandedHeaderView.setAttribute("height", document.getElementById("expandedHeadersBox").clientHeight);
            // This attribute will be reinit in the 'UpdateExpandedMessageHeaders()' method.
          ]]>
        </body>
      </method>

      <!-- internal method used to clear both our divs -->
      <method name="clearChildNodes">
        <parameter name="aParentNode"/>
        <body>
          <![CDATA[
            this.more.collapsed = true;

            // We want to keep around the first maxAddressesBeforeMore email
            // address nodes as well as any intervening comma nodes.
            var numItemsToPreserve = this.maxAddressesBeforeMore * 2 - 1;
            var numItemsInNode = aParentNode.childNodes.length;

            while (numItemsInNode && (numItemsInNode > numItemsToPreserve)) {
              aParentNode.lastChild.remove();
              numItemsInNode--;
            }
          ]]>
        </body>
      </method>

      <method name="clearHeaderValues">
        <body>
          <![CDATA[
            // clear out our local state
            this.mAddresses = [];
            this.longEmailAddresses.setAttribute("singleline", "true");
            // remove anything inside of each of our labels....
            this.clearChildNodes(this.emailAddresses);
          ]]>
        </body>
      </method>
    </implementation>
  </binding>

  <binding id="mail-messageids-headerfield">
    <content>
      <xul:hbox class="headerNameBox" align="start" pack="end">
        <xul:image class="addresstwisty" anonid="toggleIcon"
                   onclick="toggleWrap();"/>
      </xul:hbox>
      <xul:hbox class="headerValueBox" flex="1">
        <xul:label class="headerValue" anonid="headerValue" flex="1"/>
      </xul:hbox>
    </content>

    <implementation>
      <constructor>
        <![CDATA[
          this.mMessageIds = [];
          this.showFullMessageIds = false;
        ]]>
      </constructor>

      <property name="headerValue" readonly="true">
        <getter><![CDATA[
          return document.getAnonymousElementByAttribute(this, "anonid", "headerValue");
        ]]></getter>
      </property>
      <property name="toggleIcon" readonly="true">
        <getter><![CDATA[
          return document.getAnonymousElementByAttribute(this, "anonid", "toggleIcon");
        ]]></getter>
      </property>

      <field name="mMessageIds"/>

      <!-- addMessageIdView: a public method used to add a message-id to this widget. -->
      <method name="addMessageIdView">
        <parameter name="aMessageId"/>
        <body>
          <![CDATA[
            this.mMessageIds.push(aMessageId);
          ]]>
        </body>
      </method>

      <!-- updateMessageIdNode: private method used to set properties on an MessageId node -->
      <method name="updateMessageIdNode">
        <parameter name="aMessageIdNode"/>
        <parameter name="aIndex"/>
        <parameter name="aMessageId"/>
        <parameter name="aLastId"/>
        <body>
          <![CDATA[
            var showFullMessageIds = this.showFullMessageIds;

            if (showFullMessageIds || aIndex == aLastId) {
              aMessageIdNode.setAttribute("label", aMessageId);
              aMessageIdNode.removeAttribute("tooltiptext");
            } else {
              aMessageIdNode.setAttribute("label", aIndex);
              aMessageIdNode.setAttribute("tooltiptext", aMessageId);
            }

            aMessageIdNode.setAttribute("index", aIndex);
            aMessageIdNode.setAttribute("messageid", aMessageId);
          ]]>
        </body>
      </method>

      <method name="fillMessageIdNodes">
        <body>
          <![CDATA[
            var headerValue    = this.headerValue;
            var messageIdNodes = headerValue.childNodes;
            var numMessageIds  = this.mMessageIds.length;

            while (messageIdNodes.length > numMessageIds * 2 - 1)
              headerValue.lastChild.remove();

            this.toggleIcon.hidden = numMessageIds <= 1;

            for (var index = 0; index < numMessageIds; index++) {
              if (index * 2 <= messageIdNodes.length - 1) {
                this.updateMessageIdNode(messageIdNodes[index * 2], index + 1,
                                         this.mMessageIds[index], numMessageIds);
              } else {
                var newMessageIdNode = document.createElement("mail-messageid");

                if (index > 0) {
                  var textNode = document.createElement("text");
                  textNode.setAttribute("value", ", ");
                  textNode.setAttribute("class", "messageIdSeparator");
                  headerValue.appendChild(textNode);
                }
                var itemInDocument = headerValue.appendChild(newMessageIdNode);
                this.updateMessageIdNode(itemInDocument, index + 1,
                                         this.mMessageIds[index], numMessageIds);
              }
            }
          ]]>
        </body>
      </method>

      <method name="toggleWrap">
        <body>
          <![CDATA[
            var headerValue        = this.headerValue;
            var messageIdNodes     = headerValue.childNodes;
            var showFullMessageIds = !this.showFullMessageIds;
            var messageIds         = this.mMessageIds;

            for (var i = 0; i < messageIdNodes.length; i += 2) {
              if (showFullMessageIds) {
                this.toggleIcon.setAttribute("open", "true");
                messageIdNodes[i].setAttribute("label", messageIds[i / 2]);
                messageIdNodes[i].removeAttribute("tooltiptext");
                headerValue.removeAttribute("singleline");
              } else {
                this.toggleIcon.removeAttribute("open");
                messageIdNodes[i].setAttribute("label", i / 2 + 1);
                messageIdNodes[i].setAttribute("tooltiptext", messageIds[i / 2]);
              }
            }

            this.showFullMessageIds = showFullMessageIds;
          ]]>
        </body>
      </method>

      <method name="clearHeaderValues">
        <body>
          <![CDATA[
            // clear out our local state
            this.mMessageIds = [];
            if (this.showFullMessageIds) {
              this.showFullMessageIds = false;
              this.toggleIcon.removeAttribute("open");
            }
          ]]>
        </body>
      </method>
    </implementation>
  </binding>

  <binding id="search-menulist-abstract" name="searchMenulistAbstract" extends="xul:box">
    <content>
      <xul:menulist class="search-menulist" xbl:inherits="flex,disabled" oncommand="this.parentNode.onSelect(event)">
        <xul:menupopup class="search-menulist-popup"/>
      </xul:menulist>
    </content>

    <implementation>
      <field name="internalScope">null</field>
      <field name="internalValue">-1</field>
      <field readonly="true" name="validityManager">
        <![CDATA[
           Cc["@mozilla.org/mail/search/validityManager;1"].getService(Ci.nsIMsgSearchValidityManager);
        ]]>
      </field>
      <property name="searchScope" onget="return this.internalScope;">
        <!-- scope ID - retrieve the table -->
        <setter>
          <![CDATA[
            // if scope isn't changing this is a noop
            if (this.internalScope == val) return val;

            this.internalScope = val;
            this.refreshList();
            var targets = this.targets;
            if (targets) {
              for (var i = 0; i < targets.length; i++) {
                targets[i].searchScope = val;
              }
            }
            return val;
          ]]>
        </setter>
      </property>

      <property name="validityTable" readonly="true" onget="return this.validityManager.getTable(this.searchScope)"/>

      <property name="targets" readonly="true">
        <getter>
          <![CDATA[
            var forAttrs =  this.getAttribute("for");
            if (!forAttrs) return null;
            var targetIds = forAttrs.split(",");
            if (targetIds.length == 0) return null;

            var targets = [];
            for (let j = 0, i = 0; i < targetIds.length; i++) {
              var target = document.getElementById(targetIds[i]);
              if (target) targets[j++] = target;
            }
            return targets;
          ]]>
        </getter>
      </property>

      <property name="optargets" readonly="true">
        <getter>
          <![CDATA[
            var forAttrs =  this.getAttribute("opfor");
            if (!forAttrs) return null;
            var optargetIds = forAttrs.split(",");
            if (optargetIds.length == 0) return null;

            var optargets = [];
            var j = 0;
            for (var i = 0; i < optargetIds.length; i++) {
              var optarget = document.getElementById(optargetIds[i]);
              if (optarget) optargets[j++] = optarget;
            }
            return optargets;
          ]]>
        </getter>
      </property>

      <property name="value" onget="return this.internalValue;">
        <setter>
          <![CDATA[
            if (this.internalValue == val)
              return val;
            this.internalValue = val;
            var menulist = document.getAnonymousNodes(this)[0];
            menulist.selectedItem = this.validMenuitem;

            // now notify targets of new parent's value
            var targets = this.targets;
            if (targets) {
              for (var i = 0; i < targets.length; i++) {
                targets[i].parentValue = val;
              }
            }

            // now notify optargets of new op parent's value
            var optargets = this.optargets;
            if (optargets) {
              for (i = 0; i < optargets.length; i++) {
                optargets[i].opParentValue = val;
              }
            }

            return val;
          ]]>
        </setter>
      </property>
      <!-- label forwards to the internal menulist's "label" attribute -->
      <property name="label">
        <getter><![CDATA[
          return document.getAnonymousNodes(this)[0].selectedItem.getAttribute("label");
        ]]></getter>
      </property>
      <property name="validMenuitem" readonly="true">
      <!-- Prepare menulist selection, adding a missing hidden menuitem if needed, and
        updating the disabled state of the menulist label. -->
        <getter>
          <![CDATA[
            if (this.value == -1) // -1 means not initialized
              return null;

            let menulist = document.getAnonymousNodes(this)[0];
            let isCustom = isNaN(this.value);
            let typedValue = isCustom ? this.value : parseInt(this.value);

            // custom attribute to style the unavailable menulist item
            menulist.setAttribute("unavailable",
              (!this.valueIds.includes(typedValue)) ? "true" : null);

            // add a hidden menulist item if value is missing
            let menuitem = menulist.querySelector(`[value="${this.value}"]`);
            if (!menuitem) { // need to add a hidden menuitem
              menuitem = menulist.appendItem(this.valueLabel, this.value);
              menuitem.hidden = true;
            }
            return menuitem;
          ]]>
        </getter>
      </property>
      <method name="refreshList">
        <parameter name="dontRestore"/> <!-- should we not restore old selection? -->
        <body>
          <![CDATA[
            var menuItemIds = this.valueIds;
            var menuItemStrings = this.valueStrings;

            var menulist = document.getAnonymousNodes(this)[0];
            var popup = menulist.firstChild;

            // save our old "value" so we can restore it later
            var oldData;
            if (!dontRestore)
              oldData = menulist.value;

            // remove the old popup children
            while (popup.hasChildNodes())
              popup.lastChild.remove();

            var newSelection;
            var customizePos = -1;
            for (var i = 0; i < menuItemIds.length; ++i) {
              // create the menuitem
              if (Ci.nsMsgSearchAttrib.OtherHeader == menuItemIds[i].toString()) {
                customizePos = i;
              } else {
                var menuitem = document.createElement("menuitem");
                menuitem.setAttribute("label", menuItemStrings[i]);
                menuitem.setAttribute("value", menuItemIds[i]);
                popup.appendChild(menuitem);
                // try to restore the selection
                if (!newSelection || oldData == menuItemIds[i].toString())
                  newSelection = menuitem;
              }
            }
            if (customizePos != -1) {
              var separator = document.createElement("menuseparator");
              popup.appendChild(separator);
              menuitem = document.createElement("menuitem");
              menuitem.setAttribute("label", menuItemStrings[customizePos]);
              menuitem.setAttribute("value", menuItemIds[customizePos]);
              popup.appendChild(menuitem);
            }
            //
            // If we are either uninitialized, or if we are called because
            // of a change in our parent, update the value to the
            // default stored in newSelection.
            //
            if ((this.value == -1 || dontRestore) && newSelection)
              this.value = newSelection.getAttribute("value");
            menulist.selectedItem = this.validMenuitem;
          ]]>
        </body>
      </method>
      <method name="onSelect">
        <parameter name="event"/>
        <body>
          <![CDATA[
            var menulist = document.getAnonymousNodes(this)[0];
            if (menulist.value == Ci.nsMsgSearchAttrib.OtherHeader) {
              // Customize menuitem selected.
              let args = {};
              window.openDialog("chrome://messenger/content/CustomHeaders.xul",
                                "", "modal,centerscreen,resizable,titlebar,chrome",
                                args);
              // User may have removed the custom header currently selected
              // in the menulist so temporarily set the selection to a safe value.
              this.value = Ci.nsMsgSearchAttrib.OtherHeader;
              // rebuild the menulist
              UpdateAfterCustomHeaderChange();
              // Find the created or chosen custom header and select it.
              if (args.selectedVal) {
                let menuitem = menulist.querySelector(`[label="${args.selectedVal}"]`);
                this.value = menuitem.value;
              } else {
                // Nothing was picked in the custom headers editor so just pick something
                // instead of the current "Customize" menuitem.
                this.value = menulist.getItemAtIndex(0).value;
              }
            } else {
              this.value = menulist.value;
            }
          ]]>
        </body>
      </method>
    </implementation>
  </binding>

  <!-- searchattribute - Subject, Sender, To, CC, etc. -->
  <binding id="searchattribute" name="searchAttribute"
           extends="chrome://messenger/content/mailWidgets.xml#search-menulist-abstract">
    <implementation>
      <field name="stringBundle">
        <![CDATA[
          this.Services.strings.createBundle("chrome://messenger/locale/search-attributes.properties")
        ]]>
      </field>
      <property name="valueLabel" readonly="true">
        <getter>
          <![CDATA[
            if (isNaN(this.value)) { // is this a custom term?
              let customTerm = MailServices.filters.getCustomTerm(this.value);
              if (customTerm)
                return customTerm.name;
              // The custom term may be missing after the extension that added it
              // was disabled or removed. We need to notify the user.
              let scriptError = Cc["@mozilla.org/scripterror;1"]
                  .createInstance(Ci.nsIScriptError);
              scriptError.init("Missing custom search term " + this.value,
                  null, null, 0, 0,
                  Ci.nsIScriptError.errorFlag,
                  "component javascript");
              this.Services.console.logMessage(scriptError);
              return this.stringBundle.GetStringFromName("MissingCustomTerm");
            }
            return this.stringBundle.GetStringFromName(
              this.validityManager.getAttributeProperty(parseInt(this.value)));
          ]]>
        </getter>
      </property>
      <property name="valueIds" readonly="true">
        <getter>
          <![CDATA[
            var length = {};
            let result = this.validityTable.getAvailableAttributes(length);
            // add any available custom search terms
            let customEnum = MailServices.filters.getCustomTerms();
            while (customEnum && customEnum.hasMoreElements()) {
              let customTerm =
                 customEnum.getNext()
                           .QueryInterface(Ci.nsIMsgSearchCustomTerm);
              // for custom terms, the array element is a string with the custom id
              // instead of the integer attribute
              if (customTerm.getAvailable(this.searchScope, null))
                result.push(customTerm.id);
            }
            return result;
          ]]>
        </getter>
      </property>
      <property name="valueStrings" readonly="true">
        <getter>
          <![CDATA[
            let strings = [];
            let ids = this.valueIds;
            let hdrsArray = null;
            try {
              let hdrs = this.Services.prefs.getCharPref("mailnews.customHeaders");
              hdrs = hdrs.replace(/\s+/g, ""); // remove white spaces before splitting
              hdrsArray = hdrs.match(/[^:]+/g);
            } catch (ex) {
            }

            let j = 0;
            for (let i = 0; i < ids.length; i++) {
              if (isNaN(ids[i])) { // Is this a custom search term?
                let customTerm = MailServices.filters.getCustomTerm(ids[i]);
                if (customTerm)
                  strings[i] = customTerm.name;
                else
                  strings[i] = "";
              } else if (ids[i] > Ci.nsMsgSearchAttrib.OtherHeader && hdrsArray) {
                strings[i] = hdrsArray[j++];
              } else {
                strings[i] = this.stringBundle.GetStringFromName(
                               this.validityManager.getAttributeProperty(ids[i]));
              }
            }
            return strings;
          ]]>
        </getter>
      </property>
      <constructor>
      <![CDATA[
        ChromeUtils.import("resource:///modules/MailServices.jsm");
        ChromeUtils.import("resource://gre/modules/Services.jsm", this);
        initializeTermFromId(this.id);
      ]]>
      </constructor>
    </implementation>
  </binding>

  <!-- searchoperator - Contains, Is Less than, etc -->
  <binding id="searchoperator" name="searchOperator"
           extends="chrome://messenger/content/mailWidgets.xml#search-menulist-abstract">
    <implementation>
      <field name="searchAttribute">Ci.nsMsgSearchAttrib.Default</field>
      <field name="stringBundle">
        <![CDATA[
          this.Services.strings.createBundle("chrome://messenger/locale/search-operators.properties")
        ]]>
      </field>
      <property name="valueLabel" readonly="true">
        <getter>
          <![CDATA[
            return this.stringBundle.GetStringFromName(this.value);
          ]]>
        </getter>
      </property>
      <property name="valueIds" readonly="true">
        <getter>
          <![CDATA[
            var length = {};
            let isCustom = isNaN(this.searchAttribute);
            if (isCustom) {
              let customTerm = MailServices.filters.getCustomTerm(this.searchAttribute);
              if (customTerm)
                return customTerm.getAvailableOperators(this.searchScope, length);
              return [Ci.nsMsgSearchOp.Contains];
            }
            return this.validityTable.getAvailableOperators(this.searchAttribute, length);
          ]]>
        </getter>
      </property>
      <property name="valueStrings" readonly="true">
        <getter>
          <![CDATA[
            let strings = [];
            let ids = this.valueIds;
            for (let i = 0; i < ids.length; i++)
              strings[i] = this.stringBundle.GetStringFromID(ids[i]);
            return strings;
          ]]>
        </getter>
      </property>
      <property name="parentValue">
        <setter>
          <![CDATA[
            if (this.searchAttribute == val && val != Ci.nsMsgSearchAttrib.OtherHeader) return val;
            this.searchAttribute = val;
            this.refreshList(true); // don't restore the selection, since searchvalue nulls it
            if (val == Ci.nsMsgSearchAttrib.AgeInDays) {
              // We want "Age in Days" to default to "is less than".
              this.value = Ci.nsMsgSearchOp.IsLessThan;
            }
            return val;
          ]]>
        </setter>
        <getter>
          <![CDATA[
            return this.searchAttribute;
          ]]>
        </getter>
      </property>
      <constructor>
        <![CDATA[
          ChromeUtils.import("resource:///modules/MailServices.jsm");
          ChromeUtils.import("resource://gre/modules/Services.jsm", this);
        ]]>
      </constructor>
    </implementation>
  </binding>

  <!-- searchvalue - a widget which dynamically changes its user interface
       depending on what type of data it's supposed to be showing
       currently handles arbitrary text entry, and menulists for
       priority, status, junk status, tags, hasAttachment status,
       and addressbook
  -->
  <binding id="searchvalue" name="searchValue">
    <content>
      <xul:textbox flex="1" class="search-value-textbox" xbl:inherits="disabled"/>
      <xul:menulist flex="1" class="search-value-menulist" xbl:inherits="disabled">
        <xul:menupopup class="search-value-popup">
          <xul:menuitem value="6" stringTag="priorityHighest" class="search-value-menuitem"/>
          <xul:menuitem value="5" stringTag="priorityHigh" class="search-value-menuitem"/>
          <xul:menuitem value="4" stringTag="priorityNormal" class="search-value-menuitem"/>
          <xul:menuitem value="3" stringTag="priorityLow" class="search-value-menuitem"/>
          <xul:menuitem value="2" stringTag="priorityLowest" class="search-value-menuitem"/>
        </xul:menupopup>
      </xul:menulist>
      <xul:menulist flex="1" class="search-value-menulist" xbl:inherits="disabled">
        <xul:menupopup class="search-value-popup">
          <xul:menuitem value="2" stringTag="replied" class="search-value-menuitem"/>
          <xul:menuitem value="1" stringTag="read" class="search-value-menuitem"/>
          <xul:menuitem value="65536" stringTag="new" class="search-value-menuitem"/>
          <xul:menuitem value="4096" stringTag="forwarded" class="search-value-menuitem"/>
          <xul:menuitem value="4" stringTag="flagged" class="search-value-menuitem"/>
        </xul:menupopup>
      </xul:menulist>
      <xul:textbox flex="1" class="search-value-textbox" xbl:inherits="disabled"/>
      <xul:menulist flex="1" class="search-value-menulist" xbl:inherits="disabled">
        <xul:menupopup class="addrbooksPopup" localonly="true"/>
      </xul:menulist>
      <xul:menulist flex="1" class="search-value-menulist" xbl:inherits="disabled">
        <xul:menupopup class="search-value-popup">
        </xul:menupopup>
      </xul:menulist>
      <xul:menulist flex="1" class="search-value-menulist" xbl:inherits="disabled">
        <xul:menupopup class="search-value-popup">
          <xul:menuitem value="2" stringTag="junk" class="search-value-menuitem"/>
        </xul:menupopup>
      </xul:menulist>
      <xul:menulist flex="1" class="search-value-menulist" xbl:inherits="disabled">
        <xul:menupopup class="search-value-popup">
          <xul:menuitem value="0" stringTag="hasAttachments" class="search-value-menuitem"/>
        </xul:menupopup>
      </xul:menulist>
      <xul:menulist flex="1" class="search-value-menulist" xbl:inherits="disabled">
        <xul:menupopup class="search-value-popup">
          <xul:menuitem value="plugin" stringTag="junkScoreOriginPlugin"
                        class="search-value-menuitem"/>
          <xul:menuitem value="user" stringTag="junkScoreOriginUser"
                        class="search-value-menuitem"/>
          <xul:menuitem value="filter" stringTag="junkScoreOriginFilter"
                        class="search-value-menuitem"/>
          <xul:menuitem value="whitelist" stringTag="junkScoreOriginWhitelist"
                        class="search-value-menuitem"/>
          <xul:menuitem value="imapflag" stringTag="junkScoreOriginImapFlag"
                        class="search-value-menuitem"/>
        </xul:menupopup>
      </xul:menulist>
      <xul:textbox flex="1" class="search-value-textbox" xbl:inherits="disabled"
                   type="number"/>
      <xul:hbox flex="1" class="search-value-custom" xbl:inherits="disabled"/>
    </content>
    <implementation>
      <field name="internalOperator">null</field>
      <field name="internalAttribute">null</field>
      <field name="internalValue">null</field>
      <property name="opParentValue" onget="return this.internalOperator;">
        <setter>
          <![CDATA[
            // noop if we're not changing it
            if (this.internalOperator == val)
              return val;

            // Keywords has the null field IsEmpty
            if (this.searchAttribute == Ci.nsMsgSearchAttrib.Keywords) {
              if (val == Ci.nsMsgSearchOp.IsEmpty ||
                  val == Ci.nsMsgSearchOp.IsntEmpty)
                this.setAttribute("selectedIndex", "-1");
              else
                this.setAttribute("selectedIndex", "5");
            }

            // JunkStatus has the null field IsEmpty
            if (this.searchAttribute == Ci.nsMsgSearchAttrib.JunkStatus) {
              if (val == Ci.nsMsgSearchOp.IsEmpty ||
                  val == Ci.nsMsgSearchOp.IsntEmpty)
                this.setAttribute("selectedIndex", "-1");
              else
                this.setAttribute("selectedIndex", "6");
            }

              // if it's not sender, to, cc, alladdresses, or toorcc, we don't care
              if (this.searchAttribute != Ci.nsMsgSearchAttrib.Sender &&
                this.searchAttribute != Ci.nsMsgSearchAttrib.To &&
                this.searchAttribute != Ci.nsMsgSearchAttrib.ToOrCC &&
                this.searchAttribute != Ci.nsMsgSearchAttrib.AllAddresses &&
                this.searchAttribute != Ci.nsMsgSearchAttrib.CC) {
              this.internalOperator = val;
              return val;
            }

            var children = document.getAnonymousNodes(this);
            if (val == Ci.nsMsgSearchOp.IsntInAB ||
                val == Ci.nsMsgSearchOp.IsInAB) {
              // if the old internalOperator was
              // IsntInAB or IsInAB, and the new internalOperator is
              // IsntInAB or IsInAB, noop because the search value
              // was an ab type, and it still is.
              // otherwise, switch to the ab picker and select the PAB
              if (this.internalOperator != Ci.nsMsgSearchOp.IsntInAB &&
                  this.internalOperator != Ci.nsMsgSearchOp.IsInAB) {
                var abs = children[4].querySelector(`[value="moz-abmdbdirectory://abook.mab"]`);
                if (abs)
                  children[4].selectedItem = abs;
                this.setAttribute("selectedIndex", "4");
              }
            } else if (this.internalOperator == Ci.nsMsgSearchOp.IsntInAB ||
                       this.internalOperator == Ci.nsMsgSearchOp.IsInAB) {
              // if the old internalOperator wasn't
              // IsntInAB or IsInAB, and the new internalOperator isn't
              // IsntInAB or IsInAB, noop because the search value
              // wasn't an ab type, and it still isn't.
              // otherwise, switch to the textbox and clear it
              children[0].value = "";
              this.setAttribute("selectedIndex", "0");
            }

            this.internalOperator = val;
            return val;
          ]]>
        </setter>
      </property>
      <!-- parentValue forwards to the attribute -->
      <property name="parentValue" onset="return this.searchAttribute=val;"
                                   onget="return this.searchAttribute;"/>
      <property name="searchAttribute" onget="return this.internalAttribute;">
        <setter>
          <![CDATA[
            // noop if we're not changing it
            if (this.internalAttribute == val) return val;
            this.internalAttribute = val;

            // if the searchAttribute changing, null out the internalOperator
            this.internalOperator = null;

            // we inherit from a deck, so just use it's index attribute
            // to hide/show widgets
            if (isNaN(val)) { // Is this a custom attribute?
              this.setAttribute("selectedIndex", "10");
              let customHbox = document.getAnonymousNodes(this)[10];
              if (this.internalValue)
                customHbox.setAttribute("value", this.internalValue.str);
              // the searchAttribute attribute is intended as a selector in
              // CSS for custom search terms to bind a custom value
              customHbox.setAttribute("searchAttribute", val);
            } else if (val == Ci.nsMsgSearchAttrib.Priority) {
              this.setAttribute("selectedIndex", "1");
            } else if (val == Ci.nsMsgSearchAttrib.MsgStatus) {
              this.setAttribute("selectedIndex", "2");
            } else if (val == Ci.nsMsgSearchAttrib.Date) {
              this.setAttribute("selectedIndex", "3");
            } else if (val == Ci.nsMsgSearchAttrib.Sender) {
              // since the internalOperator is null
              // this is the same as the initial state
              // the initial state for Sender isn't an ab type search
              // it's a text search, so show the textbox
              this.setAttribute("selectedIndex", "0");
            } else if (val == Ci.nsMsgSearchAttrib.Keywords) {
              this.setAttribute("selectedIndex", "5");
            } else if (val == Ci.nsMsgSearchAttrib.JunkStatus) {
              this.setAttribute("selectedIndex", "6");
            } else if (val == Ci.nsMsgSearchAttrib.HasAttachmentStatus) {
              this.setAttribute("selectedIndex", "7");
            } else if (val == Ci.nsMsgSearchAttrib.JunkScoreOrigin) {
              this.setAttribute("selectedIndex", "8");
            } else if (val == Ci.nsMsgSearchAttrib.AgeInDays) {
              let valueBox = document.getAnonymousNodes(this)[9];
              valueBox.min = -40000; // ~-100 years
              valueBox.max = 40000; // ~100 years
              this.setAttribute("selectedIndex", "9");
            } else if (val == Ci.nsMsgSearchAttrib.Size) {
              let valueBox = document.getAnonymousNodes(this)[9];
              valueBox.min = 0;
              valueBox.max = 1000000000;
              this.setAttribute("selectedIndex", "9");
            } else if (val == Ci.nsMsgSearchAttrib.JunkPercent) {
              let valueBox = document.getAnonymousNodes(this)[9];
              valueBox.min = 0;
              valueBox.max = 100;
              this.setAttribute("selectedIndex", "9");
            } else {
              // a normal text field
              this.setAttribute("selectedIndex", "0");
            }
            return val;
          ]]>
        </setter>
      </property>
      <property name="value" onget="return this.internalValue;">
        <setter>
          <![CDATA[
          // val is a nsIMsgSearchValue object
          this.internalValue = val;
          var attrib = this.internalAttribute;
          var children = document.getAnonymousNodes(this);
          this.searchAttribute = attrib;
          if (isNaN(attrib)) { // a custom term
            let customHbox = document.getAnonymousNodes(this)[10];
            customHbox.setAttribute("value", val.str);
            return val;
          }
          if (attrib == Ci.nsMsgSearchAttrib.Priority) {
            var matchingPriority =
              children[1].querySelector(`[value="${val.priority}"]`);
            if (matchingPriority)
              children[1].selectedItem = matchingPriority;
          } else if (attrib == Ci.nsMsgSearchAttrib.MsgStatus) {
            var matchingStatus =
              children[2].querySelector(`[value="${val.status}"]`);
            if (matchingStatus)
              children[2].selectedItem = matchingStatus;
          } else if (attrib == Ci.nsMsgSearchAttrib.AgeInDays) {
            children[9].value = val.age;
          } else if (attrib == Ci.nsMsgSearchAttrib.Date) {
            children[3].value = convertPRTimeToString(val.date);
          } else if (attrib == Ci.nsMsgSearchAttrib.Sender ||
                     attrib == Ci.nsMsgSearchAttrib.To ||
                     attrib == Ci.nsMsgSearchAttrib.CC ||
                     attrib == Ci.nsMsgSearchAttrib.AllAddresses ||
                     attrib == Ci.nsMsgSearchAttrib.ToOrCC) {
            if (this.internalOperator == Ci.nsMsgSearchOp.IsntInAB ||
                this.internalOperator == Ci.nsMsgSearchOp.IsInAB) {
              var abs = children[4].querySelector(`[value="${val.str}"]`);
              if (abs)
                children[4].selectedItem = abs;
            } else {
              children[0].value = val.str;
            }
          } else if (attrib == Ci.nsMsgSearchAttrib.Keywords) {
            var keywordVal = children[5].querySelector(`[value="${val.str}"]`);
            if (keywordVal) {
              children[5].value = val.str;
              children[5].selectedItem = keywordVal;
            }
          } else if (attrib == Ci.nsMsgSearchAttrib.JunkStatus) {
            var junkStatus =
              children[6].querySelector(`[value="${val.junkStatus}"]`);
            if (junkStatus)
              children[6].selectedItem = junkStatus;
          } else if (attrib == Ci.nsMsgSearchAttrib.HasAttachmentStatus) {
            var hasAttachmentStatus =
              children[7].querySelector(`[value="${val.hasAttachmentStatus}"]`);
            if (hasAttachmentStatus)
              children[7].selectedItem = hasAttachmentStatus;
          } else if (attrib == Ci.nsMsgSearchAttrib.JunkScoreOrigin) {
            var junkScoreOrigin =
              children[8].querySelector(`[value="${val.str}"]`);
            if (junkScoreOrigin)
              children[8].selectedItem = junkScoreOrigin;
          } else if (attrib == Ci.nsMsgSearchAttrib.JunkPercent) {
            children[9].value = val.junkPercent;
          } else if (attrib == Ci.nsMsgSearchAttrib.Size) {
            children[9].value = val.size;
          } else {
            children[0].value = val.str;
          }
          return val;
          ]]>
        </setter>
      </property>
      <method name="save">
        <body>
          <![CDATA[
            var searchValue = this.value;
            var searchAttribute = this.searchAttribute;
            var children = document.getAnonymousNodes(this);

            searchValue.attrib = searchAttribute;
            if (searchAttribute == Ci.nsMsgSearchAttrib.Priority) {
              searchValue.priority = children[1].selectedItem.value;
            } else if (searchAttribute == Ci.nsMsgSearchAttrib.MsgStatus) {
              searchValue.status = children[2].value;
            } else if (searchAttribute == Ci.nsMsgSearchAttrib.AgeInDays) {
              searchValue.age = children[9].value;
            } else if (searchAttribute == Ci.nsMsgSearchAttrib.Date) {
              searchValue.date = convertStringToPRTime(children[3].value);
            } else if (searchAttribute == Ci.nsMsgSearchAttrib.Sender ||
                       searchAttribute == Ci.nsMsgSearchAttrib.To ||
                       searchAttribute == Ci.nsMsgSearchAttrib.CC ||
                       searchAttribute == Ci.nsMsgSearchAttrib.AllAddresses ||
                       searchAttribute == Ci.nsMsgSearchAttrib.ToOrCC) {
              if (this.internalOperator == Ci.nsMsgSearchOp.IsntInAB ||
                  this.internalOperator == Ci.nsMsgSearchOp.IsInAB) {
                searchValue.str = children[4].selectedItem.value;
              } else {
                searchValue.str = children[0].value;
              }
            } else if (searchAttribute == Ci.nsMsgSearchAttrib.Keywords) {
              searchValue.str = children[5].value;
            } else if (searchAttribute == Ci.nsMsgSearchAttrib.JunkStatus) {
              searchValue.junkStatus = children[6].value;
            } else if (searchAttribute == Ci.nsMsgSearchAttrib.JunkPercent) {
              searchValue.junkPercent = children[9].value;
            } else if (searchAttribute == Ci.nsMsgSearchAttrib.Size) {
              searchValue.size = children[9].value;
            } else if (searchAttribute == Ci.nsMsgSearchAttrib.HasAttachmentStatus) {
              searchValue.status = Ci.nsMsgMessageFlags.Attachment;
            } else if (searchAttribute == Ci.nsMsgSearchAttrib.JunkScoreOrigin) {
              searchValue.str = children[8].value;
            } else if (isNaN(searchAttribute)) { // a custom term
              searchValue.attrib = Ci.nsMsgSearchAttrib.Custom;
              searchValue.str = children[10].getAttribute("value");
            } else {
              searchValue.str = children[0].value;
            }
          ]]>
        </body>
      </method>
      <method name="saveTo">
        <parameter name="searchValue"/>
        <body>
          <![CDATA[
            this.internalValue = searchValue;
            this.save();
          ]]>
        </body>
      </method>
      <method name="fillInTags">
        <body>
          <![CDATA[
            var children = document.getAnonymousNodes(this);
            var popupMenu = children[5].firstChild;
            let tagArray = MailServices.tags.getAllTags({});
            for (var i = 0; i < tagArray.length; ++i) {
              var taginfo = tagArray[i];
              var newMenuItem = document.createElement("menuitem");
              newMenuItem.setAttribute("label", taginfo.tag);
              newMenuItem.setAttribute("value", taginfo.key);
              popupMenu.appendChild(newMenuItem);
              if (!i)
                children[5].selectedItem = newMenuItem;
            }
          ]]>
        </body>
      </method>
      <method name="fillStringsForChildren">
        <parameter name="parentNode"/>
        <parameter name="bundle"/>
        <body>
          <![CDATA[
            var children = parentNode.childNodes;
            var len = children.length;
            for (var i = 0; i < len; i++) {
              var node = children[i];
              var stringTag = node.getAttribute("stringTag");
              if (stringTag) {
                var attr = (node.tagName == "label") ? "value" : "label";
                node.setAttribute(attr, bundle.GetStringFromName(stringTag));
              }
            }
          ]]>
        </body>
      </method>
      <method name="initialize">
        <parameter name="menulist"/>
        <parameter name="bundle"/>
        <body>
          <![CDATA[
            this.fillStringsForChildren(menulist.firstChild, bundle);
          ]]>
        </body>
      </method>
      <constructor>
      <![CDATA[
        ChromeUtils.import("resource:///modules/MailServices.jsm");
        ChromeUtils.import("resource://gre/modules/Services.jsm", this);

        // initialize strings
        var bundle = this.Services.strings.createBundle("chrome://messenger/locale/messenger.properties");

        // initialize the priority picker
        this.initialize(document.getAnonymousNodes(this)[1], bundle);

        // initialize the status picker
        this.initialize(document.getAnonymousNodes(this)[2], bundle);

        // initialize the date picker
        var datePicker = document.getAnonymousNodes(this)[3];
        var searchAttribute = this.searchAttribute;
        var time;
        if (searchAttribute == Ci.nsMsgSearchAttrib.Date)
         time = datePicker.value;
        else
         time = new Date();
        // do .value instead of .setAttribute("value", xxx);
        // to work around for bug #179412
        // (caused by bug #157210)
        //
        // the searchvalue widget has two textboxes
        // one for text, one as a placeholder for a date / calendar widget
        datePicker.value = convertDateToString(time);

        // initialize the address book picker
        this.initialize(document.getAnonymousNodes(this)[4], bundle);

        // initialize the junk status picker
        this.initialize(document.getAnonymousNodes(this)[6], bundle);

        // initialize the has attachment status picker
        this.initialize(document.getAnonymousNodes(this)[7], bundle);

        // initialize the junk score origin picker
        this.initialize(document.getAnonymousNodes(this)[8], bundle);

        // initialize the tag list
        this.fillInTags();
      ]]>
      </constructor>
    </implementation>
    <handlers>
      <handler event="keypress" keycode="VK_RETURN" modifiers="accel any"
               action="onEnterInSearchTerm(event);" preventdefault="true"/>
    </handlers>
  </binding>

  <!-- Folder picker helper widgets -->
  <binding id="popup-base" extends="chrome://global/content/bindings/popup.xml#popup">
    <implementation>
      <field name="tree" readonly="true">
        document.getAnonymousNodes(this)[0];
      </field>
      <method name="updateHover">
        <parameter name="event"/>
        <body>
          <![CDATA[
            if (event.originalTarget == this.tree.treeBody) {
              var index = this.tree.getRowAt(event.clientX, event.clientY);
              this.tree.view.selection.select(index);
              return index;
            }
            return -1;
          ]]>
        </body>
      </method>
      <method name="fire">
        <body>
          <![CDATA[
            this.hidePopup();
            if (this.tree.currentIndex >= 0) {
              this.setAttribute("uri", this.tree.view.getResourceAtIndex(this.tree.currentIndex).Value);
              this.doCommand();
            }
          ]]>
        </body>
      </method>
      <method name="onBlurMenuList">
        <parameter name="event"/>
        <body>
          <![CDATA[
            this.openMenu(false);
          ]]>
        </body>
      </method>
      <field name="onKeyPressMenuList" readonly="true">
        <![CDATA[
          ({
            self: this,
            tree: this.tree,
            parentNode: this.parentNode,
            getLastVisibleRow(tree) {
              var f = tree.getFirstVisibleRow();
              var p = tree.getPageLength();
              var l = tree.view.rowCount;
              return (l < f + p ? l : f + p) - 1;
            },
            handleEvent(event) {
              if (event.altKey)
                return;
              var index;
              if (this.parentNode.hasAttribute("open")) {
                event.stopPropagation();
                event.preventDefault();
                switch (event.keyCode) {
                  case event.DOM_VK_ESCAPE:
                    this.self.hidePopup();
                    return;
                  case event.DOM_VK_RETURN:
                    this.self.fire();
                    return;
                }
                index = this.tree.currentIndex;
              } else {
                switch (event.keyCode) {
                  case event.DOM_VK_PAGE_UP:
                  case event.DOM_VK_PAGE_DOWN:
                    return;
                }
                index = this.self.setInitialSelection();
              }
              switch (event.keyCode) {
                case event.DOM_VK_UP:
                  if (index <= 0)
                    return;
                  index--;
                  break;
                case event.DOM_VK_DOWN:
                  index++;
                  if (index == this.tree.view.rowCount)
                    return;
                  break;
                case event.DOM_VK_PAGE_UP:
                  if (index == this.tree.getFirstVisibleRow())
                    this.tree.scrollByPages(-1);
                  index = this.tree.getFirstVisibleRow();
                  break;
                case event.DOM_VK_PAGE_DOWN:
                  if (index == this.getLastVisibleRow(this.tree))
                    this.tree.scrollByPages(1);
                  index = this.getLastVisibleRow(this.tree);
                  break;
                case event.DOM_VK_HOME:
                  index = 0;
                  break;
                case event.DOM_VK_END:
                  index = this.tree.view.rowCount - 1;
                  break;
                default:
                  if (event.charCode > 0 && !event.ctrlKey && !event.metaKey) {
                    event.preventDefault();
                    index = this.tree.keyNavigate(event);
                    if (index >= 0)
                      break;
                  }
                  return;
              }
              this.tree.view.selection.select(index);
              if (this.parentNode.hasAttribute("open"))
                this.tree.ensureRowIsVisible(index);
              else
                this.self.fire();
            },
          })
        ]]>
      </field>
      <method name="setInitialSelection">
        <body>
          <![CDATA[
            ChromeUtils.import("resource:///modules/MailUtils.jsm", this);

            var view = this.tree.view;

            if (!view.selection.currentColumn)
              view.selection.currentColumn = this.tree.columns.getFirstColumn();

            view.selection.selectEventsSuppressed = true;
            for (var i = 0; i < view.rowCount; i++) {
              if (view.isContainer(i)) {
                if (view.isContainerEmpty(i) == view.isContainerOpen(i))
                  view.toggleOpenState(i);
                if (view.isContainerOpen(i)) {
                  if (i + 1 == view.rowCount ||
                      view.getLevel(i + 1) <= view.getLevel(i)) {
                    view.toggleOpenState(i);
                  }
                }
              }
            }
            var index = -1;
            var uri = this.parentNode.getAttribute("uri");
            if (uri) {
              index = view.getIndexOfResource(this.MailUtils.getOrCreateFolder(uri));
            }
            view.selection.select(index);
            return index;
          ]]>
        </body>
      </method>
      <constructor>
        <![CDATA[
          this.setAttribute("ignorekeys", "true");
          this.parentNode.addEventListener("keypress", this.onKeyPressMenuList, true);
        ]]>
      </constructor>
      <destructor>
        <![CDATA[
          this.parentNode.removeEventListener("keypress", this.onKeyPressMenuList, true);
        ]]>
      </destructor>
    </implementation>
    <handlers>
      <handler event="mousemove" action="this.updateHover(event);"/>
      <handler event="click" button="0" action="if (this.updateHover(event) >= 0) this.fire();"/>
      <handler event="popupshowing">
        <![CDATA[
          this.parentNode.addEventListener("blur", this.onBlurMenuList);
          this.tree.focused = true;
          var index = this.setInitialSelection();
          var height = this.tree.view.rowCount * this.tree.rowHeight;
          height += this.boxObject.height - this.tree.treeBody.boxObject.height;
          this.height = height;
          if (index >= 0)
            setTimeout(function() { this.tree.ensureRowIsVisible(index); }, 0);
        ]]>
      </handler>
      <handler event="popuphiding">
        <![CDATA[
          this.parentNode.removeEventListener("blur", this.onBlurMenuList);
        ]]>
      </handler>
    </handlers>
  </binding>

  <binding id="splitmenu">
    <content>
      <xul:hbox anonid="menuitem" flex="1"
                class="splitmenu-menuitem"
                xbl:inherits="iconic,label,disabled,onclick=oncommand,_moz-menuactive=active"/>
      <xul:menu anonid="menu" class="splitmenu-menu"
                xbl:inherits="disabled,_moz-menuactive=active"
                oncommand="event.stopPropagation();">
        <children includes="menupopup"/>
      </xul:menu>
    </content>

    <implementation>
      <constructor><![CDATA[
        this._parentMenupopup.addEventListener("DOMMenuItemActive", this);
        this._parentMenupopup.addEventListener("popuphidden", this);
      ]]></constructor>

      <destructor><![CDATA[
        this._parentMenupopup.removeEventListener("DOMMenuItemActive", this);
        this._parentMenupopup.removeEventListener("popuphidden", this);
      ]]></destructor>

      <field name="menuitem" readonly="true">
        document.getAnonymousElementByAttribute(this, "anonid", "menuitem");
      </field>
      <field name="menu" readonly="true">
        document.getAnonymousElementByAttribute(this, "anonid", "menu");
      </field>

      <field name="_menuDelay">600</field>

      <field name="_parentMenupopup"><![CDATA[
        this._getParentMenupopup(this);
      ]]></field>

      <method name="_getParentMenupopup">
        <parameter name="aNode"/>
        <body><![CDATA[
          let node = aNode.parentNode;
          while (node) {
            if (node.localName == "menupopup")
              break;
            node = node.parentNode;
          }
          return node;
        ]]></body>
      </method>

      <method name="handleEvent">
        <parameter name="event"/>
        <body><![CDATA[
          switch (event.type) {
            case "DOMMenuItemActive":
              if (this.getAttribute("active") == "true" &&
                  event.target != this &&
                  this._getParentMenupopup(event.target) == this._parentMenupopup)
                this.removeAttribute("active");
              break;
            case "popuphidden":
              if (event.target == this._parentMenupopup)
                this.removeAttribute("active");
              break;
          }
        ]]></body>
      </method>
    </implementation>

    <handlers>
      <handler event="mouseover"><![CDATA[
        if (this.getAttribute("active") != "true") {
          this.setAttribute("active", "true");

          this.dispatchEvent(new Event("DOMMenuItemActive",
                                       { bubbles: true, cancelable: false }));

          if (this.getAttribute("disabled") != "true") {
            let self = this;
            setTimeout(function() {
              if (self.getAttribute("active") == "true")
                self.menu.open = true;
            }, this._menuDelay);
          }
        }
      ]]></handler>

      <handler event="popupshowing"><![CDATA[
        if (event.target == this.firstChild &&
            this._parentMenupopup._currentPopup)
          this._parentMenupopup._currentPopup.hidePopup();
      ]]></handler>

      <handler event="click" phase="capturing"><![CDATA[
        if (this.getAttribute("disabled") == "true") {
          // Prevent the command from being carried out
          event.stopPropagation();
          return;
        }

        let node = event.originalTarget;
        while (true) {
          if (node == this.menuitem)
            break;
          if (node == this)
            return;
          node = node.parentNode;
        }

        this._parentMenupopup.hidePopup();
      ]]></handler>
    </handlers>
  </binding>

  <binding id="appmenu-vertical" extends="chrome://messenger/content/generalBindings.xml#menu-vertical">
    <implementation>
      <method name="_setupAppmenu">
        <parameter name="event"/>
        <body><![CDATA[
          if (event.target == this) {
              let appmenuPopup = document.getElementById("appmenu-popup");
              if (this.lastChild != appmenuPopup) {
                this.appendChild(appmenuPopup);
              }
          }
        ]]></body>
      </method>
    </implementation>
    <handlers>
      <!-- While it would seem we could do this by handling oncommand, we can't
           because any external oncommand handlers might get called before ours,
           and then they would see the incorrect value of checked. Additionally
           a command attribute would redirect the command events anyway.
           Also, the appmenu-popup needs to be appended to the target 'Hamburger
           button' dynamically at every button click (as opposed to appended
           once in the binding's constructor) otherwise only one of the four
           Hamburger buttons (on the Mail, Calendar, Tasks and Chat tabs) will
           get the popup menu (namely, Mail). See Bug 890332. -->
      <handler event="mousedown" button="0" action="this._setupAppmenu(event);"/>
      <handler event="keypress" key=" " action="this._setupAppmenu(event);"/>
    </handlers>
  </binding>

  <binding id="menulist-description" display="xul:menu"
           extends="chrome://global/content/bindings/menulist.xml#menulist">
    <content sizetopopup="pref">
      <xul:hbox class="menulist-label-box" flex="1">
        <xul:image class="menulist-icon" xbl:inherits="src=image,src"/>
        <xul:label class="menulist-label" xbl:inherits="value=label,crop,accesskey" crop="right" flex="1"/>
        <xul:label class="menulist-label menulist-description" xbl:inherits="value=description" crop="right" flex="10000"/>
      </xul:hbox>
      <xul:dropmarker class="menulist-dropmarker" type="menu" xbl:inherits="disabled,open"/>
      <children includes="menupopup"/>
    </content>
  </binding>

  <binding id="menuitem-iconic-desc-noaccel" extends="chrome://global/content/bindings/menu.xml#menuitem">
    <content>
      <xul:hbox class="menu-iconic-left" align="center" pack="center"
                xbl:inherits="selected,disabled,checked">
        <xul:image class="menu-iconic-icon" xbl:inherits="src=image,validate,src"/>
      </xul:hbox>
      <xul:label class="menu-iconic-text" xbl:inherits="value=label,accesskey,crop" crop="right" flex="1"/>
      <xul:label class="menu-iconic-text menu-description" xbl:inherits="value=description" crop="right" flex="10000"/>
    </content>
  </binding>
</bindings>