mirror of
https://github.com/ManchildProductions/binoc-central-mirror.git
synced 2026-06-15 09:39:07 +00:00
2809 lines
106 KiB
JavaScript
2809 lines
106 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/dbViewWrapper.js");
|
|
Components.utils.import("resource:///modules/jsTreeSelection.js");
|
|
Components.utils.import("resource:///modules/MailUtils.js");
|
|
Components.utils.import("resource://gre/modules/Services.jsm");
|
|
|
|
var gFolderDisplay = null;
|
|
var gMessageDisplay = null;
|
|
|
|
var nsMsgFolderFlags = Components.interfaces.nsMsgFolderFlags;
|
|
var nsMsgMessageFlags = Components.interfaces.nsMsgMessageFlags;
|
|
|
|
/**
|
|
* Maintains a list of listeners for all FolderDisplayWidget instances in this
|
|
* window. The assumption is that because of our multiplexed tab
|
|
* implementation all consumers are effectively going to care about all such
|
|
* tabs.
|
|
*
|
|
* We are not just a global list so that we can add brains about efficiently
|
|
* building lists, provide try-wrapper convenience, etc.
|
|
*/
|
|
var FolderDisplayListenerManager = {
|
|
_listeners: [],
|
|
|
|
/**
|
|
* Register a listener that implements one or more of the methods defined on
|
|
* |IDBViewWrapperListener|. Note that a change from those interface
|
|
* signatures is that the first argument is always a reference to the
|
|
* FolderDisplayWidget generating the notification.
|
|
*
|
|
* We additionally support the following notifications:
|
|
* - onMakeActive. Invoked when makeActive is called on the
|
|
* FolderDisplayWidget. The second argument (after the folder display) is
|
|
* aWasInactive.
|
|
*
|
|
* - onActiveCreatedView. onCreatedView deferred to when the tab is actually
|
|
* made active.
|
|
*
|
|
* - onActiveMessagesLoaded. onMessagesLoaded deferred to when the
|
|
* tab is actually made active. Use this if the actions you need to take
|
|
* are based on the folder display actually being visible, such as updating
|
|
* some UI widget, etc. Not all messages may have been loaded, but some.
|
|
*
|
|
*/
|
|
registerListener: function FDLM_registerListener(aListener) {
|
|
this._listeners.push(aListener);
|
|
},
|
|
|
|
/**
|
|
* Unregister a previously registered event listener.
|
|
*/
|
|
unregisterListener: function FDLM_unregisterListener(aListener) {
|
|
let idx = this._listeners.indexOf(aListener);
|
|
if (idx >= 0) {
|
|
this._listeners.splice(idx, 1);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* For use by FolderDisplayWidget to trigger listener invocation.
|
|
*/
|
|
_fireListeners: function FDBLM__fireListeners(aEventName, aArgs) {
|
|
for (let listener of this._listeners) {
|
|
if (aEventName in listener) {
|
|
try {
|
|
listener[aEventName].apply(listener, aArgs);
|
|
}
|
|
catch(e) {
|
|
Components.utils.reportError(aEventName + " event listener FAILED; " +
|
|
e + " at: " + e.stack);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Abstraction for a widget that (roughly speaking) displays the contents of
|
|
* folders. The widget belongs to a tab and has a lifetime as long as the tab
|
|
* that contains it. This class is strictly concerned with the UI aspects of
|
|
* this; the DBViewWrapper class handles the view details (and is exposed on
|
|
* the 'view' attribute.)
|
|
*
|
|
* The search window subclasses this into the SearchFolderDisplayWidget rather
|
|
* than us attempting to generalize everything excessively. This is because
|
|
* we hate the search window and don't want to clutter up this code for it.
|
|
* The standalone message display window also subclasses us; we do not hate it,
|
|
* but it's not invited to our birthday party either.
|
|
* For reasons of simplicity and the original order of implementation, this
|
|
* class does alter its behavior slightly for the benefit of the standalone
|
|
* message window. If no tab info is provided, we avoid touching tabmail
|
|
* (which is good, because it won't exist!) And now we guard against treeBox
|
|
* manipulations...
|
|
*/
|
|
function FolderDisplayWidget(aTabInfo, aMessageDisplayWidget) {
|
|
this._tabInfo = aTabInfo;
|
|
|
|
/// If the folder does not get handled by the DBViewWrapper, stash it here.
|
|
/// ex: when isServer is true.
|
|
this._nonViewFolder = null;
|
|
|
|
this.view = new DBViewWrapper(this);
|
|
this.messageDisplay = aMessageDisplayWidget;
|
|
this.messageDisplay.folderDisplay = this;
|
|
|
|
/**
|
|
* The XUL tree node, as retrieved by getDocumentElementById. The caller is
|
|
* responsible for setting this.
|
|
*/
|
|
this.tree = null;
|
|
/**
|
|
* The nsITreeBoxObject on the XUL tree node, accessible from this.tree as
|
|
* this.tree.boxObject and QueryInterfaced as such. The caller is
|
|
* responsible for setting this.
|
|
*/
|
|
this.treeBox = null;
|
|
|
|
/**
|
|
* The nsIMsgWindow corresponding to the window that holds us. There is only
|
|
* one of these per tab. The caller is responsible for setting this.
|
|
*/
|
|
this.msgWindow = null;
|
|
/**
|
|
* The nsIMessenger instance that corresponds to our tab/window. We do not
|
|
* use this ourselves, but are responsible for using it to update the
|
|
* global |messenger| object so that our tab maintains its own undo and
|
|
* navigation history. At some point we might touch it for those reasons.
|
|
*/
|
|
this.messenger = null;
|
|
this.threadPaneCommandUpdater = this;
|
|
|
|
/**
|
|
* Flag to expose whether all messages are loaded or not. Set by
|
|
* onMessagesLoaded() when aAll is true.
|
|
*/
|
|
this._allMessagesLoaded = false;
|
|
|
|
/**
|
|
* Save the top row displayed when we go inactive, restore when we go active,
|
|
* nuke it when we destroy the view.
|
|
*/
|
|
this._savedFirstVisibleRow = null;
|
|
/** the next view index to select once the delete completes */
|
|
this._nextViewIndexAfterDelete = null;
|
|
/**
|
|
* Track when a mass move is in effect (we get told by hintMassMoveStarting,
|
|
* and hintMassMoveCompleted) so that we can avoid deletion-triggered
|
|
* moving to _nextViewIndexAfterDelete until the mass move completes.
|
|
*/
|
|
this._massMoveActive = false;
|
|
|
|
/**
|
|
* Used by pushNavigation to queue a navigation request for when we enter the
|
|
* next folder; onMessagesLoaded(true) is the one that processes it.
|
|
*/
|
|
this._pendingNavigation = null;
|
|
|
|
this._active = false;
|
|
/**
|
|
* A list of methods to call on 'this' object when we are next made active.
|
|
* This list is populated by calls to |_notifyWhenActive| when we are
|
|
* not active at the moment.
|
|
*/
|
|
this._notificationsPendingActivation = [];
|
|
|
|
// Create a DOM node for the fake tree box below.
|
|
let domNode = document.createElementNS(
|
|
"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "vbox");
|
|
|
|
// We care about onselect events, so add a listener for that.
|
|
let self = this;
|
|
domNode.addEventListener("select", function () {
|
|
self.view.dbView.selectionChanged();
|
|
}, false);
|
|
|
|
/**
|
|
* Create a fake tree box object for if/when this folder is in the background.
|
|
* We need to give it a DOM object to send events to, including the onselect
|
|
* event we care about and for which we added a handler above, and all the
|
|
* other events we don't care about.
|
|
*/
|
|
this._fakeTreeBox = new FakeTreeBoxObject(domNode);
|
|
|
|
/**
|
|
* Create a fake tree selection for cases where we have opened a background
|
|
* tab. We'll get rid of this as soon as we've switched to the tab for the
|
|
* first time, and have a real tree selection.
|
|
*/
|
|
this._fakeTreeSelection = new JSTreeSelection(this._fakeTreeBox);
|
|
|
|
this._mostRecentSelectionCounts = [];
|
|
this._mostRecentCurrentIndices = [];
|
|
}
|
|
FolderDisplayWidget.prototype = {
|
|
/**
|
|
* @return the currently displayed folder. This is just proxied from the
|
|
* view wrapper.
|
|
* @groupName Displayed
|
|
*/
|
|
get displayedFolder() {
|
|
return this._nonViewFolder || this.view.displayedFolder;
|
|
},
|
|
|
|
/**
|
|
* @return true if the selection should be summarized for this folder. This
|
|
* is based on the mail.operate_on_msgs_in_collapsed_threads pref and
|
|
* if we are in a newsgroup folder. XXX When bug 478167 is fixed, this
|
|
* should be limited to being disabled for newsgroups that are not stored
|
|
* offline.
|
|
*/
|
|
get summarizeSelectionInFolder() {
|
|
return Services.prefs.getBoolPref("mail.operate_on_msgs_in_collapsed_threads") &&
|
|
!(this.displayedFolder instanceof Components.interfaces.nsIMsgNewsFolder);
|
|
},
|
|
|
|
/**
|
|
* @return the nsITreeSelection object for our tree view. This exists for
|
|
* the benefit of message tabs that haven't been switched to yet.
|
|
* We provide a fake tree selection in those cases.
|
|
* @protected
|
|
*/
|
|
get treeSelection() {
|
|
// If we haven't switched to this tab yet, dbView will exist but
|
|
// dbView.selection won't, so use the fake tree selection instead.
|
|
if (this._fakeTreeSelection)
|
|
return this._fakeTreeSelection;
|
|
if (this.view.dbView)
|
|
return this.view.dbView.selection;
|
|
else
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Determine which pane currently has focus (one of the folder pane, thread
|
|
* pane, or message pane). The message pane node is the common ancestor of
|
|
* the single- and multi-message content windows. When changing focus to the
|
|
* message pane, be sure to focus the appropriate content window in addition
|
|
* to the messagepanebox (doing both is required in order to blur the
|
|
* previously-focused chrome element).
|
|
*
|
|
* @return the focused pane
|
|
*/
|
|
get focusedPane() {
|
|
let panes = ["threadTree", "folderTree", "messagepanebox"].map(id =>
|
|
document.getElementById(id));
|
|
|
|
let currentNode = top.document.activeElement;
|
|
|
|
while (currentNode) {
|
|
if (panes.includes(currentNode))
|
|
return currentNode;
|
|
|
|
currentNode = currentNode.parentNode;
|
|
}
|
|
return null;
|
|
},
|
|
|
|
|
|
/**
|
|
* Number of headers to tell the message database to cache when we enter a
|
|
* folder. This value is being propagated from legacy code which provided
|
|
* no explanation for its choice.
|
|
*
|
|
* We definitely want the header cache size to be larger than the number of
|
|
* rows that can be displayed on screen simultaneously.
|
|
*
|
|
* @private
|
|
*/
|
|
PERF_HEADER_CACHE_SIZE: 100,
|
|
|
|
/**
|
|
* @name Selection Persistence
|
|
* @private
|
|
*/
|
|
//@{
|
|
|
|
/**
|
|
* An optional object, with the following properties:
|
|
* - messages: This is a list where each item is an object with the following
|
|
* attributes sufficient to re-establish the selected items even in the
|
|
* face of folder renaming.
|
|
* - messageId: The value of the message's message-id header.
|
|
*
|
|
* That's right, we only save the message-id header value. This is arguably
|
|
* overkill and ambiguous in the face of duplicate messages, but it's the
|
|
* most persistent/reliable thing we have without gloda.
|
|
* Using the view index was ruled out because it is hardly stable. Using the
|
|
* message key alone is insufficient for cross-folder searches. Using a
|
|
* folder identifier and message key is insufficent for local folders in the
|
|
* face of compaction, let alone complexities where the folder name may
|
|
* change due to renaming/moving. Which means we eventually need to fall
|
|
* back to message-id anyways. Feel free to add in lots of complexity if
|
|
* you actually write unit tests for all the many possible cases.
|
|
* Additional justification is that selection saving/restoration should not
|
|
* happen all that frequently. A nice freebie is that message-id is
|
|
* definitely persistable.
|
|
*
|
|
* - forceSelect: Whether we are allowed to drop all filters in our quest to
|
|
* select messages.
|
|
*/
|
|
_savedSelection: null,
|
|
|
|
/**
|
|
* Save the current view selection for when we the view is getting destroyed
|
|
* or otherwise re-ordered in such a way that the nsITreeSelection will lose
|
|
* track of things (because it just has a naive view-index 'view' of the
|
|
* world.) We just save each message's message-id header. This is overkill
|
|
* and ambiguous in the face of duplicate messages (and expensive to
|
|
* restore), but is also the most reliable option for this use case.
|
|
*/
|
|
_saveSelection: function FolderDisplayWidget_saveSelection() {
|
|
this._savedSelection = {
|
|
messages: this.selectedMessages.map(msgHdr => ({messageId: msgHdr.messageId})),
|
|
forceSelect: false
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Clear the saved selection.
|
|
*/
|
|
_clearSavedSelection: function FolderDisplayWidget_clearSavedSelection() {
|
|
this._savedSelection = null;
|
|
},
|
|
|
|
/**
|
|
* Restore the view selection if we have a saved selection. We must be
|
|
* active!
|
|
*
|
|
* @return true if we were able to restore the selection and there was
|
|
* a selection, false if there was no selection (anymore).
|
|
*/
|
|
_restoreSelection: function FolderDisplayWidget_restoreSelection() {
|
|
if (!this._savedSelection || !this._active)
|
|
return false;
|
|
|
|
// translate message IDs back to messages. this is O(s(m+n)) where:
|
|
// - s is the number of messages saved in the selection
|
|
// - m is the number of messages in the view (from findIndexOfMsgHdr)
|
|
// - n is the number of messages in the underlying folders (from
|
|
// DBViewWrapper.getMsgHdrForMessageID).
|
|
// which ends up being O(sn)
|
|
var msgHdr;
|
|
let messages =
|
|
this._savedSelection.messages.
|
|
map(savedInfo => this.view.getMsgHdrForMessageID(savedInfo.messageId)).
|
|
filter(msgHdr => !!msgHdr);
|
|
|
|
this.selectMessages(messages, this._savedSelection.forceSelect, true);
|
|
this._savedSelection = null;
|
|
|
|
return this.selectedCount != 0;
|
|
},
|
|
|
|
/**
|
|
* Restore the last expandAll/collapseAll state, for both grouped and threaded
|
|
* views. Not all views respect viewFlags, ie single folder non-virtual.
|
|
*/
|
|
restoreThreadState: function() {
|
|
if (!this._active || !this.tree || !this.view.dbView.viewFolder)
|
|
return;
|
|
|
|
if (this.view._threadExpandAll &&
|
|
!(this.view.dbView.viewFlags & nsMsgViewFlagsType.kExpandAll))
|
|
this.view.dbView.doCommand(Components.interfaces.nsMsgViewCommandType.expandAll);
|
|
if (!this.view._threadExpandAll &&
|
|
this.view.dbView.viewFlags & nsMsgViewFlagsType.kExpandAll)
|
|
this.view.dbView.doCommand(Components.interfaces.nsMsgViewCommandType.collapseAll);
|
|
},
|
|
//@}
|
|
|
|
/**
|
|
* @name Columns
|
|
* @protected Folder Display
|
|
*/
|
|
//@{
|
|
|
|
/**
|
|
* The map of all stock sortable columns and their sortType. The key must
|
|
* match the column's xul <treecol> id.
|
|
*/
|
|
COLUMNS_MAP: new Map([
|
|
["accountCol", "byAccount"],
|
|
["attachmentCol", "byAttachments"],
|
|
["senderCol", "byAuthor"],
|
|
["correspondentCol", "byCorrespondent"],
|
|
["dateCol", "byDate"],
|
|
["flaggedCol", "byFlagged"],
|
|
["idCol", "byId"],
|
|
["junkStatusCol", "byJunkStatus"],
|
|
["locationCol", "byLocation"],
|
|
["priorityCol", "byPriority"],
|
|
["receivedCol", "byReceived"],
|
|
["recipientCol", "byRecipient"],
|
|
["sizeCol", "bySize"],
|
|
["statusCol", "byStatus"],
|
|
["subjectCol", "bySubject"],
|
|
["tagsCol", "byTags"],
|
|
["threadCol", "byThread"],
|
|
["unreadButtonColHeader", "byUnread"]
|
|
]),
|
|
|
|
/**
|
|
* The map of stock non-sortable columns. The key must match the column's
|
|
* xul <treecol> id.
|
|
*/
|
|
COLUMNS_MAP_NOSORT: new Set([
|
|
"totalCol", "unreadCol"
|
|
]),
|
|
|
|
/**
|
|
* The set of potential default columns in their default display order. Each
|
|
* column in this list is checked against |COLUMN_DEFAULT_TESTERS| to see if
|
|
* it is actually an appropriate default for the folder type.
|
|
*/
|
|
DEFAULT_COLUMNS: [
|
|
"threadCol",
|
|
"attachmentCol",
|
|
"flaggedCol",
|
|
"subjectCol",
|
|
"unreadButtonColHeader",
|
|
"senderCol", // news folders or incoming folders when correspondents not in use
|
|
"recipientCol", // outgoing folders when correspondents not in use
|
|
"correspondentCol", // mail folders
|
|
"junkStatusCol",
|
|
"dateCol",
|
|
"locationCol", // multiple-folder backed folders
|
|
],
|
|
|
|
/**
|
|
* Maps column ids to functions that test whether the column is a good default
|
|
* for display for the folder. Each function should expect a DBViewWrapper
|
|
* instance as its argument. The intent is that the various helper
|
|
* properties like isMailFolder/isIncomingFolder/isOutgoingFolder allow the
|
|
* constraint to be expressed concisely. If a helper does not exist, add
|
|
* one! (If doing so is out of reach, than access viewWrapper.displayedFolder
|
|
* to get at the nsIMsgFolder.)
|
|
* If a column does not have a function, it is assumed that it should be
|
|
* displayed by default.
|
|
*/
|
|
COLUMN_DEFAULT_TESTERS: {
|
|
correspondentCol: function (viewWrapper) {
|
|
if (Services.prefs.getBoolPref("mail.threadpane.use_correspondents")) {
|
|
// Don't show the correspondent for news or RSS where it doesn't make sense.
|
|
return viewWrapper.isMailFolder && !viewWrapper.isFeedFolder;
|
|
}
|
|
return false;
|
|
},
|
|
senderCol: function (viewWrapper) {
|
|
if (Services.prefs.getBoolPref("mail.threadpane.use_correspondents")) {
|
|
// Show the sender even if correspondent is enabled for news and feeds.
|
|
return viewWrapper.isNewsFolder || viewWrapper.isFeedFolder;
|
|
}
|
|
// senderCol = From. You only care in incoming folders.
|
|
return viewWrapper.isIncomingFolder;
|
|
},
|
|
recipientCol: function (viewWrapper) {
|
|
if (Services.prefs.getBoolPref("mail.threadpane.use_correspondents")) {
|
|
// No recipient column if we use correspondent.
|
|
return false;
|
|
}
|
|
// recipientCol = To. You only care in outgoing folders.
|
|
return viewWrapper.isOutgoingFolder;
|
|
},
|
|
// Only show the location column for non-single-folder results
|
|
locationCol: function(viewWrapper) {
|
|
return !viewWrapper.isSingleFolder;
|
|
},
|
|
// core UI does not provide an ability to mark newsgroup messages as spam
|
|
junkStatusCol: function(viewWrapper) {
|
|
return !viewWrapper.isNewsFolder;
|
|
},
|
|
},
|
|
|
|
/**
|
|
* The property name we use to store the column states on the
|
|
* dbFolderInfo.
|
|
*/
|
|
PERSISTED_COLUMN_PROPERTY_NAME: "columnStates",
|
|
|
|
/**
|
|
* Given a dbFolderInfo, extract the persisted state from it if there is any.
|
|
*
|
|
* @return null if there was no persisted state, the persisted state in object
|
|
* form otherwise. (Ideally the state conforms to the documentation on
|
|
* |_savedColumnStates| but we can't stop people from doing bad things.)
|
|
*/
|
|
_depersistColumnStatesFromDbFolderInfo:
|
|
function FolderDisplayWidget__depersistColumnStatesFromDBFolderInfo(
|
|
aDbFolderInfo) {
|
|
let columnJsonString =
|
|
aDbFolderInfo.getCharProperty(this.PERSISTED_COLUMN_PROPERTY_NAME);
|
|
if (!columnJsonString)
|
|
return null;
|
|
|
|
return JSON.parse(columnJsonString);
|
|
},
|
|
|
|
/**
|
|
* Persist the column state for the currently displayed folder. We are
|
|
* assuming that the message database is already open when we are called and
|
|
* therefore that we do not need to worry about cleaning up after the message
|
|
* database.
|
|
* The caller should only call this when they have reason to suspect that the
|
|
* column state has been changed. This could be because there was no
|
|
* persisted state so we figured out a default one and want to save it.
|
|
* Otherwise this should be because the user explicitly changed up the column
|
|
* configurations. You should not call this willy-nilly.
|
|
*
|
|
* @param aState State to persist.
|
|
*/
|
|
_persistColumnStates: function FolderDisplayWidget__persistColumnStates(aState) {
|
|
if (this.view.isSynthetic) {
|
|
let syntheticView = this.view._syntheticView;
|
|
if ("setPersistedSetting" in syntheticView)
|
|
syntheticView.setPersistedSetting("columns", aState);
|
|
return;
|
|
}
|
|
|
|
if (!this.view.displayedFolder || !this.view.displayedFolder.msgDatabase)
|
|
return;
|
|
|
|
let msgDatabase = this.view.displayedFolder.msgDatabase;
|
|
let dbFolderInfo = msgDatabase.dBFolderInfo;
|
|
dbFolderInfo.setCharProperty(this.PERSISTED_COLUMN_PROPERTY_NAME,
|
|
JSON.stringify(aState));
|
|
msgDatabase.Commit(Components.interfaces.nsMsgDBCommitType.kLargeCommit);
|
|
},
|
|
|
|
/**
|
|
* Let us know that the state of the columns has changed. This is either due
|
|
* to a re-ordering or hidden-ness being toggled.
|
|
*
|
|
* This method should only be called on (the active) gFolderDisplay.
|
|
*/
|
|
hintColumnsChanged: function FolderDisplayWidget_hintColumnsChanged() {
|
|
// ignore this if we are the ones doing things
|
|
if (this._touchingColumns)
|
|
return;
|
|
this._persistColumnStates(this.getColumnStates());
|
|
},
|
|
|
|
/**
|
|
* Either inherit the column state of another folder or use heuristics to
|
|
* figure out the best column state for the current folder.
|
|
*/
|
|
_getDefaultColumnsForCurrentFolder: function(aDoNotInherit) {
|
|
const InboxFlag = Components.interfaces.nsMsgFolderFlags.Inbox;
|
|
|
|
// If the view is synthetic, try asking it for its default columns. If it
|
|
// fails, just return nothing, since most synthetic views don't care about
|
|
// columns anyway.
|
|
if (this.view.isSynthetic) {
|
|
if ("getDefaultSetting" in this.view._syntheticView)
|
|
return this.view._syntheticView.getDefaultSetting("columns");
|
|
return {};
|
|
}
|
|
|
|
// do not inherit from the inbox if:
|
|
// - It's an outgoing folder; these have a different use-case and there
|
|
// should be a small number of these, so it's okay to have no defaults.
|
|
// - It's a virtual folder (single or multi-folder backed). Who knows what
|
|
// the intent of the user is in this case. This should also be bounded
|
|
// in number and our default heuristics should be pretty good.
|
|
// - It's a multiple folder; this is either a search view (which has no
|
|
// displayed folder) or a virtual folder (which we eliminated above).
|
|
// - News folders. There is no inbox so there's nothing to inherit from.
|
|
// (Although we could try and see if they have opened any other news
|
|
// folders in the same account. But it's not all that important to us.)
|
|
// - It's an inbox!
|
|
let doNotInherit = aDoNotInherit ||
|
|
this.view.isOutgoingFolder ||
|
|
this.view.isVirtual ||
|
|
this.view.isMultiFolder ||
|
|
this.view.isNewsFolder ||
|
|
this.displayedFolder.flags & InboxFlag;
|
|
|
|
// Try and grab the inbox for this account's settings. we may not be able
|
|
// to, in which case we just won't inherit. (It ends up the same since the
|
|
// inbox is obviously not customized in this case.)
|
|
if (!doNotInherit) {
|
|
let inboxFolder =
|
|
this.displayedFolder.rootFolder.getFolderWithFlags(InboxFlag);
|
|
if (inboxFolder) {
|
|
let state = this._depersistColumnStatesFromDbFolderInfo(
|
|
inboxFolder.msgDatabase.dBFolderInfo);
|
|
// inbox message databases don't get closed as a matter of policy.
|
|
|
|
if (state)
|
|
return state;
|
|
}
|
|
}
|
|
|
|
// if we are still here, use the defaults and helper functions
|
|
let state = {};
|
|
for (let colId of this.DEFAULT_COLUMNS) {
|
|
let shouldShowColumn = true;
|
|
if (colId in this.COLUMN_DEFAULT_TESTERS) {
|
|
// This is potentially going to be used by extensions; avoid them
|
|
// killing us.
|
|
try {
|
|
shouldShowColumn = this.COLUMN_DEFAULT_TESTERS[colId](this.view);
|
|
}
|
|
catch (ex) {
|
|
shouldShowColumn = false;
|
|
Components.utils.reportError(ex);
|
|
}
|
|
}
|
|
state[colId] = {visible: shouldShowColumn};
|
|
}
|
|
return state;
|
|
},
|
|
|
|
/**
|
|
* Is setColumnStates messing with the columns' DOM? This is used by
|
|
* hintColumnsChanged to avoid wasteful state persistence.
|
|
*/
|
|
_touchingColumns: false,
|
|
|
|
/**
|
|
* Set the column states of this FolderDisplay to the provided state.
|
|
*
|
|
* @param aColumnStates an object of the form described on
|
|
* |_savedColumnStates|. If ordinal attributes are omitted then no
|
|
* re-ordering will be performed. This is intentional, but potentially a
|
|
* bad idea. (Right now only gloda search underspecifies ordinals.)
|
|
* @param [aPersistChanges=false] Should we persist the changes to the view?
|
|
* This only has an effect if we are active.
|
|
*
|
|
* @public
|
|
*/
|
|
setColumnStates: function(aColumnStates, aPersistChanges) {
|
|
// If we are not active, just overwrite our current state with the provided
|
|
// state and bail.
|
|
if (!this._active) {
|
|
this._savedColumnStates = aColumnStates;
|
|
return;
|
|
}
|
|
|
|
this._touchingColumns = true;
|
|
|
|
try {
|
|
let cols = document.getElementById("threadCols");
|
|
let colChildren = cols.children;
|
|
|
|
for (let iKid = 0; iKid < colChildren.length; iKid++) {
|
|
let colChild = colChildren[iKid];
|
|
if (colChild == null)
|
|
continue;
|
|
|
|
// We only care about treecols. The splitters do not need to be marked
|
|
// hidden or un-hidden.
|
|
if (colChild.tagName == "treecol") {
|
|
// if it doesn't have preserved state it should be hidden
|
|
let shouldBeHidden = true;
|
|
// restore state
|
|
if (colChild.id in aColumnStates) {
|
|
let colState = aColumnStates[colChild.id];
|
|
if ("visible" in colState)
|
|
shouldBeHidden = !colState.visible;
|
|
if (("ordinal" in colState) &&
|
|
colChild.getAttribute("ordinal") != colState.ordinal)
|
|
colChild.setAttribute("ordinal", colState.ordinal);
|
|
}
|
|
let isHidden = colChild.getAttribute("hidden") == "true";
|
|
if (isHidden != shouldBeHidden) {
|
|
if (shouldBeHidden)
|
|
colChild.setAttribute("hidden", "true");
|
|
else
|
|
colChild.removeAttribute("hidden");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
finally {
|
|
this._touchingColumns = false;
|
|
}
|
|
|
|
if (aPersistChanges)
|
|
this.hintColumnsChanged();
|
|
},
|
|
|
|
/**
|
|
* A dictionary that maps column ids to dictionaries where each dictionary
|
|
* has the following fields:
|
|
* - visible: Is the column visible.
|
|
* - ordinal: The 1-based XUL 'ordinal' value assigned to the column. This
|
|
* corresponds to the position but is not something you want to manipulate.
|
|
* See the documentation in _saveColumnStates for more information.
|
|
*/
|
|
_savedColumnStates: null,
|
|
|
|
/**
|
|
* Return a dictionary in the form of |_savedColumnStates| representing the
|
|
* current column states.
|
|
*
|
|
* @public
|
|
*/
|
|
getColumnStates: function FolderDisplayWidget_getColumnStates() {
|
|
if (!this._active)
|
|
return this._savedColumnStates;
|
|
|
|
let columnStates = {};
|
|
|
|
let cols = document.getElementById("threadCols");
|
|
let colChildren = cols.children;
|
|
for (let iKid = 0; iKid < colChildren.length; iKid++) {
|
|
let colChild = colChildren[iKid];
|
|
if (colChild.tagName != "treecol")
|
|
continue;
|
|
columnStates[colChild.id] = {
|
|
visible: colChild.getAttribute("hidden") != "true",
|
|
ordinal: colChild.getAttribute("ordinal"),
|
|
};
|
|
}
|
|
|
|
return columnStates;
|
|
},
|
|
|
|
/**
|
|
* For now, just save the visible columns into a dictionary for use in a
|
|
* subsequent call to |setColumnStates|.
|
|
*/
|
|
_saveColumnStates: function FolderDisplayWidget__saveColumnStates() {
|
|
// In the actual nsITreeColumn, the index property indicates the column
|
|
// number. This column number is a 0-based index with no gaps; it only
|
|
// increments the number each time it sees a column.
|
|
// However, this is subservient to the 'ordinal' property which
|
|
// defines the _apparent content sequence_ provided by GetNextSibling.
|
|
// The underlying content ordering is still the same, which is how
|
|
// restoreNaturalOrder can reset things to their XUL definition sequence.
|
|
// The 'ordinal' stuff works because nsBoxFrame::RelayoutChildAtOrdinal
|
|
// messes with the sibling relationship.
|
|
// Ordinals are 1-based. restoreNaturalOrder apparently is dumb and does
|
|
// not know this, although the ordering is relative so it doesn't actually
|
|
// matter. The annoying splitters do have ordinals, and live between
|
|
// tree columns. The splitters adjacent to a tree column do not need to
|
|
// have any 'ordinal' relationship, although it would appear user activity
|
|
// tends to move them around in a predictable fashion with oddness involved
|
|
// at the edges.
|
|
// Changes to the ordinal attribute should take immediate effect in terms of
|
|
// sibling relationship, but will merely invalidate the columns rather than
|
|
// cause a re-computation of column relationships every time.
|
|
// restoreNaturalOrder invalidates the tree when it is done re-ordering; I'm
|
|
// not sure that's entirely necessary...
|
|
this._savedColumnStates = this.getColumnStates();
|
|
},
|
|
|
|
/**
|
|
* Restores the visible columns saved by |_saveColumnStates|.
|
|
*/
|
|
_restoreColumnStates: function FolderDisplayWidget__restoreColumnStates() {
|
|
if (this._savedColumnStates) {
|
|
this.setColumnStates(this._savedColumnStates);
|
|
this._savedColumnStates = null;
|
|
}
|
|
},
|
|
//@}
|
|
|
|
/**
|
|
* @name What To Display
|
|
* @protected
|
|
*/
|
|
//@{
|
|
showFolderUri: function FolderDisplayWidget_showFolderUri(aFolderURI) {
|
|
return this.show(MailUtils.getFolderForURI(aFolderURI));
|
|
},
|
|
|
|
/**
|
|
* Invoked by showFolder when it turns out the folder is in fact a server.
|
|
* @private
|
|
*/
|
|
_showServer: function FolderDisplayWidget__showServer() {
|
|
// currently nothing to do. makeActive handles everything for us (because
|
|
// what is displayed needs to be re-asserted each time we are activated
|
|
// too.)
|
|
},
|
|
|
|
/**
|
|
* Select a folder for display.
|
|
*
|
|
* @param aFolder The nsIMsgDBFolder to display.
|
|
*/
|
|
show: function FolderDisplayWidget_show(aFolder) {
|
|
if (aFolder == null) {
|
|
this._nonViewFolder = null;
|
|
this.view.close();
|
|
}
|
|
else if (aFolder instanceof Components.interfaces.nsIMsgFolder) {
|
|
if (aFolder.isServer) {
|
|
this._nonViewFolder = aFolder;
|
|
this._showServer();
|
|
this.view.close();
|
|
// A server is fully loaded immediately, for now. (When we have the
|
|
// account summary, we might want to change this to wait for the page
|
|
// load to complete.)
|
|
this._allMessagesLoaded = true;
|
|
}
|
|
else {
|
|
this._nonViewFolder = null;
|
|
this.view.open(aFolder);
|
|
}
|
|
}
|
|
// it must be a synthetic view
|
|
else {
|
|
this.view.openSynthetic(aFolder);
|
|
}
|
|
if (this._active)
|
|
this.makeActive();
|
|
|
|
if (this._tabInfo)
|
|
document.getElementById('tabmail').setTabTitle(this._tabInfo);
|
|
},
|
|
|
|
/**
|
|
* Clone an existing view wrapper as the basis for our display.
|
|
*/
|
|
cloneView: function FolderDisplayWidget_cloneView(aViewWrapper) {
|
|
this.view = aViewWrapper.clone(this);
|
|
// generate a view created notification; this will cause us to do the right
|
|
// thing in terms of associating the view with the tree and such.
|
|
this.onCreatedView();
|
|
if (this._active)
|
|
this.makeActive();
|
|
},
|
|
|
|
/**
|
|
* Close resources associated with the currently displayed folder because you
|
|
* no longer care about this FolderDisplayWidget.
|
|
*/
|
|
close: function FolderDisplayWidget_close() {
|
|
// Mark ourselves as inactive without doing any of the hard work of becoming
|
|
// inactive. This saves us from trying to update things as they go away.
|
|
this._active = false;
|
|
// Tell the message display to close itself too. We do this before we do
|
|
// anything else because closing the view could theoretically propagate
|
|
// down to the message display and we don't want it doing anything it
|
|
// doesn't have to do.
|
|
this.messageDisplay._close();
|
|
|
|
this.view.close();
|
|
this.messenger.setWindow(null, null);
|
|
this.messenger = null;
|
|
this._fakeTreeBox = null;
|
|
this._fakeTreeSelection = null;
|
|
},
|
|
//@}
|
|
|
|
/* =============================== */
|
|
/* ===== IDBViewWrapper Listener ===== */
|
|
/* =============================== */
|
|
|
|
/**
|
|
* @name IDBViewWrapperListener Interface
|
|
* @private
|
|
*/
|
|
//@{
|
|
|
|
/**
|
|
* @return true if the mail view picker is visible. This affects whether the
|
|
* DBViewWrapper will actually use the persisted mail view or not.
|
|
*/
|
|
get shouldUseMailViews() {
|
|
return ViewPickerBinding.isVisible;
|
|
},
|
|
|
|
/**
|
|
* Let the viewWrapper know if we should defer message display because we
|
|
* want the user to connect to the server first so password authentication
|
|
* can occur.
|
|
*
|
|
* @return true if the folder should be shown immediately, false if we should
|
|
* wait for updateFolder to complete.
|
|
*/
|
|
get shouldDeferMessageDisplayUntilAfterServerConnect() {
|
|
let passwordPromptRequired = false;
|
|
|
|
if (Services.prefs.getBoolPref("mail.password_protect_local_cache"))
|
|
passwordPromptRequired =
|
|
this.view.displayedFolder.server.passwordPromptRequired;
|
|
|
|
return passwordPromptRequired;
|
|
},
|
|
|
|
/**
|
|
* Let the viewWrapper know if it should mark the messages read when leaving
|
|
* the provided folder.
|
|
*
|
|
* @return true if the preference is set for the folder's server type.
|
|
*/
|
|
shouldMarkMessagesReadOnLeavingFolder:
|
|
function FolderDisplayWidget_crazyMarkOnReadChecker (aMsgFolder) {
|
|
return Services.prefs.getBoolPref("mailnews.mark_message_read." +
|
|
aMsgFolder.server.type);
|
|
},
|
|
|
|
/**
|
|
* The view wrapper tells us when it starts loading a folder, and we set the
|
|
* cursor busy. Setting the cursor busy on a per-tab basis is us being
|
|
* nice to the future. Loading a folder is a blocking operation that is going
|
|
* to make us unresponsive and accordingly make it very hard for the user to
|
|
* change tabs.
|
|
*/
|
|
onFolderLoading: function(aFolderLoading) {
|
|
if (this._tabInfo)
|
|
document.getElementById("tabmail").setTabBusy(this._tabInfo,
|
|
aFolderLoading);
|
|
|
|
FolderDisplayListenerManager._fireListeners("onFolderLoading",
|
|
[this, aFolderLoading]);
|
|
},
|
|
|
|
/**
|
|
* The view wrapper tells us when a search is active, and we mark the tab as
|
|
* thinking so the user knows something is happening. 'Searching' in this
|
|
* case is more than just a user-initiated search. Virtual folders / saved
|
|
* searches, mail views, plus the more obvious quick search are all based off
|
|
* of searches and we will receive a notification for them.
|
|
*/
|
|
onSearching: function(aIsSearching) {
|
|
if (this._tabInfo) {
|
|
let searchBundle = document.getElementById("bundle_search");
|
|
document.getElementById("tabmail").setTabThinking(
|
|
this._tabInfo,
|
|
aIsSearching && searchBundle.getString("searchingMessage"));
|
|
}
|
|
|
|
FolderDisplayListenerManager._fireListeners("onSearching",
|
|
[this, aIsSearching]);
|
|
},
|
|
|
|
/**
|
|
* Things we do on creating a view:
|
|
* - notify the observer service so that custom column handler providers can
|
|
* add their custom columns to our view.
|
|
*/
|
|
onCreatedView: function FolderDisplayWidget_onCreatedView() {
|
|
// All of our messages are not displayed if the view was just created. We
|
|
// will get an onMessagesLoaded(true) nearly immediately if this is a local
|
|
// folder where view creation is synonymous with having all messages.
|
|
this._allMessagesLoaded = false;
|
|
this.messageDisplay.onCreatedView();
|
|
|
|
FolderDisplayListenerManager._fireListeners("onCreatedView",
|
|
[this]);
|
|
|
|
this._notifyWhenActive(this._activeCreatedView);
|
|
},
|
|
_activeCreatedView: function() {
|
|
gDBView = this.view.dbView;
|
|
|
|
// A change in view may result in changes to sorts, the view menu, etc.
|
|
// Do this before we 'reroot' the dbview.
|
|
this._updateThreadDisplay();
|
|
|
|
// this creates a new selection object for the view.
|
|
if (this.treeBox)
|
|
this.treeBox.view = this.view.dbView;
|
|
|
|
FolderDisplayListenerManager._fireListeners("onActiveCreatedView",
|
|
[this]);
|
|
|
|
// The data payload used to be viewType + ":" + viewFlags. We no longer
|
|
// do this because we already have the implied contract that gDBView is
|
|
// valid at the time we generate the notification. In such a case, you
|
|
// can easily get that information from the gDBView. (The documentation
|
|
// on creating a custom column assumes gDBView.)
|
|
Services.obs.notifyObservers(this.displayedFolder, "MsgCreateDBView", "");
|
|
},
|
|
|
|
/**
|
|
* If our view is being destroyed and it is coming back, we want to save the
|
|
* current selection so we can restore it when the view comes back.
|
|
*/
|
|
onDestroyingView: function FolderDisplayWidget_onDestroyingView(
|
|
aFolderIsComingBack) {
|
|
// try and persist the selection's content if we can
|
|
if (this._active) {
|
|
// If saving the selection throws an exception, we still want continue
|
|
// destroying the view. Saving the selection can fail if an underlying
|
|
// local folder has been compacted, invalidating the message keys.
|
|
// See bug 536676 for more info.
|
|
try {
|
|
// If a new selection is coming up, there's no point in trying to
|
|
// persist any selections.
|
|
if (aFolderIsComingBack && !this._aboutToSelectMessage)
|
|
this._saveSelection();
|
|
else
|
|
this._clearSavedSelection();
|
|
}
|
|
catch (ex) {
|
|
logException(ex);
|
|
}
|
|
gDBView = null;
|
|
}
|
|
|
|
FolderDisplayListenerManager._fireListeners("onDestroyingView",
|
|
[this, aFolderIsComingBack]);
|
|
|
|
// if we have no view, no messages could be loaded.
|
|
this._allMessagesLoaded = false;
|
|
|
|
// but the actual tree view selection (based on view indicies) is a goner no
|
|
// matter what, make everyone forget.
|
|
this.view.dbView.selection = null;
|
|
this._savedFirstVisibleRow = null;
|
|
this._nextViewIndexAfterDelete = null;
|
|
// although the move may still be active, its relation to the view is moot.
|
|
this._massMoveActive = false;
|
|
|
|
// Anything pending needs to get cleared out; the new view and its related
|
|
// events will re-schedule anything required or simply run it when it
|
|
// has its initial call to makeActive compelled.
|
|
this._notificationsPendingActivation = [];
|
|
|
|
// and the message display needs to forget
|
|
this.messageDisplay.onDestroyingView(aFolderIsComingBack);
|
|
},
|
|
|
|
/**
|
|
* Restore persisted information about what columns to display for the folder.
|
|
* If we have no persisted information, we leave/set _savedColumnStates null.
|
|
* The column states will be set to default values in onDisplayingFolder in
|
|
* that case.
|
|
*/
|
|
onLoadingFolder: function FolderDisplayWidget_onLoadingFolder(aDbFolderInfo) {
|
|
this._savedColumnStates =
|
|
this._depersistColumnStatesFromDbFolderInfo(aDbFolderInfo);
|
|
|
|
FolderDisplayListenerManager._fireListeners("onLoadingFolder",
|
|
[this, aDbFolderInfo]);
|
|
},
|
|
|
|
/**
|
|
* We are entering the folder for display:
|
|
* - set the header cache size.
|
|
* - Setup the columns if we did not already depersist in |onLoadingFolder|.
|
|
*/
|
|
onDisplayingFolder: function FolderDisplayWidget_onDisplayingFolder() {
|
|
let displayedFolder = this.view.displayedFolder;
|
|
let msgDatabase = displayedFolder && displayedFolder.msgDatabase;
|
|
if (msgDatabase) {
|
|
msgDatabase.resetHdrCacheSize(this.PERF_HEADER_CACHE_SIZE);
|
|
}
|
|
|
|
// makeActive will restore the folder state
|
|
if (!this._savedColumnStates) {
|
|
if (this.view.isSynthetic &&
|
|
"getPersistedSetting" in this.view._syntheticView) {
|
|
let columns = this.view._syntheticView.getPersistedSetting("columns");
|
|
this._savedColumnStates = columns;
|
|
}
|
|
else {
|
|
// get the default for this folder
|
|
this._savedColumnStates = this._getDefaultColumnsForCurrentFolder();
|
|
// and save it so it doesn't wiggle if the inbox/prototype changes
|
|
this._persistColumnStates(this._savedColumnStates);
|
|
}
|
|
}
|
|
|
|
FolderDisplayListenerManager._fireListeners("onDisplayingFolder",
|
|
[this]);
|
|
|
|
if (this.active)
|
|
this.makeActive();
|
|
},
|
|
|
|
/**
|
|
* Notification from DBViewWrapper that it is closing the folder. This can
|
|
* happen for reasons other than our own 'close' method closing the view.
|
|
* For example, user deletion of the folder or underlying folder closes it.
|
|
*/
|
|
onLeavingFolder: function FolderDisplayWidget_onLeavingFolder() {
|
|
FolderDisplayListenerManager._fireListeners("onLeavingFolder",
|
|
[this]);
|
|
|
|
// Keep the msgWindow's openFolder up-to-date; it powers nsMessenger's
|
|
// concept of history so that it can bring you back to the actual folder
|
|
// you were looking at, rather than just the underlying folder.
|
|
if (this._active)
|
|
msgWindow.openFolder = null;
|
|
},
|
|
|
|
/**
|
|
* Indictes whether we are done loading the messages that should be in this
|
|
* folder. This is being surfaced for testing purposes, but could be useful
|
|
* to other code as well. But don't poll this property; ask for an event
|
|
* that you can hook.
|
|
*/
|
|
get allMessagesLoaded() {
|
|
return this._allMessagesLoaded;
|
|
},
|
|
|
|
/**
|
|
* Things to do once some or all the messages that should show up in a folder
|
|
* have shown up. For a real folder, this happens when the folder is
|
|
* entered. For a virtual folder, this happens when the search completes.
|
|
*
|
|
* What we do:
|
|
* - Any scrolling required!
|
|
*/
|
|
onMessagesLoaded: function FolderDisplayWidget_onMessagesLoaded(aAll) {
|
|
this._allMessagesLoaded = aAll;
|
|
|
|
FolderDisplayListenerManager._fireListeners("onMessagesLoaded",
|
|
[this, aAll]);
|
|
|
|
this._notifyWhenActive(this._activeMessagesLoaded);
|
|
},
|
|
_activeMessagesLoaded:
|
|
function FolderDisplayWidget__activeMessagesLoaded() {
|
|
FolderDisplayListenerManager._fireListeners("onActiveMessagesLoaded",
|
|
[this]);
|
|
|
|
// - if a selectMessage's coming up, get out of here
|
|
if (this._aboutToSelectMessage)
|
|
return;
|
|
|
|
// - restore user's last expand/collapse choice.
|
|
this.restoreThreadState();
|
|
|
|
// - restore selection
|
|
// Attempt to restore the selection (if we saved it because the view was
|
|
// being destroyed or otherwise manipulated in a fashion that the normal
|
|
// nsTreeSelection would be unable to handle.)
|
|
if (this._restoreSelection()) {
|
|
this.ensureRowIsVisible(this.view.dbView.viewIndexForFirstSelectedMsg);
|
|
return;
|
|
}
|
|
|
|
// - pending navigation from pushNavigation (probably spacebar triggered)
|
|
// Need to have all messages loaded first.
|
|
if (this._pendingNavigation) {
|
|
// Move it to a local and clear the state in case something bad happens.
|
|
// (We don't want to swallow the exception.)
|
|
let pendingNavigation = this._pendingNavigation;
|
|
this._pendingNavigation = null;
|
|
this.navigate.apply(this, pendingNavigation);
|
|
return;
|
|
}
|
|
|
|
// - if something's already selected (e.g. in a message tab), scroll to the
|
|
// first selected message and get out
|
|
if (this.view.dbView.numSelected > 0) {
|
|
this.ensureRowIsVisible(this.view.dbView.viewIndexForFirstSelectedMsg);
|
|
return;
|
|
}
|
|
|
|
// - new messages
|
|
// if configured to scroll to new messages, try that
|
|
if (Services.prefs.getBoolPref("mailnews.scroll_to_new_message") &&
|
|
this.navigate(nsMsgNavigationType.firstNew, /* select */ false))
|
|
return;
|
|
|
|
// - last selected message
|
|
// if configured to load the last selected message (this is currently more
|
|
// persistent than our saveSelection/restoreSelection stuff), and the view
|
|
// is backed by a single underlying folder (the only way having just a
|
|
// message key works out), try that
|
|
if (Services.prefs.getBoolPref("mailnews.remember_selected_message") &&
|
|
this.view.isSingleFolder) {
|
|
// use the displayed folder; nsMsgDBView goes to the effort to save the
|
|
// state to the viewFolder, so this is the correct course of action.
|
|
let lastLoadedMessageKey = this.view.displayedFolder.lastMessageLoaded;
|
|
if (lastLoadedMessageKey != nsMsgKey_None) {
|
|
this.view.dbView.selectMsgByKey(lastLoadedMessageKey);
|
|
// The message key may not be present in the view for a variety of
|
|
// reasons. Beyond message deletion, it simply may not match the
|
|
// active mail view or quick search, for example.
|
|
if (this.view.dbView.numSelected > 0) {
|
|
this.ensureRowIsVisible(
|
|
this.view.dbView.viewIndexForFirstSelectedMsg);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// - towards the newest messages, but don't select
|
|
if (this.view.isSortedAscending && this.view.sortImpliesTemporalOrdering &&
|
|
this.navigate(nsMsgNavigationType.lastMessage, /* select */ false))
|
|
return;
|
|
|
|
// - to the top, the coliseum
|
|
this.ensureRowIsVisible(0);
|
|
},
|
|
|
|
/**
|
|
* The DBViewWrapper tells us when someone (possibly the wrapper itself)
|
|
* changes the active mail view so that we can kick the UI to update.
|
|
*/
|
|
onMailViewChanged: function FolderDisplayWidget_onMailViewChanged() {
|
|
// only do this if we're currently active. no need to queue it because we
|
|
// always update the mail view whenever we are made active.
|
|
if (this.active) {
|
|
// you cannot cancel a view change!
|
|
window.dispatchEvent(new Event("MailViewChanged",
|
|
{ bubbles: false, cancelable: false }));
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Just the sort or threading was changed, without changing other things. We
|
|
* will not get this notification if the view was re-created, for example.
|
|
*/
|
|
onSortChanged: function FolderDisplayWidget_onSortChanged() {
|
|
if (this.active)
|
|
UpdateSortIndicators(this.view.primarySortType,
|
|
this.view.primarySortOrder);
|
|
|
|
FolderDisplayListenerManager._fireListeners("onSortChanged",
|
|
[this]);
|
|
},
|
|
|
|
/**
|
|
* Messages (that may have been displayed) have been removed; this may impact
|
|
* our message selection. We might know it's coming; if we do then
|
|
* this._nextViewIndexAfterDelete should know what view index to select next.
|
|
* For the imap mark-as-deleted we won't know beforehand.
|
|
*/
|
|
onMessagesRemoved: function FolderDisplayWidget_onMessagesRemoved() {
|
|
FolderDisplayListenerManager._fireListeners("onMessagesRemoved",
|
|
[this]);
|
|
|
|
if (this.messageDisplay.onMessagesRemoved())
|
|
return;
|
|
|
|
// - we saw this coming
|
|
let rowCount = this.view.dbView.rowCount;
|
|
if (!this._massMoveActive && (this._nextViewIndexAfterDelete != null)) {
|
|
// adjust the index if it is after the last row...
|
|
// (this can happen if the "mail.delete_matches_sort_order" pref is not
|
|
// set and the message is the last message in the view.)
|
|
if (this._nextViewIndexAfterDelete >= rowCount)
|
|
this._nextViewIndexAfterDelete = rowCount - 1;
|
|
// just select the index and get on with our lives
|
|
this.selectViewIndex(this._nextViewIndexAfterDelete);
|
|
this._nextViewIndexAfterDelete = null;
|
|
return;
|
|
}
|
|
|
|
// - we didn't see it coming
|
|
|
|
// A deletion happened to our folder.
|
|
let treeSelection = this.treeSelection;
|
|
// we can't fix the selection if we have no selection
|
|
if (!treeSelection)
|
|
return;
|
|
|
|
// For reasons unknown (but theoretically knowable), sometimes the selection
|
|
// object will be invalid. At least, I've reliably seen a selection of
|
|
// [0, 0] with 0 rows. If that happens, we need to fix up the selection
|
|
// here.
|
|
if (rowCount == 0 && treeSelection.count)
|
|
// nsTreeSelection doesn't generate an event if we use clearRange, so use
|
|
// that to avoid spurious events, given that we are going to definitely
|
|
// trigger a change notification below.
|
|
treeSelection.clearRange(0, 0);
|
|
|
|
// Check if we now no longer have a selection, but we had exactly one
|
|
// message selected previously. If we did, then try and do some
|
|
// 'persistence of having a thing selected'.
|
|
if (treeSelection.count == 0 &&
|
|
this._mostRecentSelectionCounts.length > 1 &&
|
|
this._mostRecentSelectionCounts[1] == 1 &&
|
|
this._mostRecentCurrentIndices[1] != -1) {
|
|
let targetIndex = this._mostRecentCurrentIndices[1];
|
|
if (targetIndex >= rowCount)
|
|
targetIndex = rowCount - 1;
|
|
this.selectViewIndex(targetIndex);
|
|
return;
|
|
}
|
|
|
|
// Otherwise, just tell the view that things have changed so it can update
|
|
// itself to the new state of things.
|
|
// tell the view that things have changed so it can update itself suitably.
|
|
if (this.view.dbView)
|
|
this.view.dbView.selectionChanged();
|
|
},
|
|
|
|
/**
|
|
* Messages were not actually removed, but we were expecting that they would
|
|
* be. Clean-up what onMessagesRemoved would have cleaned up, namely the
|
|
* next view index to select.
|
|
*/
|
|
onMessageRemovalFailed:
|
|
function FolderDisplayWidget_onMessageRemovalFailed() {
|
|
this._nextViewIndexAfterDelete = null;
|
|
FolderDisplayListenerManager._fireListeners("onMessagesRemovalFailed",
|
|
[this]);
|
|
},
|
|
|
|
/**
|
|
* Update the status bar to reflect our exciting message counts.
|
|
*/
|
|
onMessageCountsChanged: function FolderDisplayWidget_onMessageCountsChaned() {
|
|
if (this.active)
|
|
UpdateStatusMessageCounts(this.displayedFolder);
|
|
FolderDisplayListenerManager._fireListeners("onMessageCountsChanged",
|
|
[this]);
|
|
},
|
|
//@}
|
|
/* ===== End IDBViewWrapperListener ===== */
|
|
|
|
/* ================================== */
|
|
/* ===== nsIMsgDBViewCommandUpdater ===== */
|
|
/* ================================== */
|
|
|
|
/**
|
|
* @name nsIMsgDBViewCommandUpdater Interface
|
|
* @private
|
|
*/
|
|
//@{
|
|
|
|
/**
|
|
* This gets called when the selection changes AND !suppressCommandUpdating
|
|
* AND (we're not removing a row OR we are now out of rows).
|
|
* In response, we update the toolbar.
|
|
*/
|
|
updateCommandStatus: function FolderDisplayWidget_updateCommandStatus() {
|
|
// Do this only if we're active. If we aren't, we're going to take care of
|
|
// this when we switch back to the tab.
|
|
if (this._active)
|
|
UpdateMailToolbar("FolderDisplayWidget command updater notification");
|
|
},
|
|
|
|
/**
|
|
* This gets called by nsMsgDBView::UpdateDisplayMessage following a call
|
|
* to nsIMessenger.OpenURL to kick off message display OR (UDM gets called)
|
|
* by nsMsgDBView::SelectionChanged in lieu of loading the message because
|
|
* mSupressMsgDisplay.
|
|
* In other words, we get notified immediately after the process of displaying
|
|
* a message triggered by the nsMsgDBView happens. We get some arguments
|
|
* that are display optimizations for historical reasons (as usual).
|
|
*
|
|
* Things this makes us want to do:
|
|
* - Set the tab title, perhaps. (If we are a message display.)
|
|
* - Update message counts, because things might have changed, why not.
|
|
* - Update some toolbar buttons, why not.
|
|
*
|
|
* @param aFolder The display/view folder, as opposed to the backing folder.
|
|
* @param aSubject The subject with "Re: " if it's got one, which makes it
|
|
* notably different from just directly accessing the message header's
|
|
* subject.
|
|
* @param aKeywords The keywords, which roughly translates to message tags.
|
|
*/
|
|
displayMessageChanged: function FolderDisplayWidget_displayMessageChanged(
|
|
aFolder, aSubject, aKeywords) {
|
|
// Hide previous stale message to prevent brief threadpane selection and
|
|
// content displayed mismatch, on both folder and tab changes.
|
|
let browser = getBrowser();
|
|
if (browser && browser.contentDocument && browser.contentDocument.body)
|
|
browser.contentDocument.body.hidden = true;
|
|
|
|
UpdateMailToolbar("FolderDisplayWidget displayed message changed");
|
|
let selected = this.view.dbView.getSelectedMsgHdrs();
|
|
let msgHdr = selected.length ? selected[0] : null;
|
|
this.messageDisplay.onDisplayingMessage(msgHdr);
|
|
|
|
// Although deletes should now be so fast that the user has no time to do
|
|
// anything, treat the user explicitly choosing to display a different
|
|
// message as invalidating the choice we automatically made for them when
|
|
// they initiated the message delete / move. (bug 243532)
|
|
// Note: legacy code used to check whether the message being displayed was
|
|
// the one being deleted, so it didn't erroneously clear the next message
|
|
// to display (bug 183394). This is not a problem for us because we hook
|
|
// our notification when the message load is initiated, rather than when
|
|
// the message completes loading.
|
|
this._nextViewIndexAfterDelete = null;
|
|
|
|
// If we're displaying a different message, reset the override.
|
|
msgWindow.charsetOverride = false;
|
|
},
|
|
|
|
/**
|
|
* This gets called as a hint that the currently selected message is junk and
|
|
* said junked message is going to be moved out of the current folder, or
|
|
* right before a header is removed from the db view. The legacy behaviour
|
|
* is to retrieve the msgToSelectAfterDelete attribute off the db view,
|
|
* stashing it for benefit of the code that gets called when a message
|
|
* move/deletion is completed so that we can trigger its display.
|
|
*/
|
|
updateNextMessageAfterDelete:
|
|
function FolderDisplayWidget_updateNextMessageAfterDelete() {
|
|
this.hintAboutToDeleteMessages();
|
|
},
|
|
|
|
/**
|
|
* The most recent currentIndexes on the selection (from the last time
|
|
* summarizeSelection got called). We use this in onMessagesRemoved if
|
|
* we get an unexpected notification.
|
|
* We keep a maximum of 2 entries in this list.
|
|
*/
|
|
_mostRecentCurrentIndices: undefined, // initialized in constructor
|
|
/**
|
|
* The most recent counts on the selection (from the last time
|
|
* summarizeSelection got called). We use this in onMessagesRemoved if
|
|
* we get an unexpected notification.
|
|
* We keep a maximum of 2 entries in this list.
|
|
*/
|
|
_mostRecentSelectionCounts: undefined, // initialized in constructor
|
|
|
|
/**
|
|
* Always called by the db view when the selection changes in
|
|
* SelectionChanged. This event will come after the notification to
|
|
* displayMessageChanged (if one happens), and before the notification to
|
|
* updateCommandStatus (if one happens).
|
|
*/
|
|
summarizeSelection: function FolderDisplayWidget_summarizeSelection() {
|
|
// save the current index off in case the selection gets deleted out from
|
|
// under us and we want to have persistence of actually-having-something
|
|
// selected.
|
|
let treeSelection = this.treeSelection;
|
|
if (treeSelection) {
|
|
this._mostRecentCurrentIndices.unshift(treeSelection.currentIndex);
|
|
this._mostRecentCurrentIndices.splice(2);
|
|
this._mostRecentSelectionCounts.unshift(treeSelection.count);
|
|
this._mostRecentSelectionCounts.splice(2);
|
|
}
|
|
return this.messageDisplay.onSelectedMessagesChanged();
|
|
},
|
|
//@}
|
|
/* ===== End nsIMsgDBViewCommandUpdater ===== */
|
|
|
|
/* ===== Hints from the command infrastructure ===== */
|
|
/**
|
|
* @name Command Infrastructure Hints
|
|
* @protected
|
|
*/
|
|
//@{
|
|
|
|
/**
|
|
* doCommand helps us out by telling us when it is telling the view to delete
|
|
* some messages. Ideally it should go through us / the DB View Wrapper to
|
|
* kick off the delete in the first place, but that's a thread I don't want
|
|
* to pull on right now.
|
|
* We use this hint to figure out the next message to display once the
|
|
* deletion completes. We do this before the deletion happens because the
|
|
* selection is probably going away (except in the IMAP delete model), and it
|
|
* might be too late to figure this out after the deletion happens.
|
|
* Our automated complement (that calls us) is updateNextMessageAfterDelete.
|
|
*/
|
|
hintAboutToDeleteMessages:
|
|
function FolderDisplayWidget_hintAboutToDeleteMessages() {
|
|
// save the value, even if it is nsMsgViewIndex_None.
|
|
this._nextViewIndexAfterDelete = this.view.dbView.msgToSelectAfterDelete;
|
|
},
|
|
|
|
/**
|
|
* The archive code tells us when it is starting to archive messages. This
|
|
* is different from hinting about deletion because it will also tell us
|
|
* when it has completed its mass move.
|
|
* The UI goal is that we do not immediately jump beyond the selected messages
|
|
* to the next message until all of the selected messages have been
|
|
* processed (moved). Ideally we would also do this when deleting messages
|
|
* from a multiple-folder backed message view, but we don't know when the
|
|
* last job completes in that case (whereas in this case we do because of the
|
|
* call to hintMassMoveCompleted.)
|
|
*/
|
|
hintMassMoveStarting:
|
|
function FolderDisplayWidget_hintMassMoveStarting() {
|
|
this.hintAboutToDeleteMessages();
|
|
this._massMoveActive = true;
|
|
},
|
|
|
|
/**
|
|
* The archival has completed, we can finally let onMessagseRemoved run to
|
|
* completion.
|
|
*/
|
|
hintMassMoveCompleted:
|
|
function FolderDisplayWidget_hintMassMoveCompleted() {
|
|
this._massMoveActive = false;
|
|
this.onMessagesRemoved();
|
|
},
|
|
|
|
/**
|
|
* When a right-click on the thread pane is going to alter our selection, we
|
|
* get this notification (currently from |ChangeSelectionWithoutContentLoad|
|
|
* in msgMail3PaneWindow.js), which lets us save our state.
|
|
* This ends one of two ways: we get made inactive because a new tab popped up
|
|
* or we get a call to |hintRightClickSelectionPerturbationDone|.
|
|
*
|
|
* Ideally, we could just save off our current nsITreeSelection and restore it
|
|
* when this is all over. This assumption would rely on the underlying view
|
|
* not having any changes to its rows before we restore the selection. I am
|
|
* not confident we can rule out background processes making changes, plus
|
|
* the right-click itself may mutate the view (although we could try and get
|
|
* it to restore the selection before it gets to the mutation part). Our
|
|
* only way to resolve this would be to create a 'tee' like fake selection
|
|
* that would proxy view change notifications to both sets of selections.
|
|
* That is hard.
|
|
* So we just use the existing _saveSelection/_restoreSelection mechanism
|
|
* which is potentially very costly.
|
|
*/
|
|
hintRightClickPerturbingSelection:
|
|
function FolderDisplayWidget_hintRightClickPerturbingSelect() {
|
|
this._saveSelection();
|
|
},
|
|
|
|
/**
|
|
* When a right-click on the thread pane altered our selection (which we
|
|
* should have received a call to |hintRightClickPerturbingSelection| for),
|
|
* we should receive this notification from
|
|
* |RestoreSelectionWithoutContentLoad| when it wants to put things back.
|
|
*/
|
|
hintRightClickSelectionPerturbationDone:
|
|
function FolderDisplayWidget_hintRightClickSelectionPerturbationDone() {
|
|
this._restoreSelection();
|
|
},
|
|
//@}
|
|
/* ===== End hints from the command infrastructure ==== */
|
|
|
|
_updateThreadDisplay: function FolderDisplayWidget__updateThreadDisplay() {
|
|
if (this.active) {
|
|
if (this.view.dbView) {
|
|
UpdateSortIndicators(this.view.dbView.sortType,
|
|
this.view.dbView.sortOrder);
|
|
SetNewsFolderColumns();
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update the UI display apart from the thread tree because the folder being
|
|
* displayed has changed. This can be the result of changing the folder in
|
|
* this FolderDisplayWidget, or because this FolderDisplayWidget is being
|
|
* made active. _updateThreadDisplay handles the parts of the thread tree
|
|
* that need updating.
|
|
*/
|
|
_updateContextDisplay: function FolderDisplayWidget__updateContextDisplay() {
|
|
if (this.active) {
|
|
UpdateMailToolbar("FolderDisplayWidget updating context");
|
|
UpdateStatusQuota(this.displayedFolder);
|
|
UpdateStatusMessageCounts(this.displayedFolder);
|
|
|
|
// - mail view combo-box.
|
|
this.onMailViewChanged();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @name Activation Control
|
|
* @protected
|
|
*/
|
|
//@{
|
|
|
|
/**
|
|
* Run the provided notification function right now if we are 'active' (the
|
|
* currently displayed tab), otherwise queue it to be run when we become
|
|
* active. We do this because our tabbing model uses multiplexed (reused)
|
|
* widgets, and extensions likewise depend on these global/singleton things.
|
|
* If the requested notification function is already queued, it will not be
|
|
* added a second time, and the original call ordering will be maintained.
|
|
* If a new call ordering is required, the list of notifications should
|
|
* probably be reset by the 'big bang' event (new view creation?).
|
|
*/
|
|
_notifyWhenActive:
|
|
function FolderDisplayWidget__notifyWhenActive(aNotificationFunc) {
|
|
if (this._active) {
|
|
aNotificationFunc.call(this);
|
|
}
|
|
else {
|
|
if (!this._notificationsPendingActivation.includes(aNotificationFunc))
|
|
this._notificationsPendingActivation.push(aNotificationFunc);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Some notifications cannot run while the FolderDisplayWidget is inactive
|
|
* (presumbly because it is in a background tab). We accumulate those in
|
|
* _notificationsPendingActivation and then this method runs them when we
|
|
* become active again.
|
|
*/
|
|
_runNotificationsPendingActivation:
|
|
function FolderDisplayWidget__runNotificationsPendingActivation() {
|
|
if (!this._notificationsPendingActivation.length)
|
|
return;
|
|
|
|
let pendingNotifications = this._notificationsPendingActivation;
|
|
this._notificationsPendingActivation = [];
|
|
for (let notif of pendingNotifications) {
|
|
notif.call(this);
|
|
}
|
|
},
|
|
|
|
/// This is not guaranteed to be up to date if the folder display is active
|
|
_folderPaneVisible: null,
|
|
|
|
/**
|
|
* Whether the folder pane is visible. When we're inactive, we stash the value
|
|
* in |this._folderPaneVisible|.
|
|
*/
|
|
get folderPaneVisible() {
|
|
if (this._active) {
|
|
let folderPaneBox = document.getElementById("folderPaneBox");
|
|
if (folderPaneBox)
|
|
return !folderPaneBox.collapsed;
|
|
}
|
|
else {
|
|
return this._folderPaneVisible;
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Sets the visibility of the folder pane. This should reflect reality and
|
|
* not define it (for active tabs at least).
|
|
*/
|
|
set folderPaneVisible(aVisible) {
|
|
this._folderPaneVisible = aVisible;
|
|
},
|
|
|
|
get active() {
|
|
return this._active;
|
|
},
|
|
|
|
/**
|
|
* Make this FolderDisplayWidget the 'active' widget by updating globals and
|
|
* linking us up to the UI widgets. This is intended for use by the tabbing
|
|
* logic.
|
|
*/
|
|
makeActive: function FolderDisplayWidget_makeActive(aWasInactive) {
|
|
let wasInactive = !this._active;
|
|
|
|
// -- globals
|
|
// update per-tab globals that we own
|
|
gFolderDisplay = this;
|
|
gMessageDisplay = this.messageDisplay;
|
|
gDBView = this.view.dbView;
|
|
messenger = this.messenger;
|
|
|
|
// update singleton globals' state
|
|
msgWindow.openFolder = this.view.displayedFolder;
|
|
|
|
// This depends on us being active, so get it before we're marked active.
|
|
// We don't get this._folderPaneActive directly for idempotence's sake.
|
|
let folderPaneVisible = this.folderPaneVisible;
|
|
|
|
this._active = true;
|
|
this._runNotificationsPendingActivation();
|
|
|
|
// Make sure we get rid of this._fakeTreeSelection, whether we use it below
|
|
// or not.
|
|
let fakeTreeSelection = this._fakeTreeSelection;
|
|
this._fakeTreeSelection = null;
|
|
|
|
FolderDisplayListenerManager._fireListeners("onMakeActive",
|
|
[this, aWasInactive]);
|
|
|
|
// -- UI
|
|
|
|
// We're going to set this to true if we've already caused a
|
|
// selectionChanged event, so that the message display doesn't cause
|
|
// another, or if a select message is coming up shortly.
|
|
let dontReloadMessage = this._aboutToSelectMessage;
|
|
// thread pane if we have a db view
|
|
if (this.view.dbView) {
|
|
// Make sure said thread pane is visible. If we do this after we re-root
|
|
// the tree, the thread pane may not actually replace the account central
|
|
// pane. Concerning...
|
|
this._showThreadPane();
|
|
|
|
// some things only need to happen if we are transitioning from inactive
|
|
// to active
|
|
if (wasInactive) {
|
|
if (this.treeBox) {
|
|
// We might have assigned our JS tree selection to
|
|
// this.view.dbView.selection back in _hookUpFakeTreeBox. If we've
|
|
// done so, null the selection out so that the line after this
|
|
// causes a real selection to be created.
|
|
// If we haven't done so, we're fine as selection would be null here
|
|
// anyway. (The fake tree selection should persist only till the
|
|
// first time the tab is switched to.)
|
|
if (fakeTreeSelection)
|
|
this.view.dbView.selection = null;
|
|
|
|
// Setting the 'view' attribute on treeBox results in the following
|
|
// effective calls, noting that in makeInactive we made sure to null
|
|
// out its view so that it won't try and clean up any views or their
|
|
// selections. (The actual actions happen in
|
|
// nsTreeBodyFrame::SetView)
|
|
// - this.view.dbView.selection.tree = this.treeBox
|
|
// - this.view.dbView.setTree(this.treeBox)
|
|
// - this.treeBox.view = this.view.dbView (in
|
|
// nsTreeBodyObject::SetView)
|
|
this.treeBox.view = this.view.dbView;
|
|
|
|
if (fakeTreeSelection) {
|
|
fakeTreeSelection.duplicateSelection(this.view.dbView.selection);
|
|
// Since duplicateSelection will fire a selectionChanged event,
|
|
// which will try to reload the message, we shouldn't do the same.
|
|
dontReloadMessage = true;
|
|
}
|
|
if (this._savedFirstVisibleRow != null)
|
|
this.treeBox.scrollToRow(this._savedFirstVisibleRow);
|
|
}
|
|
}
|
|
|
|
// Always restore the column state if we have persisted state. We restore
|
|
// state on folder entry, in which case we were probably not inactive.
|
|
this._restoreColumnStates();
|
|
|
|
// the tab mode knows whether we are folder or message display, which
|
|
// impacts the legal modes
|
|
if (this._tabInfo)
|
|
mailTabType._setPaneStates(this._tabInfo.mode.legalPanes,
|
|
{folder: folderPaneVisible,
|
|
message: this.messageDisplay.visible});
|
|
|
|
// update the columns and such that live inside the thread pane
|
|
this._updateThreadDisplay();
|
|
|
|
this.messageDisplay.makeActive(dontReloadMessage);
|
|
}
|
|
// account central stuff when we don't have a dbview
|
|
else {
|
|
this._showAccountCentral();
|
|
if (this._tabInfo)
|
|
mailTabType._setPaneStates(this._tabInfo.mode.accountCentralLegalPanes,
|
|
{folder: folderPaneVisible});
|
|
}
|
|
|
|
this._updateContextDisplay();
|
|
},
|
|
|
|
/**
|
|
* Cause the displayDeck to display the thread pane.
|
|
*/
|
|
_showThreadPane: function FolderDisplayWidget__showThreadPane() {
|
|
document.getElementById("displayDeck").selectedPanel =
|
|
document.getElementById("threadPaneBox");
|
|
},
|
|
|
|
/**
|
|
* Cause the displayDeck to display the (preference configurable) account
|
|
* central page.
|
|
*/
|
|
_showAccountCentral: function FolderDisplayWidget__showAccountCentral() {
|
|
var accountBox = document.getElementById("accountCentralBox");
|
|
document.getElementById("displayDeck").selectedPanel = accountBox;
|
|
var prefName = "mailnews.account_central_page.url";
|
|
// oh yeah, 'pref' is a global all right.
|
|
var acctCentralPage =
|
|
Services.prefs.getComplexValue(prefName,
|
|
Components.interfaces.nsIPrefLocalizedString).data;
|
|
window.frames["accountCentralPane"].location.href = acctCentralPage;
|
|
},
|
|
|
|
/**
|
|
* Call this when the tab using us is being hidden.
|
|
*/
|
|
makeInactive: function FolderDisplayWidget_makeInactive() {
|
|
// - things to do before we mark ourselves inactive (because they depend on
|
|
// us being active)
|
|
|
|
// getColumnStates returns _savedColumnStates when we are inactive (and is
|
|
// used by _saveColumnStates) so we must do this before marking inactive.
|
|
this._saveColumnStates();
|
|
|
|
// - mark us inactive
|
|
this._active = false;
|
|
|
|
// - (everything after this point doesn't care that we are marked inactive)
|
|
// save the folder pane's state always
|
|
this._folderPaneVisible =
|
|
!document.getElementById("folderPaneBox").collapsed;
|
|
|
|
if (this.view.dbView) {
|
|
if (this.treeBox)
|
|
this._savedFirstVisibleRow = this.treeBox.getFirstVisibleRow();
|
|
|
|
// save the message pane's state only when it is potentially visible
|
|
this.messagePaneCollapsed =
|
|
document.getElementById("messagepaneboxwrapper").collapsed;
|
|
|
|
this.hookUpFakeTreeBox(true);
|
|
}
|
|
|
|
this.messageDisplay.makeInactive();
|
|
},
|
|
//@}
|
|
|
|
/**
|
|
* Called when we want to "disable" the real treeBox for a while and hook up
|
|
* the fake tree box to the db view. This also takes care of our
|
|
* treeSelection object.
|
|
*
|
|
* @param aNullRealTreeBoxView true if we want to null out the real tree box.
|
|
* We don't want to null out the view if we're opening a background
|
|
* tab, for example.
|
|
* @private
|
|
*/
|
|
hookUpFakeTreeBox: function FolderDisplayWidget_hookUpFakeTreeBox(
|
|
aNullRealTreeBoxView) {
|
|
// save off the tree selection object. the nsTreeBodyFrame will make the
|
|
// view forget about it when our view is removed, so it's up to us to
|
|
// save it.
|
|
// We use this.treeSelection instead of this.view.dbView.selection here,
|
|
// so that we get the fake tree selection if we have it.
|
|
let treeSelection = this.treeSelection;
|
|
// if we want to, make the tree forget about the view right now so we can
|
|
// tell the db view about its selection object so it can try and keep it
|
|
// up-to-date even while hidden in the background
|
|
if (aNullRealTreeBoxView && this.treeBox)
|
|
this.treeBox.view = null;
|
|
// (and tell the db view about its selection again...)
|
|
this.view.dbView.selection = treeSelection;
|
|
|
|
// hook the dbview up to the fake tree box
|
|
this._fakeTreeBox.view = this.view.dbView;
|
|
this.view.dbView.setTree(this._fakeTreeBox);
|
|
treeSelection.tree = this._fakeTreeBox;
|
|
},
|
|
|
|
/**
|
|
* @name Command Support
|
|
*/
|
|
//@{
|
|
|
|
/**
|
|
* @return true if there is a db view and the command is enabled on the view.
|
|
* This function hides some of the XPCOM-odditities of the getCommandStatus
|
|
* call.
|
|
*/
|
|
getCommandStatus: function FolderDisplayWidget_getCommandStatus(
|
|
aCommandType, aEnabledObj, aCheckStatusObj) {
|
|
// no view means not enabled
|
|
if (!this.view.dbView)
|
|
return false;
|
|
let enabledObj = {}, checkStatusObj = {};
|
|
this.view.dbView.getCommandStatus(aCommandType, enabledObj, checkStatusObj);
|
|
return enabledObj.value;
|
|
},
|
|
|
|
/**
|
|
* Make code cleaner by allowing peoples to call doCommand on us rather than
|
|
* having to do folderDisplayWidget.view.dbView.doCommand.
|
|
*
|
|
* @param aCommandName The command name to invoke.
|
|
*/
|
|
doCommand: function FolderDisplayWidget_doCommand(aCommandName) {
|
|
return this.view.dbView && this.view.dbView.doCommand(aCommandName);
|
|
},
|
|
|
|
/**
|
|
* Make code cleaner by allowing peoples to call doCommandWithFolder on us
|
|
* rather than having to do:
|
|
* folderDisplayWidget.view.dbView.doCommandWithFolder.
|
|
*
|
|
* @param aCommandName The command name to invoke.
|
|
* @param aFolder The folder context for the command.
|
|
*/
|
|
doCommandWithFolder: function FolderDisplayWidget_doCommandWithFolder(
|
|
aCommandName, aFolder) {
|
|
return this.view.dbView &&
|
|
this.view.dbView.doCommandWithFolder(aCommandName, aFolder);
|
|
},
|
|
//@}
|
|
|
|
/**
|
|
* @return true when account central is being displayed.
|
|
* @groupName Displayed
|
|
*/
|
|
get isAccountCentralDisplayed() {
|
|
return (this.view.dbView == null);
|
|
},
|
|
|
|
/**
|
|
* @name Navigation
|
|
* @protected
|
|
*/
|
|
//@{
|
|
|
|
/**
|
|
* Navigate using nsMsgNavigationType rules and ensuring the resulting row is
|
|
* visible. This is trickier than it used to be because we now support
|
|
* treating collapsed threads as the set of all the messages in the collapsed
|
|
* thread rather than just the root message in that thread.
|
|
*
|
|
* @param {nsMsgNavigationType} aNavType navigation command.
|
|
* @param {Boolean} [aSelect=true] should we select the message if we find
|
|
* one?
|
|
*
|
|
* @return true if the navigation constraint matched anything, false if not.
|
|
* We will have navigated if true, we will have done nothing if false.
|
|
*/
|
|
navigate: function FolderDisplayWidget_navigate(aNavType, aSelect) {
|
|
if (aSelect === undefined)
|
|
aSelect = true;
|
|
let resultKeyObj = {}, resultIndexObj = {}, threadIndexObj = {};
|
|
|
|
let summarizeSelection = this.summarizeSelectionInFolder;
|
|
|
|
let treeSelection = this.treeSelection; // potentially magic getter
|
|
let currentIndex = treeSelection ? treeSelection.currentIndex : 0;
|
|
|
|
let viewIndex;
|
|
// if we're doing next unread, and a collapsed thread is selected, and
|
|
// the top level message is unread, just set the result manually to
|
|
// the top level message, without using viewNavigate.
|
|
if (summarizeSelection &&
|
|
aNavType == nsMsgNavigationType.nextUnreadMessage &&
|
|
currentIndex != -1 &&
|
|
this.view.isCollapsedThreadAtIndex(currentIndex) &&
|
|
!(this.view.dbView.getFlagsAt(currentIndex) &
|
|
nsMsgMessageFlags.Read)) {
|
|
viewIndex = currentIndex;
|
|
}
|
|
else {
|
|
// always 'wrap' because the start index is relative to the selection.
|
|
// (keep in mind that many forms of navigation do not care about the
|
|
// starting position or 'wrap' at all; for example, firstNew just finds
|
|
// the first new message.)
|
|
// allegedly this does tree-expansion for us.
|
|
this.view.dbView.viewNavigate(aNavType, resultKeyObj, resultIndexObj,
|
|
threadIndexObj, true);
|
|
viewIndex = resultIndexObj.value;
|
|
}
|
|
|
|
if (viewIndex == nsMsgViewIndex_None)
|
|
return false;
|
|
|
|
// - Expand if required.
|
|
// (The nsMsgDBView isn't really aware of the varying semantics of
|
|
// collapsed threads, so viewNavigate might tell us about the root message
|
|
// and leave it collapsed, not realizing that it needs to be expanded.)
|
|
if (summarizeSelection &&
|
|
this.view.isCollapsedThreadAtIndex(viewIndex))
|
|
this.view.dbView.toggleOpenState(viewIndex);
|
|
|
|
if (aSelect)
|
|
this.selectViewIndex(viewIndex);
|
|
else
|
|
this.ensureRowIsVisible(viewIndex);
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Push a call to |navigate| to be what we do once we successfully open the
|
|
* next folder. This is intended to be used by cross-folder navigation
|
|
* code. It should call this method before triggering the folder change.
|
|
*/
|
|
pushNavigation: function FolderDisplayWidget_navigate(aNavType, aSelect) {
|
|
this._pendingNavigation = [aNavType, aSelect];
|
|
},
|
|
|
|
/**
|
|
* @return true if we are able to navigate using the given navigation type at
|
|
* this time.
|
|
*/
|
|
navigateStatus: function FolderDisplayWidget_navigateStatus(aNavType) {
|
|
if (!this.view.dbView)
|
|
return false;
|
|
return this.view.dbView.navigateStatus(aNavType);
|
|
},
|
|
//@}
|
|
|
|
/**
|
|
* @name Selection
|
|
*/
|
|
//@{
|
|
|
|
/**
|
|
* @returns the message header for the first selected message, or null if
|
|
* there is no selected message.
|
|
*
|
|
* If the user has right-clicked on a message, this method will return that
|
|
* message and not the 'current index' (the dude with the dotted selection
|
|
* rectangle around him.) If you instead always want the currently
|
|
* displayed message (which is not impacted by right-clicking), then you
|
|
* would want to access the displayedMessage property on the
|
|
* MessageDisplayWidget. You can get to that via the messageDisplay
|
|
* attribute on this object or (potentially) via the gMessageDisplay object.
|
|
*/
|
|
get selectedMessage() {
|
|
// there are inconsistencies in hdrForFirstSelectedMessage between
|
|
// nsMsgDBView and nsMsgSearchDBView in whether they use currentIndex,
|
|
// do it ourselves. (nsMsgDBView does not use currentIndex, search does.)
|
|
let treeSelection = this.treeSelection;
|
|
if (!treeSelection || !treeSelection.count)
|
|
return null;
|
|
let minObj = {}, maxObj = {};
|
|
treeSelection.getRangeAt(0, minObj, maxObj);
|
|
return this.view.dbView.getMsgHdrAt(minObj.value);
|
|
},
|
|
|
|
/**
|
|
* @return true if there is a selected message and it's an RSS feed message;
|
|
* a feed message does not have to be in an rss account folder if stored in
|
|
* Tb15 and later.
|
|
*/
|
|
get selectedMessageIsFeed() {
|
|
return FeedMessageHandler.isFeedMessage(this.selectedMessage);
|
|
},
|
|
|
|
/**
|
|
* @return true if there is a selected message and it's an IMAP message.
|
|
*/
|
|
get selectedMessageIsImap() {
|
|
let message = this.selectedMessage;
|
|
return Boolean(message && message.folder &&
|
|
message.folder.flags & nsMsgFolderFlags.ImapBox);
|
|
},
|
|
|
|
/**
|
|
* @return true if there is a selected message and it's a news message. It
|
|
* would be great if messages knew this about themselves, but they don't.
|
|
*/
|
|
get selectedMessageIsNews() {
|
|
let message = this.selectedMessage;
|
|
return Boolean(message && message.folder &&
|
|
(message.folder.flags & nsMsgFolderFlags.Newsgroup));
|
|
},
|
|
|
|
/**
|
|
* @return true if there is a selected message and it's an external message,
|
|
* meaning it is loaded from an .eml file on disk or is an rfc822 attachment
|
|
* on a message.
|
|
*/
|
|
get selectedMessageIsExternal() {
|
|
let message = this.selectedMessage;
|
|
// Dummy messages currently lack a folder. This is not a great heuristic.
|
|
// I have annotated msgHdrViewOverlay.js which provides the dummy header to
|
|
// express this implementation dependency.
|
|
// (Currently, since external mails can only be opened in standalone windows
|
|
// which subclass us, we could always return false, and have the subclass
|
|
// return true using its own heuristics. But since we are moving to a tab
|
|
// model more heavily, at some point the 3-pane will need this.)
|
|
return Boolean(message && !message.folder);
|
|
},
|
|
|
|
/**
|
|
* @return true if there is a selected message and the message belongs to an
|
|
* ignored thread.
|
|
*/
|
|
get selectedMessageThreadIgnored() {
|
|
let message = this.selectedMessage;
|
|
return Boolean(message && message.folder &&
|
|
message.folder.msgDatabase.IsIgnored(message.messageKey));
|
|
},
|
|
|
|
/**
|
|
* @return true if there is a selected message and the message is the base
|
|
* message for an ignored subthread.
|
|
*/
|
|
get selectedMessageSubthreadIgnored() {
|
|
let message = this.selectedMessage;
|
|
return Boolean(message && message.folder &&
|
|
(message.flags & nsMsgMessageFlags.Ignored));
|
|
},
|
|
|
|
/**
|
|
* @return true if there is a selected message and the message belongs to a
|
|
* watched thread.
|
|
*/
|
|
get selectedMessageThreadWatched() {
|
|
let message = this.selectedMessage;
|
|
return Boolean(message && message.folder &&
|
|
message.folder.msgDatabase.IsWatched(message.messageKey));
|
|
},
|
|
|
|
/**
|
|
* @return the number of selected messages. If summarizeSelectionInFolder is
|
|
* true, then any collapsed thread roots that are selected will also
|
|
* conceptually have all of the messages in that thread selected.
|
|
*/
|
|
get selectedCount() {
|
|
return this.selectedMessages.length;
|
|
},
|
|
|
|
/**
|
|
* Provides a list of the view indices that are selected which is *not* the
|
|
* same as the rows of the selected messages. When
|
|
* summarizeSelectionInFolder is true, messages may be selected but not
|
|
* visible (because the thread root is selected.)
|
|
* You probably want to use the |selectedMessages| attribute instead of this
|
|
* one. (Or selectedMessageUris in some rare cases.)
|
|
*
|
|
* If the user has right-clicked on a message, this will return that message
|
|
* and not the selection prior to the right-click.
|
|
*
|
|
* @return a list of the view indices that are currently selected
|
|
*/
|
|
get selectedIndices() {
|
|
if (!this.view.dbView)
|
|
return [];
|
|
|
|
return this.view.dbView.getIndicesForSelection({});
|
|
},
|
|
|
|
/**
|
|
* Provides a list of the message headers for the currently selected messages.
|
|
* If summarizeSelectionInFolder is true, then any collapsed thread roots
|
|
* that are selected will also (conceptually) have all of the messages in
|
|
* that thread selected and they will be included in the returned list.
|
|
*
|
|
* If the user has right-clicked on a message, this will return that message
|
|
* (and any collapsed children if so enabled) and not the selection prior to
|
|
* the right-click.
|
|
*
|
|
* @return a list of the message headers for the currently selected messages.
|
|
* If there are no selected messages, the result is an empty list.
|
|
*/
|
|
get selectedMessages() {
|
|
if (!this.view.dbView)
|
|
return [];
|
|
return this.view.dbView.getSelectedMsgHdrs();
|
|
},
|
|
|
|
/**
|
|
* @return a list of the URIs for the currently selected messages or null
|
|
* (instead of a list) if there are no selected messages. Do not
|
|
* pass around URIs unless you have a good reason. Legacy code is an
|
|
* ok reason.
|
|
*
|
|
* If the user has right-clicked on a message, this will return that message's
|
|
* URI and not the selection prior to the right-click.
|
|
*/
|
|
get selectedMessageUris() {
|
|
if (!this.view.dbView)
|
|
return null;
|
|
|
|
let messageArray = this.view.dbView.getURIsForSelection({});
|
|
return messageArray.length ? messageArray : null;
|
|
},
|
|
|
|
/**
|
|
* The maximum number of messages to try to examine directly to determine if
|
|
* they can be archived; if we exceed this count, we'll try to approximate
|
|
* the answer by looking at the server's identities. This is only here to
|
|
* let tests tweak the value.
|
|
*/
|
|
MAX_COUNT_FOR_CAN_ARCHIVE_CHECK: 100,
|
|
|
|
/**
|
|
* @return true if all the selected messages can be archived, false otherwise.
|
|
*/
|
|
get canArchiveSelectedMessages() {
|
|
if (!this.view.dbView || this.messageDisplay.isDummy)
|
|
return false;
|
|
|
|
if (this.selectedCount == 0)
|
|
return false;
|
|
|
|
// If we're looking at a single folder (i.e. not a cross-folder search), we
|
|
// can just check to see if all the identities for this folder/server have
|
|
// archives enabled (or disabled). This is way faster than checking every
|
|
// message. Note: this may be slightly inaccurate if the identity for a
|
|
// header is actually on another server.
|
|
if (this.selectedCount > this.MAX_COUNT_FOR_CAN_ARCHIVE_CHECK &&
|
|
this.view.isSingleFolder && this.displayedFolder) {
|
|
let folderIdentity = this.displayedFolder.customIdentity;
|
|
if (folderIdentity)
|
|
return folderIdentity.archiveEnabled;
|
|
|
|
if (this.displayedFolder.server) {
|
|
let serverIdentities = MailServices.accounts.getIdentitiesForServer(
|
|
this.displayedFolder.server
|
|
);
|
|
|
|
const nsIMsgIdentity = Components.interfaces.nsIMsgIdentity;
|
|
let allEnabled = undefined;
|
|
for (let identity in fixIterator(serverIdentities, nsIMsgIdentity)) {
|
|
if (allEnabled === undefined) {
|
|
allEnabled = identity.archiveEnabled;
|
|
}
|
|
else if (identity.archiveEnabled != allEnabled) {
|
|
allEnabled = undefined;
|
|
break;
|
|
}
|
|
}
|
|
if (allEnabled !== undefined)
|
|
return allEnabled;
|
|
}
|
|
}
|
|
|
|
// Either we've selected a small number of messages or we just can't
|
|
// fast-path the result; examine all the messages.
|
|
return this.selectedMessages.every(function(msg) {
|
|
let identity = getIdentityForHeader(msg);
|
|
return Boolean(identity && identity.archiveEnabled);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* The maximum number of message canMarkThreadAsRead will look through.
|
|
* If the number exceeds this limit as a performance measure we return
|
|
* true rather than looking looking through the messages and possible submessages.
|
|
*/
|
|
MAX_COUNT_FOR_MARK_THREAD : 1000,
|
|
|
|
/**
|
|
* Check if the thread for the currently-selected message can be marked as read.
|
|
* A thread can be marked as read if and only if it has at least one unread message.
|
|
*/
|
|
get canMarkThreadAsRead() {
|
|
if (this.displayedFolder && (this.displayedFolder.getNumUnread(false) > 0)) {
|
|
// If the messages limit is exceeded we bail out early and return true.
|
|
if (this.selectedIndices.length > this.MAX_COUNT_FOR_MARK_THREAD) {
|
|
return true;
|
|
}
|
|
|
|
for (let i of this.selectedIndices) {
|
|
if (this.view.dbView.getThreadContainingIndex(i).numUnreadChildren > 0) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* @return true if all the selected messages can be deleted from their
|
|
* folders, false otherwise.
|
|
*/
|
|
get canDeleteSelectedMessages() {
|
|
if (!this.view.dbView)
|
|
return false;
|
|
|
|
let selectedMessages = this.selectedMessages;
|
|
for (let i = 0; i < selectedMessages.length; ++i) {
|
|
if (selectedMessages[i].folder &&
|
|
!selectedMessages[i].folder.canDeleteMessages) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Clear the tree selection, making sure the message pane is cleared and
|
|
* the context display (toolbars, etc.) are updated.
|
|
*/
|
|
clearSelection: function FolderDisplayWidget_clearSelection() {
|
|
let treeSelection = this.treeSelection; // potentially magic getter
|
|
if (!treeSelection)
|
|
return;
|
|
treeSelection.clearSelection();
|
|
this.messageDisplay.clearDisplay();
|
|
this._updateContextDisplay();
|
|
},
|
|
|
|
/// Whether we're about to select a message
|
|
_aboutToSelectMessage: false,
|
|
|
|
/**
|
|
* This needs to be called to let us know that a selectMessage or equivalent
|
|
* is coming up right after a show() call, so that we know that a double
|
|
* message load won't be happening.
|
|
*
|
|
* This can be assumed to be idempotent.
|
|
*/
|
|
selectMessageComingUp: function FolderDisplayWidget_selectMessageComingUp() {
|
|
this._aboutToSelectMessage = true;
|
|
},
|
|
|
|
/**
|
|
* Select a message for display by header. Attempt to select the message
|
|
* right now. If we were unable to find it, update our saved selection
|
|
* to want to display the message. Threads are expanded to find the header.
|
|
*
|
|
* @param aMsgHdr The message header to select for display.
|
|
* @param [aForceSelect] If the message is not in the view and this is true,
|
|
* we will drop any applied view filters to look for the
|
|
* message. The dropping of view filters is persistent,
|
|
* so use with care. Defaults to false.
|
|
*/
|
|
selectMessage: function FolderDisplayWidget_selectMessage(aMsgHdr,
|
|
aForceSelect) {
|
|
let viewIndex = this.view.getViewIndexForMsgHdr(aMsgHdr, aForceSelect);
|
|
if (viewIndex != nsMsgViewIndex_None) {
|
|
this._savedSelection = null;
|
|
this.selectViewIndex(viewIndex);
|
|
}
|
|
else {
|
|
this._savedSelection = {messages: [{messageId: aMsgHdr.messageId}],
|
|
forceSelect: aForceSelect};
|
|
// queue the selection to be restored once we become active if we are not
|
|
// active.
|
|
if (!this.active)
|
|
this._notifyWhenActive(this._restoreSelection);
|
|
}
|
|
|
|
// Do this here instead of at the beginning to prevent reentrancy issues
|
|
this._aboutToSelectMessage = false;
|
|
},
|
|
|
|
/**
|
|
* Select all of the provided nsIMsgDBHdrs in the aMessages array, expanding
|
|
* threads as required. If we were not able to find all of the messages,
|
|
* update our saved selection to want to display the messages. The messages
|
|
* will then be selected when we are made active or all messages in the
|
|
* folder complete loading. This is to accomodate the use-case where we
|
|
* are backed by an in-progress search and no
|
|
*
|
|
* @param aMessages An array of nsIMsgDBHdr instances.
|
|
* @param [aForceSelect] If a message is not in the view and this is true,
|
|
* we will drop any applied view filters to look for the
|
|
* message. The dropping of view filters is persistent,
|
|
* so use with care. Defaults to false.
|
|
* @param aDoNotNeedToFindAll If true (can be omitted and left undefined), we
|
|
* do not attempt to save the selection for future use. This is intended
|
|
* for use by the _restoreSelection call which is the end-of-the-line for
|
|
* restoring the selection. (Once it gets called all of our messages
|
|
* should have already been loaded.)
|
|
*/
|
|
selectMessages: function FolderDisplayWidget_selectMessages(
|
|
aMessages, aForceSelect, aDoNotNeedToFindAll) {
|
|
let treeSelection = this.treeSelection; // potentially magic getter
|
|
let foundAll = true;
|
|
if (treeSelection) {
|
|
let minRow = null, maxRow = null;
|
|
|
|
treeSelection.selectEventsSuppressed = true;
|
|
treeSelection.clearSelection();
|
|
|
|
for (let msgHdr of aMessages) {
|
|
let viewIndex = this.view.getViewIndexForMsgHdr(msgHdr, aForceSelect);
|
|
|
|
if (viewIndex != nsMsgViewIndex_None) {
|
|
if (minRow == null || viewIndex < minRow)
|
|
minRow = viewIndex;
|
|
if (maxRow == null || viewIndex > maxRow )
|
|
maxRow = viewIndex;
|
|
// nsTreeSelection is actually very clever about doing this
|
|
// efficiently.
|
|
treeSelection.rangedSelect(viewIndex, viewIndex, true);
|
|
}
|
|
else {
|
|
foundAll = false;
|
|
}
|
|
|
|
// make sure the selection is as visible as possible
|
|
if (minRow != null)
|
|
this.ensureRowRangeIsVisible(minRow, maxRow);
|
|
}
|
|
|
|
treeSelection.selectEventsSuppressed = false;
|
|
|
|
// If we haven't selected every message, we'll set |this._savedSelection|
|
|
// below, so it's fine to null it out at this point.
|
|
this._savedSelection = null;
|
|
}
|
|
|
|
// Do this here instead of at the beginning to prevent reentrancy issues
|
|
this._aboutToSelectMessage = false;
|
|
|
|
// Two cases.
|
|
// 1. The tree selection isn't there at all.
|
|
// 2. The tree selection is there, and we needed to find all messages, but
|
|
// we didn't.
|
|
if (!treeSelection || (!aDoNotNeedToFindAll && !foundAll)) {
|
|
this._savedSelection = {
|
|
messages: aMessages.map(msgHdr => ({messageId: msgHdr.messageId})),
|
|
forceSelect: aForceSelect
|
|
};
|
|
if (!this.active)
|
|
this._notifyWhenActive(this._restoreSelection);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Select the message at view index.
|
|
*
|
|
* @param aViewIndex The view index to select. This will be bounds-checked
|
|
* and if it is outside the bounds, we will clear the selection and
|
|
* bail.
|
|
*/
|
|
selectViewIndex: function FolderDisplayWidget_selectViewIndex(aViewIndex) {
|
|
let treeSelection = this.treeSelection;
|
|
// if we have no selection, we can't select something
|
|
if (!treeSelection)
|
|
return;
|
|
let rowCount = this.view.dbView.rowCount;
|
|
if ((aViewIndex == nsMsgViewIndex_None) ||
|
|
(aViewIndex < 0) || (aViewIndex >= rowCount)) {
|
|
this.clearSelection();
|
|
return;
|
|
}
|
|
|
|
// Check whether the index is already selected/current. This can be the
|
|
// case when we are here as the result of a deletion. Assuming
|
|
// nsMsgDBView::NoteChange ran and was not suppressing change
|
|
// notifications, then it's very possible the selection is already where
|
|
// we want it to go. However, in that case, nsMsgDBView::SelectionChanged
|
|
// bailed without doing anything because m_deletingRows...
|
|
// So we want to generate a change notification if that is the case. (And
|
|
// we still want to call ensureRowIsVisible, as there may be padding
|
|
// required.)
|
|
if ((treeSelection.count == 1) &&
|
|
((treeSelection.currentIndex == aViewIndex) ||
|
|
treeSelection.isSelected(aViewIndex))) {
|
|
// Make sure the index we just selected is also the current index.
|
|
// This can happen when the tree selection adjusts itself as a result of
|
|
// changes to the tree as a result of deletion. This will not trigger
|
|
// a notification.
|
|
treeSelection.select(aViewIndex);
|
|
this.view.dbView.selectionChanged();
|
|
}
|
|
// Previous code was concerned about avoiding updating commands on the
|
|
// assumption that only the selection count mattered. We no longer
|
|
// make this assumption.
|
|
// Things that may surprise you about the call to treeSelection.select:
|
|
// 1) This ends up calling the onselect method defined on the XUL 'tree'
|
|
// tag. For the 3pane this is the ThreadPaneSelectionChanged method in
|
|
// threadPane.js. That code checks a global to see if it is dealing
|
|
// with a right-click, and ignores it if so.
|
|
else {
|
|
treeSelection.select(aViewIndex);
|
|
}
|
|
|
|
if (this._active)
|
|
this.ensureRowIsVisible(aViewIndex);
|
|
|
|
// The saved selection is invalidated, since we've got something newer
|
|
this._savedSelection = null;
|
|
|
|
// Do this here instead of at the beginning to prevent reentrancy issues
|
|
this._aboutToSelectMessage = false;
|
|
},
|
|
|
|
/**
|
|
* For every selected message in the display that is part of a (displayed)
|
|
* thread and is not the root message, de-select it and ensure that the
|
|
* root message of the thread is selected.
|
|
* This is primarily intended to be used when collapsing visible threads.
|
|
*
|
|
* We do nothing if we are not in a threaded display mode.
|
|
*/
|
|
selectSelectedThreadRoots:
|
|
function FolderDisplayWidget_selectSelectedThreadRoots() {
|
|
if (!this.view.showThreaded)
|
|
return;
|
|
|
|
// There are basically two implementation strategies available to us:
|
|
// 1) For each selected view index with a level > 0, keep walking 'up'
|
|
// (numerically smaller) until we find a message with level 0.
|
|
// The inefficiency here is the potentially large number of JS calls
|
|
// into XPCOM space that will be required.
|
|
// 2) Ask for the thread that each view index belongs to, use that to
|
|
// efficiently retrieve the thread root, then find the root using
|
|
// the message header. The inefficiency here is that the view
|
|
// currently does a linear scan, albeit a relatively efficient one.
|
|
// And the winner is... option 2, because the code is simpler because we
|
|
// can reuse selectMessages to do most of the work.
|
|
let selectedIndices = this.selectedIndices;
|
|
let newSelectedMessages = [];
|
|
let dbView = this.view.dbView;
|
|
for (let index of selectedIndices) {
|
|
let thread = dbView.getThreadContainingIndex(index);
|
|
// We use getChildHdrAt instead of getRootHdr because getRootHdr has
|
|
// a useless out-param and just calls getChildHdrAt anyways.
|
|
newSelectedMessages.push(thread.getChildHdrAt(0));
|
|
}
|
|
this.selectMessages(newSelectedMessages);
|
|
},
|
|
|
|
//@}
|
|
|
|
/**
|
|
* @name Ensure Visibility
|
|
*/
|
|
//@{
|
|
|
|
/**
|
|
* Minimum number of lines to display between the 'focused' message and the
|
|
* top / bottom of the thread pane.
|
|
*/
|
|
get visibleRowPadding() {
|
|
let topPadding, bottomPadding;
|
|
|
|
// If we can get the height of the folder pane, treat the values as
|
|
// percentages of that.
|
|
if (this.treeBox) {
|
|
let topPercentPadding = Services.prefs.getIntPref(
|
|
"mail.threadpane.padding.top_percent");
|
|
let bottomPercentPadding = Services.prefs.getIntPref(
|
|
"mail.threadpane.padding.bottom_percent");
|
|
|
|
// Assume the bottom row is half-visible and should generally be ignored.
|
|
// (We could actually do the legwork to see if there is a partial one...)
|
|
let paneHeight = this.treeBox.getPageLength() - 1;
|
|
|
|
// Convert from percentages to absolute row counts.
|
|
topPadding = Math.ceil((topPercentPadding / 100) * paneHeight);
|
|
bottomPadding = Math.ceil((bottomPercentPadding / 100) * paneHeight);
|
|
|
|
// We need one visible row not counted in either padding, for the actual
|
|
// target message. Also helps correct for rounding errors.
|
|
if (topPadding + bottomPadding > paneHeight) {
|
|
if (topPadding > bottomPadding)
|
|
topPadding--;
|
|
else
|
|
bottomPadding--;
|
|
}
|
|
} else {
|
|
// Something's gone wrong elsewhere, and we likely have bigger problems.
|
|
topPadding = 0;
|
|
bottomPadding = 0;
|
|
Components.utils.reportError(
|
|
"Unable to get height of folder pane (treeBox is null)");
|
|
}
|
|
|
|
return [topPadding, bottomPadding];
|
|
},
|
|
|
|
/**
|
|
* Ensure the given view index is visible, optionally with some padding.
|
|
* By padding, we mean that the index will not be the first or last message
|
|
* displayed, but rather have messages on either side.
|
|
* We have the concept of a 'lip' when we are at the end of the message
|
|
* display. If we are near the end of the display, we want to show an
|
|
* empty row (at the bottom) so the user knows they are at the end. Also,
|
|
* if a message shows up that is new and things are sorted ascending, this
|
|
* turns out to be useful.
|
|
*/
|
|
ensureRowIsVisible: function FolderDisplayWidget_ensureRowIsVisible(
|
|
aViewIndex, aBounced) {
|
|
// Dealing with the tree view layout is a nightmare, let's just always make
|
|
// sure we re-schedule ourselves. The most particular rationale here is
|
|
// that the message pane may be toggling its state and it's much simpler
|
|
// and reliable if we ensure that all of FolderDisplayWidget's state
|
|
// change logic gets to run to completion before we run ourselves.
|
|
if (!aBounced) {
|
|
let dis = this;
|
|
window.setTimeout(function() {
|
|
dis.ensureRowIsVisible(aViewIndex, true);
|
|
}, 0);
|
|
}
|
|
|
|
let treeBox = this.treeBox;
|
|
if (!treeBox)
|
|
return;
|
|
|
|
// try and trigger a reflow...
|
|
treeBox.height;
|
|
|
|
let maxIndex = this.view.dbView.rowCount - 1;
|
|
|
|
let first = treeBox.getFirstVisibleRow();
|
|
// Assume the bottom row is half-visible and should generally be ignored.
|
|
// (We could actually do the legwork to see if there is a partial one...)
|
|
const halfVisible = 1;
|
|
let last = treeBox.getLastVisibleRow() - halfVisible;
|
|
let span = treeBox.getPageLength() - halfVisible;
|
|
let [topPadding, bottomPadding] = this.visibleRowPadding;
|
|
|
|
let target;
|
|
// If the index is after the last visible guy (with padding), move down
|
|
// so that the target index is padded in 1 from the bottom.
|
|
if (aViewIndex >= (last - bottomPadding))
|
|
target = Math.min(maxIndex, (aViewIndex + bottomPadding)) - span;
|
|
// If the index is before the first visible guy (with padding), move up
|
|
else if (aViewIndex <= (first + topPadding)) // move up
|
|
target = Math.max(0, (aViewIndex - topPadding));
|
|
else // it is already visible
|
|
return;
|
|
|
|
// this sets the first visible row
|
|
treeBox.scrollToRow(target);
|
|
},
|
|
|
|
/**
|
|
* Ensure that the given range of rows is maximally visible in the thread
|
|
* pane. If the range is larger than the number of rows that can be
|
|
* displayed in the thread pane, we bias towards showing the min row (with
|
|
* padding).
|
|
*
|
|
* @param aMinRow The numerically smallest row index defining the start of
|
|
* the inclusive range.
|
|
* @param aMaxRow The numberically largest row index defining the end of the
|
|
* inclusive range.
|
|
*/
|
|
ensureRowRangeIsVisible:
|
|
function FolderDisplayWidget_ensureRowRangeIsVisible(aMinRow, aMaxRow,
|
|
aBounced) {
|
|
// Dealing with the tree view layout is a nightmare, let's just always make
|
|
// sure we re-schedule ourselves. The most particular rationale here is
|
|
// that the message pane may be toggling its state and it's much simpler
|
|
// and reliable if we ensure that all of FolderDisplayWidget's state
|
|
// change logic gets to run to completion before we run ourselves.
|
|
if (!aBounced) {
|
|
let dis = this;
|
|
window.setTimeout(function() {
|
|
dis.ensureRowRangeIsVisible(aMinRow, aMaxRow, true);
|
|
}, 0);
|
|
}
|
|
|
|
let treeBox = this.treeBox;
|
|
if (!treeBox)
|
|
return;
|
|
let first = treeBox.getFirstVisibleRow();
|
|
const halfVisible = 1;
|
|
let last = treeBox.getLastVisibleRow() - halfVisible;
|
|
let span = treeBox.getPageLength() - halfVisible;
|
|
let [topPadding, bottomPadding] = this.visibleRowPadding;
|
|
|
|
// bail if the range is already visible with padding constraints handled
|
|
if (((first + topPadding) <= aMinRow) &&
|
|
((last - bottomPadding) >= aMaxRow))
|
|
return;
|
|
|
|
let target;
|
|
// if the range is bigger than we can fit, optimize position for the min row
|
|
// with padding to make it obvious the range doesn't extend above the row.
|
|
if (aMaxRow - aMinRow > span) {
|
|
target = Math.max(0, (aMinRow - topPadding));
|
|
} else {
|
|
// So the range must fit, and it's a question of how we want to position
|
|
// it. For now, the answer is we try and center it, why not.
|
|
let rowSpan = aMaxRow - aMinRow + 1;
|
|
let halfSpare = Math.floor((span - rowSpan - topPadding - bottomPadding) / 2);
|
|
target = aMinRow - halfSpare - topPadding;
|
|
}
|
|
treeBox.scrollToRow(target);
|
|
},
|
|
|
|
/**
|
|
* Ensure that the selection is visible to the extent possible.
|
|
*/
|
|
ensureSelectionIsVisible:
|
|
function FolderDisplayWidget_ensureSelectionIsVisible() {
|
|
let treeSelection = this.treeSelection; // potentially magic getter
|
|
if (!treeSelection || !treeSelection.count)
|
|
return;
|
|
|
|
let minRow = null, maxRow = null;
|
|
|
|
let rangeCount = treeSelection.getRangeCount();
|
|
for (let iRange = 0; iRange < rangeCount; iRange++) {
|
|
let rangeMinObj = {}, rangeMaxObj = {};
|
|
treeSelection.getRangeAt(iRange, rangeMinObj, rangeMaxObj);
|
|
let rangeMin = rangeMinObj.value, rangeMax = rangeMaxObj.value;
|
|
if (minRow == null || rangeMin < minRow)
|
|
minRow = rangeMin;
|
|
if (maxRow == null || rangeMax > maxRow )
|
|
maxRow = rangeMax;
|
|
}
|
|
|
|
this.ensureRowRangeIsVisible(minRow, maxRow);
|
|
}
|
|
//@}
|
|
};
|
|
|
|
/**
|
|
* Implement a fake nsITreeBoxObject so that we can keep the view
|
|
* nsITreeSelection selections 'live' when they are in the background. We need
|
|
* to do this because nsTreeSelection changes its behaviour (and gets ornery)
|
|
* if it does not have a box object.
|
|
* This does not need to exist once we abandon multiplexed tabbing.
|
|
*
|
|
* Sometimes, nsTreeSelection tries to turn us into an nsIBoxObject and then in
|
|
* turn get the associated element, and then create DOM events on that. The
|
|
* only event that we care about is onselect, so we get a DOM node here (with
|
|
* an event listener for onselect already attached), and pass its boxObject in
|
|
* whenever nsTreeSelection QIs us to nsIBoxObject.
|
|
*/
|
|
function FakeTreeBoxObject(aDOMNode) {
|
|
this.domNode = aDOMNode;
|
|
this.view = null;
|
|
}
|
|
FakeTreeBoxObject.prototype = {
|
|
view: null,
|
|
ensureRowIsVisible: function FakeTreeBoxObject_ensureRowIsVisible() {
|
|
// NOP
|
|
},
|
|
/**
|
|
* No need to actually invalidate, as when we re-root the view this will
|
|
* happen.
|
|
*/
|
|
invalidate: function FakeTreeBoxObject_invalidate() {
|
|
// NOP
|
|
},
|
|
invalidateRange: function FakeTreeBoxObject_invalidateRange() {
|
|
// NOP
|
|
},
|
|
invalidateRow: function FakeTreeBoxObject_invalidateRow() {
|
|
// NOP
|
|
},
|
|
beginUpdateBatch: function FakeTreeBoxObject_beginUpdateBatch() {
|
|
|
|
},
|
|
endUpdateBatch: function FakeTreeBoxObject_endUpdateBatch() {
|
|
|
|
},
|
|
/**
|
|
* We're going to make an exception to our NOP rule here, as this is rather
|
|
* important for us to pass on. The db view calls this if a row's been
|
|
* inserted or deleted. Without this, the selection's going to be out of sync
|
|
* with the view.
|
|
*
|
|
* @param aIndex the index where the rows have been inserted or deleted
|
|
* @param aCount the number of rows inserted or deleted (negative for
|
|
* deleted)
|
|
*/
|
|
rowCountChanged: function FakeTreeBoxObject_rowCountChanged(aIndex, aCount) {
|
|
if (aCount == 0 || !this.view)
|
|
// Nothing to do
|
|
return;
|
|
|
|
let selection = this.view.selection;
|
|
if (selection)
|
|
selection.adjustSelection(aIndex, aCount);
|
|
},
|
|
get element() {return this.domNode;},
|
|
get x() {return this.domNode.boxObject.x},
|
|
get y() {return this.domNode.boxObject.y},
|
|
get screenX() {return this.domNode.boxObject.screenX},
|
|
get screenY() {return this.domNode.boxObject.screenY},
|
|
get width() {return this.domNode.boxObject.width},
|
|
get height() {return this.domNode.boxObject.height},
|
|
get parentBox() {return this.domNode.boxObject.parentBox},
|
|
get firstChild() {return this.domNode.boxObject.firstChild},
|
|
get lastChild() {return this.domNode.boxObject.lastChild},
|
|
get nextSibling() {return this.domNode.boxObject.nextSibling},
|
|
get previousSibling() {return this.domNode.boxObject.previousSibling},
|
|
getPropertyAsSupports : function FakeTreeBoxObject_getPropertyAsSupports(propertyName) {
|
|
return this.domNode.boxObject.getPropertyAsSupports(propertyName);
|
|
},
|
|
setPropertyAsSupports : function FakeTreeBoxObject_setPropertyAsSupports(propertyName, value) {
|
|
this.domNode.boxObject.setPropertyAsSupports(propertyName, value);
|
|
},
|
|
getProperty : function FakeTreeBoxObject_getProperty(propertyName) {
|
|
return this.domNode.boxObject.getProperty(propertyName);
|
|
},
|
|
setProperty : function FakeTreeBoxObject_setProperty(propertyName, value) {
|
|
return this.domNode.boxObject.setProperty(propertyName, value);
|
|
},
|
|
removeProperty : function FakeTreeBoxObject_removeProperty(propertyName) {
|
|
return this.domNode.boxObject.removeProperty(propertyName);
|
|
},
|
|
QueryInterface: function FakeTreeBoxObject_QueryInterface(aIID) {
|
|
if (!aIID.equals(Components.interfaces.nsISupports) &&
|
|
!aIID.equals(Components.interfaces.nsIBoxObject) &&
|
|
!aIID.equals(Components.interfaces.nsITreeBoxObject))
|
|
throw Components.results.NS_ERROR_NO_INTERFACE;
|
|
return this;
|
|
}
|
|
};
|
|
/*
|
|
* Provide attribute and function implementations that complain very loudly if
|
|
* they are used. Now, XPConnect will return an error to callers if we don't
|
|
* implement part of the interface signature, but this is unlikely to provide
|
|
* the visibility we desire. In fact, since it is a simple nsresult error,
|
|
* it may make things completely crazy. So this way we can yell via dump,
|
|
* throw an exception, etc.
|
|
*/
|
|
function FTBO_stubOutAttributes(aObj, aAttribNames) {
|
|
for (let attrName of aAttribNames) {
|
|
let myAttrName = attrName;
|
|
aObj.__defineGetter__(attrName,
|
|
function() {
|
|
let msg = "Read access to stubbed attribute " + myAttrName;
|
|
dump(msg + "\n");
|
|
debugger;
|
|
throw new Error(msg);
|
|
});
|
|
aObj.__defineSetter__(attrName,
|
|
function() {
|
|
let msg = "Write access to stubbed attribute " + myAttrName;
|
|
dump(msg + "\n");
|
|
debugger;
|
|
throw new Error(msg);
|
|
});
|
|
}
|
|
}
|
|
function FTBO_stubOutMethods(aObj, aMethodNames) {
|
|
for (let methodName of aMethodNames) {
|
|
let myMethodName = methodName;
|
|
aObj[myMethodName] = function() {
|
|
let msg = "Call to stubbed method " + myMethodName;
|
|
dump(msg + "\n");
|
|
debugger;
|
|
throw new Error(msg);
|
|
};
|
|
}
|
|
}
|
|
FTBO_stubOutAttributes(FakeTreeBoxObject.prototype, [
|
|
"columns",
|
|
"focused",
|
|
"treeBody",
|
|
"rowHeight",
|
|
"rowWidth",
|
|
"horizontalPosition",
|
|
"selectionRegion",
|
|
]);
|
|
FTBO_stubOutMethods(FakeTreeBoxObject.prototype, [
|
|
"getFirstVisibleRow",
|
|
"getLastVisibleRow",
|
|
"getPageLength",
|
|
"ensureCellIsVisible",
|
|
"scrollToRow",
|
|
"scrollByLines",
|
|
"scrollByPages",
|
|
"scrollToCell",
|
|
"scrollToColumn",
|
|
"scrollToHorizontalPosition",
|
|
"invalidateColumn",
|
|
"invalidateCell",
|
|
"invalidateColumnRange",
|
|
"getRowAt",
|
|
"getCellAt",
|
|
"getCoordsForCellItem",
|
|
"isCellCropped",
|
|
"clearStyleAndImageCaches",
|
|
]);
|