1 /* ***** BEGIN LICENSE BLOCK *****
2 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3 *
4 * The contents of this file are subject to the Mozilla Public License Version
5 * 1.1 (the "License"); you may not use this file except in compliance with
6 * the License. You may obtain a copy of the License at
7 * http://www.mozilla.org/MPL/
8 *
9 * Software distributed under the License is distributed on an "AS IS" basis,
10 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 * for the specific language governing rights and limitations under the
12 * License.
13 *
14 * The Original Code is Calendar code.
15 *
16 * The Initial Developer of the Original Code is
17 * Philipp Kewisch <mozilla@kewis.ch>
18 * Portions created by the Initial Developer are Copyright (C) 2007
19 * the Initial Developer. All Rights Reserved.
20 *
21 * Contributor(s):
22 * Michiel van Leeuwen <mvl@exedo.nl>
23 * Joey Minta <jminta@gmail.com>
24 *
25 * Alternatively, the contents of this file may be used under the terms of
26 * either the GNU General Public License Version 2 or later (the "GPL"), or
27 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
28 * in which case the provisions of the GPL or the LGPL are applicable instead
29 * of those above. If you wish to allow use of your version of this file only
30 * under the terms of either the GPL or the LGPL, and not to allow others to
31 * use your version of this file under the terms of the MPL, indicate your
32 * decision by deleting the provisions above and replace them with the notice
33 * and other provisions required by the GPL or the LGPL. If you do not delete
34 * the provisions above, a recipient may use your version of this file under
35 * the terms of any one of the MPL, the GPL or the LGPL.
36 *
37 * ***** END LICENSE BLOCK ***** */
38
39 /**
40 * Calendar specific utility functions
41 */
42 var gCompositeCalendar = null;
getCompositeCalendar
43 function getCompositeCalendar() {
44 if (!gCompositeCalendar) {
45 gCompositeCalendar =
46 Components.classes["@mozilla.org/calendar/calendar;1?type=composite"]
47 .createInstance(Components.interfaces.calICompositeCalendar);
48
49 gCompositeCalendar.prefPrefix = 'calendar-main';
50 }
51 return gCompositeCalendar;
52 }
53
getSelectedCalendar
54 function getSelectedCalendar() {
55 var tree = document.getElementById("calendar-list-tree");
56 return (tree.currentIndex > -1) &&
57 calendarListTreeView.mCalendarList[tree.currentIndex] || null;
58 }
59
promptDeleteCalendar
60 function promptDeleteCalendar(aCalendar) {
61 var calendars = getCalendarManager().getCalendars({});
62 if (calendars.length <= 1) {
63 // If this is the last calendar, don't delete it.
64 return;
65 }
66
67 var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
68 .getService(Components.interfaces.nsIPromptService);
69 var ok = promptService.confirm(
70 window,
71 calGetString("calendar", "unsubscribeCalendarTitle"),
72 calGetString("calendar",
73 "unsubscribeCalendarMessage",
74 [aCalendar.name]),
75 {});
76
77 if (ok) {
78 var calMgr = getCalendarManager();
79 calMgr.unregisterCalendar(aCalendar);
80 calMgr.deleteCalendar(aCalendar);
81 }
82 }
83
ensureCalendarVisible
84 function ensureCalendarVisible(aCalendar) {
85 var composite = getCompositeCalendar();
86 if (!composite.getCalendar(aCalendar.uri)) {
87 composite.addCalendar(aCalendar);
88 }
89 }
90
91 /**
92 * Calendar manager load/unload functions
93 */
loadCalendarManager
94 function loadCalendarManager() {
95 var calMgr = getCalendarManager();
96 var composite = getCompositeCalendar();
97 var calendars = calMgr.getCalendars({});
98 var prefService = Components.classes["@mozilla.org/preferences-service;1"]
99 .getService(Components.interfaces.nsIPrefService);
100 var branch = prefService.getBranch("").QueryInterface(Components.interfaces.nsIPrefBranch2);
101
102 if (calendars.length == 0) {
103 var url = makeURL("moz-profile-calendar://");
104 var homeCalendar = calMgr.createCalendar("storage", url);
105
106 calMgr.registerCalendar(homeCalendar);
107 var name = calGetString("calendar", "homeCalendarName");
108
109 homeCalendar.name = name;
110 composite.addCalendar(homeCalendar);
111
112 // Wrapping this in a try/catch block, as if any of the migration code
113 // fails, the app may not load.
114 if (getPrefSafe("calendar.migrator.enabled", true)) {
115 try {
116 gDataMigrator.checkAndMigrate();
117 } catch (e) {
118 Components.utils.reportError("Migrator error: " + e);
119 }
120 }
121
122 calendars = [homeCalendar];
123 }
124
125 calendarListInitCategoryColors();
126
127 // Set up the tree view
128 var tree = document.getElementById("calendar-list-tree");
129 calendarListTreeView.tree = tree;
130 tree.view = calendarListTreeView;
131
132 calMgr.addObserver(calendarManagerObserver);
133 composite.addObserver(calendarManagerCompositeObserver);
134 branch.addObserver("calendar.", calendarManagerObserver, false);
135
136 // The calendar manager will not notify for existing calendars. Go through
137 // them all and set up manually.
138 for each (var calendar in calendars) {
139 calendarManagerObserver.initializeCalendar(calendar);
140 }
141 }
142
unloadCalendarManager
143 function unloadCalendarManager() {
144 calendarManagerObserver.unload();
145 var calMgr = getCalendarManager();
146 var composite = getCompositeCalendar();
147 var prefService = Components.classes["@mozilla.org/preferences-service;1"]
148 .getService(Components.interfaces.nsIPrefService);
149 var branch = prefService.getBranch("").QueryInterface(Components.interfaces.nsIPrefBranch2);
150
151 branch.removeObserver("calendar.", calendarManagerObserver);
152 composite.removeObserver(calendarManagerCompositeObserver);
153 calMgr.removeObserver(calendarManagerObserver);
154 }
155
156 /**
157 * Color specific functions
158 */
159 var gCachedStyleSheet;
calendarListInitCategoryColors
160 function calendarListInitCategoryColors() {
161 var calendars = getCalendarManager().getCalendars({});
162 if (!gCachedStyleSheet) {
163 var cssUri = "chrome://calendar/content/calendar-view-bindings.css";
164 gCachedStyleSheet = getStyleSheet(cssUri);
165 }
166
167 var prefService = Components.classes["@mozilla.org/preferences-service;1"]
168 .getService(Components.interfaces.nsIPrefService);
169 var categoryPrefBranch = prefService.getBranch("calendar.category.color.");
170 var categories = categoryPrefBranch.getChildList("", {});
171
172 // check category preference name syntax
173 categories = calendarConvertObsoleteColorPrefs(categoryPrefBranch, categories);
174
175 // Update all categories
176 for each (var category in categories) {
177 updateStyleSheetForObject(category, gCachedStyleSheet);
178 }
179 }
180
181 /**
182 * Remove illegally formatted category names from the array coloredCategories
183 * so they don't cause CSS errors. For each illegal colored category c, if
184 * its color preference has not yet been replaced with a converted preference
185 * with key formatStringForCSSRule(c), create the preference with the
186 * converted key and with the previous preference value, and clear the old
187 * preference. (For most users who upgrade and do not later add colors with a
188 * downgrade version, this should convert any illegal preferences once, so
189 * future runs have no illegal preferences.)
190 * @param categoryPrefBranch prefBranch for "calendar.category.color."
191 * @param coloredCategories array of preference name suffixes under the prefBranch.
192 * @return same array with each illegal name replaced with formatted name if
193 * it doesn't already exist, or simply removed from array if it does.
194 */
calendarConvertObsoleteColorPrefs
195 function calendarConvertObsoleteColorPrefs(categoryPrefBranch, coloredCategories) {
196 for (var i in coloredCategories) {
197 var category = coloredCategories[i];
198 if (category.search(/[^_0-9a-z-]/) != -1) {
199 var categoryFix = formatStringForCSSRule(category);
200 if (!categoryPrefBranch.prefHasUserValue(categoryFix)) {
201 var color = categoryPrefBranch.getCharPref(category);
202 categoryPrefBranch.setCharPref(categoryFix, color);
203 categoryPrefBranch.clearUserPref(category); // not usable
204 coloredCategories[i] = categoryFix; // replace illegal name
205 } else {
206 coloredCategories.splice(i, 1); // remove illegal name
207 }
208 }
209 }
210 return coloredCategories;
211 }
212
calendarListUpdateColor
213 function calendarListUpdateColor(aCalendar) {
214 var selectorPrefix = "treechildren::-moz-tree-cell";
215
216 var color = aCalendar.getProperty("color");
217 if (!color) {
218 return;
219 }
220 var selector = selectorPrefix + "color-" + color.substr(1);
221
222 for (var i = 0; i < gCachedStyleSheet.cssRules.length; i++) {
223 var thisrule = gCachedStyleSheet.cssRules[i];
224 if (thisrule.selectorText && thisrule.selectorText == selector) {
225 return;
226 }
227 }
228
229 var ruleString = selectorPrefix + "(color-" + color.substr(1) + ") { }";
230
231 var rule = gCachedStyleSheet
232 .insertRule(ruleString, gCachedStyleSheet.cssRules.length);
233
234 gCachedStyleSheet.cssRules[rule].style.backgroundColor = color;
235 return;
236 }
237
238 /**
239 * Calendar Tree View
240 */
241 var calendarListTreeView = {
242 mCalendarList: [],
243 tree: null,
244 treebox: null,
245 mContextElement: null,
246
cLTV_QueryInterface
247 QueryInterface: function cLTV_QueryInterface(aIID) {
248 return doQueryInterface(this, calendarListTreeView.__proto__, aIID,
249 [Components.interfaces.nsISupports,
250 Components.interfaces.nsITreeView]);
251 },
252
253 /**
254 * High-level calendar tree manipulation
255 */
256
cLTV_findIndex
257 findIndex: function cLTV_findIndex(aCalendar) {
258 for (var i = 0; i < this.mCalendarList.length; i++) {
259 if (this.mCalendarList[i].id == aCalendar.id) {
260 return i;
261 }
262 }
263 return -1;
264 },
265
cLTV_findIndexByUri
266 findIndexByUri: function cLTV_findIndexByUri(aUri) {
267 for (var i = 0; i < this.mCalendarList.length; i++) {
268 if (this.mCalendarList[i].uri.equals(aUri)) {
269 return i;
270 }
271 }
272 return -1;
273 },
274
cLTV_addCalendar
275 addCalendar: function cLTV_addCalendar(aCalendar) {
276 var composite = getCompositeCalendar();
277 this.mCalendarList.push(aCalendar);
278 calendarListUpdateColor(aCalendar);
279 this.treebox.rowCountChanged(this.mCalendarList.length - 1, 1);
280
281 if (!composite.defaultCalendar ||
282 aCalendar.id == composite.defaultCalendar.id) {
283 this.tree.view.selection.select(this.mCalendarList.length - 1);
284 }
285 },
286
cLTV_removeCalendar
287 removeCalendar: function cLTV_removeCalendar(aCalendar) {
288 var index = this.findIndex(aCalendar);
289 if (index < 0) {
290 return;
291 }
292
293 this.mCalendarList.splice(index, 1);
294 this.treebox.rowCountChanged(index, -1);
295
296 if (index == this.rowCount) {
297 index--;
298 }
299
300 this.tree.view.selection.select(index);
301 },
302
cLTV_updateCalendar
303 updateCalendar: function cLTV_updateCalendar(aCalendar) {
304 var index = this.findIndex(aCalendar);
305 this.treebox.invalidateRow(index);
306 },
307
cLTV_getCalendarFromEvent
308 getCalendarFromEvent: function cLTV_getCalendarFromEvent(event,
309 aCol,
310 aRow) {
311 if (event.clientX && event.clientY) {
312 // If we have a client point, get the row directly from the client
313 // point.
314 aRow = aRow || {};
315 this.treebox.getCellAt(event.clientX,
316 event.clientY,
317 aRow,
318 aCol || {},
319 {});
320
321 } else {
322 // The event is probably coming from a context menu oncommand
323 // handler. We saved the row and column where the context menu
324 // showed up in setupContextMenu().
325 aCol = { value: this.mContextElement.column };
326 aRow = { value: this.mContextElement.row };
327 }
328 return aRow && aRow.value > -1 && this.mCalendarList[aRow.value];
329 },
330
331 /**
332 * nsITreeView methods and properties
333 */
get_rowCount
334 get rowCount() {
335 return this.mCalendarList.length;
336 },
337
cLTV_getRowProperties
338 getRowProperties: function cLTV_getRowProperties(aRow, aProps) {},
339
cLTV_getCellProperties
340 getCellProperties: function cLTV_getCellProperties(aRow, aCol, aProps) {
341 var calendar = this.mCalendarList[aRow];
342 var composite = getCompositeCalendar();
343
344 switch (aCol.id) {
345 case "calendar-list-tree-checkbox":
346 if (composite.getCalendar(calendar.uri)) {
347 aProps.AppendElement(getAtomFromService("checked"));
348 } else {
349 aProps.AppendElement(getAtomFromService("unchecked"));
350 }
351 break;
352 case "calendar-list-tree-color":
353 // Get the calendar color
354 var color = calendar.getProperty("color");
355 color = color && color.substr(1);
356
357 // Set up the calendar color (background)
358 var bgColorProp = "color-" + (color || "default");
359 aProps.AppendElement(getAtomFromService(bgColorProp));
360
361 // Set a property to get the contrasting text color (foreground)
362 var fgColorProp = getContrastingTextColor(color || "a8c2e1");
363 aProps.AppendElement(getAtomFromService(fgColorProp));
364
365 // Set up the readonly symbol
366 if (calendar.readOnly) {
367 aProps.AppendElement(getAtomFromService("readOnly"));
368 }
369
370 break;
371 }
372 },
373
cLTV_getColumnProperties
374 getColumnProperties: function cLTV_getColumnProperties(a, aProps) {},
375
cLTV_isContainer
376 isContainer: function cLTV_isContainer(aRow) {
377 return false;
378 },
379
cLTV_isContainerOpen
380 isContainerOpen: function cLTV_isContainerOpen(aRow) {
381 return false;
382 },
383
cLTV_isContainerEmpty
384 isContainerEmpty: function cLTV_isContainerEmpty(aRow) {
385 return false;
386 },
387
cLTV_isSeparator
388 isSeparator: function cLTV_isSeparator(aRow) {
389 return false;
390 },
391
cLTV_isSorted
392 isSorted: function cLTV_isSorted(aRow) {
393 return false;
394 },
395
cLTV_canDrop
396 canDrop: function cLTV_canDrop(aRow, aOrientation) {
397 return false;
398 },
399
cLTV_drop
400 drop: function cLTV_drop(aRow, aOrientation) {},
401
cLTV_getParentIndex
402 getParentIndex: function cLTV_getParentIndex(aRow) {
403 return -1;
404 },
405
cLTV_hasNextSibling
406 hasNextSibling: function cLTV_hasNextSibling(aRow, aAfterIndex) {},
407
cLTV_getLevel
408 getLevel: function cLTV_getLevel(aRow) {
409 return 0;
410 },
411
cLTV_getImageSrc
412 getImageSrc: function cLTV_getImageSrc(aRow, aOrientation) {},
413
cLTV_getProgressMode
414 getProgressMode: function cLTV_getProgressMode(aRow, aCol) {},
415
cLTV_getCellValue
416 getCellValue: function cLTV_getCellValue(aRow, aCol) {
417 var calendar = this.mCalendarList[aRow];
418 var composite = getCompositeCalendar();
419
420 switch (aCol.id) {
421 case "calendar-list-tree-checkbox":
422 return composite.getCalendar(calendar.uri) ? "true" : "false";
423 case "calendar-list-tree-color":
424 // The value of this cell shows the calendar readonly state
425 return (calendar.readOnly ? "true" : "false");
426 }
427 return null;
428 },
429
cLTV_getCellText
430 getCellText: function cLTV_getCellText(aRow, aCol) {
431 var calendar = this.mCalendarList[aRow];
432 var composite = getCompositeCalendar();
433
434 switch (aCol.id) {
435 case "calendar-list-tree-calendar":
436 return this.mCalendarList[aRow].name;
437
438 }
439 return "";
440 },
441
cLTV_setTree
442 setTree: function cLTV_setTree(aTreeBox) {
443 this.treebox = aTreeBox;
444 },
445
cLTV_toggleOpenState
446 toggleOpenState: function cLTV_toggleOpenState(aRow) {},
447
448 cycleHeader: function cLTV_cycleHeader(aCol) { },
449
cLTV_cycleCell
450 cycleCell: function cLTV_cycleCell(aRow, aCol) {
451 var calendar = this.mCalendarList[aRow];
452 var composite = getCompositeCalendar();
453
454 switch (aCol.id) {
455 case "calendar-list-tree-checkbox":
456 if (composite.getCalendar(calendar.uri)) {
457 composite.removeCalendar(calendar.uri);
458 } else {
459 composite.addCalendar(calendar);
460 }
461 break;
462 case "calendar-list-tree-color":
463 // Clicking on the color should toggle the readonly state.
464 calendar.readOnly = !calendar.readOnly;
465 break;
466 }
467 this.treebox.invalidateRow(aRow);
468 },
469
cLTV_isEditable
470 isEditable: function cLTV_isEditable(aRow, aCol) {
471 return false;
472 },
473
cLTV_setCellValue
474 setCellValue: function cLTV_setCellValue(aRow, aCol, aValue) {
475 var calendar = this.mCalendarList[aRow];
476 var composite = getCompositeCalendar();
477
478 switch (aCol.id) {
479 case "calendar-list-tree-checkbox":
480 if (aValue == "true") {
481 composite.addCalendar(calendar);
482 } else {
483 composite.removeCalendar(calendar);
484 }
485 break;
486 case "calendar-list-tree-color":
487 calendar.readOnly = (aValue == "true");
488 break;
489 default:
490 return null;
491 }
492 return aValue;
493 },
494
cLTV_setCellText
495 setCellText: function cLTV_setCellText(aRow, aCol, aValue) {},
496
cLTV_performAction
497 performAction: function cLTV_performAction(aAction) {},
498
cLTV_performActionOnRow
499 performActionOnRow: function cLTV_performActionOnRow(aAction, aRow) {},
500
cLTV_performActionOnCell
501 performActionOnCell: function cLTV_performActionOnCell(aAction, aRow, aCol) {},
502
503 /**
504 * Calendar Tree Events
505 */
cLTV_onKeyPress
506 onKeyPress: function cLTV_onKeyPress(event) {
507 const kKE = Components.interfaces.nsIDOMKeyEvent;
508 switch (event.keyCode || event.which) {
509 case kKE.DOM_VK_DELETE:
510 promptDeleteCalendar(getSelectedCalendar());
511 break;
512 case kKE.DOM_VK_SPACE:
513 if (this.tree.currentIndex > -1 ) {
514 var cbCol =
515 this.treebox.columns
516 .getNamedColumn("calendar-list-tree-checkbox");
517 this.cycleCell(this.tree.currentIndex, cbCol);
518 }
519 break;
520 }
521 },
522
cLTV_onDoubleClick
523 onDoubleClick: function cLTV_onDoubleClick(event) {
524 var col = {};
525 var calendar = this.getCalendarFromEvent(event, col);
526 if (event.button != 0 ||
527 (col.value && col.value.id == "calendar-list-tree-checkbox")) {
528 // Only left clicks that are not on the checkbox column
529 return;
530 }
531 if (calendar) {
532 openCalendarProperties(calendar, null);
533 } else {
534 openCalendarWizard();
535 }
536 },
537
cLTV_onSelect
538 onSelect: function cLTV_onSelect(event) {
539 // The select event should only fire when an item is actually selected,
540 // therefore we can assume that getSelectedCalendar() returns a
541 // calendar.
542 var composite = getCompositeCalendar();
543 composite.defaultCalendar = getSelectedCalendar();
544 },
545
546 setupContextMenu: function cLTV_setupContextMenu(event) {
547 var col = {};
548 var row = {};
549 var calendar;
550 var calendars = getCalendarManager().getCalendars({});
551
552 if (document.popupNode.localName == "tree") {
553 // Using VK_APPS to open the context menu will target the tree
554 // itself. In that case we won't have a client point even for
555 // opening the context menu. The "target" element should then be the
556 // selected element.
557 row.value = this.tree.currentIndex;
558 col.value = this.treebox.columns
559 .getNamedColumn("calendar-list-tree-calendar");
560 calendar = this.mCalendarList[row.value];
561 } else {
562 // Using the mouse, the context menu will open on the treechildren
563 // element. Here we can use client points.
564 calendar = this.getCalendarFromEvent(event, col, row);
565 }
566
567 if (col.value && col.value.id == "calendar-list-tree-checkbox") {
568 // Don't show the context menu if the checkbox was clicked.
569 return false;
570 }
571
572 // We need to save the row to return the correct calendar in
573 // getCalendarFromEvent()
574 this.mContextElement = {
575 row: row && row.value,
576 column: col && col.value
577 };
578
579 // Only enable calendar search if there's actually the chance of finding something:
580 document.getElementById("list-calendars-context-find").setAttribute(
581 "collapsed", (getCalendarSearchService().getProviders({}).length > 0 ? "false" : "true"));
582
583 if (calendar) {
584 document.getElementById("list-calendars-context-edit")
585 .removeAttribute("disabled");
586 document.getElementById("list-calendars-context-publish")
587 .removeAttribute("disabled");
588 // Only enable the delete calendars item if there is more than one
589 // calendar. We don't want to have the last calendar deleted.
590 if (calendars.length > 1) {
591 document.getElementById("list-calendars-context-delete")
592 .removeAttribute("disabled");
593 }
594 } else {
595 document.getElementById("list-calendars-context-edit")
596 .setAttribute("disabled", "true");
597 document.getElementById("list-calendars-context-publish")
598 .setAttribute("disabled", "true");
599 document.getElementById("list-calendars-context-delete")
600 .setAttribute("disabled", "true");
601 }
602 return true;
603 }
604 };
605
606 var calendarManagerCompositeObserver = {
cMCO_QueryInterface
607 QueryInterface: function cMCO_QueryInterface(aIID) {
608 if (!aIID.equals(Components.interfaces.calICompositeObserver) &&
609 !aIID.equals(Components.interfaces.calIObserver) &&
610 !aIID.equals(Components.interfaces.nsISupports)) {
611 throw Components.results.NS_ERROR_NO_INTERFACE;
612 }
613 return this;
614 },
615
cMO_onCalendarAdded
616 onCalendarAdded: function cMO_onCalendarAdded(aCalendar) {
617 // Make sure the checkbox state is updated
618 var index = calendarListTreeView.findIndex(aCalendar);
619 calendarListTreeView.treebox.invalidateRow(index);
620 },
621
cMO_onCalendarRemoved
622 onCalendarRemoved: function cMO_onCalendarRemoved(aCalendar) {
623 // Make sure the checkbox state is updated
624 var index = calendarListTreeView.findIndex(aCalendar);
625 calendarListTreeView.treebox.invalidateRow(index);
626 },
627
cMO_onDefaultCalendarChanged
628 onDefaultCalendarChanged: function cMO_onDefaultCalendarChanged(aCalendar) {
629 },
630
631 // calIObserver. Note that each registered calendar uses this observer, not
632 // only the composite calendar.
633 onStartBatch: function cMO_onStartBatch() { },
634 onEndBatch: function cMO_onEndBatch() { },
635 onLoad: function cMO_onLoad() { },
636
637 // TODO: remove these temporary caldav exclusions when it is safe to do so
638 // needed to allow cadav refresh() to update w/o forcing visibility
639 onAddItem: function cMO_onAddItem(aItem) {
640 if (aItem.calendar.type != "caldav") {
641 ensureCalendarVisible(aItem.calendar);
642 }
643 },
644
645 onModifyItem: function cMO_onModifyItem(aNewItem, aOldItem) {
646 if (aNewItem.calendar.type != "caldav") {
647 ensureCalendarVisible(aNewItem.calendar);
648 }
649 },
650
651 onDeleteItem: function cMO_onDeleteItem(aDeletedItem) { },
652 onError: function cMO_onError(aErrNo, aMessage) { },
653
654 onPropertyChanged: function cMO_onPropertyChanged(aCalendar,
655 aName,
656 aValue,
657 aOldValue) {},
658 onPropertyDeleting: function cMO_onPropertyDeleting(aCalendar,
659 aName) {}
660 }
661
662 var calendarManagerObserver = {
663 mDefaultCalendarItem: null,
664
665 QueryInterface: function cMO_QueryInterface(aIID) {
666 if (!aIID.equals(Components.interfaces.calICalendarManagerObserver) &&
667 !aIID.equals(Components.interfaces.calIObserver) &&
668 !aIID.equals(Components.interfaces.nsIObserver) &&
669 !aIID.equals(Components.interfaces.nsISupports)) {
670 throw Components.results.NS_ERROR_NO_INTERFACE;
671 }
672 return this;
673 },
674
675 /**
676 * Set up the UI for a new calendar.
677 *
678 * @param aCalendar The calendar to add.
679 */
680 initializeCalendar: function cMO_initializeCalendar(aCalendar) {
681 calendarListTreeView.addCalendar(aCalendar);
682
683 updateStyleSheetForObject(aCalendar, gCachedStyleSheet);
684 calendarListUpdateColor(aCalendar);
685
686 // Watch the calendar for changes, to ensure its visibility when adding
687 // or changing items.
688 aCalendar.addObserver(this);
689
690 // Update the calendar commands for number of remote calendars and for
691 // more than one calendar
692 document.commandDispatcher.updateCommands("calendar_commands");
693 },
694
695 unload: function cMO_unload() {
696 var calendars = getCalendarManager().getCalendars({});
697 for each (var calendar in calendars) {
698 calendar.removeObserver(this);
699 }
700 },
701
702 setupWritableCalendars: function cMO_setupWritableCalendars() {
703 var nodes = document.getElementsByAttribute("disable-when-no-writable-calendars", "true");
704 for (var i = 0; i < nodes.length; i++) {
705 if (this.mWritableCalendars < 1) {
706 nodes[i].setAttribute("disabled", "true");
707 } else {
708 nodes[i].removeAttribute("disabled");
709 }
710 }
711 },
712
713 // calICalendarManagerObserver
714 onCalendarRegistered: function cMO_onCalendarRegistered(aCalendar) {
715 this.initializeCalendar(aCalendar);
716 var composite = getCompositeCalendar();
717 var inComposite = aCalendar.getProperty(composite.prefPrefix +
718 "-in-composite");
719 if ((inComposite === null) || inComposite) {
720 composite.addCalendar(aCalendar);
721 }
722 },
723
724 onCalendarUnregistering: function cMO_onCalendarUnregistering(aCalendar) {
725 var calendars = getCalendarManager().getCalendars({});
726
727 calendarListTreeView.removeCalendar(aCalendar);
728 aCalendar.removeObserver(this);
729
730 // Make sure the calendar is removed from the composite calendar
731 getCompositeCalendar().removeCalendar(aCalendar.uri);
732
733 // Update commands to disallow deleting the last calendar and only
734 // allowing reload remote calendars when there are remote calendars.
735 document.commandDispatcher.updateCommands("calendar_commands");
736 },
737
738 onCalendarDeleting: function cMO_onCalendarDeleting(aCalendar) {
739 },
740
741 // calIObserver. Note that each registered calendar uses this observer, not
742 // only the composite calendar.
cMO_onStartBatch
743 onStartBatch: function cMO_onStartBatch() { },
cMO_onEndBatch
744 onEndBatch: function cMO_onEndBatch() { },
cMO_onLoad
745 onLoad: function cMO_onLoad() { },
746
747 // TODO: remove these temporary caldav exclusions when it is safe to do so
748 // needed to allow cadav refresh() to update w/o forcing visibility
cMO_onAddItem
749 onAddItem: function cMO_onAddItem(aItem) {
750 if (aItem.calendar.type != "caldav") {
751 ensureCalendarVisible(aItem.calendar);
752 }
753 },
754
cMO_onModifyItem
755 onModifyItem: function cMO_onModifyItem(aNewItem, aOldItem) {
756 if (aNewItem.calendar.type != "caldav") {
757 ensureCalendarVisible(aNewItem.calendar);
758 }
759 },
760
cMO_onDeleteItem
761 onDeleteItem: function cMO_onDeleteItem(aDeletedItem) { },
cMO_onError
762 onError: function cMO_onError(aErrNo, aMessage) { },
763
cMO_onPropertyChanged
764 onPropertyChanged: function cMO_onPropertyChanged(aCalendar,
765 aName,
766 aValue,
767 aOldValue) {
768 switch (aName) {
769 case "color":
770 updateStyleSheetForObject(aCalendar, gCachedStyleSheet);
771 calendarListUpdateColor(aCalendar);
772 // Fall through, update item in any case
773 case "name":
774 calendarListTreeView.updateCalendar(aCalendar);
775 break;
776 case "readOnly":
777 calendarListTreeView.updateCalendar(aCalendar);
778 // Fall through, update commands in any cases.
779 case "requiresNetwork":
780 document.commandDispatcher.updateCommands("calendar_commands");
781 break;
782 }
783 },
784
cMO_onPropertyDeleting
785 onPropertyDeleting: function cMO_onPropertyDeleting(aCalendar,
786 aName) {
787 // Since the old value is not used directly in onPropertyChanged,
788 // but should not be the same as the value, set it to a different
789 // value.
790 this.onPropertyChanged(aCalendar, aName, null, null);
791 },
792
793 // nsIObserver
794 observe: function cMO_observe(aSubject, aTopic, aPrefName) {
795
796 switch (aPrefName) {
797 case "calendar.week.start":
798 getMinimonth().refreshDisplay(true);
799 break;
800 case "calendar.date.format":
801 var view = currentView();
802 var day = view.selectedDay;
803 if (day) {
804 // The view may not be initialized, only refresh if there is
805 // a selected day.
806 view.goToDay(day);
807 }
808
809 if (isSunbird()) {
810 refreshEventTree();
811 }
812 toDoUnifinderRefresh();
813 break;
814 case "calendar.timezone.local":
815 var subject = aSubject.QueryInterface(Components.interfaces.nsIPrefBranch2);
816 gDefaultTimezone = subject.getCharPref(aPrefName);
817
818 var view = currentView();
819 var day = view.selectedDay;
820 if (day) {
821 // The view may not be initialized, only refresh if there is
822 // a selected day.
823 view.goToDay(day);
824 }
825
826 if (isSunbird()) {
827 refreshEventTree();
828 }
829 toDoUnifinderRefresh();
830 break;
831 default :
832 break;
833 }
834
835 // Since we want to take care of all categories, this must be done
836 // extra.
837 if (aPrefName.substring(0, 24) == "calendar.category.color.") {
838 var categoryName = aPrefName.substring(24);
839 updateStyleSheetForObject(categoryName, gCachedStyleSheet);
840 }
841 }
842 };
843
844 function openCalendarSubscriptionsDialog() {
845 // the dialog will reset this to auto when it is done loading
846 window.setCursor("wait");
847
848 // open the dialog modally
849 window.openDialog("chrome://calendar/content/calendar-subscriptions-dialog.xul",
850 "_blank",
851 "chrome,titlebar,modal,resizable");
852 }
853
854 /**
855 * Calendar Offline Manager
856 */
857 var calendarOfflineManager = {
858 QueryInterface: function cOM_QueryInterface(aIID) {
859 return doQueryInterface(this, calendarOfflineManager.prototype, aIID,
860 [Components.interfaces.nsIObserver, Components.interfaces.nsISupports]);
861 },
862
863 init: function cOM_init() {
864 if (this.initialized) {
865 throw Components.results.NS_ERROR_ALREADY_INITIALIZED;
866 }
867 var os = Components.classes["@mozilla.org/observer-service;1"]
868 .getService(Components.interfaces.nsIObserverService);
869 os.addObserver(this, "network:offline-status-changed", false);
870
871 this.updateOfflineUI(!this.isOnline());
872 this.initialized = true;
873 },
874
875 uninit: function cOM_uninit() {
876 if (!this.initialized) {
877 throw Components.results.NS_ERROR_NOT_INITIALIZED;
878 }
879 var os = Components.classes["@mozilla.org/observer-service;1"]
880 .getService(Components.interfaces.nsIObserverService);
881 os.removeObserver(this, "network:offline-status-changed", false);
882 this.initialized = false;
883 },
884
885 isOnline: function cOM_isOnline() {
886 return (!getIOService().offline);
887
888 },
889
890 updateOfflineUI: function cOM_updateOfflineUI(aIsOffline) {
891 // Refresh the current view
892 currentView().goToDay(currentView().selectedDay);
893
894 // Set up disabled locks for offline
895 document.commandDispatcher.updateCommands("calendar_commands");
896 },
897
898 observe: function cOM_observe(aSubject, aTopic, aState) {
899 if (aTopic == "network:offline-status-changed") {
900 this.updateOfflineUI(aState == "offline");
901 }
902 }
903 };