mirror of
https://github.com/ManchildProductions/binoc-central-mirror.git
synced 2026-06-15 11:59:00 +00:00
3006 lines
107 KiB
JavaScript
3006 lines
107 KiB
JavaScript
/* 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/. */
|
|
|
|
Components.utils.import("resource:///modules/folderUtils.jsm");
|
|
Components.utils.import("resource:///modules/iteratorUtils.jsm");
|
|
Components.utils.import("resource:///modules/mailServices.js");
|
|
Components.utils.import("resource:///modules/MailUtils.js");
|
|
Components.utils.import("resource:///modules/IOUtils.js");
|
|
Components.utils.import("resource://gre/modules/Services.jsm");
|
|
|
|
var kDefaultMode = "all";
|
|
|
|
var nsMsgFolderFlags = Components.interfaces.nsMsgFolderFlags;
|
|
|
|
/**
|
|
* 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|.
|
|
*/
|
|
var 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.
|
|
*/
|
|
var 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) {
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
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;
|
|
|
|
try {
|
|
// Normally our tree takes care of keeping the last selected by itself.
|
|
// However older versions of TB stored this in a preference, which we need
|
|
// to migrate
|
|
let modeIndex = Services.prefs.getIntPref("mail.ui.folderpane.view");
|
|
this._mode = this._modeNames[modeIndex];
|
|
Services.prefs.deleteBranch("mail.ui.folderpane");
|
|
} catch(ex) {
|
|
// This is ok. If we've already migrated we'll end up here
|
|
}
|
|
|
|
if (aJSONFile) {
|
|
// Parse our persistent-open-state json file
|
|
let data = IOUtils.loadFileToString(aJSONFile);
|
|
if (data) {
|
|
try {
|
|
this._persistOpenMap = JSON.parse(data);
|
|
} catch (x) {
|
|
Components.utils.reportError(
|
|
gFolderTreeView.messengerBundle
|
|
.getFormattedString("failedToReadFile", [aJSONFile, x]));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load our data
|
|
this._updateCompactState(this.mode);
|
|
this._selectModeInSelector(this.mode);
|
|
this._rebuild();
|
|
// And actually draw the tree
|
|
aTree.view = this;
|
|
|
|
this.toggleCols(true);
|
|
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) {
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
|
|
// 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();
|
|
},
|
|
|
|
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;
|
|
},
|
|
|
|
/**
|
|
* Toggle displaying the headers of columns in the folder pane.
|
|
* @param aSetup Set to true if the columns should be set up according
|
|
* to the pref, not toggle them.
|
|
*/
|
|
toggleCols: function(aSetup = false) {
|
|
let hide = Services.prefs.getBoolPref("mail.folderpane.showColumns");
|
|
if (aSetup)
|
|
hide = !hide;
|
|
this._treeElement.setAttribute("hidecolumnpicker", hide ? "true" : "false");
|
|
for (let columnName of ["folderNameCol", "folderUnreadCol",
|
|
"folderTotalCol", "folderSizeCol"])
|
|
{
|
|
let column = document.getElementById(columnName);
|
|
if (hide) {
|
|
column.setAttribute("hideheader", "true");
|
|
column.removeAttribute("label");
|
|
if (columnName != "folderNameCol") {
|
|
if (!aSetup) {
|
|
// If user hides the columns store their visible state in a special attribute
|
|
// that is persisted by XUL.
|
|
let state = column.getAttribute("hidden");
|
|
column.setAttribute("hiddeninactive", state);
|
|
}
|
|
column.setAttribute("hidden", "true");
|
|
}
|
|
} else {
|
|
column.setAttribute("label", column.getAttribute("label2"));
|
|
column.setAttribute("hideheader", "false");
|
|
if (!aSetup) {
|
|
// If user unhides the columns restore their visible state
|
|
// from our special attribute.
|
|
if (column.hasAttribute("hiddeninactive")) {
|
|
let state = column.getAttribute("hiddeninactive");
|
|
column.setAttribute("hidden", state);
|
|
} else if (columnName == "folderTotalCol") {
|
|
// If there was no hiddeninactive attribute set, that means this is
|
|
// our first showing of the folder pane columns. Show the TotalCol
|
|
// as a sample so the user notices what is happening.
|
|
column.setAttribute("hidden", "false");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!aSetup)
|
|
Services.prefs.setBoolPref("mail.folderpane.showColumns", !hide);
|
|
},
|
|
|
|
/**
|
|
* Toggles the compact view of the current mode.
|
|
*
|
|
* @param aCompact Boolean telling whether compact view should be enabled.
|
|
*/
|
|
toggleCompact: function(aCompact) {
|
|
let targetMode = this.fullMode(this.baseMode(), aCompact);
|
|
this.mode = targetMode;
|
|
},
|
|
|
|
/**
|
|
* Toggles the folder mode, but tries to keep the "compact" variant the same
|
|
* as the previous mode.
|
|
*
|
|
* @param aMode The base name of the new mode selected.
|
|
*/
|
|
toggleMode: function(aMode) {
|
|
// Take the base name and add compact variant according to the state of the
|
|
// "Compact" checkbox in the UI.
|
|
let userMode = this.fullMode(aMode,
|
|
document.getElementById("appmenu_compactFolderView").hasAttribute("checked"));
|
|
|
|
// Some combinations of user selection and "Compact view" checkbox are not supported.
|
|
// In that case fall back to a version of this mode that exists.
|
|
if (!(userMode in this._modes)) {
|
|
let baseMode = this.baseMode(aMode);
|
|
if (baseMode in this._modes)
|
|
userMode = baseMode;
|
|
else
|
|
userMode = this.fullMode(baseMode, true);
|
|
}
|
|
|
|
this.mode = userMode;
|
|
},
|
|
|
|
/**
|
|
* Update state of checkboxes according to currently selected mode.
|
|
* Synchronize the state of our 2 "compact" menuitems and decide if they
|
|
* should be disabled.
|
|
*
|
|
* @param aMode The current folder mode.
|
|
*/
|
|
_updateCompactState: function(aMode) {
|
|
let checked = aMode.endsWith("_compact");
|
|
let menuitem = document.getElementById("menu_compactFolderView");
|
|
let appmenuitem = document.getElementById("appmenu_compactFolderView");
|
|
if (checked) {
|
|
menuitem.setAttribute("checked", "true");
|
|
appmenuitem.setAttribute("checked", "true");
|
|
} else {
|
|
menuitem.removeAttribute("checked");
|
|
appmenuitem.removeAttribute("checked");
|
|
}
|
|
let baseMode = this.baseMode(aMode);
|
|
let compactToggleable = (baseMode in this._modes) &&
|
|
(this.fullMode(baseMode, true) in this._modes);
|
|
menuitem.disabled = !compactToggleable;
|
|
appmenuitem.disabled = !compactToggleable;
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
this._updateCompactState(this._mode);
|
|
|
|
this._selectModeInSelector(this._mode);
|
|
|
|
// Store current mode and actually build the folder pane.
|
|
this._treeElement.setAttribute("mode", this._mode);
|
|
this._rebuild();
|
|
},
|
|
|
|
/**
|
|
* Name of the mode without the _compact suffix, used e.g. in the menulists.
|
|
*
|
|
* @param aMode If set, construct the base name from this mode name instead
|
|
* of the currently active one.
|
|
*/
|
|
baseMode: function(aMode) {
|
|
if (!aMode)
|
|
aMode = this.mode;
|
|
|
|
return aMode.replace(/_compact$/, "");
|
|
},
|
|
|
|
/**
|
|
* Name of the mode including the _compact suffix if appropriate.
|
|
*
|
|
* @param aMode If set, construct the base name from this mode name instead
|
|
* of the currently active one.
|
|
* @param aCOmpact Bool value whether to force adding the suffix or not.
|
|
*/
|
|
fullMode: function(aMode, aCompact) {
|
|
if (!aMode)
|
|
aMode = this.mode;
|
|
if (aCompact == undefined)
|
|
aCompact = aMode.endsWith("_compact");
|
|
|
|
return this.baseMode(aMode) + (aCompact ? "_compact" : "");
|
|
},
|
|
|
|
_initFolderModeSelector: function() {
|
|
// Populate the mode selector menulist on the toolbar.
|
|
let fullModes = [];
|
|
let compactModes = [];
|
|
for (let mode of this._modeNames) {
|
|
let array = mode.endsWith("_compact") ? compactModes : fullModes;
|
|
array.push(mode);
|
|
}
|
|
|
|
let modeSelector = document.getElementById("folderpane-mode-selector").firstChild;
|
|
// Can't use modeSelector.removeAllItems() here as it would remove the menupopup too, with its attributes.
|
|
while (modeSelector.menupopup.hasChildNodes())
|
|
modeSelector.menupopup.lastChild.remove();
|
|
|
|
let currentMode = this.mode;
|
|
let parent = this;
|
|
|
|
function appendMode(aMode) {
|
|
let name;
|
|
if (aMode in parent._modeDisplayNames) {
|
|
name = parent._modeDisplayNames[aMode];
|
|
} else {
|
|
let key = "folderPaneModeHeader_" + aMode;
|
|
name = parent.messengerBundle.getString(key);
|
|
}
|
|
let item = modeSelector.appendItem(name, aMode);
|
|
item.setAttribute("type", "radio");
|
|
if (aMode == currentMode)
|
|
item.setAttribute("checked", "true");
|
|
else
|
|
item.setAttribute("checked", "false");
|
|
}
|
|
|
|
for (let mode of fullModes)
|
|
appendMode(mode);
|
|
|
|
if ((fullModes.length > 0) && (compactModes.length > 0))
|
|
modeSelector.menupopup.appendChild(document.createElement("menuseparator"));
|
|
|
|
for (let mode of compactModes)
|
|
appendMode(mode);
|
|
},
|
|
|
|
_selectModeInSelector: function(aMode) {
|
|
// Show the mode in the mode selector, if it is on a toolbar.
|
|
let modeSelector = document.getElementById("folderpane-mode-selector");
|
|
if (modeSelector) {
|
|
if (!modeSelector.querySelector('[value="' + aMode + '"]'))
|
|
this._initFolderModeSelector();
|
|
modeSelector.firstChild.value = aMode;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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) {
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
let targetFolder = gFolderTreeView._rowMap[aRow]._folder;
|
|
if (!targetFolder)
|
|
return false;
|
|
let dt = this._currentTransfer;
|
|
let types = dt.mozTypesAt(0);
|
|
if (Array.indexOf(types, "text/x-moz-message") != -1) {
|
|
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 (Array.indexOf(types, "text/x-moz-folder") != -1) {
|
|
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 & 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 (Array.indexOf(types, "text/x-moz-newsfolder") != -1) {
|
|
// 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 (Array.indexOf(types, "application/x-moz-file") != -1) {
|
|
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)
|
|
.QueryInterface(Ci.nsILocalFile);
|
|
return extFile.isFile();
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
drop: function ftv_drop(aRow, aOrientation) {
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
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 = dt.mozTypesAt(0);
|
|
if (Array.indexOf(types, "text/x-moz-folder") != -1) {
|
|
for (let i = 0; i < count; i++) {
|
|
let folders = new Array;
|
|
folders.push(dt.mozGetDataAt("text/x-moz-folder", i)
|
|
.QueryInterface(Ci.nsIMsgFolder));
|
|
let array = toXPCOMArray(folders, Ci.nsIMutableArray);
|
|
cs.CopyFolders(array, targetFolder,
|
|
(folders[0].server == targetFolder.server), null,
|
|
msgWindow);
|
|
}
|
|
}
|
|
else if (Array.indexOf(types, "text/x-moz-newsfolder") != -1) {
|
|
// 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 (Array.indexOf(types, "text/x-moz-message") != -1) {
|
|
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, false);
|
|
}
|
|
let prefBranch = Services.prefs.getBranch("mail.");
|
|
let isMove = Cc["@mozilla.org/widget/dragservice;1"]
|
|
.getService(Ci.nsIDragService).getCurrentSession()
|
|
.dragAction == Ci.nsIDragService.DRAGDROP_ACTION_MOVE;
|
|
if (!sourceFolder.canDeleteMessages)
|
|
isMove = false;
|
|
|
|
prefBranch.setCharPref("last_msg_movecopy_target_uri", targetFolder.URI);
|
|
prefBranch.setBoolPref("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 (Array.indexOf(types, "application/x-moz-file") != -1) {
|
|
for (let i = 0; i < count; i++) {
|
|
let extFile = dt.mozGetDataAt("application/x-moz-file", i)
|
|
.QueryInterface(Ci.nsILocalFile);
|
|
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;
|
|
},
|
|
|
|
/**
|
|
* 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 "";
|
|
},
|
|
|
|
/**
|
|
* For feed folders get, cache, and return a favicon. Otherwise return "" to
|
|
* let css set the image per nsITreeView requirements.
|
|
*/
|
|
getImageSrc: function(aRow, aCol) {
|
|
if (aCol.id != "folderNameCol")
|
|
return "";
|
|
|
|
let rowItem = gFolderTreeView._rowMap[aRow];
|
|
let folder = rowItem._folder;
|
|
if (folder.server.type != "rss" || folder.isServer)
|
|
return "";
|
|
|
|
let favicon = this.getFolderCacheProperty(folder, "favicon");
|
|
if (favicon != null)
|
|
return favicon;
|
|
|
|
let callback = (iconUrl => {
|
|
this.setFolderCacheProperty(folder, "favicon", iconUrl);
|
|
this._tree.invalidateRow(aRow);
|
|
});
|
|
|
|
// Cache empty string initiallly to return default while getting favicon,
|
|
// so as to never return here. Alternatively, a blank image could be cached.
|
|
this.setFolderCacheProperty(folder, "favicon", "");
|
|
|
|
// On startup, allow the ui to paint first before spawning potentially
|
|
// many requests for favicons, even though they are async.
|
|
setTimeout(() => {
|
|
FeedUtils.getFavicon(folder, null, favicon, window, callback);
|
|
}, 0);
|
|
},
|
|
|
|
/**
|
|
* 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] in Iterator(this._rowMap[aNewIndex].children)) {
|
|
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 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 Components.interfaces.nsIMsgImapMailFolder)
|
|
folder.performExpand(msgWindow);
|
|
}
|
|
}
|
|
},
|
|
|
|
_subFoldersWithStringProperty: function ftv_subFoldersWithStringProperty(folder, folders, aFolderName, deep)
|
|
{
|
|
for (let child in fixIterator(folder.subFolders, Components.interfaces.nsIMsgFolder)) {
|
|
// if the folder selection is based on a string propery, use that
|
|
if (aFolderName == getSmartFolderName(child)) {
|
|
folders.push(child);
|
|
// Add sub-folders if requested.
|
|
if (deep)
|
|
this.addSubFolders(child, folders);
|
|
}
|
|
else
|
|
// if this folder doesn't have a property set, check Its children
|
|
this._subFoldersWithStringProperty(child, folders, aFolderName, deep);
|
|
}
|
|
},
|
|
|
|
_allFoldersWithStringProperty: function ftv_getAllFoldersWithProperty(accounts, aFolderName, deep)
|
|
{
|
|
let folders = [];
|
|
for (let acct of accounts) {
|
|
let folder = acct.incomingServer.rootFolder;
|
|
this._subFoldersWithStringProperty(folder, folders, aFolderName, deep);
|
|
}
|
|
return folders;
|
|
},
|
|
|
|
_allFoldersWithFlag: function ftv_getAllFolders(accounts, aFolderFlag, deep)
|
|
{
|
|
let folders = [];
|
|
for (let acct of accounts) {
|
|
let foldersWithFlag = acct.incomingServer.rootFolder.getFoldersWithFlags(aFolderFlag);
|
|
if (foldersWithFlag.length > 0) {
|
|
for (let folderWithFlag in fixIterator(foldersWithFlag,
|
|
Components.interfaces.nsIMsgFolder)) {
|
|
folders.push(folderWithFlag);
|
|
// Add sub-folders of Sent and Archive to the result.
|
|
if (deep && (aFolderFlag & (nsMsgFolderFlags.SentMail | nsMsgFolderFlags.Archive)))
|
|
this.addSubFolders(folderWithFlag, folders);
|
|
}
|
|
}
|
|
}
|
|
return folders;
|
|
},
|
|
|
|
/**
|
|
* get folders by flag or property based on the value of flag
|
|
*/
|
|
_allSmartFolders: function ftv_allSmartFolders(accounts, flag, folderName, deep) {
|
|
return flag ?
|
|
gFolderTreeView._allFoldersWithFlag(accounts, flag, deep) :
|
|
gFolderTreeView._allFoldersWithStringProperty(accounts, folderName, deep);
|
|
},
|
|
|
|
/**
|
|
* Add a smart folder for folders with the passed flag set. But if there's
|
|
* only one folder with the flag set, just put it at the top level.
|
|
*
|
|
* @param map array to add folder item to.
|
|
* @param accounts array of accounts.
|
|
* @param smartRootFolder root folder of the smart folders server
|
|
* @param flag folder flag to create smart folders for
|
|
* @param folderName name to give smart folder
|
|
* @param position optional place to put folder item in map. If not specified,
|
|
* folder item will be appended at the end of map.
|
|
* @returns The smart folder's ftvItem if one was added, null otherwise.
|
|
*/
|
|
_addSmartFoldersForFlag: function ftv_addSFForFlag(map, accounts, smartRootFolder,
|
|
flag, folderName, position)
|
|
{
|
|
// If there's only one subFolder, just put it at the root.
|
|
let subFolders = gFolderTreeView._allSmartFolders(accounts, flag, folderName, false);
|
|
if (flag && subFolders.length == 1) {
|
|
let folderItem = new ftvItem(subFolders[0]);
|
|
folderItem._level = 0;
|
|
if (flag & nsMsgFolderFlags.Inbox)
|
|
folderItem.__defineGetter__("children", () => []);
|
|
if (position == undefined)
|
|
map.push(folderItem);
|
|
else
|
|
map[position] = folderItem;
|
|
// No smart folder was added
|
|
return null;
|
|
}
|
|
|
|
let smartFolder;
|
|
try {
|
|
let folderUri = smartRootFolder.URI + "/" + encodeURI(folderName);
|
|
smartFolder = smartRootFolder.getChildWithURI(folderUri, false, true);
|
|
} catch (ex) {
|
|
smartFolder = null;
|
|
};
|
|
if (!smartFolder) {
|
|
let searchFolders = gFolderTreeView._allSmartFolders(accounts, flag, folderName, true);
|
|
let searchFolderURIs = "";
|
|
for (let searchFolder of searchFolders) {
|
|
if (searchFolderURIs.length)
|
|
searchFolderURIs += '|';
|
|
searchFolderURIs += searchFolder.URI;
|
|
}
|
|
if (!searchFolderURIs.length)
|
|
return null;
|
|
smartFolder = gFolderTreeView._createVFFolder(folderName, smartRootFolder,
|
|
searchFolderURIs, flag);
|
|
}
|
|
|
|
let smartFolderItem = new ftvItem(smartFolder);
|
|
smartFolderItem._level = 0;
|
|
if (position == undefined)
|
|
map.push(smartFolderItem);
|
|
else
|
|
map[position] = smartFolderItem;
|
|
// Add the actual special folders as sub-folders of the saved search.
|
|
// By setting _children directly, we bypass the normal calculation
|
|
// of subfolders.
|
|
smartFolderItem._children = subFolders.map(f => new ftvItem(f));
|
|
|
|
let prevChild = null;
|
|
// Each child is a level one below the smartFolder
|
|
for (let child of smartFolderItem._children) {
|
|
child._level = smartFolderItem._level + 1;
|
|
child._parent = smartFolderItem;
|
|
// don't show sub-folders of the inbox, but I think Archives/Sent, etc
|
|
// should have the sub-folders.
|
|
if (flag & nsMsgFolderFlags.Inbox)
|
|
child.__defineGetter__("children", () => []);
|
|
// If we have consecutive children with the same server, then both
|
|
// should display as folder - server.
|
|
if (prevChild && (child._folder.server == prevChild._folder.server)) {
|
|
child.addServerName = true;
|
|
prevChild.addServerName = true;
|
|
prevChild.useServerNameOnly = false;
|
|
}
|
|
else if (flag)
|
|
child.useServerNameOnly = true;
|
|
else
|
|
child.addServerName = true;
|
|
prevChild = child;
|
|
}
|
|
// new custom folders from addons may contain lots of children, sort them
|
|
if (flag == 0)
|
|
sortFolderItems(smartFolderItem._children);
|
|
return smartFolderItem;
|
|
},
|
|
_createVFFolder: function ftv_createVFFolder(newName, parentFolder,
|
|
searchFolderURIs, folderFlag)
|
|
{
|
|
let newFolder;
|
|
try {
|
|
if (parentFolder instanceof(Components.interfaces.nsIMsgLocalMailFolder))
|
|
newFolder = parentFolder.createLocalSubfolder(newName);
|
|
else
|
|
newFolder = parentFolder.addSubfolder(newName);
|
|
newFolder.setFlag(nsMsgFolderFlags.Virtual);
|
|
// provide a way to make the top level folder just a container, not
|
|
// a search folder
|
|
let type = this._modes["smart"].getSmartFolderTypeByName(newName);
|
|
if (type[3]) { // isSearch
|
|
let vfdb = newFolder.msgDatabase;
|
|
let dbFolderInfo = vfdb.dBFolderInfo;
|
|
// set the view string as a property of the db folder info
|
|
// set the original folder name as well.
|
|
dbFolderInfo.setCharProperty("searchStr", "ALL");
|
|
dbFolderInfo.setCharProperty("searchFolderUri", searchFolderURIs);
|
|
dbFolderInfo.setUint32Property("searchFolderFlag", folderFlag);
|
|
dbFolderInfo.setBooleanProperty("searchOnline", true);
|
|
vfdb.summaryValid = true;
|
|
vfdb.Close(true);
|
|
}
|
|
parentFolder.NotifyItemAdded(newFolder);
|
|
MailServices.accounts.saveVirtualFolders();
|
|
}
|
|
catch(e) {
|
|
throw(e);
|
|
dump ("Exception : creating virtual folder \n");
|
|
}
|
|
return newFolder;
|
|
},
|
|
|
|
// 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 ""; },
|
|
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 separatelly. 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", "smart"],
|
|
_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 = allAccountsSorted(true);
|
|
|
|
// Don't show deferred pop accounts.
|
|
accounts = accounts.filter(function isNotDeferred(a) {
|
|
let server = a.incomingServer;
|
|
return !(server instanceof Components.interfaces.nsIPop3IncomingServer &&
|
|
server.deferredToAccount);
|
|
});
|
|
|
|
return accounts;
|
|
},
|
|
|
|
/*
|
|
* Session cache keyed by folder url, for properties intended to survive
|
|
* a _rowMap rebuild and avoid expensive requeries. Not for persistence
|
|
* across restarts; _persistOpenMap could be used for that.
|
|
*/
|
|
_cache: {},
|
|
|
|
/**
|
|
* Update a folder property in the session cache.
|
|
*
|
|
* @param nsIMsgFolder aFolder - folder.
|
|
* @param string aProperty - property, currently in "favicon".
|
|
* @param aValue - string or object value.
|
|
*/
|
|
setFolderCacheProperty: function(aFolder, aProperty, aValue) {
|
|
if (!aFolder || !aProperty)
|
|
return;
|
|
|
|
if (!this._cache[aFolder.URI])
|
|
this._cache[aFolder.URI] = {};
|
|
|
|
this._cache[aFolder.URI][aProperty] = aValue;
|
|
},
|
|
|
|
/**
|
|
* Get a folder property from the session cache.
|
|
*
|
|
* @param nsIMsgFolder aFolder - folder.
|
|
* @param string aProperty - property key.
|
|
* @return value or null - null indicates uninitialized.
|
|
*/
|
|
getFolderCacheProperty: function(aFolder, aProperty) {
|
|
if (!aFolder || !aProperty)
|
|
return null;
|
|
|
|
if (!(aFolder.URI in this._cache))
|
|
return null;
|
|
|
|
return this._cache[aFolder.URI][aProperty];
|
|
},
|
|
|
|
/**
|
|
* 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_all_generateMap(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_unread_generateMap(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_favorite_generateMap(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(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 & Components.interfaces.nsMsgFolderFlags.Favorite) !=
|
|
(aNew & Components.interfaces.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.getFlag(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 & Components.interfaces.nsMsgFolderFlags.Favorite) !=
|
|
(aNew & Components.interfaces.nsMsgFolderFlags.Favorite))) {
|
|
gFolderTreeView._rebuild();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
},
|
|
|
|
recent_compact: {
|
|
__proto__: IFolderTreeMode,
|
|
|
|
generateMap: function(ftv) {
|
|
const MAXRECENT = 15;
|
|
|
|
// Get 15 (MAXRECENT) most recently accessed folders.
|
|
let recentFolders = 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 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;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* The smart folder mode combines special folders of a particular type
|
|
* across accounts into a single cross-folder saved search.
|
|
*/
|
|
smart: {
|
|
__proto__: IFolderTreeMode,
|
|
|
|
/**
|
|
* The smart server. This will create the server if it doesn't exist.
|
|
*/
|
|
get _smartServer() {
|
|
let smartServer;
|
|
try {
|
|
smartServer = MailServices.accounts.FindServer("nobody", "smart mailboxes", "none");
|
|
}
|
|
catch (ex) {
|
|
smartServer = MailServices.accounts.createIncomingServer("nobody", "smart mailboxes", "none");
|
|
// We don't want the "smart" server/account leaking out into the ui in
|
|
// other places, so set it as hidden.
|
|
smartServer.hidden = true;
|
|
let account = MailServices.accounts.createAccount();
|
|
account.incomingServer = smartServer;
|
|
}
|
|
delete this._smartServer;
|
|
return this._smartServer = smartServer;
|
|
},
|
|
|
|
/**
|
|
* A list of [flag, name, isDeep, isSearch] for smart folders. isDeep ==
|
|
* false means that subfolders are displayed as subfolders of the account,
|
|
* not of the smart folder. This list is expected to be constant through a
|
|
* session.
|
|
*/
|
|
_flagNameList: [
|
|
[nsMsgFolderFlags.Inbox, "Inbox", false, true],
|
|
[nsMsgFolderFlags.Drafts, "Drafts", false, true],
|
|
[nsMsgFolderFlags.SentMail, "Sent", true, true],
|
|
[nsMsgFolderFlags.Trash, "Trash", true, true],
|
|
[nsMsgFolderFlags.Templates, "Templates", false, true],
|
|
[nsMsgFolderFlags.Archive, "Archives", true, true],
|
|
[nsMsgFolderFlags.Junk, "Junk", false, true],
|
|
[nsMsgFolderFlags.Queue, "Outbox", true, true]
|
|
],
|
|
|
|
/**
|
|
* support for addons to add special folder types, this must be called
|
|
* prior to onload.
|
|
*
|
|
* @param aFolderName name of the folder
|
|
* @param isDeep include subfolders
|
|
* @param folderOptions object with searchStr and searchOnline options, or null
|
|
*/
|
|
addSmartFolderType: function ftv_addSmartFolderType(aFolderName, isDeep, isSearchFolder) {
|
|
this._flagNameList.push([0, aFolderName, isDeep, isSearchFolder]);
|
|
},
|
|
|
|
/**
|
|
* Returns an array of 4 elements describing the smart folder
|
|
* if the given folder is a special folder, else returns null.
|
|
*/
|
|
getSmartFolderTypeByName: function ftv_smart__getSmartFolderType(aName) {
|
|
for (let type of this._flagNameList) {
|
|
if (type[1] == aName)
|
|
return type;
|
|
}
|
|
return null;
|
|
},
|
|
/**
|
|
* check to see if a folder is a smart folder
|
|
*/
|
|
isSmartFolder: function ftv_smart__isSmartFolder(aFolder) {
|
|
if (aFolder.flags & this._allSmartFlags)
|
|
return true;
|
|
// Also check the folder name itself, as containers do not
|
|
// have the smartFolderName property. We check all folders here, since
|
|
// a "real" folder might be marked as a child of a smart folder.
|
|
let smartFolderName = getSmartFolderName(aFolder);
|
|
return smartFolderName && this.getSmartFolderTypeByName(smartFolderName) ||
|
|
this.getSmartFolderTypeByName(aFolder.name);
|
|
},
|
|
|
|
/**
|
|
* All the flags above, bitwise ORed.
|
|
*/
|
|
get _allSmartFlags() {
|
|
delete this._allSmartFlags;
|
|
return this._allSmartFlags = this._flagNameList.reduce(
|
|
(res, [flag,, isDeep,]) => res | flag, 0);
|
|
},
|
|
|
|
/**
|
|
* All the "shallow" flags above (isDeep set to false), bitwise ORed.
|
|
*/
|
|
get _allShallowFlags() {
|
|
delete this._allShallowFlags;
|
|
return this._allShallowFlags = this._flagNameList.reduce(
|
|
(res, [flag,, isDeep,]) => isDeep ? res : (res | flag), 0);
|
|
},
|
|
|
|
/**
|
|
* Returns an array of 4 elements describing the smart folder
|
|
* if the given folder is a special folder, else returns null.
|
|
*/
|
|
_getSmartFolderType: function ftv_smart__getSmartFolderType(aFolder) {
|
|
let smartFolderName = getSmartFolderName(aFolder);
|
|
for (let type of this._flagNameList) {
|
|
if (smartFolderName) {
|
|
if (type[1] == smartFolderName)
|
|
return type;
|
|
continue;
|
|
}
|
|
if (aFolder.flags & type[0])
|
|
return type;
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Returns the smart folder with the given name.
|
|
*/
|
|
_getSmartFolderNamed: function ftv_smart__getSmartFolderNamed(aName) {
|
|
let smartRoot = this._smartServer.rootFolder;
|
|
return smartRoot.getChildWithURI(smartRoot.URI + "/" + encodeURI(aName), false,
|
|
true);
|
|
},
|
|
|
|
generateMap: function ftv_smart_generateMap(ftv) {
|
|
let map = [];
|
|
let accounts = gFolderTreeView._sortedAccounts();
|
|
let smartServer = this._smartServer;
|
|
smartServer.prettyName = gFolderTreeView.messengerBundle
|
|
.getString("unifiedAccountName");
|
|
smartServer.canHaveFilters = false;
|
|
|
|
let smartRoot = smartServer.rootFolder;
|
|
let smartChildren = [];
|
|
for (let [flag, name,,] of this._flagNameList) {
|
|
gFolderTreeView._addSmartFoldersForFlag(smartChildren, accounts,
|
|
smartRoot, flag, name);
|
|
}
|
|
|
|
sortFolderItems(smartChildren);
|
|
for (let smartChild of smartChildren)
|
|
map.push(smartChild);
|
|
|
|
MailUtils.discoverFolders();
|
|
|
|
for (let acct of accounts)
|
|
map.push(new ftv_SmartItem(acct.incomingServer.rootFolder));
|
|
|
|
return map;
|
|
},
|
|
|
|
/**
|
|
* Returns the parent of a folder in the view.
|
|
*
|
|
* - The smart mailboxes are all top-level, so there's no parent.
|
|
* - For one of the special folders, it is the smart folder of that kind
|
|
* if we're showing it (this happens when there's more than one folder
|
|
* of the kind). Otherwise it's a top-level folder, so there isn't a
|
|
* parent.
|
|
* - For a child of a "shallow" special folder (see |_flagNameList| for
|
|
* the definition), it is the account.
|
|
* - Otherwise it is simply the folder's actual parent.
|
|
*/
|
|
getParentOfFolder: function ftv_smart_getParentOfFolder(aFolder) {
|
|
let smartServer = this._smartServer;
|
|
if (aFolder.server == smartServer)
|
|
// This is a smart mailbox
|
|
return null;
|
|
|
|
let smartType = this._getSmartFolderType(aFolder);
|
|
if (smartType) {
|
|
// This is a special folder
|
|
let smartFolder = this._getSmartFolderNamed(smartType[1]);
|
|
if (smartFolder &&
|
|
gFolderTreeView.getIndexOfFolder(smartFolder) != null)
|
|
return smartFolder;
|
|
|
|
return null;
|
|
}
|
|
|
|
let parent = aFolder.parent;
|
|
if (parent && parent.isSpecialFolder(this._allShallowFlags, false)) {
|
|
// Child of a shallow special folder
|
|
return aFolder.server.rootFolder;
|
|
}
|
|
|
|
return parent;
|
|
},
|
|
|
|
/**
|
|
* For a folder of a particular type foo, this returns the smart folder of
|
|
* that type (if it's displayed). Otherwise this returns the folder the
|
|
* message is in.
|
|
*/
|
|
getFolderForMsgHdr: function ftv_smart_getFolderForMsgHdr(aMsgHdr) {
|
|
let folder = aMsgHdr.folder;
|
|
|
|
let smartType = this._getSmartFolderType(folder);
|
|
if (smartType) {
|
|
let smartFolder = this._getSmartFolderNamed(smartType[1]);
|
|
if (smartFolder &&
|
|
gFolderTreeView.getIndexOfFolder(smartFolder) != null)
|
|
return smartFolder;
|
|
}
|
|
return folder;
|
|
},
|
|
|
|
/**
|
|
* Handles the case of a new folder being added.
|
|
*
|
|
* - If a new special folder is added, we need to add it as a child of the
|
|
* corresponding smart folder.
|
|
* - If the parent is a shallow special folder, we need to add it as a
|
|
* top-level folder in its account.
|
|
* - Otherwise, we need to add it as a child of its parent (as normal).
|
|
*/
|
|
onFolderAdded: function ftv_smart_onFolderAdded(aParent, aFolder) {
|
|
if (aFolder.flags & this._allSmartFlags) {
|
|
// add as child of corresponding smart folder
|
|
let smartServer = this._smartServer;
|
|
let smartRoot = smartServer.rootFolder;
|
|
// In theory, a folder can have multiple flags set, so we need to
|
|
// check each flag separately.
|
|
for (let [flag, name,,] of this._flagNameList) {
|
|
if (aFolder.flags & flag)
|
|
gFolderTreeView._addSmartSubFolder(aFolder, smartRoot, name, flag);
|
|
}
|
|
}
|
|
else if (aParent.isSpecialFolder(this._allShallowFlags, false)) {
|
|
// add as a child of the account
|
|
let rootIndex = gFolderTreeView.getIndexOfFolder(
|
|
aFolder.server.rootFolder);
|
|
let root = gFolderTreeView._rowMap[rootIndex];
|
|
if (!root)
|
|
return;
|
|
|
|
let newChild = new ftv_SmartItem(aFolder);
|
|
root.children.push(newChild);
|
|
newChild._level = root._level + 1;
|
|
newChild._parent = root;
|
|
sortFolderItems(root._children);
|
|
|
|
gFolderTreeView._addChildToView(root, rootIndex, newChild);
|
|
}
|
|
else {
|
|
// add as normal
|
|
gFolderTreeView.addFolder(aParent, aFolder);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* This is a helper attribute that simply returns a flat list of all folders
|
|
*/
|
|
get _enumerateFolders() {
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
let folders = [];
|
|
|
|
for (let server in 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 in fixIterator(folder.subFolders, Components.interfaces.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);
|
|
}
|
|
},
|
|
_addSmartSubFolder: function ftl_addSmartSubFolder(aItem, aSmartRoot, aName, aFlag) {
|
|
let smartFolder = aSmartRoot.getChildWithURI(aSmartRoot.URI + "/" + encodeURI(aName),
|
|
false, true);
|
|
let parent = null;
|
|
let parentIndex = -1;
|
|
let newChild;
|
|
let newChildIndex = 0;
|
|
if (!smartFolder || this.getIndexOfFolder(smartFolder) == null) {
|
|
newChild = new ftv_SmartItem(aItem);
|
|
newChild._level = 0;
|
|
while (newChildIndex < this.rowCount) {
|
|
if (this._rowMap[newChildIndex]._folder.flags & aFlag) {
|
|
// This type of folder seems to already exist, so replace the row
|
|
// with a smartFolder.
|
|
this._addSmartFoldersForFlag(this._rowMap, this._sortedAccounts(),
|
|
aSmartRoot, aFlag, aName, newChildIndex);
|
|
return;
|
|
}
|
|
if (this._rowMap[newChildIndex]._folder.isServer)
|
|
break;
|
|
newChildIndex++;
|
|
}
|
|
} else {
|
|
parentIndex = this.getIndexOfFolder(smartFolder);
|
|
parent = this._rowMap[parentIndex];
|
|
if (!parent)
|
|
return;
|
|
|
|
newChild = new ftv_SmartItem(aItem);
|
|
parent.children.push(newChild);
|
|
newChild._level = parent._level + 1;
|
|
newChild._parent = parent;
|
|
sortFolderItems(parent._children);
|
|
newChild.useServerNameOnly = true;
|
|
}
|
|
if (aItem.flags & nsMsgFolderFlags.Inbox)
|
|
newChild.__defineGetter__("children", () => []);
|
|
if (parent)
|
|
this._addChildToView(parent, parentIndex, newChild);
|
|
else {
|
|
this._rowMap.splice(newChildIndex, 0, newChild);
|
|
this._tree.rowCountChanged(newChildIndex, 1);
|
|
}
|
|
},
|
|
/**
|
|
* This is our implementation of nsIMsgFolderListener to watch for changes
|
|
*/
|
|
OnItemAdded: function ftl_add(aParentItem, aItem) {
|
|
// Ignore this item if it's not a folder, or we knew about it.
|
|
if (!(aItem instanceof Components.interfaces.nsIMsgFolder) ||
|
|
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(Components.interfaces.nsIMsgFolder), aItem);
|
|
},
|
|
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 & nsMsgFolderFlags.SpecialUse) {
|
|
this._toggleRow(parentIndex, false);
|
|
return;
|
|
}
|
|
}
|
|
this._addChildToView(parent, parentIndex, newChild);
|
|
},
|
|
|
|
OnItemRemoved: function ftl_remove(aRDFParentItem, aItem) {
|
|
if (!(aItem instanceof Components.interfaces.nsIMsgFolder))
|
|
return;
|
|
|
|
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._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);
|
|
},
|
|
|
|
OnItemPropertyChanged: function(aItem, aProperty, aOld, aNew) {},
|
|
OnItemIntPropertyChanged: 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 Components.interfaces.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);
|
|
}
|
|
},
|
|
|
|
OnItemBoolPropertyChanged: function(aItem, aProperty, aOld, aNew) {
|
|
let index = this.getIndexOfFolder(aItem);
|
|
if (index != null)
|
|
this._tree.invalidateRow(index);
|
|
},
|
|
OnItemUnicharPropertyChanged: function(aItem, aProperty, aOld, aNew) {},
|
|
OnItemPropertyFlagChanged: function(aItem, aProperty, aOld, aNew) {},
|
|
OnItemEvent: 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 in a _mapGenerator, 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]);
|
|
}
|
|
|
|
// 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 "";
|
|
|
|
// From folderUtils.jsm
|
|
let properties = getFolderProperties(this._folder, this.open);
|
|
if (this._folder.getFlag(nsMsgFolderFlags.Virtual)) {
|
|
properties += " specialFolder-Smart";
|
|
// a second possibility for customized smart folders
|
|
properties += " specialFolder-" + this._folder.name.replace(/\s+/g, "");
|
|
}
|
|
// if there is a smartFolder name property, add it
|
|
let smartFolderName = getSmartFolderName(this._folder);
|
|
if (smartFolderName) {
|
|
properties += " specialFolder-" + smartFolderName.replace(/\s+/g, "");
|
|
}
|
|
|
|
if (this._folder.server.type == "rss" && !this._folder.isServer &&
|
|
FeedUtils.getFeedUrlsInFolder(this._folder))
|
|
properties += " isFeedFolder-true";
|
|
|
|
return properties;
|
|
},
|
|
|
|
command: function fti_command() {
|
|
if (!Services.prefs.getBoolPref("mailnews.reuse_thread_window2"))
|
|
MsgOpenNewWindowForFolder(this._folder.URI, -1 /* key */);
|
|
},
|
|
|
|
_children: null,
|
|
get children() {
|
|
const Ci = Components.interfaces;
|
|
// 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 in 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 commmands 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: function ftc_newFolder(aParent) {
|
|
let folder = aParent || gFolderTreeView.getSelectedFolders()[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();
|
|
}
|
|
|
|
if (!folder)
|
|
return;
|
|
|
|
let dualUseFolders = true;
|
|
if (folder.server instanceof Components.interfaces.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 also 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,resizable=no,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: function ftc_editFolder(aTabID, aFolder) {
|
|
let folder = aFolder || gFolderTreeView.getSelectedFolders()[0];
|
|
|
|
// If this is actually a server, send it off to that controller
|
|
if (folder.isServer) {
|
|
MsgAccountManager(null, folder.server);
|
|
return;
|
|
}
|
|
|
|
if (folder.flags & nsMsgFolderFlags.Virtual) {
|
|
this.editVirtualFolder(folder);
|
|
return;
|
|
}
|
|
|
|
let title = gFolderTreeView.messengerBundle
|
|
.getString("folderProperties");
|
|
|
|
//xxx useless param
|
|
function editFolderCallback(aNewName, aOldName, aUri) {
|
|
if (aNewName != aOldName)
|
|
folder.rename(aNewName, msgWindow);
|
|
}
|
|
|
|
//xxx useless param
|
|
function rebuildSummary(aFolder) {
|
|
// folder is already introduced in our containing function and is
|
|
// lexically captured and available to us.
|
|
if (folder.locked) {
|
|
folder.throwAlertMsg("operationFailedFolderBusy", msgWindow);
|
|
return;
|
|
}
|
|
if (folder.supportsOffline) {
|
|
// Remove the offline store, if any.
|
|
let offlineStore = folder.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);
|
|
}
|
|
gFolderDisplay.view.close();
|
|
|
|
// Send a notification that we are triggering a database rebuild.
|
|
MailServices.mfn.notifyItemEvent(folder, "FolderReindexTriggered", null);
|
|
|
|
folder.msgDatabase.summaryValid = false;
|
|
|
|
var msgDB = folder.msgDatabase;
|
|
msgDB.summaryValid = false;
|
|
try {
|
|
folder.closeAndBackupFolderDB("");
|
|
}
|
|
catch(e) {
|
|
// In a failure, proceed anyway since we're dealing with problems
|
|
folder.ForceDBClosed();
|
|
}
|
|
folder.updateFolder(msgWindow);
|
|
gFolderDisplay.show(folder);
|
|
}
|
|
|
|
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: function ftc_rename(aFolder) {
|
|
let folder = aFolder || gFolderTreeView.getSelectedFolders()[0];
|
|
|
|
//xxx no need for uri now
|
|
let controller = this;
|
|
function renameCallback(aName, aUri) {
|
|
if (aUri != folder.URI)
|
|
Components.utils.reportError("got back a different folder to rename!");
|
|
|
|
controller._tree.view.selection.clearSelection();
|
|
|
|
// Actually do the rename
|
|
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: function ftc_delete(aFolder) {
|
|
const Ci = Components.interfaces;
|
|
let folders = aFolder ? [aFolder] : gFolderTreeView.getSelectedFolders();
|
|
let folder = folders[0];
|
|
|
|
// For newsgroups, "delete" means "unsubscribe".
|
|
if (folder.server.type == "nntp" && !folder.getFlag(nsMsgFolderFlags.Virtual)) {
|
|
MsgUnsubscribe(folders);
|
|
return;
|
|
}
|
|
|
|
var canDelete = (folder.isSpecialFolder(nsMsgFolderFlags.Junk, false)) ?
|
|
CanRenameDeleteJunkMail(folder.URI) : folder.deletable;
|
|
|
|
if (!canDelete)
|
|
throw new Error("Can't delete folder: " + folder.name);
|
|
|
|
if (folder.flags & nsMsgFolderFlags.Virtual) {
|
|
let confirmation = gFolderTreeView.messengerBundle
|
|
.getString("confirmSavedSearchDeleteMessage");
|
|
let title = gFolderTreeView.messengerBundle
|
|
.getString("confirmSavedSearchTitle");
|
|
if (Services.prompt
|
|
.confirmEx(window, title, confirmation,
|
|
Services.prompt.STD_YES_NO_BUTTONS + Services.prompt.BUTTON_POS_1_DEFAULT,
|
|
"", "", "", "", {}) != 0) /* the yes button is in position 0 */
|
|
return;
|
|
}
|
|
|
|
let array = toXPCOMArray([folder], Ci.nsIMutableArray);
|
|
folder.parent.deleteSubFolders(array, msgWindow);
|
|
},
|
|
|
|
/**
|
|
* 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
|
|
* @note Calling this function on a non-trash folder will result in strange
|
|
* behavior!
|
|
*/
|
|
emptyTrash: function ftc_emptyTrash(aFolder) {
|
|
let folder = aFolder || gFolderTreeView.getSelectedFolders()[0];
|
|
|
|
if (!folder)
|
|
return;
|
|
|
|
if (!this._checkConfirmationPrompt("emptyTrash", folder))
|
|
return;
|
|
|
|
// Check if this is a top-level smart folder. If so, we're going
|
|
// to empty all the trash folders.
|
|
if (folder.server.hostName == "smart mailboxes" &&
|
|
folder.parent.isServer) {
|
|
let subFolders = gFolderTreeView
|
|
._allFoldersWithFlag(gFolderTreeView._sortedAccounts(),
|
|
nsMsgFolderFlags.Trash, false);
|
|
for (let trash of subFolders)
|
|
trash.emptyTrash(msgWindow, null);
|
|
}
|
|
else {
|
|
folder.emptyTrash(msgWindow, 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
|
|
*/
|
|
emptyJunk: function ftc_emptyJunk(aFolder) {
|
|
const Ci = Components.interfaces;
|
|
let folder = aFolder || gFolderTreeView.getSelectedFolders()[0];
|
|
|
|
if (!folder || !folder.getFlag(nsMsgFolderFlags.Junk))
|
|
return;
|
|
|
|
if (!this._checkConfirmationPrompt("emptyJunk", folder))
|
|
return;
|
|
|
|
// Delete any subfolders this folder might have
|
|
let iter = folder.subFolders;
|
|
while (iter.hasMoreElements())
|
|
folder.propagateDelete(iter.getNext(), true, msgWindow);
|
|
|
|
// Now delete the messages
|
|
let messages = Array.from(fixIterator(folder.messages));
|
|
let children = toXPCOMArray(messages, Ci.nsIMutableArray);
|
|
folder.deleteMessages(children, 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: function ftc_compactFolders(aFolders) {
|
|
let folders = aFolders || gFolderTreeView.getSelectedFolders();
|
|
for (let i = 0; i < folders.length; i++) {
|
|
// Can't compact folders that have just been compacted.
|
|
if (folders[i].server.type != "imap" && !folders[i].expungedBytes)
|
|
continue;
|
|
|
|
folders[i].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: function ftc_compactAllFoldersOfAccount(aFolders) {
|
|
let folders = aFolders || gFolderTreeView.getSelectedFolders();
|
|
for (let i = 0; i < folders.length; i++) {
|
|
folders[i].compactAll(null, msgWindow, folders[i].server.type == "imap" ||
|
|
folders[i].server.type == "nntp");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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: function ftc_newVFolder(aName, aSearchTerms, aParent) {
|
|
let folder = aParent || gFolderTreeView.getSelectedFolders()[0] ||
|
|
GetDefaultAccountRootFolder();
|
|
if (!folder)
|
|
return;
|
|
|
|
let name = folder.prettyName;
|
|
if (aName)
|
|
name += "-" + aName;
|
|
|
|
window.openDialog("chrome://messenger/content/virtualFolderProperties.xul",
|
|
"",
|
|
"chrome,modal,centerscreen",
|
|
{folder: folder, searchTerms: aSearchTerms,
|
|
newFolderName: name});
|
|
},
|
|
|
|
editVirtualFolder: function ftc_editVirtualFolder(aFolder) {
|
|
let folder = aFolder || gFolderTreeView.getSelectedFolders()[0];
|
|
|
|
//xxx should pass the folder object
|
|
function editVirtualCallback(aURI) {
|
|
// we need to reload the folder if it is the currently loaded folder...
|
|
if (gFolderDisplay.displayedFolder &&
|
|
aURI == gFolderDisplay.displayedFolder.URI)
|
|
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: function ftc_searchMessages(aFolder) {
|
|
MsgSearchMessages(aFolder || gFolderTreeView.getSelectedFolders()[0]);
|
|
},
|
|
|
|
/**
|
|
* Prompts for confirmation, if the user hasn't already chosen the "don't ask
|
|
* again" option.
|
|
*
|
|
* @param aCommand the command to prompt for
|
|
* @param aFolder The folder for which the confirmation is requested.
|
|
*/
|
|
_checkConfirmationPrompt: function ftc_confirm(aCommand, aFolder) {
|
|
// If no folder was specified, reject the operation.
|
|
if (!aFolder)
|
|
return false;
|
|
|
|
let showPrompt = true;
|
|
try {
|
|
showPrompt = !Services.prefs.getBoolPref("mailnews." + aCommand + ".dontAskAgain");
|
|
} catch (ex) {}
|
|
|
|
if (showPrompt) {
|
|
let checkbox = {value:false};
|
|
let title = gFolderTreeView.messengerBundle
|
|
.getFormattedString(aCommand + "FolderTitle", [aFolder.prettyName]);
|
|
let msg = gFolderTreeView.messengerBundle.getString(aCommand + "FolderMessage");
|
|
let ok = Services.prompt.confirmEx(window,
|
|
title,
|
|
msg,
|
|
Services.prompt.STD_YES_NO_BUTTONS,
|
|
null, null, null,
|
|
gFolderTreeView.messengerBundle.getString(aCommand + "DontAsk"),
|
|
checkbox) == 0;
|
|
if (checkbox.value)
|
|
Services.prefs.setBoolPref("mailnews." + aCommand + ".dontAskAgain", true);
|
|
if (!ok)
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
get _tree() {
|
|
let tree = document.getElementById("folderTree");
|
|
delete this._tree;
|
|
return this._tree = tree;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Constructor for ftv_SmartItem. This is a top level item in the "smart"
|
|
* (a.k.a. "Unified") folder mode.
|
|
*/
|
|
function ftv_SmartItem(aFolder)
|
|
{
|
|
ftvItem.call(this, aFolder); // call super constructor
|
|
this._level = 0;
|
|
}
|
|
|
|
ftv_SmartItem.prototype = {
|
|
__proto__: ftvItem.prototype,
|
|
get children() {
|
|
const Ci = Components.interfaces;
|
|
let smartMode = gFolderTreeView.getFolderTreeMode("smart");
|
|
|
|
// We're caching our child list to save perf.
|
|
if (!this._children) {
|
|
this._children = [];
|
|
let iter = fixIterator(this._folder.subFolders, Ci.nsIMsgFolder);
|
|
for (let folder in iter) {
|
|
if (!smartMode.isSmartFolder(folder)) {
|
|
this._children.push(new ftv_SmartItem(folder));
|
|
}
|
|
else if (folder.flags & nsMsgFolderFlags.Inbox) {
|
|
let subIter = fixIterator(folder.subFolders, Ci.nsIMsgFolder);
|
|
for (let subfolder in subIter) {
|
|
if (!smartMode.isSmartFolder(subfolder))
|
|
this._children.push(new ftv_SmartItem(subfolder));
|
|
}
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* An extension wishing to set a folderpane tree property must use
|
|
* gFolderTreeView.setFolderCacheProperty(). Due to severe perf and memory
|
|
* issues, direct access by nsITreeView methods to any call which opens a
|
|
* folder's msgDatabase is disallowed.
|
|
*
|
|
* Example:
|
|
* gFolderTreeView.setFolderCacheProperty(folder, // nsIMsgFolder
|
|
* "smartFolderName",
|
|
* "My Smart Folder");
|
|
* Note: for css styling using nsITreeView pseudo elements, the name property
|
|
* is returned with all spaces removed, eg |specialFolder-MySmartFolder|.
|
|
*
|
|
* @param nsIMsgFolder aFolder - The folder.
|
|
* @return property || null - Cached property value, or null if not set.
|
|
*/
|
|
function getSmartFolderName(aFolder) {
|
|
return gFolderTreeView.getFolderCacheProperty(aFolder, "smartFolderName");
|
|
}
|
|
|
|
function setSmartFolderName(aFolder, aName) {
|
|
gFolderTreeView.setFolderCacheProperty(aFolder, "smartFolderName", aName);
|
|
}
|
|
|
|
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(Components.interfaces.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];
|
|
}
|
|
};
|