calendar/base/content/calendar-task-tree.xml
author Stefan Sitter <ssitter@gmail.com>
Fri, 09 Sep 2011 00:36:50 +0200
changeset 8437 fe74f461b4b8357818fa289765b2ae053b3237fb
parent 8434 05afa98d07c2e7ff77a2bff0211696bbe9582902
child 8486 347508f378fd3cb39057313b3beb2563077cb75b
permissions -rw-r--r--
Bug 684582 - Fix strict warnings: assignment to undeclared variable. r=philipp, a=philipp

<?xml version="1.0"?>
<!-- ***** BEGIN LICENSE BLOCK *****
   - Version: MPL 1.1/GPL 2.0/LGPL 2.1
   -
   - The contents of this file are subject to the Mozilla Public License Version
   - 1.1 (the "License"); you may not use this file except in compliance with
   - the License. You may obtain a copy of the License at
   - http://www.mozilla.org/MPL/
   -
   - Software distributed under the License is distributed on an "AS IS" basis,
   - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
   - for the specific language governing rights and limitations under the
   - License.
   -
   - The Original Code is OEone Calendar Code, released October 31st, 2001.
   -
   - The Initial Developer of the Original Code is OEone Corporation.
   - Portions created by the Initial Developer are Copyright (C) 2001
   - the Initial Developer. All Rights Reserved.
   -
   - Contributor(s):
   -   Garth Smedley <garths@oeone.com>
   -   Mike Potter <mikep@oeone.com>
   -   Chris Charabaruk <coldacid@meldstar.com>
   -   Colin Phillips <colinp@oeone.com>
   -   ArentJan Banck <ajbanck@planet.nl>
   -   Curtis Jewell <csjewell@mail.freeshell.org>
   -   Eric Belhaire <eric.belhaire@ief.u-psud.fr>
   -   Mark Swaffer <swaff@fudo.org>
   -   Michael Buettner <michael.buettner@sun.com>
   -   Philipp Kewisch <mozilla@kewis.ch>
   -   Lars Wohlfahrt <thetux.moz@googlemail.com>
   -   Fred Jendrzejewski <fred.jen@web.de>
   -
   - Alternatively, the contents of this file may be used under the terms of
   - either the GNU General Public License Version 2 or later (the "GPL"), or
   - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
   - in which case the provisions of the GPL or the LGPL are applicable instead
   - of those above. If you wish to allow use of your version of this file only
   - under the terms of either the GPL or the LGPL, and not to allow others to
   - use your version of this file under the terms of the MPL, indicate your
   - decision by deleting the provisions above and replace them with the notice
   - and other provisions required by the GPL or the LGPL. If you do not delete
   - the provisions above, a recipient may use your version of this file under
   - the terms of any one of the MPL, the GPL or the LGPL.
   -
   - ***** END LICENSE BLOCK ***** -->

<!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;">
            <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;">
            <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;"/>
          <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;"/>
          <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;"/>
          <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;"/>
          <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;"/>
          <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;"/>
          <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;"/>
          <xul:splitter class="tree-splitter" ordinal="18"/>
          <xul:treecol anonid="calendar-task-tree-col-location"
                       itemproperty="location"
                       label="&calendar.unifinder.tree.location.label;"/>
          <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;"/>
          <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;"/>
        </xul:treecols>
        <xul:treechildren tooltip="taskTreeTooltip"/>
      </xul:tree>
    </content>

    <implementation implements="nsIObserver">
      <constructor><![CDATA[
        Components.utils.import("resource://gre/modules/PluralForm.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 prefService = Components.classes["@mozilla.org/preferences-service;1"]
                                    .getService(Components.interfaces.nsIPrefService);
        let branch = prefService.getBranch("")
                                .QueryInterface(Components.interfaces.nsIPrefBranch2);
        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[
          // remove composite calendar observer
          let composite = getCompositeCalendar();
          composite.removeObserver(this.mTaskTreeObserver);

          // remove the preference observer
          let prefService = Components.classes["@mozilla.org/preferences-service;1"]
                                      .getService(Components.interfaces.nsIPrefService);
          let branch = prefService.getBranch("")
                                  .QueryInterface(Components.interfaces.nsIPrefBranch2);
          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="mRefreshQueue">[]</field>
      <field name="mPendingRefresh">null</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>

      <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>
      <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
           */

          addItem: function tTV_addItem(aItem, aDontSort) {
              if (aItem.isCompleted && !this.binding.showCompleted) {
                  return;
              }
              var index = this.binding.mHash2Index[aItem.hashId];
              if (index === undefined) {
                  var index = this.binding.mTaskArray.length;
                  this.binding.mTaskArray.push(aItem);
                  this.binding.mHash2Index[aItem.hashId] = index;
                  // The rowCountChanged function takes two arguments, the index where the
                  // first row was inserted and the number of rows to insert.
                  this.treebox.rowCountChanged(index, 1);
                  this.tree.view.selection.select(index);
              }
              this.treebox.ensureRowIsVisible(this.rowCount - 1);

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

          removeItem: function tTV_removeItem(aItem, aDontSort) {
              var index = this.binding.mHash2Index[aItem.hashId];
              if (index != undefined) {
                  delete this.binding.mHash2Index[aItem.hashId];
                  this.binding.mTaskArray.splice(index, 1);
                  this.treebox.rowCountChanged(index, -1);

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

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

                  this.binding.recreateHashTable();
              }
          },

           modifyItem: function tTV_modifyItem(aNewItem, aOldItem, aDontSort) {
              var index = this.binding.mHash2Index[aOldItem.hashId];
              if (index != undefined) {
                  // if a filter is installed we need to make sure that
                  // the item still belongs to the set of valid items before
                  // moving forward. if the filter cuts this item off, we
                  // need to act accordingly.
                  if (!this.binding.mFilter.isItemInFilters(aNewItem)) {
                      this.removeItem(aNewItem);
                      return;
                  }
                  // same holds true for the completed filter, which is
                  // currently modeled as an explicit boolean.
                  if (aNewItem.isCompleted != aOldItem.isCompleted) {
                      if (aNewItem.isCompleted && !this.binding.showCompleted) {
                          this.removeItem(aNewItem);
                          return;
                      }
                  }
                  delete this.binding.mHash2Index[aOldItem.hashId];
                  this.binding.mHash2Index[aNewItem.hashId] = index;
                  this.binding.mTaskArray[index] = aNewItem;
                  this.tree.view.selection.select(index);

                  if(aDontSort) {
                      this.treebox.invalidateRow(index);
                  } else {
                      this.binding.sortItems();
                  }
              }
          },

          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, aProps) {
              this.getRowProperties(aRow, aProps);
              this.getColumnProperties(aCol, aProps);
          },

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

          getRowProperties: function mTV_getRowProperties(aRow, aProps) {
              var item = this.binding.mTaskArray[aRow];
              if (item.priority > 0 && item.priority < 5) {
                  aProps.AppendElement(getAtomFromService("highpriority"));
              } else if (item.priority > 5 && item.priority < 10) {
                  aProps.AppendElement(getAtomFromService("lowpriority"));
              }
              aProps.AppendElement(getAtomFromService(getProgressAtom(item)));

              // Add calendar name and id atom
              var calendarNameAtom = "calendar-" + formatStringForCSSRule(item.calendar.name);
              var calendarIdAtom = "calendarid-" +  formatStringForCSSRule(item.calendar.id);
              aProps.AppendElement(getAtomFromService(calendarNameAtom));
              aProps.AppendElement(getAtomFromService(calendarIdAtom));

              // Add item status atom
              if (item.status) {
                  aProps.AppendElement(getAtomFromService("status-" + item.status.toLowerCase()));
              }

              // Alarm status atom
              if (item.getAlarms({}).length) {
                  aProps.AppendElement(getAtomFromService("alarm"));
              }

              // Task categories
              var categories = item.getCategories({});
              categories.map(formatStringForCSSRule)
                        .map(getAtomFromService)
                        .forEach(aProps.AppendElement, aProps);
          },

          // 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,
          mBatchCount: 0,

          QueryInterface: function tTO_QueryInterface(aIID) {
              return doQueryInterface(this, null, aIID,
                                      [Components.interfaces.calICompositeObserver,
                                       Components.interfaces.calIObserver]);
          },

         /**
           * calIObserver methods and properties
           */
          onStartBatch: function tTO_onStartBatch() {
              this.mBatchCount++;
          },

          onEndBatch: function tTO_onEndBatch() {
              this.mBatchCount--;
              if (this.mBatchCount == 0) {
                  this.binding.refresh();
              }
          },

          onLoad: function tTO_onLoad() {
              if (this.mBatchCount == 0) {
                  this.binding.refresh();
              }
          },

          onAddItem: function tTO_onAddItem(aItem) {
              if (cal.isToDo(aItem) &&
                  !this.mBatchCount) { 

                  // get occurrences of repeating items
                  let occs;
                  if (this.binding.mFilter.endDate) {
                      occs = aItem.getOccurrencesBetween(this.binding.mFilter.startDate,
                                                         this.binding.mFilter.endDate,
                                                         {});
                  } else {
                      occs = [aItem];
                  }
                  for each (let occ in occs) {
                      if (this.binding.mFilter.isItemInFilters(occ)) {
                          this.binding.mTreeView.addItem(occ);
                      }
                  }
              }
          },

          onModifyItem: function tTO_onModifyItem(aNewItem, aOldItem) {
              if ((cal.isToDo(aNewItem) || cal.isToDo(aOldItem))
                  && !this.mBatchCount) {

                  if ((this.binding.mFilter.endDate) &&
                      (aOldItem.recurrenceInfo || aNewItem.recurrenceInfo)) {

                      // if item is repeating refresh to updated all modified occurrences
                      this.binding.refresh();
                  } else {
                      // forward the call to the view which will in turn
                      // update the internal reference and the view.
                      this.binding.mTreeView.modifyItem(aNewItem, 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.mBatchCount) {

                  // get occurrences of repeating items
                  let occs;
                  if (this.binding.mFilter.endDate) {
                      occs = aDeletedItem.getOccurrencesBetween(this.binding.mFilter.startDate,
                                                                this.binding.mFilter.endDate,
                                                                {});
                  } else {
                      occs = [aDeletedItem];
                  }
                  occs.forEach(this.binding.mTreeView.removeItem, this.binding.mTreeView);
              }
          },

          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"/>
        <parameter name="aFilter"/>
        <parameter name="aCompleteRefresh"/>
        <body><![CDATA[
          // XXX: why do I need this ?
          if (!this.mFilter) {
              this.mFilter = new calFilter();
          }
          var savedThis = this;
          var newArray = [];
          if (!aCompleteRefresh) {
              newArray = [ task for each (task in this.mTaskArray)
                            if (task.calendar.id != aCalendar.id) ];
          }

          var refreshListener = {
              onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDateTime) {
                  savedThis.mTaskArray = newArray;
                  savedThis.onOperationComplete();
              },

              onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
                  for (let i=0; i < aCount; i++) {
                      if (savedThis.mFilter.isItemInFilters(aItems[i])) {
                          newArray.push(aItems[i]);
                      }
                  }
              }
          };

          var refreshJob = {
              execute: function() {
                  var filter = aFilter || !savedThis.mShowCompletedTasks ?
                      aCalendar.ITEM_FILTER_COMPLETED_NO :
                      aCalendar.ITEM_FILTER_COMPLETED_ALL;
                  filter |= aCalendar.ITEM_FILTER_TYPE_TODO;

                  if (savedThis.mFilter.endDate) {
                      filter |= aCalendar.ITEM_FILTER_CLASS_OCCURRENCES;
                  }
                  aCalendar.getItems(filter,
                                     0,
                                     savedThis.mFilter.startDate,
                                     savedThis.mFilter.endDate,
                                     refreshListener);
              }
          };
          this.mRefreshQueue.push(refreshJob);
          this.popRefreshQueue();
        ]]></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[
            this.refreshFromCalendar(getCompositeCalendar(), aFilter, true);
        ]]></body>
      </method>

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

      <method name="onCalendarRemoved">
        <parameter name="aCalendar"/>
        <body><![CDATA[
            this.mTreeView.treebox.beginUpdateBatch();
            let tasks = this.mTaskArray.concat([]);
            for each (let task in tasks) {
                if (task.calendar.id == aCalendar.id) {
                    this.mTreeView.removeItem(task);
                }
            }
            this.mTreeView.treebox.endUpdateBatch();
        ]]></body>
      </method>

      <method name="popRefreshQueue">
        <body><![CDATA[
          var pendingRefresh = this.mPendingRefresh;
          if (pendingRefresh) {
              if (calInstanceOf(pendingRefresh, Components.interfaces.calIOperation)) {
                  this.mPendingRefresh = null;
                  pendingRefresh.cancel(null);
              } else {
                  return;
              }
          }

          var refreshJob = this.mRefreshQueue.pop();
          if (!refreshJob) {
              return;
          }

          this.mPendingRefresh = true;
          pendingRefresh = refreshJob.execute();
          if (pendingRefresh && pendingRefresh.isPending) {
              this.mPendingRefresh = pendingRefresh;
          }
        ]]></body>
      </method>

      <method name="onOperationComplete">
        <body><![CDATA[
          // signal that the current operation finished.
          this.mPendingRefresh = null;

          // immediately start the next job on the queue.
          this.popRefreshQueue();

          var tree = document.getAnonymousNodes(this)[0];
          if(this.mTreeView.selectedColumn) {
              this.sortItems();
          } else {
              this.recreateHashTable();
          }

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

          // we also need to notify potential listeners.
          var event = document.createEvent('Events');
          event.initEvent('select', true, false);
          this.dispatchEvent(event);
        ]]></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="updateFilter">
        <parameter name="aFilter"/>
        <body><![CDATA[
            this.mFilter.propertyFilter = aFilter || this.mFilter.propertyFilter || "all";
            this.mDateRangeFilter = aFilter || this.mDateRangeFilter;
            if (this.mDateRangeFilter) {
                this.mFilter.setDateFilter(this.mDateRangeFilter);
            } else {
                let oneDay = cal.createDuration();
                oneDay.days = 1;
                this.mFilter.startDate = cal.createDateTime();
                this.mFilter.endDate = this.getInitialDate().clone();
                this.mFilter.endDate.addDuration(oneDay);
            }

            this.refresh();
        ]]></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>
    </implementation>    
  </binding>
</bindings>