Files
binoc-central-mirror/mail/modules/dbViewWrapper.js
T
2020-05-10 13:52:36 -04:00

2059 lines
78 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/. */
this.EXPORTED_SYMBOLS = ['DBViewWrapper', 'IDBViewWrapperListener'];
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cr = Components.results;
var Cu = Components.utils;
Cu.import("resource:///modules/mailServices.js");
Cu.import("resource:///modules/mailViewManager.js");
Cu.import("resource:///modules/searchSpec.js");
Cu.import("resource:///modules/virtualFolderWrapper.js");
var nsMsgFolderFlags = Ci.nsMsgFolderFlags;
var nsMsgViewType = Ci.nsMsgViewType;
var nsMsgViewFlagsType = Ci.nsMsgViewFlagsType;
var nsMsgViewSortType = Ci.nsMsgViewSortType;
var nsMsgViewSortOrder = Ci.nsMsgViewSortOrder;
var nsMsgMessageFlags = Ci.nsMsgMessageFlags;
var MSG_VIEW_FLAG_DUMMY = 0x20000000;
var nsMsgViewIndex_None = 0xffffffff;
/**
* Helper singleton for DBViewWrapper that tells instances when something
* interesting is happening to the folder(s) they care about. The rationale
* for this is to:
* - reduce listener overhead (although arguably the events we listen to are
* fairly rare)
* - make testing / verification easier by centralizing and exposing listeners.
*
*/
var FolderNotificationHelper = {
/**
* Maps URIs of pending folder loads to the DBViewWrapper instances that
* are waiting on the loads. The value is a list of instances in case
* a quick-clicking user is able to do something unexpected.
*/
_pendingFolderUriToViewWrapperLists: {},
/**
* Map URIs of folders to view wrappers interested in hearing about their
* deletion.
*/
_interestedWrappers: {},
/**
* Array of wrappers that are interested in all folders, used for
* search results wrappers.
*/
_curiousWrappers: [],
/**
* Initialize our listeners. We currently don't bother cleaning these up
* because we are a singleton and if anyone imports us, they probably want
* us for as long as their application so shall live.
*/
_init: function FolderNotificationHelper__init() {
// register with the session for our folded loaded notifications
MailServices.mailSession.AddFolderListener(this,
Ci.nsIFolderListener.event |
Ci.nsIFolderListener.intPropertyChanged);
// register with the notification service for deleted folder notifications
MailServices.mfn.addListener(this,
Ci.nsIMsgFolderNotificationService.folderDeleted |
// we need to track renames because we key off of URIs. frick.
Ci.nsIMsgFolderNotificationService.folderRenamed |
Ci.nsIMsgFolderNotificationService.folderMoveCopyCompleted);
},
/**
* Call updateFolder, and assuming all goes well, request that the provided
* FolderDisplayWidget be notified when the folder is loaded. This method
* performs the updateFolder call for you so there is less chance of leaking.
* In the event the updateFolder call fails, we will propagate the exception.
*/
updateFolderAndNotifyOnLoad:
function FolderNotificationHelper_notifyOnLoad(aFolder,
aFolderDisplay,
aMsgWindow) {
// set up our datastructure first in case of wacky event sequences
let folderURI = aFolder.URI;
let wrappers = this._pendingFolderUriToViewWrapperLists[folderURI];
if (wrappers == null)
wrappers = this._pendingFolderUriToViewWrapperLists[folderURI] = [];
wrappers.push(aFolderDisplay);
try {
aFolder.updateFolder(aMsgWindow);
}
catch (ex) {
// uh-oh, that didn't work. tear down the data structure...
wrappers.pop();
if (wrappers.length == 0)
delete this._pendingFolderUriToViewWrapperLists[folderURI];
throw ex;
}
},
/**
* Request notification of every little thing these folders do.
*
* @param aFolders The folders.
* @param aNotherFolder A folder that may or may not be in aFolders.
* @param aViewWrapper The view wrapper that is up to no good.
*/
stalkFolders: function FolderNotificationHelper_stalkFolders(
aFolders, aNotherFolder, aViewWrapper) {
let folders = aFolders ? aFolders.concat() : [];
if (aNotherFolder && !folders.includes(aNotherFolder))
folders.push(aNotherFolder);
for (let folder of folders) {
let wrappers = this._interestedWrappers[folder.URI];
if (wrappers == null)
wrappers = this._interestedWrappers[folder.URI] = [];
wrappers.push(aViewWrapper);
}
},
/**
* Request notification of every little thing every folder does.
*
* @param aViewWrapper - the viewWrapper interested in every notification.
* This will be a search results view of some sort.
*/
noteCuriosity: function FolderNotificationHelper_noteCuriosity(aViewWrapper) {
this._curiousWrappers.push(aViewWrapper);
},
/**
* Removal helper for use by removeNotifications.
*
* @param aTable The table mapping URIs to list of view wrappers.
* @param aFolder The folder we care about.
* @param aViewWrapper The view wrapper of interest.
*/
_removeWrapperFromListener: function(aTable, aFolder, aViewWrapper) {
let wrappers = aTable[aFolder.URI];
if (wrappers) {
let index = wrappers.indexOf(aViewWrapper);
if (index >= 0)
wrappers.splice(index, 1);
if (wrappers.length == 0)
delete aTable[aFolder.URI];
}
},
/**
* Remove notification requests on the provided folders by the given view
* wrapper.
*/
removeNotifications: function FolderNotificationHelper_removeNotifications(
aFolders, aViewWrapper) {
if (!aFolders) {
this._curiousWrappers.splice(this._curiousWrappers.indexOf(aViewWrapper), 1);
return;
}
for (let folder of aFolders) {
this._removeWrapperFromListener(
this._interestedWrappers, folder, aViewWrapper);
this._removeWrapperFromListener(
this._pendingFolderUriToViewWrapperLists, folder, aViewWrapper);
}
},
/**
* @return true if there are any listeners still registered. This is intended
* to support debugging code. If you are not debug code, you are a bad
* person/code.
*/
haveListeners: function FolderNotificationHelper_haveListeners() {
for (let folderURI in this._pendingFolderUriToViewWrapperLists) {
let wrappers = this._pendingFolderUriToViewWrapperLists[folderURI];
return true;
}
for (let folderURI in this._interestedWrappers) {
let wrappers = this._interestedWrappers[folderURI];
return true;
}
return this._curiousWrappers.length != 0;
},
/* ***** Notifications ***** */
_notifyHelper: function FolderNotificationHelper__notifyHelper(aFolder,
aHandlerName) {
let wrappers = this._interestedWrappers[aFolder.URI];
if (wrappers) {
// clone the list to avoid confusing mutation by listeners
for (let wrapper of wrappers.concat()) {
wrapper[aHandlerName](aFolder);
}
}
for (let wrapper of this._curiousWrappers)
wrapper[aHandlerName](aFolder);
},
OnItemEvent: function FolderNotificationHelper_OnItemEvent(
aFolder, aEvent) {
let eventType = aEvent.toString();
if (eventType == "FolderLoaded") {
let folderURI = aFolder.URI;
let widgets = this._pendingFolderUriToViewWrapperLists[folderURI];
if (widgets) {
for (let widget of widgets) {
// we are friends, this is an explicit relationship.
// (we don't use a generic callback mechanism because the 'this' stuff
// gets ugly and no one else should be hooking in at this level.)
try {
widget._folderLoaded(aFolder);
}
catch (ex) {
dump("``` EXCEPTION DURING NOTIFY: " + ex.fileName + ":" +
ex.lineNumber + ": " + ex + "\n");
if (ex.stack)
dump("STACK: " + ex.stack + "\n");
Cu.reportError(ex);
}
}
delete this._pendingFolderUriToViewWrapperLists[folderURI];
}
}
else if (eventType == "AboutToCompact") {
this._notifyHelper(aFolder, "_aboutToCompactFolder");
}
else if (eventType == "CompactCompleted") {
this._notifyHelper(aFolder, "_compactedFolder");
}
else if (eventType == "DeleteOrMoveMsgCompleted") {
this._notifyHelper(aFolder, "_deleteCompleted");
}
else if (eventType == "DeleteOrMoveMsgFailed") {
this._notifyHelper(aFolder, "_deleteFailed");
}
else if (eventType == "RenameCompleted") {
this._notifyHelper(aFolder, "_renameCompleted");
}
},
OnItemIntPropertyChanged: function(aFolder, aProperty, aOldValue, aNewValue) {
let propertyString = aProperty.toString();
if ((propertyString == "TotalMessages") ||
(propertyString == "TotalUnreadMessages"))
this._notifyHelper(aFolder, "_messageCountsChanged");
},
_folderMoveHelper: function(aOldFolder, aNewFolder) {
let oldURI = aOldFolder.URI;
let newURI = aNewFolder.URI;
// fix up our listener tables.
if (oldURI in this._pendingFolderUriToViewWrapperLists) {
this._pendingFolderUriToViewWrapperLists[newURI] =
this._pendingFolderUriToViewWrapperLists[oldURI];
delete this._pendingFolderUriToViewWrapperLists[oldURI];
}
if (oldURI in this._interestedWrappers) {
this._interestedWrappers[newURI] =
this._interestedWrappers[oldURI];
delete this._interestedWrappers[oldURI];
}
let wrappers = this._interestedWrappers[newURI];
if (wrappers) {
// clone the list to avoid confusing mutation by listeners
for (let wrapper of wrappers.concat()) {
wrapper._folderMoved(aOldFolder, aNewFolder);
}
}
},
/**
* Update our URI mapping tables when renames happen.
*/
folderRenamed: function FolderNotificationHelper_folderRenamed(aOrigFolder,
aNewFolder) {
this._folderMoveHelper(aOrigFolder, aNewFolder);
},
folderMoveCopyCompleted:
function FolderNotificationHelper_folderMoveCopyCompleted(aMove,
aSrcFolder,
aDestFolder) {
if (aMove) {
let aNewFolder = aDestFolder.getChildNamed(aSrcFolder.prettyName);
this._folderMoveHelper(aSrcFolder, aNewFolder);
}
},
folderDeleted: function FolderNotificationHelper_folderDeleted(aFolder) {
let wrappers = this._interestedWrappers[aFolder.URI];
if (wrappers) {
// clone the list to avoid confusing mutation by listeners
for (let wrapper of wrappers.concat()) {
wrapper._folderDeleted(aFolder);
}
// if the folder is deleted, it's not going to ever do anything again
delete this._interestedWrappers[aFolder.URI];
}
},
};
FolderNotificationHelper._init();
/**
* Defines the DBViewWrapper listener interface. This class exists exclusively
* for documentation purposes and should never be instantiated.
*/
function IDBViewWrapperListener() {
}
IDBViewWrapperListener.prototype = {
// uh, this is secretly exposed for debug purposes. DO NOT LOOK AT ME!
_FNH: FolderNotificationHelper,
/* ===== Exposure of UI Globals ===== */
messenger: null,
msgWindow: null,
threadPaneCommandUpdater: null,
/* ===== Guidance ===== */
/**
* Indicate whether mail view settings should be used/honored. A UI oddity
* is that we only have mail views be sticky if its combo box UI is visible.
* (Without the view combobox, it may not be obvious that the mail is
* filtered.)
*/
get shouldUseMailViews() {
return false;
},
/**
* Should we defer displaying the messages in this folder until after we have
* talked to the server? This is for our poor man's password protection
* via the "mail.password_protect_local_cache" pref. We add this specific
* check rather than internalizing the logic in the wrapper because the
* password protection is a shoddy UI-only protection.
*/
get shouldDeferMessageDisplayUntilAfterServerConnect() {
return false;
},
/**
* Should we mark all messages in a folder as read on exit?
* This is nominally controlled by the "mailnews.mark_message_read.SERVERTYPE"
* preference (on a per-server-type basis).
* For the record, this functionality should not remotely be in the core.
*
* @param aMsgFolder The folder we are leaving and are unsure if we should
* mark all its messages read. I pass the folder instead of the server
* type because having a crazy feature like this will inevitably lead to
* a more full-featured crazy feature (why not on a per-folder basis, eh?)
* @return true if we should mark all the dudes as read, false if not.
*/
shouldMarkMessagesReadOnLeavingFolder: function (aMsgFolder) {
return false;
},
/* ===== Event Notifications ===== */
/* === Status Changes === */
/**
* We tell you when we start and stop loading the folder. This is a good
* time to mess with the hour-glass cursor machinery if you are inclined to
* do so.
*/
onFolderLoading: function (aIsFolderLoading) {
},
/**
* We tell you when we start and stop searching. This is a good time to mess
* with progress spinners (meteors) and the like, if you are so inclined.
*/
onSearching: function (aIsSearching) {
},
/**
* This event is generated when a new view has been created. It is intended
* to be used to provide the MsgCreateDBView notification so that custom
* columns can add themselves to the view.
* The notification is not generated by the DBViewWrapper itself because this
* is fundamentally a UI issue. Additionally, because the MsgCreateDBView
* notification consumers assume gDBView whose exposure is affected by tabs,
* the tab logic needs to be involved.
*/
onCreatedView: function() {
},
/**
* This event is generated just before we close/destroy a message view.
*
* @param aFolderIsComingBack Indicates whether we are planning to create a
* new view to display the same folder after we destroy this view. This
* will be the case unless we are switching to display a new folder or
* closing the view wrapper entirely.
*/
onDestroyingView: function(aFolderIsComingBack) {
},
/**
* Generated when we are loading information about the folder from its
* dbFolderInfo. The dbFolderInfo object is passed in.
* The DBViewWrapper has already restored its state when this function is
* called, but has not yet created the dbView. A view update is in process,
* so the view settings can be changed and will take effect when the update
* is closed.
* |onDisplayingFolder| is the next expected notification following this
* notification.
*/
onLoadingFolder: function(aDbFolderInfo) {
},
/**
* Generated when the folder is being entered for display. This is the chance
* for the listener to affect any UI-related changes to the folder required.
* Currently, this just means setting the header cache size (which needs to
* be proportional to the number of lines in the tree view, and is thus a
* UI issue.)
* The dbView has already been created and is valid when this function is
* called.
* |onLoadingFolder| is called before this notification.
*/
onDisplayingFolder: function() {
},
/**
* Generated when we are leaving a folder.
*/
onLeavingFolder: function() {
},
/**
* Things to do once 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 (multi-folder) virtual folder, this happens when the search
* completes.
* You may get onMessagesLoaded called with aAll false immediately after
* the view is opened. You will definitely get onMessagesLoaded(true)
* when we've finished getting the headers for the view.
*/
onMessagesLoaded: function(aAll) {
},
/**
* The mail view changed. The mail view widget is likely to care about this.
*/
onMailViewChanged: function() {
},
/**
* The active sort changed, and that is all that changed. If the sort is
* changing because the view is being destroyed and re-created, this event
* will not be generated.
*/
onSortChanged: function() {
},
/**
* This event is generated when messages in one of the folders backing the
* view have been removed by message moves / deletion. If there is a search
* in effect, it is possible that the removed messages were not visible in
* the view in the first place.
*/
onMessagesRemoved: function () {
},
/**
* Like onMessagesRemoved, but something went awry in the move/deletion and
* it failed. Although this is not a very interesting event on its own,
* it is useful in cases where the listener was expecting an
* onMessagesRemoved and might need to clean some state up.
*/
onMessageRemovalFailed: function () {
},
/**
* The total message count or total unread message counts changed.
*/
onMessageCountsChanged: function () {
},
};
/**
* Encapsulates everything related to working with our nsIMsgDBView
* implementations.
*
* Things we do not do and why we do not do them:
* - Selection. This depends on having an nsITreeSelection object and we choose
* to avoid entanglement with XUL/layout code. Selection accordingly must be
* handled a layer up in the FolderDisplayWidget.
*/
function DBViewWrapper(aListener) {
this.displayedFolder = null;
this.listener = aListener;
this._underlyingData = this.kUnderlyingNone;
this._underlyingFolders = null;
this._syntheticView = null;
this._viewUpdateDepth = 0;
this._mailViewIndex = MailViewConstants.kViewItemAll;
this._mailViewData = null;
this._specialView = null;
this._sort = [];
// see the _viewFlags getter and setter for info on our use of __viewFlags.
this.__viewFlags = null;
/**
* It's possible to support grouped view thread expand/collapse, and also sort
* by thread despite the back end (see nsMsgQuickSearchDBView::SortThreads).
* Also, nsMsgQuickSearchDBView does not respect the kExpandAll flag, fix that.
*/
this._threadExpandAll = true,
this.dbView = null;
this.search = null;
this._folderLoading = false;
this._searching = false;
}
DBViewWrapper.prototype = {
/* = constants explaining the nature of the underlying data = */
/**
* We currently don't have any underlying data.
*/
kUnderlyingNone: 0,
/**
* The underlying data source is a single folder.
*/
kUnderlyingRealFolder: 1,
/**
* The underlying data source is a virtual folder that is operating over
* multiple underlying folders.
*/
kUnderlyingMultipleFolder: 2,
/**
* Our data source is transient, most likely a gloda search that crammed the
* results into us. This is different from a search view.
*/
kUnderlyingSynthetic: 3,
/**
* We are a search view, which translates into a search that has underlying
* folders, just like kUnderlyingMultipleFolder, but we have no
* displayedFolder. We differ from kUnderlyingSynthetic in that we are
* not just a bunch of message headers randomly crammed in.
*/
kUnderlyingSearchView: 4,
/**
* @return true if the folder being displayed is backed by a single 'real'
* folder. This folder can be a saved search on that folder or just
* an outright un-filtered display of that folder.
*/
get isSingleFolder() {
return this._underlyingData == this.kUnderlyingRealFolder;
},
/**
* @return true if the folder being displayed is a virtual folder backed by
* multiple 'real' folders or a search view. This corresponds to a
* cross-folder saved search.
*/
get isMultiFolder() {
return (this._underlyingData == this.kUnderlyingMultipleFolder) ||
(this._underlyingData == this.kUnderlyingSearchView);
},
/**
* @return true if the folder being displayed is not a real folder at all,
* but rather the result of an un-scoped search, such as a gloda search.
*/
get isSynthetic() {
return this._underlyingData == this.kUnderlyingSynthetic;
},
/**
* Check if the folder in question backs the currently displayed folder. For
* a virtual folder, this is a test of whether the virtual folder includes
* messages from the given folder. For a 'real' single folder, this is
* effectively a test against displayedFolder.
* If you want to see if the displayed folder is a folder, just compare
* against the displayedFolder attribute.
*/
isUnderlyingFolder: function DBViewWrapper_isUnderlyingFolder(aFolder) {
return this._underlyingFolders.some(underlyingFolder => aFolder == underlyingFolder);
},
/**
* Refresh the view by re-creating the view. You would do this to get rid of
* messages that no longer match the view but are kept around for view
* stability reasons. (In other words, in an unread-messages view, you would
* go insane if when you clicked on a message it immediately disappeared
* because it no longer matched.)
* This method was adding for testing purposes and does not have a (legacy) UI
* reason for existing. (The 'open' method is intended to behave identically
* to the legacy UI if you click on the currently displayed folder.)
*/
refresh: function DBViewWrapper_refresh() {
this._applyViewChanges();
},
/**
* Null out the folder's database to avoid memory bloat if we don't have a
* reason to keep the database around. Currently, we keep all Inboxes
* around and null out everyone else. This is a standard stopgap measure
* until we have something more clever going on.
* In general, there is little potential downside to nulling out the message
* database reference when it is in use. As long as someone is holding onto
* a message header from the database, the database will be kept open, and
* therefore the database service will still have a reference to the db.
* When the folder goes to ask for the database again, the service will have
* it, and it will not need to be re-opened.
*
* Another heuristic we could theoretically use is use the mail session's
* isFolderOpenInWindow call, except that uses the outmoded concept that each
* window will have at most one folder open. So nuts to that.
*
* Note: regrettably a unit test cannot verify that we did this; msgDatabase
* is a getter that will always try and load the message database!
*/
_releaseFolderDatabase: function DBViewWrapper__nullFolderDatabase(aFolder) {
if (!aFolder.isSpecialFolder(nsMsgFolderFlags.Inbox, false))
aFolder.msgDatabase = null;
},
/**
* Clone this DBViewWrapper and its underlying nsIMsgDBView.
*
* @param aListener {IDBViewWrapperListener} The listener to use on the new view.
*/
clone: function DBViewWrapper_clone(aListener) {
let doppel = new DBViewWrapper(aListener);
// -- copy attributes
doppel.displayedFolder = this.displayedFolder;
doppel._underlyingData = this._underlyingData;
doppel._underlyingFolders = this._underlyingFolders ?
this._underlyingFolders.concat() : null;
doppel._syntheticView = this._syntheticView;
// _viewUpdateDepth should stay at its initial value of zero
doppel._mailViewIndex = this._mailViewIndex;
doppel._mailViewData = this._mailViewData;
doppel._specialView = this._specialView;
// a shallow copy is all that is required for sort; we do not mutate entries
doppel._sort = this._sort.concat();
// -- register listeners...
// note: this does not get us a folder loaded notification. Our expected
// use case for cloning is displaying a single message already visible in
// the original view, which implies we don't need to hang about for folder
// loaded notification messages.
FolderNotificationHelper.stalkFolders(doppel._underlyingFolders,
doppel.displayedFolder,
doppel);
// -- clone the view
if (this.dbView)
doppel.dbView = this.dbView.cloneDBView(aListener.messenger,
aListener.msgWindow,
aListener.threadPaneCommandUpdater)
.QueryInterface(Components.interfaces.nsITreeView);
// -- clone the search
if (this.search)
doppel.search = this.search.clone(doppel);
if (doppel._underlyingData == this.kUnderlyingSearchView ||
doppel._underlyingData == this.kUnderlyingSynthetic)
FolderNotificationHelper.noteCuriosity(doppel);
return doppel;
},
/**
* Close the current view. You would only do this if you want to clean up all
* the resources associated with this view wrapper. You would not do this
* for UI reasons like the user de-selecting the node in the tree; we should
* always be displaying something when used in a UI context!
*
* @param aFolderIsDead If true, tells us not to try and tidy up on our way
* out by virtue of the fact that the folder is dead and should not be
* messed with.
*/
close: function DBViewWrapper_close(aFolderIsDead) {
if (this.displayedFolder != null) {
// onLeavingFolder does all the application-level stuff related to leaving
// the folder (marking as read, etc.) We only do this when the folder
// is not dead (for obvious reasons).
if (!aFolderIsDead) {
// onLeavingFolder must be called before we potentially null out its
// msgDatabase, which we will do in the upcoming underlyingFolders loop
this.onLeavingFolder(); // application logic
this.listener.onLeavingFolder(); // display logic
}
// (potentially) zero out the display folder if we are dealing with a
// virtual folder and so the next loop won't take care of it.
if (this.isVirtual) {
FolderNotificationHelper.removeNotifications([this.displayedFolder],
this);
this._releaseFolderDatabase(this.displayedFolder);
}
this.folderLoading = false;
this.displayedFolder = null;
}
FolderNotificationHelper.removeNotifications(this._underlyingFolders,
this);
if (this._underlyingFolders) {
// (potentially) zero out the underlying msgDatabase references
for (let folder of this._underlyingFolders)
this._releaseFolderDatabase(folder);
}
// kill off the view and its search association
if (this.dbView) {
this.listener.onDestroyingView(false);
this.search.dissociateView(this.dbView);
this.dbView.setTree(null);
this.dbView.selection = null;
this.dbView.close();
this.dbView = null;
}
// zero out the view update depth here. We don't do it on open because it's
// theoretically be nice to be able to start a view update before you open
// something so you can defer the open. In practice, that is not yet
// tested.
this._viewUpdateDepth = 0;
this._underlyingData = this.kUnderlyingNone;
this._underlyingFolders = null;
this._syntheticView = null;
this._mailViewIndex = MailViewConstants.kViewItemAll;
this._mailViewData = null;
this._specialView = null;
this._sort = [];
this.__viewFlags = null;
this.search = null;
},
/**
* Open the passed-in nsIMsgFolder folder. Use openSynthetic for synthetic
* view providers.
*/
open: function DBViewWrapper_open(aFolder) {
if (aFolder == null) {
this.close();
return;
}
// If we are in the same folder, there is nothing to do unless we are a
// virtual folder. Virtual folders apparently want to try and get updated.
if (this.displayedFolder == aFolder) {
if (!this.isVirtual)
return;
// note: we intentionally (for consistency with old code, not that the
// code claimed to have a good reason) fall through here and call
// onLeavingFolder via close even though that's debatable in this case.
}
this.close();
this.displayedFolder = aFolder;
this._enteredFolder = false;
this.search = new SearchSpec(this);
this._sort = [];
if (aFolder.isServer) {
this._showServer();
return;
}
this.beginViewUpdate();
let msgDatabase;
try {
// This will throw an exception if the .msf file is missing,
// out of date (e.g., the local folder has changed), or corrupted.
msgDatabase = this.displayedFolder.msgDatabase;
} catch (e) {}
if (msgDatabase)
this._prepareToLoadView(msgDatabase, aFolder);
if (!this.isVirtual) {
this.folderLoading = true;
FolderNotificationHelper.updateFolderAndNotifyOnLoad(
this.displayedFolder, this, this.listener.msgWindow);
}
// we do this after kicking off the update because this could initiate a
// search which could fight our explicit updateFolder call if the search
// is already outstanding.
if (this.shouldShowMessagesForFolderImmediately())
this._enterFolder();
},
/**
* Open a synthetic view provider as backing our view.
*/
openSynthetic: function DBViewWrapper_openSynthetic(aSyntheticView) {
this.close();
this._underlyingData = this.kUnderlyingSynthetic;
this._syntheticView = aSyntheticView;
this.search = new SearchSpec(this);
this._sort = this._syntheticView.defaultSort.concat();
this._applyViewChanges();
FolderNotificationHelper.noteCuriosity(this);
this.listener.onDisplayingFolder();
},
/**
* Makes us irrevocavbly be a search view, for use in search windows.
* Once you call this, you are not allowed to use us for anything
* but a search view!
* We add a 'searchFolders' property that allows you to control what
* folders we are searching over.
*/
openSearchView: function DBViewWrapper_openSearchView() {
this.close();
this._underlyingData = this.kUnderlyingSearchView;
this._underlyingFolders = [];
let dis = this;
this.__defineGetter__('searchFolders', function() {
return dis._underlyingFolders;
});
this.__defineSetter__('searchFolders', function(aSearchFolders) {
dis._underlyingFolders = aSearchFolders;
dis._applyViewChanges();
});
this.search = new SearchSpec(this);
// the search view uses the order in which messages are added as the
// order by default.
this._sort = [[nsMsgViewSortType.byNone, nsMsgViewSortOrder.ascending]];
FolderNotificationHelper.noteCuriosity(this);
this._applyViewChanges();
},
get folderLoading() {
return this._folderLoading;
},
set folderLoading(aFolderLoading) {
if (this._folderLoading == aFolderLoading)
return;
this._folderLoading = aFolderLoading;
// tell the folder about what is going on so it can remove its db change
// listener and restore it, respectively.
if (aFolderLoading)
this.displayedFolder.startFolderLoading();
else
this.displayedFolder.endFolderLoading();
this.listener.onFolderLoading(aFolderLoading);
},
get searching() {
return this._searching;
},
set searching(aSearching) {
if (aSearching == this._searching)
return;
this._searching = aSearching;
this.listener.onSearching(aSearching);
// notify that all messages are loaded if searching has concluded
if (!aSearching)
this.listener.onMessagesLoaded(true);
},
/**
* Do we want to show the messages immediately, or should we wait for
* updateFolder to complete? The historical heuristic is:
* - Virtual folders get shown immediately (and updateFolder has no
* meaning for them anyways.)
* - If _underlyingFolders == null, we failed to open the database,
* so we need to wait for UpdateFolder to reparse the folder (in the
* local folder case).
* - Wait on updateFolder if our poor man's security via
* "mail.password_protect_local_cache" preference is enabled and the
* server requires a password to login. This is accomplished by asking our
* listener via shouldDeferMessageDisplayUntilAfterServerConnect. Note that
* there is an obvious hole in this logic because of the virtual folder case
* above.
*
* @pre this.folderDisplayed is the folder we are talking about.
*
* @return true if the folder should be shown immediately, false if we should
* wait for updateFolder to complete.
*/
shouldShowMessagesForFolderImmediately:
function DBViewWrapper_showShowMessagesForFolderImmediately() {
return (this.isVirtual ||
!(this._underlyingFolders == null ||
this.listener.shouldDeferMessageDisplayUntilAfterServerConnect));
},
/**
* Extract information about the view from the dbFolderInfo (e.g., sort type,
* sort order, current view flags, etc), and save in the view wrapper.
*/
_prepareToLoadView:
function DBViewWrapper_prepareToLoadView(msgDatabase, aFolder) {
let dbFolderInfo = msgDatabase.dBFolderInfo;
// - retrieve persisted sort information
this._sort = [[dbFolderInfo.sortType, dbFolderInfo.sortOrder]];
// - retrieve persisted display settings
this.__viewFlags = dbFolderInfo.viewFlags;
// - retrieve persisted thread last expanded state.
this._threadExpandAll = Boolean(this.__viewFlags & nsMsgViewFlagsType.kExpandAll);
// Make sure the threaded bit is set if group-by-sort is set. The views
// encode 3 states in 2-bits, and we want to avoid that odd-man-out
// state.
// The expand flag must be set when opening a single virtual folder
// (quicksearch) in grouped view. The user's last set expand/collapse state
// for grouped/threaded in this use case is restored later.
if (this.__viewFlags & nsMsgViewFlagsType.kGroupBySort) {
this.__viewFlags |= nsMsgViewFlagsType.kThreadedDisplay;
this.__viewFlags |= nsMsgViewFlagsType.kExpandAll;
this._ensureValidSort();
}
// See if the last-used view was one of the special views. If so, put us in
// that special view mode. We intentionally do this after restoring the
// view flags because _setSpecialView enforces threading.
// The nsMsgDBView is the one who persists this information for us. In this
// case the nsMsgThreadedDBView superclass of the special views triggers it
// when opened.
let viewType = dbFolderInfo.viewType;
if ((viewType == nsMsgViewType.eShowThreadsWithUnread) ||
(viewType == nsMsgViewType.eShowWatchedThreadsWithUnread))
this._setSpecialView(viewType);
// - retrieve virtual folder configuration
if (aFolder.flags & nsMsgFolderFlags.Virtual) {
let virtFolder = VirtualFolderHelper.wrapVirtualFolder(aFolder);
// Filter out the server roots; they only exist for UI reasons.
this._underlyingFolders =
virtFolder.searchFolders.filter(folder => !folder.isServer);
this._underlyingData = (this._underlyingFolders.length > 1) ?
this.kUnderlyingMultipleFolder :
this.kUnderlyingRealFolder;
// figure out if we are using online IMAP searching
this.search.onlineSearch = virtFolder.onlineSearch;
// retrieve and chew the search query
this.search.virtualFolderTerms = virtFolder.searchTerms;
}
else {
this._underlyingData = this.kUnderlyingRealFolder;
this._underlyingFolders = [this.displayedFolder];
}
FolderNotificationHelper.stalkFolders(this._underlyingFolders,
this.displayedFolder,
this);
// - retrieve mail view configuration
if (this.listener.shouldUseMailViews) {
// if there is a view tag (basically ":tagname"), then it's a
// mailview tag. clearly.
let mailViewTag = dbFolderInfo.getCharProperty(
MailViewConstants.kViewCurrentTag);
// "0" and "1" are all and unread views, respectively, from 2.0
if (mailViewTag && mailViewTag != "0" && mailViewTag != "1") {
// the tag gets stored with a ":" on the front, presumably done
// as a means of name-spacing that was never subsequently leveraged.
if (mailViewTag.startsWith(":"))
mailViewTag = mailViewTag.substr(1);
// (the true is so we don't persist)
this.setMailView(MailViewConstants.kViewItemTags, mailViewTag, true);
}
// otherwise it's just an index. we kinda-sorta migrate from old-school
// $label tags, except someone reused one of the indices for
// kViewItemNotDeleted, which means that $label2 can no longer be
// migrated.
else {
let mailViewIndex = dbFolderInfo.getUint32Property(
MailViewConstants.kViewCurrent,
MailViewConstants.kViewItemAll);
// label migration per above
if ((mailViewIndex == MailViewConstants.kViewItemTags) ||
((MailViewConstants.kViewItemTags + 2 <= mailViewIndex) &&
(mailViewIndex < MailViewConstants.kViewItemVirtual)))
this.setMailView(MailViewConstants.kViewItemTags,
"$label" + (mailViewIndex-1));
else
this.setMailView(mailViewIndex);
}
}
this.listener.onLoadingFolder(dbFolderInfo);
},
/**
* Creates a view appropriate to the current settings of the folder display
* widget, returning it. The caller is responsible to assign the result to
* this.dbView (or whatever it wants to do with it.)
*/
_createView: function DBViewWrapper__createView() {
let dbviewContractId = "@mozilla.org/messenger/msgdbview;1?type=";
// we will have saved these off when closing our view
let viewFlags = this.__viewFlags || 0;
// real folders are subject to the most interest set of possibilities...
if (this._underlyingData == this.kUnderlyingRealFolder) {
// quick-search inherits from threaded which inherits from group, so this
// is right to choose it first.
if (this.search.hasSearchTerms)
dbviewContractId += "quicksearch";
else if (this.showGroupedBySort)
dbviewContractId += "group";
else if (this.specialViewThreadsWithUnread)
dbviewContractId += "threadswithunread";
else if (this.specialViewWatchedThreadsWithUnread)
dbviewContractId += "watchedthreadswithunread";
else
dbviewContractId += "threaded";
}
// if we're dealing with virtual folders, the answer is always an xfvf
else if (this._underlyingData == this.kUnderlyingMultipleFolder) {
dbviewContractId += "xfvf";
}
else { // kUnderlyingSynthetic or kUnderlyingSearchView
dbviewContractId += "search";
}
// and now zero the saved-off flags.
this.__viewFlags = null;
let dbView = Cc[dbviewContractId]
.createInstance(Ci.nsIMsgDBView);
dbView.init(this.listener.messenger, this.listener.msgWindow,
this.listener.threadPaneCommandUpdater);
// use the least-specific sort so we can clock them back through to build up
// the correct sort order...
let [sortType, sortOrder, sortCustomCol] =
this._getSortDetails(this._sort.length-1);
let outCount = {};
// when the underlying folder is a single real folder (virtual or no), we
// tell the view about the underlying folder.
if (this.isSingleFolder) {
// If the folder is virtual, m_viewFolder needs to be set before the
// folder is opened, otherwise persisted sort info will not be restored
// from the right dbFolderInfo. The use case is for a single folder
// backed saved search. Currently, sort etc. changes in quick filter are
// persisted (gloda list and quick filter in gloda list are not involved).
if (this.isVirtual)
dbView.viewFolder = this.displayedFolder;
// Open the folder.
dbView.open(this._underlyingFolders[0], sortType, sortOrder, viewFlags,
outCount);
// If there are any search terms, we need to tell the db view about the
// the display (/virtual) folder so it can store all the view-specific
// data there (things like the active mail view and such that go in
// dbFolderInfo.) This also goes for cases where the quick search is
// active; the C++ code explicitly nulls out the view folder for no
// good/documented reason, so we need to set it again if we want changes
// made with the quick filter applied. (We don't just change the C++
// code because there could be SeaMonkey fallout.) See bug 502767 for
// info about the quick-search part of the problem.
if (this.search.hasSearchTerms)
dbView.viewFolder = this.displayedFolder;
}
// when we're dealing with a multi-folder virtual folder, we just tell the
// db view about the display folder. (It gets its own XFVF view, so it
// knows what to do.)
// and for a synthetic folder, displayedFolder is null anyways
else {
dbView.open(this.displayedFolder, sortType, sortOrder, viewFlags,
outCount);
}
if (sortCustomCol)
dbView.curCustomColumn = sortCustomCol;
// we all know it's a tree view, make sure the interface is available
// so no one else has to do this.
dbView.QueryInterface(Ci.nsITreeView);
// clock through the rest of the sorts, if there are any
for (let iSort = this._sort.length - 2; iSort >=0; iSort--) {
[sortType, sortOrder, sortCustomCol] = this._getSortDetails(iSort);
if (sortCustomCol)
dbView.curCustomColumn = sortCustomCol;
dbView.sort(sortType, sortOrder);
}
return dbView;
},
/**
* Callback method invoked by FolderNotificationHelper when our folder is
* loaded. Assuming we are still interested in the folder, we enter the
* folder via _enterFolder.
*/
_folderLoaded: function DBViewWrapper__folderLoaded(aFolder) {
if (aFolder == this.displayedFolder) {
this.folderLoading = false;
// If _underlyingFolders is null, DBViewWrapper_open probably got
// an exception trying to open the db, but after reparsing the local
// folder, we should have a db, so set up the view based on info
// from the db.
if (this._underlyingFolders == null) {
this._prepareToLoadView(aFolder.msgDatabase, aFolder);
}
this._enterFolder();
this.listener.onMessagesLoaded(true);
}
},
/**
* Enter this.displayedFolder if we have not yet entered it.
*
* Things we do on entering a folder:
* - clear the folder's biffState!
* - set the message database's header cache size
*/
_enterFolder: function DBViewWrapper__enterFolder() {
if (this._enteredFolder)
return;
this.displayedFolder.biffState =
Ci.nsIMsgFolder.nsMsgBiffState_NoMail;
// we definitely want a view at this point; force the view.
this._viewUpdateDepth = 0;
this._applyViewChanges();
this.listener.onDisplayingFolder();
this._enteredFolder = true;
},
/**
* Renames, moves to the trash, it's all crazy. We have to update all our
* references when this happens.
*/
_folderMoved: function DBViewWrapper__folderMoved(aOldFolder, aNewFolder) {
if (aOldFolder == this.displayedFolder)
this.displayedFolder = aNewFolder;
// indexOf doesn't work for this (reliably)
for (let [i, underlyingFolder] of this._underlyingFolders.entries()) {
if (aOldFolder == underlyingFolder) {
this._underlyingFolders[i] = aNewFolder;
break;
}
}
// re-populate the view.
this._applyViewChanges();
},
/**
* FolderNotificationHelper tells us when folders we care about are deleted
* (because we asked it to in |open|). If it was the folder we were
* displaying (real or virtual), this closes it. If we are virtual and
* backed by a single folder, this closes us. If we are backed by multiple
* folders, we just update ourselves. (Currently, cross-folder views are
* not clever enough to purge the mooted messages, so we need to do this to
* help them out.)
* We do not update virtual folder definitions as a result of deletion; we are
* a display abstraction. That (hopefully) happens elsewhere.
*/
_folderDeleted: function DBViewWrapper__folderDeleted(aFolder) {
// XXX When we empty the trash, we're actually sending a folder deleted
// notification around. This check ensures we don't think we've really
// deleted the trash folder in the DBViewWrapper, and that stops nasty
// things happening, like forgetting we've got the trash folder selected.
if (aFolder.isSpecialFolder(nsMsgFolderFlags.Trash, false))
return;
if (aFolder == this.displayedFolder) {
this.close();
return;
}
// indexOf doesn't work for this (reliably)
for (let [i, underlyingFolder] of this._underlyingFolders.entries()) {
if (aFolder == underlyingFolder) {
this._underlyingFolders.splice(i,1);
break;
}
}
if (this._underlyingFolders.length == 0) {
this.close();
return;
}
// if we are virtual, this will update the search session which draws its
// search scopes from this._underlyingFolders anyways.
this._applyViewChanges();
},
/**
* Compacting a local folder nukes its message keys, requiring the view to be
* rebuilt. If the folder is IMAP, it doesn't matter because the UIDs are
* the message keys and we can ignore it. In the local case we want to
* notify our listener so they have a chance to save the selected messages.
*/
_aboutToCompactFolder: function DBViewWrapper__aboutToCompactFolder(aFolder) {
// IMAP compaction does not affect us unless we are holding headers
if (aFolder.server.type == "imap")
return;
// we will have to re-create the view, so nuke the view now.
if (this.dbView) {
this.listener.onDestroyingView(true);
this.search.dissociateView(this.dbView);
this.dbView.close();
this.dbView = null;
}
},
/**
* Compaction is all done, let's re-create the view! (Unless the folder is
* IMAP, in which case we are ignoring this event sequence.)
*/
_compactedFolder: function DBViewWrapper__compactedFolder(aFolder) {
// IMAP compaction does not affect us unless we are holding headers
if (aFolder.server.type == "imap")
return;
this.refresh();
},
/**
* DB Views need help to know when their move / deletion operations complete.
* This happens in both single-folder and multiple-folder backed searches.
* In the latter case, there is potential danger that we tell a view that did
* not initiate the move / deletion but has kicked off its own about the
* completion and confuse it. However, that's on the view code.
*/
_deleteCompleted: function DBViewWrapper__deleteCompleted(aFolder) {
if (this.dbView)
this.dbView.onDeleteCompleted(true);
this.listener.onMessagesRemoved();
},
/**
* See _deleteCompleted for an explanation of what is going on.
*/
_deleteFailed: function DBViewWrapper__deleteFailed(aFolder) {
if (this.dbView)
this.dbView.onDeleteCompleted(false);
this.listener.onMessageRemovalFailed();
},
_forceOpen: function DBViewWrapper__forceOpen(aFolder) {
this.displayedFolder = null;
this.open(aFolder);
},
_renameCompleted: function DBViewWrapper__renameCompleted(aFolder) {
if (aFolder == this.displayedFolder)
this._forceOpen(aFolder);
},
/**
* If the displayed folder had its total message count or total unread message
* count change, notify the listener. (Note: only for the display folder;
* not the underlying folders!)
*/
_messageCountsChanged: function DBViewWrapper__messageCountsChanged(aFolder) {
if (aFolder == this.displayedFolder)
this.listener.onMessageCountsChanged();
},
/**
* @return the current set of viewFlags. This may be:
* - A modified set of flags that are pending application because a view
* update is in effect and we don't want to modify the view when it's just
* going to get destroyed.
* - The live set of flags from the current dbView.
* - The 'limbo' set of flags because we currently lack a view but will have
* one soon (and then we will apply the flags).
*/
get _viewFlags() {
if (this.__viewFlags != null)
return this.__viewFlags;
if (this.dbView)
return this.dbView.viewFlags;
return 0;
},
/**
* Update the view flags to use on the view. If we are in a view update or
* currently don't have a view, we save the view flags for later usage when
* the view gets (re)built. If we have a view, depending on what's happening
* we may re-create the view or just set the bits. The rules/reasons are:
* - XFVF views can handle the flag changes, just set the flags.
* - XFVF threaded/unthreaded change must re-sort, the backend forgot.
* - Single-folder virtual folders (quicksearch) can handle viewFlag changes,
* to/from grouped included, so set it.
* - Single-folder threaded/unthreaded can handle a change to/from unthreaded/
* threaded, so set it.
* - Single-folder can _not_ handle a change between grouped and not-grouped,
* so re-generate the view. Also it can't handle a change involving
* kUnreadOnly or kShowIgnored.
*/
set _viewFlags(aViewFlags) {
if (this._viewUpdateDepth || !this.dbView) {
this.__viewFlags = aViewFlags;
return;
}
// For viewFlag changes, do not make a random selection if there is not
// actually anything selected; some views do this (looking at xfvf).
if (this.dbView.selection && this.dbView.selection.count == 0)
this.dbView.selection.currentIndex = -1;
let setViewFlags = true;
let reSort = false;
let oldFlags = this.dbView.viewFlags;
let changedFlags = oldFlags ^ aViewFlags;
if (this.isVirtual) {
if (this.isMultiFolder &&
(changedFlags & nsMsgViewFlagsType.kThreadedDisplay &&
!(changedFlags & nsMsgViewFlagsType.kGroupBySort)))
reSort = true;
if (this.isSingleFolder)
// ugh, and the single folder case needs us to re-apply his sort...
reSort = true;
}
else {
// The regular single folder case.
if (changedFlags & (nsMsgViewFlagsType.kGroupBySort |
nsMsgViewFlagsType.kUnreadOnly |
nsMsgViewFlagsType.kShowIgnored)) {
setViewFlags = false;
}
// ugh, and the single folder case needs us to re-apply his sort...
reSort = true;
}
if (setViewFlags) {
this.dbView.viewFlags = aViewFlags;
if (reSort)
this.dbView.sort(this.dbView.sortType, this.dbView.sortOrder);
this.listener.onSortChanged();
}
else {
this.__viewFlags = aViewFlags;
this._applyViewChanges();
}
},
/**
* Apply accumulated changes to the view. If we are in a batch, we do
* nothing, relying on endDisplayUpdate to call us.
*/
_applyViewChanges: function DBViewWrapper__applyViewChanges() {
// if we are in a batch, wait for endDisplayUpdate to be called to get us
// out to zero.
if (this._viewUpdateDepth)
return;
// make the dbView stop being a search listener if it is one
if (this.dbView) {
// save the view's flags if it has any and we haven't already overridden
// them.
if (this.__viewFlags == null)
this.__viewFlags = this.dbView.viewFlags;
this.listener.onDestroyingView(true); // we will re-create it!
this.search.dissociateView(this.dbView);
this.dbView.close();
this.dbView = null;
}
this.dbView = this._createView();
// if the synthetic view defines columns, add those for it
if (this.isSynthetic) {
for (let [, customCol] in Iterator(this._syntheticView.customColumns)) {
customCol.bindToView(this.dbView);
this.dbView.addColumnHandler(customCol.id, customCol);
}
}
this.listener.onCreatedView();
// this ends up being a no-op if there are no search terms
this.search.associateView(this.dbView);
// If we are searching, then the search will generate the all messages
// loaded notification. Although in some cases the search may have
// completed by now, that is not a guarantee. The search logic is
// time-slicing, which is why this can vary. (If it uses up its time
// slices, it will re-schedule itself, returning to us before completing.)
// Which is why we always defer to the search if one is active.
// If we are loading the folder, the load completion will also notify us,
// so we should not generate all messages loaded right now.
if (!this.searching && !this.folderLoading)
this.listener.onMessagesLoaded(true);
else if (this.dbView.numMsgsInView > 0)
this.listener.onMessagesLoaded(false);
},
get isMailFolder() {
return Boolean(this.displayedFolder &&
(this.displayedFolder.flags & nsMsgFolderFlags.Mail));
},
get isNewsFolder() {
return Boolean(this.displayedFolder &&
(this.displayedFolder.flags & nsMsgFolderFlags.Newsgroup));
},
get isFeedFolder() {
return Boolean(this.displayedFolder &&
this.displayedFolder.server.type == "rss");
},
OUTGOING_FOLDER_FLAGS: nsMsgFolderFlags.SentMail |
nsMsgFolderFlags.Drafts |
nsMsgFolderFlags.Queue |
nsMsgFolderFlags.Templates,
/**
* @return true if the folder is an outgoing folder by virtue of being a
* sent mail folder, drafts folder, queue folder, or template folder,
* or being a sub-folder of one of those types of folders.
*/
get isOutgoingFolder() {
return this.displayedFolder &&
this.displayedFolder.isSpecialFolder(this.OUTGOING_FOLDER_FLAGS,
true);
},
/**
* @return true if the folder is not known to be a special outgoing folder
* or the descendent of a special outgoing folder.
*/
get isIncomingFolder() {
return !this.isOutgoingFolder;
},
get isVirtual() {
return Boolean(this.displayedFolder &&
(this.displayedFolder.flags & nsMsgFolderFlags.Virtual));
},
/**
* Prevent view updates from running until a paired |endViewUpdate| call is
* made. This is an advisory method intended to aid us in performing
* redundant view re-computations and does not forbid us from building the
* view earlier if we have a good reason.
* Since calling endViewUpdate will compel a view update when the update
* depth reaches 0, you should only call this method if you are sure that
* you will need the view to be re-built. If you are doing things like
* changing to/from threaded mode that do not cause the view to be rebuilt,
* you should just set those attributes directly.
*/
beginViewUpdate: function DBViewWrapper_beginViewUpdate() {
this._viewUpdateDepth++;
},
/**
* Conclude a paired call to |beginViewUpdate|. Assuming the view depth has
* reached 0 with this call, the view will be re-created with the current
* settings.
*/
endViewUpdate: function DBViewWrapper_endViewUpdate(aForceLevel) {
if (--this._viewUpdateDepth == 0)
this._applyViewChanges();
// Avoid pathological situations.
if (this._viewUpdateDepth < 0)
this._viewUpdateDepth = 0;
},
/**
* @return the primary sort type (as one of the numeric constants from
* nsMsgViewSortType).
*/
get primarySortType() {
return this._sort[0][0];
},
/**
* @return the primary sort order (as one of the numeric constants from
* nsMsgViewSortOrder.)
*/
get primarySortOrder() {
return this._sort[0][1];
},
/**
* @return true if the dominant sort is ascending.
*/
get isSortedAscending() {
return this._sort.length &&
this._sort[0][1] == nsMsgViewSortOrder.ascending;
},
/**
* @return true if the dominant sort is descending.
*/
get isSortedDescending() {
return this._sort.length &&
this._sort[0][1] == nsMsgViewSortOrder.descending;
},
/**
* Indicate if we are sorting by time or something correlated with time.
*
* @return true if the dominant sort is by time.
*/
get sortImpliesTemporalOrdering() {
if (!this._sort.length)
return false;
let sortType = this._sort[0][0];
return sortType == nsMsgViewSortType.byDate ||
sortType == nsMsgViewSortType.byReceived ||
sortType == nsMsgViewSortType.byId ||
sortType == nsMsgViewSortType.byThread;
},
sortAscending: function() {
if (!this.isSortedAscending)
this.magicSort(this._sort[0][0], nsMsgViewSortOrder.ascending);
},
sortDescending: function() {
if (!this.isSortedDescending)
this.magicSort(this._sort[0][0], nsMsgViewSortOrder.descending);
},
/**
* Explicit sort command. We ignore all previous sort state and only apply
* what you tell us. If you want implied secondary sort, use |magicSort|.
* You must use this sort command, and never directly call the sort commands
* on the underlying db view! If you do not, make sure to fight us every
* step of the way, because we will keep clobbering your manually applied
* sort.
* For secondary and multiple custom column support, a byCustom aSortType and
* aSecondaryType must be the column name string.
*/
sort: function DBViewWrapper_sort(aSortType, aSortOrder,
aSecondaryType, aSecondaryOrder) {
// For sort changes, do not make a random selection if there is not
// actually anything selected; some views do this (looking at xfvf).
if (this.dbView.selection && this.dbView.selection.count == 0)
this.dbView.selection.currentIndex = -1;
this._sort = [[aSortType, aSortOrder]];
if (aSecondaryType != null && aSecondaryOrder != null)
this._sort.push([aSecondaryType, aSecondaryOrder]);
// make sure the sort won't make the view angry...
this._ensureValidSort();
// if we are not in a view update, invoke the sort.
if ((this._viewUpdateDepth == 0) && this.dbView) {
for (let iSort = this._sort.length - 1; iSort >=0; iSort--) {
// apply them in the reverse order
let [sortType, sortOrder, sortCustomCol] = this._getSortDetails(iSort);
if (sortCustomCol)
this.dbView.curCustomColumn = sortCustomCol;
this.dbView.sort(sortType, sortOrder);
}
// (only generate the event since we're not in a update batch)
this.listener.onSortChanged();
}
// (if we are in a view update, then a new view will be created when the
// update ends, and it will just use the new sort order anyways.)
},
/**
* Logic that compensates for custom column identifiers being provided as
* sort types.
*
* @return [sort type, sort order, sort custom column name]
*/
_getSortDetails: function(aIndex) {
let [sortType, sortOrder] = this._sort[aIndex];
let sortCustomColumn = null;
let sortTypeType = typeof(sortType);
if (sortTypeType != "number") {
sortCustomColumn = (sortTypeType == "string") ? sortType : sortType.id;
sortType = nsMsgViewSortType.byCustom;
}
return [sortType, sortOrder, sortCustomColumn];
},
/**
* Accumulates implied secondary sorts based on multiple calls to this method.
* This is intended to be hooked up to be controlled by the UI.
* Because we are lazy, we actually just poke the view's sort method and save
* the apparent secondary sort. This also allows perfect compliance with the
* way this used to be implemented!
* For secondary and multiple custom column support, a byCustom aSortType must
* be the column name string.
*/
magicSort: function DBViewWrapper_magicSort(aSortType, aSortOrder) {
if (this.dbView) {
// For sort changes, do not make a random selection if there is not
// actually anything selected; some views do this (looking at xfvf).
if (this.dbView.selection && this.dbView.selection.count == 0)
this.dbView.selection.currentIndex = -1;
// so, the thing we just set obviously will be there
this._sort = [[aSortType, aSortOrder]];
// (make sure it is valid...)
this._ensureValidSort();
// get sort details, handle custom column as string sortType
let [sortType, sortOrder, sortCustomCol] = this._getSortDetails(0);
if (sortCustomCol)
this.dbView.curCustomColumn = sortCustomCol;
// apply the sort to see what happens secondary-wise
this.dbView.sort(sortType, sortOrder);
// there is only a secondary sort if it's not none and not the same.
if (this.dbView.secondarySortType != nsMsgViewSortType.byNone &&
(this.dbView.secondarySortType != sortType ||
(this.dbView.secondarySortType == nsMsgViewSortType.byCustom &&
this.dbView.secondaryCustomColumn != sortCustomCol)))
this._sort.push([this.dbView.secondaryCustomColumn || this.dbView.secondarySortType,
this.dbView.secondarySortOrder]);
// only tell our listener if we're not in a view update batch
if (this._viewUpdateDepth == 0)
this.listener.onSortChanged();
}
},
/**
* Make sure the current sort is valid under our other constraints, make it
* safe if it is not. Most specifically, some sorts are illegal when
* grouping by sort, and we reset the sort to date in those cases.
*
* @param aViewFlags Optional set of view flags to consider instead of the
* potentially live view flags.
*/
_ensureValidSort: function DBViewWrapper_ensureValidSort(aViewFlags) {
if ((aViewFlags != null ? aViewFlags : this._viewFlags) &
nsMsgViewFlagsType.kGroupBySort) {
// We cannot be sorting by thread, id, none, or size. If we are, switch
// to sorting by date.
for (let sortPair of this._sort) {
let sortType = sortPair[0];
if (sortType == nsMsgViewSortType.byThread ||
sortType == nsMsgViewSortType.byId ||
sortType == nsMsgViewSortType.byNone ||
sortType == nsMsgViewSortType.bySize) {
this._sort = [[nsMsgViewSortType.byDate, this._sort[0][1]]];
break;
}
}
}
},
/**
* @return true if we are grouped-by-sort, false if not. If we are not
* grouped by sort, then we are either threaded or unthreaded; check
* the showThreaded property to find out which of those it is.
*/
get showGroupedBySort() {
return Boolean(this._viewFlags & nsMsgViewFlagsType.kGroupBySort);
},
/**
* Enable grouped-by-sort which is mutually exclusive with threaded display
* (as controlled/exposed by showThreaded). Grouped-by-sort is not legal
* for sorts by thread/id/size/none and enabling this will cause us to change
* our sort to by date in those cases.
*/
set showGroupedBySort(aShowGroupBySort) {
if (this.showGroupedBySort != aShowGroupBySort) {
if (aShowGroupBySort) {
// For virtual single folders, the kExpandAll flag must be set.
// Do not apply the flag change until we have made the sort safe.
let viewFlags = this._viewFlags |
nsMsgViewFlagsType.kGroupBySort |
nsMsgViewFlagsType.kExpandAll |
nsMsgViewFlagsType.kThreadedDisplay;
this._ensureValidSort(viewFlags);
this._viewFlags = viewFlags;
}
// maybe we shouldn't do anything in this case?
else
this._viewFlags &= ~(nsMsgViewFlagsType.kGroupBySort |
nsMsgViewFlagsType.kThreadedDisplay);
}
},
/**
* Are we showing ignored/killed threads?
*/
get showIgnored() {
return Boolean(this._viewFlags & nsMsgViewFlagsType.kShowIgnored);
},
/**
* Set whether we are showing ignored/killed threads.
*/
set showIgnored(aShowIgnored) {
if (this.showIgnored == aShowIgnored)
return;
if (aShowIgnored)
this._viewFlags |= nsMsgViewFlagsType.kShowIgnored;
else
this._viewFlags &= ~nsMsgViewFlagsType.kShowIgnored;
},
/**
* @return true if we are in threaded display (as opposed to grouped or
* unthreaded.)
*/
get showThreaded() {
return (this._viewFlags & nsMsgViewFlagsType.kThreadedDisplay) &&
!(this._viewFlags & nsMsgViewFlagsType.kGroupBySort);
},
/**
* Set us to threaded display mode when set to true. If we are already in
* threaded display mode, we do nothing. If you want to set us to unthreaded
* mode, set |showUnthreaded| to true. (Because we have three modes of
* operation: unthreaded, threaded, and grouped-by-sort, we are a tri-state
* and setting us to false is ambiguous. We should probably be using a
* single attribute with three constants...)
*/
set showThreaded(aShowThreaded) {
if (this.showThreaded != aShowThreaded) {
let viewFlags = this._viewFlags;
if (aShowThreaded)
viewFlags |= nsMsgViewFlagsType.kThreadedDisplay;
// maybe we shouldn't do anything in this case?
else
viewFlags &= ~nsMsgViewFlagsType.kThreadedDisplay;
// lose the group bit...
viewFlags &= ~nsMsgViewFlagsType.kGroupBySort;
this._viewFlags = viewFlags;
}
},
/**
* @return true if we are in unthreaded mode (which means not threaded and
* not grouped by sort).
*/
get showUnthreaded() {
return Boolean(!(this._viewFlags & (nsMsgViewFlagsType.kGroupBySort |
nsMsgViewFlagsType.kThreadedDisplay)));
},
/**
* Set to true to put us in unthreaded mode (which means not threaded and
* not grouped by sort).
*/
set showUnthreaded(aShowUnthreaded) {
if (this.showUnthreaded != aShowUnthreaded) {
if (aShowUnthreaded)
this._viewFlags &= ~(nsMsgViewFlagsType.kGroupBySort |
nsMsgViewFlagsType.kThreadedDisplay);
// maybe we shouldn't do anything in this case?
else
this._viewFlags = (this._viewFlags & ~nsMsgViewFlagsType.kGroupBySort) |
nsMsgViewFlagsType.kThreadedDisplay;
}
},
/**
* @return true if we are showing only unread messages.
*/
get showUnreadOnly() {
return Boolean(this._viewFlags & nsMsgViewFlagsType.kUnreadOnly);
},
/**
* Enable/disable showing only unread messages using the view's flag-based
* mechanism. This functionality can also be approximated using a mail
* view (or other search) for unread messages. There also exist special
* views for showing messages with unread threads which is different and
* has serious limitations because of its nature.
* Setting anything to this value clears any active special view because the
* actual UI use case (the "View... Threads..." menu) uses this setter
* intentionally as a mutually exclusive UI choice from the special views.
*/
set showUnreadOnly(aShowUnreadOnly) {
if (this._specialView || (this.showUnreadOnly != aShowUnreadOnly)) {
let viewRebuildRequired = (this._specialView != null);
this._specialView = null;
if (viewRebuildRequired)
this.beginViewUpdate();
if (aShowUnreadOnly)
this._viewFlags |= nsMsgViewFlagsType.kUnreadOnly;
else
this._viewFlags &= ~nsMsgViewFlagsType.kUnreadOnly;
if (viewRebuildRequired)
this.endViewUpdate();
}
},
/**
* Read-only attribute indicating if a 'special view' is in use. There are
* two special views in existence, both of which are concerned about
* showing you threads that have any unread messages in them. They are views
* rather than search predicates because the search mechanism is not capable
* of expressing such a thing. (Or at least it didn't use to be? We might
* be able to whip something up these days...)
*/
get specialView() {
return this._specialView != null;
},
/**
* Private helper for use by the specialView* setters that handles the common
* logic. We don't want this method to be public because we want it to be
* feasible for the view hierarchy and its enumerations to go away without
* code outside this class having to care so much.
*/
_setSpecialView: function DBViewWrapper__setSpecialView(aViewEnum) {
// special views simply cannot work for virtual folders. explode.
if (this.isVirtual)
throw new Exception("Virtual folders cannot use special views!");
this.beginViewUpdate();
// all special views imply a threaded view
this.showThreaded = true;
this._specialView = aViewEnum;
// We clear the search for paranoia/correctness reasons. However, the UI
// layer is currently responsible for making sure these are already zeroed
// out.
this.search.clear();
this.endViewUpdate();
},
/**
* @return true if the special view that shows threads with unread messages
* in them is active.
*/
get specialViewThreadsWithUnread() {
return this._specialView == nsMsgViewType.eShowThreadsWithUnread;
},
/**
* If true is assigned, attempts to enable the special view that shows threads
* with unread messages in them. This will not work on virtual folders
* because of the inheritance hierarchy.
* Any mechanism that requires search terms (quick search, mailviews) will be
* reset/disabled when enabling this view.
*/
set specialViewThreadsWithUnread(aSpecial) {
this._setSpecialView(nsMsgViewType.eShowThreadsWithUnread);
},
/**
* @return true if the special view that shows watched threads with unread
* messages in them is active.
*/
get specialViewWatchedThreadsWithUnread() {
return this._specialView == nsMsgViewType.eShowWatchedThreadsWithUnread;
},
/**
* If true is assigned, attempts to enable the special view that shows watched
* threads with unread messages in them. This will not work on virtual
* folders because of the inheritance hierarchy.
* Any mechanism that requires search terms (quick search, mailviews) will be
* reset/disabled when enabling this view.
*/
set specialViewWatchedThreadsWithUnread(aSpecial) {
this._setSpecialView(nsMsgViewType.eShowWatchedThreadsWithUnread);
},
get mailViewIndex() {
return this._mailViewIndex;
},
get mailViewData() {
return this._mailViewData;
},
/**
* Set the current mail view to the given mail view index with the provided
* data (normally only used for the 'tag' mail views.) We persist the state
* change
*
* @param aMailViewIndex The view to use, one of the kViewItem* constants from
* msgViewPickerOverlay.js OR the name of a custom view. (It's really up
* to MailViewManager.getMailViewByIndex...)
* @param aData Some piece of data appropriate to the mail view, currently
* this is only used for the tag name for kViewItemTags (sans the ":").
* @param aDoNotPersist If true, we don't save this change to the db folder
* info. This is intended for internal use only.
*/
setMailView: function DBViewWrapper_setMailView(aMailViewIndex, aData,
aDoNotPersist) {
let mailViewDef = MailViewManager.getMailViewByIndex(aMailViewIndex);
this._mailViewIndex = aMailViewIndex;
this._mailViewData = aData;
// - update the search terms
// (this triggers a view update if we are not in a batch)
this.search.viewTerms = mailViewDef.makeTerms(this.search.session,
aData);
// - persist the view to the folder.
if (!aDoNotPersist && this.displayedFolder) {
let msgDatabase = this.displayedFolder.msgDatabase;
if (msgDatabase) {
let dbFolderInfo = msgDatabase.dBFolderInfo;
dbFolderInfo.setUint32Property(MailViewConstants.kViewCurrent,
this._mailViewIndex);
// _mailViewData attempts to be sane and be the tag name, as opposed to
// magic-value ":"-prefixed value historically stored on disk. Because
// we want to be forwards and backwards compatible, we put this back on
// when we persist it. It's not like the property is really generic
// anyways.
dbFolderInfo.setCharProperty(
MailViewConstants.kViewCurrentTag,
this._mailViewData ? (":" + this._mailViewData) : "");
}
}
// we don't need to notify the view picker to update because the makeActive
// that cascades out of the view update will do it for us.
},
/**
* @return true if the row at the given index contains a collapsed thread,
* false if the row is a collapsed group or anything else.
*/
isCollapsedThreadAtIndex:
function DBViewWrapper_isCollapsedThreadAtIndex(aViewIndex) {
let flags = this.dbView.getFlagsAt(aViewIndex);
return (flags & nsMsgMessageFlags.Elided) &&
!(flags & MSG_VIEW_FLAG_DUMMY) &&
this.dbView.isContainer(aViewIndex);
},
/**
* @return true if the row at the given index is a grouped view dummy header
* row, false if anything else.
*/
isGroupedByHeaderAtIndex: function(aViewIndex) {
if (aViewIndex < 0 || aViewIndex >= this.dbView.rowCount ||
!this.showGroupedBySort)
return false;
return Boolean(this.dbView.getFlagsAt(aViewIndex) & MSG_VIEW_FLAG_DUMMY);
},
/**
* Perform application-level behaviors related to leaving a folder that have
* nothing to do with our abstraction.
*
* Things we do on leaving a folder:
* - Mark the folder's messages as no longer new
* - Mark all messages read in the folder _if so configured_.
*/
onLeavingFolder: function DBViewWrapper_onLeavingFolder() {
// Suppress useless InvalidateRange calls to the tree by the dbView.
if (this.dbView)
this.dbView.suppressChangeNotifications = true;
this.displayedFolder.clearNewMessages();
this.displayedFolder.hasNewMessages = false;
try {
// For legacy reasons, we support marking all messages as read when we
// leave a folder based on the server type. It's this listener's job
// to do the legwork to figure out if this is desired.
//
// Mark all messages of aFolder as read:
// We can't use the command controller, because it is already tuned in to
// the new folder, so we just mimic its behaviour wrt
// goDoCommand('cmd_markAllRead').
if (this.dbView &&
this.listener.shouldMarkMessagesReadOnLeavingFolder(
this.displayedFolder))
this.dbView.doCommand(Ci.nsMsgViewCommandType.markAllRead);
}
catch(e){/* ignore */}
},
/**
* Returns the view index for this message header in this view.
*
* - If this is a single folder view, we first check whether the folder is the
* right one. If it is, we call the db view's findIndexOfMsgHdr. We do the
* first check because findIndexOfMsgHdr only checks for whether the message
* key matches, which might lead to false positives.
*
* - If this isn't, we trust findIndexOfMsgHdr to do the right thing.
*
* @param aMsgHdr The message header for which the view index should be
* returned.
* @param [aForceFind] 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.
*
* @returns the view index for this header, or nsMsgViewIndex_None if it isn't
* found.
*
* @public
*/
getViewIndexForMsgHdr: function DBViewWrapper_getViewIndexForMsgHdr(aMsgHdr,
aForceFind) {
if (this.dbView) {
if (this.isSingleFolder && aMsgHdr.folder != this.dbView.msgFolder)
return nsMsgViewIndex_None;
let viewIndex = this.dbView.findIndexOfMsgHdr(aMsgHdr, true);
if (aForceFind && viewIndex == nsMsgViewIndex_None) {
// Consider dropping view filters.
// - If we're not displaying all messages, switch to All
if (viewIndex == nsMsgViewIndex_None &&
this.mailViewIndex != MailViewConstants.kViewItemAll) {
this.setMailView(MailViewConstants.kViewItemAll, null);
viewIndex = this.dbView.findIndexOfMsgHdr(aMsgHdr, true);
}
// - Don't just show unread only
if (viewIndex == nsMsgViewIndex_None) {
this.showUnreadOnly = false;
viewIndex = this.dbView.findIndexOfMsgHdr(aMsgHdr, true);
}
}
// We've done all we can.
return viewIndex;
}
// No db view, so we can't do anything
return nsMsgViewIndex_None;
},
/**
* Convenience function to retrieve the first nsIMsgDBHdr in any of the
* folders backing this view with the given message-id header. This
* is for the benefit of FolderDisplayWidget's selection logic.
* When thinking about using this, please keep in mind that, currently, this
* is O(n) for the total number of messages across all the backing folders.
* Since the folder database should already be in memory, this should
* ideally not involve any disk I/O.
* Additionally, duplicate message-ids can and will happen, but since we
* are using the message database's getMsgHdrForMessageID method to be fast,
* our semantics are limited to telling you about only the first one we find.
*
* @param aMessageId The message-id of the message you want.
* @return The first nsIMsgDBHdr found in any of the underlying folders with
* the given message header, null if none are found. The fact that we
* return something does not guarantee that it is actually visible in the
* view. (The search may be filtering it out.)
*/
getMsgHdrForMessageID: function DBViewWrapper_getMsgHdrForMessageID(
aMessageId) {
if (this._syntheticView)
return this._syntheticView.getMsgHdrForMessageID(aMessageId);
if (!this._underlyingFolders)
return null;
for (let folder of this._underlyingFolders) {
let msgHdr = folder.msgDatabase.getMsgHdrForMessageID(aMessageId);
if (msgHdr)
return msgHdr;
}
return null;
},
};