calendar/base/content/widgets/calendar-list-tree.xml
author Geoff Lankow <geoff@darktrojan.net>
Mon, 14 Jan 2019 15:20:56 +1300
changeset 33340 582cf461149a
parent 32021 7973e4623106
child 33346 4ae0c5713dfe
permissions -rw-r--r--
Bug 1519755 - Fix more XBL binding errors at Lightning startup. r=philipp

<?xml version="1.0" encoding="UTF-8"?>
<!-- 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://calendar/locale/calendar.dtd">

<bindings id="calendar-list-tree-bindings"
          xmlns="http://www.mozilla.org/xbl"
          xmlns:xbl="http://www.mozilla.org/xbl"
          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <binding id="full-calendar-list-tree" extends="#calendar-list-tree">
    <!--
      - This binding implements a full calendar list, that automatically adds
      - and removes calendars when a calendar is registered or unregistered.
      -->
    <implementation>
      <constructor><![CDATA[
          let calMgr = cal.getCalendarManager();
          calMgr.addObserver(this.calMgrObserver);

          this.dispatchEvent(new CustomEvent("bindingattached", { bubbles: false }));
      ]]></constructor>
      <destructor><![CDATA[
          let calMgr = cal.getCalendarManager();
          calMgr.removeObserver(this.calMgrObserver);
          this.calMgrObserver.listTree = null;
      ]]></destructor>

      <field name="mAddingFromComposite">false</field>

      <property name="compositeCalendar">
        <getter><![CDATA[
            if (!this.mCompositeCalendar) {
                throw Components.Exception("Calendar list has no composite calendar yet",
                                           Components.results.NS_ERROR_NOT_INITIALIZED);
            }
            return this.mCompositeCalendar;
        ]]></getter>
        <setter><![CDATA[
            this.mCompositeCalendar = val;
            this.mCompositeCalendar.addObserver(this.compositeObserver);

            // Now that we have a composite calendar, we can get all calendars
            // from the calendar manager.
            this.mAddingFromComposite = true;
            let calendars = sortCalendarArray(cal.getCalendarManager().getCalendars({}));
            calendars.forEach(this.addCalendar, this);
            this.mAddingFromComposite = false;

            return val;
        ]]></setter>
      </property>

      <property name="calendars">
        <getter><![CDATA[
            return this.mCalendarList;
        ]]></getter>
        <setter><![CDATA[
            // Setting calendars externally is not wanted. This is done internally
            // in the compositeCalendar setter.
            throw Components.Exception("Seting calendars on type='full' is not supported",
                                       Components.results.NS_ERROR_NOT_IMPLEMENTED);
        ]]></setter>
      </property>

      <field name="calMgrObserver"><![CDATA[
        ({
            listTree: this,
            QueryInterface: ChromeUtils.generateQI([Ci.calICalendarManagerObserver]),

            // calICalendarManagerObserver
            onCalendarRegistered: function(aCalendar) {
                this.listTree.addCalendar(aCalendar);
                let composite = this.listTree.compositeCalendar;
                let inComposite = aCalendar.getProperty(composite.prefPrefix +
                                                        "-in-composite");
                if ((inComposite === null) || inComposite) {
                    composite.addCalendar(aCalendar);
                }
            },

            onCalendarUnregistering: function(aCalendar) {
                this.listTree.removeCalendar(aCalendar);
            },

            onCalendarDeleting: function(aCalendar) {
                // Now that the calendar is unregistered, update the commands to
                // make sure that New Event/Task commands are correctly
                // enabled/disabled.
                document.commandDispatcher.updateCommands("calendar_commands");
            }
        })
      ]]></field>
      <field name="compositeObserver"><![CDATA[
        ({
            listTree: this,
            QueryInterface: cal.generateQI([
                Ci.calICompositeObserver,
                Ci.calIObserver
            ]),

            // calICompositeObserver
            onCalendarAdded: function(aCalendar) {
                // Make sure the checkbox state is updated
                this.listTree.updateCalendar(aCalendar);
            },

            onCalendarRemoved: function(aCalendar) {
                // Make sure the checkbox state is updated
                this.listTree.updateCalendar(aCalendar);
            },

            onDefaultCalendarChanged: function(aCalendar) {
            },

            // calIObserver
            onStartBatch: function() { },
            onEndBatch: function() { },
            onLoad: function() { },

            onAddItem: function(aItem) {
                if (aItem.calendar.type != "caldav") {
                    this.listTree.ensureCalendarVisible(aItem.calendar);
                }
            },
            onModifyItem: function(aNewItem, aOldItem) {
                if (aNewItem.calendar.type != "caldav") {
                    this.listTree.ensureCalendarVisible(aNewItem.calendar);
                }
            },
            onDeleteItem: function(aDeletedItem) { },
            onError: function(aCalendar, aErrNo, aMessage) { },

            onPropertyChanged: function(aCalendar, aName, aValue, aOldValue) {
                switch (aName) {
                    case "disabled":
                    case "readOnly":
                        calendarUpdateNewItemsCommand();
                        document.commandDispatcher.updateCommands("calendar_commands");
                        break;
                }
            },

            onPropertyDeleting: function(aCalendar, aName) {
            }
        })
      ]]></field>
    </implementation>
    <handlers>
      <handler event="dblclick"><![CDATA[
          let col = {};
          let calendar = this.getCalendarFromEvent(event, col);
          if (event.button != 0 ||
              (col.value && col.value.element &&
               col.value.element.getAttribute("anonid") == "checkbox-treecol")) {
              // Only left clicks that are not on the checkbox column
              return;
          }
          if (calendar) {
              cal.window.openCalendarProperties(window, calendar);
          } else {
              cal.window.openCalendarWizard(window);
          }
      ]]></handler>
    </handlers>
  </binding>

  <binding id="calendar-list-tree">
    <content>
      <xul:tree anonid="tree"
                xbl:inherits="hidecolumnpicker"
                hidecolumnpicker="true"
                seltype="single"
                flex="1">
        <xul:treecols anonid="treecols"
                      xbl:inherits="hideheader"
                      hideheader="true">
          <xul:treecol anonid="checkbox-treecol"
                       xbl:inherits="cycler,hideheader"
                       cycler="true"
                       hideheader="true"
                       width="17"/>
          <xul:treecol anonid="color-treecol"
                       xbl:inherits="cycler,hideheader"
                       hideheader="true"
                       width="16"/>
          <xul:treecol anonid="calendarname-treecol"
                       xbl:inherits="cycler,hideheader"
                       hideheader="true"
                       label="&calendar.unifinder.tree.calendarname.label;"
                       flex="1"/>
          <xul:treecol anonid="status-treecol"
                       xbl:inherits="cycler,hideheader"
                       hideheader="true"
                       width="18"/>
          <children includes="treecol"/>
          <xul:treecol anonid="scrollbar-spacer"
                       xbl:inherits="cycler,hideheader"
                       fixed="true"
                       hideheader="true">
            <!-- This is a very elegant workaround to make sure the last column
                 is not covered by the scrollbar in case of an overflow. This
                 treecol needs to be here last -->
            <xul:slider anonid="scrollbar-slider" orient="vertical"/>
          </xul:treecol>
        </xul:treecols>
        <xul:treechildren anonid="treechildren"
                          xbl:inherits="tooltip=childtooltip,context=childcontext"
                          tooltip="_child"
                          context="_child"
                          ondragstart="onDragStart(event);"
                          onoverflow="displayScrollbarSpacer(true)"
                          onunderflow="displayScrollbarSpacer(false)">
          <children includes="tooltip|menupopup"/>
        </xul:treechildren>
      </xul:tree>
    </content>
    <implementation implements="nsITreeView">

      <field name="mCalendarList">[]</field>
      <field name="mCompositeCalendar">null</field>
      <field name="tree">null</field>
      <field name="treebox">null</field>
      <field name="ruleCache">null</field>
      <field name="mCachedSheet">null</field>

      <field name="mCycleCalendarFlag">null</field>
      <field name="mCycleTimer">null</field>
      <field name="cycleDebounce">200</field>

      <constructor><![CDATA[
          this.tree.view = this;
          this.ruleCache = {};
          this.mCycleCalendarFlag = {};
      ]]></constructor>
      <destructor><![CDATA[
          // Clean up the calendar manager observers. Do not use removeCalendar
          // here since that will remove the calendar from the composite calendar.
          for (let calendar of this.mCalendarList) {
              calendar.removeObserver(this.calObserver);
          }

          this.tree.view = null;
          this.calObserver.listTree = null;

          if (this.mCompositeCalendar) {
              this.mCompositeCalendar.removeObserver(this.compositeObserver);
          }
      ]]></destructor>

      <field name="calObserver"><![CDATA[
        ({
            listTree: this,
            QueryInterface: ChromeUtils.generateQI([Ci.calIObserver]),

            // calIObserver. Note that each registered calendar uses this observer
            onStartBatch: function() { },
            onEndBatch: function() { },
            onLoad: function() { },

            onAddItem: function(aItem) { },
            onModifyItem: function(aNewItem, aOldItem) { },
            onDeleteItem: function(aDeletedItem) { },
            onError: function(aCalendar, aErrNo, aMessage) { },

            onPropertyChanged: function(aCalendar, aName, aValue, aOldValue) {
                switch (aName) {
                    case "color":
                        // TODO See other TODO in this file about updateStyleSheetForViews
                        if ("updateStyleSheetForViews" in window) {
                            updateStyleSheetForViews(aCalendar);
                        }
                        this.listTree.updateCalendarColor(aCalendar);
                        // Fall through, update item in any case
                    case "name":
                    case "currentStatus":
                    case "readOnly":
                    case "disabled":
                        this.listTree.updateCalendar(aCalendar);
                        // Fall through, update commands in any cases.
                }
            },

            onPropertyDeleting: function(aCalendar, aName) {
                // Since the old value is not used directly in onPropertyChanged,
                // but should not be the same as the value, set it to a different
                // value.
                this.onPropertyChanged(aCalendar, aName, null, null);
            }
        })
      ]]></field>

      <field name="compositeObserver"><![CDATA[
        ({
            listTree: this,
            QueryInterface: ChromeUtils.generateQI([Ci.calICompositeObserver]),

            // calICompositeObserver
            onCalendarAdded: function(aCalendar) {
                // Make sure the checkbox state is updated
                this.listTree.updateCalendar(aCalendar);
            },

            onCalendarRemoved: function(aCalendar) {
                // Make sure the checkbox state is updated
                this.listTree.updateCalendar(aCalendar);
            },

            onDefaultCalendarChanged: function(aCalendar) {
            }
        })
      ]]></field>

      <property name="treechildren"
                readonly="true"
                onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'treechildren')"/>
      <property name="tree"
                readonly="true"
                onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'tree')"/>


      <property name="sheet" readonly="true">
        <getter><![CDATA[
            if (!this.mCachedSheet) {
                for (let sheet of document.styleSheets) {
                    if (sheet.href == "chrome://calendar/skin/calendar-management.css") {
                        this.mCachedSheet = sheet;
                        break;
                    }
                }
                if (!this.mCachedSheet) {
                    cal.ERROR("Could not find calendar-management.css, needs to be added to " +
                              window.document.title + "'s stylesheets");
                }
            }

            return this.mCachedSheet;
        ]]></getter>
      </property>

      <property name="calendars">
        <getter><![CDATA[
            return this.mCalendarList;
        ]]></getter>
        <setter><![CDATA[
            this.treebox.beginUpdateBatch();
            try {
                this.clear();
                this.mCalendarList = [];
                for (let calendar of val) {
                    this.addCalendar(calendar);
                }
                return this.mCalendarList;
            } finally {
                this.treebox.endUpdateBatch();
            }
        ]]></setter>
      </property>

      <property name="compositeCalendar">
        <getter><![CDATA[
            if (!this.mCompositeCalendar) {
                this.mCompositeCalendar =
                    Components.classes["@mozilla.org/calendar/calendar;1?type=composite"]
                              .createInstance(Components.interfaces.calICompositeCalendar);
            }

            return this.mCompositeCalendar;
        ]]></getter>
        <setter><![CDATA[
            if (this.mCompositeCalendar) {
                throw Components.Exception("A composite calendar has already been set",
                                           Components.results.NS_ERROR_ALREADY_INITIALIZED);
            }
            this.mCompositeCalendar = val;
            this.mCompositeCalendar.addObserver(this.compositeObserver);
            return val;
        ]]></setter>
      </property>

      <property name="sortOrder"
                readonly="true"
                onget="return this.mCalendarList.map(x => x.id);"/>
      <property name="selectedCalendars"
                readonly="true"
                onget="return this.compositeCalendar.getCalendars({});"/>
      <property name="allowDrag"
                onget="return (this.getAttribute('allowdrag') == 'true');"
                onset="return setBooleanAttribute(this, 'allowdrag', val);"/>
      <property name="writable"
                onget="return (this.getAttribute('writable') == 'true');"
                onset="return setBooleanAttribute(this, 'writable', val);"/>
      <property name="disabledState"
                onget="return this.getAttribute('disabledstate') || 'disabled';"
                onset="return this.setAttribute('disabledstate', val);"/>

      <method name="sortOrderChanged">
        <parameter name=""/>
        <body><![CDATA[
            if (this.mAddingFromComposite) {
                return;
            }
            let event = document.createEvent("Events");
            event.initEvent("SortOrderChanged", true, false);
            event.sortOrder = this.sortOrder;
            this.dispatchEvent(event);

            let handler = this.getAttribute("onSortOrderChanged");
            if (handler) {
                // Call the given code in a function
                let func = new Function("event", handler);
                func(event);
            }
        ]]></body>
      </method>
      <method name="displayScrollbarSpacer">
        <parameter name="aShouldDisplay"/>
        <body><![CDATA[
            let spacer = document.getAnonymousElementByAttribute(this, "anonid", "scrollbar-spacer");
            spacer.collapsed = !aShouldDisplay;
        ]]></body>
      </method>

      <method name="ensureCalendarVisible">
        <parameter name="aCalendar"/>
        <body><![CDATA[
            this.compositeCalendar.addCalendar(aCalendar);
        ]]></body>
      </method>

      <method name="getColumn">
        <parameter name="aAnonId"/>
        <body><![CDATA[
            let colElem = document.getAnonymousElementByAttribute(this, "anonid", aAnonId);
            return this.treebox.columns.getColumnFor(colElem);
        ]]></body>
      </method>

      <method name="findIndexById">
        <!--
          - Find the array index of the calendar with the passed id.
          -
          - @param aId           The calendar id to find an index for.
          - @return              The array index, or -1 if not found.
          -->
        <parameter name="aId"/>
        <body><![CDATA[
            for (let i = 0; i < this.mCalendarList.length; i++) {
                if (this.mCalendarList[i].id == aId) {
                    return i;
                }
            }
            return -1;
        ]]></body>
      </method>

      <method name="selectCalendarById">
        <parameter name="aId"/>
        <body><![CDATA[
            let index = this.findIndexById(aId);
            this.tree.view.selection.select(index);
        ]]></body>
      </method>

      <method name="addCalendar">
        <!--
          - Add a calendar to the calendar list
          -
          - @param aCalendar     The calendar to add.
          -->
        <parameter name="aCalendar"/>
        <body><![CDATA[
            let composite = this.compositeCalendar;

            let initialSortOrderPos = aCalendar.getProperty("initialSortOrderPos");
            if (initialSortOrderPos != null && initialSortOrderPos < this.mCalendarList.length) {
                // Insert the calendar at the requested sort order position
                // and then discard the property
                this.mCalendarList.splice(initialSortOrderPos, 0, aCalendar);
                aCalendar.deleteProperty("initialSortOrderPos");
            } else {
                this.mCalendarList.push(aCalendar);
            }
            this.treebox.rowCountChanged(this.mCalendarList.length - 1, 1);

            if (!composite.defaultCalendar ||
                aCalendar.id == composite.defaultCalendar.id) {
                this.tree.view.selection.select(this.mCalendarList.length - 1);
            }

            this.updateCalendarColor(aCalendar);

            // TODO This should be done only once outside of this binding, but to
            // do that right, we need to have an easy way to register an observer
            // all calendar properties. This could be the calendar manager that
            // holds an observer on every calendar anyway, which would then use the
            // global observer service which clients can register with.
            if ("updateStyleSheetForViews" in window) {
                updateStyleSheetForViews(aCalendar);
            }

            // Watch the calendar for changes, i.e color.
            aCalendar.addObserver(this.calObserver);

            // Adding a calendar causes the sortorder to be changed.
            this.sortOrderChanged();

            // Re-assign defaultCalendar, sometimes it is not the right one after
            // remove & add calendar.
            if (composite.defaultCalendar && this.tree.currentIndex > -1) {
                let currentCal = this.getCalendar(this.tree.currentIndex);
                if (composite.defaultCalendar.id != currentCal.id) {
                    composite.defaultCalendar = currentCal;
                }
            }
        ]]></body>
      </method>

      <method name="removeCalendar">
        <!--
          - Remove a calendar from the calendar list
          -
          - @param aCalendar     The calendar to remove.
          -->
        <parameter name="aCalendar"/>
        <body><![CDATA[
            let index = this.findIndexById(aCalendar.id);
            if (index < 0) {
                return;
            }

            this.mCalendarList.splice(index, 1);
            this.treebox.rowCountChanged(index, -1);

            if (index == this.rowCount) {
                index--;
            }

            this.tree.view.selection.select(index + 1);

            aCalendar.removeObserver(this.calObserver);

            // Make sure the calendar is removed from the composite calendar
            this.compositeCalendar.removeCalendar(aCalendar);

            // Remove the css style rule from the sheet.
            let sheet = this.sheet;
            for (let i = 0; i < sheet.cssRules.length; i++) {
                if (sheet.cssRules[i] == this.ruleCache[aCalendar.id][0] ||
                    sheet.cssRules[i] == this.ruleCache[aCalendar.id][1]) {
                    sheet.deleteRule(i);
                }
            }
            delete this.ruleCache[aCalendar.id];

            this.sortOrderChanged();
        ]]></body>
      </method>

      <method name="clear">
        <body><![CDATA[
            this.treebox.beginUpdateBatch();
            try {
                this.mCalendarList.forEach(this.removeCalendar, this);
            } finally {
                this.treebox.endUpdateBatch();
            }
        ]]></body>
      </method>

      <method name="updateCalendar">
        <!--
          - Update a calendar's tree row (to refresh the color and such)
          -
          - @param aCalendar     The calendar to update.
          -->
        <parameter name="aCalendar"/>
        <body><![CDATA[
            this.treebox.invalidateRow(this.findIndexById(aCalendar.id));
        ]]></body>
      </method>

      <method name="updateCalendarColor">
        <!--
          - Update a calendar's color rules.
          -
          - @param aCalendar     The calendar to update.
          -->
        <parameter name="aCalendar"/>
        <body><![CDATA[
            let color = aCalendar.getProperty("color") || "#a8c2e1";
            let sheet = this.sheet;
            if (!(aCalendar.id in this.ruleCache)) {
                let ruleString = "calendar-list-tree > tree > treechildren" +
                                 "::-moz-tree-cell(color-treecol, id-" +
                                 aCalendar.id + ") {}";

                let disabledRuleString = "calendar-list-tree > tree > treechildren" +
                                         "::-moz-tree-cell(color-treecol, id-" +
                                         aCalendar.id + ", disabled) {}";

                try {
                    let ruleIndex = sheet.insertRule(ruleString, sheet.cssRules.length);
                    let disabledIndex = sheet.insertRule(disabledRuleString, sheet.cssRules.length);
                    this.ruleCache[aCalendar.id] = [sheet.cssRules[ruleIndex], sheet.cssRules[disabledIndex]];
                } catch (ex) {
                    sheet.ownerNode.addEventListener("load",
                                                     () => this.updateCalendarColor(aCalendar),
                                                     { once: true });
                    return;
                }
            }

            let [enabledRule, disabledRule] = this.ruleCache[aCalendar.id];
            enabledRule.style.backgroundColor = color;

            let colorMatch = color.match(/#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})?/);
            if (colorMatch && this.disabledState == "disabled") {
                let gray = (
                    0.2126 * parseInt(colorMatch[1], 16) +
                    0.7152 * parseInt(colorMatch[2], 16) +
                    0.0722 * parseInt(colorMatch[3], 16)
                );
                let alpha = colorMatch[4] ? parseInt(colorMatch[4], 16) : 255;
                disabledRule.style.backgroundColor = `rgba(${gray}, ${gray}, ${gray}, ${alpha})`;
            } else {
                disabledRule.style.backgroundColor = color;
            }
        ]]></body>
      </method>

      <method name="getCalendarFromEvent">
        <!--
          - Get the calendar from the given DOM event. This can be a Mouse event or a
          - keyboard event.
          -
          - @param event     The DOM event to check
          - @param aCol      An out-object for the column id.
          - @param aRow      An out-object for the row index.
          -->
        <parameter name="event"/>
        <parameter name="aCol"/>
        <parameter name="aRow"/>
        <body><![CDATA[
            if (event.clientX && event.clientY) {
                // If we have a client point, get the row directly from the client
                // point.
                aRow = aRow || {};
                this.treebox.getCellAt(event.clientX,
                                       event.clientY,
                                       aRow,
                                       aCol || {},
                                       {});
            } else if (document.popupNode && document.popupNode.contextCalendar) {
                // Otherwise, we can try to get the context calendar from the popupNode.
                return document.popupNode.contextCalendar;
            }
            return aRow && aRow.value > -1 && this.mCalendarList[aRow.value];
        ]]></body>
      </method>

      <method name="getCalendar">
        <!--
          - Get the calendar from a certain index.
          -
          - @param aIndex     The index to get the calendar for.
          -->
        <parameter name="aIndex"/>
        <body><![CDATA[
            let index = Math.max(0, Math.min(this.mCalendarList.length - 1, aIndex));
            return this.mCalendarList[index];
        ]]></body>
      </method>

      <!-- Implement nsITreeView -->
      <property name="rowCount"
                readonly="true"
                onget="return this.mCalendarList.length"/>

      <method name="getCellProperties">
        <parameter name="aRow"/>
        <parameter name="aCol"/>
        <body><![CDATA[
            try {
                let rowProps = this.getRowProperties(aRow);
                let colProps = this.getColumnProperties(aCol);
                return rowProps + (rowProps && colProps ? " " : "") + colProps;
            } catch (e) {
                // It seems errors in these functions are not shown, do this
                // explicitly.
                cal.ERROR("Error getting cell props: " + e);
                return "";
            }
        ]]></body>
      </method>

      <method name="getRowProperties">
        <parameter name="aRow"/>
        <body><![CDATA[
            let properties = [];
            let calendar = this.getCalendar(aRow);
            let composite = this.compositeCalendar;

            // Set up the calendar id
            properties.push("id-" + calendar.id);

            // Get the calendar color
            let color = (calendar.getProperty("color") || "").substr(1);

            // Set up the calendar color (background)
            properties.push("color-" + (color || "default"));

            // Set a property to get the contrasting text color (foreground)
            properties.push(cal.view.getContrastingTextColor(color || "a8c2e1"));

            let currentStatus = calendar.getProperty("currentStatus");
            if (!Components.isSuccessCode(currentStatus)) {
                // 'readfailed' is supposed to "win" over 'readonly', meaning that
                // if reading from a calendar fails there is no further need to also display
                // information about 'readonly' status
                properties.push("readfailed");
            } else if (calendar.readOnly) {
                properties.push("readonly");
            }

            // Set up the composite calendar status and disabled state
            let checkedState = composite.getCalendarById(calendar.id) ? "checked" : "unchecked";
            let isDisabled = calendar.getProperty("disabled");
            let disabledState = isDisabled ? "disabled" : "enabled";

            if (isDisabled && this.disabledState == "ignore") {
                disabledState = "enabled";
            } else if (isDisabled && this.disabledState == "checked") {
                checkedState = "checked";
            } else if (isDisabled) {
                checkedState = "unchecked";
            }

            properties.push(disabledState, checkedState);

            return properties.join(" ");
        ]]></body>
      </method>

      <method name="getColumnProperties">
        <parameter name="aCol"/>
        <body><![CDATA[
            // Workaround for anonymous treecols
            return aCol.element.getAttribute("anonid");
        ]]></body>
      </method>

      <method name="isContainer">
        <parameter name="aRow"/>
        <body><![CDATA[
            return false;
        ]]></body>
      </method>

      <method name="isContainerOpen">
        <parameter name="aRow"/>
        <body><![CDATA[
            return false;
        ]]></body>
      </method>

      <method name="isContainerEmpty">
        <parameter name="aRow"/>
        <body><![CDATA[
            return false;
        ]]></body>
      </method>

      <method name="isSeparator">
        <parameter name="aRow"/>
        <body><![CDATA[
            return false;
        ]]></body>
      </method>

      <method name="isSorted">
        <parameter name="aRow"/>
        <body><![CDATA[
            return false;
        ]]></body>
      </method>

      <method name="onDragStart">
        <!--
          - Initiate a drag operation for the calendar list. Can be used in the
          - dragstart handler.
          -
          - @param event     The DOM event containing drag information.
          -->
        <parameter name="event"/>
        <body><![CDATA[
            let calendar = this.getCalendarFromEvent(event);
            if (this.allowDrag && event.dataTransfer) {
                // Setting data starts a drag session, do this only if dragging
                // is enabled for this binding.
                event.dataTransfer.setData("application/x-moz-calendarID", calendar.id);
                event.dataTransfer.effectAllowed = "move";
            }
        ]]></body>
      </method>

      <method name="canDrop">
        <parameter name="aRow"/>
        <parameter name="aOrientation"/>
        <body><![CDATA[
            let dragSession = cal.getDragService().getCurrentSession();
            let dataTransfer = dragSession && dragSession.dataTransfer;
            if (!this.allowDrag || !dataTransfer) {
                // If dragging is not allowed or there is no data transfer then
                // we can't drop (i.e dropping a file on the calendar list).
                return false;
            }

            let dragCalId = dataTransfer.getData("application/x-moz-calendarID");

            return (aOrientation != Components.interfaces.nsITreeView.DROP_ON &&
                    dragCalId != null);
        ]]></body>
      </method>

      <method name="drop">
        <parameter name="aRow"/>
        <parameter name="aOrientation"/>
        <body><![CDATA[
            let dragSession = cal.getDragService().getCurrentSession();
            let dataTransfer = dragSession.dataTransfer;
            let dragCalId = dataTransfer &&
                            dataTransfer.getData("application/x-moz-calendarID");
            if (!this.allowDrag || !dataTransfer || !dragCalId) {
                return false;
            }

            let oldIndex = -1;
            for (let i = 0; i < this.mCalendarList.length; i++) {
                if (this.mCalendarList[i].id == dragCalId) {
                    oldIndex = i;
                    break;
                }
            }
            if (oldIndex < 0) {
                return false;
            }

            // If no row is specified (-1), then assume append.
            let row = (aRow < 0 ? this.mCalendarList.length - 1 : aRow);
            let targetIndex = row + Math.max(0, aOrientation);

            // We don't need to move if the target row has the same index as the old
            // row. The same goes for dropping after the row before the old row or
            // before the row after the old row. Think about it :-)
            if (aRow != oldIndex && row + aOrientation != oldIndex) {
                // Add the new one, remove the old one.
                this.mCalendarList.splice(targetIndex, 0, this.mCalendarList[oldIndex]);
                this.mCalendarList.splice(oldIndex + (oldIndex > targetIndex ? 1 : 0), 1);

                // Invalidate the tree rows between the old item and the new one.
                if (oldIndex < targetIndex) {
                    this.treebox.invalidateRange(oldIndex, targetIndex);
                } else {
                    this.treebox.invalidateRange(targetIndex, oldIndex);
                }

                // Fire event
                this.sortOrderChanged();
            }
            return true;
        ]]></body>
      </method>

      <method name="foreignDrop">
        <!--
          - This function can be used by other nodes to simulate dropping on the
          - tree. This can be used for example on the tree header so that the row
          - will be inserted before the first visible row. The event client
          - coordinate are used to determine if the row should be dropped before the
          - first row (above treechildren) or below the last visible row (below top
          - of treechildren).
          -
          - @param event     The DOM drop event.
          - @return          Boolean indicating if the drop succeeded.
          -
          -->
        <parameter name="event"/>
        <body><![CDATA[
            let hasDropped;
            if (event.clientY < this.tree.boxObject.y) {
                hasDropped = this.drop(this.treebox.getFirstVisibleRow(), -1);
            } else {
                hasDropped = this.drop(this.treebox.getLastVisibleRow(), 1);
            }
            if (hasDropped) {
                event.preventDefault();
            }
            return hasDropped;
        ]]></body>
      </method>

      <method name="foreignCanDrop">
        <!--
          - Similar function to foreignCanDrop but for the dragenter event
          - @see ::foreignDrop
          -->
        <parameter name="event"/>
        <body><![CDATA[
            // The dragenter/dragover events expect false to be returned when
            // dropping is allowed, therefore we return !canDrop.
            if (event.clientY < this.tree.boxObject.y) {
                return !this.canDrop(this.treebox.getFirstVisibleRow(), -1);
            } else {
                return !this.canDrop(this.treebox.getLastVisibleRow(), 1);
            }
        ]]></body>
      </method>

      <method name="getParentIndex">
        <parameter name="aRow"/>
        <body><![CDATA[
            return -1;
        ]]></body>
      </method>

      <method name="hasNextSibling">
        <parameter name="aRow"/>
        <parameter name="aAfterIndex"/>
        <body><![CDATA[
        ]]></body>
      </method>

      <method name="getLevel">
        <parameter name="aRow"/>
        <body><![CDATA[
            return 0;
        ]]></body>
      </method>

      <method name="getImageSrc">
        <parameter name="aRow"/>
        <body><![CDATA[
        ]]></body>
      </method>

      <method name="getCellValue">
        <parameter name="aRow"/>
        <parameter name="aCol"/>
        <body><![CDATA[
            let calendar = this.getCalendar(aRow);
            let composite = this.compositeCalendar;

            switch (aCol.element.getAttribute("anonid")) {
                case "checkbox-treecol":
                    return composite.getCalendarById(calendar.id) ? "true" : "false";
                case "status-treecol":
                    // The value of this cell shows the calendar readonly state
                    return (calendar.readOnly ? "true" : "false");
            }
            return null;
        ]]></body>
      </method>

      <method name="getCellText">
        <parameter name="aRow"/>
        <parameter name="aCol"/>
        <body><![CDATA[
            switch (aCol.element.getAttribute("anonid")) {
                case "calendarname-treecol":
                    return this.getCalendar(aRow).name;
            }
            return "";
        ]]></body>
      </method>

      <method name="setTree">
        <parameter name="aTreeBox"/>
        <body><![CDATA[
            this.treebox = aTreeBox;
        ]]></body>
      </method>

      <method name="toggleOpenState">
        <parameter name="aRow"/>
        <body><![CDATA[
        ]]></body>
      </method>

      <method name="cycleHeader">
        <parameter name="aCol"/>
        <body><![CDATA[
        ]]></body>
      </method>

      <method name="cycleCell">
        <parameter name="aRow"/>
        <parameter name="aCol"/>
        <body><![CDATA[
            let calendar = this.getCalendar(aRow);
            if (this.disabledState != "ignore" && calendar.getProperty("disabled")) {
                return;
            }

            if (this.mCycleCalendarFlag[calendar.id]) {
                delete this.mCycleCalendarFlag[calendar.id];
            } else {
                this.mCycleCalendarFlag[calendar.id] = [calendar, aRow];
            }

            if (this.cycleDebounce) {
                if (this.mCycleTimer) {
                    clearTimeout(this.mCycleTimer);
                }
                this.mCycleTimer = setTimeout(this.cycleCellCommit.bind(this), 200);
            } else {
                this.cycleCellCommit();
            }
        ]]></body>
      </method>

      <method name="cycleCellCommit">
        <body><![CDATA[
            let composite = this.compositeCalendar;
            this.treebox.beginUpdateBatch();
            composite.startBatch();
            try {
                for (let [id, [calendar, row]] of Object.entries(this.mCycleCalendarFlag)) {
                    if (composite.getCalendarById(id)) {
                        composite.removeCalendar(calendar);
                    } else {
                        composite.addCalendar(calendar);
                    }
                    this.treebox.invalidateRow(row);
                }
                this.mCycleCalendarFlag = {};
            } finally {
                composite.endBatch();
                this.treebox.endUpdateBatch();
            }
        ]]></body>
      </method>

      <method name="isEditable">
        <parameter name="aRow"/>
        <parameter name="aCol"/>
        <body><![CDATA[
            return false;
        ]]></body>
      </method>

      <method name="setCellValue">
        <parameter name="aRow"/>
        <parameter name="aCol"/>
        <parameter name="aValue"/>
        <body><![CDATA[
            let calendar = this.getCalendar(aRow);
            let composite = this.compositeCalendar;

            switch (aCol.element.getAttribute("anonid")) {
                case "checkbox-treecol":
                    if (aValue == "true") {
                        composite.addCalendar(calendar);
                    } else {
                        composite.removeCalendar(calendar);
                    }
                    break;
                default:
                    return null;
            }
            return aValue;
        ]]></body>
      </method>

      <method name="setCellText">
        <parameter name="aRow"/>
        <parameter name="aCol"/>
        <parameter name="aValue"/>
        <body><![CDATA[
        ]]></body>
      </method>

      <method name="performAction">
        <parameter name="aAction"/>
        <body><![CDATA[
        ]]></body>
      </method>

      <method name="performActionOnRow">
        <parameter name="aAction"/>
        <parameter name="aRow"/>
        <body><![CDATA[
        ]]></body>
      </method>

      <method name="performActionOnCell">
        <parameter name="aAction"/>
        <parameter name="aRow"/>
        <parameter name="aCol"/>
        <body><![CDATA[
        ]]></body>
      </method>
    </implementation>
    <handlers>
      <handler event="select"><![CDATA[
          this.compositeCalendar.defaultCalendar = this.getCalendar(this.tree.currentIndex);
      ]]></handler>

      <handler event="keypress" keycode="VK_DELETE"><![CDATA[
          if (this.writable) {
              promptDeleteCalendar(this.compositeCalendar.defaultCalendar);
              event.preventDefault();
          }
      ]]></handler>

      <!-- use key=" " since keycode="VK_SPACE" doesn't work -->
      <handler event="keypress" key=" "><![CDATA[
          if (this.tree.currentIndex > -1) {
              this.cycleCell(this.tree.currentIndex, this.getColumn("checkbox-treecol"));
              this.treebox.invalidateRow(this.tree.currentIndex);
              event.preventDefault();
          }
      ]]></handler>

      <handler event="keypress" keycode="VK_DOWN" modifiers="control"><![CDATA[
          if (!this.allowDrag) {
              return;
          }

          let idx = this.tree.currentIndex;

          if (idx < this.mCalendarList.length - 1) {
              this.mCalendarList.splice(idx + 1, 0, this.mCalendarList.splice(idx, 1)[0]);
              this.treebox.invalidateRange(idx, idx + 1);

              if (this.tree.view.selection.isSelected(idx)) {
                  this.tree.view.selection.toggleSelect(idx);
                  this.tree.view.selection.toggleSelect(idx + 1);
              }
              if (this.tree.view.selection.currentIndex == idx) {
                  this.tree.view.selection.currentIndex = idx + 1;
              }

              // Fire event
              this.sortOrderChanged();
          }
          // Don't call the default <key> handler.
          event.preventDefault();
      ]]></handler>

      <handler event="keypress" keycode="VK_UP" modifiers="control"><![CDATA[
          if (!this.allowDrag) {
              return;
          }

          let idx = this.tree.currentIndex;
          if (idx > 0) {
              this.mCalendarList.splice(idx - 1, 0, this.mCalendarList.splice(idx, 1)[0]);
              this.treebox.invalidateRange(idx - 1, idx);

              if (this.tree.view.selection.isSelected(idx)) {
                  this.tree.view.selection.toggleSelect(idx);
                  this.tree.view.selection.toggleSelect(idx - 1);
              }
              if (this.tree.view.selection.currentIndex == idx) {
                  this.tree.view.selection.currentIndex = idx - 1;
              }

              // Fire event
              this.sortOrderChanged();
          }
          // Don't call the default <key> handler.
          event.preventDefault();
      ]]></handler>
    </handlers>
  </binding>
</bindings>