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

<!DOCTYPE dialog [
  <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/global.dtd" > %dtd1;
  <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd2;
]>

<bindings id="calendar-task-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="calendar-task-tree">
    <resources>
      <stylesheet src="chrome://calendar/skin/calendar-task-tree.css"/>
    </resources>
    <content>
      <xul:tree anonid="calendar-task-tree"
                class="calendar-task-tree"
                flex="1"
                enableColumnDrag="false"
                keepcurrentinview="true">
        <xul:treecols anonid="calendar-task-tree-cols">
          <xul:treecol anonid="calendar-task-tree-col-completed"
                       class="calendar-task-tree-col-completed"
                       minwidth="19"
                       fixed="true"
                       cycler="true"
                       sortKey="completedDate"
                       itemproperty="completed"
                       label="&calendar.unifinder.tree.done.label;"
                       tooltiptext="&calendar.unifinder.tree.done.tooltip;">
            <xul:image anonid="checkboximg" />
          </xul:treecol>
          <xul:splitter class="tree-splitter" ordinal="2"/>
          <xul:treecol anonid="calendar-task-tree-col-priority"
                       class="calendar-task-tree-col-priority"
                       minwidth="17"
                       fixed="true"
                       itemproperty="priority"
                       label="&calendar.unifinder.tree.priority.label;"
                       tooltiptext="&calendar.unifinder.tree.priority.tooltip;">
            <xul:image anonid="priorityimg"/>
          </xul:treecol>
          <xul:splitter class="tree-splitter" ordinal="4"/>
          <xul:treecol anonid="calendar-task-tree-col-title"
                       flex="1"
                       itemproperty="title"
                       label="&calendar.unifinder.tree.title.label;"
                       tooltiptext="&calendar.unifinder.tree.title.tooltip;"/>
          <xul:splitter class="tree-splitter" ordinal="6"/>
          <xul:treecol anonid="calendar-task-tree-col-entrydate"
                       itemproperty="entryDate"
                       flex="1"
                       label="&calendar.unifinder.tree.startdate.label;"
                       tooltiptext="&calendar.unifinder.tree.startdate.tooltip;"/>
          <xul:splitter class="tree-splitter" ordinal="8"/>
          <xul:treecol anonid="calendar-task-tree-col-duedate"
                       itemproperty="dueDate"
                       flex="1"
                       label="&calendar.unifinder.tree.duedate.label;"
                       tooltiptext="&calendar.unifinder.tree.duedate.tooltip;"/>
          <xul:splitter class="tree-splitter" ordinal="10"/>
          <xul:treecol anonid="calendar-task-tree-col-duration"
                       sortKey="dueDate"
                       itemproperty="duration"
                       flex="1"
                       label="&calendar.unifinder.tree.duration.label;"
                       tooltiptext="&calendar.unifinder.tree.duration.tooltip;"/>
          <xul:splitter class="tree-splitter" ordinal="12"/>
          <xul:treecol anonid="calendar-task-tree-col-completeddate"
                       itemproperty="completedDate"
                       flex="1"
                       label="&calendar.unifinder.tree.completeddate.label;"
                       tooltiptext="&calendar.unifinder.tree.completeddate.tooltip;"/>
          <xul:splitter class="tree-splitter" ordinal="14"/>
          <xul:treecol anonid="calendar-task-tree-col-percentcomplete"
                       flex="1"
                       type="progressmeter"
                       minwidth="19"
                       itemproperty="percentComplete"
                       label="&calendar.unifinder.tree.percentcomplete.label;"
                       tooltiptext="&calendar.unifinder.tree.percentcomplete.tooltip;"/>
          <xul:splitter class="tree-splitter" ordinal="16"/>
          <xul:treecol anonid="calendar-task-tree-col-categories"
                       itemproperty="categories"
                       flex="1"
                       label="&calendar.unifinder.tree.categories.label;"
                       tooltiptext="&calendar.unifinder.tree.categories.tooltip;"/>
          <xul:splitter class="tree-splitter" ordinal="18"/>
          <xul:treecol anonid="calendar-task-tree-col-location"
                       itemproperty="location"
                       label="&calendar.unifinder.tree.location.label;"
                       tooltiptext="&calendar.unifinder.tree.location.tooltip;"/>
          <xul:splitter class="tree-splitter" ordinal="20"/>
          <xul:treecol anonid="calendar-task-tree-col-status"
                       flex="1"
                       itemproperty="status"
                       label="&calendar.unifinder.tree.status.label;"
                       tooltiptext="&calendar.unifinder.tree.status.tooltip;"/>
          <xul:splitter class="tree-splitter" ordinal="22"/>
          <xul:treecol anonid="calendar-task-tree-col-calendarname"
                       flex="1"
                       itemproperty="calendar"
                       label="&calendar.unifinder.tree.calendarname.label;"
                       tooltiptext="&calendar.unifinder.tree.calendarname.tooltip;"/>
        </xul:treecols>
        <xul:treechildren tooltip="taskTreeTooltip"/>
      </xul:tree>
    </content>

    <implementation implements="nsIObserver">
      <constructor><![CDATA[
        Components.utils.import("resource://gre/modules/PluralForm.jsm");
        Components.utils.import("resource://gre/modules/Services.jsm");
        Components.utils.import("resource://calendar/modules/calItemUtils.jsm");
        Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
        let self = this;

        // set up the tree filter
        this.mFilter = new calFilter();

        // set up the custom tree view
        let tree = document.getAnonymousElementByAttribute(this, "anonid", "calendar-task-tree");
        this.mTreeView.tree = tree;
        tree.view = this.mTreeView;

        // set up our calendar event observer
        let composite = getCompositeCalendar();
        composite.addObserver(this.mTaskTreeObserver);

        // set up the preference observer
        let branch = Services.prefs.getBranch("");
        branch.addObserver("calendar.", this, false);


        // we want to make several attributes on the column
        // elements persistent, but unfortunately there's no
        // relyable way with the 'persist' feature.
        // that's why we need to store the necessary bits and
        // pieces at the element this binding is attached to.
        let names = this.getAttribute("visible-columns").split(' ');
        let ordinals = this.getAttribute("ordinals").split(' ');
        let widths = this.getAttribute("widths").split(' ');
        let sorted = this.getAttribute("sort-active");
        let sortDirection = this.getAttribute("sort-direction") || "ascending";
        let tree = document.getAnonymousNodes(this)[0];
        let treecols = tree.getElementsByTagNameNS(tree.namespaceURI, "treecol");
        for (let i = 0; i < treecols.length; i++) {
            let content = treecols[i].getAttribute("itemproperty");
            if (names.some(
                function(element) {
                    return (element == content);
                })) {
                treecols[i].removeAttribute("hidden");
            } else {
                treecols[i].setAttribute("hidden","true");
            }
            if (ordinals && ordinals.length > 0) {
               treecols[i].ordinal = Number(ordinals.shift());
            }
            if (widths && widths.length > 0) {
               treecols[i].width = Number(widths.shift());
            }
            if (sorted && sorted.length > 0) {
                if (sorted == content) {
                    this.mTreeView.sortDirection = sortDirection;
                    this.mTreeView.selectedColumn = treecols[i];
                }
            }
        }
      ]]></constructor>
      <destructor><![CDATA[
          Components.utils.import("resource://gre/modules/Services.jsm");

          // remove composite calendar observer
          let composite = getCompositeCalendar();
          composite.removeObserver(this.mTaskTreeObserver);

          // remove the preference observer
          let branch = Services.prefs.getBranch("");
          branch.removeObserver("calendar.", this, false);

          let widths = "";
          let ordinals = "";
          let visible = "";
          let sorted = this.mTreeView.selectedColumn;
          let tree = document.getAnonymousNodes(this)[0];
          let treecols = tree.getElementsByTagNameNS(tree.namespaceURI, "treecol");
          for (let i = 0; i < treecols.length; i++) {
              if (treecols[i].getAttribute("hidden") != "true") {
                let content = treecols[i].getAttribute("itemproperty");
                visible += (visible.length > 0) ? " "+content : content;
              }
              if(ordinals.length > 0)
                  ordinals += " ";
              ordinals += treecols[i].ordinal;
              if(widths.length > 0)
                  widths += " ";
              widths += treecols[i].width || 0;
          }
          this.setAttribute("visible-columns",visible);
          this.setAttribute("ordinals",ordinals);
          this.setAttribute("widths",widths);
          if (sorted) {
              this.setAttribute("sort-active",sorted.getAttribute("itemproperty"));
              this.setAttribute("sort-direction",this.mTreeView.sortDirection);
          } else {
              this.removeAttribute("sort-active");
              this.removeAttribute("sort-direction");
          }
      ]]></destructor>

      <field name="mTaskArray">[]</field>
      <field name="mHash2Index"><![CDATA[({})]]></field>
      <field name="mPendingRefreshJobs"><![CDATA[({})]]></field>
      <field name="mShowCompletedTasks">true</field>
      <field name="mFilter">null</field>
      <field name="mStartDate">null</field>
      <field name="mEndDate">null</field>
      <field name="mDateRangeFilter">null</field>
      <field name="mTextFilterField">null</field>

      <property name="currentIndex">
        <getter><![CDATA[
          var tree = document.getAnonymousElementByAttribute(
              this, "anonid", "calendar-task-tree");
          return tree.currentIndex;
        ]]></getter>
      </property>

      <property name="currentTask">
        <getter><![CDATA[
          let tree = document.getAnonymousElementByAttribute(
              this, "anonid", "calendar-task-tree");
          let index = tree.currentIndex;
          if (tree.view && tree.view.selection) {
              // If the current index is not selected, then ignore
              index = (tree.view.selection.isSelected(index) ? index : -1);
          }
          return (index < 0) ? null : this.mTaskArray[index];
        ]]></getter>
      </property>

      <property name="selectedTasks" readonly="true">
        <getter><![CDATA[
          var tasks = [];
          var start = {};
          var end = {};
          if (!this.mTreeView.selection) {
              return tasks;
          }

          var rangeCount = this.mTreeView.selection.getRangeCount();
          for (var range = 0; range < rangeCount; range++) {
              this.mTreeView.selection.getRangeAt(range, start, end);
              for (var i = start.value; i <= end.value; i++) {
                  var task = this.getTaskAtRow(i);
                  if (task) {
                      tasks.push(this.getTaskAtRow(i));
                  }
              }
          }
          return tasks;
        ]]></getter>
      </property>

      <property name="showCompleted">
        <getter><![CDATA[
          return this.mShowCompletedTasks;
        ]]></getter>
        <setter><![CDATA[
          this.mShowCompletedTasks = val;
          return val;
        ]]></setter>
      </property>

      <property name="textFilterField">
        <getter><![CDATA[
          return this.mTextFilterField;
        ]]></getter>
        <setter><![CDATA[
          this.mTextFilterField = val;
          return val;
        ]]></setter>
      </property>

      <method name="duration">
        <parameter name="aTask"/>
        <body><![CDATA[
          if (aTask && aTask.dueDate && aTask.dueDate.isValid){
              let dur = aTask.dueDate.subtractDate(cal.now());
              if (!dur.isNegative) {
                  let minutes = Math.ceil(dur.inSeconds / 60);
                  if (minutes >= 1440) { // 1 day or more
                      let dueIn = PluralForm.get(dur.days, calGetString("calendar", "dueInDays"));
                      return dueIn.replace("#1", dur.days);
                  } else if (minutes >= 60) { // 1 hour or more
                      let dueIn = PluralForm.get(dur.hours, calGetString("calendar", "dueInHours"));
                      return dueIn.replace("#1", dur.hours);
                  } else {
                      // Less than one hour
                      return calGetString("calendar", "dueInLessThanOneHour");
                  }
              } else if (!aTask.completedDate || !aTask.completedDate.isValid) {
                  // Overdue task
                  let minutes = Math.ceil( -dur.inSeconds / 60);
                  if (minutes >= 1440) { // 1 day or more
                      let dueIn = PluralForm.get(dur.days, calGetString("calendar", "dueInDays"));
                      return "-" + dueIn.replace("#1", dur.days);
                  } else if (minutes >= 60) { // 1 hour or more
                      let dueIn = PluralForm.get(dur.hours, calGetString("calendar", "dueInHours"));
                      return "-" + dueIn.replace("#1", dur.hours);
                  } else {
                      // Less than one hour
                      return calGetString("calendar", "dueInLessThanOneHour");
                  }
              }
          }
          // No due date specified
          return null;
        ]]></body>
      </method>
      <method name="getTaskAtRow">
        <parameter name="aRow"/>
        <body><![CDATA[
          return (aRow > -1 ? this.mTaskArray[aRow] : null);
        ]]></body>
      </method>

      <method name="getTaskFromEvent">
        <parameter name="aEvent"/>
        <body><![CDATA[
          return this.mTreeView._getItemFromEvent(aEvent);
        ]]></body>
      </method>

      <field name="mTreeView"><![CDATA[
      ({
          /**
           * Attributes
           */

          // back reference to the binding
          binding: this,
          tree: null,
          treebox: null,
          mSelectedColumn: null,
          sortDirection: null,

          get selectedColumn() {
              return this.mSelectedColumn;
          },

          set selectedColumn(aCol) {
              var tree = document.getAnonymousNodes(this.binding)[0];
              var treecols = tree.getElementsByTagNameNS(tree.namespaceURI, "treecol");
              for (var i = 0; i < treecols.length; i++) {
                  var col = treecols[i];
                  if (col.getAttribute("sortActive")) {
                      col.removeAttribute("sortActive");
                      col.removeAttribute("sortDirection");
                  }
                  if (aCol.getAttribute("itemproperty") == col.getAttribute("itemproperty")) {
                      col.setAttribute("sortActive", "true");
                      col.setAttribute("sortDirection", this.sortDirection);
                  }
              }
              return (this.mSelectedColumn = aCol);
          },

          /**
           * High-level task tree manipulation
           */

          // Adds an array of items to the list if they match the currently applied filter.
          addItems: function tTV_addItems(aItems, aDontSort) {
              this.modifyItems(aItems, [], aDontSort, true);
          },

          // Removes an array of items from the list.
          removeItems: function tTV_removeItems(aItems) {
              this.modifyItems([], aItems, true, false);
          },

          // Removes an array of old items from the list, and adds an array of new items if
          // they match the currently applied filter.
          modifyItems: function tTV_modifyItems(aNewItems, aOldItems, aDontSort, aSelectNew) {
              let selItem = this.binding.currentTask;
              let selIndex = this.tree.currentIndex;
              let firstHash = null;
              let remIndexes = [];
              aNewItems = aNewItems || [];
              aOldItems = aOldItems || [];
              let _this = this;

              this.treebox.beginUpdateBatch();

              let idiff = new itemDiff();
              idiff.load(aOldItems);
              idiff.difference(aNewItems);
              idiff.complete();
              let delItems = idiff.deletedItems;
              let addItems = idiff.addedItems;
              let modItems = idiff.modifiedItems;

              // find the indexes of the old items that need to be removed
              for each (let item in delItems.mArray) {
                  if (item.hashId in this.binding.mHash2Index) {
                      // the old item needs to be removed
                      remIndexes.push(this.binding.mHash2Index[item.hashId]);
                      delete this.binding.mHash2Index[item.hashId];
                  }
              }

              // modified items need to be updated
              for each (let item in modItems.mArray) {
                  if (item.hashId in this.binding.mHash2Index) {
                      // make sure we're using the new version of a modified item
                      this.binding.mTaskArray[this.binding.mHash2Index[item.hashId]] = item;
                  }
              }

              // remove the old items working backward from the end so the indexes stay valid
              remIndexes.sort(function(a, b) {return b - a;}).forEach(function(index) {
                  this.binding.mTaskArray.splice(index, 1);
                  this.treebox.rowCountChanged(index, -1);
              }, this);

              // add the new items
              for each (let item in addItems.mArray) {
                  if (!(item.hashId in this.binding.mHash2Index)) {
                      let index = this.binding.mTaskArray.length;
                      this.binding.mTaskArray.push(item);
                      this.binding.mHash2Index[item.hashId] = index;
                      this.treebox.rowCountChanged(index, 1);
                      firstHash = firstHash || item.hashId;
                  }
              }

              if (aDontSort) {
                  this.binding.recreateHashTable();
              } else {
                  this.binding.sortItems();
              }

              if (aSelectNew && firstHash && firstHash in this.binding.mHash2Index) {
                  // select the first item added into the list
                  selIndex = this.binding.mHash2Index[firstHash];
              } else if (selItem && selItem.hashId in this.binding.mHash2Index) {
                  // select the previously selected item
                  selIndex = this.binding.mHash2Index[selItem.hashId];
              } else if (selIndex >= this.binding.mTaskArray.length) {
                  // make sure the previously selected index is valid
                  selIndex = this.binding.mTaskArray.length - 1;
              }

              if (selIndex > -1) {
                  this.tree.view.selection.select(selIndex);
                  this.treebox.ensureRowIsVisible(selIndex);
              }

              this.treebox.endUpdateBatch();
          },

          clear: function tTV_clear() {
              var count = this.binding.mTaskArray.length;
              if (count > 0) {
                  this.binding.mTaskArray = [];
                  this.binding.mHash2Index = {};
                  this.treebox.rowCountChanged(0, -count);
                  this.tree.view.selection.clearSelection();
              }
          },

          updateItem: function tTV_updateItem(aItem) {
              var index = this.binding.mHash2Index[aItem.hashId];
              if (index) {
                  this.treebox.invalidateRow(index);
              }
          },

          /**
           * nsITreeView methods and properties
           */

          get rowCount() {
              return this.binding.mTaskArray.length;
          },

          // TODO this code is currently identical to the unifinder. We should
          // create an itemTreeView that these tree views can inherit, that
          // contains this code, and possibly other code related to sorting and
          // storing items. See bug 432582 for more details.
          getCellProperties: function mTV_getCellProperties(aRow, aCol) {
              let rowProps = this.getRowProperties(aRow);
              let colProps = this.getColumnProperties(aCol);
              return rowProps + (rowProps && colProps ? " " : "") + colProps;
          },

          // Called to get properties to paint a column background.
          // For shading the sort column, etc.
          getColumnProperties: function mTV_getColumnProperties(aCol) {
              return aCol.element.getAttribute("anonid") || "";
          },

          getRowProperties: function mTV_getRowProperties(aRow) {
              let properties = [];
              let item = this.binding.mTaskArray[aRow];
              if (item.priority > 0 && item.priority < 5) {
                  properties.push("highpriority");
              } else if (item.priority > 5 && item.priority < 10) {
                  properties.push("lowpriority");
              }
              properties.push(getProgressAtom(item));

              // Add calendar name and id atom
              properties.push("calendar-" + formatStringForCSSRule(item.calendar.name));
              properties.push("calendarid-" +  formatStringForCSSRule(item.calendar.id));

              // Add item status atom
              if (item.status) {
                  properties.push("status-" + item.status.toLowerCase());
              }

              // Alarm status atom
              if (item.getAlarms({}).length) {
                  properties.push("alarm");
              }

              // Task categories
              properties = properties.concat(item.getCategories({})
                                                 .map(formatStringForCSSRule));

              return properties.join(" ");
          },

          // Called on the view when a cell in a non-selectable cycling
          // column (e.g., unread/flag/etc.) is clicked.
          cycleCell: function mTV_cycleCell(aRow, aCol) {
              var task = this.binding.mTaskArray[aRow];

              // prevent toggling completed status for parent items of repeating tasks
              if (!task || task.recurrenceInfo) {
                  return;
              }
              if (aCol != null) {
                  var content = aCol.element.getAttribute("itemproperty");
                  if (content == "completed")  {
                      var newTask = task.clone().QueryInterface(Components.interfaces.calITodo);
                      newTask.isCompleted = !task.completedDate;
                      doTransaction('modify', newTask, newTask.calendar, task, null);
                  }
              }
          },

          // Called on the view when a header is clicked.
          cycleHeader: function mTV_cycleHeader(aCol) {
              if (!this.selectedColumn) {
                  this.sortDirection = "ascending";
              }
              else {
                  if(!this.sortDirection || this.sortDirection == "descending") {
                      this.sortDirection = "ascending";
                  } else {
                      this.sortDirection = "descending";
                  }
              }
              this.selectedColumn = aCol.element;
              let selectedItems = this.binding.selectedTasks;
              this.binding.sortItems();
              if (selectedItems != undefined) {
                  this.tree.view.selection.clearSelection();
                  for each (let item in selectedItems){
                      let index = this.binding.mHash2Index[item.hashId];
                      this.tree.view.selection.toggleSelect(index);
                  }
              }
          },

          // The text for a given cell. If a column consists only of an
          // image, then the empty string is returned.
          getCellText: function mTV_getCellText(aRow, aCol) {
              var task = this.binding.mTaskArray[aRow];
              if (!task)
                  return false;
              switch (aCol.element.getAttribute("itemproperty")) {
                  case "title":
                      // return title, or "Untitled" if empty/null
                      return (task.title ? task.title.replace(/\n/g, ' ') : calGetString("calendar", "eventUntitled"));
                  case "entryDate":
                      return task.recurrenceInfo ? calGetString("dateFormat", "Repeating") : this._formatDateTime(task.entryDate);
                  case "dueDate":
                      return task.recurrenceInfo ? calGetString("dateFormat", "Repeating") : this._formatDateTime(task.dueDate);
                  case "completedDate":
                      return task.recurrenceInfo ? calGetString("dateFormat", "Repeating") : this._formatDateTime(task.completedDate);
                  case "percentComplete":
                      return (task.percentComplete > 0 ? task.percentComplete + "%" : "");
                  case "categories":
                      return task.getCategories({}).join(", "); // TODO l10n-unfriendly
                  case "location":
                      return task.getProperty("LOCATION");
                  case "status":
                      return getToDoStatusString(task);
                  case "calendar":
                      return task.calendar.name;
                  case "duration":
                      return this.binding.duration(task);
                  case "completed":
                  case "priority":
                  default:
                      return "";
              }
          },

          // This method is only called for columns of type other than text.
          getCellValue: function mTV_getCellValue(aRow, aCol) {
              var task = this.binding.mTaskArray[aRow];
              if (!task) {
                  return null;
              }
              switch (aCol.element.getAttribute("itemproperty")) {
                  case "percentComplete":
                      return task.percentComplete;
              }
              return null;
          },

          // SetCellValue is called when the value of the cell has been set by the user.
          // This method is only called for columns of type other than text.
          setCellValue: function mTV_setCellValue(aRow, aCol, aValue) {
              return null;
          },

          // The image path for a given cell. For defining an icon for a cell.
          // If the empty string is returned, the :moz-tree-image pseudoelement will be used.
          getImageSrc: function mTV_getImageSrc(aRow, aCol) {
             // Return the empty string in order
             // to use moz-tree-image pseudoelement :
             // it is mandatory to return "" and not false :-(
             return("");
          },

          // IsEditable is called to ask the view if the cell contents are editable.
          // A value of true will result in the tree popping up a text field when the user
          // tries to inline edit the cell.
          isEditable: function mTV_isEditable(aRow, aCol) {
              return true;
          },

          // Called during initialization to link the view to the front end box object.
          setTree: function mTV_setTree(aTreeBox) {
            this.treebox = aTreeBox;
          },

          // Methods that can be used to test whether or not a twisty should
          // be drawn, and if so, whether an open or closed twisty should be used.
          isContainer: function mTV_isContainer(aRow) {
              return false;
          },
          isContainerOpen: function mTV_isContainerOpen(aRow) {
              return false;
          },
          isContainerEmpty: function mTV_isContainerEmpty(aRow) {
              return false;
          },

          // IsSeparator is used to determine if the row at index is a separator.
          // A value of true will result in the tree drawing a horizontal separator.
          // The tree uses the ::moz-tree-separator pseudoclass to draw the separator.
          isSeparator: function mTV_isSeparator(aRow) {
              return false;
          },

          // Specifies if there is currently a sort on any column.
          // Used mostly by dragdrop to affect drop feedback.
          isSorted: function mTV_isSorted(aRow) {
              return false;
          },

          canDrop: function mTV_canDrop() { return false; },

          drop: function mTV_drop(aRow, aOrientation) {},

          getParentIndex: function mTV_getParentIndex(aRow) {
              return -1;
          },

          // The level is an integer value that represents the level of indentation.
          // It is multiplied by the width specified in the :moz-tree-indentation
          // pseudoelement to compute the exact indendation.
          getLevel: function mTV_getLevel(aRow) {
              return 0;
          },

          // The image path for a given cell. For defining an icon for a cell.
          // If the empty string is returned, the :moz-tree-image pseudoelement
          // will be used.
          getImgSrc: function mTV_getImgSrc(aRow, aCol) {
              return null;
          },

          // The progress mode for a given cell. This method is only called for
          // columns of type |progressmeter|.
          getProgressMode: function mTV_getProgressMode(aRow, aCol) {
              switch(aCol.element.getAttribute("itemproperty")) {
                  case "percentComplete":
                      var task = this.binding.mTaskArray[aRow];
                      if (aCol.element.boxObject.width > 75 &&
                          task.percentComplete > 0 ) {
                          // XXX Would be nice if we could use relative widths,
                          // i.e "15ex", but there is no scriptable interface.
                          return Components.interfaces.nsITreeView.PROGRESS_NORMAL;
                      }
                      break;
              }

              return Components.interfaces.nsITreeView.PROGRESS_NONE;
          },

          /**
           * Task Tree Events
           */
          onSelect: function tTV_onSelect(event) {},

          onDoubleClick: function tTV_onDoubleClick(event) {
              if (event.button == 0) {
                  let initialDate = getDefaultStartDate(this.binding.getInitialDate());
                  var col = {};
                  var item = this._getItemFromEvent(event, col);
                  if (item) {
                      var colAnonId = col.value.element.getAttribute("itemproperty");
                      if (colAnonId == "completed") {
                          // item holds checkbox state toggled by first click,
                          // so don't call modifyEventWithDialog
                          // to make sure user notices state changed.
                      } else {
                          modifyEventWithDialog(item, null, true, initialDate);
                      }
                  } else {
                      createTodoWithDialog(null, null, null, null, initialDate);
                  }
              }
          },

          onKeyPress: function tTV_onKeyPress(event) {
              const kKE = Components.interfaces.nsIDOMKeyEvent;
              switch (event.keyCode || event.which) {
                  case kKE.DOM_VK_DELETE:
                      document.popupNode = this.binding;
                      document.getElementById('calendar_delete_todo_command').doCommand();
                      event.preventDefault();
                      event.stopPropagation();
                      break;
                  case kKE.DOM_VK_SPACE:
                      if (this.tree.currentIndex > -1) {
                          var col = document.getAnonymousElementByAttribute(
                              this.binding, "itemproperty", "completed");
                          this.cycleCell(
                              this.tree.currentIndex,
                              { element: col });
                      }
                      break;
                  case kKE.DOM_VK_RETURN:
                      var index = this.tree.currentIndex;
                      if (index > -1) {
                          modifyEventWithDialog(this.binding.mTaskArray[index]);
                      }
                      break;
              }
          },

          // Set the context menu on mousedown to change it before it is opened
          onMouseDown: function tTV_onMouseDown(event) {
              let tree = document.getAnonymousElementByAttribute(this.binding,
                                                                 "anonid",
                                                                 "calendar-task-tree");

              if (!this._getItemFromEvent(event)) {
                  tree.view.selection.invalidateSelection();
              }
          },

          /**
           * Private methods and attributes
           */

          _getItemFromEvent: function tTV_getItemFromEvent(event, aCol, aRow) {
            aRow = aRow || {};
            var childElt = {};
            this.treebox.getCellAt(event.clientX, event.clientY, aRow, aCol || {}, childElt);
            if (!childElt.value) {
                return false;
            }
            return aRow && aRow.value > -1 && this.binding.mTaskArray[aRow.value];
          },

          // Helper function to display datetimes
          _formatDateTime: function tTV_formatDateTime(aDateTime) {
              var dateFormatter = Components.classes["@mozilla.org/calendar/datetime-formatter;1"]
                                            .getService(Components.interfaces.calIDateTimeFormatter);

              // datetime is from todo object, it is not a javascript date
              if (aDateTime && aDateTime.isValid) {
                  var dateTime = aDateTime.getInTimezone(calendarDefaultTimezone());
                  return dateFormatter.formatDateTime(dateTime);
              }
              return "";
          }
      })
      ]]></field>

      <!--
        Observer for the calendar event data source. This keeps the unifinder
        display up to date when the calendar event data is changed
        -->
      <field name="mTaskTreeObserver"><![CDATA[
      ({
          binding: this,

          QueryInterface: XPCOMUtils.generateQI([
              Components.interfaces.calICompositeObserver,
              Components.interfaces.calIObserver
          ]),

          /**
           * calIObserver methods and properties
           */
          onStartBatch: function tTO_onStartBatch() {
          },

          onEndBatch: function tTO_onEndBatch() {
          },

          onLoad: function tTO_onLoad() {
              this.binding.refresh();
          },

          onAddItem: function tTO_onAddItem(aItem) {
              if (cal.isToDo(aItem)) {
                  this.binding.mTreeView.addItems(this.binding.mFilter.getOccurrences(aItem));
              }
          },

          onModifyItem: function tTO_onModifyItem(aNewItem, aOldItem) {
              if ((cal.isToDo(aNewItem) || cal.isToDo(aOldItem))) {
                  this.binding.mTreeView.modifyItems(this.binding.mFilter.getOccurrences(aNewItem),
                                                     this.binding.mFilter.getOccurrences(aOldItem));

                  // we also need to notify potential listeners.
                  var event = document.createEvent('Events');
                  event.initEvent('select', true, false);
                  this.binding.dispatchEvent(event);
              }
          },

          onDeleteItem: function tTO_onDeleteItem(aDeletedItem) {
              if (cal.isToDo(aDeletedItem)) {
                  this.binding.mTreeView.removeItems(this.binding.mFilter.getOccurrences(aDeletedItem));
              }
          },

          onError: function tTO_onError(aCalendar, aErrNo, aMessage) {},
          onPropertyChanged: function tTO_onPropertyChanged(aCalendar, aName, aValue, aOldValue) {
              switch (aName) {
                  case "disabled":
                      if (aValue) {
                          this.binding.onCalendarRemoved(aCalendar);
                      } else {
                          this.binding.onCalendarAdded(aCalendar);
                      }
                      break;
              }
          },

          onPropertyDeleting: function tTO_onPropertyDeleting(aCalendar, aName) {
              this.onPropertyChanged(aCalendar, aName, null, null);
          },

          /**
           * calICompositeObserver methods and properties
           */
          onCalendarAdded: function tTO_onCalendarAdded(aCalendar) {
              if (!aCalendar.getProperty("disabled")) {
                  this.binding.onCalendarAdded(aCalendar);
              }
          },

          onCalendarRemoved: function tTO_onCalendarRemoved(aCalendar) {
              this.binding.onCalendarRemoved(aCalendar);
          },

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

      <method name="observe">
        <parameter name="aSubject"/>
        <parameter name="aTopic"/>
        <parameter name="aPrefName"/>
        <body><![CDATA[
          switch (aPrefName) {
              case "calendar.date.format":
              case "calendar.timezone.local":
                  this.refresh();
                  break;
          }

        ]]></body>
      </method>

      <method name="refreshFromCalendar">
        <parameter name="aCalendar"/>
        <body><![CDATA[
          let refreshJob = {
              binding: this,
              calendar: null,
              items: null,
              operation: null,

              onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDateTime) {
                  if (aCalendar.id in this.binding.mPendingRefreshJobs) {
                      delete this.binding.mPendingRefreshJobs[aCalendar.id];
                  }

                  let oldItems = this.binding.mTaskArray.filter(function(item) {
                      return item.calendar.id == aCalendar.id;
                  });

                  this.binding.mTreeView.modifyItems(this.items, oldItems);
              },

              onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
                  this.items = this.items.concat(aItems);
              },

              cancel: function() {
                  if (this.operation && this.operation.isPending) {
                      this.operation.cancel();
                      this.operation = null;
                      this.items = [];
                  }
              },

              execute: function(aCalendar) {
                  if (aCalendar.id in this.binding.mPendingRefreshJobs) {
                      this.binding.mPendingRefreshJobs[aCalendar.id].cancel();
                  }
                  this.calendar = aCalendar;
                  this.items = [];

                  let op = this.binding.mFilter.getItems(aCalendar,
                                                         aCalendar.ITEM_FILTER_TYPE_TODO,
                                                         this);
                  if (op && op.isPending) {
                      this.operation = op;
                      this.binding.mPendingRefreshJobs[aCalendar.id] = this;
                  }
              }
          };

          refreshJob.execute(aCalendar);
        ]]></body>
      </method>

      <method name="selectAll">
        <body><![CDATA[
          if (this.mTreeView.selection) {
            this.mTreeView.selection.selectAll();
          }
        ]]></body>
      </method>

      <!-- Called by event observers to update the display -->
      <method name="refresh">
        <parameter name="aFilter"/>
        <body><![CDATA[
          let cals = getCompositeCalendar().getCalendars({}) || [];
          for each (let calendar in cals) {
              if (!calendar.getProperty("disabled")) {
                  this.refreshFromCalendar(calendar, aFilter);
              }
          }
        ]]></body>
      </method>

      <method name="onCalendarAdded">
        <parameter name="aCalendar"/>
        <parameter name="aFilter"/>
        <body><![CDATA[
            if (!aCalendar.getProperty("disabled")) {
                this.refreshFromCalendar(aCalendar, aFilter);
            }
        ]]></body>
      </method>

      <method name="onCalendarRemoved">
        <parameter name="aCalendar"/>
        <body><![CDATA[
            let tasks = this.mTaskArray.filter(function(task) {
                return task.calendar.id == aCalendar.id;
            });
            this.mTreeView.removeItems(tasks);
        ]]></body>
      </method>

      <method name="sortItems">
        <body><![CDATA[
          if (this.mTreeView.selectedColumn) {
              var modifier = (this.mTreeView.sortDirection == "descending" ? -1 : 1);
              var sortKey = cal.sortEntry.mSortKey = this.mTreeView.selectedColumn.getAttribute("sortKey") ?
                          this.mTreeView.selectedColumn.getAttribute("sortKey") :
                          this.mTreeView.selectedColumn.getAttribute("itemproperty");
              var sortType = cal.getSortTypeForSortKey(sortKey);

              // sort (key,item) entries
              cal.sortEntry.mSortStartedDate = now();
              var entries = this.mTaskArray.map(cal.sortEntry, cal.sortEntry);
              entries.sort(cal.sortEntryComparer(sortType, modifier));
              this.mTaskArray = entries.map(cal.sortEntryItem);
          }

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

      <method name="recreateHashTable">
        <body><![CDATA[
          this.mHash2Index = {};
          for (var i=0; i<this.mTaskArray.length; i++) {
              var item = this.mTaskArray[i];
              this.mHash2Index[item.hashId] = i;
          }
          if (this.mTreeView.treebox) {
            this.mTreeView.treebox.invalidate();
          }
        ]]></body>
      </method>

      <method name="getInitialDate">
        <body><![CDATA[
          let initialDate = currentView().selectedDay;
          return initialDate ? initialDate : now();
        ]]></body>
      </method>

      <method name="doUpdateFilter">
        <parameter name="aFilter"/>
        <body><![CDATA[
            let needsRefresh = false;
            let oldStart = this.mFilter.mStartDate;
            let oldEnd = this.mFilter.mEndDate;
            let filterText = this.mFilter.filterText || "";

            if (aFilter) {
                let props = this.mFilter.filterProperties;
                this.mFilter.applyFilter(aFilter);
                needsRefresh = !props || !props.equals(this.mFilter.filterProperties);
            } else {
                this.mFilter.updateFilterDates();
            }

            if (this.mTextFilterField) {
                let field = document.getElementById(this.mTextFilterField);
                if (field) {
                    this.mFilter.filterText = field.value;
                    needsRefresh = needsRefresh || filterText.toLowerCase() != this.mFilter.filterText.toLowerCase();
                }
            }

            // we only need to refresh the tree if the filter properties or date range changed
            if (needsRefresh ||
                !((!oldStart && !this.mFilter.mStartDate) ||
                  (oldStart && this.mFilter.mStartDate && oldStart.compare(this.mFilter.mStartDate) == 0)) ||
                !((!oldEnd && !this.mFilter.mEndDate) ||
                  (oldEnd && this.mFilter.mEndDate && oldEnd.compare(this.mFilter.mEndDate) == 0))) {
                this.refresh();
            }
        ]]></body>
      </method>

      <method name="updateFilter">
        <parameter name="aFilter"/>
        <body><![CDATA[
            this.doUpdateFilter(aFilter);
        ]]></body>
      </method>

      <method name="updateFocus">
        <body><![CDATA[
            let tree = document.getAnonymousElementByAttribute(this, "anonid", "calendar-task-tree");
            let menuOpen = false;

            // we need to consider the tree focused if the context menu is open.
            if (this.hasAttribute("context")) {
                let context = document.getElementById(this.getAttribute("context"));
                if (context && context.state) {
                    menuOpen = (context.state == "open") || (context.state == "showing");
                }
            }

            let focused = (document.activeElement == tree) || menuOpen;

            calendarController.onSelectionChanged({detail:(focused ? this.selectedTasks : [])});
            calendarController.todo_tasktree_focused = focused;
        ]]></body>
      </method>

    </implementation>

    <handlers>
      <handler event="select"><![CDATA[
        this.mTreeView.onSelect(event);
        if (calendarController.todo_tasktree_focused) {
          calendarController.onSelectionChanged({detail:this.selectedTasks});
        }
      ]]></handler>
      <handler event="dblclick" button="0"><![CDATA[
        this.mTreeView.onDoubleClick(event);
      ]]></handler>
      <handler event="focus"><![CDATA[
        this.updateFocus();
      ]]></handler>
      <handler event="blur"><![CDATA[
        this.updateFocus();
      ]]></handler>
      <handler event="keypress"><![CDATA[
        this.mTreeView.onKeyPress(event);
      ]]></handler>
      <handler event="mousedown"><![CDATA[
        this.mTreeView.onMouseDown(event);
      ]]></handler>
      <handler event="draggesture"><![CDATA[
        if (event.originalTarget.localName != "treechildren") {
            // We should only drag treechildren, not for example the scrollbar.
            return;
        }
        var item = this.mTreeView._getItemFromEvent(event);
        if (!item || item.calendar.readOnly) {
            return;
        }

        var tree = document.getAnonymousElementByAttribute(this, "anonid", "calendar-task-tree");

        // let's build the drag region
        var region = null;
        try {
          region = Components.classes["@mozilla.org/gfx/region;1"].createInstance(Components.interfaces.nsIScriptableRegion);
          region.init();
          var obo = tree.treeBoxObject;
          var bo = obo.treeBody.boxObject;
          var sel= tree.view.selection;

          var rowX = bo.x;
          var rowY = bo.y;
          var rowHeight = obo.rowHeight;
          var rowWidth = bo.width;

          //add a rectangle for each visible selected row
          for (var i = obo.getFirstVisibleRow(); i <= obo.getLastVisibleRow(); i ++) {
            if (sel.isSelected(i))
              region.unionRect(rowX, rowY, rowWidth, rowHeight);
            rowY = rowY + rowHeight;
          }

          //and finally, clip the result to be sure we don't spill over...
          if(!region.isEmpty())
            region.intersectRect(bo.x, bo.y, bo.width, bo.height);
        } catch(ex) {
          ASSERT(false, "Error while building selection region: " + ex + "\n");
          region = null;
        }
        invokeEventDragSession(item, event.target);
      ]]></handler>
    </handlers>

  </binding>

  <binding id="calendar-task-tree-todaypane" extends="chrome://calendar/content/calendar-task-tree.xml#calendar-task-tree">
    <implementation>
      <method name="getInitialDate">
            <body><![CDATA[
              let initialDate = agendaListbox.today ? agendaListbox.today.start : now();
              return initialDate ? initialDate : now();
            ]]></body>
      </method>
      <method name="updateFilter">
        <parameter name="aFilter"/>
        <body><![CDATA[
            this.mFilter.selectedDate = agendaListbox.today && agendaListbox.today.start ? 
                                        agendaListbox.today.start : now();

            this.doUpdateFilter(aFilter);
        ]]></body>
      </method>
    </implementation>    
  </binding>
</bindings>
