1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  * This file defines a tree controller.
 27  *
 28  */
 29 
 30 /**
 31  * Creates a tree controller.
 32  * @class
 33  * This class is a base class for controllers for organizers. Those are
 34  * represented by trees, both as data and visually. This class uses the support provided by
 35  * {@link ZmOperation}. Each type of organizer has a singleton tree controller which manages all 
 36  * the tree views of that type.
 37  *
 38  * @author Conrad Damon
 39  * 
 40  * @param {constant}	type		the type of organizer we are displaying/controlling
 41  * 
 42  * @extends	ZmController
 43  */
 44 ZmTreeController = function(type) {
 45 
 46 	if (arguments.length == 0) { return; }
 47 
 48 	ZmController.call(this, null);
 49 
 50 	this.type = type;
 51 	this._opc = appCtxt.getOverviewController();
 52 	
 53 	// common listeners
 54 	this._listeners = {};
 55 	this._listeners[ZmOperation.DELETE]			            = this._deleteListener.bind(this);
 56 	this._listeners[ZmOperation.DELETE_WITHOUT_SHORTCUT]    = this._deleteListener.bind(this);
 57 	this._listeners[ZmOperation.MOVE]			            = this._moveListener.bind(this);
 58 	this._listeners[ZmOperation.EXPAND_ALL]		            = this._expandAllListener.bind(this);
 59 	this._listeners[ZmOperation.MARK_ALL_READ]	            = this._markAllReadListener.bind(this);
 60 	this._listeners[ZmOperation.SYNC]			            = this._syncListener.bind(this);
 61 	this._listeners[ZmOperation.SYNC_ALL]		            = this._syncAllListener.bind(this);
 62 	this._listeners[ZmOperation.EDIT_PROPS]		            = this._editPropsListener.bind(this);
 63 	this._listeners[ZmOperation.EMPTY_FOLDER]               = this._emptyListener.bind(this);
 64 	this._listeners[ZmOperation.FIND_SHARES]	            = this._findSharesListener.bind(this);
 65 	this._listeners[ZmOperation.OPEN_IN_TAB]                = this._openInTabListener.bind(this);
 66 
 67 	// drag-and-drop
 68 	this._dragSrc = new DwtDragSource(Dwt.DND_DROP_MOVE);
 69 	this._dragSrc.addDragListener(this._dragListener.bind(this));
 70 	this._dropTgt = new DwtDropTarget(ZmTreeController.DROP_SOURCES[type]);
 71 	this._dropTgt.addDropListener(this._dropListener.bind(this));
 72 
 73 	this._treeView = {};	// hash of tree views of this type, by overview ID
 74 	this._hideEmpty = {};	// which tree views to hide if they have no data
 75 	this._dataTree = {};	// data tree per account
 76 
 77 	this._treeSelectionShortcutDelay = ZmTreeController.TREE_SELECTION_SHORTCUT_DELAY;
 78 };
 79 
 80 ZmTreeController.prototype = new ZmController;
 81 ZmTreeController.prototype.constructor = ZmTreeController;
 82 
 83 ZmTreeController.COLOR_CLASS = {};
 84 ZmTreeController.COLOR_CLASS[ZmOrganizer.C_ORANGE]	= "OrangeBg";
 85 ZmTreeController.COLOR_CLASS[ZmOrganizer.C_BLUE]	= "BlueBg";
 86 ZmTreeController.COLOR_CLASS[ZmOrganizer.C_CYAN]	= "CyanBg";
 87 ZmTreeController.COLOR_CLASS[ZmOrganizer.C_GREEN]	= "GreenBg";
 88 ZmTreeController.COLOR_CLASS[ZmOrganizer.C_PURPLE]	= "PurpleBg";
 89 ZmTreeController.COLOR_CLASS[ZmOrganizer.C_RED]		= "RedBg";
 90 ZmTreeController.COLOR_CLASS[ZmOrganizer.C_YELLOW]	= "YellowBg";
 91 ZmTreeController.COLOR_CLASS[ZmOrganizer.C_PINK]	= "PinkBg";
 92 ZmTreeController.COLOR_CLASS[ZmOrganizer.C_GRAY]	= "Gray";	// not GrayBg so it doesn't blend in
 93 
 94 // time that selection via up/down arrow must remain on an item to trigger a search
 95 ZmTreeController.TREE_SELECTION_SHORTCUT_DELAY = 750;
 96 
 97 // valid sources for drop target for different tree controllers
 98 ZmTreeController.DROP_SOURCES = {};
 99 
100 // interval of retrying empty folder (seconds)
101 ZmTreeController.EMPTY_FOLDER_RETRY_INTERVAL = 5;
102 
103 // the maximum number of trials of empty folder
104 ZmTreeController.EMPTY_FOLDER_MAX_TRIALS = 6;
105 
106 // Abstract protected methods
107 
108 // Enables/disables operations based on the given organizer ID
109 ZmTreeController.prototype.resetOperations = function() {};
110 
111 // Returns a list of desired header action menu operations
112 ZmTreeController.prototype._getHeaderActionMenuOps = function() {};
113 
114 // Returns a list of desired action menu operations
115 ZmTreeController.prototype._getActionMenuOps = function() {};
116 
117 // Returns the dialog for organizer creation
118 ZmTreeController.prototype._getNewDialog = function() {};
119 
120 // Returns the dialog for renaming an organizer
121 ZmTreeController.prototype._getRenameDialog = function() {};
122 
123 // Method that is run when a tree item is left-clicked
124 ZmTreeController.prototype._itemClicked = function() {};
125 
126 // Method that is run when a tree item is dbl-clicked
127 ZmTreeController.prototype._itemDblClicked = function() {};
128 
129 // Handles a drop event
130 ZmTreeController.prototype._dropListener = function() {};
131 
132 // Returns an appropriate title for the "Move To" dialog
133 ZmTreeController.prototype._getMoveDialogTitle = function() {};
134 
135 /**
136  * @private
137  */
138 ZmTreeController.prototype._resetOperation =
139 function(parent, id, text, image, enabled, visible) {
140 	var op = parent && parent.getOp(id);
141 	if (!op) return;
142 
143 	if (text) op.setText(text);
144 	if (image) op.setImage(image);
145 	if (enabled != null) op.setEnabled(enabled);
146 	if (visible != null) op.setVisible(visible);
147 };
148 
149 /**
150  * @private
151  */
152 ZmTreeController.prototype._resetButtonPerSetting =
153 function(parent, op, isSupported) {
154 	var button = parent.getOp(op);
155 	if (button) {
156 		if (isSupported) {
157 			button.setVisible(true);
158 			if (appCtxt.isOffline && !appCtxt.getActiveAccount().isZimbraAccount) {
159 				button.setEnabled(false);
160 			}
161 		} else {
162 			button.setVisible(false);
163 		}
164 	}
165 };
166 
167 ZmTreeController.prototype._enableRecoverDeleted =
168 function (parent, isTrash) {
169 	op = parent.getOp(ZmOperation.RECOVER_DELETED_ITEMS);
170 	if (!op) {
171 		return;
172 	}
173 	var featureEnabled = appCtxt.get(ZmSetting.DUMPSTER_ENABLED);
174 	op.setVisible(featureEnabled && isTrash);
175 	op.setEnabled(isTrash);
176 };
177 
178 ZmTreeController.prototype._findSharesListener =
179 function(ev) {
180 	var folder = this._getActionedOrganizer(ev);
181 	var account = folder.getAccount();
182 
183 	if (appCtxt.multiAccounts && account && account.isZimbraAccount) {
184 		appCtxt.accountList.setActiveAccount(account);
185 	}
186 	var dialog = appCtxt.getShareSearchDialog();
187 	var addCallback = this._handleAddShare.bind(this);
188 	dialog.popup(folder.type, addCallback);
189 };
190 
191 ZmTreeController.prototype._handleAddShare = function () {
192 	var dialog = appCtxt.getShareSearchDialog();
193 	var shares = dialog.getShares();
194 	dialog.popdown();
195 	if (shares.length === 0) {
196 		return;
197 	}
198 
199 	AjxDispatcher.require("Share");
200 	var requests = [];
201 	for (var i = 0; i < shares.length; i++) {
202 		var share = shares[i];
203 		requests.push({
204 			_jsns: "urn:zimbraMail",
205 			link: {
206 				l: ZmOrganizer.ID_ROOT,
207 				name: share.defaultMountpointName,
208 				view: share.view,
209 				zid: share.ownerId,
210 				rid: share.folderId
211 			}
212 		});
213 	}
214 
215 	var params = {
216 		jsonObj: {
217 			BatchRequest: {
218 				_jsns: "urn:zimbra",
219 				CreateMountpointRequest: requests
220 			}
221 		},
222 		asyncMode: true
223 	};
224 	appCtxt.getAppController().sendRequest(params);
225 };
226 
227 // Opens a view of the given organizer in a search tab
228 ZmTreeController.prototype._openInTabListener = function(ev) {
229 	this._itemClicked(this._getActionedOrganizer(ev), true);
230 };
231 
232 
233 
234 
235 
236 // Public methods
237 
238 /**
239  * Returns a string representation of the object.
240  * 
241  * @return		{String}		a string representation of the object
242  */
243 ZmTreeController.prototype.toString =
244 function() {
245 	return "ZmTreeController";
246 };
247 
248 /**
249  * Displays the tree of this type.
250  *
251  * @param {Hash}	params		a hash of parameters
252  * @param	{constant}	params.overviewId		the overview ID
253  * @param	{Boolean}	params.showUnread		if <code>true</code>, unread counts will be shown
254  * @param	{Object}	params.omit				a hash of organizer IDs to ignore
255  * @param	{Object}	params.include			a hash of organizer IDs to include
256  * @param	{Boolean}	params.forceCreate		if <code>true</code>, tree view will be created
257  * @param	{String}	params.app				the app that owns the overview
258  * @param	{Boolean}	params.hideEmpty		if <code>true</code>, don't show header if there is no data
259  * @param	{Boolean}	params.noTooltips	if <code>true</code>, don't show tooltips for tree items
260  */
261 ZmTreeController.prototype.show =
262 function(params) {
263 	var id = params.overviewId;
264 	this._hideEmpty[id] = params.hideEmpty;
265 
266 	if (!this._treeView[id] || params.forceCreate) {
267 		this._treeViewCreated = false;
268 		this._treeView[id] = null;
269 		this._treeView[id] = this.getTreeView(id, true);
270 	}
271 
272 	// bug fix #24241 - for offline, zimlet tree is re-used across accounts
273 	var isMultiAccountZimlet = (appCtxt.multiAccounts && this.type == ZmOrganizer.ZIMLET);
274 	var account = isMultiAccountZimlet
275 		? appCtxt.accountList.mainAccount
276 		: (this.type == ZmOrganizer.VOICE ? id : params.account); // HACK for voice app
277 	var dataTree = this.getDataTree(account);
278 
279 	if (dataTree) {
280 		params.dataTree = dataTree;
281 		var setting = ZmOrganizer.OPEN_SETTING[this.type];
282 		params.collapsed = (!isMultiAccountZimlet && (!(!setting || (appCtxt.get(setting, null, account) !== false)))); // yikes!
283 
284 		var overview = this._opc.getOverview(id);
285 
286 		if (overview && overview.showNewButtons && this.type != ZmOrganizer.ZIMLET && this.type != ZmId.ORG_PREF_PAGE ) { 
287 			this._setupOptButton(params);
288 		}
289 
290 		this._treeView[id].set(params);
291 		this._checkTreeView(id);
292 	}
293 
294 	if (!this._treeViewCreated) {
295 		this._treeViewCreated = true;
296 		this._postSetup(id, params.account);
297 	}
298 	return this._treeView[id];
299 };
300 
301 /**
302  * Gets the tree view for the given overview.
303  *
304  * @param {constant}	overviewId	the overview ID
305  * @param {Boolean}	force			if <code>true</code>, force tree view creation
306  * @return	{ZmTreeView}		the tree view
307  */
308 ZmTreeController.prototype.getTreeView =
309 function(overviewId, force) {
310 	// TODO: What side-effects will this have in terms of the _postSetup???
311 	if (force && !this._treeView[overviewId]) {
312 		this._treeView[overviewId] = this._setup(overviewId);
313 	}
314 	return this._treeView[overviewId];
315 };
316 
317 /**
318  * Clears the tree view for the given overview.
319  *
320  * @param {constant}		overviewId		the overview ID
321  *
322  */
323 ZmTreeController.prototype.clearTreeView =
324 function(overviewId) {
325 	// TODO: remove change listener if last tree view cleared
326 	if (this._treeView[overviewId]) {
327 		this._treeView[overviewId].dispose();
328 		delete this._treeView[overviewId];
329 	}
330 };
331 
332 /**
333  * Gets the controller drop target.
334  * 
335  * @return	{DwtDropTarget}	the drop target
336  */
337 ZmTreeController.prototype.getDropTarget =
338 function() {
339 	return this._dropTgt;
340 };
341 
342 /**
343  * Gets the data tree.
344  * 
345  * @param	{ZmZimbraAccount}	account		the account
346  * @return	{Object}	the data tree
347  */
348 ZmTreeController.prototype.getDataTree =
349 function(account) {
350 	account = account || appCtxt.getActiveAccount();
351 	var dataTree = this._dataTree[account.id];
352 	if (!dataTree) {
353 		dataTree = this._dataTree[account.id] = appCtxt.getTree(this.type, account);
354 		if (dataTree) {
355 			dataTree.addChangeListener(this._getTreeChangeListener());
356 		}
357 	}
358 	return dataTree;
359 };
360 
361 /**
362  * Dispose of this controller. Removes the tree change listener.
363  * called when ZmComposeController is disposed (new window).
364  * If the change listener stayed we would get exceptions since this window will no longer exist.
365  *
366  */
367 ZmTreeController.prototype.dispose =
368 function() {
369 	var account = appCtxt.getActiveAccount();
370 	var dataTree = this._dataTree[account.id];
371 	if (!dataTree) {
372 		return;
373 	}
374 	dataTree.removeChangeListener(this._getTreeChangeListener());
375 };
376 
377 
378 
379 ZmTreeController.prototype.setVisibleIfExists =
380 function(parent, opId, visible) {
381 	var op = parent.getOp(opId);
382 	if (!op) {
383 		return;
384 	}
385 	op.setVisible(visible);
386 };
387 
388 // Private and protected methods
389 
390 /**
391  * Sets up the params for the new button in the header item
392  *
393  * @param {Hash}	params		a hash of parameters
394  * 
395  * @private
396  */
397 ZmTreeController.prototype._setupOptButton =
398 function(params) {
399 	var tooltipKey = ZmOperation.getProp(ZmOperation.OPTIONS, "tooltipKey");
400 	params.optButton = {
401 		image: ZmOperation.getProp(ZmOperation.OPTIONS, "image"),
402 		tooltip: tooltipKey ? ZmMsg[tooltipKey] : null,
403 		callback: new AjxCallback(this, this._dispOpts)
404 	};
405 };
406 
407 /**
408  * Shows options for header item
409  *
410  * @param {Hash}	params		a hash of parameters
411  * 
412  * @private
413  */
414 
415 ZmTreeController.prototype._dispOpts =
416 function(ev){
417 
418 	var treeItem = ev.dwtObj;
419 
420        var type = treeItem && treeItem.getData(ZmTreeView.KEY_TYPE);
421        if (!type) { return; }
422 
423        var actionMenu = this._getHeaderActionMenu(ev);
424        if (actionMenu) {
425 		actionMenu.popup(0, ev.docX, ev.docY);
426 	}
427 };
428 
429 ZmTreeController.prototype._getTreeChangeListener =
430 function() {
431 	if (!this._dataChangeListener) {
432 		this._dataChangeListener = appCtxt.isChildWindow ? AjxCallback.simpleClosure(this._treeChangeListener, this) : new AjxListener(this, this._treeChangeListener);
433 	}
434 	return this._dataChangeListener;
435 };
436 
437 /**
438  * Performs initialization.
439  *
440  * @param overviewId		[constant]	overview ID
441  */
442 ZmTreeController.prototype._setup =
443 function(overviewId) {
444 	var treeView = this._initializeTreeView(overviewId);
445 	if (this._opc.getOverview(overviewId).actionSupported) {
446 		this._initializeActionMenus();
447 	}
448 	return treeView;
449 };
450 
451 /**
452  * Performs any little fixups after the tree view is first created
453  * and shown.
454  *
455  * @param {constant}	overviewId		the overview ID
456  * @param {ZmZimbraAccount}	account			the current account
457  * 
458  * @private
459  */
460 ZmTreeController.prototype._postSetup =
461 function(overviewId, account) {
462 
463 	var treeView = this.getTreeView(overviewId);
464 	if (!treeView.isCheckedStyle && !ZmOrganizer.HAS_COLOR[this.type]) { return; }
465 
466 	var rootId = ZmOrganizer.getSystemId(ZmOrganizer.ID_ROOT, account);
467 	var rootTreeItem = treeView.getTreeItemById(rootId);
468 	if (!rootTreeItem) { return; }
469 	if (treeView.isCheckedStyle) {
470 		rootTreeItem.showCheckBox(false);
471 	}
472 	var treeItems = rootTreeItem.getItems();
473 	for (var i = 0; i < treeItems.length; i++) {
474 		this._fixupTreeNode(treeItems[i], null, treeView, true);
475 	}
476 };
477 
478 /**
479  * Takes care of the tree item's color and/or checkbox.
480  *
481  * @param {DwtTreeItem}	treeItem	the tree item
482  * @param {ZmOrganizer}	organizer	the organizer it represents
483  * @param {ZmTreeView}	treeView	the tree view this organizer belongs to
484  * 
485  * @private
486  */
487 ZmTreeController.prototype._fixupTreeNode =
488 function(treeItem, organizer, treeView, skipNotify) {
489 	if (treeItem._isSeparator) { return; }
490 	organizer = organizer || treeItem.getData(Dwt.KEY_OBJECT);
491 	if (organizer) {
492 		if (ZmOrganizer.HAS_COLOR[this.type]) {
493 			this._setTreeItemColor(treeItem, organizer);
494 		}
495 		if (treeView.isCheckedStyle) {
496 			if ((organizer.type == this.type && treeView.isCheckedStyle) ||
497                 organizer.nId == ZmOrganizer.ID_TRASH || organizer.nId == ZmOrganizer.ID_DRAFTS) {
498 				treeItem.setChecked(organizer.isChecked, true);
499 			} else {
500 				treeItem.showCheckBox(false);
501 				treeItem.enableSelection(true);
502 			}
503 		}
504 
505 		// set expand state per user's prefs
506 		this._expandTreeItem(treeItem, skipNotify);
507 	}
508     var treeItems = treeItem.getItems();
509     for (var i = 0; i < treeItems.length; i++) {
510         this._fixupTreeNode(treeItems[i], null, treeView, skipNotify);
511     }
512 };
513 
514 ZmTreeController.prototype._expandTreeItem =
515 function(treeItem, skipNotify) {
516     var expanded = appCtxt.get(ZmSetting.FOLDERS_EXPANDED);
517 	var folderId = treeItem.getData(Dwt.KEY_ID);
518 	var parentTi = treeItem.parent;
519 
520 	// only expand if the parent is also expanded
521 	if (expanded[folderId] &&
522 		parentTi && (parentTi instanceof DwtTreeItem) && parentTi.getExpanded())
523 	{
524 		treeItem.setExpanded(true, null, skipNotify);
525 	}
526 };
527 
528 ZmTreeController.prototype._expandTreeItems =
529 function(treeItem) {
530 	if (treeItem._isSeparator) { return; }
531 
532 	this._expandTreeItem(treeItem);
533 
534 	// recurse!
535 	var treeItems = treeItem.getItems();
536 	for (var i = 0; i < treeItems.length; i++) {
537 		this._expandTreeItems(treeItems[i]);
538 	}
539 };
540 
541 /**
542  * Sets the background color of the tree item.
543  *
544  * @param treeItem	[DwtTreeItem]		tree item
545  * @param organizer	[ZmOrganizer]		organizer it represents
546  */
547 ZmTreeController.prototype._setTreeItemColor =
548 function(treeItem, organizer) {
549 	treeItem.setImage(organizer.getIconWithColor());
550 };
551 
552 ZmTreeController.prototype._getTreeItemColorClassName =
553 function(treeItem, organizer) {
554 	if (!treeItem || !organizer) { return null; }
555 	if (organizer.isInTrash()) { return null; }
556 
557 	// a color value of 0 means DEFAULT
558 	var color = organizer.color
559 		? organizer.color
560 		: ZmOrganizer.DEFAULT_COLOR[organizer.type];
561 
562 	return (color && (color != ZmOrganizer.C_NONE))
563 		? ZmTreeController.COLOR_CLASS[color] : "";
564 };
565 
566 /**
567  * Lazily creates a tree view of this type, using options from the overview.
568  *
569  * @param {constant}	overviewId		the overview ID
570  * 
571  * @private
572  */
573 ZmTreeController.prototype._initializeTreeView =
574 function(overviewId) {
575 	var overview = this._opc.getOverview(overviewId);
576 	var params = {
577 		parent: overview,
578 		parentElement: overview.getTreeParent(this.type),
579 		overviewId: overviewId,
580 		type: this.type,
581 		headerClass: overview.headerClass,
582 		dragSrc: (overview.dndSupported ? this._dragSrc : null),
583 		dropTgt: (overview.dndSupported ? this._dropTgt : null),
584 		treeStyle: overview.treeStyle,
585 		isCheckedByDefault: overview.isCheckedByDefault,
586 		allowedTypes: this._getAllowedTypes(),
587 		allowedSubTypes: this._getAllowedSubTypes()
588 	};
589 	params.id = ZmId.getTreeId(overviewId, params.type);
590 	if (params.type && params.type.match(/TASK|ADDRBOOK|FOLDER|BRIEFCASE|CALENDAR|PREF_PAGE/) && 
591 			(!params.headerClass || params.headerClass == "overviewHeader")){
592 		params.headerClass = "FirstOverviewHeader overviewHeader";
593 	}
594 	var treeView = this._createTreeView(params);
595 	treeView.addSelectionListener(new AjxListener(this, this._treeViewListener));
596 	treeView.addTreeListener(new AjxListener(this, this._treeListener));
597 
598 	return treeView;
599 };
600 
601 /**
602  * @private
603  */
604 ZmTreeController.prototype._createTreeView =
605 function(params) {
606 	return new ZmTreeView(params);
607 };
608 
609 /**
610  * Creates up to two action menus, one for the tree view's header item, and
611  * one for the rest of the items. Note that each of these two menus is a
612  * singleton, shared among the tree views of this type.
613  * 
614  * @private
615  */
616 ZmTreeController.prototype._initializeActionMenus =
617 function() {
618 	var obj = this;
619 	var func = this._createActionMenu;
620 
621 	var ops = this._getHeaderActionMenuOps();
622 	if (!this._headerActionMenu && ops) {
623 		var args = [this._shell, ops];
624 		this._headerActionMenu = new AjxCallback(obj, func, args);
625 	}
626 	var ops = this._getActionMenuOps();
627 	if (!this._actionMenu && ops) {
628 		var args = [this._shell, ops];
629 		this._actionMenu = new AjxCallback(obj, func, args);
630 	}
631 };
632 
633 /**
634  * Instantiates the header action menu if necessary.
635  * 
636  * @private
637  */
638 ZmTreeController.prototype._getHeaderActionMenu =
639 function(ev) {
640 	if (this._headerActionMenu instanceof AjxCallback) {
641 		var callback = this._headerActionMenu;
642 		this._headerActionMenu = callback.run();
643 	}
644 	return this._headerActionMenu;
645 };
646 
647 /**
648  * Instantiates the action menu if necessary.
649  * 
650  * @private
651  */
652 ZmTreeController.prototype._getActionMenu =
653 function(ev, item) {
654     var controller = this;
655 
656     // special case - search folder. might have moved under a regular folder  
657     if (item && item.type == ZmOrganizer.SEARCH) {
658         controller = this._opc.getTreeController(ZmOrganizer.SEARCH);
659     }
660 
661 	if (controller._actionMenu instanceof AjxCallback) {
662 		var callback = controller._actionMenu;
663 		controller._actionMenu = callback.run();
664 	}
665 	return controller._actionMenu;
666 };
667 
668 /**
669  * Creates and returns an action menu, and sets its listeners.
670  *
671  * @param {DwtControl}	parent		the menu parent widget
672  * @param {Array}	menuItems		the list of menu items
673  * 
674  * @private
675  */
676 ZmTreeController.prototype._createActionMenu =
677 function(parent, menuItems) {
678 	if (!menuItems) return;
679 
680 	var map = appCtxt.getCurrentController() && appCtxt.getCurrentController().getKeyMapName();
681 	var id = map ? ("ZmActionMenu_" + map):Dwt.getNextId("ZmActionMenu_")
682 	id = (map && this.type) ? id + "_" + this.type : id;
683 	var actionMenu = new ZmActionMenu({parent:parent, menuItems:menuItems, id: id});
684 
685 	menuItems = actionMenu.opList;
686 	for (var i = 0; i < menuItems.length; i++) {
687 		var menuItem = menuItems[i];
688 		if (this._listeners[menuItem]) {
689 			actionMenu.addSelectionListener(menuItem, this._listeners[menuItem]);
690 		}
691 	}
692 	actionMenu.addPopdownListener(new AjxListener(this, this._menuPopdownActionListener));
693 
694 	return actionMenu;
695 };
696 
697 /**
698  * Determines which types of organizer may be displayed at the top level. By default,
699  * the tree shows its own type.
700  * 
701  * @private
702  */
703 ZmTreeController.prototype._getAllowedTypes =
704 function() {
705 	var types = {};
706 	types[this.type] = true;
707 	return types;
708 };
709 
710 /**
711  * Determines which types of organizer may be displayed below the top level. By default,
712  * the tree shows its own type.
713  * 
714  * @private
715  */
716 ZmTreeController.prototype._getAllowedSubTypes =
717 function() {
718 	var types = {};
719 	types[this.type] = true;
720 	return types;
721 };
722 
723 // Actions
724 
725 /**
726  * Creates a new organizer and adds it to the tree of that type.
727  *
728  * @param {Hash}	params	a hash of parameters
729  * @param {constant}	params.type		the type of organizer
730  * @param {ZmOrganizer}	params.parent	parent of the new organizer
731  * @param {String}	params.name		the name of the new organizer
732  *        
733  * @private
734  */
735 ZmTreeController.prototype._doCreate =
736 function(params) {
737 	params.type = this.type;
738 	var funcName = ZmOrganizer.CREATE_FUNC[this.type];
739 	if (funcName) {
740 		var func = eval(funcName);
741 		return func(params);
742 	}
743 };
744 
745 /**
746  * Deletes an organizer and removes it from the tree.
747  *
748  * @param {ZmOrganizer}	organizer		the organizer to delete
749  */
750 ZmTreeController.prototype._doDelete =
751 function(organizer) {
752 	organizer._delete();
753 };
754 
755 /**
756  * 
757  * @param {ZmOrganizer}	organizer		the organizer
758  * @param {int}	trialCounter		the number of trials of empty folder
759  * @param {AjxException}	ex		the exception
760  *
761  * @private
762  */
763 ZmTreeController.prototype._doEmpty =
764 function(organizer, trialCounter, ex) {
765 	var recursive = false;
766 	var timeout = ZmTreeController.EMPTY_FOLDER_RETRY_INTERVAL;
767 	var noBusyOverlay = true;
768 	if (!trialCounter) {
769 		trialCounter = 1;
770 	}
771 	var errorCallback = this._doEmptyErrorHandler.bind(this, organizer, trialCounter);
772 	organizer.empty(recursive, null, this._doEmptyHandler.bind(this, organizer), timeout, errorCallback, noBusyOverlay);
773 };
774 
775 /**
776  *
777  * @param {ZmOrganizer}	organizer		the organizer
778  * @param {int}	trialCounter		the number of trials of empty folder
779  * @param {AjxException}	ex		the exception
780  *
781  * @private
782  */
783 ZmTreeController.prototype._doEmptyErrorHandler =
784 function(organizer, trialCounter, ex) {
785 	if (ex) {
786 		if (ex.code == ZmCsfeException.SVC_ALREADY_IN_PROGRESS) {
787 			appCtxt.setStatusMsg(ZmMsg.emptyFolderAlreadyInProgress);
788 			return true;
789 		} else if(ex.code != AjxException.CANCELED) {
790 			return false;
791 		}
792 	}
793 
794 	if (trialCounter > ZmTreeController.EMPTY_FOLDER_MAX_TRIALS -1){
795 		appCtxt.setStatusMsg(ZmMsg.emptyFolderNoResponse, ZmStatusView.LEVEL_CRITICAL);
796 		return true;
797 	}
798 	trialCounter++;
799 	this._doEmpty(organizer, trialCounter);
800 };
801 
802 ZmTreeController.prototype._doEmptyHandler =
803 function(organizer) {
804 	appCtxt.setStatusMsg({msg: AjxMessageFormat.format(ZmMsg.folderEmptied, organizer.getName())});
805 	var ctlr = appCtxt.getCurrentController();
806 	if (!ctlr || !ctlr._getSearchFolderId || !ctlr.getListView) {
807 		return;
808 	}
809 	var folderId = ctlr._getSearchFolderId();
810 	if (folderId !== organizer.id) {
811 		return;
812 	}
813 	var view = ctlr.getListView();
814 	view._resetList();
815 	view._setNoResultsHtml();
816 };
817 
818 /**
819  * Renames an organizer.
820  *
821  * @param {ZmOrganizer}	organizer	the organizer to rename
822  * @param {String}	name		the new name of the organizer
823  * 
824  * @private
825  */
826 ZmTreeController.prototype._doRename =
827 function(organizer, name) {
828 	organizer.rename(name);
829 };
830 
831 /**
832  * Moves an organizer to a new folder.
833  *
834  * @param {ZmOrganizer}	organizer	the organizer to move
835  * @param {ZmFolder}	folder		the target folder
836  * 
837  * @private
838  */
839 ZmTreeController.prototype._doMove =
840 function(organizer, folder) {
841 	organizer.move(folder);
842 };
843 
844 /**
845  * Marks an organizer's items as read.
846  *
847  * @param {ZmOrganizer}	organizer	the organizer
848  * 
849  * @private
850  */
851 ZmTreeController.prototype._doMarkAllRead =
852 function(organizer) {
853 	organizer.markAllRead();
854 };
855 
856 /**
857  * Syncs an organizer to its feed (URL).
858  *
859  *  @param {ZmOrganizer}	organizer	the organizer
860  *  
861  *  @private
862  */
863 ZmTreeController.prototype._doSync =
864 function(organizer) {
865 	organizer.sync();
866 };
867 
868 // Listeners
869 
870 /**
871  * Handles left and right mouse clicks. A left click generates a selection event.
872  * If selection is supported for the overview, some action (typically a search)
873  * will be performed. A right click generates an action event, which pops up an
874  * action menu if supported.
875  *
876  * @param {DwtUiEvent}	ev		the UI event
877  * 
878  * @private
879  */
880 ZmTreeController.prototype._treeViewListener = function(ev) {
881 
882 	if (ev.detail !== DwtTree.ITEM_ACTIONED && ev.detail !== DwtTree.ITEM_SELECTED && ev.detail !== DwtTree.ITEM_DBL_CLICKED) {
883 		return;
884 	}
885 
886 	var treeItem = ev.item;
887 
888 	var type = treeItem.getData(ZmTreeView.KEY_TYPE);
889 	if (!type) {
890         return;
891     }
892 
893 	var item = treeItem.getData(Dwt.KEY_OBJECT);
894 	if (item) {
895 		this._actionedOrganizer = item;
896 		if (item.noSuchFolder) {
897 			var folderTree = appCtxt.getFolderTree();
898 			if (folderTree) {
899 				folderTree.handleDeleteNoSuchFolder(item);
900 			}
901 			return;
902 		}
903         if (item && item.type === ZmOrganizer.SEARCH) {
904             var controller = this._opc.getTreeController(ZmOrganizer.SEARCH);
905             if (controller) {
906                 controller._actionedOrganizer = item;
907                 controller._actionedOverviewId = treeItem.getData(ZmTreeView.KEY_ID);
908             }
909         }
910 	}
911 
912 	var id = treeItem.getData(Dwt.KEY_ID);
913 	var overviewId = this._actionedOverviewId = treeItem.getData(ZmTreeView.KEY_ID);
914 	var overview = this._opc.getOverview(overviewId);
915 	if (!overview) {
916         return;
917     }
918 
919 	if (ev.detail === DwtTree.ITEM_ACTIONED) {
920 		// right click
921 		if (overview.actionSupported) {
922 			var actionMenu = this.getItemActionMenu(ev, item);
923 			if (actionMenu) {
924 				this.resetOperations(actionMenu, type, id);
925 				actionMenu.popup(0, ev.docX, ev.docY);
926 			}
927 		}
928 	}
929     else if ((ev.detail === DwtTree.ITEM_SELECTED) && item) {
930 		if (appCtxt.multiAccounts && (item instanceof ZmOrganizer)) {
931 			this._handleMultiAccountItemSelection(ev, overview, treeItem, item);
932 		}
933         else {
934 			this._handleItemSelection(ev, overview, treeItem, item);
935 		}
936 	}
937     else if ((ev.detail === DwtTree.ITEM_DBL_CLICKED) && item) {
938 		this._itemDblClicked(item);
939 	}
940 };
941 
942 ZmTreeController.prototype.getItemActionMenu = function(ev, item) {
943 	var actionMenu = (item.nId == ZmOrganizer.ID_ROOT || item.isDataSource(ZmAccount.TYPE_IMAP))
944 		? this._getHeaderActionMenu(ev)
945 		: this._getActionMenu(ev, item);
946 	return actionMenu;
947 }
948 
949 /**
950  * @private
951  */
952 ZmTreeController.prototype._handleItemSelection =
953 function(ev, overview, treeItem, item) {
954 	// left click or selection via shortcut
955 	overview.itemSelected(treeItem);
956 
957 	if (ev.kbNavEvent) {
958 		Dwt.scrollIntoView(treeItem._itemDiv, overview.getHtmlElement());
959 		ZmController.noFocus = true;
960 	}
961 
962 	if (overview._treeSelectionShortcutDelayActionId) {
963 		AjxTimedAction.cancelAction(overview._treeSelectionShortcutDelayActionId);
964 	}
965 
966 	if ((overview.selectionSupported || item._showFoldersCallback) && !treeItem._isHeader) {
967 		if (ev.kbNavEvent) {
968 			// for shortcuts, process selection via Enter immediately; selection via up/down keys
969 			// is delayed (or can be disabled by setting the delay to 0)
970 			if (ev.enter || this._treeSelectionShortcutDelay) {
971 				var action = new AjxTimedAction(this, ZmTreeController.prototype._treeSelectionTimedAction, [item, overview]);
972 				overview._treeSelectionShortcutDelayActionId = AjxTimedAction.scheduleAction(action, this._treeSelectionShortcutDelay);
973 			}
974 		} else {
975 			if ((appCtxt.multiAccounts && (item instanceof ZmOrganizer)) ||
976 				(item.type == ZmOrganizer.VOICE))
977 			{
978 				appCtxt.getCurrentApp().getOverviewContainer().deselectAll(overview);
979 
980 				// set the active account based on the item clicked
981 				var account = item.account || appCtxt.accountList.mainAccount;
982 				appCtxt.accountList.setActiveAccount(account);
983 			}
984 
985 			this._itemSelected(item);
986 		}
987 	}
988 };
989 
990 /**
991  * @private
992  */
993 ZmTreeController.prototype._itemSelected =
994 function(item) {
995 	if (item && item._showFoldersCallback) {
996 		item._showFoldersCallback.run();
997 	} else {
998 		this._itemClicked(item);
999 	}
1000 
1001 };
1002 
1003 /**
1004  * Allows subclass to overload in case something needs to be done before
1005  * processing tree item selection in a multi-account environment. Otherwise,
1006  * do the normal tree item selection.
1007  * 
1008  * @private
1009  */
1010 ZmTreeController.prototype._handleMultiAccountItemSelection =
1011 function(ev, overview, treeItem, item) {
1012 	this._handleItemSelection(ev, overview, treeItem, item);
1013 };
1014 
1015 /**
1016  * @private
1017  */
1018 ZmTreeController.prototype._treeSelectionTimedAction =
1019 function(item, overview) {
1020 	if (overview._treeSelectionShortcutDelayActionId) {
1021 		AjxTimedAction.cancelAction(overview._treeSelectionShortcutDelayActionId);
1022 	}
1023 	this._itemSelected(item);
1024 };
1025 
1026 /**
1027  * Propagates a change in tree state to other trees of the same type in app overviews.
1028  * 
1029  * @param {ZmTreeEvent}	ev		a tree event
1030  * 
1031  * @private
1032  */
1033 ZmTreeController.prototype._treeListener =
1034 function(ev) {
1035 	var treeItem = ev && ev.item;
1036 	var overviewId = treeItem && treeItem._tree && treeItem._tree.overviewId;
1037     var overview = appCtxt.getOverviewController().getOverview(overviewId);
1038     var acct = overview.account;
1039     if (appCtxt.multiAccounts && acct) {
1040         appCtxt.accountList.setActiveAccount(acct);
1041     }
1042 
1043 	// persist expand/collapse state for folders
1044 	var isExpand = ev.detail == DwtTree.ITEM_EXPANDED;
1045 	var folderId = (ev.detail == DwtTree.ITEM_COLLAPSED || isExpand)
1046 		? treeItem.getData(Dwt.KEY_ID) : null;
1047 
1048 	if (folderId && !treeItem._isHeader) {
1049 		var setExpanded = appCtxt.get(ZmSetting.FOLDERS_EXPANDED, folderId) || false; //I think it's set as undefined if "false" in ZmSetting.prototype.setValue)
1050 		if (typeof(setExpanded) == "string") {//I can't figure out why it becomes a string sometimes. That's nasty.
1051 			setExpanded = (setExpanded === "true");
1052 		}
1053 		//setting in case of skipImplicit is still causing problems (the fix to bug 72590 was not good enough), since even if this "set" is not persisting,
1054 		//future ones (collapse/expand in the mail tab) would cause it to save implicitly, which is not what we want.
1055 		//so I simply do not call "set" in case of skipImplicit. Might want to change the name of this variable slightly, but not sure to what.
1056 		if (!overview.skipImplicit && setExpanded !== isExpand) { //set only if changed (ZmSetting.prototype.setValue is supposed to not send a request if no change, but it might have bugs)
1057 			appCtxt.set(ZmSetting.FOLDERS_EXPANDED, isExpand, folderId);
1058 		}
1059 
1060 		// check if any of this treeItem's children need to be expanded as well
1061 		if (isExpand) {
1062 			this._expandTreeItems(treeItem);
1063 		}
1064 	}
1065 
1066 	// only handle events that come from headers in app overviews
1067 	if (!(ev && ev.detail && overview && overview.isAppOverview && treeItem._isHeader)) { return; }
1068 
1069 	var settings = appCtxt.getSettings(acct);
1070 	var setting = settings.getSetting(ZmOrganizer.OPEN_SETTING[this.type]);
1071 	if (setting) {
1072 		setting.setValue(ev.detail == DwtTree.ITEM_EXPANDED);
1073 	}
1074 };
1075 
1076 /**
1077  * Handles changes to the underlying model. The change is propagated to
1078  * all the tree views known to this controller.
1079  *
1080  * @param {ZmEvent}	ev		a change event
1081  * 
1082  * @private
1083  */
1084 ZmTreeController.prototype._treeChangeListener =
1085 function(ev) {
1086 	this._evHandled = {};
1087 	for (var overviewId in this._treeView) {
1088 		this._changeListener(ev, this._treeView[overviewId], overviewId);
1089 	}
1090 };
1091 
1092 /**
1093  * Handles a change event for one tree view.
1094  *
1095  * @param {ZmEvent}	ev				a change event
1096  * @param {ZmTreeView}	treeView		a tree view
1097  * @param {constant}	overviewId		overview ID
1098  * 
1099  * @private
1100  */
1101 ZmTreeController.prototype._changeListener =
1102 function(ev, treeView, overviewId) {
1103 	if (this._evHandled[overviewId]) { return; }
1104 	if (!treeView.allowedTypes[ev.type] && !treeView.allowedSubTypes[ev.type]) { return; }
1105 
1106 	var organizers = ev.getDetail("organizers");
1107 	if (!organizers && ev.source) {
1108 		organizers = [ev.source];
1109 	}
1110 
1111 	// handle one organizer at a time
1112 	for (var i = 0; i < organizers.length; i++) {
1113 		var organizer = organizers[i];
1114 
1115 		var node = treeView.getTreeItemById(organizer.id);
1116 		// Note: source tree handles moves - it will have node
1117 		if (!node && (ev.event != ZmEvent.E_CREATE)) { continue; }
1118 
1119 		var fields = ev.getDetail("fields");
1120 		if (ev.event == ZmEvent.E_DELETE) {
1121 			if (organizer.nId == ZmFolder.ID_TRASH || organizer.nId == ZmFolder.ID_SPAM) {
1122 				node.setText(organizer.getName(false));	// empty Trash or Junk
1123 			} else {
1124 				node.dispose();
1125 			}
1126             this._checkTreeView(overviewId);
1127 			this._evHandled[overviewId] = true;
1128 		} else if (ev.event == ZmEvent.E_CREATE || ev.event == ZmEvent.E_MOVE) {
1129 			// for multi-account, make sure this organizer applies to the given overview
1130 			if (appCtxt.multiAccounts) {
1131 				var overview = this._opc.getOverview(overviewId);
1132 				if (overview && overview.account != organizer.getAccount()) {
1133 					continue;
1134 				}
1135 			}
1136 			var parentNode = this._getParentNode(organizer, ev, overviewId);
1137 			var idx = parentNode ? ZmTreeView.getSortIndex(parentNode, organizer, eval(ZmTreeView.COMPARE_FUNC[organizer.type])) : null;
1138 			if (parentNode && (ev.event == ZmEvent.E_CREATE)) {
1139 				// parent's tree controller should handle creates - root is shared by all folder types
1140 				var type = ((organizer.parent.nId == ZmOrganizer.ID_ROOT) || organizer.parent.isRemoteRoot()) ? ev.type : organizer.parent.type;
1141 				if (type !== this.type || !treeView._isAllowed(organizer.parent, organizer)) {
1142 					continue;
1143 				}
1144 				if (organizer.isOfflineGlobalSearch) {
1145 					appCtxt.getApp(ZmApp.MAIL).getOverviewContainer().addSearchFolder(organizer);
1146 					return;
1147 				} else {
1148 					node = this._addNew(treeView, parentNode, organizer, idx); // add to new parent
1149 				}
1150                 this.createDataSource(organizer);
1151 			} else if (ev.event == ZmEvent.E_MOVE) {
1152 				var selectedItem = treeView.getSelected();
1153 				if (AjxUtil.isArray1(selectedItem)) { //make sure this tree is not a checked style one (no idea where we have that, but see the getSelected code
1154 					selectedItem = null;
1155 				}
1156 				node.dispose();
1157 				if (parentNode) {
1158 					node = this._addNew(treeView, parentNode, organizer, idx); // add to new parent
1159 				}
1160 				//highlight the current chosen one again, in case it was moved, thus losing selection
1161 				if (!treeView.getSelected() && selectedItem) { //if item was selected but now it is not
1162 					treeView.setSelected(selectedItem.id, true, true);
1163 				}
1164 			}
1165 			if (parentNode) {
1166 				parentNode.setExpanded(true); // so that new node is visible
1167 
1168 				this._fixupTreeNode(node, organizer, treeView);
1169 			}
1170 			this._checkTreeView(overviewId);
1171 			this._evHandled[overviewId] = true;
1172 		} else if (ev.event == ZmEvent.E_MODIFY) {
1173 			if (!fields) { return; }
1174 			if (fields[ZmOrganizer.F_TOTAL] || fields[ZmOrganizer.F_SIZE] || fields[ZmOrganizer.F_UNREAD] || fields[ZmOrganizer.F_NAME]) {
1175 				node.setToolTipContent(organizer.getToolTip(true));
1176 				if (appCtxt.multiAccounts && organizer.type == ZmOrganizer.FOLDER) {
1177 					appCtxt.getApp(ZmApp.MAIL).getOverviewContainer().updateTooltip(organizer.nId);
1178 				}
1179 			}
1180 
1181 			if (fields[ZmOrganizer.F_NAME] ||
1182 				fields[ZmOrganizer.F_UNREAD] ||
1183 				fields[ZmOrganizer.F_FLAGS] ||
1184 				fields[ZmOrganizer.F_COLOR] ||
1185 				((organizer.nId == ZmFolder.ID_DRAFTS || organizer.rid == ZmFolder.ID_DRAFTS ||
1186 				  organizer.nId == ZmOrganizer.ID_OUTBOX) && fields[ZmOrganizer.F_TOTAL]))
1187 			{
1188 				this._updateOverview({
1189 					organizer:  organizer,
1190 					node:       node,
1191 					fields:     fields,
1192 					treeView:   treeView,
1193 					overviewId: overviewId,
1194 					ev:         ev
1195 				});
1196 
1197 				this._evHandled[overviewId] = true;
1198 			}
1199 		}
1200 	}
1201 };
1202 
1203 /**
1204  * Handle an organizer change by updating the tree view. For example, a name change requires sorting.
1205  *
1206  * @param   params      hash            hash of params:
1207  *
1208  *          organizer   ZmOrganizer     organizer that changed
1209  *          node        DwtTreeItem     organizer node in tree view
1210  *          fields      hash            changed fields
1211  *          treeView    ZmTreeView      tree view for this organizer type
1212  *          overviewId  string          ID of containing overview
1213  *          ev          ZmEvent         change event
1214  *
1215  * @private
1216  */
1217 ZmTreeController.prototype._updateOverview = function(params) {
1218 
1219 	var org = params.organizer,
1220 		node = params.node,
1221 		parentNode = this._getParentNode(org, params.ev, params.overviewId);
1222 
1223 	node.setText(org.getName(params.treeView._showUnread));
1224 
1225 	// If the name changed, re-sort the containing list
1226 	if (params.fields && params.fields[ZmOrganizer.F_NAME]) {
1227 		if (parentNode && (parentNode.getNumChildren() > 1)) {
1228 			var nodeSelected = node._selected;
1229 			// remove and re-insert the node (if parent has more than one child)
1230 			node.dispose();
1231 			var idx = ZmTreeView.getSortIndex(parentNode, org, eval(ZmTreeView.COMPARE_FUNC[org.type]));
1232 			node = params.treeView._addNew(parentNode, org, idx);
1233 			if (nodeSelected) {
1234 				//if it was selected, re-select it so it is highlighted. No need for notifications.
1235 				params.treeView.setSelected(org, true);
1236 			}
1237 		} else {
1238 			node.setDndText(org.getName());
1239 		}
1240 		appCtxt.getAppViewMgr().updateTitle();
1241 	}
1242 
1243 	// A folder aggregates unread status of its descendents, so propagate up parent chain
1244 	if (params.fields[ZmOrganizer.F_UNREAD]) {
1245 		var parent = org.parent;
1246 		while (parent && parentNode && parent.nId != ZmOrganizer.ID_ROOT) {
1247 			parentNode.setText(parent.getName(params.treeView._showUnread));
1248 			parentNode = this._getParentNode(parent, params.ev, params.overviewId);
1249 			parent = parent.parent;
1250 		}
1251 	}
1252 
1253 	// Miscellaneous cleanup (color, selection)
1254 	this._fixupTreeNode(node, org, params.treeView);
1255 };
1256 
1257 ZmTreeController.prototype._getParentNode = function(organizer, ev, overviewId) {
1258 
1259 	if (organizer.parent) {
1260 		// if node being moved to root, we assume new parent must be the container of its type
1261 		var type = (organizer.parent.nId == ZmOrganizer.ID_ROOT) ? ev.type : null;
1262 		return this._opc.getOverview(overviewId).getTreeItemById(organizer.parent.id, type);
1263 	}
1264 };
1265 
1266 /**
1267  * Makes a request to add a new item to the tree, returning true if the item was
1268  * actually added, or false if it was omitted.
1269  *
1270  * @param {ZmTreeView}	treeView		the tree view
1271  * @param {DwtTreeItem}	parentNode	the node under which to add the new one
1272  * @param {ZmOrganizer}	organizer		the organizer for the new node
1273  * @param {int}	idx			the position at which to add the new node
1274  * 
1275  * @private
1276  */
1277 ZmTreeController.prototype._addNew =
1278 function(treeView, parentNode, organizer, idx) {
1279 	return treeView._addNew(parentNode, organizer, idx);
1280 };
1281 
1282 /**
1283  * Pops up the appropriate "New ..." dialog.
1284  *
1285  * @param {DwtUiEvent}	ev		the UI event
1286  * @param {ZmZimbraAccount}	account	used by multi-account mailbox (optional)
1287  * 
1288  * @private
1289  */
1290 ZmTreeController.prototype._newListener =
1291 function(ev, account) {
1292 	this._pendingActionData = this._getActionedOrganizer(ev);
1293 	var newDialog = this._getNewDialog();
1294 	if (!this._newCb) {
1295 		this._newCb = new AjxCallback(this, this._newCallback);
1296 	}
1297 	if (this._pendingActionData && !appCtxt.getById(this._pendingActionData.id)) {
1298 		this._pendingActionData = appCtxt.getFolderTree(account).root;
1299 	}
1300 
1301 	if (!account && appCtxt.multiAccounts) {
1302 		var ov = this._opc.getOverview(this._actionedOverviewId);
1303 		account = ov && ov.account;
1304 	}
1305 
1306 	ZmController.showDialog(newDialog, this._newCb, this._pendingActionData, account);
1307 	newDialog.registerCallback(DwtDialog.CANCEL_BUTTON, this._clearDialog, this, newDialog);
1308 };
1309 
1310 ZmTreeController.prototype.createDataSource =
1311 function(organizer) {
1312     //override
1313 };
1314 
1315 /**
1316  * Pops up the appropriate "Rename ..." dialog.
1317  *
1318  * @param {DwtUiEvent}	ev		the UI event
1319  * 
1320  * @private
1321  */
1322 ZmTreeController.prototype._renameListener =
1323 function(ev) {
1324 	this._pendingActionData = this._getActionedOrganizer(ev);
1325 	var renameDialog = this._getRenameDialog();
1326 	if (!this._renameCb) {
1327 		this._renameCb = new AjxCallback(this, this._renameCallback);
1328 	}
1329 	ZmController.showDialog(renameDialog, this._renameCb, this._pendingActionData);
1330 	renameDialog.registerCallback(DwtDialog.CANCEL_BUTTON, this._clearDialog, this, renameDialog);
1331 };
1332 
1333 /**
1334  * Deletes an organizer.
1335  *
1336  * @param {DwtUiEvent}	ev		the UI event
1337  * 
1338  * @private
1339  */
1340 ZmTreeController.prototype._deleteListener =
1341 function(ev) {
1342 	this._doDelete(this._getActionedOrganizer(ev));
1343 };
1344 
1345 /**
1346  * @private
1347  */
1348 ZmTreeController.prototype._emptyListener =
1349 function(ev) {
1350 	this._doEmpty(this._getActionedOrganizer(ev));
1351 };
1352 
1353 /**
1354  * Moves an organizer into another folder.
1355  *
1356  * @param {DwtUiEvent}	ev		the UI event
1357  * 
1358  * @private
1359  */
1360 ZmTreeController.prototype._moveListener =
1361 function(ev) {
1362 	this._pendingActionData = this._getActionedOrganizer(ev);
1363 	var moveToDialog = appCtxt.getChooseFolderDialog();
1364 	if (!this._moveCb) {
1365 		this._moveCb = new AjxCallback(this, this._moveCallback);
1366 	}
1367 	ZmController.showDialog(moveToDialog, this._moveCb, this._getMoveParams(moveToDialog));
1368 	moveToDialog.registerCallback(DwtDialog.CANCEL_BUTTON, this._clearDialog, this, moveToDialog);
1369 };
1370 
1371 /**
1372  * @private
1373  */
1374 ZmTreeController.prototype._getMoveParams =
1375 function(dlg) {
1376 	var omit = {};
1377 	omit[ZmFolder.ID_SPAM] = true;
1378 	return {
1379 		data:			this._pendingActionData,
1380 		treeIds:		[this.type],
1381 		overviewId:		dlg.getOverviewId(appCtxt.getCurrentAppName() + '_' + this.type),
1382 		omit:			omit,
1383 		title:			AjxStringUtil.htmlEncode(this._getMoveDialogTitle()),
1384 		description:	ZmMsg.targetFolder,
1385 		appName:		ZmOrganizer.APP[this.type]
1386 	};
1387 };
1388 
1389 /**
1390  * Expands the tree below the action'd node.
1391  *
1392  * @param {DwtUiEvent}	ev		the UI event
1393  * 
1394  * @private
1395  */
1396 ZmTreeController.prototype._expandAllListener =
1397 function(ev) {
1398 	var organizer = this._getActionedOrganizer(ev);
1399 	var treeView = this.getTreeView(this._actionedOverviewId);
1400 	var ti = treeView.getTreeItemById(organizer.id);
1401 	window.duringExpandAll = true;
1402 	ti.setExpanded(true, true);
1403 	window.duringExpandAll = false;
1404 	if (window.afterExpandAllCallback) {
1405 		window.afterExpandAllCallback(); //save the explicit setting now after all was expanded - so only one request instead of many
1406 		window.afterExpandAllCallback  = null;
1407 	}
1408 };
1409 
1410 /**
1411  * Mark's an organizer's contents as read.
1412  *
1413  * @param {DwtUiEvent}	ev		the UI event
1414  * 
1415  * @private
1416  */
1417 ZmTreeController.prototype._markAllReadListener =
1418 function(ev) {
1419 	this._doMarkAllRead(this._getActionedOrganizer(ev));
1420 };
1421 
1422 /**
1423  * Syncs all the organizers to its feed (URL).
1424  *
1425  * @param {DwtUiEvent}	ev		the UI event
1426  * 
1427  * @private
1428  */
1429 ZmTreeController.prototype._syncAllListener =
1430 function(ev) {
1431 	// Loop over all the TreeViews
1432 	for (var overviewId in this._treeView) {
1433 		var treeView = this.getTreeView(overviewId);
1434 		var rootId = ZmOrganizer.getSystemId(ZmOrganizer.ID_ROOT, appCtxt.getActiveAccount());
1435 		var rootTreeItem = treeView.getTreeItemById(rootId);
1436 		var treeItems = rootTreeItem && rootTreeItem.getItems();
1437 		if (treeItems) {
1438 			for (var i = 0; i < treeItems.length; i++) {
1439 				var ti = treeItems[i];
1440 				var folder = ti && ti.getData && ti.getData(Dwt.KEY_OBJECT);
1441 				if (folder && (folder.isFeed() || folder.hasFeeds())) {
1442 					this._syncFeeds(folder);
1443 				}
1444 			}
1445 		}
1446 	}
1447 };
1448 
1449 /**
1450  * Syncs an organizer to its feed (URL).
1451  *
1452  * @param {DwtUiEvent}	ev		the UI event
1453  * 
1454  * @private
1455  */
1456 ZmTreeController.prototype._syncListener =
1457 function(ev) {
1458 	this._syncFeeds(this._getActionedOrganizer(ev));
1459 };
1460 
1461 /**
1462  * @private
1463  */
1464 ZmTreeController.prototype._syncFeeds =
1465 function(f) {
1466 	if (f.isFeed()) {
1467 		this._doSync(f);
1468 	} else if (f.hasFeeds()) {
1469 		var a = f.children.getArray();
1470 		var sz = f.children.size();
1471 		for (var i = 0; i < sz; i++) {
1472 			if (a[i].isFeed() || (a[i].hasFeeds && a[i].hasFeeds())) {
1473 				this._syncFeeds(a[i]);
1474 			}
1475 		}
1476 	}
1477 };
1478 
1479 /**
1480  * Brings up a dialog for editing organizer properties.
1481  *
1482  * @param {DwtUiEvent}	ev		the UI event
1483  * 
1484  * @private
1485  */
1486 ZmTreeController.prototype._editPropsListener = 
1487 function(ev) {
1488 	var folderPropsDialog = appCtxt.getFolderPropsDialog();
1489 	folderPropsDialog.popup(this._getActionedOrganizer(ev));
1490 };
1491 
1492 /**
1493  * Handles a drag event by setting the source data.
1494  *
1495  * @param {DwtDragEvent}	ev		a drag event
1496  * 
1497  * @private
1498  */
1499 ZmTreeController.prototype._dragListener =
1500 function(ev) {
1501 	switch (ev.action) {
1502 		case DwtDragEvent.SET_DATA:
1503 			ev.srcData = {data:ev.srcControl.getData(Dwt.KEY_OBJECT), controller:this};
1504 			break;
1505 	}
1506 };
1507 
1508 /**
1509  * Called when a dialog we opened is closed. Sets the style of the actioned
1510  * tree item from "actioned" back to its normal state.
1511  * 
1512  * @private
1513  */
1514 ZmTreeController.prototype._menuPopdownActionListener = 
1515 function() {
1516 	if (this._pendingActionData) { return; }
1517 
1518 	var treeView = this.getTreeView(this._actionedOverviewId);
1519 	if (this._actionedOrganizer && (treeView.getSelected() != this._actionedOrganizer)) {
1520 		var ti = treeView.getTreeItemById(this._actionedOrganizer.id);
1521 		if (ti) {
1522 			ti._setActioned(false);
1523 		}
1524 	}
1525 };
1526 
1527 // Callbacks
1528 
1529 /**
1530  * Called when a "New ..." dialog is submitted to create the organizer.
1531  *
1532  * @param {Hash}	params	a hash of parameters
1533  * @param {ZmOrganizer}	params.organizer	the parent organizer
1534  * @param {String}  params.name	the name of the new organizer
1535  * 
1536  * @private
1537  */
1538 ZmTreeController.prototype._newCallback =
1539 function(params) {
1540 	this._doCreate(params);
1541 	this._clearDialog(this._getNewDialog());
1542 };
1543 
1544 /**
1545  * Called when a "Rename ..." dialog is submitted to rename the organizer.
1546  *
1547  * @param {ZmOrganizer}	organizer		the organizer
1548  * @param {String}	name		the new name of the organizer
1549  * 
1550  * @private
1551  */
1552 ZmTreeController.prototype._renameCallback =
1553 function(organizer, name) {
1554 	this._doRename(organizer, name);
1555 	this._clearDialog(this._getRenameDialog());
1556 };
1557 
1558 /**
1559  * Called when a "Move To ..." dialog is submitted to move the organizer.
1560  *
1561  * @param {ZmFolder}	folder		the target folder
1562  * 
1563  * @private
1564  */
1565 ZmTreeController.prototype._moveCallback =
1566 function(folder) {
1567 	this._doMove(this._pendingActionData, folder);
1568 	this._clearDialog(appCtxt.getChooseFolderDialog());
1569 };
1570 
1571 /**
1572  * Called if a user has agreed to go ahead and delete an organizer.
1573  *
1574  * @param {ZmOrganizer}	organizer	the organizer to delete
1575  * 
1576  * @private
1577  */
1578 ZmTreeController.prototype._deleteShieldYesCallback =
1579 function(organizer) {
1580 	this._doDelete(organizer);
1581 	this._clearDialog(this._deleteShield);
1582 };
1583 
1584 /**
1585  * @private
1586  */
1587 ZmTreeController.prototype._emptyShieldYesCallback = 
1588 function(organizer) {
1589 	this._doEmpty(organizer);
1590 	this._clearDialog(this._emptyShield);
1591 };
1592 
1593 /**
1594  * Prompts user before folder is emptied.
1595  *
1596  * @param {DwtUiEvent}		ev		the UI event
1597  *
1598  * @private
1599  */
1600 
1601 ZmTreeController.prototype._getEmptyShieldWarning =
1602 function(ev) {
1603     var organizer = this._pendingActionData = this._getActionedOrganizer(ev);
1604 	var ds = this._emptyShield = appCtxt.getOkCancelMsgDialog();
1605 	ds.reset();
1606 	ds.registerCallback(DwtDialog.OK_BUTTON, this._emptyShieldYesCallback, this, organizer);
1607 	ds.registerCallback(DwtDialog.CANCEL_BUTTON, this._clearDialog, this, this._emptyShield);
1608 	var msg = (organizer.nId != ZmFolder.ID_TRASH)
1609 		? (AjxMessageFormat.format(ZmMsg.confirmEmptyFolder, organizer.getName()))
1610 		: ZmMsg.confirmEmptyTrashFolder;
1611 	ds.setMessage(msg, DwtMessageDialog.WARNING_STYLE);
1612 
1613 	var focusButtonId = (organizer.nId == ZmFolder.ID_TRASH || organizer.nId == ZmFolder.ID_SPAM) ?  DwtDialog.OK_BUTTON : DwtDialog.CANCEL_BUTTON;
1614 	ds.associateEnterWithButton(focusButtonId);
1615 	ds.popup(null, focusButtonId);
1616 
1617 	if (!(organizer.nId == ZmFolder.ID_SPAM || organizer.isInTrash())) {
1618 		var cancelButton = ds.getButton(DwtDialog.CANCEL_BUTTON);
1619 		cancelButton.focus();
1620 	}
1621 };
1622 
1623 // Miscellaneous private methods
1624 
1625 /**
1626  * Returns the organizer that's currently selected for action (via right-click).
1627  * Note: going up the object tree to find the actioned organizer will only work 
1628  * for tree item events; it won't work for action menu item events, since action
1629  * menus are children of the shell.
1630  *
1631  * @param {DwtUiEvent}	ev		the UI event
1632  * 
1633  * @private
1634  */
1635 ZmTreeController.prototype._getActionedOrganizer =
1636 function(ev) {
1637 	if (this._actionedOrganizer) {
1638 		return this._actionedOrganizer;
1639 	}
1640 		
1641 	var obj = ev.item;
1642 	while (obj) {
1643 		var data = obj.getData(Dwt.KEY_OBJECT);
1644 		if (data instanceof ZmOrganizer) {
1645 			this._actionedOrganizer = data;
1646 			return this._actionedOrganizer;
1647 		}
1648 		obj = obj.parent;
1649 	}
1650 	return null;
1651 };
1652 
1653 /**
1654  * Shows or hides the tree view. It is hidden only if there is no data, and we
1655  * have been told to hide empty tree views of this type.
1656  * 
1657  * @param {constant}	overviewId		the overview ID
1658  * 
1659  * @private
1660  */
1661 ZmTreeController.prototype._checkTreeView =
1662 function(overviewId) {
1663 	if (!overviewId || !this._treeView[overviewId]) { return; }
1664 
1665 	var account = this._opc.getOverview(overviewId).account;
1666 	var dataTree = this.getDataTree(account);
1667 	var hide = (ZmOrganizer.HIDE_EMPTY[this.type] && dataTree && (dataTree.size() == 0));
1668 	this._treeView[overviewId].setVisible(!hide);
1669 };
1670