/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // Implements a tree of folders. It shows icons depending on folder type // and other fancy styling. // This is used in the main folder pane, but also some dialogs that need // to show a nice list of folders. var { FeedUtils } = ChromeUtils.import("resource:///modules/FeedUtils.jsm"); var { FolderUtils } = ChromeUtils.import("resource:///modules/FolderUtils.jsm"); var { IOUtils } = ChromeUtils.import("resource:///modules/IOUtils.js"); var { IteratorUtils } = ChromeUtils.import("resource:///modules/iteratorUtils.jsm"); var { mailServices } = ChromeUtils.import("resource:///modules/mailServices.js"); var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.js"); var { AppConstants } = ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); if (typeof FeedMessageHandler != "object") { Services.scriptloader.loadSubScript("chrome://messenger-newsblog/content/newsblogOverlay.js"); } const kDefaultMode = "all"; /** * This file contains the controls and functions for the folder pane. * The following definitions will be useful to know: * * gFolderTreeView - the controller for the folder tree. * ftvItem - folder tree view item, representing a row in the tree * mode - folder view type, e.g., all folders, favorite folders, MRU... */ /** * An interface that needs to be implemented in order to add a new view to the * folder tree. For default behavior, it is recommended that implementers * subclass this interface instead of relying on duck typing. * * For implementation examples, see |gFolderTreeView._modes|. For how to * register this mode with |gFolderTreeView|, see * |gFolderTreeView.registerFolderTreeMode|. */ let IFolderTreeMode = { /** * Generates the folder map for this mode. * * @param aFolderTreeView The gFolderTreeView for which this mode is being * activated. * * @returns An array containing ftvItem instances representing the top-level * folders in this view. */ generateMap: function IFolderTreeMode_generateMap(aFolderTreeView) { return null; }, /** * Given an nsIMsgFolder, returns its parent in the map. The default behaviour * is to return the folder's actual parent (aFolder.parent). Folder tree modes * may decide to override it. * * If the parent isn't easily computable given just the folder, you may * consider generating the entire ftvItem tree at once and using a map from * folders to ftvItems. * * @returns an nsIMsgFolder representing the parent of the folder in the view, * or null if the folder is a top-level folder in the map. It is expected * that the returned parent will have the given folder as one of its * children. * @note This function need not guarantee that either the folder or its parent * is actually in the view. */ getParentOfFolder: function IFolderTreeMode_getParentOfFolder(aFolder) { return aFolder.parent; }, /** * Given an nsIMsgDBHdr, returns the folder it is considered to be contained * in, in this mode. This is usually just the physical folder it is contained * in (aMsgHdr.folder), but some modes may decide to override this. For * example, combined views like Smart Folders return the smart inbox for any * messages in any inbox. * * The folder returned doesn't need to be in the view. * * @returns The folder the message header is considered to be contained in, in * this mode. The returned folder may or may not actually be in the view * -- however, given a valid nsIMsgDBHdr, it is expected that a) a * non-null folder is returned, and that b) the folder that is returned * actually does contain the message header. */ getFolderForMsgHdr: function IFolderTreeMode_getFolderForMsgHdr(aMsgHdr) { return aMsgHdr.folder; }, /** * Notified when a folder is added. The default behavior is to add it as a * child of the parent item, but some views may decide to override this. For * example, combined views like Smart Folders add any new inbox as a child of * the smart inbox. * * @param aParent The parent of the folder that was added. * @param aFolder The folder that was added. */ onFolderAdded: function IFolderTreeMode_onFolderAdded(aParent, aFolder) { gFolderTreeView.addFolder(aParent, aFolder); }, /** * Notified when a folder int property is changed. * * Returns true if the event was processed inside the function and no further * default handling should be done in the caller. Otherwise false. * * @param aItem The folder with a change. * @param aProperty The changed property string. * @param aOld The old value of the property. * @param aNew The new value of the property. */ handleChangedIntProperty: function(aItem, aProperty, aOld, aNew) { return false; } }; /** * This is our controller for the folder-tree. It includes our nsITreeView * implementation, as well as other control functions. */ let gFolderTreeView = { messengerBundle: null, /** * Called when the window is initially loaded. This function initializes the * folder-pane to the view last shown before the application was closed. */ load: function ftv_load(aTree, aJSONFile) { this._treeElement = aTree; this.messengerBundle = document.getElementById("bundle_messenger"); // The folder pane can be used for other trees which may not have these // elements. if (document.getElementById("folderpane-splitter")) document.getElementById("folderpane-splitter").collapsed = false; if (document.getElementById("folderPaneBox")) document.getElementById("folderPaneBox").collapsed = false; if (aJSONFile) { // Parse our persistent-open-state json file. let data = IOUtils.loadFileToString(aJSONFile); if (data) { try { this._persistOpenMap = JSON.parse(data); } catch (x) { Cu.reportError(gFolderTreeView.messengerBundle.getFormattedString("failedToReadFile", [aJSONFile, x])); } } } // Load our data. this._rebuild(); // And actually draw the tree. aTree.view = this; gFolderStatsHelpers.init(); // Add this listener so that we can update the tree when things change. MailServices.mailSession.AddFolderListener(this, Ci.nsIFolderListener.all); }, /** * Called when the window is being torn down. Here we undo everything we did * onload. That means removing our listener and serializing our JSON. */ unload: function ftv_unload(aJSONFile) { // Remove our listener. MailServices.mailSession.RemoveFolderListener(this); if (aJSONFile) { // Write out our json file... let data = JSON.stringify(this._persistOpenMap); IOUtils.saveStringToFile(aJSONFile, data); } }, /** * Extensions can use this function to add a new mode to the folder pane. * * @param aCommonName an internal name to identify this mode. Must be unique * @param aMode An implementation of |IFolderTreeMode| for this mode. * @param aDisplayName a localized name for this mode */ registerFolderTreeMode: function ftv_registerFolderTreeMode(aCommonName, aMode, aDisplayName) { this._modeNames.push(aCommonName); this._modes[aCommonName] = aMode; this._modeDisplayNames[aCommonName] = aDisplayName; }, /** * Unregisters a previously registered mode. Since common-names must be unique * this is all that need be provided to unregister. * @param aCommonName the common-name with which the mode was previously * registered */ unregisterFolderTreeMode: function ftv_unregisterFolderTreeMode(aCommonName) { this._modeNames.splice(this._modeNames.indexOf(aCommonName), 1); delete this._modes[aCommonName]; delete this._modeDisplayNames[aCommonName]; if (this._mode == aCommonName) this.mode = kDefaultMode; }, /** * Retrieves a specific mode object * @param aCommonName the common-name with which the mode was previously * registered */ getFolderTreeMode: function ftv_getFolderTreeMode(aCommonName) { return this._modes[aCommonName]; }, /** * Called to move to the next/prev folder-mode in the list * * @param aForward whether or not we should move forward in the list */ cycleMode: function ftv_cycleMode(aForward) { let index = this._modeNames.indexOf(this.mode); let offset = aForward ? 1 : this._modeNames.length - 1; index = (index + offset) % this._modeNames.length; this.mode = this._modeNames[index]; }, /** * If the hidden pref is set, then double-clicking on a folder should open it * * @param event the double-click event */ onDoubleClick: function ftv_onDoubleClick(aEvent) { if (aEvent.button != 0 || aEvent.originalTarget.localName == "twisty" || aEvent.originalTarget.localName == "slider" || aEvent.originalTarget.localName == "scrollbarbutton") return; let row = gFolderTreeView._treeElement.treeBoxObject.getRowAt(aEvent.clientX, aEvent.clientY); let folderItem = gFolderTreeView._rowMap[row]; if (folderItem) folderItem.command(); // Don't let the double-click toggle the open state of the folder here. aEvent.stopPropagation(); }, onKeyPress(event) { if (event.keyCode == KeyEvent.DOM_VK_RETURN) { if ((AppConstants.platform == "macosx" ? event.metaKey : event.ctrlKey) && AllowOpenTabOnMiddleClick()) { FolderPaneContextMenuNewTab(event); let folderTree = document.getElementById("folderTree"); RestoreSelectionWithoutContentLoad(folderTree); } } }, getFolderAtCoords: function ftv_getFolderAtCoords(aX, aY) { let row = gFolderTreeView._treeElement.treeBoxObject.getRowAt(aX, aY); if (row in gFolderTreeView._rowMap) return gFolderTreeView._rowMap[row]._folder; return null; }, /** * A string representation for the current display-mode. Each value here must * correspond to an entry in _modes */ _mode: null, get mode() { if (!this._mode) { this._mode = this._treeElement.getAttribute("mode"); // This can happen when an extension is removed. if (!(this._mode in this._modes)) this._mode = kDefaultMode; } return this._mode; }, /** * @param aMode The final name of the mode to switch to. */ set mode(aMode) { // Ignore unknown modes. if (!(aMode in this._modes)) return; this._mode = aMode; // Store current mode and actually build the folder pane. this._treeElement.setAttribute("mode", this._mode); this._rebuild(); }, /** * Selects a given nsIMsgFolder in the tree. This function will also ensure * that the folder is actually being displayed (that is, that none of its * ancestors are collapsed. * * @param aFolder the nsIMsgFolder to select * @param [aForceSelect] Whether we should switch to the default mode to * select the folder in case we didn't find the folder in the current * view. Defaults to false. * @returns true if the folder selection was successful, false if it failed * (probably because the folder isn't in the view at all) */ selectFolder: function ftv_selectFolder(aFolder, aForceSelect = false) { // "this" inside the nested function refers to the function... // Also note that openIfNot is recursive. let tree = this; let folderTreeMode = this._modes[this._mode]; function openIfNot(aFolderToOpen) { let index = tree.getIndexOfFolder(aFolderToOpen); if (index != null) { if (!tree._rowMap[index].open) tree._toggleRow(index, false); return true; } // Not found, so open the parent. let parent = folderTreeMode.getParentOfFolder(aFolderToOpen); if (parent && openIfNot(parent)) { // Now our parent is open, so we can open ourselves. index = tree.getIndexOfFolder(aFolderToOpen); if (index != null) { tree._toggleRow(index, false); return true; } } // No way we can find the folder now. return false; } let parent = folderTreeMode.getParentOfFolder(aFolder); if (parent) openIfNot(parent); let folderIndex = tree.getIndexOfFolder(aFolder); if (folderIndex == null) { if (aForceSelect) { // Switch to the default mode. The assumption here is that the default // mode can display every folder. this.mode = kDefaultMode; // We don't want to get stuck in an infinite recursion, // so pass in false. return this.selectFolder(aFolder, false); } return false; } this.selection.select(folderIndex); this._treeElement.treeBoxObject.ensureRowIsVisible(folderIndex); return true; }, /** * Returns the index of a folder in the current display. * * @param aFolder the folder whose index should be returned. * @returns The index of the folder in the view (a number). * @note If the folder is not in the display (perhaps because one of its * anscetors is collapsed), this function returns null. */ getIndexOfFolder: function ftv_getIndexOfFolder(aFolder) { for (let [iRow, row] of this._rowMap.entries()) { if (row.id == aFolder.URI) return iRow; } return null; }, /** * Returns the folder for an index in the current display. * * @param aIndex the index for which the folder should be returned. * @note If the index is out of bounds, this function returns null. */ getFolderForIndex: function ftv_getFolderForIndex(aIndex) { if (aIndex < 0 || aIndex >= this._rowMap.length) return null; return this._rowMap[aIndex]._folder; }, /** * Returns the parent of a folder in the current view. This may be, but is not * necessarily, the actual parent of the folder (aFolder.parent). In * particular, in the smart view, special folders are usually children of the * smart folder of that kind. * * @param aFolder The folder to get the parent of. * @returns The parent of the folder, or null if the parent wasn't found. * @note This function does not guarantee that either the folder or its parent * is actually in the view. */ getParentOfFolder: function ftv_getParentOfFolder(aFolder) { return this._modes[this._mode].getParentOfFolder(aFolder); }, /** * Given an nsIMsgDBHdr, returns the folder it is considered to be contained * in, in the current mode. This is usually, but not necessarily, the actual * folder the message is in (aMsgHdr.folder). For more details, see * |IFolderTreeMode.getFolderForMsgHdr|. */ getFolderForMsgHdr: function ftv_getFolderForMsgHdr(aMsgHdr) { return this._modes[this._mode].getFolderForMsgHdr(aMsgHdr); }, /** * Returns the |ftvItem| for an index in the current display. Intended for use * by folder tree mode implementers. * * @param aIndex The index for which the ftvItem should be returned. * @note If the index is out of bounds, this function returns null. */ getFTVItemForIndex: function ftv_getFTVItemForIndex(aIndex) { return this._rowMap[aIndex]; }, /** * Returns an array of nsIMsgFolders corresponding to the current selection * in the tree */ getSelectedFolders: function ftv_getSelectedFolders() { let selection = this.selection; if (!selection) return []; let folderArray = []; let rangeCount = selection.getRangeCount(); for (let i = 0; i < rangeCount; i++) { let startIndex = {}; let endIndex = {}; selection.getRangeAt(i, startIndex, endIndex); for (let j = startIndex.value; j <= endIndex.value; j++) { if (j < this._rowMap.length) folderArray.push(this._rowMap[j]._folder); } } return folderArray; }, /** * Adds a new child |ftvItem| to the given parent |ftvItem|. Intended for use * by folder tree mode implementers. * * @param aParentItem The parent ftvItem. It is assumed that this is visible * in the view. * @param aParentIndex The index of the parent ftvItem in the view. * @param aItem The item to add. */ addChildItem: function ftv_addChildItem(aParentItem, aParentIndex, aItem) { this._addChildToView(aParentItem, aParentIndex, aItem); }, // ****************** Start of nsITreeView implementation **************** // get rowCount() { return this._rowMap.length; }, /** * drag drop interfaces */ canDrop: function ftv_canDrop(aRow, aOrientation) { let targetFolder = gFolderTreeView._rowMap[aRow]._folder; if (!targetFolder) return false; let dt = this._currentTransfer; let types = Array.from(dt.mozTypesAt(0)); if (types.includes("text/x-moz-message")) { if (aOrientation != Ci.nsITreeView.DROP_ON) return false; // Don't allow drop onto server itself. if (targetFolder.isServer) return false; // Don't allow drop into a folder that cannot take messages. if (!targetFolder.canFileMessages) return false; let messenger = Cc["@mozilla.org/messenger;1"] .createInstance(Ci.nsIMessenger); for (let i = 0; i < dt.mozItemCount; i++) { let msgHdr = messenger.msgHdrFromURI(dt.mozGetDataAt("text/x-moz-message", i)); // Don't allow drop onto original folder. if (msgHdr.folder == targetFolder) return false; } return true; } else if (types.includes("text/x-moz-folder")) { if (aOrientation != Ci.nsITreeView.DROP_ON) return false; // If cannot create subfolders then don't allow drop here. if (!targetFolder.canCreateSubfolders) return false; for (let i = 0; i < dt.mozItemCount; i++) { let folder = dt.mozGetDataAt("text/x-moz-folder", i) .QueryInterface(Ci.nsIMsgFolder); // Don't allow to drop on itself. if (targetFolder == folder) return false; // Don't copy within same server. if ((folder.server == targetFolder.server) && (dt.dropEffect == 'copy')) return false; // Don't allow immediate child to be dropped onto its parent. if (targetFolder == folder.parent) return false; // Don't allow dragging of virtual folders across accounts. if ((folder.flags & Ci.nsMsgFolderFlags.Virtual) && folder.server != targetFolder.server) return false; // Don't allow parent to be dropped on its ancestors. if (folder.isAncestorOf(targetFolder)) return false; // If there is a folder that can't be renamed, don't allow it to be // dropped if it is not to "Local Folders" or is to the same account. if (!folder.canRename && (targetFolder.server.type != "none" || folder.server == targetFolder.server)) return false; } return true; } else if (types.includes("text/x-moz-newsfolder")) { // Don't allow dragging onto element. if (aOrientation == Ci.nsITreeView.DROP_ON) return false; // Don't allow drop onto server itself. if (targetFolder.isServer) return false; for (let i = 0; i < dt.mozItemCount; i++) { let folder = dt.mozGetDataAt("text/x-moz-newsfolder", i) .QueryInterface(Ci.nsIMsgFolder); // Don't allow dragging newsgroup to other account. if (targetFolder.rootFolder != folder.rootFolder) return false; // Don't allow dragging newsgroup to before/after itself. if (targetFolder == folder) return false; // Don't allow dragging newsgroup to before item after or // after item before. let row = aRow + aOrientation; if (row in gFolderTreeView._rowMap && (gFolderTreeView._rowMap[row]._folder == folder)) return false; } return true; } // Allow subscribing to feeds by dragging an url to a feed account. else if (targetFolder.server.type == "rss" && dt.mozItemCount == 1) return FeedUtils.getFeedUriFromDataTransfer(dt) ? true : false; else if (types.includes("application/x-moz-file")) { if (aOrientation != Ci.nsITreeView.DROP_ON) return false; // Don't allow drop onto server itself. if (targetFolder.isServer) return false; // Don't allow drop into a folder that cannot take messages. if (!targetFolder.canFileMessages) return false; for (let i = 0; i < dt.mozItemCount; i++) { let extFile = dt.mozGetDataAt("application/x-moz-file", i); if (!extFile) { continue; } return extFile.QueryInterface(Ci.nsIFile).isFile(); } } return false; }, drop: function ftv_drop(aRow, aOrientation) { let targetFolder = gFolderTreeView._rowMap[aRow]._folder; let dt = this._currentTransfer; let count = dt.mozItemCount; let cs = MailServices.copy; // This is a potential rss feed. A link image as well as link text url // should be handled; try to extract a url from non moz apps as well. let feedUri = targetFolder.server.type == "rss" && count == 1 ? FeedUtils.getFeedUriFromDataTransfer(dt) : null; // We only support drag of a single flavor at a time. let types = Array.from(dt.mozTypesAt(0)); if (types.includes("text/x-moz-folder")) { for (let i = 0; i < count; i++) { let folder = dt.mozGetDataAt("text/x-moz-folder", i) .QueryInterface(Ci.nsIMsgFolder); cs.copyFolders(folder, targetFolder, (folder.server == targetFolder.server), null, msgWindow); } } else if (types.includes("text/x-moz-newsfolder")) { // Start by getting folders into order. let folders = new Array; for (let i = 0; i < count; i++) { let folder = dt.mozGetDataAt("text/x-moz-newsfolder", i) .QueryInterface(Ci.nsIMsgFolder); folders[this.getIndexOfFolder(folder)] = folder; } let newsFolder = targetFolder.rootFolder .QueryInterface(Ci.nsIMsgNewsFolder); // When moving down, want to insert first one last. // When moving up, want to insert first one first. let i = (aOrientation == 1) ? folders.length - 1 : 0; while (i >= 0 && i < folders.length) { let folder = folders[i]; if (folder) { newsFolder.moveFolder(folder, targetFolder, aOrientation); this.selection.toggleSelect(this.getIndexOfFolder(folder)); } i -= aOrientation; } } else if (types.includes("text/x-moz-message")) { let array = Cc["@mozilla.org/array;1"] .createInstance(Ci.nsIMutableArray); let sourceFolder; let messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger); for (let i = 0; i < count; i++) { let msgHdr = messenger.msgHdrFromURI(dt.mozGetDataAt("text/x-moz-message", i)); if (!i) sourceFolder = msgHdr.folder; array.appendElement(msgHdr); } let isMove = Cc["@mozilla.org/widget/dragservice;1"] .getService(Ci.nsIDragService).getCurrentSession() .dragAction == Ci.nsIDragService.DRAGDROP_ACTION_MOVE; let isNews = sourceFolder.flags & Ci.nsMsgFolderFlags.Newsgroup; if (!sourceFolder.canDeleteMessages || isNews) isMove = false; Services.prefs.setCharPref("mail.last_msg_movecopy_target_uri", targetFolder.URI); Services.prefs.setBoolPref("mail.last_msg_movecopy_was_move", isMove); // ### ugh, so this won't work with cross-folder views. We would // really need to partition the messages by folder. cs.copyMessages(sourceFolder, array, targetFolder, isMove, null, msgWindow, true); } else if (feedUri) { Cc["@mozilla.org/newsblog-feed-downloader;1"] .getService(Ci.nsINewsBlogFeedDownloader) .subscribeToFeed(feedUri.spec, targetFolder, msgWindow); } else if (types.includes("application/x-moz-file")) { for (let i = 0; i < count; i++) { let extFile = dt.mozGetDataAt("application/x-moz-file", i) .QueryInterface(Ci.nsIFile); if (extFile.isFile()) { let len = extFile.leafName.length; if (len > 4 && extFile.leafName.toLowerCase().endsWith(".eml")) cs.copyFileMessage(extFile, targetFolder, null, false, 1, "", null, msgWindow); } } } }, _onDragStart: function ftv_dragStart(aEvent) { // Ugh, this is ugly but necessary. let view = gFolderTreeView; if (aEvent.originalTarget.localName != "treechildren") return; let folders = view.getSelectedFolders(); folders = folders.filter(function(f) { return !f.isServer; }); for (let i in folders) { let flavor = folders[i].server.type == "nntp" ? "text/x-moz-newsfolder" : "text/x-moz-folder"; aEvent.dataTransfer.mozSetDataAt(flavor, folders[i], i); } aEvent.dataTransfer.effectAllowed = "copyMove"; aEvent.dataTransfer.addElement(aEvent.originalTarget); return; }, _onDragOver: function ftv_onDragOver(aEvent) { this._currentTransfer = aEvent.dataTransfer; }, _onDragDrop: function ftv_onDragDrop(aEvent) { this._currentTransfer = aEvent.dataTransfer; }, /** * CSS files will cue off of these. Note that we reach into the rowMap's * items so that custom data-displays can define their own properties */ getCellProperties: function ftv_getCellProperties(aRow, aCol) { return this._rowMap[aRow].getProperties(aCol); }, /** * The actual text to display in the tree */ getCellText: function ftv_getCellText(aRow, aCol) { if ((aCol.id == "folderNameCol") || (aCol.id == "folderUnreadCol") || (aCol.id == "folderTotalCol") || (aCol.id == "folderSizeCol")) return this._rowMap[aRow].getText(aCol.id); return ""; }, /** * The ftvItems take care of assigning this when created. */ getLevel: function ftv_getLevel(aIndex) { return this._rowMap[aIndex].level; }, /** * The ftvItems take care of assigning this when building children lists */ getServerNameAdded: function ftv_getServerNameAdded(aIndex) { return this._rowMap[aIndex].addServerName; }, /** * This is easy since the ftv items assigned the _parent property when making * the child lists */ getParentIndex: function ftv_getParentIndex(aIndex) { return this._rowMap.indexOf(this._rowMap[aIndex]._parent); }, /** * This is duplicative for our normal ftv views, but custom data-displays may * want to do something special here */ getRowProperties: function ftv_getRowProperties(aRow) { return this._rowMap[aRow].getProperties(); }, /** * Check whether there are any more rows with our level before the next row * at our parent's level */ hasNextSibling: function ftv_hasNextSibling(aIndex, aNextIndex) { var currentLevel = this._rowMap[aIndex].level; for (var i = aNextIndex + 1; i < this._rowMap.length; i++) { if (this._rowMap[i].level == currentLevel) return true; if (this._rowMap[i].level < currentLevel) return false; } return false; }, /** * All folders are containers, so we can drag drop messages to them. */ isContainer: function ftv_isContainer(aIndex) { return true; }, isContainerEmpty: function ftv_isContainerEmpty(aIndex) { // If the folder has no children, the container is empty. return !this._rowMap[aIndex].children.length; }, /** * Just look at the ftvItem here */ isContainerOpen: function ftv_isContainerOpen(aIndex) { return this._rowMap[aIndex].open; }, getSummarizedCounts: function(aIndex, aColName) { return this._rowMap[aIndex]._summarizedCounts.get(aColName); }, isEditable: function ftv_isEditable(aRow, aCol) { // We don't support editing rows in the tree yet. We may want to later as // an easier way to rename folders. return false; }, isSeparator: function ftv_isSeparator(aIndex) { // There are no separators in our trees. return false; }, isSorted: function ftv_isSorted() { // We do our own customized sorting. return false; }, setTree: function ftv_setTree(aTree) { this._tree = aTree; }, /** * Opens or closes a folder with children. The logic here is a bit hairy, so * be very careful about changing anything. */ toggleOpenState: function ftv_toggleOpenState(aIndex) { this._toggleRow(aIndex, true); }, recursivelyAddToMap: function ftv_recursivelyAddToMap(aChild, aNewIndex) { // When we add sub-children, we're going to need to increase our index // for the next add item at our own level. let count = 0; if (aChild.children.length && aChild.open) { for (let [i, child] of Array.from(this._rowMap[aNewIndex].children).entries()) { count++; let index = Number(aNewIndex) + Number(i) + 1; this._rowMap.splice(index, 0, child); let kidsAdded = this.recursivelyAddToMap(child, index); count += kidsAdded; // Somehow the aNewIndex turns into a string without this. aNewIndex = Number(aNewIndex) + kidsAdded; } } return count; }, _toggleRow: function toggleRow(aIndex, aExpandServer) { // Ok, this is a bit tricky. this._rowMap[aIndex].open = !this._rowMap[aIndex].open; if (!this._rowMap[aIndex].open) { // We're closing the current container. Remove the children. // Note that we can't simply splice out children.length, because some of // them might have children too. Find out how many items we're actually // going to splice. let count = 0; let i = aIndex + 1; let row = this._rowMap[i]; while (row && row.level > this._rowMap[aIndex].level) { count++; row = this._rowMap[++i]; } this._rowMap.splice(aIndex + 1, count); // Remove us from the persist map. this._persistItemClosed(this._rowMap[aIndex].id); // Notify the tree of changes. if (this._tree) { this._tree.rowCountChanged(aIndex + 1, (-1) * count); this._tree.invalidateRow(aIndex); } } else { // We're opening the container. Add the children to our map. // Note that these children may have been open when we were last closed, // and if they are, we also have to add those grandchildren to the map. let oldCount = this._rowMap.length; this.recursivelyAddToMap(this._rowMap[aIndex], aIndex); // Add this folder to the persist map. this._persistItemOpen(this._rowMap[aIndex].id); // Notify the tree of changes. if (this._tree) { this._tree.rowCountChanged(aIndex + 1, this._rowMap.length - oldCount); this._tree.invalidateRow(aIndex); } if (this._treeElement.getAttribute("simplelist") == "true") return; // If this was a server that was expanded, let it update its counts. let folder = this._rowMap[aIndex]._folder; if (aExpandServer) { if (folder.isServer) folder.server.performExpand(msgWindow); else if (folder instanceof Ci.nsIMsgImapMailFolder) folder.performExpand(msgWindow); } } }, // We don't implement any of these at the moment. performAction: function ftv_performAction(aAction) {}, performActionOnCell: function ftv_performActionOnCell(aAction, aRow, aCol) {}, performActionOnRow: function ftv_performActionOnRow(aAction, aRow) {}, selectionChanged: function ftv_selectionChanged() {}, setCellText: function ftv_setCellText(aRow, aCol, aValue) {}, setCellValue: function ftv_setCellValue(aRow, aCol, aValue) {}, getCellValue: function ftv_getCellValue(aRow, aCol) {}, getColumnProperties: function ftv_getColumnProperties(aCol) { return ""; }, getImageSrc: function ftv_getImageSrc(aRow, aCol) {}, getProgressMode: function ftv_getProgressMode(aRow, aCol) {}, cycleCell: function ftv_cycleCell(aRow, aCol) {}, cycleHeader: function ftv_cycleHeader(aCol) {}, // ****************** End of nsITreeView implementation **************** // // // WARNING: Everything below this point is considered private. Touch at your // own risk. /** * This is an array of all possible modes for the folder tree. You should not * modify this directly, but rather use registerFolderTreeMode. * * Internally each mode is defined separately. But in the UI we currently * expose only the "base" name (see baseMode()) of the mode plus a * "Compact view" option. The internal name of the mode to use is then * constructed from the base name and "_compact" suffix if compact view is * selected. See bug 978592. */ _modeNames: ["all", "unread", "unread_compact", "favorite", "favorite_compact", "recent_compact"], _modeDisplayNames: {}, /** * This is a javascript map of which folders we had open, so that we can * persist their state over-time. It is designed to be used as a JSON object. */ _persistOpenMap: {}, _notPersistedModes: ["unread", "unread_compact", "favorite", "favorite_compact", "recent_compact"], /** * Iterate over the persistent list and open the items (folders) stored in it. */ _restoreOpenStates: function ftv__persistOpenStates() { let mode = this.mode; // Remove any saved state of modes where open state should not be persisted. // This is mostly for migration from older profiles that may have the info // stored. if (this._notPersistedModes.includes(mode)) { delete this._persistOpenMap[mode]; } let curLevel = 0; let tree = this; let map = tree._persistOpenMap[mode]; // may be undefined function openLevel() { let goOn = false; // We can't use a js iterator because we're changing the array as we go. // So fallback on old trick of going backwards from the end, which // doesn't care when you add things at the end. for (let i = tree._rowMap.length - 1; i >= 0; i--) { let row = tree._rowMap[i]; if (row.level != curLevel) continue; // The initial state of all rows is closed, // so toggle those we want open. if (!map || map.includes(row.id)) { tree._toggleRow(i, false); goOn = true; } } // If we opened up any new kids, we need to check their level as well. curLevel++; if (goOn) openLevel(); } openLevel(); }, /** * Remove the item from the persistent list, meaning the item should * be persisted as closed in the tree. * * @param aItemId The URI of the folder item. */ _persistItemClosed: function ftv_unpersistItem(aItemId) { let mode = this.mode; if (this._notPersistedModes.includes(mode)) return; // If the whole mode is not in the map yet, // we can silently ignore the folder removal. if (!this._persistOpenMap[mode]) return; let persistMapIndex = this._persistOpenMap[mode].indexOf(aItemId); if (persistMapIndex != -1) this._persistOpenMap[mode].splice(persistMapIndex, 1); }, /** * Add the item from the persistent list, meaning the item should * be persisted as open (expanded) in the tree. * * @param aItemId The URI of the folder item. */ _persistItemOpen: function ftv_persistItem(aItemId) { let mode = this.mode; if (this._notPersistedModes.includes(mode)) return; if (!this._persistOpenMap[mode]) this._persistOpenMap[mode] = []; if (!this._persistOpenMap[mode].includes(aItemId)) this._persistOpenMap[mode].push(aItemId); }, _tree: null, selection: null, /** * An array of ftvItems, where each item corresponds to a row in the tree */ _rowMap: null, /** * Completely discards the current tree and rebuilds it based on current * settings */ _rebuild: function ftv__rebuild() { let newRowMap; try { newRowMap = this._modes[this.mode].generateMap(this); } catch(ex) { Services.console.logStringMessage("generator " + this.mode + " failed with exception: " + ex); this.mode = kDefaultMode; newRowMap = this._modes[this.mode].generateMap(this); } let selectedFolders = this.getSelectedFolders(); if (this.selection) this.selection.clearSelection(); // There's a chance the call to the map generator altered this._rowMap, so // evaluate oldCount after calling it rather than before. let oldCount = this._rowMap ? this._rowMap.length : null; this._rowMap = newRowMap; this._treeElement.dispatchEvent(new Event("mapRebuild", { bubbles: true, cancelable: false })); if (this._tree) { if (oldCount !== null) this._tree.rowCountChanged(0, this._rowMap.length - oldCount); this._tree.invalidate(); } this._restoreOpenStates(); // Restore selection. for (let folder of selectedFolders) { if (folder) { let index = this.getIndexOfFolder(folder); if (index != null) this.selection.toggleSelect(index); } } }, _sortedAccounts: function ftv_getSortedAccounts() { let accounts = FolderUtils.allAccountsSorted(true); // Don't show deferred pop accounts. accounts = accounts.filter(function isNotDeferred(a) { let server = a.incomingServer; return !(server instanceof Ci.nsIPop3IncomingServer && server.deferredToAccount); }); return accounts; }, /** * Contains the set of modes registered with the folder tree, initially those * included by default. This is a map from names of modes to their * implementations of |IFolderTreeMode|. */ _modes: { /** * The all mode returns all folders, arranged in a hierarchy */ all: { __proto__: IFolderTreeMode, generateMap: function(ftv) { let accounts = gFolderTreeView._sortedAccounts(); // Force each root folder to do its local subfolder discovery. MailUtils.discoverFolders(); return accounts.map(acct => new ftvItem(acct.incomingServer.rootFolder)); } }, /** * The unread mode returns all folders that are not root-folders and that * have unread items. Also always keep the currently selected folder * so it doesn't disappear under the user. * It also includes parent folders of the Unread folders so the hierarchy * shown. */ unread: { __proto__: IFolderTreeMode, generateMap: function(ftv) { let filterUnread = function filterUnread(aFolder) { let currentFolder = gFolderTreeView.getSelectedFolders()[0]; return ((aFolder.getNumUnread(true) > 0) || (aFolder == currentFolder)); } let accounts = gFolderTreeView._sortedAccounts(); // Force each root folder to do its local subfolder discovery. MailUtils.discoverFolders(); let unreadRootFolders = []; for (let acct of accounts) { let rootFolder = acct.incomingServer.rootFolder; // Add rootFolders of accounts that contain at least one Favorite // folder. if (rootFolder.getNumUnread(true) > 0) unreadRootFolders.push(new ftvItem(rootFolder, filterUnread)); } return unreadRootFolders; }, handleChangedIntProperty: function(aItem, aProperty, aOld, aNew) { // We want to rebuild only if we have a newly unread folder // and we didn't already have the folder. if (aProperty == "TotalUnreadMessages" && aOld == 0 && aNew > 0 && gFolderTreeView.getIndexOfFolder(aItem) == null) { gFolderTreeView._rebuild(); return true; } return false; } }, /** * A variant of the 'unread' mode above. This does not include the parent * folders and the unread folders are shown in a flat list with no * hierarchy. */ unread_compact: { __proto__: IFolderTreeMode, generateMap: function(ftv) { let map = []; let currentFolder = gFolderTreeView.getSelectedFolders()[0]; for (let folder of ftv._enumerateFolders) { if ((!folder.isServer && folder.getNumUnread(false) > 0) || (folder == currentFolder)) map.push(new ftvItem(folder)); } // There are no children in this view! for (let folder of map) { folder.__defineGetter__("children", () => []); folder.addServerName = true; } sortFolderItems(map); return map; }, getParentOfFolder: function(aFolder) { // This is a flat view, so no folders have parents. return null; }, handleChangedIntProperty: function(aItem, aProperty, aOld, aNew) { // We want to rebuild only if we have a newly unread folder // and we didn't already have the folder. if (aProperty == "TotalUnreadMessages" && aOld == 0 && aNew > 0 && gFolderTreeView.getIndexOfFolder(aItem) == null) { gFolderTreeView._rebuild(); return true; } return false; } }, /** * The favorites mode returns all folders whose flags are set to include * the favorite flag. * It also includes parent folders of the Unread folders so the hierarchy * shown. */ favorite: { __proto__: IFolderTreeMode, generateMap: function(ftv) { let accounts = gFolderTreeView._sortedAccounts(); // Force each root folder to do its local subfolder discovery. MailUtils.discoverFolders(); let favRootFolders = []; let filterFavorite = function filterFavorite(aFolder) { return aFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Favorite) != null; } for (let acct of accounts) { let rootFolder = acct.incomingServer.rootFolder; // Add rootFolders of accounts that contain at least one Favorite folder. if (filterFavorite(rootFolder)) favRootFolders.push(new ftvItem(rootFolder, filterFavorite)); } return favRootFolders; }, handleChangedIntProperty: function(aItem, aProperty, aOld, aNew) { // We want to rebuild if the favorite status of a folder changed. if (aProperty == "FolderFlag" && ((aOld & Ci.nsMsgFolderFlags.Favorite) != (aNew & Ci.nsMsgFolderFlags.Favorite))) { gFolderTreeView._rebuild(); return true; } return false; } }, /** * A variant of the 'favorite' mode above. This does not include the parent * folders and the unread folders are shown in a compact list with no * hierarchy. */ favorite_compact: { __proto__: IFolderTreeMode, generateMap: function(ftv) { let faves = []; for (let folder of ftv._enumerateFolders) { if (folder.flags & Ci.nsMsgFolderFlags.Favorite) faves.push(new ftvItem(folder)); } // We want to display the account name alongside folders that have // duplicated folder names. let uniqueNames = new Set(); // set of folder names seen at least once let dupeNames = new Set(); // set of folders seen at least twice for (let item of faves) { let name = item._folder.abbreviatedName.toLocaleLowerCase(); if (uniqueNames.has(name)) { if (!dupeNames.has(name)) dupeNames.add(name); } else { uniqueNames.add(name); } } // There are no children in this view! for (let item of faves) { let name = item._folder.abbreviatedName.toLocaleLowerCase(); item.__defineGetter__("children", () => []); item.addServerName = dupeNames.has(name); } sortFolderItems(faves); return faves; }, getParentOfFolder: function(aFolder) { // This is a flat view, so no folders have parents. return null; }, handleChangedIntProperty: function(aItem, aProperty, aOld, aNew) { // We want to rebuild if the favorite status of a folder changed. if (aProperty == "FolderFlag" && ((aOld & Ci.nsMsgFolderFlags.Favorite) != (aNew & Ci.nsMsgFolderFlags.Favorite))) { gFolderTreeView._rebuild(); return true; } return false; } }, /** * The recent mode is a flat view of the 15 most recently used folders */ recent_compact: { __proto__: IFolderTreeMode, generateMap: function(ftv) { const MAXRECENT = 15; // Get 15 (MAXRECENT) most recently accessed folders. let recentFolders = FolderUtils.getMostRecentFolders( ftv._enumerateFolders, MAXRECENT, "MRUTime", null ); // Sort the folder names alphabetically. recentFolders.sort(function rf_sort(a, b){ let aLabel = a.prettyName; let bLabel = b.prettyName; if (aLabel == bLabel) { aLabel = a.server.prettyName; bLabel = b.server.prettyName; } return FolderUtils.folderNameCompare(aLabel, bLabel); }); let items = recentFolders.map(f => new ftvItem(f)); // There are no children in this view! // And we want to display the account name to distinguish folders w/ // the same name. for (let folder of items) { folder.__defineGetter__("children", () => []); folder.addServerName = true; } return items; }, getParentOfFolder: function(aFolder) { // This is a flat view, so no folders have parents. return null; } } }, /** * This is a helper attribute that simply returns a flat list of all folders */ get _enumerateFolders() { let folders = []; for (let server of fixIterator(MailServices.accounts.allServers, Ci.nsIMsgIncomingServer)) { // Skip deferred accounts. if (server instanceof Ci.nsIPop3IncomingServer && server.deferredToAccount) continue; let rootFolder = server.rootFolder; folders.push(rootFolder); this.addSubFolders(rootFolder, folders); } return folders; }, /** * This is a recursive function to add all subfolders to the array. It * assumes that the passed in folder itself has already been added. * * @param aFolder the folder whose subfolders should be added * @param folders the array to add the folders to. */ addSubFolders : function ftv_addSubFolders (folder, folders) { for (let f of fixIterator(folder.subFolders, Ci.nsIMsgFolder)) { folders.push(f); this.addSubFolders(f, folders); } }, /** * This updates the rowmap and invalidates the right row(s) in the tree */ _addChildToView: function ftl_addChildToView(aParent, aParentIndex, aNewChild) { if (aParent.open) { let newChildIndex; let newChildNum = aParent._children.indexOf(aNewChild); // Only child - go right after our parent. if (newChildNum == 0) { newChildIndex = Number(aParentIndex) + 1 } // If we're not the last child, insert ourselves before the next child. else if (newChildNum < aParent._children.length - 1) { newChildIndex = this.getIndexOfFolder(aParent._children[Number(newChildNum) + 1]._folder); } // Otherwise, go after the last child. else { let lastChild = aParent._children[newChildNum - 1]; let lastChildIndex = this.getIndexOfFolder(lastChild._folder); newChildIndex = Number(lastChildIndex) + 1; while (newChildIndex < this.rowCount && this._rowMap[newChildIndex].level > this._rowMap[lastChildIndex].level) newChildIndex++; } this._rowMap.splice(newChildIndex, 0, aNewChild); this._tree.rowCountChanged(newChildIndex, 1); } else { this._tree.invalidateRow(aParentIndex); } }, /** * This is our implementation of nsIMsgFolderListener to watch for changes */ onFolderAdded: function ftl_add(aParentItem, aItem) { // Ignore this item if it's not a folder, or we knew about it. if (this.getIndexOfFolder(aItem) != null) return; // If no parent, this is an account, so let's rebuild. if (!aParentItem) { if (!aItem.server.hidden) // Ignore hidden server items. this._rebuild(); return; } this._modes[this._mode].onFolderAdded( aParentItem.QueryInterface(Ci.nsIMsgFolder), aItem); }, onMessageAdded: function(parentFolder, msg) {}, addFolder: function ftl_add_folder(aParentItem, aItem) { // This intentionally adds any new folder even if it would not pass the // _filterFunction. The idea is that the user can add new folders even // in modes like "unread" or "favorite" and could wonder why they // are not appearing (forgetting they do not meet the criteria of the view). // The folders will be hidden properly next time the view is rebuilt. let parentIndex = this.getIndexOfFolder(aParentItem); let parent = this._rowMap[parentIndex]; if (!parent) return; // Getting these children might have triggered our parent to build its // array just now, in which case the added item will already exist. let children = parent.children; var newChild; for (let child of children) { if (child._folder == aItem) { newChild = child; break; } } if (!newChild) { newChild = new ftvItem(aItem); parent.children.push(newChild); newChild._level = parent._level + 1; newChild._parent = parent; sortFolderItems(parent._children); } // If the parent is open, add the new child into the folder pane. // Otherwise, just invalidate the parent row. Note that this code doesn't // get called for the smart folder case. if (!parent.open) { // Special case adding a special folder when the parent is collapsed. // Expand the parent so the user can see the special child. // Expanding the parent is sufficient to add the folder to the view, // because either we knew about it, or we will have added a child item // for it above. if (newChild._folder.flags & Ci.nsMsgFolderFlags.SpecialUse) { this._toggleRow(parentIndex, false); return; } } this._addChildToView(parent, parentIndex, newChild); }, onFolderRemoved: function ftl_remove(aRDFParentItem, aItem) { this._persistItemClosed(aItem.URI); let index = this.getIndexOfFolder(aItem); if (index == null) return; // Forget our parent's children; they'll get rebuilt. if (aRDFParentItem && this._rowMap[index]._parent) this._rowMap[index]._parent._children = null; let kidCount = 1; let walker = Number(index) + 1; while (walker < this.rowCount && this._rowMap[walker].level > this._rowMap[index].level) { walker++; kidCount++; } this._rowMap.splice(index, kidCount); this._tree.rowCountChanged(index, -1 * kidCount); this._tree.invalidateRow(index); }, onMessageRemoved: function(parentFolder, msg) {}, onFolderPropertyChanged: function(aItem, aProperty, aOld, aNew) {}, onFolderIntPropertyChanged: function(aItem, aProperty, aOld, aNew) { // First try mode specific handling of the changed property. if (this._modes[this.mode].handleChangedIntProperty(aItem, aProperty, aOld, aNew)) return; if (aItem instanceof Ci.nsIMsgFolder) { let index = this.getIndexOfFolder(aItem); let folder = aItem; let folderTreeMode = this._modes[this._mode]; // Look for first visible ancestor. while (index == null) { folder = folderTreeMode.getParentOfFolder(folder); if (!folder) break; index = this.getIndexOfFolder(folder); } if (index != null) this._tree.invalidateRow(index); } }, onFolderBoolPropertyChanged: function(aItem, aProperty, aOld, aNew) { let index = this.getIndexOfFolder(aItem); if (index != null) this._tree.invalidateRow(index); }, onFolderUnicharPropertyChanged: function(aItem, aProperty, aOld, aNew) {}, onFolderPropertyFlagChanged: function(aItem, aProperty, aOld, aNew) {}, onFolderEvent: function(aFolder, aEvent) { let index = this.getIndexOfFolder(aFolder); if (index != null) this._tree.invalidateRow(index); } }; /** * The ftvItem object represents a single row in the tree view. Because I'm lazy * I'm just going to define the expected interface here. You are free to return * an alternative object, provided that it matches this interface: * * id (attribute) - a unique string for this object. Must persist over sessions * text (attribute) - the text to display in the tree * level (attribute) - the level in the tree to display the item at * open (rw, attribute) - whether or not this container is open * children (attribute) - an array of child items also conforming to this spec * getProperties (function) - a call from getRowProperties or getCellProperties * for this item will be passed into this function * command (function) - this function will be called when the item is double- * clicked */ /** * The ftvItem constructor takes these arguments: * * @param aFolder The folder attached to this row in the tree. * @param aFolderFilter When showing children folders of this one, * only show those that pass this filter function. * If unset, show all subfolders. */ function ftvItem(aFolder, aFolderFilter) { this._folder = aFolder; this._level = 0; this._parent = null; this._folderFilter = aFolderFilter; // The map contains message counts for each folder column. // Each key is a column name (ID) from the folder tree. // Value is an array of the format // "[value_for_folder, value_for_all_its_subfolders]". this._summarizedCounts = new Map(); } ftvItem.prototype = { open: false, addServerName: false, useServerNameOnly: false, get id() { return this._folder.URI; }, get text() { return this.getText("folderNameCol"); }, getText(aColName) { // Only show counts / total size of subtree if the pref is set, // we are in "All folders" mode and this folder row is not expanded. gFolderStatsHelpers.sumSubfolders = gFolderStatsHelpers.sumSubfoldersPref && (gFolderTreeView.mode == kDefaultMode) && this._folder.hasSubFolders && !this.open; this._summarizedCounts.delete(aColName); switch (aColName) { case "folderNameCol": let text; if (this.useServerNameOnly) text = this._folder.server.prettyName; else { text = this._folder.abbreviatedName; if (this.addServerName) { text = gFolderTreeView.messengerBundle.getFormattedString( "folderWithAccount", [text, this._folder.server.prettyName]); } } // In a simple list tree we don't care for attributes other than folder // name. if (gFolderTreeView._treeElement.getAttribute("simplelist") == "true") return text; // If the unread column is shown, we don't need to add the count // to the name. if (!document.getElementById("folderUnreadCol").hidden) return text; let unread = this._folder.getNumUnread(false); let totalUnread = gFolderStatsHelpers.sumSubfolders ? this._folder.getNumUnread(true) : unread; this._summarizedCounts.set(aColName, [unread, totalUnread - unread]); if (totalUnread > 0) { text = gFolderTreeView.messengerBundle.getFormattedString( "folderWithUnreadMsgs", [text, gFolderStatsHelpers.addSummarizedPrefix(totalUnread, unread != totalUnread)]); } return text; case "folderUnreadCol": let folderUnread = this._folder.getNumUnread(false); let subfoldersUnread = gFolderStatsHelpers.sumSubfolders ? this._folder.getNumUnread(true) : folderUnread; this._summarizedCounts.set(aColName, [folderUnread, subfoldersUnread - folderUnread]); return gFolderStatsHelpers .fixNum(subfoldersUnread, folderUnread != subfoldersUnread); case "folderTotalCol": let folderTotal = this._folder.getTotalMessages(false); let subfoldersTotal = gFolderStatsHelpers.sumSubfolders ? this._folder.getTotalMessages(true) : folderTotal; this._summarizedCounts.set(aColName, [folderTotal, subfoldersTotal - folderTotal]); return gFolderStatsHelpers .fixNum(subfoldersTotal, folderTotal != subfoldersTotal); case "folderSizeCol": let thisFolderSize = gFolderStatsHelpers.getFolderSize(this._folder); let subfoldersSize = gFolderStatsHelpers.sumSubfolders ? gFolderStatsHelpers.getSubfoldersSize(this._folder) : 0; if (subfoldersSize == gFolderStatsHelpers.kUnknownSize || thisFolderSize == gFolderStatsHelpers.kUnknownSize) return gFolderStatsHelpers.kUnknownSize; let totalSize = thisFolderSize + subfoldersSize; if (totalSize == 0) return ""; let [totalText, folderUnit] = gFolderStatsHelpers.formatFolderSize(totalSize); let folderText = (subfoldersSize == 0) ? totalText : gFolderStatsHelpers.formatFolderSize(thisFolderSize, folderUnit)[0]; let subfoldersText = (subfoldersSize == 0) ? "" : gFolderStatsHelpers.formatFolderSize(subfoldersSize, folderUnit)[0]; this._summarizedCounts.set(aColName, [folderText, subfoldersText]); return gFolderStatsHelpers .addSummarizedPrefix(totalText, totalSize != thisFolderSize); default: return ""; } }, get level() { return this._level; }, getProperties: function (aColumn) { if (aColumn && aColumn.id != "folderNameCol") return ""; let properties = FolderUtils.getFolderProperties(this._folder, this.open); return properties; }, command: function fti_command() { if (!Services.prefs.getBoolPref("mailnews.reuse_thread_window2")) { MsgOpenNewWindowForFolder(this._folder.URI, -1 /* key */); } }, _children: null, get children() { // We're caching our child list to save perf. if (!this._children) { let iter; try { iter = fixIterator(this._folder.subFolders, Ci.nsIMsgFolder); } catch (ex) { Services.console.logStringMessage("Discovering children for " + this._folder.URI + " failed with " + "exception: " + ex); iter = []; } this._children = []; // Out of all children, only keep those that match the _folderFilter // and those that contain such children. for (let folder of iter) { if (!this._folderFilter || this._folderFilter(folder)) { this._children.push(new ftvItem(folder, this._folderFilter)); } } sortFolderItems(this._children); // Each child is a level one below us. for (let child of this._children) { child._level = this._level + 1; child._parent = this; } } return this._children; } }; /** * This handles the invocation of most commands dealing with folders, based off * of the current selection, or a passed in folder. */ var gFolderTreeController = { /** * Opens the dialog to create a new sub-folder, and creates it if the user * accepts * * @param aParent (optional) the parent for the new subfolder */ newFolder(aParent) { let folder = aParent || GetSelectedMsgFolders()[0]; // Make sure we actually can create subfolders. if (!folder.canCreateSubfolders) { // Check if we can create them at the root. let rootMsgFolder = folder.server.rootMsgFolder; if (rootMsgFolder.canCreateSubfolders) folder = rootMsgFolder; else // just use the default account folder = GetDefaultAccountRootFolder(); } let dualUseFolders = true; if (folder.server instanceof Ci.nsIImapIncomingServer) dualUseFolders = folder.server.dualUseFolders; function newFolderCallback(aName, aFolder) { // createSubfolder can throw an exception, causing the newFolder dialog // to not close and wait for another input. // TODO: Rewrite this logic and move the opening of alert dialogs from // nsMsgLocalMailFolder::CreateSubfolderInternal to here (bug 831190#c16). if (aName) aFolder.createSubfolder(aName, msgWindow); } window.openDialog("chrome://messenger/content/newFolderDialog.xul", "", "chrome,modal,centerscreen", {folder: folder, dualUseFolders: dualUseFolders, okCallback: newFolderCallback}); }, /** * Opens the dialog to edit the properties for a folder * * @param aTabID (optional) the tab to show in the dialog * @param aFolder (optional) the folder to edit, if not the selected one */ editFolder(aTabID, aFolder) { let folder = aFolder || GetSelectedMsgFolders()[0]; // If a server is selected, view settings for that account. if (folder.isServer) { MsgAccountManager(null, folder.server); return; } if (folder.getFlag(Ci.nsMsgFolderFlags.Virtual)) { // virtual folders get their own property dialog that contains all of the // search information related to the virtual folder. this.editVirtualFolder(folder); return; } let title = gFolderTreeView.messengerBundle.getString("folderProperties"); function editFolderCallback(aNewName, aOldName, aUri) { if (aNewName != aOldName) folder.rename(aNewName, msgWindow); } function rebuildSummary(msgFolder) { if (msgFolder.locked) { msgFolder.throwAlertMsg("operationFailedFolderBusy", msgWindow); return; } if (msgFolder.supportsOffline) { // Remove the offline store, if any. let offlineStore = msgFolder.filePath; // XXX todo: figure out how to delete a maildir directory async. This // delete causes main thread lockup for large maildir folders. if (offlineStore.exists()) offlineStore.remove(true); } // Send a notification that we are triggering a database rebuild. MailServices.mfn.notifyItemEvent(folder, "FolderReindexTriggered", null, null); msgFolder.msgDatabase.summaryValid = false; try { msgFolder.closeAndBackupFolderDB(""); } catch(e) { // In a failure, proceed anyway since we're dealing with problems msgFolder.ForceDBClosed(); } // these two lines will cause the thread pane to get reloaded // when the download/reparse is finished. Only do this // if the selected folder is loaded (i.e., not thru the // context menu on a non-loaded folder). if (msgFolder == GetLoadedMsgFolder()) { gRerootOnFolderLoad = true; gCurrentFolderToReroot = msgFolder.URI; } msgFolder.updateFolder(msgWindow); } window.openDialog("chrome://messenger/content/folderProps.xul", "", "chrome,modal,centerscreen", {folder: folder, serverType: folder.server.type, msgWindow: msgWindow, title: title, okCallback: editFolderCallback, tabID: aTabID, name: folder.prettyName, rebuildSummaryCallback: rebuildSummary}); }, /** * Opens the dialog to rename a particular folder, and does the renaming if * the user clicks OK in that dialog * * @param aFolder (optional) the folder to rename, if different than the * currently selected one */ renameFolder(aFolder) { let folder = aFolder || GetSelectedMsgFolders()[0]; let controller = this; function renameCallback(aName, aUri) { if (aUri != folder.URI) Cu.reportError("got back a different folder to rename!"); controller._resetThreadPane(); let folderTree = document.getElementById("folderTree"); folderTree.view.selection.clearSelection(); folder.rename(aName, msgWindow); } window.openDialog("chrome://messenger/content/renameFolderDialog.xul", "", "chrome,modal,centerscreen", {preselectedURI: folder.URI, okCallback: renameCallback, name: folder.prettyName}); }, /** * Deletes a folder from its parent. Also handles unsubscribe from newsgroups * if the selected folder/s happen to be nntp. * * @param aFolder (optional) the folder to delete, if not the selected one */ deleteFolder(aFolder) { let folders = aFolder ? [aFolder] : GetSelectedMsgFolders(); let prompt = Services.prompt; for (let folder of folders) { // For newsgroups, "delete" means "unsubscribe". if (folder.server.type == "nntp" && !folder.getFlag(Ci.nsMsgFolderFlags.Virtual)) { MsgUnsubscribe([folder]); continue; } let canDelete = folder.isSpecialFolder(Ci.nsMsgFolderFlags.Junk, false) ? CanRenameDeleteJunkMail(folder.URI) : folder.deletable; if (!canDelete) continue; if (folder.getFlag(Ci.nsMsgFolderFlags.Virtual)) { let confirmation = gMessengerBundle.getString("confirmSavedSearchDeleteMessage"); let title = gMessengerBundle.getString("confirmSavedSearchDeleteTitle"); let buttonTitle = gMessengerBundle.getString("confirmSavedSearchDeleteButton"); let buttonFlags = prompt.BUTTON_TITLE_IS_STRING * prompt.BUTTON_POS_0 + prompt.BUTTON_TITLE_CANCEL * prompt.BUTTON_POS_1; if (prompt.confirmEx(window, title, confirmation, buttonFlags, buttonTitle, "", "", "", {}) != 0) /* the yes button is in position 0 */ continue; if (gCurrentVirtualFolderUri == folder.URI) gCurrentVirtualFolderUri = null; } // We can delete this folder. try { folder.deleteSelf(msgWindow); } // Ignore known errors from canceled warning dialogs. catch (ex) { const NS_MSG_ERROR_COPY_FOLDER_ABORTED = 0x8055001a; if (ex.result != NS_MSG_ERROR_COPY_FOLDER_ABORTED) { throw ex; } } } }, /** * Prompts the user to confirm and empties the trash for the selected folder. * The folder and its children are only emptied if it has the proper Trash * flag. * * @param aFolder (optional) The trash folder to empty. If unspecified or not * a trash folder, the currently selected server's * trash folder is used. */ emptyTrash(aFolder) { let folder = aFolder || GetSelectedMsgFolders()[0]; if (!folder.getFlag(Ci.nsMsgFolderFlags.Trash)) folder = folder.rootFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Trash); if (!folder) return; if (this._checkConfirmationPrompt("emptyTrash")) folder.emptyTrash(null); }, /** * Deletes everything (folders and messages) in the selected folder. * The folder is only emptied if it has the proper Junk flag. * * @param aFolder (optional) The folder to empty. If unspecified, the * currently selected folder is used, if it * is junk. */ emptyJunk(aFolder) { let folder = aFolder || GetSelectedMsgFolders()[0]; if (!folder || !folder.getFlag(Ci.nsMsgFolderFlags.Junk)) return; if (!this._checkConfirmationPrompt("emptyJunk")) return; // Delete any sub-folders this folder might have. for (let f of folder.subFolders) { folder.propagateDelete(f, true); } // Now delete the messages. folder.deleteMessages([...folder.messages], msgWindow, true, false, null, false); }, /** * Compacts either particular folder/s, or selected folders. * * @param aFolders (optional) the folders to compact, if different than the * currently selected ones */ compactFolders(aFolders) { let folders = aFolders || GetSelectedMsgFolders(); for (let folder of folders) { let isImapFolder = folder.server.type == "imap"; // Can't compact folders that have just been compacted if (!isImapFolder && !folder.expungedBytes) return; // Reset thread pane for non-imap folders. if (!isImapFolder && gDBView && gDBView.msgFolder == folder) { this._resetThreadPane(); } folder.compact(null, msgWindow); } }, /** * Compacts all folders for accounts that the given folders belong * to, or all folders for accounts of the currently selected folders. * * @param aFolders (optional) the folders for whose accounts we should compact * all folders, if different than the currently * selected ones */ compactAllFoldersForAccount(aFolders) { let folders = aFolders || GetSelectedMsgFolders(); for (let folder of folders) { folder.compactAll(null, msgWindow); // Reset thread pane for non-imap folders. if (gDBView && folder.server.type != "imap") this._resetThreadPane(); } }, /** * Opens the dialog to create a new virtual folder * * @param aName - the default name for the new folder * @param aSearchTerms - the search terms associated with the folder * @param aParent - the folder to run the search terms on */ newVirtualFolder(aName, aSearchTerms, aParent) { let folder = aParent || GetSelectedMsgFolders()[0]; if (!folder) folder = GetDefaultAccountRootFolder(); let name = folder.prettyName; if (aName) name += "-" + aName; window.openDialog("chrome://messenger/content/virtualFolderProperties.xul", "", "chrome,modal,centerscreen", {folder: folder, searchTerms: aSearchTerms, newFolderName: name}); }, /** * Opens the dialog to edit the properties for a virtual folder * * @param aFolder (optional) the folder to edit, if not the selected one */ editVirtualFolder(aFolder) { let folder = aFolder || GetSelectedMsgFolders()[0]; function editVirtualCallback(aURI) { // we need to reload the folder if it is the currently loaded folder... if (gMsgFolderSelected && aURI == gMsgFolderSelected.URI) { // force the folder pane to reload the virtual folder gMsgFolderSelected = null; FolderPaneSelectionChange(); } } window.openDialog("chrome://messenger/content/virtualFolderProperties.xul", "", "chrome,modal,centerscreen", {folder: folder, editExistingFolder: true, onOKCallback: editVirtualCallback, msgWindow:msgWindow}); }, /** * Opens a search window with the given folder, or the selected one if none * is given. * * @param [aFolder] the folder to open the search window for, if different * from the selected one */ searchMessages(aFolder) { MsgSearchMessages(aFolder || GetSelectedMsgFolders()[0]); }, /** * For certain folder commands, the thread pane needs to be invalidated, this * takes care of doing so. */ _resetThreadPane() { if (gDBView) gCurrentlyDisplayedMessage = gDBView.currentlyDisplayedMessage; ClearThreadPaneSelection(); ClearThreadPane(); ClearMessagePane(); }, /** * Prompts for confirmation, if the user hasn't already chosen the "don't ask * again" option. * * @param aCommand - the command to prompt for */ _checkConfirmationPrompt(aCommand) { const kDontAskAgainPref = "mailnews." + aCommand + ".dontAskAgain"; // default to ask user if the pref is not set if (!Services.prefs.getBoolPref(kDontAskAgainPref, false)) { let checkbox = {value: false}; let choice = Services.prompt.confirmEx( window, gMessengerBundle.getString(aCommand + "Title"), gMessengerBundle.getString(aCommand + "Message"), Services.prompt.STD_YES_NO_BUTTONS, null, null, null, gMessengerBundle.getString(aCommand + "DontAsk"), checkbox); if (checkbox.value) Services.prefs.setBoolPref(kDontAskAgainPref, true); if (choice != 0) return false; } return true; }, } /** * Sorts the passed in array of folder items using the folder sort key * * @param aFolders - the array of ftvItems to sort. */ function sortFolderItems (aFtvItems) { function sorter(a, b) { return a._folder.compareSortKeys(b._folder); } aFtvItems.sort(sorter); } var gFolderStatsHelpers = { kUnknownSize: "-", sumSubfoldersPref: false, sumSubfolders: false, sizeUnits: "", kiloUnit: "KB", megaUnit: "MB", init: function() { // We cache these values because the cells in the folder pane columns // using these helpers can be redrawn often. this.sumSubfoldersPref = Services.prefs.getBoolPref("mail.folderpane.sumSubfolders"); this.sizeUnits = Services.prefs.getCharPref("mail.folderpane.sizeUnits"); this.kiloUnit = gFolderTreeView.messengerBundle.getString("kiloByteAbbreviation2"); this.megaUnit = gFolderTreeView.messengerBundle.getString("megaByteAbbreviation2"); }, /** * Add a prefix to denote the value is actually a sum of all the subfolders. * The prefix is useful as this sum may not always be the exact sum of * individual folders when they are shown expanded (due to rounding to a * unit). * E.g. folder1 600bytes -> 1KB, folder2 700bytes -> 1KB * summarized at parent folder: 1300bytes -> 1KB * * @param aValue The value to be displayed. * @param aSubfoldersContributed Boolean indicating whether subfolders * contributed to the accumulated total value. */ addSummarizedPrefix: function(aValue, aSubfoldersContributed) { if (!this.sumSubfolders) return aValue; if (!aSubfoldersContributed) return aValue; return gFolderTreeView.messengerBundle.getFormattedString("folderSummarizedSymbolValue", [aValue]); }, /** * nsIMsgFolder uses -1 as a magic number to mean "I don't know". In those * cases we indicate it to the user. The user has to open the folder * so that the property is initialized from the DB. * * @param aNumber The number to translate for the user. * @param aSubfoldersContributed Boolean indicating whether subfolders * contributed to the accumulated total value. */ fixNum: function(aNumber, aSubfoldersContributed) { if (aNumber < 0) return this.kUnknownSize; return (aNumber == 0 ? "" : this.addSummarizedPrefix(aNumber, aSubfoldersContributed)); }, /** * Get the size of the specified folder. * * @param aFolder The nsIMsgFolder to analyze. */ getFolderSize: function(aFolder) { let folderSize = 0; try { folderSize = aFolder.sizeOnDisk; if (folderSize < 0) return this.kUnknownSize; } catch(ex) { return this.kUnknownSize; } return folderSize; }, /** * Get the total size of all subfolders of the specified folder. * * @param aFolder The nsIMsgFolder to analyze. */ getSubfoldersSize: function(aFolder) { let folderSize = 0; if (aFolder.hasSubFolders) { let subFolders = aFolder.subFolders; while (subFolders.hasMoreElements()) { let subFolder = subFolders.getNext().QueryInterface(Ci.nsIMsgFolder); let subSize = this.getFolderSize(subFolder); let subSubSize = this.getSubfoldersSize(subFolder); if (subSize == this.kUnknownSize || subSubSize == this.kUnknownSize) return subSize; folderSize += subSize + subSubSize; } } return folderSize; }, /** * Format the given folder size into a string with an appropriate unit. * * @param aSize The size in bytes to format. * @param aUnit Optional unit to use for the format. * Possible values are "KB" or "MB". * @return An array with 2 values. * First is the resulting formatted strings. * The second one is the final unit used to format the string. */ formatFolderSize: function(aSize, aUnit = gFolderStatsHelpers.sizeUnits) { let size = Math.round(aSize / 1024); let unit = gFolderStatsHelpers.kiloUnit; // If size is non-zero try to show it in a unit that fits in 3 digits, // but if user specified a fixed unit, use that. if (aUnit != "KB" && (size > 999 || aUnit == "MB")) { size = Math.round(size / 1024); unit = gFolderStatsHelpers.megaUnit; aUnit = "MB"; } // This needs to be updated if the "%.*f" placeholder string // in "*ByteAbbreviation2" in messenger.properties changes. return [unit.replace("%.*f", size).replace(" ",""), aUnit]; } };