1171 строка
47 KiB
XML
1171 строка
47 KiB
XML
<?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">
|
|
<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>
|
|
|
|
<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){
|
|
var dur = aTask.dueDate.subtractDate(now());
|
|
if (!dur.isNegative) {
|
|
var 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");
|
|
}
|
|
}
|
|
}
|
|
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 tTV_getSelectedColumn() {
|
|
return this.mSelectedColumn;
|
|
},
|
|
|
|
set selectedColumn tTV_setSelectedColumn(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-" + 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];
|
|
if(!task)
|
|
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 (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 || calGetString("calendar", "eventUntitled");
|
|
case "entryDate":
|
|
return this._formatDateTime(task.entryDate);
|
|
case "dueDate":
|
|
return this._formatDateTime(task.dueDate);
|
|
case "completedDate":
|
|
return 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) {
|
|
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);
|
|
}
|
|
} else {
|
|
createTodoWithDialog();
|
|
}
|
|
}
|
|
},
|
|
|
|
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,
|
|
mInBatch: false,
|
|
|
|
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.mInBatch = true;
|
|
},
|
|
|
|
onEndBatch: function tTO_onEndBatch() {
|
|
if (this.mInBatch) {
|
|
this.mInBatch = false;
|
|
this.binding.refresh();
|
|
}
|
|
},
|
|
|
|
onLoad: function tTO_onLoad() {
|
|
if (!this.mInBatch) {
|
|
this.binding.refresh();
|
|
}
|
|
},
|
|
|
|
onAddItem: function tTO_onAddItem(aItem) {
|
|
// XXX: We have to filter here
|
|
if (isToDo(aItem) &&
|
|
!this.mInBatch &&
|
|
this.binding.mFilter.isItemInFilters(aItem)) {
|
|
this.binding.mTreeView.addItem(aItem);
|
|
}
|
|
},
|
|
|
|
onModifyItem: function tTO_onModifyItem(aNewItem, aOldItem) {
|
|
if ((isToDo(aNewItem) || isToDo(aOldItem)) &&
|
|
!this.mInBatch) {
|
|
|
|
// 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 (isToDo(aDeletedItem) &&
|
|
!this.mInBatch) {
|
|
this.binding.mTreeView.removeItem(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 (!this.mInBatch && !aCalendar.getProperty("disabled")) {
|
|
this.binding.onCalendarAdded(aCalendar);
|
|
}
|
|
},
|
|
|
|
onCalendarRemoved: function tTO_onCalendarRemoved(aCalendar) {
|
|
if (!this.mInBatch && !aCalendar.getProperty("disabled")) {
|
|
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>
|
|
|
|
<!-- Called by event observers to update the display -->
|
|
<method name="refresh">
|
|
<parameter name="aFilter"/>
|
|
<body><![CDATA[
|
|
// XXX: why do I need this ?
|
|
if(!this.mFilter) {
|
|
this.mFilter = new calFilter();
|
|
}
|
|
var savedThis = this;
|
|
var refreshListener = {
|
|
mTaskArray: [],
|
|
|
|
onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDateTime) {
|
|
savedThis.mTaskArray = this.mTaskArray;
|
|
savedThis.onOperationComplete();
|
|
},
|
|
|
|
onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
|
|
for (var i=0; i<aCount; i++) {
|
|
if (savedThis.mFilter.isItemInFilters(aItems[i])) {
|
|
refreshListener.mTaskArray.push(aItems[i]);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
var refreshJob = {
|
|
execute: function() {
|
|
var composite = getCompositeCalendar();
|
|
var filter = aFilter || !savedThis.mShowCompletedTasks ?
|
|
composite.ITEM_FILTER_COMPLETED_NO :
|
|
composite.ITEM_FILTER_COMPLETED_ALL;
|
|
filter |= composite.ITEM_FILTER_TYPE_TODO;
|
|
|
|
if (savedThis.mFilter.startDate && savedThis.mFilter.endDate) {
|
|
filter |= composite.ITEM_FILTER_CLASS_OCCURRENCES;
|
|
}
|
|
composite.getItems(filter, 0, savedThis.mFilter.startDate, savedThis.mFilter.endDate, refreshListener);
|
|
}
|
|
};
|
|
this.mRefreshQueue.push(refreshJob);
|
|
this.popRefreshQueue();
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="onCalendarAdded">
|
|
<parameter name="aCalendar"/>
|
|
<parameter name="aFilter"/>
|
|
<body><![CDATA[
|
|
var savedThis = this;
|
|
var refreshListener = {
|
|
onOperationComplete: function (aCalendar, aStatus, aOperationType, aId, aDateTime) {
|
|
savedThis.onOperationComplete();
|
|
},
|
|
|
|
onGetResult: function (aCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
|
|
for (var i=0; i<aCount; i++) {
|
|
if (savedThis.mFilter.isItemInFilters(aItems[i])) {
|
|
savedThis.mTaskArray.push(aItems[i]);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
var refreshJob = {
|
|
execute: function() {
|
|
var composite = getCompositeCalendar();
|
|
var filter = aFilter || !savedThis.mShowCompletedTasks ?
|
|
composite.ITEM_FILTER_COMPLETED_NO :
|
|
composite.ITEM_FILTER_COMPLETED_ALL;
|
|
filter |= composite.ITEM_FILTER_TYPE_TODO;
|
|
if (savedThis.mFilter.startDate && savedThis.mFilter.endDate) {
|
|
filter |= composite.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="onCalendarRemoved">
|
|
<parameter name="aCalendar"/>
|
|
<body><![CDATA[
|
|
if (this.mTaskArray.length > 0) {
|
|
var index = this.mTaskArray.length - 1;
|
|
while(index >= 0) {
|
|
var item = this.mTaskArray[index];
|
|
if (item.calendar == aCalendar) {
|
|
this.mTreeView.removeItem(item);
|
|
}
|
|
--index;
|
|
}
|
|
}
|
|
|
|
// we also need to notify potential listeners.
|
|
var event = document.createEvent('Events');
|
|
event.initEvent('select', true, false);
|
|
this.dispatchEvent(event);
|
|
]]></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>
|
|
|
|
</implementation>
|
|
|
|
<handlers>
|
|
<handler event="select"><![CDATA[
|
|
this.mTreeView.onSelect(event);
|
|
calendarController.onSelectionChanged({detail:this.selectedTasks});
|
|
]]></handler>
|
|
<handler event="dblclick" button="0"><![CDATA[
|
|
this.mTreeView.onDoubleClick(event);
|
|
]]></handler>
|
|
<handler event="focus"><![CDATA[
|
|
calendarController.onSelectionChanged({detail:this.selectedTasks});
|
|
calendarController.todo_tasktree_focused = true;
|
|
]]></handler>
|
|
<handler event="blur"><![CDATA[
|
|
calendarController.onSelectionChanged({detail:[]});
|
|
calendarController.todo_tasktree_focused = false;
|
|
]]></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>
|
|
|
|
</bindings>
|