--- a/mail/base/content/SearchDialog.js
+++ b/mail/base/content/SearchDialog.js
@@ -1,10 +1,9 @@
-/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
- * ***** BEGIN LICENSE BLOCK *****
+/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
@@ -17,49 +16,47 @@
*
* The Initial Developer of the Original Code is
* Netscape Communications Corporation.
* Portions created by the Initial Developer are Copyright (C) 1998-1999
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Håkan Waara <hwaara@chello.se>
+ * Andrew Sutherland <asutherland@asutherland.org>
*
* Alternatively, the contents of this file may be used under the terms of
* either of the GNU General Public License Version 2 or later (the "GPL"),
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
-var searchSessionContractID = "@mozilla.org/messenger/searchSession;1";
-var gSearchView;
-var gSearchSession;
var gCurrentFolder;
-var nsIMsgFolder = Components.interfaces.nsIMsgFolder;
+var gFolderDisplay;
+// Although we don't display messages, we have a message display object to
+// simplify our code. It's just always disabled.
+var gMessageDisplay;
+
var nsIMsgWindow = Components.interfaces.nsIMsgWindow;
-var nsIMsgRDFDataSource = Components.interfaces.nsIMsgRDFDataSource;
-var nsMsgSearchScope = Components.interfaces.nsMsgSearchScope;
-var gFolderDatasource;
var gFolderPicker;
var gStatusFeedback;
var gTimelineEnabled = false;
var gMessengerBundle = null;
var RDF;
var gSearchBundle;
-var gNextMessageViewIndexAfterDelete = -2;
// Datasource search listener -- made global as it has to be registered
// and unregistered in different functions.
var gDataSourceSearchListener;
var gViewSearchListener;
var gSearchStopButton;
@@ -84,34 +81,35 @@ var nsSearchResultsController =
},
// this controller only handles commands
// that rely on items being selected in
// the search results pane.
isCommandEnabled: function(command)
{
var enabled = true;
-
- switch (command) {
+
+ switch (command) {
case "goto_folder_button":
if (GetNumSelectedMessages() != 1)
enabled = false;
break;
case "cmd_delete":
case "cmd_shiftDelete":
case "button_delete":
// this assumes that advanced searches don't cross accounts
- if (GetNumSelectedMessages() <= 0 || isNewsURI(gSearchView.getURIForViewIndex(0)))
+ if (GetNumSelectedMessages() <= 0 ||
+ isNewsURI(gFolderDisplay.view.dbView.getURIForViewIndex(0)))
enabled = false;
break;
case "saveas_vf_button":
// need someway to see if there are any search criteria...
return true;
case "cmd_selectAll":
- return GetDBView() != null;
+ return true;
default:
if (GetNumSelectedMessages() <= 0)
enabled = false;
break;
}
return enabled;
},
@@ -137,230 +135,218 @@ var nsSearchResultsController =
case "saveas_vf_button":
saveAsVirtualFolder();
return true;
case "cmd_selectAll":
// move the focus to the search results pane
GetThreadTree().focus();
- GetDBView().doCommand(nsMsgViewCommandType.selectAll)
+ gFolderDisplay.doCommand(nsMsgViewCommandType.selectAll);
return true;
-
+
default:
return false;
}
},
onEvent: function(event)
{
}
}
function UpdateMailSearch(caller)
{
- //dump("XXX update mail-search " + caller + "\n");
document.commandDispatcher.updateCommands('mail-search');
}
+/**
+ * FolderDisplayWidget currently calls this function when the command updater
+ * notification for updateCommandStatus is called. We don't have a toolbar,
+ * but our 'mail-search' command set serves the same purpose.
+ */
+var UpdateMailToolbar = UpdateMailSearch;
+
+/**
+ * No-op clear message pane function for FolderDisplayWidget.
+ */
+function ClearMessagePane() {
+}
function SetAdvancedSearchStatusText(aNumHits)
{
- var statusMsg;
- // if there are no hits, it means no matches were found in the search.
- if (aNumHits == 0)
- statusMsg = gSearchBundle.getString("searchFailureMessage");
- else
- {
- if (aNumHits == 1)
- statusMsg = gSearchBundle.getString("searchSuccessMessage");
- else
- statusMsg = gSearchBundle.getFormattedString("searchSuccessMessages", [aNumHits]);
- }
-
- gStatusFeedback.showStatusString(statusMsg);
}
-// nsIMsgSearchNotify object
-var gSearchNotificationListener =
-{
- onSearchHit: function(header, folder)
- {
- // XXX TODO
- // update status text?
- },
-
- onSearchDone: function(status)
- {
- gSearchStopButton.setAttribute("label", gSearchBundle.getString("labelForSearchButton"));
- gSearchStopButton.setAttribute("accesskey", gSearchBundle.getString("labelForSearchButton.accesskey"));
- gStatusFeedback._stopMeteors();
- SetAdvancedSearchStatusText(gSearchView.QueryInterface(Components.interfaces.nsITreeView).rowCount);
- },
-
- onNewSearch: function()
- {
- gSearchStopButton.setAttribute("label", gSearchBundle.getString("labelForStopButton"));
- gSearchStopButton.setAttribute("accesskey", gSearchBundle.getString("labelForStopButton.accesskey"));
- UpdateMailSearch("new-search");
- gStatusFeedback._startMeteors();
- gStatusFeedback.showStatusString(gSearchBundle.getString("searchingMessage"));
- }
+/**
+ * Subclass the FolderDisplayWidget to deal with UI specific to the search
+ * window.
+ */
+function SearchFolderDisplayWidget(aMessageDisplay) {
+ FolderDisplayWidget.call(this, /* no tab info */ null, aMessageDisplay);
}
-// the folderListener object
-var gFolderListener = {
- OnItemAdded: function(parentItem, item) {},
+SearchFolderDisplayWidget.prototype = {
+ __proto__: FolderDisplayWidget.prototype,
+
+ /// folder display will want to show the thread pane; we need do nothing
+ _showThreadPane: function () {},
- OnItemRemoved: function(parentItem, item){},
+ onSearching: function SearchFolderDisplayWidget_onSearch(aIsSearching) {
+ if (aIsSearching) {
+ // Search button becomes the "stop" button
+ gSearchStopButton.setAttribute(
+ "label", gSearchBundle.getString("labelForStopButton"));
+ gSearchStopButton.setAttribute(
+ "accesskey", gSearchBundle.getString("labelForStopButton.accesskey"));
- OnItemPropertyChanged: function(item, property, oldValue, newValue) {},
-
- OnItemIntPropertyChanged: function(item, property, oldValue, newValue) {},
-
- OnItemBoolPropertyChanged: function(item, property, oldValue, newValue) {},
-
- OnItemUnicharPropertyChanged: function(item, property, oldValue, newValue){},
- OnItemPropertyFlagChanged: function(item, property, oldFlag, newFlag) {},
+ // update our toolbar equivalent
+ UpdateMailSearch("new-search");
+ // spin the meteors
+ gStatusFeedback._startMeteors();
+ // tell the user that we're searching
+ gStatusFeedback.showStatusString(
+ gSearchBundle.getString("searchingMessage"));
+ }
+ else {
+ // Stop button resumes being the "search" button
+ gSearchStopButton.setAttribute(
+ "label", gSearchBundle.getString("labelForSearchButton"));
+ gSearchStopButton.setAttribute(
+ "accesskey", gSearchBundle.getString("labelForSearchButton.accesskey"));
- OnItemEvent: function(folder, event) {
- var eventType = event.toString();
-
- if (eventType == "DeleteOrMoveMsgCompleted") {
- HandleDeleteOrMoveMessageCompleted(folder);
- }
- else if (eventType == "DeleteOrMoveMsgFailed") {
- HandleDeleteOrMoveMessageFailed(folder);
- }
+ // update our toolbar equivalent
+ UpdateMailSearch("done-search");
+ // stop spining the meteors
+ gStatusFeedback._stopMeteors();
+ // set the result test
+ this.updateStatusResultText();
}
-}
+ },
-function HideSearchColumn(id)
-{
- var col = document.getElementById(id);
- if (col) {
- col.setAttribute("hidden","true");
- col.setAttribute("ignoreincolumnpicker","true");
- }
-}
+ /**
+ * If messages were removed, we might have lost some search results and so
+ * should update our search result text. Also, defer to our super-class.
+ */
+ onMessagesRemoved: function SearchFolderDisplayWidget_onMessagesRemoved() {
+ // result text is only for when we are not searching
+ if (!this.view.searching)
+ this.updateStatusResultText();
+ this.__proto__.__proto__.onMessagesRemoved.call(this);
+ },
-function ShowSearchColumn(id)
-{
- var col = document.getElementById(id);
- if (col) {
- col.removeAttribute("hidden");
- col.removeAttribute("ignoreincolumnpicker");
- }
-}
+ updateStatusResultText: function() {
+ let statusMsg, rowCount = this.view.dbView.rowCount;
+ // if there are no hits, it means no matches were found in the search.
+ if (rowCount == 0)
+ statusMsg = gSearchBundle.getString("searchFailureMessage");
+ else if (rowCount == 1)
+ statusMsg = gSearchBundle.getString("searchSuccessMessage");
+ else
+ statusMsg = gSearchBundle.getFormattedString("searchSuccessMessages",
+ [rowCount]);
+
+ gStatusFeedback.showStatusString(statusMsg);
+ },
+};
+
function searchOnLoad()
{
initializeSearchWidgets();
initializeSearchWindowWidgets();
messenger = Components.classes["@mozilla.org/messenger;1"]
.createInstance(Components.interfaces.nsIMessenger);
gSearchBundle = document.getElementById("bundle_search");
gSearchStopButton.setAttribute("label", gSearchBundle.getString("labelForSearchButton"));
gSearchStopButton.setAttribute("accesskey", gSearchBundle.getString("labelForSearchButton.accesskey"));
gMessengerBundle = document.getElementById("bundle_messenger");
- setupDatasource();
- setupSearchListener();
+
+ gMessageDisplay = new NeverVisisbleMessageDisplayWidget();
+ gFolderDisplay = new SearchFolderDisplayWidget(gMessageDisplay);
+ gFolderDisplay.messenger = messenger;
+ gFolderDisplay.msgWindow = msgWindow;
+ gFolderDisplay.tree = document.getElementById("threadTree");
+ gFolderDisplay.treeBox = gFolderDisplay.tree.boxObject.QueryInterface(
+ Components.interfaces.nsITreeBoxObject);
+ gFolderDisplay.view.openSearchView();
+ gFolderDisplay.makeActive();
+
+ gFolderDisplay.setVisibleColumns({subjectCol: true,
+ senderCol: true,
+ dateCol: true,
+ locationCol: true,
+ });
if (window.arguments && window.arguments[0])
selectFolder(window.arguments[0].folder);
+ // trigger searchTermOverlay.js to create the first criterion
onMore(null);
+ // make sure all the buttons are configured
UpdateMailSearch("onload");
-
- // hide and remove these columns from the column picker. you can't thread search results
- HideSearchColumn("threadCol"); // since you can't thread search results
- HideSearchColumn("totalCol"); // since you can't thread search results
- HideSearchColumn("unreadCol"); // since you can't thread search results
- HideSearchColumn("unreadButtonColHeader");
- HideSearchColumn("statusCol");
- HideSearchColumn("flaggedCol");
- HideSearchColumn("idCol");
- HideSearchColumn("junkStatusCol");
- HideSearchColumn("accountCol");
-
- // we want to show the location column for search
- ShowSearchColumn("locationCol");
}
function searchOnUnload()
{
- // unregister listeners
- gSearchSession.unregisterListener(gViewSearchListener);
- gSearchSession.unregisterListener(gSearchNotificationListener);
+ gFolderDisplay.close();
+ top.controllers.removeController(nsSearchResultsController);
- Components.classes["@mozilla.org/messenger/services/session;1"]
- .getService(Components.interfaces.nsIMsgMailSession)
- .RemoveFolderListener(gFolderListener);
-
- if (gSearchView) {
- gSearchView.close();
- gSearchView = null;
- }
-
- top.controllers.removeController(nsSearchResultsController);
-
- // release this early because msgWindow holds a weak reference
- msgWindow.rootDocShell = null;
+ // release this early because msgWindow holds a weak reference
+ msgWindow.rootDocShell = null;
}
function initializeSearchWindowWidgets()
{
gFolderPicker = document.getElementById("searchableFolders");
gSearchStopButton = document.getElementById("search-button");
hideMatchAllItem();
-
+
msgWindow = Components.classes["@mozilla.org/messenger/msgwindow;1"]
.createInstance(nsIMsgWindow);
msgWindow.domWindow = window;
msgWindow.rootDocShell.appType = Components.interfaces.nsIDocShell.APP_TYPE_MAIL;
gStatusFeedback = new nsMsgStatusFeedback();
msgWindow.statusFeedback = gStatusFeedback;
// functionality to enable/disable buttons using nsSearchResultsController
// depending of whether items are selected in the search results thread pane.
top.controllers.insertControllerAt(0, nsSearchResultsController);
}
function onSearchStop() {
- gSearchSession.interruptSearch();
+ gFolderDisplay.view.search.session.interruptSearch();
}
function onResetSearch(event) {
- onReset(event);
-
- var tree = GetThreadTree();
- tree.treeBoxObject.view = null;
- gStatusFeedback.showStatusString("");
+ onReset(event);
+ gFolderDisplay.view.search.clear();
+
+ gStatusFeedback.showStatusString("");
}
-function selectFolder(folder)
+function selectFolder(folder)
{
var folderURI;
// if we can't search messages on this folder, just select the first one
if (!folder || !folder.server.canSearchMessages ||
(folder.flags & Components.interfaces.nsMsgFolderFlags.Virtual)) {
// find first item in our folder picker menu list
folderURI = gFolderPicker.firstChild.tree.builderView.getResourceAtIndex(0).Value;
} else {
folderURI = folder.URI;
}
updateSearchFolderPicker(folderURI);
}
-function updateSearchFolderPicker(folderURI)
-{
+function updateSearchFolderPicker(folderURI)
+{
SetFolderPicker(folderURI, gFolderPicker.id);
// use the URI to get the real folder
gCurrentFolder = GetMsgFolderFromUri(folderURI);
var searchLocalSystem = document.getElementById("checkSearchLocalSystem");
if (searchLocalSystem)
searchLocalSystem.disabled = gCurrentFolder.server.searchScope == nsMsgSearchScope.offlineMail;
@@ -384,80 +370,96 @@ function onChooseFolder(event) {
}
}
function onEnterInSearchTerm()
{
// on enter
// if not searching, start the search
// if searching, stop and then start again
- if (gSearchStopButton.getAttribute("label") == gSearchBundle.getString("labelForSearchButton")) {
- onSearch();
+ if (gSearchStopButton.getAttribute("label") == gSearchBundle.getString("labelForSearchButton")) {
+ onSearch();
}
else {
onSearchStop();
onSearch();
}
}
function onSearch()
{
- // set the view. do this on every search, to
- // allow the tree to reset itself
- var treeView = gSearchView.QueryInterface(Components.interfaces.nsITreeView);
- if (treeView)
- {
- var tree = GetThreadTree();
- tree.treeBoxObject.view = treeView;
- }
-
- gSearchSession.clearScopes();
- // tell the search session what the new scope is
- if (!gCurrentFolder.isServer && !gCurrentFolder.noSelect)
- gSearchSession.addScopeTerm(GetScopeForFolder(gCurrentFolder),
- gCurrentFolder);
+ let viewWrapper = gFolderDisplay.view;
+ let searchTerms = getSearchTerms();
- var searchSubfolders = document.getElementById("checkSearchSubFolders").checked;
- if (gCurrentFolder && (searchSubfolders || gCurrentFolder.isServer || gCurrentFolder.noSelect))
- {
- AddSubFolders(gCurrentFolder);
- }
- // reflect the search widgets back into the search session
- saveSearchTerms(gSearchSession.searchTerms, gSearchSession);
-
- try
- {
- gSearchSession.search(msgWindow);
- }
- catch(ex)
- {
- dump("Search Exception\n");
- }
- // refresh the tree after the search starts, because initiating the
- // search will cause the datasource to clear itself
+ viewWrapper.beginViewUpdate();
+ viewWrapper.search.userTerms = searchTerms.length ? searchTerms : null;
+ viewWrapper.searchFolders = getSearchFolders();
+ viewWrapper.endViewUpdate();
}
-function AddSubFolders(folder) {
+/**
+ * Get the current set of search terms, returning them as a list. We filter out
+ * dangerous and insane predicates.
+ */
+function getSearchTerms() {
+ let termCreator = gFolderDisplay.view.search.session;
+
+ let searchTerms = [];
+ // searchTermOverlay stores wrapper objects in its gSearchTerms array. Pluck
+ // them.
+ for (let iTerm = 0; iTerm < gSearchTerms.length; iTerm++) {
+ let termWrapper = gSearchTerms[iTerm].obj;
+ let realTerm = termCreator.createTerm();
+ termWrapper.saveTo(realTerm);
+ // A header search of "" is illegal for IMAP and will cause us to
+ // explode. You don't want that and I don't want that. So let's check
+ // if the bloody term is a subject search on a blank string, and if it
+ // is, let's secretly not add the term. Everyone wins!
+ if ((realTerm.attrib != Components.interfaces.nsMsgSearchAttrib.Subject) ||
+ (realTerm.value.str != ""))
+ searchTerms.push(realTerm);
+ }
+
+ return searchTerms;
+}
+
+/**
+ * @return the list of folders the search should cover.
+ */
+function getSearchFolders() {
+ let searchFolders = [];
+
+ if (!gCurrentFolder.isServer && !gCurrentFolder.noSelect)
+ searchFolders.push(gCurrentFolder);
+
+ var searchSubfolders =
+ document.getElementById("checkSearchSubFolders").checked;
+ if (gCurrentFolder &&
+ (searchSubfolders || gCurrentFolder.isServer || gCurrentFolder.noSelect))
+ AddSubFolders(gCurrentFolder, searchFolders);
+
+ return searchFolders;
+}
+
+function AddSubFolders(folder, outFolders) {
var subFolders = folder.subFolders;
- while (subFolders.hasMoreElements())
- {
+ while (subFolders.hasMoreElements()) {
var nextFolder =
subFolders.getNext().QueryInterface(Components.interfaces.nsIMsgFolder);
- if (!(nextFolder.flags & Components.interfaces.nsMsgFolderFlags.Virtual))
- {
+ if (!(nextFolder.flags & Components.interfaces.nsMsgFolderFlags.Virtual)) {
if (!nextFolder.noSelect)
- gSearchSession.addScopeTerm(GetScopeForFolder(nextFolder), nextFolder);
+ outFolders.push(nextFolder);
- AddSubFolders(nextFolder);
+ AddSubFolders(nextFolder, outFolders);
}
}
}
-function AddSubFoldersToURI(folder)
+function AddSubFoldersToURI(folder)
{
var returnString = "";
var subFolders = folder.subFolders;
while (subFolders.hasMoreElements())
{
var nextFolder =
@@ -479,17 +481,17 @@ function AddSubFoldersToURI(folder)
returnString += subFoldersString;
}
}
}
return returnString;
}
-function GetScopeForFolder(folder)
+function GetScopeForFolder(folder)
{
var searchLocalSystem = document.getElementById("checkSearchLocalSystem");
return searchLocalSystem && searchLocalSystem.checked ? nsMsgSearchScope.offlineMail : folder.server.searchScope;
}
var nsMsgViewSortType = Components.interfaces.nsMsgViewSortType;
var nsMsgViewSortOrder = Components.interfaces.nsMsgViewSortOrder;
var nsMsgViewFlagsType = Components.interfaces.nsMsgViewFlagsType;
@@ -502,287 +504,89 @@ function goUpdateSearchItems(commandset)
var commandID = commandset.childNodes[i].getAttribute("id");
if (commandID)
{
goUpdateCommand(commandID);
}
}
}
-function nsMsgSearchCommandUpdater()
-{}
-
-nsMsgSearchCommandUpdater.prototype =
-{
- updateCommandStatus : function()
- {
- // the back end is smart and is only telling us to update command status
- // when the # of items in the selection has actually changed.
- document.commandDispatcher.updateCommands('mail-search');
- },
- displayMessageChanged : function(aFolder, aSubject, aKeywords)
- {
- },
-
- updateNextMessageAfterDelete : function()
- {
- SetNextMessageAfterDelete();
- },
-
- summarizeSelection : function() {return false},
-
- QueryInterface : function(iid)
- {
- if (iid.equals(Components.interfaces.nsIMsgDBViewCommandUpdater) ||
- iid.equals(Components.interfaces.nsISupports))
- return this;
-
- throw Components.results.NS_NOINTERFACE;
- }
-}
-
-function setupDatasource() {
- gSearchView = Components.classes["@mozilla.org/messenger/msgdbview;1?type=search"].createInstance(Components.interfaces.nsIMsgDBView);
- var count = new Object;
- var cmdupdator = new nsMsgSearchCommandUpdater();
-
- gSearchView.init(messenger, msgWindow, cmdupdator);
- gSearchView.open(null, nsMsgViewSortType.byId, nsMsgViewSortOrder.ascending, nsMsgViewFlagsType.kNone, count);
-
- // the thread pane needs to use the search datasource (to get the
- // actual list of messages) and the message datasource (to get any
- // attributes about each message)
- gSearchSession = Components.classes[searchSessionContractID].createInstance(Components.interfaces.nsIMsgSearchSession);
-
- var nsIFolderListener = Components.interfaces.nsIFolderListener;
- var notifyFlags = nsIFolderListener.event;
- Components.classes["@mozilla.org/messenger/services/session;1"]
- .getService(Components.interfaces.nsIMsgMailSession)
- .AddFolderListener(gFolderListener, notifyFlags);
-
- // the datasource is a listener on the search results
- gViewSearchListener = gSearchView.QueryInterface(Components.interfaces.nsIMsgSearchNotify);
- gSearchSession.registerListener(gViewSearchListener);
-}
-
-
-function setupSearchListener()
-{
- // Setup the javascript object as a listener on the search results
- gSearchSession.registerListener(gSearchNotificationListener);
-}
-
-// stuff after this is implemented to make the thread pane work
-function GetFolderDatasource()
-{
- if (!gFolderDatasource)
- gFolderDatasource = Components.classes["@mozilla.org/rdf/datasource;1?name=mailnewsfolders"]
- .getService(Components.interfaces.nsIRDFDataSource);
- return gFolderDatasource;
-}
-
-// used to determine if we should try to load a message
-function IsThreadAndMessagePaneSplitterCollapsed()
-{
- return true;
-}
-
// used to toggle functionality for Search/Stop button.
function onSearchButton(event)
{
if (event.target.label == gSearchBundle.getString("labelForSearchButton"))
onSearch();
else
onSearchStop();
}
// threadPane.js will be needing this, too
function GetNumSelectedMessages()
{
- try {
- return gSearchView.numSelected;
- }
- catch (ex) {
- return 0;
- }
-}
-
-function GetDBView()
-{
- return gSearchView;
+ return gFolderDisplay.treeSelection.count;
}
function MsgDeleteSelectedMessages(aCommandType)
{
// we don't delete news messages, we just return in that case
- if (isNewsURI(gSearchView.getURIForViewIndex(0)))
+ if (gFolderDisplay.selectedMessageIsNews)
return;
// if mail messages delete
- SetNextMessageAfterDelete();
- gSearchView.doCommand(aCommandType);
-}
-
-function SetNextMessageAfterDelete()
-{
- gNextMessageViewIndexAfterDelete = gSearchView.msgToSelectAfterDelete;
-}
-
-function HandleDeleteOrMoveMessageFailed(folder)
-{
- gNextMessageViewIndexAfterDelete = -2;
-}
-
-function HandleDeleteOrMoveMessageCompleted(folder)
-{
- var treeView = gSearchView.QueryInterface(Components.interfaces.nsITreeView);
- var treeSelection = treeView.selection;
- var viewSize = treeView.rowCount;
-
- if (gNextMessageViewIndexAfterDelete == -2) {
- // a move or delete can cause our selection can change underneath us.
- // this can happen when the user
- // deletes message from the stand alone msg window
- // or the three pane
- if (!treeSelection) {
- // this can happen if you open the search window
- // and before you do any searches
- // and you do delete from another mail window
- return;
- }
- else if (treeSelection.count == 0) {
- // this can happen if you double clicked a message
- // in the thread pane, and deleted it from the stand alone msg window
- // see bug #185147
- treeSelection.clearSelection();
-
- UpdateMailSearch("delete from another view, 0 rows now selected");
- }
- else if (treeSelection.count == 1) {
- // this can happen if you had two messages selected
- // in the search results pane, and you deleted one of them from another view
- // (like the view in the stand alone msg window or the three pane)
- // since one item is selected, we should load it.
- var startIndex = {};
- var endIndex = {};
- treeSelection.getRangeAt(0, startIndex, endIndex);
-
- // select the selected item, so we'll load it
- treeSelection.select(startIndex.value);
- treeView.selectionChanged();
-
- EnsureRowInThreadTreeIsVisible(startIndex.value);
- UpdateMailSearch("delete from another view, 1 row now selected");
- }
- else {
- // this can happen if you have more than 2 messages selected
- // in the search results pane, and you deleted one of them from another view
- // (like the view in the stand alone msg window or the three pane)
- // since multiple messages are still selected, do nothing.
- }
- }
- else {
- if (gNextMessageViewIndexAfterDelete != nsMsgViewIndex_None && gNextMessageViewIndexAfterDelete >= viewSize)
- {
- if (viewSize > 0)
- gNextMessageViewIndexAfterDelete = viewSize - 1;
- else
- {
- gNextMessageViewIndexAfterDelete = nsMsgViewIndex_None;
-
- // there is nothing to select since viewSize is 0
- treeSelection.clearSelection();
-
- UpdateMailSearch("delete from current view, 0 rows left");
- }
- }
-
- // if we are about to set the selection with a new element then DON'T clear
- // the selection then add the next message to select. This just generates
- // an extra round of command updating notifications that we are trying to
- // optimize away.
- if (gNextMessageViewIndexAfterDelete != nsMsgViewIndex_None)
- {
- treeSelection.select(gNextMessageViewIndexAfterDelete);
- // since gNextMessageViewIndexAfterDelete probably has the same value
- // as the last index we had selected, the tree isn't generating a new
- // selectionChanged notification for the tree view. So we aren't loading the
- // next message. to fix this, force the selection changed update.
- if (treeView)
- treeView.selectionChanged();
-
- EnsureRowInThreadTreeIsVisible(gNextMessageViewIndexAfterDelete);
-
- // XXX TODO
- // I think there is a bug in the suppression code above.
- // what if I have two rows selected, and I hit delete,
- // and so we load the next row.
- // what if I have commands that only enable where
- // exactly one row is selected?
- UpdateMailSearch("delete from current view, at least one row selected");
- }
- }
-
- // default value after delete/move/copy is over
- gNextMessageViewIndexAfterDelete = -2;
-
- // something might have been deleted, so update the status text
- SetAdvancedSearchStatusText(viewSize);
+ gFolderDisplay.hintAboutToDeleteMessages();
+ gFolderDisplay.doCommand(aCommandType);
}
function MoveMessageInSearch(destFolder)
{
- try {
- // get the msg folder we're moving messages into
- // if the id (uri) is not set, use file-uri which is set for
- // "File Here"
- var destUri = destFolder.getAttribute('id');
- if (destUri.length == 0) {
- destUri = destFolder.getAttribute('file-uri')
- }
+ // Get the msg folder we're moving messages into.
+ // If the id (uri) is not set, use file-uri which is set for
+ // "File Here".
+ let destUri = destFolder.getAttribute('id');
+ if (destUri.length == 0)
+ destUri = destFolder.getAttribute('file-uri');
- var destMsgFolder = GetMsgFolderFromUri(destUri).QueryInterface(Components.interfaces.nsIMsgFolder);
+ let destMsgFolder = GetMsgFolderFromUri(destUri).QueryInterface(
+ Components.interfaces.nsIMsgFolder);
- // we don't move news messages, we copy them
- if (isNewsURI(gSearchView.getURIForViewIndex(0))) {
- gSearchView.doCommandWithFolder(nsMsgViewCommandType.copyMessages, destMsgFolder);
- }
- else {
- SetNextMessageAfterDelete();
- gSearchView.doCommandWithFolder(nsMsgViewCommandType.moveMessages, destMsgFolder);
- }
- }
- catch (ex) {
- dump("MsgMoveMessage failed: " + ex + "\n");
- }
+ // we don't move news messages, we copy them
+ if (gFolderDisplay.selectedMessageIsNews) {
+ gFolderDisplay.doCommandWithFolder(nsMsgViewCommandType.copyMessages,
+ destMsgFolder);
+ }
+ else {
+ gFolderDisplay.hintAboutToDeleteMessages();
+ gFolderDisplay.doCommandWithFolder(nsMsgViewCommandType.moveMessages,
+ destMsgFolder);
+ }
}
function GoToFolder()
{
- var hdr = gSearchView.hdrForFirstSelectedMessage;
- MsgOpenNewWindowForFolder(hdr.folder.URI, hdr.messageKey);
+ MsgOpenNewWindowForFolder(gFolderDisplay.selectedMessage);
}
function BeginDragThreadPane(event)
{
// no search pane dnd yet
return false;
}
function saveAsVirtualFolder()
{
- searchFolderURIs = window.arguments[0].folder.URI;
+ var searchFolderURIs = window.arguments[0].folder.URI;
var searchSubfolders = document.getElementById("checkSearchSubFolders").checked;
if (gCurrentFolder && (searchSubfolders || gCurrentFolder.isServer || gCurrentFolder.noSelect))
{
var subFolderURIs = AddSubFoldersToURI(gCurrentFolder);
if (subFolderURIs.length > 0)
searchFolderURIs += '|' + subFolderURIs;
}
var dialog = window.openDialog("chrome://messenger/content/virtualFolderProperties.xul", "",
"chrome,titlebar,modal,centerscreen",
- {folder:window.arguments[0].folder,
- searchTerms:gSearchSession.searchTerms,
+ {folder: window.arguments[0].folder,
+ searchTerms: toXPCOMArray(getSearchTerms(),
+ Components.interfaces.nsISupportsArray),
searchFolderURIs: searchFolderURIs});
}
--- a/mail/base/content/SearchDialog.xul
+++ b/mail/base/content/SearchDialog.xul
@@ -53,16 +53,18 @@
style="width: 52em; height: 34em;"
persist="screenX screenY width height sizemode">
<stringbundle id="bundle_search" src="chrome://messenger/locale/search.properties"/>
<stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
<stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/>
<script type="application/x-javascript" src="chrome://messenger/content/mailWindow.js"/>
+ <script type="application/x-javascript" src="chrome://messenger/content/folderDisplay.js"/>
+ <script type="application/x-javascript" src="chrome://messenger/content/messageDisplay.js"/>
<script type="application/x-javascript" src="chrome://messenger/content/threadPane.js"/>
<script type="application/x-javascript" src="chrome://messenger/content/msgMail3PaneWindow.js"/>
<script type="application/x-javascript" src="chrome://global/content/globalOverlay.js"/>
<script type="application/x-javascript" src="chrome://messenger/content/mailCommands.js"/>
<script type="application/x-javascript" src="chrome://messenger/content/mailWindowOverlay.js"/>
<script type="application/x-javascript" src="chrome://messenger/content/commandglue.js"/>
<script type="application/x-javascript" src="chrome://messenger/content/SearchDialog.js"/>
<script type="application/x-javascript" src="chrome://messenger/content/messengerdnd.js"/>
--- a/mail/base/content/commandglue.js
+++ b/mail/base/content/commandglue.js
@@ -1,485 +1,111 @@
-# -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is Mozilla Communicator client code, released
-# March 31, 1998.
-#
-# The Initial Developer of the Original Code is
-# Netscape Communications Corporation.
-# Portions created by the Initial Developer are Copyright (C) 1998-1999
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-# Jan Varga <varga@nixcorp.com>
-# HÃ¥kan Waara (hwaara@chello.se)
-# David Bienvenu (bienvenu@nventure.com)
-# Jeremy Morton (bugzilla@game-point.net)
-#
-# Alternatively, the contents of this file may be used under the terms of
-# either the GNU General Public License Version 2 or later (the "GPL"), or
-# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-# in which case the provisions of the GPL or the LGPL are applicable instead
-# of those above. If you wish to allow use of your version of this file only
-# under the terms of either the GPL or the LGPL, and not to allow others to
-# use your version of this file under the terms of the MPL, indicate your
-# decision by deleting the provisions above and replace them with the notice
-# and other provisions required by the GPL or the LGPL. If you do not delete
-# the provisions above, a recipient may use your version of this file under
-# the terms of any one of the MPL, the GPL or the LGPL.
-#
-# ***** END LICENSE BLOCK *****
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Communicator client code, released
+ * March 31, 1998.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1998-1999
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Jan Varga <varga@nixcorp.com>
+ * HÃ¥kan Waara (hwaara@chello.se)
+ * David Bienvenu (bienvenu@nventure.com)
+ * Jeremy Morton (bugzilla@game-point.net)
+ * Andrew Sutherland <asutherland@asutherland.org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
/*
* Command-specific code. This stuff should be called by the widgets
*/
Components.utils.import("resource://gre/modules/iteratorUtils.jsm");
//NOTE: gMessengerBundle and gBrandBundle must be defined and set
// for this Overlay to work properly
-var gFolderJustSwitched = false;
-var gVirtualFolderTerms;
-var gXFVirtualFolderTerms;
-var gCurrentVirtualFolderUri;
-var gPrevFolderFlags;
-var gPrevSelectedFolder;
var gMsgFolderSelected;
-function setTitleFromFolder(msgfolder, subject)
-{
- var wintype = document.documentElement.getAttribute('windowtype');
- var title;
-
- // If we are showing the mail:3pane. Never include the subject of the selected
- // message in the title. ("Inbox - My Mail - Mozilla Thunderbird")
- // If we are a stand alone message window, we should show the Subject
- // and the product but not the account name: "Re: New window Title - Mozilla Thunderbird"
-
- if (wintype == "mail:messageWindow")
- title = subject ? subject : "";
- else if (msgfolder)
- {
- title = msgfolder.prettyName;
-
- if (!msgfolder.isServer)
- {
- var server = msgfolder.server;
- var middle;
- var end;
- if (server.type == "nntp") {
- // <folder> on <hostname>
- middle = gMessengerBundle.getString("titleNewsPreHost");
- end = server.hostName;
- }
- else {
- // <folder> - <server.prettyName>
- middle = "-";
- end = server.prettyName;
- }
- if (middle) title += " " + middle;
- if (end) title += " " + end;
- }
- }
-
-#ifndef XP_MACOSX
- title += " - " + gBrandBundle.getString("brandFullName");
-#endif
- document.title = title;
-}
-
function UpdateMailToolbar(caller)
{
//dump("XXX update mail-toolbar " + caller + "\n");
document.commandDispatcher.updateCommands('mail-toolbar');
// hook for extra toolbar items
var observerService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
observerService.notifyObservers(window, "mail:updateToolbarItems", null);
}
-/**
- * @param folder - If viewFolder is a single folder saved
- - search, this folder is the scope of the
- - saved search, the real, underlying folder.
- - Otherwise, it's the same as the viewFolder.
- * @param viewFolder - nsIMsgFolder selected in the folder pane.
- - Will be the same as folder, except if
- - it's a single folder saved search.
- * @param viewType - nsMsgViewType (see nsIMsgDBView.idl)
- * @param viewFlags - nsMsgViewFlagsType (see nsIMsgDBView.idl)
- * @param sortType - nsMsgViewSortType (see nsIMsgDBView.idl)
- * @param sortOrder - nsMsgViewSortOrder (see nsIMsgDBView.idl)
- **/
-function ChangeFolder(folder, viewFolder, viewType, viewFlags, sortType, sortOrder)
-{
- if (folder.URI == gCurrentLoadingFolderURI)
- return;
-
- // hook for extra toolbar items
- var observerService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
- observerService.notifyObservers(window, "mail:setupToolbarItems", folder.URI);
-
- try {
- setTitleFromFolder(viewFolder, null);
- } catch (ex) {
- dump("error setting title: " + ex + "\n");
- }
-
- //if it's a server, clear the threadpane and don't bother trying to load.
- if(folder.isServer)
- {
- msgWindow.openFolder = null;
- ClearThreadPane();
- UpdateStatusQuota(null);
- // Load AccountCentral page here.
- ShowAccountCentral();
- return;
- }
- else
- {
- if (folder.server.displayStartupPage)
- {
- gDisplayStartupPage = true;
- folder.server.displayStartupPage = false;
- }
- }
-
- // If the user clicks on msgfolder, time to display thread pane and message pane.
- ShowThreadPane();
-
- gCurrentLoadingFolderURI = folder.URI;
- gNextMessageAfterDelete = null; // forget what message to select, if any
-
- gCurrentFolderToReroot = folder.URI;
- gCurrentLoadingFolderViewFlags = viewFlags;
- gCurrentLoadingFolderViewType = viewType;
- gCurrentLoadingFolderSortType = sortType;
- gCurrentLoadingFolderSortOrder = sortOrder;
-
- var showMessagesAfterLoading;
- try {
- var server = folder.server;
- if (gPrefBranch.getBoolPref("mail.password_protect_local_cache"))
- {
- showMessagesAfterLoading = server.passwordPromptRequired;
- // servers w/o passwords (like local mail) will always be non-authenticated.
- // So we need to use the account manager for that case.
- }
- else
- showMessagesAfterLoading = false;
- }
- catch (ex) {
- showMessagesAfterLoading = false;
- }
-
- if (viewType != nsMsgViewType.eShowVirtualFolderResults && (folder.manyHeadersToDownload || showMessagesAfterLoading))
- {
- gRerootOnFolderLoad = true;
- try
- {
- ClearThreadPane();
- SetBusyCursor(window, true);
- folder.startFolderLoading();
- folder.updateFolder(msgWindow);
- }
- catch(ex)
- {
- SetBusyCursor(window, false);
- dump("Error loading with many headers to download: " + ex + "\n");
- }
- }
- else
- {
- if (viewType != nsMsgViewType.eShowVirtualFolderResults)
- SetBusyCursor(window, true);
- RerootFolder(folder.URI, folder, viewType, viewFlags, sortType, sortOrder);
- gRerootOnFolderLoad = false;
- folder.startFolderLoading();
-
- //Need to do this after rerooting folder. Otherwise possibility of receiving folder loaded
- //notification before folder has actually changed.
- if (viewType != nsMsgViewType.eShowVirtualFolderResults)
- folder.updateFolder(msgWindow);
- }
-}
-
function isNewsURI(uri)
{
if (!uri || uri[0] != 'n') {
return false;
}
else {
return ((uri.substring(0,6) == "news:/") || (uri.substring(0,14) == "news-message:/"));
}
}
-function UpdateColumnsForView(folder, viewType)
-{
- const nsMsgFolderFlags = Components.interfaces.nsMsgFolderFlags;
- // if this is the drafts, sent, or send later folder,
- // we show "Recipient" instead of "Author"
- SetSentFolderColumns(IsSpecialFolder(folder, nsMsgFolderFlags.SentMail | nsMsgFolderFlags.Drafts | nsMsgFolderFlags.Queue, true));
- ShowLocationColumn(viewType == nsMsgViewType.eShowVirtualFolderResults);
- // Only show 'Received' column for e-mails. For newsgroup messages, the 'Date' header is as reliable as an e-mail's
- // 'Received' header, as it is replaced with the news server's (more reliable) date.
- UpdateReceivedColumn(folder);
-}
-
-function RerootFolder(uri, newFolder, viewType, viewFlags, sortType, sortOrder)
-{
- viewDebug("In reroot folder, sortType = " + sortType + "viewType = " + viewType + "\n");
-
- if (sortType == 0)
- {
- try
- {
- var dbFolderInfo = newFolder.msgDatabase.dBFolderInfo;
- sortType = dbFolderInfo.sortType;
- sortOrder = dbFolderInfo.sortOrder;
- viewFlags = dbFolderInfo.viewFlags;
- viewType = dbFolderInfo.viewType;
- dbFolderInfo = null;
- }
- catch(ex)
- {
- dump("invalid db in RerootFolder: " + ex + "\n");
- }
- }
-
- // workaround for #39655
- gFolderJustSwitched = true;
-
- ClearThreadPaneSelection();
-
- //Clear the new messages of the old folder
- var oldFolder = gPrevSelectedFolder;
- if (oldFolder) {
- oldFolder.clearNewMessages();
- oldFolder.hasNewMessages = false;
- }
-
- //Set the window's new open folder.
- msgWindow.openFolder = newFolder;
-
- //the new folder being selected should have its biff state get cleared.
- if(newFolder)
- {
- newFolder.biffState =
- Components.interfaces.nsIMsgFolder.nsMsgBiffState_NoMail;
- }
-
- //Clear out the thread pane so that we can sort it with the new sort id without taking any time.
- // folder.setAttribute('ref', "");
-
- // null this out, so we don't try sort.
- if (gDBView) {
- gDBView.close();
- gDBView = null;
- }
-
- // cancel the pending mark as read timer
- ClearPendingReadTimer();
-
- UpdateColumnsForView(newFolder, viewType);
- // now create the db view, which will sort it.
- CreateDBView(newFolder, viewType, viewFlags, sortType, sortOrder);
-
- if (oldFolder)
- {
- /* we don't null out the db reference for inbox because inbox is like the "main" folder
- and performance outweighs footprint*/
- if (!IsSpecialFolder(oldFolder, Components.interfaces.nsMsgFolderFlags.Inbox, false))
- {
- if (oldFolder.URI != newFolder.URI)
- oldFolder.msgDatabase = null;
- }
- }
- // that should have initialized gDBView, now re-root the thread pane
- RerootThreadPane();
- SetUpToolbarButtons(uri);
- UpdateStatusMessageCounts(gMsgFolderSelected);
-
- // hook for extra toolbar items
- var observerService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
- observerService.notifyObservers(window, "mail:updateToolbarItems", null);
- // this is to kick off cross-folder searches for virtual folders.
- if (gSearchSession && !gVirtualFolderTerms) // another var might be better...
- {
- viewDebug("doing a xf folder search in rerootFolder\n");
- gCurrentLoadingFolderURI = ""
- ViewChangeByFolder(newFolder);
- gPreQuickSearchView = null; // don't remember the cross folder search
- ScrollToMessageAfterFolderLoad(newFolder);
- }
-}
-
function SwitchView(command)
{
// when switching thread views, we might be coming out of quick search
// or a message view.
// first set view picker to all
- if (gCurrentViewValue != kViewItemAll)
- ViewChangeByValue(kViewItemAll);
+ if (gFolderDisplay.view.mailViewIndex != kViewItemAll)
+ gFolderDisplay.view.setMailView(kViewItemAll);
- // clear the QS text, if we need to
- ClearQSIfNecessary();
-
- var oldSortType, oldSortOrder, viewFlags, viewType, db;
- // now switch views
- if (gDBView)
- {
- oldSortType = gDBView.sortType;
- oldSortOrder = gDBView.sortOrder;
- viewFlags = gDBView.viewFlags;
- viewType = gDBView.viewType;
- db = gDBView.db;
- gDBView.close();
- gDBView = null;
- }
- else
- {
- oldSortType = nsMsgViewSortType.byThread;
- oldSortOrder = nsMsgViewSortOrder.ascending;
- viewFlags = gCurViewFlags;
- viewType = nsMsgViewType.eShowAllThreads;
- db = null;
- }
switch(command)
{
// "All" threads and "Unread" threads don't change threading state
case "cmd_viewAllMsgs":
- viewType = nsMsgViewType.eShowAllThreads;
- viewFlags = viewFlags & ~nsMsgViewFlagsType.kUnreadOnly;
+ gFolderDisplay.view.showUnreadOnly = false;
break;
case "cmd_viewUnreadMsgs":
- viewType = nsMsgViewType.eShowAllThreads;
- viewFlags = viewFlags | nsMsgViewFlagsType.kUnreadOnly;
+ gFolderDisplay.view.showUnreadOnly = true;
break;
// "Threads with Unread" and "Watched Threads with Unread" force threading
case "cmd_viewWatchedThreadsWithUnread":
- viewType = nsMsgViewType.eShowWatchedThreadsWithUnread;
- viewFlags |= nsMsgViewFlagsType.kThreadedDisplay;
+ gFolderDisplay.view.specialViewWatchedThreadsWithUnread = true;
break;
case "cmd_viewThreadsWithUnread":
- viewType = nsMsgViewType.eShowThreadsWithUnread;
- viewFlags |= nsMsgViewFlagsType.kThreadedDisplay;
+ gFolderDisplay.view.specialViewThreadsWithUnread = true;
break;
// "Ignored Threads" toggles 'ignored' inclusion --
// but it also resets 'With Unread' views to 'All'
case "cmd_viewIgnoredThreads":
- viewType = nsMsgViewType.eShowAllThreads;
- if (viewFlags & nsMsgViewFlagsType.kShowIgnored)
- viewFlags = viewFlags & ~nsMsgViewFlagsType.kShowIgnored;
- else
- viewFlags = viewFlags | nsMsgViewFlagsType.kShowIgnored;
+ gFolderDisplay.view.showIgnored = !gFolderDisplay.view.showIgnored;
break;
}
-
- if (db && viewType == nsMsgViewType.eShowVirtualFolderResults)
- {
- db.dBFolderInfo.viewFlags = viewFlags;
- gMsgFolderSelected = null;
- msgWindow.openFolder = null;
- FolderPaneSelectionChange();
- LoadCurrentlyDisplayedMessage();
- }
- else
- {
- CreateDBView(msgWindow.openFolder, viewType, viewFlags, oldSortType,
- oldSortOrder);
- RerootThreadPane();
- }
-}
-
-function SetSentFolderColumns(isSentFolder)
-{
- var tree = GetThreadTree();
-
- var lastFolderSent = tree.getAttribute("lastfoldersent") == "true";
- if (isSentFolder != lastFolderSent)
- {
- var senderColumn = document.getElementById("senderCol");
- var recipientColumn = document.getElementById("recipientCol");
-
- var saveHidden = senderColumn.getAttribute("hidden");
- senderColumn.setAttribute("hidden", senderColumn.getAttribute("swappedhidden"));
- senderColumn.setAttribute("swappedhidden", saveHidden);
-
- saveHidden = recipientColumn.getAttribute("hidden");
- recipientColumn.setAttribute("hidden", recipientColumn.getAttribute("swappedhidden"));
- recipientColumn.setAttribute("swappedhidden", saveHidden);
- }
-
- if(isSentFolder)
- tree.setAttribute("lastfoldersent", "true");
- else
- tree.setAttribute("lastfoldersent", "false");
-}
-
-function ShowLocationColumn(show)
-{
- var col = document.getElementById("locationCol");
- if (col) {
- if (show) {
- col.removeAttribute("hidden");
- col.removeAttribute("ignoreincolumnpicker");
- }
- else {
- col.setAttribute("hidden","true");
- col.setAttribute("ignoreincolumnpicker","true");
- }
- }
-}
-
-function UpdateReceivedColumn(newFolder)
-{
- // Only show 'Received' column for e-mails. For newsgroup messages, the 'Date' header is as reliable as an e-mail's
- // 'Received' header, as it is replaced with the news server's (more reliable) date.
- var receivedColumn = document.getElementById("receivedCol");
-
- const nsMsgFolderFlags = Components.interfaces.nsMsgFolderFlags;
- var newFolderShowsRcvd = (newFolder.flags & nsMsgFolderFlags.Mail) &&
- !(newFolder.flags & (nsMsgFolderFlags.Queue | nsMsgFolderFlags.Drafts |
- nsMsgFolderFlags.Templates |
- nsMsgFolderFlags.SentMail));
-
- var tempHidden = receivedColumn.getAttribute("temphidden") == "true";
- var isHidden = receivedColumn.getAttribute("hidden") == "true";
-
- if (!newFolderShowsRcvd && !isHidden)
- {
- // Record state & hide
- receivedColumn.setAttribute("temphidden", "true");
- receivedColumn.setAttribute("hidden", "true");
- }
- else if (newFolderShowsRcvd && tempHidden && isHidden)
- {
- receivedColumn.setAttribute("hidden", "false");
- }
-
- if (newFolderShowsRcvd)
- {
- receivedColumn.removeAttribute("ignoreincolumnpicker");
- receivedColumn.removeAttribute("temphidden");
- }
- else
- receivedColumn.setAttribute("ignoreincolumnpicker", "true");
}
function SetNewsFolderColumns()
{
var sizeColumn = document.getElementById("sizeCol");
if (gDBView.usingLines) {
sizeColumn.setAttribute("label",gMessengerBundle.getString("linesColumnHeader"));
@@ -494,17 +120,17 @@ function UpdateStatusMessageCounts(folde
var unreadElement = GetUnreadCountElement();
var totalElement = GetTotalCountElement();
if(folder && unreadElement && totalElement)
{
var numSelected = GetNumSelectedMessages();
var numUnread = (numSelected > 1) ?
gMessengerBundle.getFormattedString("selectedMsgStatus",
- [numSelected]) :
+ [numSelected]) :
gMessengerBundle.getFormattedString("unreadMsgStatus",
[ folder.getNumUnread(false)]);
var numTotal =
gMessengerBundle.getFormattedString("totalMsgStatus",
[folder.getTotalMessages(false)]);
unreadElement.setAttribute("label", numUnread);
totalElement.setAttribute("label", numTotal);
@@ -637,19 +263,18 @@ function ConvertSortTypeToColumnID(sortK
case nsMsgViewSortType.byAttachments:
columnID = "attachmentCol";
break;
case nsMsgViewSortType.byCustom:
//TODO: either change try() catch to if (property exists) or restore the getColumnHandler() check
try //getColumnHandler throws an errror when the ID is not handled
{
- columnID = gDBView.db.dBFolderInfo.getProperty('customSortCol');
+ columnID = gDBView.curCustomColumn;
}
-
catch (err) { //error - means no handler
dump("ConvertSortTypeToColumnID: custom sort key but no handler for column '" + columnID + "'\n");
columnID = "dateCol";
}
break;
default:
dump("unsupported sort key: " + sortKey + "\n");
@@ -665,314 +290,86 @@ var nsMsgViewFlagsType = Components.inte
var nsMsgViewCommandType = Components.interfaces.nsMsgViewCommandType;
var nsMsgViewType = Components.interfaces.nsMsgViewType;
var nsMsgNavigationType = Components.interfaces.nsMsgNavigationType;
var gDBView = null;
var gCurViewFlags;
var gCurSortType;
-// CreateDBView is called when we have a thread pane. CreateBareDBView is called when there is no
-// tree associated with the view. CreateDBView will call into CreateBareDBView...
-
-function CreateBareDBView(originalView, msgFolder, viewType, viewFlags, sortType, sortOrder)
-{
- var dbviewContractId = "@mozilla.org/messenger/msgdbview;1?type=";
- // hack to turn this into an integer, if it was a string
- // it would be a string if it came from localStore.rdf
- viewType = viewType - 0;
-
- switch (viewType) {
- case nsMsgViewType.eShowQuickSearchResults:
- dbviewContractId += "quicksearch";
- break;
- case nsMsgViewType.eShowThreadsWithUnread:
- dbviewContractId += "threadswithunread";
- break;
- case nsMsgViewType.eShowWatchedThreadsWithUnread:
- dbviewContractId += "watchedthreadswithunread";
- break;
- case nsMsgViewType.eShowVirtualFolderResults:
- dbviewContractId += "xfvf";
- break;
- case nsMsgViewType.eShowSearch:
- dbviewContractId += "search";
- break;
- case nsMsgViewType.eShowAllThreads:
- default:
- if (sortType == nsMsgViewSortType.byThread || sortType == nsMsgViewSortType.byId
- || sortType == nsMsgViewSortType.byNone)
- viewFlags &= ~nsMsgViewFlagsType.kGroupBySort;
-
- if (viewFlags & nsMsgViewFlagsType.kGroupBySort)
- dbviewContractId += "group";
- else
- dbviewContractId += "threaded";
- break;
- }
-
- // dump ("contract id = " + dbviewContractId + "original view = " + originalView + "\n");
- if (!originalView)
- gDBView = Components.classes[dbviewContractId].createInstance(Components.interfaces.nsIMsgDBView);
-
- gCurViewFlags = viewFlags;
- var count = new Object;
- if (!gThreadPaneCommandUpdater)
- gThreadPaneCommandUpdater = new nsMsgDBViewCommandUpdater();
-
- gCurSortType = sortType;
-
- if (!originalView) {
- gDBView.init(messenger, msgWindow, gThreadPaneCommandUpdater);
- gDBView.open(msgFolder, gCurSortType, sortOrder, viewFlags, count);
- if (viewType == nsMsgViewType.eShowVirtualFolderResults)
- {
- // the view is a listener on the search results
- gViewSearchListener = gDBView.QueryInterface(Components.interfaces.nsIMsgSearchNotify);
- gSearchSession.registerListener(gViewSearchListener);
- }
- }
- else {
- gDBView = originalView.cloneDBView(messenger, msgWindow, gThreadPaneCommandUpdater);
- }
-}
-
-function CreateDBView(msgFolder, viewType, viewFlags, sortType, sortOrder)
-{
- // call the inner create method
- CreateBareDBView(null, msgFolder, viewType, viewFlags, sortType, sortOrder);
-
- // now do tree specific work
-
- // based on the collapsed state of the thread pane/message pane splitter,
- // suppress message display if appropriate.
- gDBView.suppressMsgDisplay = IsMessagePaneCollapsed();
-
- UpdateSortIndicators(gCurSortType, sortOrder);
- var ObserverService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
- ObserverService.notifyObservers(msgFolder, "MsgCreateDBView", viewType + ":" + viewFlags);
-}
function ChangeMessagePaneVisibility(now_hidden)
{
// we also have to hide the File/Attachments menuitem
var node = document.getElementById("fileAttachmentMenu");
if (node)
node.hidden = now_hidden;
- if (gDBView) {
- // the collapsed state is the state after we released the mouse
- // so we take it as it is
- gDBView.suppressMsgDisplay = now_hidden;
- }
+ gMessageDisplay.visible = !now_hidden;
+
var event = document.createEvent('Events');
if (now_hidden) {
event.initEvent('messagepane-hide', false, true);
}
else {
event.initEvent('messagepane-unhide', false, true);
}
document.getElementById("messengerWindow").dispatchEvent(event);
}
function OnMouseUpThreadAndMessagePaneSplitter()
{
- // the collapsed state is the state after we released the mouse
- // so we take it as it is
+ // The collapsed state is the state after we released the mouse,
+ // so we take it as it is.
ChangeMessagePaneVisibility(IsMessagePaneCollapsed());
}
+/**
+ * Our multiplexed tabbing model ends up sending synthetic folder pane
+ * selection change notifications. We want to ignore these because the
+ * user may explicitly re-select a folder intentionally, and we want to
+ * be able to know that. So we filter out the synthetics here.
+ * The tabbing logic sets this global to help us out.
+ */
+var gIgnoreSyntheticFolderPaneSelectionChange = false;
function FolderPaneSelectionChange()
{
- var folderSelection = gFolderTreeView.selection;
-
- // This prevents a folder from being loaded in the case that the user
- // has right-clicked on a folder different from the one that was
- // originally highlighted. On a right-click, the highlight (selection)
- // of a row will be different from the value of currentIndex, thus if
- // the currentIndex is not selected, it means the user right-clicked
- // and we don't want to load the contents of the folder.
- if (!folderSelection.isSelected(folderSelection.currentIndex))
- return;
-
- gVirtualFolderTerms = null;
- gXFVirtualFolderTerms = null;
-
- var folders = GetSelectedMsgFolders();
- if (folders.length == 1)
- {
- var msgFolder = folders[0];
- var uriToLoad = msgFolder.URI;
+ if (gIgnoreSyntheticFolderPaneSelectionChange) {
+ gIgnoreSyntheticFolderPaneSelectionChange = false;
+ return;
+ }
- if (msgFolder == gMsgFolderSelected)
- return;
- // If msgFolder turns out to be a single folder saved search, a virtual folder,
- // realFolder will get set to the underlying folder the
- // saved search is based on.
- var realFolder = msgFolder;
- gPrevSelectedFolder = gMsgFolderSelected;
- gMsgFolderSelected = msgFolder;
- var folderFlags = msgFolder.flags;
- // If this is same folder, and we're not showing a virtual folder
- // then do nothing.
- const nsMsgFolderFlags = Components.interfaces.nsMsgFolderFlags;
- if (msgFolder == msgWindow.openFolder &&
- !(folderFlags & nsMsgFolderFlags.Virtual) &&
- !(gPrevFolderFlags & nsMsgFolderFlags.Virtual))
- {
- return;
- }
- else
- {
- const outFolderFlagMask = nsMsgFolderFlags.SentMail |
- nsMsgFolderFlags.Drafts | nsMsgFolderFlags.Queue |
- nsMsgFolderFlags.Templates;
- if (IsSpecialFolder(gMsgFolderSelected, outFolderFlagMask, true))
- {
- if (!gPrevSelectedFolder ||
- !IsSpecialFolder(gPrevSelectedFolder, outFolderFlagMask, true))
- onSearchFolderTypeChanged(true);
- }
- else
- {
- if (!gPrevSelectedFolder ||
- IsSpecialFolder(gPrevSelectedFolder, outFolderFlagMask, true))
- onSearchFolderTypeChanged(false);
- }
+ let folderSelection = gFolderTreeView.selection;
- OnLeavingFolder(gPrevSelectedFolder); // mark all read in last folder
- var sortType = 0;
- var sortOrder = 0;
- var viewFlags = 0;
- var viewType = 0;
- gDefaultSearchViewTerms = null;
- gVirtualFolderTerms = null;
- gXFVirtualFolderTerms = null;
- gPrevFolderFlags = folderFlags;
- gCurrentVirtualFolderUri = null;
- // don't get the db if this folder is a server
- // we're going to be display account central
- if (!(msgFolder.isServer))
- {
- try
- {
- var msgDatabase = msgFolder.msgDatabase;
- if (msgDatabase)
- {
- var dbFolderInfo = msgDatabase.dBFolderInfo;
- sortType = dbFolderInfo.sortType;
- sortOrder = dbFolderInfo.sortOrder;
- viewFlags = dbFolderInfo.viewFlags;
- if (folderFlags & Components.interfaces.nsMsgFolderFlags.Virtual)
- {
- viewType = nsMsgViewType.eShowQuickSearchResults;
- var searchTermString = dbFolderInfo.getCharProperty("searchStr");
- var searchOnline = dbFolderInfo.getBooleanProperty("searchOnline", false);
- // trick the view code into updating the real folder...
- gCurrentVirtualFolderUri = uriToLoad;
- viewDebug("uriToLoad = " + uriToLoad + "\n");
- var srchFolderUri = dbFolderInfo.getCharProperty("searchFolderUri");
- var srchFolderUriArray = srchFolderUri.split('|');
- // cross folder search
- var filterService = Components.classes["@mozilla.org/messenger/services/filters;1"].getService(Components.interfaces.nsIMsgFilterService);
- var filterList = filterService.getTempFilterList(msgFolder);
- var tempFilter = filterList.createFilter("temp");
- filterList.parseCondition(tempFilter, searchTermString);
- if (srchFolderUriArray.length > 1)
- {
- viewType = nsMsgViewType.eShowVirtualFolderResults;
- gXFVirtualFolderTerms = CreateGroupedSearchTerms(tempFilter.searchTerms);
- setupXFVirtualFolderSearch(srchFolderUriArray, gXFVirtualFolderTerms, searchOnline);
- // need to set things up so that reroot folder issues the search
- }
- else
- {
- gSearchSession = null;
- uriToLoad = srchFolderUri;
- // we need to load the db for the actual folder so that many hdrs to download
- // will return false...
- realFolder = GetMsgFolderFromUri(uriToLoad);
- msgDatabase = realFolder.msgDatabase;
- gVirtualFolderTerms = CreateGroupedSearchTerms(tempFilter.searchTerms);
- }
- }
- else
- {
- gSearchSession = null;
- viewFlags = dbFolderInfo.viewFlags;
- viewType = dbFolderInfo.viewType;
- }
- msgDatabase = null;
- dbFolderInfo = null;
- }
- }
- catch (ex)
- {
- dump("failed to get view & sort values. ex = " + ex +"\n");
- }
- }
- if (gDBView && gDBView.viewType == nsMsgViewType.eShowQuickSearchResults)
- {
- if (gPreQuickSearchView) //close cached view before quick search
- {
- gPreQuickSearchView.close();
- gPreQuickSearchView = null;
- }
- ClearQSIfNecessary();
- }
- ClearMessagePane();
+ // This prevents a folder from being loaded in the case that the user
+ // has right-clicked on a folder different from the one that was
+ // originally highlighted. On a right-click, the highlight (selection)
+ // of a row will be different from the value of currentIndex, thus if
+ // the currentIndex is not selected, it means the user right-clicked
+ // and we don't want to load the contents of the folder.
+ if (!folderSelection.isSelected(folderSelection.currentIndex))
+ return;
- if (gXFVirtualFolderTerms)
- viewType = nsMsgViewType.eShowVirtualFolderResults;
- else if (gSearchEmailAddress || gVirtualFolderTerms)
- viewType = nsMsgViewType.eShowQuickSearchResults;
- else if (viewType == nsMsgViewType.eShowQuickSearchResults)
- viewType = nsMsgViewType.eShowAllThreads; //override viewType - we don't want to start w/ quick search
- ChangeFolder(realFolder, msgFolder, viewType, viewFlags, sortType, sortOrder);
- if (gVirtualFolderTerms)
- gDBView.viewFolder = msgFolder;
- }
- document.getElementById('tabmail').setTabTitle(null);
- }
- else
- {
- msgWindow.openFolder = null;
- ClearThreadPane();
- }
-
- var startpageenabled = gPrefBranch.getBoolPref("mailnews.start_page.enabled");
- if (gDisplayStartupPage && startpageenabled)
- {
- loadStartPage();
- gDisplayStartupPage = false;
- }
- UpdateMailToolbar("FolderPaneSelectionChange");
-}
-
-function ClearThreadPane()
-{
- if (gDBView) {
- gDBView.close();
- gDBView = null;
- }
+ let folders = GetSelectedMsgFolders();
+ gFolderDisplay.show(folders.length ? folders[0] : null);
}
function IsSpecialFolder(msgFolder, flags, checkAncestors)
{
- if (!msgFolder)
+ if (!msgFolder)
return false;
else if ((msgFolder.flags & flags) == 0)
{
var parentMsgFolder = msgFolder.parentMsgFolder;
return (parentMsgFolder && checkAncestors) ? IsSpecialFolder(parentMsgFolder, flags, true) : false;
}
else {
// the user can set their INBOX to be their SENT folder.
- // in that case, we want this folder to act like an INBOX,
+ // in that case, we want this folder to act like an INBOX,
// and not a SENT folder
const nsMsgFolderFlags = Components.interfaces.nsMsgFolderFlags;
return !((flags & nsMsgFolderFlags.SentMail) &&
(msgFolder.flags & nsMsgFolderFlags.Inbox));
}
}
function Undo()
@@ -980,166 +377,10 @@ function Undo()
messenger.undo(msgWindow);
}
function Redo()
{
messenger.redo(msgWindow);
}
-function getSearchTermString(searchTerms)
-{
- var searchIndex;
- var condition = "";
- var count = searchTerms.Count();
- for (searchIndex = 0; searchIndex < count; )
- {
- var term = searchTerms.QueryElementAt(searchIndex++, Components.interfaces.nsIMsgSearchTerm);
-
- if (condition.length > 1)
- condition += ' ';
-
- if (term.matchAll)
- {
- condition = "ALL";
- break;
- }
- condition += (term.booleanAnd) ? "AND (" : "OR (";
- condition += term.termAsString + ')';
- }
- return condition;
-}
-
-function CreateVirtualFolder(newName, parentFolder, searchFolderURIs, searchTerms, searchOnline)
-{
- // ### need to make sure view/folder doesn't exist.
- if (searchFolderURIs && (searchFolderURIs != "") && newName && (newName != ""))
- {
- try
- {
- var newFolder = parentFolder.addSubfolder(newName);
- newFolder.prettyName = newName;
- newFolder.setFlag(Components.interfaces.nsMsgFolderFlags.Virtual);
- var vfdb = newFolder.msgDatabase;
- var searchTermString = getSearchTermString(searchTerms);
- var dbFolderInfo = vfdb.dBFolderInfo;
- // set the view string as a property of the db folder info
- // set the original folder name as well.
- dbFolderInfo.setCharProperty("searchStr", searchTermString);
- dbFolderInfo.setCharProperty("searchFolderUri", searchFolderURIs);
- dbFolderInfo.setBooleanProperty("searchOnline", searchOnline);
-
- vfdb.summaryValid = true;
- vfdb.Close(true);
- parentFolder.NotifyItemAdded(newFolder);
- var accountManager = Components.classes["@mozilla.org/messenger/account-manager;1"].getService(Components.interfaces.nsIMsgAccountManager);
- accountManager.saveVirtualFolders();
- }
- catch(e)
- {
- throw(e); // so that the dialog does not automatically close
- dump ("Exception : creating virtual folder \n");
- }
- }
- else
- {
- dump("no name or nothing selected\n");
- }
-}
-
-var gSearchSession;
-
-var nsMsgSearchScope = Components.interfaces.nsMsgSearchScope;
-
var gMessengerBundle = null;
-// Datasource search listener -- made global as it has to be registered
-// and unregistered in different functions.
-var gViewSearchListener;
-
-function GetScopeForFolder(folder)
-{
- return folder.server.searchScope;
-}
-
-function setupXFVirtualFolderSearch(folderUrisToSearch, searchTerms, searchOnline)
-{
- const Ci = Components.interfaces;
- var count = new Object;
- var i;
-
- gSearchSession = Components.classes["@mozilla.org/messenger/searchSession;1"]
- .createInstance(Ci.nsIMsgSearchSession);
-
- for (i in folderUrisToSearch)
- {
- var realFolder = GetMsgFolderFromUri(folderUrisToSearch[i]);
- if (!realFolder.isServer)
- gSearchSession.addScopeTerm(!searchOnline ? nsMsgSearchScope.offlineMail : GetScopeForFolder(realFolder), realFolder);
- }
-
- var termsArray = searchTerms.QueryInterface(Components.interfaces.nsISupportsArray);
- const nsIMsgSearchTerm = Components.interfaces.nsIMsgSearchTerm;
- for each (var term in fixIterator(termsArray, nsIMsgSearchTerm)) {
- gSearchSession.appendTerm(term);
- }
-}
-
-function CreateGroupedSearchTerms(searchTermsArray)
-{
- const Ci = Components.interfaces;
- var searchSession = gSearchSession;
- if (!searchSession) {
- searchSession = Components.classes["@mozilla.org/messenger/searchSession;1"]
- .createInstance(Ci.nsIMsgSearchSession);
- }
-
- // create a temporary isupports array to store our search terms
- // since we will be modifying the terms so they work with quick search
- var searchTermsArrayForQS = Components.classes["@mozilla.org/supports-array;1"].createInstance(Components.interfaces.nsISupportsArray);
-
- var numEntries = searchTermsArray.Count();
- for (var i = 0; i < numEntries; i++) {
- var searchTerm = searchTermsArray.GetElementAt(i).QueryInterface(Components.interfaces.nsIMsgSearchTerm);
-
- // clone the term, since we might be modifying it
- var searchTermForQS = searchSession.createTerm();
- searchTermForQS.value = searchTerm.value;
- searchTermForQS.attrib = searchTerm.attrib;
- searchTermForQS.arbitraryHeader = searchTerm.arbitraryHeader
- searchTermForQS.op = searchTerm.op;
-
- // mark the first node as a group
- if (i == 0)
- searchTermForQS.beginsGrouping = true;
- else if (i == numEntries - 1)
- searchTermForQS.endsGrouping = true;
-
- // turn the first term to true to work with quick search...
- searchTermForQS.booleanAnd = i ? searchTerm.booleanAnd : true;
-
- searchTermsArrayForQS.AppendElement(searchTermForQS);
- }
- return searchTermsArrayForQS;
-}
-
-function OnLeavingFolder(aFolder)
-{
- try
- {
- // 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 (gDBView && gPrefBranch.getBoolPref("mailnews.mark_message_read." + aFolder.server.type))
- {
- gDBView.doCommand(nsMsgViewCommandType.markAllRead);
- }
- }
- catch(e){/* ignore */}
-}
-
-var gViewDebug = false;
-
-function viewDebug(str)
-{
- if (gViewDebug)
- dump(str);
-}
--- a/mail/base/content/extraCustomizeItems.xul
+++ b/mail/base/content/extraCustomizeItems.xul
@@ -1,46 +1,48 @@
<?xml version="1.0"?>
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is Mozilla Communicator client code, released
-# March 31, 1998.
-#
-# The Initial Developer of the Original Code is
-# Netscape Communications Corporation.
-# Portions created by the Initial Developer are Copyright (C) 1998-1999
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-# Karsten Düsterloh <mnyromyr@tprac.de>
-# Simon Paquet <bugzilla@babylonsounds.com>
-#
-# Alternatively, the contents of this file may be used under the terms of
-# either the GNU General Public License Version 2 or later (the "GPL"), or
-# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-# in which case the provisions of the GPL or the LGPL are applicable instead
-# of those above. If you wish to allow use of your version of this file only
-# under the terms of either the GPL or the LGPL, and not to allow others to
-# use your version of this file under the terms of the MPL, indicate your
-# and other provisions required by the GPL or the LGPL. If you do not delete
-# the provisions above, a recipient may use your version of this file under
-# the terms of any one of the MPL, the GPL or the LGPL.
-#
-# ***** END LICENSE BLOCK *****
+<!--
+ - ***** BEGIN LICENSE BLOCK *****
+ - Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ -
+ - The contents of this file are subject to the Mozilla Public License Version
+ - 1.1 (the "License"); you may not use this file except in compliance with
+ - the License. You may obtain a copy of the License at
+ - http://www.mozilla.org/MPL/
+ -
+ - Software distributed under the License is distributed on an "AS IS" basis,
+ - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ - for the specific language governing rights and limitations under the
+ - License.
+ -
+ - The Original Code is Mozilla Communicator client code, released
+ - March 31, 1998.
+ -
+ - The Initial Developer of the Original Code is
+ - Netscape Communications Corporation.
+ - Portions created by the Initial Developer are Copyright (C) 1998-1999
+ - the Initial Developer. All Rights Reserved.
+ -
+ - Contributor(s):
+ - Karsten Düsterloh <mnyromyr@tprac.de>
+ - Simon Paquet <bugzilla@babylonsounds.com>
+ -
+ - Alternatively, the contents of this file may be used under the terms of
+ - either the GNU General Public License Version 2 or later (the "GPL"), or
+ - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ - in which case the provisions of the GPL or the LGPL are applicable instead
+ - of those above. If you wish to allow use of your version of this file only
+ - under the terms of either the GPL or the LGPL, and not to allow others to
+ - use your version of this file under the terms of the MPL, indicate your
+ - and other provisions required by the GPL or the LGPL. If you do not delete
+ - the provisions above, a recipient may use your version of this file under
+ - the terms of any one of the MPL, the GPL or the LGPL.
+ -
+ - ***** END LICENSE BLOCK *****
+ -->
<!DOCTYPE overlay [
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
%globalDTD;
<!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd">
%messengerDTD;
<!ENTITY % msgViewPickerDTD SYSTEM "chrome://messenger/locale/msgViewPickerOverlay.dtd" >
%msgViewPickerDTD;
@@ -152,20 +154,26 @@
title="&mailViewsToolbarItem.title;"
align="center"
class="chromeclass-toolbar-additional">
<label id="viewPickerLabel"
value="&viewPicker.label;"
control="viewPicker"
accesskey="&viewPicker.accesskey;"/>
<menulist id="viewPicker" oncommand="ViewChangeByMenuitem(event.target);">
- <menupopup id="viewPickerPopup" onpopupshowing="RefreshViewPopup(this, false);">
- <menuitem id="viewPickerAll" value="0" label="&viewAll.label;"/>
- <menuitem id="viewPickerUnread" value="1" label="&viewUnread.label;"/>
- <menuitem id="viewPickerNotDeleted" value="3" label="&viewNotDeleted.label;"/>
+ <menupopup id="viewPickerPopup" onpopupshowing="RefreshViewPopup(this);">
+ <menuitem id="viewPickerAll" value="0"
+ label="&viewAll.label;"
+ type="radio"/>
+ <menuitem id="viewPickerUnread" value="1"
+ label="&viewUnread.label;"
+ type="radio"/>
+ <menuitem id="viewPickerNotDeleted" value="3"
+ label="&viewNotDeleted.label;"
+ type="radio"/>
<menuseparator id="afterViewPickerUnreadSeparator"/>
<menu id="viewPickerTags" label="&viewTags.label;">
<menupopup id="viewPickerTagsPopup"
class="menulist-menupopup"
onpopupshowing="RefreshTagsPopup(this, true);"/>
</menu>
<menu id="viewPickerCustomViews" label="&viewCustomViews.label;">
<menupopup id="viewPickerCustomViewsPopup"
new file mode 100644
--- /dev/null
+++ b/mail/base/content/folderDisplay.js
@@ -0,0 +1,2073 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Thunderbird Mail Client.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Messaging, Inc.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Andrew Sutherland <asutherland@asutherland.org>
+ * David Bienvenu <bienvenu@nventure.com>
+ * Siddharth Agarwal <sid.bugzilla@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+Components.utils.import("resource://app/modules/dbViewWrapper.js");
+
+var gFolderDisplay = null;
+var gMessageDisplay = null;
+
+var nsMsgFolderFlags = Components.interfaces.nsMsgFolderFlags;
+
+/**
+ * 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
+ * onAllMessagesLoaded.
+ */
+ 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; onAllMessagesLoaded 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 = [];
+
+ let dummyDOMNode = document.getElementById('mail-toolbox');
+ /**
+ * Create a fake tree box object for if/when this folder is in the background.
+ * We need to give it a bogus DOM object to send events to, so we choose the
+ * mail-toolbox, who is hopefully unlikely to take offense.
+ */
+ this._fakeTreeBox = dummyDOMNode ?
+ new FakeTreeBoxObject(dummyDOMNode.boxObject) : null;
+
+ this._mostRecentSelectionCounts = [];
+ this._mostRecentCurrentIndices = [];
+}
+FolderDisplayWidget.prototype = {
+ /**
+ * @return the currently displayed folder. This is just proxied from the
+ * view wrapper.
+ */
+ get displayedFolder() {
+ return this._nonViewFolder || this.view.displayedFolder;
+ },
+
+ /**
+ * @return the nsITreeSelection object for our tree view if there is one,
+ * null otherwise.
+ */
+ get treeSelection() {
+ if (this.view.dbView)
+ return this.view.dbView.selection;
+ else
+ 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.
+ */
+ PERF_HEADER_CACHE_SIZE: 100,
+
+ /**
+ * An optional 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.
+ */
+ _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 = [{messageId: msgHdr.messageId} for each
+ ([, msgHdr] in Iterator(this.selectedMessages))];
+ },
+
+ /**
+ * 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 =
+ [msgHdr for each
+ ([, savedInfo] in Iterator(this._savedSelection)) if
+ ((msgHdr = this.view.getMsgHdrForMessageID(savedInfo.messageId)))];
+
+ this.selectMessages(messages, true);
+ this._savedSelection = null;
+
+ return this.selectedCount != 0;
+ },
+
+ /**
+ * Maps column ids to functions that test whether the column is legal 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 to be legal for display
+ * in all cases.
+ */
+ COLUMN_LEGALITY_TESTERS: {
+ // Only show 'Received' column for e-mails. For newsgroup messages, the
+ // 'Date' header is as reliable as an e-mail's 'Received' header, as it is
+ // replaced with the news server's (more reliable) date.
+ receivedCol: function (viewWrapper) {
+ return viewWrapper.isMailFolder && !viewWrapper.isOutgoingFolder;
+ },
+ // senderCol = From. You only care in incoming folders.
+ senderCol: function (viewWrapper) {
+ return viewWrapper.isIncomingFolder;
+ },
+ // recipient = To. You only care in outgoing folders.
+ recipientCol: function (viewWrapper) {
+ return viewWrapper.isOutgoingFolder;
+ },
+ // Only show the location column for non-single-folder results
+ locationCol: function(viewWrapper) {
+ return !viewWrapper.isSingleFolder;
+ },
+ },
+
+ /**
+ * If we determine that a column is illegal but was displayed, use this
+ * mapping to find suggested legal alternatives. This basically exists
+ * just to flip-flop between senderCol and recipientCol.
+ */
+ COLUMN_LEGAL_ALTERNATIVES: {
+ // swap between sender and recipient
+ senderCol: ["recipientCol"],
+ recipientCol: ["senderCol"],
+ // if we nuke received, put back date...
+ receivedCol: ["dateCol"],
+ },
+
+ /**
+ * Columns to display whenever we can. This is currently a bit of a hack to
+ * always show the location column when relevant. Arguably, it would be
+ * better to use this as a default for a folder you've never been in and
+ * the rest of the time we restore the last column set for that folder from
+ * properties.
+ */
+ COLUMNS_DISPLAY_WHEN_POSSIBLE: ["locationCol"],
+
+ /**
+ * Update the displayed columns so that:
+ * - Only legal columns (per COLUMN_LEGALITY_TESTERS) are displayed.
+ * - Alternatives to now-illegal columns may be displayed.
+ */
+ updateColumns: function() {
+ // Keep a list of columns we might want to make show up if they are not
+ // illegal.
+ let legalize = this.COLUMNS_DISPLAY_WHEN_POSSIBLE.concat();
+
+ // figure out who is illegal and make them go away
+ for (let [colId, legalityFunc] in Iterator(this.COLUMN_LEGALITY_TESTERS)) {
+ let column = document.getElementById(colId);
+ // The search window does not have all the columns we know about, bail in
+ // such cases.
+ if (!column)
+ continue;
+ let legal = legalityFunc(this.view);
+
+ if (!legal) {
+ let isHidden = column.getAttribute("hidden") == "true";
+ // If it wasn't hidden, consider making its alternatives visible in the
+ // next pass.
+ if (!isHidden && (colId in this.COLUMN_LEGAL_ALTERNATIVES))
+ legalize = legalize.concat(this.COLUMN_LEGAL_ALTERNATIVES[colId]);
+ // but definitely hide the heck out of it right now
+ column.setAttribute("hidden", true);
+ column.setAttribute("ignoreincolumnpicker", true);
+ }
+ else {
+ column.removeAttribute("ignoreincolumnpicker");
+ }
+ }
+ // If we have any columns we should consider making visible because they are
+ // alternatives to columns that became illegal, uh, do that.
+ for each (let [, colId] in Iterator(legalize)) {
+ let column = document.getElementById(colId);
+ let isLegal = column.getAttribute("ignoreincolumnpicker") != "true";
+ let isHidden = column.getAttribute("hidden") == "true";
+ if (isLegal && isHidden)
+ column.removeAttribute("hidden");
+ }
+ },
+
+ /**
+ * @param aColumnMap an object where the attribute names are column ids and
+ * the values are a boolean indicating whether the column should be
+ * visible or not. If a column is not in the map, it is assumed that it
+ * should be hidden.
+ */
+ setVisibleColumns: function(aColumnMap) {
+ let cols = document.getElementById("threadCols");
+ let colChildren = cols.children;
+
+ // because the splitter correspondence can be confusing and tricky, let's
+ // build a list of the nodes ordered by their ordinal
+ let ordinalOrdered = [], iKid;
+ for (iKid = 0; iKid < colChildren.length; iKid++)
+ ordinalOrdered.push(null);
+
+ for (iKid = 0; iKid < colChildren.length; iKid++) {
+ let colChild = colChildren[iKid];
+ let ordinal = colChild.getAttribute("ordinal") - 1;
+ ordinalOrdered[ordinal] = colChild;
+ }
+
+ function twiddleAround(index, makeHidden) {
+ if (index + 1 < ordinalOrdered.length) {
+ let nexty = ordinalOrdered[index+1];
+ if (nexty) {
+ let isHidden = nexty.getAttribute("hidden") == "true";
+ if (isHidden != makeHidden) {
+ nexty.setAttribute("hidden", true);
+ return;
+ }
+ }
+ }
+ if (index - 1 > 0) {
+ let prevy = ordinalOrdered[index-1];
+ if (prevy) {
+ let isHidden = prevy.getAttribute("hidden") == "true";
+ if (isHidden != makeHidden) {
+ prevy.setAttribute("hidden", true);
+ return;
+ }
+ }
+ }
+ }
+
+ for (iKid = 0; iKid < ordinalOrdered.length; iKid++) {
+ let colChild = ordinalOrdered[iKid];
+ if (colChild == null)
+ continue;
+
+ if (colChild.tagName == "treecol") {
+ if (colChild.id in aColumnMap) {
+ // only need to do something if currently hidden
+ if (colChild.getAttribute("hidden") == "true") {
+ colChild.removeAttribute("hidden");
+ twiddleAround(iKid, false);
+ }
+ }
+ else {
+ // only need to do something if currently visible
+ if (colChild.getAttribute("hidden") != "true") {
+ colChild.setAttribute("hidden", true);
+ twiddleAround(iKid, true);
+ }
+ }
+ }
+ }
+ },
+
+ _savedColumnStates: null,
+
+ /**
+ * For now, just save the visible columns into a dictionary for use in a
+ * subsequent call to setVisibleColumns. This does not do anything about
+ * re-arranging columns.
+ */
+ saveColumnStates: function() {
+ // 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-computaiton of column relationships every time.
+ // restoreNaturalOrder invalidates the tree when it is done re-ordering; I'm
+ // not sure that's entirely necessary...
+
+ let visibleMap = {};
+
+ let cols = document.getElementById("threadCols");
+ let colChildren = cols.children;
+ for (let iKid = 0; iKid < colChildren.length; iKid++) {
+ let colChild = colChildren[iKid];
+ if (colChild.getAttribute("hidden") != "true")
+ visibleMap[colChild.id] = true;
+ }
+
+ this._savedColumnStates = visibleMap;
+ },
+
+ /**
+ * Restores the visible columns saved by saveColumnStates. Some day, in the
+ * future we might do something about positions and the like. But we don't
+ * currently.
+ */
+ restoreColumnStates: function () {
+ if (this._savedColumnStates) {
+ this.setVisibleColumns(this._savedColumnStates);
+ this._savedColumnStates = null;
+ }
+ },
+
+ showFolderUri: function FolderDisplayWidget_showFolderUri(aFolderURI) {
+ return this.show(GetMsgFolderFromUri(aFolderURI));
+ },
+
+ /**
+ * Invoked by showFolder when it turns out the folder is in fact a server.
+ */
+ _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;
+ },
+
+ /* =============================== */
+ /* ===== IDBViewWrapper Listener ===== */
+ /* =============================== */
+
+ /**
+ * @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 (gPrefBranch.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 gPrefBranch.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);
+ },
+
+ /**
+ * 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) {
+ // getDocumentElements() sets gSearchBundle
+ getDocumentElements();
+ if (this._tabInfo)
+ document.getElementById("tabmail").setTabThinking(
+ this._tabInfo,
+ aIsSearching && gSearchBundle.getString("searchingMessage"));
+ },
+
+ /**
+ * 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 onAllMessagesLoaded nearly immediately if this is a local
+ // folder where view creation is synonymous with having all messages.
+ this._allMessagesLoaded = false;
+ this.messageDisplay.onCreatedView();
+ 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;
+
+ let ObserverService =
+ Components.classes["@mozilla.org/observer-service;1"]
+ .getService(Components.interfaces.nsIObserverService);
+ // 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.)
+ ObserverService.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 (aFolderIsComingBack)
+ this._saveSelection();
+ else
+ this._clearSavedSelection();
+ gDBView = null;
+ }
+
+ // 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);
+ },
+ /**
+ * We are entering the folder for display, set the header cache size.
+ */
+ onDisplayingFolder: function FolderDisplayWidget_onDisplayingFolder() {
+ let msgDatabase = this.view.displayedFolder.msgDatabase;
+ if (msgDatabase) {
+ msgDatabase.resetHdrCacheSize(this.PERF_HEADER_CACHE_SIZE);
+ }
+
+ // the quick-search gets nuked when we show a new folder
+ ClearQSIfNecessary();
+ // update the quick-search relative to whether it's incoming/outgoing
+ onSearchFolderTypeChanged(this.view.isOutgoingFolder);
+
+ 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() {
+ // 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 FolderDisplayWidget_get_allMessagesLoaded() {
+ return this._allMessagesLoaded;
+ },
+
+ /**
+ * 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 virtual folder, this happens when the search completes.
+ *
+ * What we do:
+ * - Any scrolling required!
+ */
+ onAllMessagesLoaded: function() {
+ this._allMessagesLoaded = true;
+ this._notifyWhenActive(this._activeAllMessagesLoaded);
+ },
+ _activeAllMessagesLoaded: function() {
+ // - 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)
+ 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;
+ }
+
+ // - new messages
+ // if configured to scroll to new messages, try that
+ if (gPrefBranch.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 (gPrefBranch.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) {
+ 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) {
+ let event = document.createEvent("datacontainerevents");
+ // you cannot cancel a view change!
+ event.initEvent("MailViewChanged", false, false);
+ //event.setData("folderDisplay", this);
+ window.dispatchEvent(event);
+ }
+ },
+
+ /**
+ * 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);
+ },
+
+ /**
+ * Messages (that may have been displayed) have been removed; this may impact
+ * our message selection. If we saw this coming, then
+ * this._nextViewIndexAfterDelete should know what view index we should
+ * select next. If we didn't see this coming, the cause is likely an
+ * explicit deletion in another tab/window.
+ * Because the nsMsgDBView is on top of things, it will already have called
+ * summarizeSelection as a result of the changes to the message display.
+ * So our job here is really just to try and potentially improve on the
+ * default selection logic.
+ */
+ onMessagesRemoved: function FolderDisplayWidget_onMessagesRemoved() {
+ // - 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;
+ }
+
+ // - surprise!
+ // 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 does'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;
+ },
+
+ /**
+ * Update the status bar to reflect our exciting message counts.
+ */
+ onMessageCountsChanged: function FolderDisplayWidget_onMessageCountsChaned() {
+ if (this.active)
+ UpdateStatusMessageCounts(this.displayedFolder);
+ },
+
+ /* ===== End IDBViewWrapperListener ===== */
+
+ /* ================================== */
+ /* ===== nsIMsgDBViewCommandUpdater ===== */
+ /* ================================== */
+
+ /**
+ * 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() {
+ 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) {
+ UpdateMailToolbar("FolderDisplayWidget displayed message changed");
+ let viewIndex = this.view.dbView.currentlyDisplayedMessage;
+ let msgHdr = (viewIndex != nsMsgViewIndex_None) ?
+ this.view.dbView.getMsgHdrAt(viewIndex) : 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;
+ },
+
+ /**
+ * 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. 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 ===== */
+
+ /**
+ * 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() {
+ // If there is a right click going on, then the possibilities are:
+ // 1) The user right-clicked in the selection. In this case, the selection
+ // is maintained. This holds true for one or multiple messages.
+ // 2) The user right-clicked outside the selection. In this case, the
+ // selection, but not the current index, reflects the single message
+ // the user right-clicked on.
+ // We want to treat case #1 as if a right-click was not involved and we
+ // want to ignore case #2 by bailing because our existing selection (or
+ // lack thereof) we want maintained.
+// if (gRightMouseButtonDown && gRightMouseButtonChangedSelection)
+// return;
+
+ // 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) {
+ this.updateColumns();
+ 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();
+ }
+ },
+
+ /**
+ * 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 (aNotificationFunc) {
+ if (this._active) {
+ aNotificationFunc.call(this);
+ }
+ else {
+ if (this._notificationsPendingActivation.indexOf(aNotificationFunc) == -1)
+ 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 each (let [, notif] in Iterator(pendingNotifications)) {
+ notif.call(this);
+ }
+ },
+
+ 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._active = true;
+ this._runNotificationsPendingActivation();
+
+ // -- UI
+
+ // 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) {
+ // 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)
+ if (this.treeBox) {
+ this.treeBox.view = this.view.dbView;
+ if (this._savedFirstVisibleRow != null)
+ this.treeBox.scrollToRow(this._savedFirstVisibleRow);
+
+ this.restoreColumnStates();
+ }
+
+ // restore the quick search widget
+ let searchInput = document.getElementById("searchInput");
+ if (searchInput && this._savedQuickSearch) {
+ searchInput.searchMode = this._savedQuickSearch.searchMode;
+ if (this._savedQuickSearch.text) {
+ searchInput.value = this._savedQuickSearch.text;
+ searchInput.showingSearchCriteria = false;
+ searchInput.clearButtonHidden = false;
+ }
+ else {
+ searchInput.setSearchCriteriaText();
+ }
+ }
+ }
+
+ // 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: !this._tabInfo.folderPaneCollapsed,
+ message: !this._tabInfo.messagePaneCollapsed});
+
+ // update the columns and such that live inside the thread pane
+ this._updateThreadDisplay();
+
+ this.messageDisplay.makeActive();
+ }
+ // account central stuff when we don't have a dbview
+ else {
+ this._showAccountCentral();
+ if (this._tabInfo)
+ mailTabType._setPaneStates(this._tabInfo.mode.legalPanes,
+ {folder: !this._tabInfo.folderPaneCollapsed,
+ message: false});
+ }
+
+ 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 =
+ pref.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() {
+ this._active = false;
+ // save the folder pane's state always
+ this.folderPaneCollapsed =
+ document.getElementById("folderPaneBox").collapsed;
+
+ if (this.view.dbView) {
+ if (this.treeBox)
+ this._savedFirstVisibleRow = this.treeBox.getFirstVisibleRow();
+
+ // save column states
+ this.saveColumnStates();
+
+ // save the message pane's state only when it is potentially visible
+ this.messagePaneCollapsed =
+ document.getElementById("messagepanebox").collapsed;
+
+ // save the actual quick-search query text
+ let searchInput = document.getElementById("searchInput");
+ if (searchInput) {
+ this._savedQuickSearch = {
+ text: searchInput.showingSearchCriteria ? null : searchInput.value,
+ searchMode: searchInput.searchMode
+ };
+ }
+
+ // 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.
+ let treeViewSelection = this.view.dbView.selection;
+ // 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 (this.treeBox)
+ this.treeBox.view = null;
+ // (and tell the db view about its selection again...)
+ this.view.dbView.selection = treeViewSelection;
+
+ // hook the dbview up to the fake tree box
+ this._fakeTreeBox.view = this.view.dbView;
+ this.view.dbView.setTree(this._fakeTreeBox);
+ treeViewSelection.tree = this._fakeTreeBox;
+ }
+
+ this.messageDisplay.makeInactive();
+ },
+
+ /**
+ * @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);
+ },
+
+
+
+ /**
+ * 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 aNavType {nsMsgNavigationType} navigation command.
+ * @param aSelect {Boolean} should we select the message if we find one?
+ * Defaults to true if omitted.
+ *
+ * @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 =
+ gPrefBranch.getBoolPref("mail.operate_on_msgs_in_collapsed_threads");
+
+ 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);
+ },
+
+ /**
+ * @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 FolderDisplayWidget_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.
+ */
+ get selectedMessageIsFeed FolderDisplayWidget_get_selectedMessageIsFeed() {
+ let message = this.selectedMessage;
+ return Boolean(message && message.folder &&
+ message.folder.server.type == 'rss');
+ },
+
+ /**
+ * @return true if there is a selected message and it's an IMAP message.
+ */
+ get selectedMessageIsImap FolderDisplayWidget_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 FolderDisplayWidget_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
+ FolderDisplayWidget_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 the number of selected messages. If the
+ * "mail.operate_on_msgs_in_collapsed_threads" preference is enabled, then
+ * any collapsed thread roots that are selected will also conceptually have
+ * all of the messages in that thread selected.
+ */
+ get selectedCount FolderDisplayWidget_get_selectedCount() {
+ if (!this.view.dbView)
+ return 0;
+ return this.view.dbView.numSelected;
+ },
+
+ /**
+ * Provides a list of the view indices that are selected which is *not* the
+ * same as the rows of the selected messages. When the
+ * "mail.operate_on_msgs_in_collapsed_threads" preference is enabled,
+ * 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 FolderDisplayWidget_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 the "mail.operate_on_msgs_in_collapsed_threads" preference is enabled,
+ * 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 FolderDisplayWidget_get_selectedMessages() {
+ if (!this.view.dbView)
+ return [];
+ // getMsgHdrsForSelection returns an nsIMutableArray. We want our callers
+ // to have a user-friendly JS array and not have to worry about
+ // QueryInterfacing the values (or needing to know to use fixIterator).
+ return [msgHdr for each
+ (msgHdr in fixIterator(
+ this.view.dbView.getMsgHdrsForSelection().enumerate(),
+ Components.interfaces.nsIMsgDBHdr))];
+ },
+
+ /**
+ * @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 FolderDisplayWidget_get_selectedMessageUris() {
+ if (!this.view.dbView)
+ return null;
+
+ let messageArray = this.view.dbView.getURIsForSelection({});
+ return messageArray.length ? messageArray : null;
+ },
+
+ /**
+ * 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();
+ },
+
+ /**
+ * Select a message for display by header. If the view is active, attempt
+ * to select the message right now. If the view is not active or 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.
+ */
+ selectMessage: function FolderDisplayWidget_selectMessage(aMsgHdr,
+ aForceNotification) {
+ if (this.active) {
+ let viewIndex = this.view.dbView.findIndexOfMsgHdr(aMsgHdr, true);
+ if (viewIndex != nsMsgViewIndex_None) {
+ this.selectViewIndex(viewIndex);
+ return;
+ }
+ }
+ this._savedSelection = [{messageId: aMsgHdr.messageId}];
+ // queue the selection to be restored once we become active if we are not
+ // active.
+ if (!this.active)
+ this._notifyWhenActive(this._restoreSelection);
+ },
+
+ /**
+ * Select all of the provided nsIMsgDBHdrs in the aMessages array, expanding
+ * threads as required. If the view is not active, or 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 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, aDoNotNeedToFindAll) {
+ if (this.active && this.treeSelection) {
+ let foundAll = true;
+ let treeSelection = this.treeSelection;
+ let dbView = this.view.dbView;
+ let minRow = null, maxRow = null;
+
+ treeSelection.selectEventsSuppressed = true;
+ treeSelection.clearSelection();
+
+ for each (let [, msgHdr] in Iterator(aMessages)) {
+ let viewIndex = dbView.findIndexOfMsgHdr(msgHdr, true);
+ 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 (!aDoNotNeedToFindAll || foundAll)
+ return;
+ }
+ this._savedSelection = [{messageId: msgHdr.messageId} for each
+ ([, msgHdr] in Iterator(aMessages))];
+ 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);
+ }
+
+ this.ensureRowIsVisible(aViewIndex);
+ },
+
+ /**
+ * 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 each (let [, index] in Iterator(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);
+ },
+
+ /**
+ * Number of padding messages before the 'focused' message when it is at the
+ * top of the thread pane.
+ */
+ TOP_VIEW_PADDING: 1,
+ /**
+ * Number of padding messages after the 'focused' message when it is at the
+ * bottom of the thread pane and lip padding does not apply.
+ */
+ BOTTOM_VIEW_PADDING: 1,
+
+ /**
+ * Ensure the given view index is visible, preferably 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.
+ * If we get near the end of the list of messages, we 'snap' to the last page
+ * of messages. The intent is that we later implement a
+ * 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 target;
+ // If the index is near the end, try and latch on to the bottom.
+ if (aViewIndex + span - this.TOP_VIEW_PADDING > maxIndex)
+ target = maxIndex - span;
+ // 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.
+ else if (aViewIndex >= last - this.BOTTOM_VIEW_PADDING)
+ target = Math.min(maxIndex, aViewIndex + this.BOTTOM_VIEW_PADDING) -
+ span;
+ // If the index is before the first visible guy (with padding), move up
+ else if (aViewIndex <= first + this.TOP_VIEW_PADDING) // move up
+ target = Math.max(0, aViewIndex - this.TOP_VIEW_PADDING);
+ else // it is already visible
+ return;
+
+ // this sets the first visible row
+ treeBox.scrollToRow(target);
+ },
+
+ /**
+ * Ensure that the given range of rows is visible 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;
+
+ // bail if the range is already visible with padding constraints handled
+ if ((first + this.TOP_VIEW_PADDING <= aMinRow) &&
+ (last - this.BOTTOM_VIEW_PADDING >= 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 - this.TOP_VIEW_PADDING);
+ // 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.
+ else {
+ let rowSpan = aMaxRow - aMinRow + 1;
+ let halfSpare = parseInt((span - rowSpan - this.TOP_VIEW_PADDING -
+ this.BOTTOM_VIEW_PADDING) / 2);
+ target = aMinRow - halfSpare - this.TOP_VIEW_PADDING;
+ }
+ 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. We
+ * can't really stop that, but we can use misdirection to tell it about a box
+ * object that we don't care about. That way it gets the bogus events,
+ * effectively blackholing them.
+ */
+function FakeTreeBoxObject(aDummyBoxObject) {
+ this.dummyBoxObject = aDummyBoxObject.QueryInterface(
+ Components.interfaces.nsIBoxObject);
+ this.view = null;
+}
+FakeTreeBoxObject.prototype = {
+ view: null,
+ /**
+ * 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() {
+
+ },
+ rowCountChanged: function FakeTreeBoxObject_rowCountChanged() {
+
+ },
+ /**
+ * Sleight of hand! If someone asks us about an nsIBoxObject, we tell them
+ * about a real box object that is just a dummy and is never used for
+ * anything.
+ */
+ QueryInterface: function FakeTreeBoxObject_QueryInterface(aIID) {
+ if (aIID.equals(Components.interfaces.nsIBoxObject))
+ return this.dummyBoxObject;
+ if (!aIID.equals(Components.interfaces.nsISupports) &&
+ !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] in Iterator(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] in Iterator(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",
+ "ensureRowIsVisible",
+ "ensureCellIsVisible",
+ "scrollToRow",
+ "scrollByLines",
+ "scrollByPages",
+ "scrollToCell",
+ "scrollToColumn",
+ "scrollToHorizontalPosition",
+ "invalidateColumn",
+ "invalidateRow",
+ "invalidateCell",
+ "invalidateColumnRange",
+ "getRowAt",
+ "getCellAt",
+ "getCoordsForCellItem",
+ "isCellCropped",
+ "clearStyleAndImageCaches",
+ ]);
\ No newline at end of file
--- a/mail/base/content/folderPane.js
+++ b/mail/base/content/folderPane.js
@@ -58,17 +58,17 @@ let gFolderTreeView = {
load: function ftv_load(aTree, aJSONFile) {
const Cc = Components.classes;
const Ci = Components.interfaces;
this._treeElement = aTree;
let smartName = document.getElementById("bundle_messenger")
.getString("folderPaneHeader_smart");
this.registerMode("smart", this._smartFoldersGenerator, smartName);
- // the folder pane can be used for other trees which may not have these elements.
+ // the folder pane can be used for other trees which may not have these elements.
if (document.getElementById("folderpane_splitter"))
document.getElementById("folderpane_splitter").collapsed = false;
if (document.getElementById("folderPaneBox"))
document.getElementById("folderPaneBox").collapsed = false;
try {
// Normally our tree takes care of keeping the last selected by itself.
// However older versions of TB stored this in a preference, which we need
@@ -105,17 +105,17 @@ let gFolderTreeView = {
.createInstance(Ci.nsIFileInputStream);
let sstream = Cc["@mozilla.org/scriptableinputstream;1"]
.createInstance(Ci.nsIScriptableInputStream);
fstream.init(file, -1, 0, 0);
sstream.init(fstream);
while (sstream.available())
data += sstream.read(4096);
-
+
sstream.close();
fstream.close();
let JSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
this._persistOpenMap = JSON.decode(data);
}
}
// Load our data
@@ -211,17 +211,17 @@ let gFolderTreeView = {
aEvent.button != 0 || aEvent.originalTarget.localName == "twisty" ||
aEvent.originalTarget.localName == "slider" ||
aEvent.originalTarget.localName == "scrollbarbutton")
return;
let row = gFolderTreeView._treeElement.treeBoxObject.getRowAt(aEvent.clientX,
aEvent.clientY);
let folderItem = gFolderTreeView._rowMap[row];
- if (folderItem)
+ if (folderItem)
folderItem.command();
// Don't let the double-click toggle the open state of the folder here
aEvent.stopPropagation();
},
getFolderAtCoords: function ftv_getFolderAtCoords(aX, aY) {
let row = gFolderTreeView._treeElement.treeBoxObject.getRowAt(aX, aY);
@@ -265,17 +265,17 @@ let gFolderTreeView = {
* that the folder is actually being displayed (that is, that none of its
* ancestors are collapsed.
*
* @param aFolder the nsIMsgFolder to select
*/
selectFolder: function ftv_selectFolder(aFolder) {
// "this" inside the nested function refers to the function...
// Also note that openIfNot is recursive.
- let tree = this;
+ let tree = this;
function openIfNot(aFolderToOpen) {
let index = tree.getIndexOfFolder(aFolderToOpen);
if (index) {
if (!tree._rowMap[index].open)
tree._toggleRow(index, false);
return;
}
@@ -444,17 +444,17 @@ let gFolderTreeView = {
let folders = new Array;
folders.push(dt.mozGetDataAt("text/x-moz-folder", i)
.QueryInterface(Ci.nsIMsgFolder));
let array = toXPCOMArray(folders, Ci.nsIMutableArray);
cs.CopyFolders(array, targetFolder,
(folders[0].server == targetFolder.server), null,
msgWindow);
}
- }
+ }
else if (Array.indexOf(types, "text/x-moz-newsfolder") != -1) {
// Start by getting folders into order.
let folders = new Array;
for (let i = 0; i < count; i++) {
let folder = dt.mozGetDataAt("text/x-moz-newsfolder", i)
.QueryInterface(Ci.nsIMsgFolder);
folders[this.getIndexOfFolder(folder)] = folder;
}
@@ -1027,17 +1027,17 @@ let gFolderTreeView = {
acct.incomingServer.rootFolder.subFolders;
}
catch (ex) {
Components.classes["@mozilla.org/consoleservice;1"]
.getService(Components.interfaces.nsIConsoleService)
.logStringMessage("Discovering folders for account failed with exception: " + ex);
}
}
-
+
return [new ftvItem(acct.incomingServer.rootFolder)
for each (acct in accounts)];
},
/**
* The unread mode returns all folders that are not root-folders and that
* have unread items. Also always keep the currently selected folder
* so it doesn't disappear under the user.
@@ -1129,17 +1129,17 @@ let gFolderTreeView = {
for each (let folder in ftv._enumerateFolders)
addIfRecent(folder);
recentFolders.sort(sorter);
let items = [new ftvItem(f) for each (f in recentFolders)];
- // There are no children in this view!
+ // There are no children in this view!
// And we want to display the account name to distinguish folders w/
// the same name.
for each (let folder in items) {
folder.__defineGetter__("children", function() []);
folder.addServerName = true;
}
return items;
@@ -1253,17 +1253,17 @@ let gFolderTreeView = {
OnItemRemoved: function ftl_remove(aRDFParentItem, aItem) {
if (!(aItem instanceof Components.interfaces.nsIMsgFolder))
return;
let persistMapIndex = this._persistOpenMap[this.mode].indexOf(aItem.URI);
if (persistMapIndex != -1)
this._persistOpenMap[this.mode].splice(persistMapIndex, 1);
-
+
let index = this.getIndexOfFolder(aItem);
if (!index)
return;
// forget our parent's children; they'll get rebuilt
if (aRDFParentItem)
this._rowMap[index]._parent._children = null;
let kidCount = 1;
let walker = Number(index) + 1;
@@ -1465,23 +1465,16 @@ let gFolderTreeController = {
msgDB.summaryValid = false;
try {
folder.closeAndBackupFolderDB("");
}
catch(e) {
// In a failure, proceed anyway since we're dealing with problems
folder.ForceDBClosed();
}
- // these lines will cause the thread pane to get reloaded when the
- // download/reparse is finished. Only do this if the selected folder is
- // loaded (i.e., not thru the context menu on a non-loaded folder).
- if (folder == GetLoadedMsgFolder()) {
- gRerootOnFolderLoad = true;
- gCurrentFolderToReroot = folder.URI;
- }
folder.updateFolder(msgWindow);
}
window.openDialog("chrome://messenger/content/folderProps.xul", "",
"chrome,centerscreen,titlebar,modal",
{folder: folder, serverType: folder.server.type,
msgWindow: msgWindow, title: title,
okCallback: editFolderCallback,
@@ -1500,17 +1493,16 @@ let gFolderTreeController = {
let folder = aFolder || gFolderTreeView.getSelectedFolders()[0];
//xxx no need for uri now
let controller = this;
function renameCallback(aName, aUri) {
if (aUri != folder.URI)
Components.utils.reportError("got back a different folder to rename!");
- controller._resetThreadPane();
controller._tree.view.selection.clearSelection();
// Actually do the rename
folder.rename(aName, msgWindow);
}
window.openDialog("chrome://messenger/content/renameFolderDialog.xul",
"newFolder", "chrome,titlebar,modal",
{preselectedURI: folder.URI,
@@ -1559,21 +1551,19 @@ let gFolderTreeController = {
}
if (folder.flags & FLAGS.Virtual) {
let confirmation = bundle.getString("confirmSavedSearchDeleteMessage");
let title = bundle.getString("confirmSavedSearchTitle");
let IPS = Components.interfaces.nsIPromptService;
if (Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(IPS)
- .confirmEx(window, title, confirmation, IPS.STD_YES_NO_BUTTONS + IPS.BUTTON_POS_1_DEFAULT,
+ .confirmEx(window, title, confirmation, IPS.STD_YES_NO_BUTTONS + IPS.BUTTON_POS_1_DEFAULT,
"", "", "", "", {}) != 0) /* the yes button is in position 0 */
return;
- if (gCurrentVirtualFolderUri == folder.URI)
- gCurrentVirtualFolderUri = null;
}
let array = toXPCOMArray([folder], Ci.nsIMutableArray);
folder.parent.deleteSubFolders(array, msgWindow);
},
/**
* Prompts the user to confirm and empties the trash for the selected folder
@@ -1621,20 +1611,16 @@ let gFolderTreeController = {
*/
compactFolders: function ftc_compactFolders(aFolders) {
let folders = aFolders || gFolderTreeView.getSelectedFolders();
for (let i = 0; i < folders.length; i++) {
// Can't compact folders that have just been compacted.
if (folders[i].server.type != "imap" && !folders[i].expungedBytes)
continue;
- // Reset thread pane for non-imap folders if the folder is selected.
- if (gDBView && gDBView.msgFolder == folders[i] && folders[i].server.type != "imap")
- this._resetThreadPane();
-
folders[i].compact(null, msgWindow);
}
},
/**
* Compacts all folders for accounts that the given folders belong
* to, or all folders for accounts of the currently selected folders.
*
@@ -1686,29 +1672,16 @@ let gFolderTreeController = {
window.openDialog("chrome://messenger/content/virtualFolderProperties.xul",
"", "chrome,titlebar,modal,centerscreen",
{folder: folder, editExistingFolder: true,
onOKCallback: editVirtualCallback,
msgWindow: msgWindow});
},
/**
- * For certain folder commands, the thread pane needs to be invalidated, this
- * takes care of doing so.
- */
- _resetThreadPane: function ftc_resetThreadPane() {
- if (gDBView)
- gCurrentlyDisplayedMessage = gDBView.currentlyDisplayedMessage;
-
- ClearThreadPaneSelection();
- ClearThreadPane();
- ClearMessagePane();
- },
-
- /**
* Prompts for confirmation, if the user hasn't already chosen the "don't ask
* again" option.
*
* @param aCommand - the command to prompt for
*/
_checkConfirmationPrompt: function ftc_confirm(aCommand) {
const Cc = Components.classes;
const Ci = Components.interfaces;
--- a/mail/base/content/mail-offline.js
+++ b/mail/base/content/mail-offline.js
@@ -259,12 +259,11 @@ var MailOfflineMgr = {
}
},
/**
* private helper method called whenever we detect a change to the offline state
*/
mailOfflineStateChanged: function (aGoingOffline)
{
- gFolderJustSwitched = true;
this.updateOfflineUI(aGoingOffline);
}
};
--- a/mail/base/content/mail3PaneWindowCommands.js
+++ b/mail/base/content/mail3PaneWindowCommands.js
@@ -1,48 +1,47 @@
-# -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is Mozilla Communicator client code, released
-# March 31, 1998.
-#
-# The Initial Developer of the Original Code is
-# Netscape Communications Corporation.
-# Portions created by the Initial Developer are Copyright (C) 1998-2000
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-# Jan Varga <varga@nixcorp.com>
-# HÃ¥kan Waara <hwaara@gmail.com>
-# Magnus Melin <mkmelin+mozilla@iki.fi>
-#
-# Alternatively, the contents of this file may be used under the terms of
-# either the GNU General Public License Version 2 or later (the "GPL"), or
-# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-# in which case the provisions of the GPL or the LGPL are applicable instead
-# of those above. If you wish to allow use of your version of this file only
-# under the terms of either the GPL or the LGPL, and not to allow others to
-# use your version of this file under the terms of the MPL, indicate your
-# decision by deleting the provisions above and replace them with the notice
-# and other provisions required by the GPL or the LGPL. If you do not delete
-# the provisions above, a recipient may use your version of this file under
-# the terms of any one of the MPL, the GPL or the LGPL.
-#
-# ***** END LICENSE BLOCK *****
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Communicator client code, released
+ * March 31, 1998.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1998-2000
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Jan Varga <varga@nixcorp.com>
+ * HÃ¥kan Waara <hwaara@gmail.com>
+ * Magnus Melin <mkmelin+mozilla@iki.fi>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
var gMessengerBundle = document.getElementById("bundle_messenger");
// Controller object for folder pane
var FolderPaneController =
{
supportsCommand: function(command)
{
@@ -103,17 +102,17 @@ var FolderPaneController =
if (!this.isCommandEnabled(command)) return;
switch ( command )
{
case "cmd_delete":
case "cmd_shiftDelete":
case "button_delete":
case "cmd_deleteFolder":
- gFolderTreeController.deleteFolder();
+ gFolderTreeController.deleteFolder();
break;
}
},
onEvent: function(event)
{
}
};
@@ -237,17 +236,17 @@ var DefaultController =
case "cmd_downloadFlagged":
case "cmd_downloadSelected":
case "cmd_synchronizeOffline":
return MailOfflineMgr.isOnline();
case "cmd_watchThread":
case "cmd_killThread":
case "cmd_killSubthread":
- return(isNewsURI(GetFirstSelectedMessage()));
+ return(gFolderDisplay.selectedMessageIsNews);
default:
return false;
}
},
isCommandEnabled: function(command)
{
@@ -257,45 +256,37 @@ var DefaultController =
switch ( command )
{
case "cmd_delete":
UpdateDeleteCommand();
// fall through
case "button_delete":
UpdateDeleteToolbarButton();
- if (gDBView)
- gDBView.getCommandStatus(nsMsgViewCommandType.deleteMsg, enabled, checkStatus);
- return enabled.value;
+ return gFolderDisplay.getCommandStatus(nsMsgViewCommandType.deleteMsg);
case "cmd_shiftDelete":
- if (gDBView)
- gDBView.getCommandStatus(nsMsgViewCommandType.deleteNoTrash, enabled, checkStatus);
- return enabled.value;
+ return gFolderDisplay.getCommandStatus(nsMsgViewCommandType.deleteNoTrash);
case "cmd_deleteFolder":
var folders = gFolderTreeView.getSelectedFolders();
if (folders.length == 1) {
var folder = folders[0];
if (folder.server.type == "nntp")
return false; // Just disable the command for news.
else
return CanDeleteFolder(folder);
}
return false;
case "button_junk":
UpdateJunkToolbarButton();
- if (gDBView)
- gDBView.getCommandStatus(nsMsgViewCommandType.junk, enabled, checkStatus);
- return enabled.value;
+ return gFolderDisplay.getCommandStatus(nsMsgViewCommandType.junk);
case "cmd_killThread":
case "cmd_killSubthread":
return GetNumSelectedMessages() > 0;
case "cmd_watchThread":
- if (gDBView)
- gDBView.getCommandStatus(nsMsgViewCommandType.toggleThreadWatched, enabled, checkStatus);
- return enabled.value;
+ return gFolderDisplay.getCommandStatus(nsMsgViewCommandType.toggleThreadWatched);
case "cmd_createFilterFromPopup":
case "cmd_createFilterFromMenu":
var loadedFolder = GetLoadedMsgFolder();
if (!(loadedFolder && loadedFolder.server.canHaveFilters))
return false; // else fall thru
case "cmd_saveAsFile":
case "cmd_saveAsTemplate":
if (GetNumSelectedMessages() > 1)
@@ -323,30 +314,21 @@ var DefaultController =
{
var whichText = "valueMessage";
if (GetNumSelectedMessages() > 1)
whichText = "valueSelection";
goSetMenuValue(command, whichText);
goSetAccessKey(command, whichText + "AccessKey");
}
if (GetNumSelectedMessages() > 0)
- {
- if (gDBView)
- {
- gDBView.getCommandStatus(nsMsgViewCommandType.cmdRequiringMsgBody, enabled, checkStatus);
- return enabled.value;
- }
- }
+ return gFolderDisplay.getCommandStatus(nsMsgViewCommandType.cmdRequiringMsgBody);
return false;
case "cmd_printpreview":
- if ( GetNumSelectedMessages() == 1 && gDBView)
- {
- gDBView.getCommandStatus(nsMsgViewCommandType.cmdRequiringMsgBody, enabled, checkStatus);
- return enabled.value;
- }
+ if (GetNumSelectedMessages() == 1)
+ return gFolderDisplay.getCommandStatus(nsMsgViewCommandType.cmdRequiringMsgBody);
return false;
case "cmd_printSetup":
return true;
case "cmd_markAsFlagged":
case "button_file":
case "cmd_file":
case "cmd_archive":
return (GetNumSelectedMessages() > 0 );
@@ -355,33 +337,27 @@ var DefaultController =
let folder = GetLoadedMsgFolder();
return GetNumSelectedMessages() > 0 && folder &&
!(IsSpecialFolder(folder, Components.interfaces.nsMsgFolderFlags.Archive,
true));
}
case "cmd_markAsJunk":
case "cmd_markAsNotJunk":
// can't do news on junk yet.
- return (GetNumSelectedMessages() > 0 && !isNewsURI(GetFirstSelectedMessage()));
+ return (GetNumSelectedMessages() > 0 && !gFolderDisplay.selectedMessageIsNews);
case "cmd_recalculateJunkScore":
if (GetNumSelectedMessages() > 0)
- gDBView.getCommandStatus(nsMsgViewCommandType.runJunkControls, enabled, checkStatus);
- return enabled.value;
+ return gFolderDisplay.getCommandStatus(nsMsgViewCommandType.runJunkControls);
+ return false;
case "cmd_applyFilters":
- if (gDBView)
- gDBView.getCommandStatus(nsMsgViewCommandType.applyFilters, enabled, checkStatus);
- return enabled.value;
+ return gFolderDisplay.getCommandStatus(nsMsgViewCommandType.applyFilters);
case "cmd_runJunkControls":
- if (gDBView)
- gDBView.getCommandStatus(nsMsgViewCommandType.runJunkControls, enabled, checkStatus);
- return enabled.value;
+ return gFolderDisplay.getCommandStatus(nsMsgViewCommandType.runJunkControls);
case "cmd_deleteJunk":
- if (gDBView)
- gDBView.getCommandStatus(nsMsgViewCommandType.deleteJunk, enabled, checkStatus);
- return enabled.value;
+ return gFolderDisplay.getCommandStatus(nsMsgViewCommandType.deleteJunk);
case "button_mark":
case "cmd_tag":
case "cmd_markAsRead":
case "cmd_markThreadAsRead":
return GetNumSelectedMessages() > 0;
case "button_previous":
case "button_next":
return IsViewNavigationItemEnabled();
@@ -413,28 +389,27 @@ var DefaultController =
case "cmd_selectAll":
case "cmd_selectFlagged":
return gDBView != null;
// these are enabled on when we are in threaded mode
case "cmd_selectThread":
if (GetNumSelectedMessages() <= 0) return false;
case "cmd_expandAllThreads":
case "cmd_collapseAllThreads":
- return gDBView && (gDBView.viewFlags & nsMsgViewFlagsType.kThreadedDisplay);
+ return gFolderDisplay.view.showThreaded;
case "cmd_nextFlaggedMsg":
case "cmd_previousFlaggedMsg":
return IsViewNavigationItemEnabled();
case "cmd_viewAllMsgs":
case "cmd_viewUnreadMsgs":
case "cmd_viewIgnoredThreads":
return gDBView;
case "cmd_viewThreadsWithUnread":
case "cmd_viewWatchedThreadsWithUnread":
- return gDBView && !(GetSelectedMsgFolders()[0].flags &
- Components.interfaces.nsMsgFolderFlags.Virtual);
+ return !gFolderDisplay.view.isVirtual;
case "cmd_stop":
return true;
case "cmd_undo":
case "cmd_redo":
return SetupUndoRedoCommand(command);
case "cmd_renameFolder":
{
let folders = gFolderTreeView.getSelectedFolders();
@@ -564,36 +539,41 @@ var DefaultController =
break;
case "cmd_createFilterFromPopup":
break;// This does nothing because the createfilter is invoked from the popupnode oncommand.
case "button_delete":
case "cmd_delete":
// if the user deletes a message before its mark as read timer goes off, we should mark it as read
// this ensures that we clear the biff indicator from the system tray when the user deletes the new message
MarkSelectedMessagesRead(true);
- SetNextMessageAfterDelete();
- gDBView.doCommand(nsMsgViewCommandType.deleteMsg);
+ // If this is a right-click triggered delete, then do not hint about
+ // the deletion. Note: The code that swaps the selection back in will
+ // take care of ensuring that this deletion does not make the saved
+ // selection incorrect.
+ if (!gRightMouseButtonSavedSelection)
+ gFolderDisplay.hintAboutToDeleteMessages();
+ gFolderDisplay.doCommand(nsMsgViewCommandType.deleteMsg);
break;
case "cmd_shiftDelete":
MarkSelectedMessagesRead(true);
- SetNextMessageAfterDelete();
- gDBView.doCommand(nsMsgViewCommandType.deleteNoTrash);
+ gFolderDisplay.hintAboutToDeleteMessages();
+ gFolderDisplay.doCommand(nsMsgViewCommandType.deleteNoTrash);
break;
case "cmd_deleteFolder":
gFolderTreeController.deleteFolder();
break;
case "cmd_killThread":
/* kill thread kills the thread and then does a next unread */
GoNextMessage(nsMsgNavigationType.toggleThreadKilled, true);
break;
case "cmd_killSubthread":
GoNextMessage(nsMsgNavigationType.toggleSubthreadKilled, true);
break;
case "cmd_watchThread":
- gDBView.doCommand(nsMsgViewCommandType.toggleThreadWatched);
+ gFolderDisplay.doCommand(nsMsgViewCommandType.toggleThreadWatched);
break;
case "button_next":
case "cmd_nextUnreadMsg":
GoNextMessage(nsMsgNavigationType.nextUnreadMessage, true);
break;
case "cmd_nextUnreadThread":
GoNextMessage(nsMsgNavigationType.nextUnreadThread, true);
break;
@@ -634,20 +614,23 @@ var DefaultController =
break;
case "cmd_undo":
messenger.undo(msgWindow);
break;
case "cmd_redo":
messenger.redo(msgWindow);
break;
case "cmd_expandAllThreads":
- gDBView.doCommand(nsMsgViewCommandType.expandAll);
+ gFolderDisplay.doCommand(nsMsgViewCommandType.expandAll);
+ gFolderDisplay.ensureSelectionIsVisible();
break;
case "cmd_collapseAllThreads":
- gDBView.doCommand(nsMsgViewCommandType.collapseAll);
+ gFolderDisplay.selectSelectedThreadRoots();
+ gFolderDisplay.doCommand(nsMsgViewCommandType.collapseAll);
+ gFolderDisplay.ensureSelectionIsVisible();
break;
case "cmd_renameFolder":
gFolderTreeController.renameFolder();
return;
case "cmd_sendUnsentMsgs":
// if offline, prompt for sendUnsentMessages
if (MailOfflineMgr.isOnline())
SendUnsentMessages();
@@ -668,17 +651,17 @@ var DefaultController =
return;
case "cmd_saveAsFile":
MsgSaveAsFile();
return;
case "cmd_saveAsTemplate":
MsgSaveAsTemplate();
return;
case "cmd_viewPageSource":
- ViewPageSource(GetSelectedMessages());
+ ViewPageSource(gFolderDisplay.selectedMessageUris);
return;
case "cmd_setFolderCharset":
gFolderTreeController.editFolder();
return;
case "cmd_reload":
ReloadMessage();
return;
case "cmd_find":
@@ -709,20 +692,20 @@ var DefaultController =
MsgSearchMessages();
return;
case "button_mark":
case "cmd_markAsRead":
MsgMarkMsgAsRead();
return;
case "cmd_markThreadAsRead":
ClearPendingReadTimer();
- gDBView.doCommand(nsMsgViewCommandType.markThreadRead);
+ gFolderDisplay.doCommand(nsMsgViewCommandType.markThreadRead);
return;
case "cmd_markAllRead":
- gDBView.doCommand(nsMsgViewCommandType.markAllRead);
+ gFolderDisplay.doCommand(nsMsgViewCommandType.markAllRead);
return;
case "button_junk":
MsgJunk();
return;
case "cmd_stop":
msgWindow.StopUrls();
return;
case "cmd_markAsFlagged":
@@ -754,20 +737,20 @@ var DefaultController =
return;
case "cmd_compactFolder":
gFolderTreeController.compactAllFoldersForAccount();
return;
case "button_compact":
gFolderTreeController.compactFolders();
return;
case "cmd_downloadFlagged":
- gDBView.doCommand(nsMsgViewCommandType.downloadFlaggedForOffline);
+ gFolderDisplay.doCommand(nsMsgViewCommandType.downloadFlaggedForOffline);
break;
case "cmd_downloadSelected":
- gDBView.doCommand(nsMsgViewCommandType.downloadSelectedForOffline);
+ gFolderDisplay.doCommand(nsMsgViewCommandType.downloadSelectedForOffline);
break;
case "cmd_synchronizeOffline":
MsgSynchronizeOffline();
break;
case "cmd_settingsOffline":
MailOfflineMgr.openOfflineAccountSettings();
break;
case "cmd_moveToFolderAgain":
@@ -776,27 +759,23 @@ var DefaultController =
MsgMoveMessage(GetMsgFolderFromUri(folderId));
else
MsgCopyMessage(GetMsgFolderFromUri(folderId));
break;
case "cmd_selectAll":
// move the focus so the user can delete the newly selected messages, not the folder
SetFocusThreadPane();
// if in threaded mode, the view will expand all before selecting all
- gDBView.doCommand(nsMsgViewCommandType.selectAll)
- if (gDBView.numSelected != 1) {
- setTitleFromFolder(gDBView.msgFolder,null);
- ClearMessagePane();
- }
+ gFolderDisplay.doCommand(nsMsgViewCommandType.selectAll);
break;
case "cmd_selectThread":
- gDBView.doCommand(nsMsgViewCommandType.selectThread);
+ gFolderDisplay.doCommand(nsMsgViewCommandType.selectThread);
break;
case "cmd_selectFlagged":
- gDBView.doCommand(nsMsgViewCommandType.selectFlagged);
+ gFolderDisplay.doCommand(nsMsgViewCommandType.selectFlagged);
break;
case "cmd_fullZoomReduce":
ZoomManager.reduce();
break;
case "cmd_fullZoomEnlarge":
ZoomManager.enlarge();
break;
case "cmd_fullZoomReset":
--- a/mail/base/content/mailContextMenus.js
+++ b/mail/base/content/mailContextMenus.js
@@ -1,113 +1,77 @@
-# -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-#
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is Mozilla Communicator client code, released
-# March 31, 1998.
-#
-# The Initial Developer of the Original Code is
-# Netscape Communications Corporation.
-# Portions created by the Initial Developer are Copyright (C) 2000
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-# Jan Varga <varga@nixcorp.com>
-# Hakan Waara <hwaara@chello.se>
-# Markus Hossner <markushossner@gmx.de>
-# Magnus Melin <mkmelin+mozilla@iki.fi>
-#
-# Alternatively, the contents of this file may be used under the terms of
-# either the GNU General Public License Version 2 or later (the "GPL"), or
-# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-# in which case the provisions of the GPL or the LGPL are applicable instead
-# of those above. If you wish to allow use of your version of this file only
-# under the terms of either the GPL or the LGPL, and not to allow others to
-# use your version of this file under the terms of the MPL, indicate your
-# decision by deleting the provisions above and replace them with the notice
-# and other provisions required by the GPL or the LGPL. If you do not delete
-# the provisions above, a recipient may use your version of this file under
-# the terms of any one of the MPL, the GPL or the LGPL.
-#
-# ***** END LICENSE BLOCK *****
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Communicator client code, released
+ * March 31, 1998.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2000
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Jan Varga <varga@nixcorp.com>
+ * Hakan Waara <hwaara@chello.se>
+ * Markus Hossner <markushossner@gmx.de>
+ * Magnus Melin <mkmelin+mozilla@iki.fi>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
//NOTE: gMessengerBundle must be defined and set or this Overlay won't work
Components.utils.import("resource://gre/modules/PluralForm.jsm");
const mailtolength = 7;
/**
* Function to change the highlighted row back to the row that is currently
* outline/dotted without loading the contents of either rows. This is
* triggered when the context menu for a given row is hidden/closed
* (onpopuphiding).
* @param tree the tree element to restore selection for
*/
function RestoreSelectionWithoutContentLoad(tree)
{
- if (!tree)
- return;
-
- // If a delete or move command had been issued, then we should
- // reset gRightMouseButtonDown and gThreadPaneDeleteOrMoveOccurred
- // and return (see bug 142065).
- if(gThreadPaneDeleteOrMoveOccurred)
- {
- gRightMouseButtonDown = false;
- gThreadPaneDeleteOrMoveOccurred = false;
- return;
- }
-
- var treeSelection = tree.view.selection;
-
- // make sure that currentIndex is valid so that we don't try to restore
- // a selection of an invalid row.
- if((!treeSelection.isSelected(treeSelection.currentIndex)) &&
- (treeSelection.currentIndex >= 0))
- {
- treeSelection.selectEventsSuppressed = true;
- treeSelection.select(treeSelection.currentIndex);
- treeSelection.selectEventsSuppressed = false;
+ if (gRightMouseButtonSavedSelection) {
+ let view = gRightMouseButtonSavedSelection.view;
+ // restore the selection
+ let transientSelection = gRightMouseButtonSavedSelection.transientSelection;
+ let realSelection = gRightMouseButtonSavedSelection.realSelection;
+ view.selection = realSelection;
+ // replay any calls to adjustSelection, this handles suppression.
+ transientSelection.replayAdjustSelectionLog(realSelection);
+ gRightMouseButtonSavedSelection = null;
- // Keep track of which row in the thread pane is currently selected.
- // This is currently only needed when deleting messages. See
- // declaration of var in msgMail3PaneWindow.js.
- if(tree.id == "threadTree")
- gThreadPaneCurrentSelectedIndex = treeSelection.currentIndex;
- }
- else if(treeSelection.currentIndex < 0)
- // Clear the selection in the case of when a folder has just been
- // loaded where the message pane does not have a message loaded yet.
- // When right-clicking a message in this case and dismissing the
- // popup menu (by either executing a menu command or clicking
- // somewhere else), the selection needs to be cleared.
- // However, if the 'Delete Message' or 'Move To' menu item has been
- // selected, DO NOT clear the selection, else it will prevent the
- // tree view from refreshing.
- treeSelection.clearSelection();
-
- // Need to reset gRightMouseButtonDown to false here because
- // TreeOnMouseDown() is only called on a mousedown, not on a key down.
- // So resetting it here allows the loading of messages in the messagepane
- // when navigating via the keyboard or the toolbar buttons *after*
- // the context menu has been dismissed.
- gRightMouseButtonDown = false;
+ if (tree)
+ tree.treeBoxObject.invalidate();
+ }
}
/**
* Function to clear out the global nsContextMenu, and in the case when we
* were a threadpane context menu, restore the selection so that a right-click
* on a non-selected row doesn't move the selection.
* @param event the onpopuphiding event
*/
@@ -149,18 +113,18 @@ function fillMailContextMenu(event)
var numSelected = GetNumSelectedMessages();
if (numSelected == 0)
return false; // Don't show the context menu if no items are selected.
var inThreadPane = popupNodeIsInThreadPane();
gContextMenu = new nsContextMenu(event.target);
- var selectedMessage = GetFirstSelectedMessage();
- var isNewsgroup = IsNewsMessage(selectedMessage);
+ var selectedMessage = gFolderDisplay.selectedMessage;
+ var isNewsgroup = gFolderDisplay.selectedMessageIsNews;
// Clear the global var used to keep track if a 'Delete Message' or 'Move
// To' command has been triggered via the thread pane context menu.
gThreadPaneDeleteOrMoveOccurred = false;
// Don't show mail items for links/images, just show related items.
var hideMailItems = !inThreadPane &&
(gContextMenu.onImage || gContextMenu.onLink);
@@ -227,21 +191,20 @@ function fillMailContextMenu(event)
ShowMenuItem("paneContext-afterMove", !inThreadPane);
ShowMenuItem("mailContext-tags", !hideMailItems && msgFolder);
ShowMenuItem("mailContext-mark", !hideMailItems && msgFolder);
setSingleSelection("mailContext-saveAs");
-#ifdef XP_MACOSX
- ShowMenuItem("mailContext-printpreview", false);
-#else
- setSingleSelection("mailContext-printpreview");
-#endif
+ if (gPlatformOSX)
+ ShowMenuItem("mailContext-printpreview", false);
+ else
+ setSingleSelection("mailContext-printpreview");
ShowMenuItem("mailContext-print", !hideMailItems);
ShowMenuItem("mailContext-delete", !hideMailItems && (isNewsgroup || canMove));
// This function is needed for the case where a folder is just loaded (while
// there isn't a message loaded in the message pane), a right-click is done
// in the thread pane. This function will disable enable the 'Delete
// Message' menu item.
@@ -390,21 +353,19 @@ function OpenMessageForMessageId(message
function OpenMessageByHeader(messageHeader, openInNewWindow)
{
var folder = messageHeader.folder;
var folderURI = folder.URI;
if (openInNewWindow)
{
- var messageURI = folder.getUriForMsg(messageHeader);
-
window.openDialog("chrome://messenger/content/messageWindow.xul",
"_blank", "all,chrome,dialog=no,status,toolbar",
- messageURI, folderURI, null);
+ messageHeader);
}
else
{
if (msgWindow.openFolder != folderURI)
gFolderTreeView.selectFolder(folder);
var tree = null;
var wintype = document.documentElement.getAttribute('windowtype');
@@ -728,17 +689,17 @@ function SetMenuItemLabel(id, label)
}
// helper function used by shouldShowSeparator
function hasAVisibleNextSibling(aNode)
{
var sibling = aNode.nextSibling;
while (sibling)
{
- if (sibling.getAttribute("hidden") != "true"
+ if (sibling.getAttribute("hidden") != "true"
&& sibling.localName != "menuseparator")
return true;
sibling = sibling.nextSibling;
}
return false;
}
function IsMenuItemShowing(menuID)
@@ -770,24 +731,24 @@ function composeEmailTo ()
params.type = Components.interfaces.nsIMsgCompType.New;
params.format = Components.interfaces.nsIMsgCompFormat.Default;
params.identity = accountManager.getFirstIdentityForServer(GetLoadedMsgFolder().server);
params.composeFields = fields;
msgComposeService.OpenComposeWindowWithParams(null, params);
}
// Extracts email address from url string
-function getEmail (url)
+function getEmail (url)
{
var qmark = url.indexOf( "?" );
var addresses;
- if ( qmark > mailtolength )
+ if ( qmark > mailtolength )
addresses = url.substring( mailtolength, qmark );
- else
+ else
addresses = url.substr( mailtolength );
// Let's try to unescape it using a character set
try {
var characterSet = gContextMenu.target.ownerDocument.characterSet;
const textToSubURI = Components.classes["@mozilla.org/intl/texttosuburi;1"]
.getService(Components.interfaces.nsITextToSubURI);
addresses = textToSubURI.unEscapeURIForUI(characterSet, addresses);
}
--- a/mail/base/content/mailWindow.js
+++ b/mail/base/content/mailWindow.js
@@ -1,47 +1,46 @@
-# -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is Mozilla Communicator client code, released
-# March 31, 1998.
-#
-# The Initial Developer of the Original Code is
-# Netscape Communications Corporation.
-# Portions created by the Initial Developer are Copyright (C) 1998-1999
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-# Jan Varga <varga@nixcorp.com>
-# HÃ¥kan Waara <hwaara@gmail.com>
-#
-# Alternatively, the contents of this file may be used under the terms of
-# either the GNU General Public License Version 2 or later (the "GPL"), or
-# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-# in which case the provisions of the GPL or the LGPL are applicable instead
-# of those above. If you wish to allow use of your version of this file only
-# under the terms of either the GPL or the LGPL, and not to allow others to
-# use your version of this file under the terms of the MPL, indicate your
-# decision by deleting the provisions above and replace them with the notice
-# and other provisions required by the GPL or the LGPL. If you do not delete
-# the provisions above, a recipient may use your version of this file under
-# the terms of any one of the MPL, the GPL or the LGPL.
-#
-# ***** END LICENSE BLOCK *****
+/** ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Communicator client code, released
+ * March 31, 1998.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1998-1999
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Jan Varga <varga@nixcorp.com>
+ * HÃ¥kan Waara <hwaara@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
Components.utils.import("resource://app/modules/appIdleManager.js");
//This file stores variables common to mail windows
var messenger;
var pref;
var statusFeedback;
var msgWindow;
@@ -54,40 +53,45 @@ var gBrandBundle;
Components.utils.import("resource://app/modules/gloda/log4moz.js");
var gContextMenu;
var gMailWindowLog = Log4Moz.getConfiguredLogger("mailWindow", Log4Moz.Level.Debug, Log4Moz.Level.Debug, Log4Moz.Level.Debug);
var gAccountCentralLoaded = true;
+/**
+ * Indicate whether we are running on Mac OS X. Our code is currently littered
+ * with #ifdef/#ifndef XP_MACOSX's that do not need to exist in js code. Use
+ * of preprocessing makes error line numbers misleading, complicates
+ * development because preprocessed files can't be symlinked when using
+ * --enable-chrome-format=symlink, etc.
+ */
+var gPlatformOSX =
+ (window.navigator.oscpu.substring(0, 3).toLowerCase() == "mac");
+
+/**
+ * Called by messageWindow.xul:onunload, the 'single message display window'.
+ *
+ * Also called by messenger.xul:onunload's (the 3-pane window inside of tabs
+ * window) unload function, OnUnloadMessenger.
+ */
function OnMailWindowUnload()
{
MailOfflineMgr.uninit();
ClearPendingReadTimer();
- var searchSession = GetSearchSession();
- if (searchSession)
- {
- removeGlobalListeners();
- if (gPreQuickSearchView) //close the cached pre quick search view
- gPreQuickSearchView.close();
- }
-
- var dbview = GetDBView();
- if (dbview) {
- dbview.close();
- }
+ // all dbview closing is handled by OnUnloadMessenger for the 3-pane (it closes
+ // the tabs which close their views) and OnUnloadMessageWindow for the
+ // standalone message window.
var mailSession = Components.classes["@mozilla.org/messenger/services/session;1"]
.getService(Components.interfaces.nsIMsgMailSession);
- mailSession.RemoveFolderListener(folderListener);
-
mailSession.RemoveMsgWindow(msgWindow);
- messenger.setWindow(null, null);
+ // the tabs have the FolderDisplayWidget close their 'messenger' instances for us
msgWindow.closeWindow();
window.MsgStatusFeedback.unload();
Components.classes["@mozilla.org/activity-manager;1"]
.getService(Components.interfaces.nsIActivityManager)
.removeListener(window.MsgStatusFeedback);
}
@@ -98,17 +102,17 @@ function CreateMailWindowGlobals()
messenger = Components.classes["@mozilla.org/messenger;1"]
.createInstance(Components.interfaces.nsIMessenger);
pref = Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefBranch2);
window.addEventListener("blur", appIdleManager.onBlur, false);
window.addEventListener("focus", appIdleManager.onFocus, false);
-
+
//Create windows status feedback
// set the JS implementation of status feedback before creating the c++ one..
window.MsgStatusFeedback = new nsMsgStatusFeedback();
// double register the status feedback object as the xul browser window implementation
window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIWebNavigation)
.QueryInterface(Components.interfaces.nsIDocShellTreeItem).treeOwner
.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
@@ -156,17 +160,17 @@ function InitMsgWindow()
}
// We're going to implement our status feedback for the mail window in JS now.
// the following contains the implementation of our status feedback object
function nsMsgStatusFeedback()
{
this._statusText = document.getElementById("statusText");
- this._progressBar = document.getElementById("statusbar-icon");
+ this._progressBar = document.getElementById("statusbar-icon");
this._progressBarContainer = document.getElementById("statusbar-progresspanel");
this._throbber = document.getElementById("navigator-throbber");
this._stopCmd = document.getElementById("cmd_stop");
this._activeProcesses = new Array();
}
nsMsgStatusFeedback.prototype =
{
@@ -351,17 +355,17 @@ nsMsgStatusFeedback.prototype =
this._progressBarContainer.removeAttribute('collapsed');
this._progressBarVisible = true;
}
}
else {
// Stop the bar spinning as we're not doing anything now.
this._progressBar.setAttribute("mode", "determined");
this._progressBar.value = 0;
- this._progressBar.label = "";
+ this._progressBar.label = "";
if (this._progressBarVisible) {
this._progressBarContainer.collapsed = true;
this._progressBarVisible = false;
}
}
},
@@ -437,25 +441,26 @@ nsMsgWindowCommands.prototype =
selectFolder: function(folderUri)
{
gFolderTreeView.selectFolder(GetMsgFolderFromUri(folderUri));
},
selectMessage: function(messageUri)
{
- SelectMessage(messageUri);
+ let msgHdr = messenger.msgHdrFromURI(messageUri);
+ gFolderDisplay.selectMessage(msgHdr);
},
clearMsgPane: function()
{
- if (gDBView)
- setTitleFromFolder(gDBView.msgFolder,null);
- else
- setTitleFromFolder(null,null);
+ // This call happens as part of a display decision made by the nsMsgDBView
+ // instance. Strictly speaking, we don't want this. I think davida's
+ // patch will change this, so we can figure it out after that lands if
+ // there are issues.
ClearMessagePane();
}
}
/**
* @returns the pref name to use for fetching the start page url. Every time the application version changes,
* return "mailnews.start_page.override_url". If this is the first time the application has been
* launched, return "mailnews.start_page.welcome_url". Otherwise return "mailnews.start_page.url".
@@ -475,17 +480,16 @@ function startPageUrlPref()
}
/**
* Loads the mail start page.
*/
function loadStartPage()
{
gMessageNotificationBar.clearMsgNotifications();
- ClearThreadPaneSelection();
let startpage = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
.getService(Components.interfaces.nsIURLFormatter)
.formatURLPref(startPageUrlPref());
if (startpage)
{
try {
let urifixup = Components.classes["@mozilla.org/docshell/urifixup;1"]
.getService(Components.interfaces.nsIURIFixup);
@@ -498,67 +502,50 @@ function loadStartPage()
}
}
else
{
GetMessagePaneFrame().location.href = "about:blank";
}
}
-// When the ThreadPane is hidden via the displayDeck, we should collapse the
-// elements that are only meaningful to the thread pane. When AccountCentral is
-// shown via the displayDeck, we need to switch the displayDeck to show the
-// accountCentralBox, and load the iframe in the AccountCentral box with
-// corresponding page.
-function ShowAccountCentral()
-{
- var accountBox = document.getElementById("accountCentralBox");
- document.getElementById("displayDeck").selectedPanel = accountBox;
- var prefName = "mailnews.account_central_page.url";
- var acctCentralPage = pref.getComplexValue(prefName,
- Components.interfaces.nsIPrefLocalizedString).data;
- window.frames["accountCentralPane"].location.href = acctCentralPage;
-}
-
-function ShowThreadPane()
-{
- document.getElementById("displayDeck").selectedPanel =
- document.getElementById("threadPaneBox");
-}
-
function ShowingThreadPane()
{
var threadPaneSplitter = document.getElementById("threadpane-splitter");
threadPaneSplitter.collapsed = false;
GetMessagePane().collapsed = (threadPaneSplitter.getAttribute("state") == "collapsed");
// XXX We need to force the tree to refresh its new height
// so that it will correctly scroll to the newest message
GetThreadTree().boxObject.height;
document.getElementById("key_toggleMessagePane").removeAttribute("disabled");
}
function HidingThreadPane()
{
- ClearThreadPane();
GetUnreadCountElement().hidden = true;
GetTotalCountElement().hidden = true;
GetMessagePane().collapsed = true;
document.getElementById("threadpane-splitter").collapsed = true;
document.getElementById("key_toggleMessagePane").setAttribute("disabled", "true");
}
// The zoom manager, view source and possibly some other functions still rely
// on the getBrowser function.
function getBrowser()
{
let tabmail = document.getElementById('tabmail');
return tabmail ? tabmail.getBrowserForSelectedTab() :
document.getElementById("messagepane");
}
+// When the ThreadPane is hidden via the displayDeck, we should collapse the
+// elements that are only meaningful to the thread pane. When AccountCentral is
+// shown via the displayDeck, we need to switch the displayDeck to show the
+// accountCentralBox, and load the iframe in the AccountCentral box with
+// corresponding page.
var gCurrentDisplayDeckId = "";
function ObserveDisplayDeckChange(event)
{
var selectedPanel = document.getElementById("displayDeck").selectedPanel;
var nowSelected = selectedPanel ? selectedPanel.id : null;
// onselect fires for every mouse click inside the deck, so ObserveDisplayDeckChange is getting called every time we click
// on a message in the thread pane. Only show / Hide elements if the selected deck is actually changing.
if (nowSelected != gCurrentDisplayDeckId)
@@ -579,33 +566,24 @@ function ObserveDisplayDeckChange(event)
}
}
// Given the server, open the twisty and the set the selection
// on inbox of that server.
// prompt if offline.
function OpenInboxForServer(server)
{
- ShowThreadPane();
gFolderTreeView.selectFolder(GetInboxFolder(server));
if (MailOfflineMgr.isOnline() || MailOfflineMgr.getNewMail()) {
if (server.type != "imap")
GetMessagesForInboxOnServer(server);
}
}
-function GetSearchSession()
-{
- if (("gSearchSession" in top) && gSearchSession)
- return gSearchSession;
- else
- return null;
-}
-
/** Update state of zoom type (text vs. full) menu item. */
function UpdateFullZoomMenu() {
var menuItem = document.getElementById("menu_fullZoomToggle");
menuItem.setAttribute("checked", !ZoomManager.useFullZoom);
}
/**
* This class implements nsIBadCertListener2. Its job is to prevent "bad cert"
--- a/mail/base/content/mailWindowOverlay.js
+++ b/mail/base/content/mailWindowOverlay.js
@@ -1,55 +1,54 @@
-# -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is Mozilla Communicator client code, released
-# March 31, 1998.
-#
-# The Initial Developer of the Original Code is
-# Netscape Communications Corporation.
-# Portions created by the Initial Developer are Copyright (C) 1998-1999
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-# timeless
-# slucy@objectivesw.co.uk
-# HÃ¥kan Waara <hwaara@chello.se>
-# Jan Varga <varga@nixcorp.com>
-# Seth Spitzer <sspitzer@netscape.com>
-# David Bienvenu <bienvenu@nventure.com>
-# Karsten Düsterloh <mnyromyr@tprac.de>
-# Christopher Thomas <cst@yecc.com>
-# Jeremy Morton <bugzilla@game-point.net>
-# Andrew Sutherland <asutherland@asutherland.org>
-#
-# Alternatively, the contents of this file may be used under the terms of
-# either the GNU General Public License Version 2 or later (the "GPL"), or
-# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-# in which case the provisions of the GPL or the LGPL are applicable instead
-# of those above. If you wish to allow use of your version of this file only
-# under the terms of either the GPL or the LGPL, and not to allow others to
-# use your version of this file under the terms of the MPL, indicate your
-# decision by deleting the provisions above and replace them with the notice
-# and other provisions required by the GPL or the LGPL. If you do not delete
-# the provisions above, a recipient may use your version of this file under
-# the terms of any one of the MPL, the GPL or the LGPL.
-#
-# ***** END LICENSE BLOCK *****
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Communicator client code, released
+ * March 31, 1998.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1998-1999
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * timeless
+ * slucy@objectivesw.co.uk
+ * HÃ¥kan Waara <hwaara@chello.se>
+ * Jan Varga <varga@nixcorp.com>
+ * Seth Spitzer <sspitzer@netscape.com>
+ * David Bienvenu <bienvenu@nventure.com>
+ * Karsten Düsterloh <mnyromyr@tprac.de>
+ * Christopher Thomas <cst@yecc.com>
+ * Jeremy Morton <bugzilla@game-point.net>
+ * Andrew Sutherland <asutherland@asutherland.org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
const ADDR_DB_LARGE_COMMIT = 1;
const kClassicMailLayout = 0;
const kWideMailLayout = 1;
const kVerticalMailLayout = 2;
// Per message header flags to keep track of whether the user is allowing remote
@@ -153,17 +152,17 @@ function InitEditMessagesMenu()
function InitGoMessagesMenu()
{
document.commandDispatcher.updateCommands('create-menu-go');
}
function view_init()
{
- var isFeed = IsFeedItem();
+ var isFeed = gFolderDisplay.selectedMessageIsFeed;
if (!gMessengerBundle)
gMessengerBundle = document.getElementById("bundle_messenger");
var messagePaneMenuItem = document.getElementById("menu_showMessage");
if (!messagePaneMenuItem.hidden) { // Hidden in the standalone msg window.
messagePaneMenuItem.setAttribute("checked", !IsMessagePaneCollapsed());
messagePaneMenuItem.disabled = gAccountCentralLoaded;
@@ -232,105 +231,106 @@ function InitViewFolderViewsMenu(event)
function setSortByMenuItemCheckState(id, value)
{
var menuitem = document.getElementById(id);
if (menuitem)
menuitem.setAttribute("checked", value);
}
+/**
+ * Called when showing the menu_viewSortPopup menupopup, so it should always
+ * be up-to-date.
+ */
function InitViewSortByMenu()
{
- var sortType = gDBView.sortType;
+ var sortType = gFolderDisplay.view.primarySortType;
setSortByMenuItemCheckState("sortByDateMenuitem", (sortType == nsMsgViewSortType.byDate));
setSortByMenuItemCheckState("sortByReceivedMenuitem", (sortType == nsMsgViewSortType.byReceived));
setSortByMenuItemCheckState("sortByFlagMenuitem", (sortType == nsMsgViewSortType.byFlagged));
setSortByMenuItemCheckState("sortByOrderReceivedMenuitem", (sortType == nsMsgViewSortType.byId));
setSortByMenuItemCheckState("sortByPriorityMenuitem", (sortType == nsMsgViewSortType.byPriority));
setSortByMenuItemCheckState("sortBySizeMenuitem", (sortType == nsMsgViewSortType.bySize));
setSortByMenuItemCheckState("sortByStatusMenuitem", (sortType == nsMsgViewSortType.byStatus));
setSortByMenuItemCheckState("sortBySubjectMenuitem", (sortType == nsMsgViewSortType.bySubject));
setSortByMenuItemCheckState("sortByUnreadMenuitem", (sortType == nsMsgViewSortType.byUnread));
setSortByMenuItemCheckState("sortByTagsMenuitem", (sortType == nsMsgViewSortType.byTags));
setSortByMenuItemCheckState("sortByJunkStatusMenuitem", (sortType == nsMsgViewSortType.byJunkStatus));
setSortByMenuItemCheckState("sortByFromMenuitem", (sortType == nsMsgViewSortType.byAuthor));
setSortByMenuItemCheckState("sortByRecipientMenuitem", (sortType == nsMsgViewSortType.byRecipient));
setSortByMenuItemCheckState("sortByAttachmentsMenuitem", (sortType == nsMsgViewSortType.byAttachments));
- var sortOrder = gDBView.sortOrder;
+ var sortOrder = gFolderDisplay.view.primarySortOrder;
var sortTypeSupportsGrouping = (sortType == nsMsgViewSortType.byAuthor ||
sortType == nsMsgViewSortType.byDate || sortType == nsMsgViewSortType.byReceived ||
sortType == nsMsgViewSortType.byPriority ||
sortType == nsMsgViewSortType.bySubject || sortType == nsMsgViewSortType.byTags ||
sortType == nsMsgViewSortType.byRecipient || sortType == nsMsgViewSortType.byAccount ||
sortType == nsMsgViewSortType.byStatus || sortType == nsMsgViewSortType.byFlagged ||
sortType == nsMsgViewSortType.byAttachments);
setSortByMenuItemCheckState("sortAscending", (sortOrder == nsMsgViewSortOrder.ascending));
setSortByMenuItemCheckState("sortDescending", (sortOrder == nsMsgViewSortOrder.descending));
- var grouped = ((gDBView.viewFlags & nsMsgViewFlagsType.kGroupBySort) != 0);
- var threaded = ((gDBView.viewFlags & nsMsgViewFlagsType.kThreadedDisplay) != 0 && !grouped);
+ var grouped = gFolderDisplay.view.showGroupedBySort;
+ var threaded = gFolderDisplay.view.showThreaded;
var sortThreadedMenuItem = document.getElementById("sortThreaded");
var sortUnthreadedMenuItem = document.getElementById("sortUnthreaded");
sortThreadedMenuItem.setAttribute("checked", threaded);
sortUnthreadedMenuItem.setAttribute("checked", !threaded && !grouped);
var groupBySortOrderMenuItem = document.getElementById("groupBySort");
groupBySortOrderMenuItem.setAttribute("disabled", !sortTypeSupportsGrouping);
groupBySortOrderMenuItem.setAttribute("checked", grouped);
}
function InitViewMessagesMenu()
{
- var viewFlags = (gDBView) ? gDBView.viewFlags : 0;
- var viewType = (gDBView) ? gDBView.viewType : 0;
-
document.getElementById("viewAllMessagesMenuItem").setAttribute("checked",
- (viewFlags & nsMsgViewFlagsType.kUnreadOnly) == 0 &&
- (viewType == nsMsgViewType.eShowAllThreads));
+ !gFolderDisplay.view.showUnreadOnly &&
+ !gFolderDisplay.view.specialView);
document.getElementById("viewUnreadMessagesMenuItem").setAttribute("checked",
- (viewFlags & nsMsgViewFlagsType.kUnreadOnly) != 0);
+ gFolderDisplay.view.showUnreadOnly);
document.getElementById("viewThreadsWithUnreadMenuItem").setAttribute("checked",
- viewType == nsMsgViewType.eShowThreadsWithUnread);
+ gFolderDisplay.view.specialViewThreadsWithUnread);
document.getElementById("viewWatchedThreadsWithUnreadMenuItem").setAttribute("checked",
- viewType == nsMsgViewType.eShowWatchedThreadsWithUnread);
+ gFolderDisplay.view.specialViewWatchedThreadsWithUnread);
document.getElementById("viewIgnoredThreadsMenuItem").setAttribute("checked",
- (viewFlags & nsMsgViewFlagsType.kShowIgnored) != 0);
+ gFolderDisplay.view.showIgnored);
}
function InitMessageMenu()
{
- var selectedMsg = GetFirstSelectedMessage();
- var isNews = IsNewsMessage(selectedMsg);
- var isFeed = IsFeedItem();
+ var selectedMsg = gFolderDisplay.selectedMessage;
+ var isNews = gFolderDisplay.selectedMessageIsNews;
+ var isFeed = gFolderDisplay.selectedMessageIsFeed;
// We show reply to Newsgroups only for news messages.
document.getElementById("replyNewsgroupMainMenu").hidden = !isNews;
// For mail messages we say reply. For news we say ReplyToSender.
document.getElementById("replyMainMenu").hidden = isNews;
document.getElementById("replySenderMainMenu").hidden = !isNews;
// We only kill and watch threads for news.
document.getElementById("threadItemsSeparator").hidden = !isNews;
document.getElementById("killThread").hidden = !isNews;
document.getElementById("killSubthread").hidden = !isNews;
document.getElementById("watchThread").hidden = !isNews;
// Disable the move and copy menus if there are no messages selected.
// Disable the move menu if we can't delete msgs from the folder.
- var msgFolder = GetLoadedMsgFolder();
+ var msgFolder = gFolderDisplay.displayedFolder;
var enableMenuItem = selectedMsg && msgFolder && msgFolder.canDeleteMessages;
document.getElementById("moveMenu").disabled = !enableMenuItem;
// Also disable copy when no folder is loaded (like for .eml files).
document.getElementById("copyMenu").disabled = !(selectedMsg && msgFolder);
initMoveToFolderAgainMenu(document.getElementById("moveToFolderAgain"));
@@ -411,17 +411,17 @@ function InitViewHeadersMenu()
menuitem.setAttribute("checked", "true");
}
function InitViewBodyMenu()
{
var html_as = 0;
var prefer_plaintext = false;
var disallow_classes = 0;
- var isFeed = IsFeedItem();
+ var isFeed = gFolderDisplay.selectedMessageIsFeed;
const defaultIDs = ["bodyAllowHTML",
"bodySanitized",
"bodyAsPlaintext"];
const rssIDs = ["bodyFeedSummaryAllowHTML",
"bodyFeedSummarySanitized",
"bodyFeedSummaryAsPlaintext"];
var menuIDs = isFeed ? rssIDs : defaultIDs;
try
@@ -465,45 +465,27 @@ function InitViewBodyMenu()
if (isFeed) {
AllowHTML_menuitem.hidden = !gShowFeedSummary;
Sanitized_menuitem.hidden = !gShowFeedSummary;
AsPlaintext_menuitem.hidden = !gShowFeedSummary;
document.getElementById("viewFeedSummarySeparator").hidden = !gShowFeedSummary;
}
}
-function IsNewsMessage(messageUri)
-{
- return (/^news-message:/.test(messageUri));
-}
-
-function IsImapMessage(messageUri)
-{
- return (/^imap-message:/.test(messageUri));
-}
-
-function IsFeedItem()
-{
- return (GetFirstSelectedMessage() &&
- ((gMsgFolderSelected &&
- gMsgFolderSelected.server.type == 'rss') ||
- 'content-base' in currentHeaderData));
-}
-
function SetMenuItemLabel(menuItemId, customLabel)
{
var menuItem = document.getElementById(menuItemId);
if (menuItem)
menuItem.setAttribute('label', customLabel);
}
function RemoveAllMessageTags()
{
- var selectedMsgUris = GetSelectedMessages();
- if (!selectedMsgUris.length)
+ var selectedMessages = gFolderDisplay.selectedMessages;
+ if (!selectedMessages.length)
return;
var messages = Components.classes["@mozilla.org/array;1"]
.createInstance(Components.interfaces.nsIMutableArray);
var tagService = Components.classes["@mozilla.org/messenger/tagservice;1"]
.getService(Components.interfaces.nsIMsgTagService);
var tagArray = tagService.getAllTags({});
@@ -518,19 +500,19 @@ function RemoveAllMessageTags()
var prevHdrFolder = null;
// this crudely handles cross-folder virtual folders with selected messages
// that spans folders, by coalescing consecutive messages in the selection
// that happen to be in the same folder. nsMsgSearchDBView does this better,
// but nsIMsgDBView doesn't handle commands with arguments, and untag takes a
// key argument. Furthermore, we only delete legacy labels and known tags,
// keeping other keywords like (non)junk intact.
- for (var i = 0; i < selectedMsgUris.length; ++i)
+ for (var i = 0; i < selectedMessages.length; ++i)
{
- var msgHdr = messenger.msgHdrFromURI(selectedMsgUris[i]);
+ var msgHdr = selectedMessages[i];
msgHdr.label = 0; // remove legacy label
if (prevHdrFolder != msgHdr.folder)
{
if (prevHdrFolder)
prevHdrFolder.removeKeywordsFromMessages(messages, allKeys);
messages.clear();
prevHdrFolder = msgHdr.folder;
}
@@ -542,17 +524,17 @@ function RemoveAllMessageTags()
}
function ToggleMessageTagKey(index)
{
if (GetNumSelectedMessages() < 1)
return;
// set the tag state based upon that of the first selected message,
// just like we do for markAsRead etc.
- var msgHdr = gDBView.hdrForFirstSelectedMessage;
+ var msgHdr = gFolderDisplay.selectedMessage;
var tagService = Components.classes["@mozilla.org/messenger/tagservice;1"]
.getService(Components.interfaces.nsIMsgTagService);
var tagArray = tagService.getAllTags({});
for (var i = 0; i < tagArray.length; ++i)
{
var key = tagArray[i].key;
if (!--index)
{
@@ -575,27 +557,27 @@ function ToggleMessageTagMenu(target)
}
function ToggleMessageTag(key, addKey)
{
var messages = Components.classes["@mozilla.org/array;1"]
.createInstance(Components.interfaces.nsIMutableArray);
var msg = Components.classes["@mozilla.org/array;1"]
.createInstance(Components.interfaces.nsIMutableArray);
- var selectedMsgUris = GetSelectedMessages();
+ var selectedMessages = gFolderDisplay.selectedMessages;
var toggler = addKey ? "addKeywordsToMessages" : "removeKeywordsFromMessages";
var prevHdrFolder = null;
// this crudely handles cross-folder virtual folders with selected messages
// that spans folders, by coalescing consecutive msgs in the selection
// that happen to be in the same folder. nsMsgSearchDBView does this
// better, but nsIMsgDBView doesn't handle commands with arguments,
// and (un)tag takes a key argument.
- for (var i = 0; i < selectedMsgUris.length; ++i)
+ for (var i = 0; i < selectedMessages.length; ++i)
{
- var msgHdr = messenger.msgHdrFromURI(selectedMsgUris[i]);
+ var msgHdr = selectedMessages[i];
if (msgHdr.label)
{
// Since we touch all these messages anyway, migrate the label now.
// If we don't, the thread tree won't always show the correct tag state,
// because resetting a label doesn't update the tree anymore...
msg.clear();
msg.appendElement(msgHdr, false);
msgHdr.folder.addKeywordsToMessages(msg, "$label" + msgHdr.label);
@@ -643,17 +625,17 @@ function AddTagCallback(name, color)
function SetMessageTagLabel(menuitem, index, name)
{
// if a <key> is defined for this tag, use its key as the accesskey
// (the key for the tag at index n needs to have the id key_tag<n>)
var shortcutkey = document.getElementById("key_tag" + index);
var accesskey = shortcutkey ? shortcutkey.getAttribute("key") : "";
if (accesskey)
menuitem.setAttribute("accesskey", accesskey);
- var label = gMessengerBundle.getFormattedString("mailnews.tags.format",
+ var label = gMessengerBundle.getFormattedString("mailnews.tags.format",
[accesskey, name]);
menuitem.setAttribute("label", label);
}
function InitMessageTags(menuPopup)
{
var tagService = Components.classes["@mozilla.org/messenger/tagservice;1"]
.getService(Components.interfaces.nsIMsgTagService);
@@ -668,17 +650,17 @@ function InitMessageTags(menuPopup)
// hide double menuseparator
menuseparator.previousSibling.hidden = !tagCount;
// create label and accesskey for the static remove item
var tagRemoveLabel = gMessengerBundle.getString("mailnews.tags.remove");
SetMessageTagLabel(menuPopup.firstChild, 0, tagRemoveLabel);
// now rebuild the list
- var msgHdr = gDBView.hdrForFirstSelectedMessage;
+ var msgHdr = gFolderDisplay.selectedMessage;
var curKeys = msgHdr.getStringProperty("keywords");
if (msgHdr.label)
curKeys += " $label" + msgHdr.label;
for (var i = 0; i < tagCount; ++i)
{
var taginfo = tagArray[i];
// TODO we want to either remove or "check" the tags that already exist
@@ -686,17 +668,17 @@ function InitMessageTags(menuPopup)
SetMessageTagLabel(newMenuItem, i + 1, taginfo.tag);
newMenuItem.setAttribute("value", taginfo.key);
newMenuItem.setAttribute("type", "checkbox");
var removeKey = (" " + curKeys + " ").indexOf(" " + taginfo.key + " ") > -1;
newMenuItem.setAttribute('checked', removeKey);
newMenuItem.setAttribute('oncommand', 'ToggleMessageTagMenu(event.target);');
var color = taginfo.color;
if (color)
- newMenuItem.setAttribute("class", "lc-" + color.substr(1));
+ newMenuItem.setAttribute("class", "lc-" + color.substr(1));
menuPopup.insertBefore(newMenuItem, menuseparator);
}
}
function backToolbarMenu_init(menuPopup)
{
populateHistoryMenu(menuPopup, true);
}
@@ -722,38 +704,43 @@ function populateHistoryMenu(menuPopup,
var numEntries = new Object;
var historyEntries = new Object;
messenger.getNavigateHistory(curPos, numEntries, historyEntries);
curPos.value = curPos.value * 2;
navDebug("curPos = " + curPos.value + " numEntries = " + numEntries.value + "\n");
var historyArray = historyEntries.value;
var folder;
var newMenuItem;
- if (GetLoadedMessage())
+ if (gFolderDisplay.selectedMessage)
{
if (!isBackMenu)
curPos.value += 2;
else
curPos.value -= 2;
}
// For populating the back menu, we want the most recently visited
// messages first in the menu. So we go backward from curPos to 0.
// For the forward menu, we want to go forward from curPos to the end.
var relPos = 0;
for (var i = curPos.value; (isBackMenu) ? i >= 0 : i < historyArray.length; i += ((isBackMenu) ? -2 : 2))
{
navDebug("history[" + i + "] = " + historyArray[i] + "\n");
navDebug("history[" + i + "] = " + historyArray[i + 1] + "\n");
- folder = GetMsgFolderFromUri(historyArray[i + 1])
+ folder = GetMsgFolderFromUri(historyArray[i + 1]);
navDebug("folder URI = " + folder.URI + "pretty name " + folder.prettyName + "\n");
var menuText = "";
+ // If the message was not being displayed via the current folder, prepend
+ // the folder name. We do not need to check underlying folders for
+ // virtual folders because 'folder' is the display folder, not the
+ // underlying one.
+ if (folder != gFolderDisplay.displayedFolder)
+ menuText = folder.prettyName + " - ";
+
var msgHdr = messenger.msgHdrFromURI(historyArray[i]);
- if (!IsCurrentLoadedFolder(folder))
- menuText = folder.prettyName + " - ";
var subject = "";
if (msgHdr.flags & Components.interfaces.nsMsgMessageFlags.HasRe)
subject = "Re: ";
if (msgHdr.mime2DecodedSubject)
subject += msgHdr.mime2DecodedSubject;
if (subject)
menuText += subject + " - ";
@@ -766,27 +753,38 @@ function populateHistoryMenu(menuPopup,
newMenuItem.folder = folder;
newMenuItem.setAttribute('oncommand', 'NavigateToUri(event.target); event.stopPropagation();');
menuPopup.appendChild(newMenuItem);
if (! (relPos % 20))
break;
}
}
+/**
+ * This is triggered by the history navigation menu options, as created by
+ * populateHistoryMenu above.
+ */
function NavigateToUri(target)
{
var historyIndex = target.getAttribute('value');
var msgUri = messenger.getMsgUriAtNavigatePos(historyIndex);
var folder = target.folder;
var msgHdr = messenger.msgHdrFromURI(msgUri);
navDebug("navigating from " + messenger.navigatePos + " by " + historyIndex + " to " + msgUri + "\n");
// this "- 0" seems to ensure that historyIndex is treated as an int, not a string.
messenger.navigatePos += (historyIndex - 0);
- LoadNavigatedToMessage(msgHdr, folder, folder.URI);
+
+ if (gFolderDisplay.displayedFolder != folder) {
+ if (gFolderTreeView)
+ gFolderTreeView.selectFolder(folder);
+ else
+ gFolderDisplay.show(folder);
+ }
+ gFolderDisplay.selectMessage(msgHdr);
}
function forwardToolbarMenu_init(menuPopup)
{
populateHistoryMenu(menuPopup, false);
}
function InitMessageMark()
@@ -804,33 +802,33 @@ function UpdateJunkToolbarButton()
{
var junkButtonDeck = document.getElementById("junk-deck");
if (junkButtonDeck)
junkButtonDeck.selectedIndex = SelectedMessagesAreJunk() ? 1 : 0;
}
function UpdateReplyButtons()
{
- let msgHdr = messenger.msgHdrFromURI(GetLoadedMessage());
+ let msgHdr = gFolderDisplay.selectedMessage;
let myEmail = getIdentityForHeader(msgHdr).email;
let recipients = msgHdr.recipients + "," + msgHdr.ccList;
// If my email address isn't in the to or cc list, then I've been bcc-ed.
let imBcced = recipients.indexOf(myEmail) == -1;
// Now, let's get the number of unique recipients
let hdrParser = Components.classes["@mozilla.org/messenger/headerparser;1"]
.getService(Components.interfaces.nsIMsgHeaderParser);
let uniqueRecipients = hdrParser.removeDuplicateAddresses(recipients, {});
let numAddresses = hdrParser.parseHeadersWithArray(uniqueRecipients, {}, {}, {});
// If I've been bcc-ed, then add 1 to the number of addresses to compensate.
if (imBcced)
- numAddresses++
+ numAddresses++;
// By default, ReplyAll if there is more than 1 person to reply to.
let showReplyAll = numAddresses > 1;
// And ReplyToList if there is a List-Post header.
let showReplyList = currentHeaderData["list-post"];
// Get the server type.
@@ -913,57 +911,57 @@ function UpdateDeleteToolbarButton()
GetNumSelectedMessages() == 0)
deleteButtonDeck.selectedIndex = 0;
else
deleteButtonDeck.selectedIndex = SelectedMessagesAreDeleted() ? 1 : 0;
}
function UpdateDeleteCommand()
{
var value = "value";
- var uri = GetFirstSelectedMessage();
- if (IsNewsMessage(uri))
+ if (gFolderDisplay.selectedMessageIsNews)
value += "News";
else if (SelectedMessagesAreDeleted())
value += "IMAPDeleted";
if (GetNumSelectedMessages() < 2)
value += "Message";
else
value += "Messages";
goSetMenuValue("cmd_delete", value);
goSetAccessKey("cmd_delete", value + "AccessKey");
}
function SelectedMessagesAreDeleted()
{
- return gDBView && gDBView.numSelected &&
- (gDBView.hdrForFirstSelectedMessage.flags &
+ let firstSelectedMessage = gFolderDisplay.selectedMessage;
+ return firstSelectedMessage &&
+ (firstSelectedMessage.flags &
Components.interfaces.nsMsgMessageFlags.IMAPDeleted);
}
function SelectedMessagesAreJunk()
{
try {
- var junkScore = gDBView.hdrForFirstSelectedMessage.getStringProperty("junkscore");
+ var junkScore = gFolderDisplay.selectedMessage.getStringProperty("junkscore");
return (junkScore != "") && (junkScore != "0");
}
catch (ex) {
return false;
}
}
function SelectedMessagesAreRead()
{
- return gDBView && gDBView.numSelected &&
- gDBView.hdrForFirstSelectedMessage.isRead;
+ let firstSelectedMessage = gFolderDisplay.selectedMessage;
+ return firstSelectedMessage && firstSelectedMessage.isRead;
}
function SelectedMessagesAreFlagged()
{
- return gDBView && gDBView.numSelected &&
- gDBView.hdrForFirstSelectedMessage.isFlagged;
+ let firstSelectedMessage = gFolderDisplay.selectedMessage;
+ return firstSelectedMessage && firstSelectedMessage.isFlagged;
}
function GetFirstSelectedMsgFolder()
{
var selectedFolders = GetSelectedMsgFolders();
return (selectedFolders.length > 0) ? selectedFolders[0] : null;
}
@@ -1092,20 +1090,20 @@ function MsgGetNextNMessages()
if (MailOfflineMgr.isOnline() || MailOfflineMgr.getNewMail())
GetNextNMessages(GetFirstSelectedMsgFolder());
}
function MsgDeleteMessage(reallyDelete, fromToolbar)
{
// If from the toolbar, return right away if this is a news message
// only allow cancel from the menu: "Edit | Cancel / Delete Message".
- if (fromToolbar && isNewsURI(GetLoadedMsgFolder().URI))
+ if (fromToolbar && gFolderDisplay.view.isNewsFolder)
return;
- SetNextMessageAfterDelete();
+ gFolderDisplay.hintAboutToDeleteMessages();
if (reallyDelete)
gDBView.doCommand(nsMsgViewCommandType.deleteNoTrash);
else
gDBView.doCommand(nsMsgViewCommandType.deleteMsg);
}
/**
* Copies the selected messages to the destination folder
@@ -1124,17 +1122,17 @@ function MsgCopyMessage(aDestFolder)
*/
function MsgMoveMessage(aDestFolder)
{
// We don't move news messages, we copy them.
if (isNewsURI(gDBView.msgFolder.URI))
gDBView.doCommandWithFolder(nsMsgViewCommandType.copyMessages, aDestFolder);
else
{
- SetNextMessageAfterDelete();
+ gFolderDisplay.hintAboutToDeleteMessages();
gDBView.doCommandWithFolder(nsMsgViewCommandType.moveMessages, aDestFolder);
}
pref.setCharPref("mail.last_msg_movecopy_target_uri", aDestFolder.URI);
pref.setBoolPref("mail.last_msg_movecopy_was_move", true);
}
/**
* Calls the ComposeMessage function with the desired type, and proper default
@@ -1142,32 +1140,34 @@ function MsgMoveMessage(aDestFolder)
*
* @param aCompType the nsIMsgCompType to pass to the function
* @param aEvent (optional) the event that triggered the call
*/
function composeMsgByType(aCompType, aEvent) {
if (aEvent && aEvent.shiftKey) {
ComposeMessage(aCompType,
Components.interfaces.nsIMsgCompFormat.OppositeOfDefault,
- GetFirstSelectedMsgFolder(), GetSelectedMessages());
+ GetFirstSelectedMsgFolder(),
+ gFolderDisplay.selectedMessageUris);
}
else {
ComposeMessage(aCompType, Components.interfaces.nsIMsgCompFormat.Default,
- GetFirstSelectedMsgFolder(), GetSelectedMessages());
+ GetFirstSelectedMsgFolder(),
+ gFolderDisplay.selectedMessageUris);
}
}
function MsgNewMessage(event)
{
composeMsgByType(Components.interfaces.nsIMsgCompType.New, event);
}
function MsgReplyMessage(event)
{
- var loadedFolder = GetLoadedMsgFolder();
+ var loadedFolder = gFolderDisplay.displayedFolder;
if (loadedFolder)
{
var server = loadedFolder.server;
if(server && server.type == "nntp")
{
MsgReplyGroup(event);
return;
}
@@ -1202,50 +1202,47 @@ function BatchMessageMover()
this._batches = {};
this._currentKey = null;
}
BatchMessageMover.prototype = {
archiveSelectedMessages: function()
{
- // subtle:
- SetNextMessageAfterDelete(); // we're just pretending
- this.messageToSelectAfterWereDone = gNextMessageViewIndexAfterDelete;
- gNextMessageViewIndexAfterDelete = -2;
-
- let selectedMsgUris = GetSelectedMessages();
- if (!selectedMsgUris.length)
+ gFolderDisplay.hintMassMoveStarting();
+
+ let selectedMessages = gFolderDisplay.selectedMessages;
+ if (!selectedMessages.length)
return;
-
+
let messages = Components.classes["@mozilla.org/array;1"]
.createInstance(Components.interfaces.nsIMutableArray);
-
- for (let i = 0; i < selectedMsgUris.length; ++i)
+
+ for (let i = 0; i < selectedMessages.length; ++i)
{
- let msgHdr = messenger.msgHdrFromURI(selectedMsgUris[i]);
-
+ let msgHdr = selectedMessages[i];
+
let rootFolder = msgHdr.folder.server.rootFolder;
-
+
let msgDate = new Date(msgHdr.date / 1000); // convert date to JS date object
let msgYear = msgDate.getFullYear().toString();
let monthFolderName = msgDate.toLocaleFormat("%Y-%m")
let dstFolderName = monthFolderName;
let copyBatchKey = msgHdr.folder.URI + '\000' + dstFolderName;
if (! (copyBatchKey in this._batches)) {
this._batches[copyBatchKey] = [msgHdr.folder, msgYear, dstFolderName];
}
this._batches[copyBatchKey].push(msgHdr);
}
// Now we launch the code that will iterate over all of the message copies
// one in turn
this.processNextBatch();
},
-
+
processNextBatch: function()
{
for (let key in this._batches)
{
this._currentKey = key;
let batch = this._batches[key];
let srcFolder = batch[0];
let msgYear = batch[1];
@@ -1256,18 +1253,18 @@ BatchMessageMover.prototype = {
// rss servers don't have an identity so we special case the archives URI
let archiveFolderUri = (srcFolder.server.type == 'rss')
? srcFolder.server.serverURI + "/Archives"
: getIdentityForHeader(msgs[0], Ci.nsIMsgCompType
.ReplyAll).archiveFolder;
let archiveFolder = GetMsgFolderFromUri(archiveFolderUri, false);
let granularity = archiveFolder.server.archiveGranularity;
- // for imap folders, we need to create the sub-folders asynchronously,
- // so we chain the urls using the listener called back from
+ // for imap folders, we need to create the sub-folders asynchronously,
+ // so we chain the urls using the listener called back from
// createStorageIfMissing. For local, creatStorageIfMissing is
// synchronous.
let isImap = archiveFolder.server.type == "imap";
if (!archiveFolder.parent) {
archiveFolder.createStorageIfMissing(this);
if (isImap)
return;
}
@@ -1349,28 +1346,20 @@ BatchMessageMover.prototype = {
this._currentKey = null;
// is there a safe way to test whether this._batches is empty?
let empty = true;
for (let key in this._batches) {
empty = false;
}
- if (empty)
- {
- // we're just going to select the message now
- var treeView = gDBView.QueryInterface(Components.interfaces.nsITreeView);
- var treeSelection = treeView.selection;
- treeSelection.select(this.messageToSelectAfterWereDone);
- treeView.selectionChanged();
- }
- else
- {
+ if (!empty)
this.processNextBatch();
- }
+ else // this will select the appropriate next message
+ gFolderDisplay.hintMassMoveCompleted();
}
},
QueryInterface: function(iid) {
if (!iid.equals(Components.interfaces.nsIUrlListener) &&
!iid.equals(Components.interfaces.nsIMsgCopyServiceListener) &&
!iid.equals(Components.interfaces.nsISupports))
throw Components.results.NS_ERROR_NO_INTERFACE;
return this;
@@ -1415,28 +1404,26 @@ function MsgForwardAsInline(event)
function MsgEditMessageAsNew()
{
composeMsgByType(Components.interfaces.nsIMsgCompType.Template);
}
function MsgComposeDraftMessage()
{
- var loadedFolder = GetLoadedMsgFolder();
- var messageArray = GetSelectedMessages();
-
ComposeMessage(Components.interfaces.nsIMsgCompType.Draft,
Components.interfaces.nsIMsgCompFormat.Default,
- loadedFolder, messageArray);
+ gFolderDisplay.displayedFolder,
+ gFolderDisplay.selectedMessageUris);
}
function MsgCreateFilter()
{
// retrieve Sender direct from selected message's headers
- var msgHdr = gDBView.hdrForFirstSelectedMessage;
+ var msgHdr = gFolderDisplay.selectedMessage;
var headerParser = Components.classes["@mozilla.org/messenger/headerparser;1"]
.getService(Components.interfaces.nsIMsgHeaderParser);
var emailAddress = headerParser.extractHeaderAddressMailboxes(msgHdr.author);
if (emailAddress)
top.MsgFilters(emailAddress, null);
}
function MsgNewFolder(callBackFunctionName)
@@ -1552,24 +1539,24 @@ function ToggleFavoriteFolderFlag()
{
var folder = GetFirstSelectedMsgFolder();
folder.toggleFlag(Components.interfaces.nsMsgFolderFlags.Favorite);
}
function MsgSaveAsFile()
{
if (GetNumSelectedMessages() == 1)
- SaveAsFile(GetFirstSelectedMessage());
+ SaveAsFile(gFolderDisplay.selectedMessageUris[0]);
}
function MsgSaveAsTemplate()
{
- var folder = GetLoadedMsgFolder();
if (GetNumSelectedMessages() == 1)
- SaveAsTemplate(GetFirstSelectedMessage(), folder);
+ SaveAsTemplate(gFolderDisplay.selectedMessageUris[0],
+ gFolderDisplay.displayedFolder);
}
function CreateToolbarTooltip(document, event)
{
event.stopPropagation();
var tn = document.tooltipNode;
if (tn.localName != "tab")
return false; // Not a tab, so cancel the tooltip.
@@ -1580,315 +1567,337 @@ function CreateToolbarTooltip(document,
if (tn.hasAttribute("label")) {
event.target.setAttribute("label", tn.getAttribute("label"));
return true;
}
return false;
}
/**
- * mailTabType provides both "folder" and "message" tab modes. Under the
- * previous TabOwner framework, their logic was separated into two 'classes'
- * which called common helper methods and had similar boilerplate logic.
+ * Displays message "folder"s, mail "message"s, and "glodaSearch" results. The
+ * commonality is that they all use the "mailContent" panel's folder tree,
+ * thread tree, and message pane objects. This happens for historical reasons,
+ * likely involving the fact that prior to the introduction of this
+ * abstraction, everything was always stored in global objects. For the 3.0
+ * release cycle we considered avoiding this 'multiplexed' style of operation
+ * but decided against moving to making each tab be indepdendent because of
+ * presumed complexity.
+ *
+ * The tab info objects (as tabmail's currentTabInfo/tabInfo fields contain)
+ * have the following attributes specific to our implementation:
+ *
+ *
+ * @property {string} uriToOpen
+ * @property {nsIMsgFolder} msgSelectedFolder Preserves gMsgFolderSelected
+ * global.
+ * @property {nsIMsgDBView} dbView The database view to use with the thread tree
+ * when this tab is displayed. The value will be assigned to the global
+ * gDBView in the process.
+ * @property {nsIMessenger} messenger Used to preserve "messenger" global value.
+ * The messenger object is the keeper of the 'undo' state and navigation
+ * history, which is why we do this.
+ *
+ * @property {boolean} folderPaneCollapsed In "folder" mode, has the user
+ * intentionally collapsed the folder pane.
+ * @property {boolean} messagePaneCollapsed In "folder" or "glodaSearch" mode,
+ * has the user intentionally collapsed the message pane.
+ *
+ * @property {nsIMsgDBHdr} hdr In "message" mode, the header of the message
+ * being displayed.
+ * @property {nsIMsgSearchSession} searchSession Used to preserve gSearchSession
+ * global value.
+ *
*/
let mailTabType = {
name: "mail",
panelId: "mailContent",
modes: {
+ /**
+ * The folder view displays the contents of an nsIMsgDBFolder, with the
+ * folder pane (potentially), thread pane (always), and message pane
+ * (potentially) displayed.
+ *
+ * The actual nsMsgDBView can be any of the following types of things:
+ * - A single folder.
+ * - A quicksearch on a single folder.
+ * - A virtual folder potentially containing messages from multiple
+ * folders. (eShowVirtualFolderResults)
+ */
folder: {
isDefault: true,
type: "folder",
- openTab: function(aTab, aFolderUri) {
- aTab.uriToOpen = aFolderUri;
-
- this.openTab(aTab); // call superclass logic
+ /// The set of panes that are legal to be displayed in this mode
+ legalPanes: {
+ folder: true,
+ thread: true,
+ message: true
+ },
+ openFirstTab: function(aTab) {
+ this.openTab(aTab, true, new MessagePaneDisplayWidget());
+ aTab.folderDisplay.makeActive();
},
- showTab: function(aTab) {
- this.folderAndThreadPaneVisible = true;
- ClearMessagePane();
-
- this.showTab(aTab);
+ /**
+ * @param aFolder The nsIMsgFolder to display.
+ * @param aMsgHdr Optional message header to display.
+ */
+ openTab: function(aTab, aFolder, aMsgHdr) {
+ // Get a tab that we can initialize our user preferences from.
+ // (We don't want to assume that our immediate predecessor was a
+ // "folder" tab.)
+ let modelTab = document.getElementById("tabmail")
+ .getTabInfoForCurrentOrFirstModeInstance(aTab.mode);
+ // copy its state
+ aTab.folderPaneCollapsed = modelTab.folderPaneCollapsed;
+ aTab.messagePaneCollapsed = modelTab.messagePaneCollapsed;
+
+ this.openTab(aTab, false, new MessagePaneDisplayWidget());
+
+ // Clear selection, because context clicking on a folder and opening in a
+ // new tab needs to have SelectFolder think the selection has changed.
+ // We also need to clear these globals to subvert the code that prevents
+ // folder loads when things haven't changed.
+ var folderTree = document.getElementById("folderTree");
+ folderTree.view.selection.clearSelection();
+ folderTree.view.selection.currentIndex = -1;
+
+ aTab.folderDisplay.makeActive();
+
+ // selecting the folder effectively calls
+ // aTab.folderDisplay.show(aFolder)
+ gFolderTreeView.selectFolder(aFolder);
+ if(aMsgHdr)
+ aTab.folderDisplay.selectMessage(aMsgHdr);
},
onTitleChanged: function(aTab, aTabNode) {
- if (!gMsgFolderSelected) {
+ if (!aTab.folderDisplay || !aTab.folderDisplay.displayedFolder) {
// Don't show "undefined" as title when there is no account.
aTab.title = " ";
return;
}
- aTab.title = gMsgFolderSelected.prettyName;
- if (!gMsgFolderSelected.isServer && this._getNumberOfRealAccounts() > 1)
- aTab.title += " - " + gMsgFolderSelected.server.prettyName;
-
- // The user may have changed folders, triggering our onTitleChanged callback.
+ // The user may have changed folders, triggering our onTitleChanged
+ // callback.
+ let folder = aTab.folderDisplay.displayedFolder;
+ aTab.title = folder.prettyName;
+ if (!folder.isServer && this._getNumberOfRealAccounts() > 1)
+ aTab.title += " - " + folder.server.prettyName;
+
// Update the appropriate attributes on the tab.
- aTabNode.setAttribute('SpecialFolder', getSpecialFolderString(gMsgFolderSelected));
- aTabNode.setAttribute('ServerType', gMsgFolderSelected.server.type);
- aTabNode.setAttribute('IsServer', gMsgFolderSelected.isServer);
- aTabNode.setAttribute('IsSecure', gMsgFolderSelected.server.isSecure);
+ aTabNode.setAttribute('SpecialFolder',
+ getSpecialFolderString(folder));
+ aTabNode.setAttribute('ServerType', folder.server.type);
+ aTabNode.setAttribute('IsServer', folder.isServer);
+ aTabNode.setAttribute('IsSecure', folder.server.isSecure);
}
},
+ /**
+ * The message view displays a single message. In this view, the folder
+ * pane and thread pane are forced hidden and only the message pane is
+ * displayed.
+ */
message: {
type: "message",
- openTab: function(aTab, aFolderUri, aMsgHdr) {
- aTab.uriToOpen = aFolderUri;
- aTab.hdr = aMsgHdr;
-
+ /// The set of panes that are legal to be displayed in this mode
+ legalPanes: {
+ folder: false,
+ thread: false,
+ message: true
+ },
+ openTab: function(aTab, aMsgHdr, aViewWrapperToClone) {
+ aTab.mode.onTitleChanged.call(this, aTab, null, aMsgHdr);
+
+ this.openTab(aTab, false, new MessageTabDisplayWidget());
+
+ if (aViewWrapperToClone)
+ aTab.folderDisplay.cloneView(aViewWrapperToClone);
+ else
+ aTab.folderDisplay.show(aMsgHdr.folder);
+
+ aTab.folderDisplay.selectMessage(aMsgHdr);
+
+ // we only want to make it active after setting up the view and the message
+ // to avoid generating bogus summarization events.
+ aTab.folderDisplay.makeActive();
+ },
+ onTitleChanged: function(aTab, aTabNode, aMsgHdr) {
+ if (aMsgHdr == null)
+ aMsgHdr = aTab.folderDisplay.selectedMessage;
aTab.title = "";
- if (aTab.hdr.flags & Components.interfaces.nsMsgMessageFlags.HasRe)
+ if (aMsgHdr.flags & Components.interfaces.nsMsgMessageFlags.HasRe)
aTab.title = "Re: ";
- if (aTab.hdr.mime2DecodedSubject)
- aTab.title += aTab.hdr.mime2DecodedSubject;
-
- aTab.title += " - " + aTab.hdr.folder.prettyName;
+ if (aMsgHdr.mime2DecodedSubject)
+ aTab.title += aMsgHdr.mime2DecodedSubject;
+
+ aTab.title += " - " + aMsgHdr.folder.prettyName;
if (this._getNumberOfRealAccounts() > 1)
- aTab.title += " - " + aTab.hdr.folder.server.prettyName;
-
- // let's try hiding the thread pane and folder pane
- this.folderAndThreadPaneVisible = false;
-
- this.openTab(aTab); // call superclass logic
-
- gCurrentlyDisplayedMessage = nsMsgViewIndex_None;
- ClearThreadPaneSelection();
- setTimeout(gDBView.selectFolderMsgByKey, 0, aTab.hdr.folder,
- aTab.hdr.messageKey);
- },
- showTab: function(aTab) {
- this.folderAndThreadPaneVisible = false;
- this.showTab(aTab);
+ aTab.title += " - " + aMsgHdr.folder.server.prettyName;
}
}
},
_getNumberOfRealAccounts : function() {
let mgr = Components.classes["@mozilla.org/messenger/account-manager;1"]
.getService(Components.interfaces.nsIMsgAccountManager);
let accountCount = mgr.accounts.Count();
// If we have an account, we also always have a "Local Folders" account.
return accountCount > 0 ? (accountCount - 1) : 0;
},
/**
- * Create the new tab's state, which engenders some side effects. Part of our
- * contract is that we leave the tab in the selected state.
+ * Common tab opening code shared by the various tab modes.
*/
- openTab: function(aTab) {
+ openTab: function(aTab, aIsFirstTab, aMessageDisplay) {
// Set the messagepane as the primary browser for content.
document.getElementById("messagepane").setAttribute("type",
"content-primary");
- ClearThreadPaneSelection();
-
- // Each tab gets its own messenger instance; I assume this is so each one
- // gets its own undo/redo stack?
- messenger = Components.classes["@mozilla.org/messenger;1"]
- .createInstance(Components.interfaces.nsIMessenger);
- messenger.setWindow(window, msgWindow);
- aTab.messenger = messenger;
-
- aTab.msgSelectedFolder = gMsgFolderSelected;
-
- // Clear selection, because context clicking on a folder and opening in a
- // new tab needs to have SelectFolder think the selection has changed.
- // We also need to clear these globals to subvert the code that prevents
- // folder loads when things haven't changed.
- var folderTree = document.getElementById("folderTree");
- folderTree.view.selection.clearSelection();
- folderTree.view.selection.currentIndex = -1;
- gMsgFolderSelected = null;
- msgWindow.openFolder = null;
-
- // Clear thread pane selection - otherwise, the tree tries to impose the
- // the current selection on the new view.
- gDBView = null; // clear gDBView so we won't try to close it.
- gFolderTreeView.selectFolder(GetMsgFolderFromUri(aTab.uriToOpen));
- aTab.dbView = gDBView;
+ aTab.messageDisplay = aMessageDisplay;
+ aTab.folderDisplay = new FolderDisplayWidget(aTab, aTab.messageDisplay);
+ aTab.folderDisplay.msgWindow = msgWindow;
+ aTab.folderDisplay.tree = document.getElementById("threadTree");
+ aTab.folderDisplay.treeBox = aTab.folderDisplay.tree.boxObject.QueryInterface(
+ Components.interfaces.nsITreeBoxObject);
+
+ if (aIsFirstTab) {
+ aTab.folderDisplay.messenger = messenger;
+ }
+ else {
+ // Each tab gets its own messenger instance; this provides each tab with
+ // its own undo/redo stack and back/forward navigation history.
+ messenger = Components.classes["@mozilla.org/messenger;1"]
+ .createInstance(Components.interfaces.nsIMessenger);
+ messenger.setWindow(window, msgWindow);
+ aTab.folderDisplay.messenger = messenger;
+ }
},
closeTab: function(aTab) {
- if (aTab.dbView)
- aTab.dbView.close();
- if (aTab.messenger)
- aTab.messenger.setWindow(null, null);
+ aTab.folderDisplay.close();
},
saveTabState: function(aTab) {
- aTab.messenger = messenger;
- aTab.dbView = gDBView;
- aTab.searchSession = gSearchSession;
- aTab.msgSelectedFolder = gMsgFolderSelected;
- if (!gDBView)
- return;
-
- aTab.firstVisibleRow = document.getElementById("threadTree").treeBoxObject.getFirstVisibleRow();
-
- if (gDBView.currentlyDisplayedMessage != nsMsgViewIndex_None)
- {
- try // there may not be a selected message.
- {
- var curMsgHdr = gDBView.hdrForFirstSelectedMessage;
- aTab.selectedMsgId = curMsgHdr.messageId;
- }
- catch (ex) {}
- }
- else
- {
- aTab.selectedMsgId = null;
- aTab.msgSelectedFolder = gDBView.msgFolder;
- }
- if (aTab.msgSelectedFolder)
- aTab.mailView = GetMailViewForFolder(aTab.msgSelectedFolder);
-
// Now let other tabs have a primary browser if they want.
document.getElementById("messagepane").setAttribute("type",
"content-targetable");
+
+ aTab.folderDisplay.makeInactive();
},
- _displayFolderAndThreadPane: function(show) {
- let collapse = !show;
+ /**
+ * Some panes simply are illegal in certain views, and some panes are legal
+ * but the user may have collapsed/hidden them. If that was not enough, we
+ * have three different layouts that are possible, each of which requires a
+ * slightly different DOM configuration, and accordingly for us to poke at
+ * different DOM nodes. Things are made somewhat simpler by our decision
+ * that all tabs share the same layout.
+ * This method takes the legal states and current display states and attempts
+ * to apply the appropriate logic to make it all work out. This method is
+ * not in charge of figuring out or preserving display states.
+ *
+ * We take a dictionary of desired visibility booleans as our argument because
+ * it is both readable and scalable.
+ *
+ * @param aLegalStates A dictionary where each key and value indicates whether
+ * the pane in question (key) is legal to be displayed in this mode. If
+ * the value is true, then the pane is legal. Omitted pane keys imply
+ * that the pane is illegal. Keys are:
+ * - folder: The folder (tree) pane.
+ * - thread: The thread pane.
+ * - message: The message pane. Required/assumed to be true for now.
+ * - glodaFacets: The gloda search facets pane.
+ * @param aVisibleStates A dictionary where each value indicates whether the
+ * pane should be 'visible' (not collapsed). Only panes that are governed
+ * by splitters are options here. Keys are:
+ * - folder: The folder (tree) pane.
+ * - message: The message pane.
+ */
+ _setPaneStates: function mailTabType_setPaneStates(aLegalStates,
+ aVisibleStates) {
let layout = pref.getIntPref("mail.pane_config.dynamic");
if (layout == kWidePaneConfig)
{
- document.getElementById("messengerBox").collapsed = collapse;
- // If opening a standalone message, need to give the messagepanebox flex.
- if (collapse)
+ // in the "wide" configuration, the #messengerBox is left holding the
+ // folder pane and thread pane, and the message pane has migrated to be
+ // its sibling (under #mailContent).
+ // Accordingly, if both the folder and thread panes are illegal, we
+ // want to collapse the #messengerBox and make sure the #messagepanebox
+ // fills up the screen. (For example, when in "message" mode.)
+ let collapseMessengerBox = !aLegalStates.folder && !aLegalStates.thread;
+ document.getElementById("messengerBox").collapsed = collapseMessengerBox;
+ if (collapseMessengerBox)
document.getElementById("messagepanebox").flex = 1;
}
- if (layout == kVerticalPaneConfig)
- document.getElementById("threadTree").collapsed = collapse;
- else
- document.getElementById("displayDeck").collapsed = collapse;
-
- let fpSplitter = document.getElementById("folderpane_splitter");
- if (show && fpSplitter.getAttribute("state") == "collapsed")
- {
- // If we're showing everything but the folder pane splitter was
- // previously collapsed, then keep it collapsed.
- // However, in the case of it being collapsed from showing a
- // a full message in a tab, then uncollapse just the splitter.
- if (fpSplitter.getAttribute("substate"))
- fpSplitter.collapsed = false;
- }
+ // -- folder pane
+ // collapse the splitter when not legal
+ document.getElementById("folderpane_splitter").collapsed =
+ !aLegalStates.folder;
+ // collapse the folder pane when not visible
+ document.getElementById("folderPaneBox").collapsed =
+ !aLegalStates.folder || !aVisibleStates.folder;
+
+ // -- thread pane
+ // in a vertical view, the threadContentArea sits in the #threadPaneBox
+ // next to the message pane and its splitter.
+ if (layout == kVerticalMailLayout)
+ document.getElementById("threadContentArea").collapsed =
+ !aLegalStates.thread;
+ // whereas in the default view, the displayDeck is the one next to the
+ // message pane and its splitter
else
- {
- fpSplitter.collapsed = collapse;
- document.getElementById("folderPaneBox").collapsed = collapse;
- }
-
- let tpSplitter = document.getElementById("threadpane-splitter");
- if (show && tpSplitter.getAttribute("state") == "collapsed")
- {
- // If we're showing everything but the thread pane splitter was
- // previously collapsed, then keep it collapsed.
- // However, in the case of it being collapsed from showing a
- // a full message in a tab, then uncollapse just the splitter.
- tpSplitter.collapsed = tpSplitter.getAttribute("substate") ? false : true;
- document.getElementById("messagepanebox").collapsed = true;
- }
- else
- {
- tpSplitter.collapsed = collapse;
- document.getElementById("messagepanebox").collapsed = false;
- }
- // Need to call ChangeMessagePaneVisibility() a little later after the
- // message was selected so that the tab state data gets the message key
- // saved off, which happens below in showTab().
- setTimeout(ChangeMessagePaneVisibility, 0, IsMessagePaneCollapsed());
-
+ document.getElementById("displayDeck").collapsed =
+ !aLegalStates.thread;
+
+ // the threadpane-splitter collapses the message pane (arguably a misnomer),
+ // but it only needs to exist when the thread-pane is legal
+ document.getElementById("threadpane-splitter").collapsed =
+ !aLegalStates.thread;
+
+ // Some things do not make sense if the thread pane is not legal.
+ // (This is likely an example of something that should be using the command
+ // mechanism to update the UI elements as to the state of what the user
+ // is looking at, rather than home-brewing it in here.)
try {
- document.getElementById("search-container").collapsed = collapse;
+ // you can't quick-search if you don't have a collection of messages
+ document.getElementById("search-container").collapsed =
+ !aLegalStates.thread;
} catch (ex) {}
try {
- document.getElementById("mailviews-container").collapsed = collapse;
+ // views only work on the thread pane; no thread pane, no views
+ document.getElementById("mailviews-container").collapsed =
+ !aLegalStates.thread;
} catch (ex) {}
- },
-
- _folderAndThreadPaneVisible: true,
- get folderAndThreadPaneVisible() { return this._folderAndThreadPaneVisible; },
- set folderAndThreadPaneVisible(aDesiredVisible) {
- if (aDesiredVisible != this._folderAndThreadPaneVisible) {
- this._displayFolderAndThreadPane(aDesiredVisible);
- this._folderAndThreadPaneVisible = aDesiredVisible;
- }
+
+ // -- message pane
+ // the message pane can only be collapsed when the thread pane is legal
+ document.getElementById("messagepanebox").collapsed =
+ aLegalStates.thread && !aVisibleStates.message;
+
+ // -- gloda facets
+ //document.getElementById("glodaSearchFacets").hidden =
+ // !aLegalStates.glodaFacets;
},
showTab: function(aTab) {
// Set the messagepane as the primary browser for content.
document.getElementById("messagepane").setAttribute("type",
"content-primary");
- // restore globals
- messenger = aTab.messenger;
- gDBView = aTab.dbView;
- gSearchSession = aTab.searchSession;
-
- // restore selection in folder pane;
- let folderToSelect = gDBView ? gDBView.msgFolder : aTab.msgSelectedFolder;
- // restore view state if we had one
- var row = gFolderTreeView.getIndexOfFolder(folderToSelect);
-
- var treeBoxObj = document.getElementById("folderTree").treeBoxObject;
- var folderTreeSelection = treeBoxObj.view.selection;
- // make sure that row.value is valid so that it doesn't mess up
- // the call to ensureRowIsVisible().
- if ((row >= 0) && !folderTreeSelection.isSelected(row))
- {
- gMsgFolderSelected = folderToSelect;
- folderTreeSelection.selectEventsSuppressed = true;
- folderTreeSelection.select(row);
- treeBoxObj.ensureRowIsVisible(row);
- folderTreeSelection.selectEventsSuppressed = false;
- }
- if (gDBView)
- {
- UpdateColumnsForView(gMsgFolderSelected, gDBView.viewType);
- UpdateSortIndicators(gDBView.sortType, gDBView.sortOrder);
- // This sets the thread pane tree's view to the gDBView view.
- RerootThreadPane();
- // Only refresh the view picker if the views toolbar is visible.
- if (document.getElementById("mailviews-container"))
- UpdateViewPickerByValue(aTab.mailView);
-
- // We need to restore the selection to what it was when we switched away
- // from this tab. We need to remember the selected keys, instead of the
- // selected indices, since the view might have changed. But maybe the
- // selectedIndices adjust as items are added/removed from the (hidden)
- // view.
- try
- {
- if (aTab.selectedMsgId && aTab.msgSelectedFolder)
- {
- // We clear the selection in order to generate an event when we
- // re-select our message.
- ClearThreadPaneSelection();
-
- var msgDB = aTab.msgSelectedFolder.msgDatabase;
- var msgHdr = msgDB.getMsgHdrForMessageID(aTab.selectedMsgId);
- setTimeout(gDBView.selectFolderMsgByKey, 0, aTab.msgSelectedFolder,
- msgHdr.messageKey);
- }
- // We do not clear the selection if there was more than one message
- // displayed. this leaves our selection intact. there was originally
- // some claim that the selection might lose synchronization with the
- // view, but this is unsubstantiated. said comment came from the
- // original code that stored information on the selected rows, but
- // then failed to do anything with it, probably because there is no
- // existing API call that accomplishes it.
+ aTab.folderDisplay.makeActive();
+
+ // - restore folder pane/tree selection
+ if (aTab.folderDisplay.displayedFolder) {
+ // but don't generate any events while doing so!
+ gFolderTreeView.selection.selectEventsSuppressed = true;
+ try {
+ gFolderTreeView.selectFolder(aTab.folderDisplay.displayedFolder);
}
- catch (ex) {dump(ex);}
-
- document.getElementById("threadTree").treeBoxObject.scrollToRow(aTab.firstVisibleRow);
- ShowThreadPane();
- }
- else if (gMsgFolderSelected.isServer)
- {
- UpdateStatusQuota(null);
- // Load AccountCentral page here.
- ShowAccountCentral();
+ finally {
+ gIgnoreSyntheticFolderPaneSelectionChange = true;
+ gFolderTreeView.selection.selectEventsSuppressed = false;
+ }
}
},
supportsCommand: function(aTab, aCommand) {
return DefaultController.supportsCommand(aCommand);
},
isCommandEnabled: function(aTab, aCommand) {
@@ -1912,90 +1921,64 @@ let mailTabType = {
function MsgOpenNewWindowForFolder(folderURI, msgKeyToSelect)
{
if (folderURI) {
window.openDialog("chrome://messenger/content/", "_blank",
"chrome,all,dialog=no", folderURI, msgKeyToSelect);
return;
}
- // Use gFolderTreeView.getSelectedFolders() to find out which folder to open
- // instead of GetLoadedMsgFolder().URI. This is required because on a
- // right-click, the currentIndex value will be different from the actual row
- // that is highlighted. gFolderTreeView.getSelectedFolders() will return the
- // folder that is highlighted.
+ // If there is a right-click happening, gFolderTreeView.getSelectedFolders()
+ // will tell us about it (while the selection's currentIndex would reflect
+ // the node that was selected/displayed before the right-click.)
let selectedFolders = gFolderTreeView.getSelectedFolders();
for (let i = 0; i < selectedFolders.length; i++) {
window.openDialog("chrome://messenger/content/", "_blank",
"chrome,all,dialog=no",
selectedFolders[i].URI, msgKeyToSelect);
}
}
-function MsgOpenNewTabForFolder(folderURI, msgKeyToSelect)
+/**
+ * UI-triggered command to open the currently selected folder(s) in new tabs.
+ */
+function MsgOpenNewTabForFolder()
{
- // XXX implement the usage of msgKeyToSelect...
- if (folderURI) {
- document.getElementById("tabmail").openTab("folder", folderURI);
- return;
- }
-
- // Use gFolderTreeView.getSelectedFolders() to find out which folder to open
- // instead of GetLoadedMsgFolder().URI. This is required because on a
- // right-click, the currentIndex value will be different from the actual row
- // that is highlighted. gFolderTreeView.getSelectedFolders() will return the
- // folder that is highlighted.
+ // If there is a right-click happening, gFolderTreeView.getSelectedFolders()
+ // will tell us about it (while the selection's currentIndex would reflect
+ // the node that was selected/displayed before the right-click.)
let selectedFolders = gFolderTreeView.getSelectedFolders();
for (let i = 0; i < selectedFolders.length; i++) {
- // Set up the first tab, which was previously invisible.
- // This assumes the first tab is always a 3-pane ui, which
- // may not be right, especially if we have the ability
- // to persist your tab setup.
- document.getElementById("tabmail").openTab("folder", selectedFolders[i].URI);
+ document.getElementById("tabmail").openTab("folder", selectedFolders[i]);
}
}
-function MsgOpenNewTabForMessage(messageKey, folderUri)
+/**
+ * UI-triggered command to open the currently selected message in a new tab.
+ */
+function MsgOpenNewTabForMessage()
{
- var hdr;
-
- // messageKey can be 0 for an actual message (first message in the folder)
- if (folderUri)
- {
- hdr = GetMsgFolderFromUri(folderUri).GetMessageHeader(messageKey);
- }
- else
- {
- hdr = gDBView.hdrForFirstSelectedMessage;
- // Use the header's folder - this will open a msg in a virtual folder view
- // in its real folder, which is needed if the msg wouldn't be in a new
- // view with the same terms - e.g., it's read and the view is unread only.
- // If we cloned the view, we wouldn't have to do this.
- folderUri = hdr.folder.URI;
- }
-
- // Fix it so we won't try to load the previously loaded message.
- hdr.folder.lastMessageLoaded = nsMsgKey_None;
-
- document.getElementById('tabmail').openTab("message", folderUri, hdr);
+ if (!gFolderDisplay.selectedMessage)
+ return;
+ document.getElementById('tabmail').openTab("message",
+ gFolderDisplay.selectedMessage,
+ gFolderDisplay.view);
}
function MsgOpenSelectedMessages()
{
// Toggle message body (rss summary) and content-base url in message
// pane per pref, otherwise open summary or web page in new window.
- if (IsFeedItem() && GetFeedOpenHandler() == 2) {
+ if (gFolderDisplay.selectedMessageIsFeed && GetFeedOpenHandler() == 2) {
FeedSetContentViewToggle();
return;
}
- var dbView = GetDBView();
-
- var indices = GetSelectedIndices(dbView);
- var numMessages = indices.length;
+ let selectedMessages = gFolderDisplay.selectedMessages;
+ var numMessages = selectedMessages.length;
var windowReuse = gPrefBranch.getBoolPref("mailnews.reuse_message_window");
// This is a radio type button pref, currently with only 2 buttons.
// We need to keep the pref type as 'bool' for backwards compatibility
// with 4.x migrated prefs. For future radio button(s), please use another
// pref (either 'bool' or 'int' type) to describe it.
//
// windowReuse values: false, true
@@ -2013,55 +1996,37 @@ function MsgOpenSelectedMessages()
var text = gMessengerBundle.getFormattedString("openWindowWarningText", [numMessages]);
var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService);
if (!promptService.confirm(window, title, text))
return;
}
for (var i = 0; i < numMessages; i++) {
- MsgOpenNewWindowForMessage(dbView.getURIForViewIndex(indices[i]),
- dbView.getFolderForViewIndex(indices[i]).URI);
+ MsgOpenNewWindowForMessage(selectedMessages[i]);
}
}
function MsgOpenSelectedMessageInExistingWindow()
{
var windowID = GetWindowByWindowType("mail:messageWindow");
if (!windowID)
return false;
- try {
- var messageURI = gDBView.URIForFirstSelectedMessage;
- var msgHdr = gDBView.hdrForFirstSelectedMessage;
-
- // Reset the window's message uri and folder uri vars, and
- // update the command handlers to what's going to be used.
- // This has to be done before the call to CreateView().
- windowID.gCurrentMessageUri = messageURI;
- windowID.gCurrentFolderUri = msgHdr.folder.URI;
- windowID.UpdateMailToolbar('MsgOpenExistingWindowForMessage');
-
- // Even if the folder uri's match, we can't use the existing view
- // (msgHdr.folder.URI == windowID.gCurrentFolderUri)
- // - the reason is quick search and mail views. See bug #187673.
- //
- // For the sake of simplicity, let's always call CreateView(gDBView);
- // which will clone gDBView.
- windowID.CreateView(gDBView);
- windowID.LoadMessageByMsgKey(msgHdr.messageKey);
-
- // bring existing window to front
- windowID.focus();
- return true;
- }
- catch (ex) {
- dump("reusing existing standalone message window failed: " + ex + "\n");
- }
- return false;
+ var msgHdr = gFolderDisplay.selectedMessage;
+
+ // (future work: perhaps make the window have a method we can call to do this)
+ // make the window's folder clone our view
+ windowID.gFolderDisplay.cloneView(gFolderDisplay.view);
+ // boss the window into showing our message
+ windowID.gFolderDisplay.selectMessage(msgHdr);
+
+ // bring existing window to front
+ windowID.focus();
+ return true;
}
function MsgOpenFromFile()
{
const nsIFilePicker = Components.interfaces.nsIFilePicker;
var fp = Components.classes["@mozilla.org/filepicker;1"]
.createInstance(nsIFilePicker);
@@ -2086,62 +2051,53 @@ function MsgOpenFromFile()
dump("filePicker.chooseInputFile threw an exception\n");
return;
}
var uri = fp.fileURL.QueryInterface(Components.interfaces.nsIURL);
uri.query = "type=application/x-message-display";
window.openDialog("chrome://messenger/content/messageWindow.xul", "_blank",
- "all,chrome,dialog=no,status,toolbar", uri, null, null);
+ "all,chrome,dialog=no,status,toolbar", uri);
}
-function MsgOpenNewWindowForMessage(messageUri, folderUri)
+function MsgOpenNewWindowForMessage(aMsgHdr)
{
- if (!messageUri)
- // Use GetFirstSelectedMessage() to find out which message to open
- // instead of gDBView.getURIForViewIndex(currentIndex). This is
- // required because on a right-click, the currentIndex value will be
- // different from the actual row that is highlighted.
- // GetFirstSelectedMessage() will return the message that is
- // highlighted.
- messageUri = GetFirstSelectedMessage();
-
- if (!folderUri)
- // Use GetSelectedMsgFolders() to find out which message to open
- // instead of gDBView.getURIForViewIndex(currentIndex). This is
- // required because on a right-click, the currentIndex value will be
- // different from the actual row that is highlighted.
- // GetSelectedMsgFolders() will return the message that is
- // highlighted.
- folderUri = GetSelectedMsgFolders()[0].URI;
-
- // be sure to pass in the current view....
- if (messageUri && folderUri) {
+ // no message header provided? get the selected message (this will give us
+ // the right-click selected message if that's what is going down.)
+ if (!aMsgHdr)
+ aMsgHdr = gFolderDisplay.selectedMessage;
+
+ // (there might not have been a selected message, so check...)
+ if (aMsgHdr)
+ // we also need to tell the window about our current view so that it can
+ // clone it. This enables advancing through the messages, etc.
window.openDialog("chrome://messenger/content/messageWindow.xul", "_blank",
"all,chrome,dialog=no,status,toolbar",
- messageUri, folderUri, gDBView);
- }
+ aMsgHdr, gFolderDisplay.view);
}
function MsgJunk()
{
MsgJunkMailInfo(true);
JunkSelectedMessages(!SelectedMessagesAreJunk());
}
function UpdateJunkButton()
{
- let hdr = msgHdrForCurrentMessage();
- if (!hdr) // .eml file
+ // The junk message should slave off the selected message, as the preview pane
+ // may not be visible
+ let hdr = gFolderDisplay.selectedMessage;
+ // But only the message display knows if we are dealing with a dummy.
+ if (gMessageDisplay.isDummy) // .eml file
return;
let junkScore = hdr.getStringProperty("junkscore");
let hideJunk = (junkScore != "") && (junkScore != "0");
- if (isNewsURI(hdr.folder.URI))
+ if (gFolderDisplay.selectedMessageIsNews)
hideJunk = true;
// which DOM node is the current junk button in the
// message reader depends on whether it's the collapsed or
// expanded header
let buttonBox = document.getElementById(gCollapsedHeaderViewMode ?
"collapsedButtonBox" : "expandedButtonBox");
buttonBox.getButton('hdrJunkButton').disabled = hideJunk;
}
@@ -2155,17 +2111,17 @@ function MsgMarkAsFlagged()
{
MarkSelectedMessagesFlagged(!SelectedMessagesAreFlagged());
}
function MsgMarkReadByDate()
{
window.openDialog("chrome://messenger/content/markByDate.xul","",
"chrome,modal,titlebar,centerscreen",
- GetLoadedMsgFolder());
+ gFolderDisplay.displayedFolder);
}
function MsgMarkAllRead()
{
var folder = GetSelectedMsgFolders()[0];
if (folder)
folder.markAllMessagesRead(msgWindow);
@@ -2175,17 +2131,17 @@ function MsgFilters(emailAddress, folder
{
if (!folder)
{
// Try to determine the folder from the selected message.
if (gDBView)
{
try
{
- var msgHdr = gDBView.hdrForFirstSelectedMessage;
+ var msgHdr = gFolderDisplay.selectedMessage;
var accountKey = msgHdr.accountKey;
if (accountKey.length > 0)
{
var account = accountManager.getAccount(accountKey);
if (account)
{
var server = account.incomingServer;
if (server)
@@ -2267,43 +2223,30 @@ function MsgApplyFilters()
newFilterIndex++;
}
}
filterService.applyFiltersToFolders(tempFilterList, selectedFolders, msgWindow);
}
function MsgApplyFiltersToSelection()
{
- var filterService = Components.classes["@mozilla.org/messenger/services/filters;1"]
- .getService(Components.interfaces.nsIMsgFilterService);
-
- var folder = gDBView.msgFolder;
- var indices = GetSelectedIndices(gDBView);
- if (indices && indices.length)
- {
- var selectedMsgs = Components.classes["@mozilla.org/array;1"]
- .createInstance(Components.interfaces.nsIMutableArray);
- for (var i = 0; i < indices.length; i++)
- {
- try
- {
- // Getting the URI will tell us if the item is real or a dummy header
- var uri = gDBView.getURIForViewIndex(indices[i]);
- if (uri)
- {
- var msgHdr = folder.GetMessageHeader(gDBView.getKeyAt(indices[i]));
- if (msgHdr)
- selectedMsgs.appendElement(msgHdr, false);
- }
- } catch (ex) {}
- }
+ // bail if we're dealing with a dummy header
+ if (gMessageDisplay.isDummy)
+ return;
+
+ var selectedMessages = gFolderDisplay.selectedMessages;
+ if (selectedMessages.length) {
+ var filterService =
+ Components.classes["@mozilla.org/messenger/services/filters;1"]
+ .getService(Components.interfaces.nsIMsgFilterService);
filterService.applyFilters(Components.interfaces.nsMsgFilterType.Manual,
- selectedMsgs,
- folder,
+ toXPCOMArray(selectedMessages,
+ Components.interfaces.nsIMutableArray),
+ gFolderDisplay.displayedFolder,
msgWindow);
}
}
function ChangeMailLayout(newLayout)
{
gPrefBranch.setIntPref("mail.pane_config.dynamic", newLayout);
}
@@ -2381,18 +2324,18 @@ function ToggleInlineAttachment(target)
var viewAttachmentInline = !pref.getBoolPref("mail.inline_attachments");
pref.setBoolPref("mail.inline_attachments", viewAttachmentInline)
target.setAttribute("checked", viewAttachmentInline ? "true" : "false");
ReloadMessage();
}
function PrintEnginePrintInternal(doPrintPreview, msgType)
{
- var messageList = GetSelectedMessages();
- if (messageList.length == 0) {
+ var messageList = gFolderDisplay.selectedMessageUris;
+ if (!messageList) {
dump("PrintEnginePrintInternal(): No messages selected.\n");
return;
}
window.openDialog("chrome://messenger/content/msgPrintEngine.xul", "",
"chrome,dialog=no,all,centerscreen",
messageList.length, messageList, statusFeedback,
doPrintPreview, msgType, window);
@@ -2670,17 +2613,17 @@ function CommandUpdate_UndoRedo()
EnableMenuItem("menu_undo", SetupUndoRedoCommand("cmd_undo"));
EnableMenuItem("menu_redo", SetupUndoRedoCommand("cmd_redo"));
}
function SetupUndoRedoCommand(command)
{
// If we have selected a server, and are viewing account central
// there is no loaded folder.
- var loadedFolder = GetLoadedMsgFolder();
+ var loadedFolder = gFolderDisplay.displayedFolder;
if (!loadedFolder || !loadedFolder.server.canUndoDeleteOnServer)
return false;
var canUndoOrRedo;
var txnType;
if (command == "cmd_undo")
{
canUndoOrRedo = messenger.canUndo();
@@ -2689,41 +2632,57 @@ function SetupUndoRedoCommand(command)
else
{
canUndoOrRedo = messenger.canRedo();
txnType = messenger.getRedoTransactionType();
}
if (canUndoOrRedo)
{
- var commands =
+ var commands =
['valueDefault', 'valueDeleteMsg', 'valueMoveMsg', 'valueCopyMsg', 'valueUnmarkAllMsgs'];
goSetMenuValue(command, commands[txnType]);
}
else
{
goSetMenuValue(command, 'valueDefault');
}
return canUndoOrRedo;
}
+/**
+ * Triggered by the global JunkStatusChanged notification, we handle updating
+ * the message display if our displayed message might have had its junk status
+ * change. This primarily entails updating the notification bar (that thing
+ * that appears above the message and says "this message might be junk") and
+ * (potentially) reloading the message because junk status affects the form of
+ * HTML display used (sanitized vs not).
+ * When our tab implementation is no longer multiplexed (reusing the same
+ * display widget), this must be moved into the MessageDisplayWidget or
+ * otherwise be scoped to the tab.
+ */
function HandleJunkStatusChanged(folder)
{
+ // We have nothing to do (and should bail) if:
+ // - There is no currently displayed message.
+ // - The displayed message is an .eml file from disk or an attachment.
+ // - The folder that has had a junk change is not backing the display folder.
+
// This might be the stand alone window, open to a message that was
// and attachment (or on disk), in which case, we want to ignore it.
- var loadedMessage = GetLoadedMessage();
- if (!loadedMessage || (/type=application\/x-message-display/.test(loadedMessage)) ||
- !IsCurrentLoadedFolder(folder))
+ if (!gMessageDisplay.displayedMessage ||
+ gMessageDisplay.isDummy ||
+ gFolderDisplay.displayedFolder != folder)
return;
// If multiple message are selected and we change the junk status
// we don't want to show the junk bar (since the message pane is blank).
var msgHdr = null;
if (GetNumSelectedMessages() == 1)
- msgHdr = messenger.msgHdrFromURI(loadedMessage);
+ msgHdr = gMessageDisplay.displayedMessage;
var junkBarWasDisplayed = gMessageNotificationBar.isFlagSet(kMsgNotificationJunkBar);
gMessageNotificationBar.setJunkMsg(msgHdr);
// Only reload message if junk bar display state has changed.
if (msgHdr && junkBarWasDisplayed != gMessageNotificationBar.isFlagSet(kMsgNotificationJunkBar))
{
// We may be forcing junk mail to be rendered with sanitized html.
// In that scenario, we want to reload the message if the status has just
@@ -2764,17 +2723,17 @@ var gMessageNotificationBar =
mMsgNotificationBar: document.getElementById('msgNotificationBar'),
setJunkMsg: function(aMsgHdr)
{
var isJunk = false;
if (aMsgHdr)
{
- var junkScore = aMsgHdr.getStringProperty("junkscore");
+ var junkScore = aMsgHdr.getStringProperty("junkscore");
isJunk = ((junkScore != "") && (junkScore != "0"));
}
this.updateMsgNotificationBar(kMsgNotificationJunkBar, isJunk);
goUpdateCommand('button_junk');
},
@@ -2834,34 +2793,25 @@ function LoadMsgWithRemoteContent()
// we want to get the msg hdr for the currently selected message
// change the "remoteContentBar" property on it
// then reload the message
setMsgHdrPropertyAndReload("remoteContentPolicy", kAllowRemoteContent);
}
/**
- * Returns the msg hdr associated with the current loaded message.
- */
-function msgHdrForCurrentMessage()
-{
- var msgURI = GetLoadedMessage();
- return (msgURI && !(/type=application\/x-message-display/.test(msgURI))) ? messenger.msgHdrFromURI(msgURI) : null;
-}
-
-/**
* Reloads the message after adjusting the remote content policy for the sender.
- * Iterate through the local address books looking for a card with the same e-mail address as the
+ * Iterate through the local address books looking for a card with the same e-mail address as the
* sender of the current loaded message. If we find a card, update the allow remote content field.
* If we can't find a card, prompt the user with a new AB card dialog, pre-selecting the remote content field.
*/
function allowRemoteContentForSender()
{
// get the sender of the msg hdr
- var msgHdr = msgHdrForCurrentMessage();
+ var msgHdr = gMessageDisplay.displayedMessage;
if (!msgHdr)
return;
var headerParser = Components.classes["@mozilla.org/messenger/headerparser;1"]
.getService(Components.interfaces.nsIMsgHeaderParser);
var names = {};
var addresses = {};
var fullNames = {};
@@ -2925,17 +2875,17 @@ function IgnorePhishingWarning()
// This property is used to supress the phishing bar for the message.
setMsgHdrPropertyAndReload("notAPhishMessage", 1);
}
function setMsgHdrPropertyAndReload(aProperty, aValue)
{
// we want to get the msg hdr for the currently selected message
// change the appropiate property on it then reload the message
- var msgHdr = msgHdrForCurrentMessage();
+ var msgHdr = gMessageDisplay.displayedMessage;
if (msgHdr)
{
msgHdr.setUint32Property(aProperty, aValue);
ReloadMessage();
}
}
/**
@@ -2955,40 +2905,40 @@ function ClearPendingReadTimer()
{
if (gMarkViewedMessageAsReadTimer)
{
clearTimeout(gMarkViewedMessageAsReadTimer);
gMarkViewedMessageAsReadTimer = null;
}
}
-// this is called when layout is actually finished rendering a
+// this is called when layout is actually finished rendering a
// mail message. OnMsgLoaded is called when libmime is done parsing the message
function OnMsgParsed(aUrl)
{
// If rss feed (has 'content-base' header), show summary or load web
// page per pref; earliest we have content DOM is here (onMsgParsed).
FeedSetContentView();
// browser doesn't do this, but I thought it could be a useful thing to test out...
- // If the find bar is visible and we just loaded a new message, re-run
+ // If the find bar is visible and we just loaded a new message, re-run
// the find command. This means the new message will get highlighted and
// we'll scroll to the first word in the message that matches the find text.
var findBar = document.getElementById("FindToolbar");
if (!findBar.hidden)
findBar.onFindAgainCommand(false);
// Run the phishing detector on the message if it hasn't been marked as not
// a scam already.
- var msgHdr = msgHdrForCurrentMessage();
+ var msgHdr = gMessageDisplay.displayedMessage;
if (msgHdr && !msgHdr.getUint32Property("notAPhishMessage"))
gPhishingDetector.analyzeMsgForPhishingURLs(aUrl);
// notify anyone (e.g., extensions) who's interested in when a message is loaded.
- var msgURI = GetLoadedMessage();
+ var msgURI = gFolderDisplay.selectedMessageUris[0];
var observerService = Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService);
observerService.notifyObservers(msgWindow.msgHeaderSink, "MsgMsgDisplayed", msgURI);
// scale any overflowing images
var doc = document.getElementById("messagepane").contentDocument;
var imgs = doc.getElementsByTagName("img");
for each (var img in imgs)
@@ -3010,33 +2960,25 @@ function OnMsgLoaded(aUrl)
// nsIMsgMailNewsUrl.folder throws an error when opening .eml files.
var folder;
try {
folder = aUrl.folder;
}
catch (ex) {}
- var msgURI = GetLoadedMessage();
-
- if (!folder || !msgURI)
+ var msgHdr = gMessageDisplay.displayedMessage;
+ gMessageDisplay.messageLoading = false;
+ gMessageDisplay.messageLoaded = true;
+
+ if (!folder || !msgHdr)
return;
- // If we are in the middle of a delete or move operation, make sure that
- // if the user clicks on another message then that message stays selected
- // and the selection does not "snap back" to the message chosen by
- // SetNextMessageAfterDelete() when the operation completes (bug 243532).
- // But the just loaded message might be getting deleted, if the user
- // deletes it before the message is loaded (bug 183394).
var wintype = document.documentElement.getAttribute('windowtype');
- if (wintype == "mail:messageWindow" ||
- GetThreadTree().view.selection.currentIndex != gSelectedIndexWhenDeleting)
- gNextMessageViewIndexAfterDelete = -2;
-
- var msgHdr = msgHdrForCurrentMessage();
+
gMessageNotificationBar.setJunkMsg(msgHdr);
goUpdateCommand('button_delete');
var markReadAutoMode = gPrefBranch.getBoolPref("mailnews.mark_message_read.auto");
// We just finished loading a message. If messages are to be marked as read
// automatically, set a timer to mark the message is read after n seconds
@@ -3062,17 +3004,17 @@ function OnMsgLoaded(aUrl)
{
MarkMessageAsRead(msgHdr);
}
}
// See if MDN was requested but has not been sent.
HandleMDNResponse(aUrl);
- if (!IsImapMessage(msgURI))
+ if (!gFolderDisplay.selectedMessageIsImap)
return;
var imapServer = folder.server.QueryInterface(Components.interfaces.nsIImapIncomingServer);
if (imapServer.storeReadMailInPFC)
{
// Look in read mail PFC for msg with same msg id - if we find one,
// don't put this message in the read mail pfc.
var outputPFC = imapServer.GetReadMailPFC(true);
@@ -3100,26 +3042,25 @@ function OnMsgLoaded(aUrl)
* no need to check it either.
*/
function HandleMDNResponse(aUrl)
{
if (!aUrl)
return;
var msgFolder = aUrl.folder;
- var msgURI = GetLoadedMessage();
- if (!msgFolder || !msgURI || IsNewsMessage(msgURI))
+ var msgHdr = gFolderDisplay.selectedMessage;
+ if (!msgFolder || !msgHdr || gFolderDisplay.selectedMessageIsNews)
return;
// if the message is marked as junk, do NOT attempt to process a return receipt
// in order to better protect the user
if (SelectedMessagesAreJunk())
return;
- var msgHdr = messenger.msgHdrFromURI(msgURI);
var mimeHdr;
try {
mimeHdr = aUrl.mimeHeaders;
} catch (ex) {
return;
}
@@ -3159,30 +3100,26 @@ function HandleMDNResponse(aUrl)
msgHdr.OrFlags(Components.interfaces.nsMsgMessageFlags.MDNReportSent);
// Commit db changes.
var msgdb = msgFolder.msgDatabase;
if (msgdb)
msgdb.Commit(ADDR_DB_LARGE_COMMIT);
}
-function QuickSearchFocus()
+function QuickSearchFocus()
{
var quickSearchTextBox = document.getElementById('searchInput');
if (quickSearchTextBox)
quickSearchTextBox.focus();
}
function MsgSearchMessages()
{
- var preselectedFolder = null;
- if ("GetFirstSelectedMsgFolder" in window)
- preselectedFolder = GetFirstSelectedMsgFolder();
-
- var args = { folder: preselectedFolder };
+ var args = { folder: gFolderDisplay.displayedFolder };
OpenOrFocusWindow(args, "mailnews:search", "chrome://messenger/content/SearchDialog.xul");
}
function MsgJunkMailInfo(aCheckFirstUse)
{
if (aCheckFirstUse) {
if (!pref.getBoolPref("mailnews.ui.junk.firstuse"))
return;
@@ -3206,17 +3143,17 @@ function MsgJunkMailInfo(aCheckFirstUse)
"centerscreen,resizeable=no,titlebar,chrome,modal", null);
}
function MsgSearchAddresses()
{
var args = { directory: null };
OpenOrFocusWindow(args, "mailnews:absearch", "chrome://messenger/content/ABSearchDialog.xul");
}
-
+
function MsgFilterList(args)
{
OpenOrFocusWindow(args, "mailnews:filterlist", "chrome://messenger/content/FilterListDialog.xul");
}
function GetWindowByWindowType(windowType)
{
var windowManager = Components.classes['@mozilla.org/appshell/window-mediator;1']
@@ -3244,17 +3181,17 @@ function FeedSetContentViewToggle()
gShowFeedSummaryToggle = true;
FeedSetContentView(gShowFeedSummary ? 0 : 1);
}
// Check message format
function FeedCheckContentFormat()
{
// Not an rss message
- if (!IsFeedItem())
+ if (!gFolderDisplay.selectedMessageIsFeed)
return false;
var contentWindowDoc = window.top.content.document;
// Thunderbird 2 rss messages with 'Show article summary' not selected,
// ie message body constructed to show web page in an iframe, can't show
// a summary - notify user.
var rssIframe = contentWindowDoc.getElementById('_mailrssiframe');
--- a/mail/base/content/mailWindowOverlay.xul
+++ b/mail/base/content/mailWindowOverlay.xul
@@ -57,16 +57,18 @@
%brandDTD;
]>
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/x-javascript" src="chrome://messenger/content/mailCommands.js"/>
<script type="application/x-javascript" src="chrome://messenger/content/junkCommands.js"/>
<script type="application/x-javascript" src="chrome://messenger/content/mailWindowOverlay.js"/>
+<script type="application/x-javascript" src="chrome://messenger/content/messageDisplay.js"/>
+<script type="application/x-javascript" src="chrome://messenger/content/folderDisplay.js"/>
<script type="application/x-javascript" src="chrome://messenger-newsblog/content/newsblogOverlay.js"/>
<script type="application/x-javascript" src="chrome://messenger/content/mail-offline.js"/>
<script type="application/x-javascript" src="chrome://global/content/printUtils.js"/>
<script type="application/x-javascript" src="chrome://messenger/content/msgViewPickerOverlay.js"/>
<script type="application/x-javascript" src="chrome://global/content/viewZoomOverlay.js"/>
<stringbundleset id="stringbundleset">
<stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
@@ -722,17 +724,17 @@
oncommand="MsgGetMessage();"/>
<menuitem id="folderPaneContext-openNewWindow"
label="&folderContextOpenNewWindow.label;"
accesskey="&folderContextOpenNewWindow.accesskey;"
oncommand="MsgOpenNewWindowForFolder(null,-1);"/>
<menuitem id="folderPaneContext-openNewTab"
label="&folderContextOpenNewTab.label;"
accesskey="&folderContextOpenNewTab.accesskey;"
- oncommand="MsgOpenNewTabForFolder(null,-1);"/>
+ oncommand="MsgOpenNewTabForFolder();"/>
<menuitem id="folderPaneContext-searchMessages"
label="&folderContextSearchMessages.label;"
accesskey="&folderContextSearchMessages.accesskey;"
command="cmd_search"/>
<menuitem id="folderPaneContext-subscribe"
label="&folderContextSubscribe.label;"
accesskey="&folderContextSubscribe.accesskey;"
oncommand="MsgSubscribe();"/>
new file mode 100644
--- /dev/null
+++ b/mail/base/content/messageDisplay.js
@@ -0,0 +1,460 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Thunderbird Mail Client.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Messaging, Inc.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Andrew Sutherland <asutherland@asutherland.org>
+ * David Bienvenu <bienvenu@nventure.com>
+ * Siddharth Agarwal <sid.bugzilla@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Base abstraction for message display along the line of FolderDisplayWidget,
+ * but for message display. This really only exists to keep
+ * FolderDisplayWidget manageable and free from taking on responsibility for
+ * the (different) horrors of message display. The reality of the situation
+ * is that FolderDisplayWidget still has a lot to do with message display,
+ * and so we are just where helper logic gets to live, but the FDW still
+ * may deal with some things internally.
+ * You should not use this class directly, but rather its friendly subclasses
+ * that live later in this file.
+ */
+function MessageDisplayWidget() {
+}
+MessageDisplayWidget.prototype = {
+ _active: false,
+ get active MessageDisplayWidget_get_active() {
+ return this._active;
+ },
+
+ /**
+ * Track whether the single message display pane is desired to be displayed
+ * (it is actually displayed when active, does't matter when not), or
+ * otherwise the multiple message display pane is desired to be displayed.
+ */
+ _singleMessageDisplay: null,
+ get singleMessageDisplay MessageDisplayWidget_get_singleMessageDisplay() {
+ // when null, assume single message display
+ return this._singleMessageDisplay != false;
+ },
+ set singleMessageDisplay MessageDisplayWidget_set_singleMessageDisplay(
+ aSingleDisplay) {
+ if (this._singleMessageDisplay != aSingleDisplay) {
+ this._singleMessageDisplay = aSingleDisplay;
+ if (this._active)
+ this._updateActiveMessagePane();
+ }
+ },
+
+ /**
+ * Set pane visibility based on this.singleMessageDisplay.
+ */
+ _updateActiveMessagePane: function MessageDisplayWidget_updateMessagePane() {
+ // _singleMessageDisplay can be null, so use the property (getter)
+ document.getElementById("singlemessage").hidden =
+ !this.singleMessageDisplay;
+ document.getElementById("multimessage").hidden =
+ this.singleMessageDisplay;
+ },
+
+ /**
+ * Cleanup the MessageDisplayWidget in preparation for going away. Called by
+ * FolderDisplayWidget's close method.
+ */
+ _close: function MessageDisplayWidget_close() {
+ // mark ourselves inactive without doing any work.
+ this._active = false;
+ },
+
+ /**
+ * The FolderDisplayWidget that owns us.
+ */
+ folderDisplay: null,
+ /**
+ * The currently displayed message's nsIMsgDBHdr. null if there's no message.
+ */
+ displayedMessage: null,
+
+ clearDisplay: function MessageDisplayWidget_clearDisplay() {
+ this.displayedMessage = null;
+ this.messageLoading = false;
+ this.messageLoaded = false;
+ ClearPendingReadTimer();
+ ClearMessagePane();
+ },
+
+ onCreatedView: function MessageDisplayWidget_onCreatedView() {
+ // we need to compel setting this because nsMsgSearchDBView defaults it on
+ this.folderDisplay.view.dbView.suppressMsgDisplay = !this.visible;
+ },
+
+ /**
+ * FolderDisplayWidget tells us when it is killing the view, which means our
+ * displayed message is no longer valid.
+ */
+ onDestroyingView: function MessageDisplayWidget_onDestroyingView(
+ aFolderIsComingBack) {
+ this.displayedMessage = null;
+ // The only time we want to do anything is when the folder is not coming
+ // back. If it is coming back, we can handle things when it shows up.
+ if (!aFolderIsComingBack && this._active) {
+ // and in this case, we just want to clear things.
+ this.clearDisplay();
+ this.singleMessageDisplay = true;
+ }
+ },
+
+ /**
+ * FolderDisplayWidget tells us when a message is being displayed.
+ */
+ onDisplayingMessage: function MessageDisplayWidget_onDisplayingMessage(
+ aMsgHdr) {
+ this.displayedMessage = aMsgHdr;
+ this.messageLoading = true;
+ this.messageLoaded = false;
+ },
+
+ /**
+ * The maximum number of messages to summarize at any given time. If there
+ * are more messages than this, we don't summarize, and instead give a blank
+ * window pane. Arguably something that says "there are two many messages"
+ * would be a better idea.
+ */
+ MAX_MESSAGES_TO_SUMMARIZE: 100,
+
+ /**
+ * FolderDisplayWidget tells us when the set of selected messages has changed.
+ * FDW is doing this because an nsMsgDBView/subclass called
+ * summarizeSelection. Although the call is purely an outgrowth of the
+ * introduction of folder summaries, it also provides a means for us to
+ * completely replace the nsMsgDBView logic. Namely, we get first bat at
+ * taking an action as a result of the selection change, and we can cause the
+ * nsMsgDBView to not do anything (by returning true).
+ * This notification will come prior to an onDisplayingMessage notification,
+ * and we will only get that notification if we return false and the
+ * nsMsgDBView logic wanted to display a message (read: there is exactly
+ * one message displayed and it wasn't already displayed.)
+ *
+ * The prime responsibilities of this function are:
+ * - To make sure the right message pane (single or multi) is displayed.
+ * - To kick off a multi-message or thread summarization if multiple messages
+ * are selected.
+ * - To throttle the rate at which we perform summarizations in case the
+ * selection is being updated frequently. This could be as a result of the
+ * user holding down shift and using their keyboard to expand the selection,
+ * use of the archive mechanism, or other.
+ * - To clear the message pane if no messages are selected. This used to be
+ * triggered by nsMsgDBView::SelectionChanged but is now our responsibility.
+ *
+ * In the event that the controlling preference for message summarization is
+ * not enabled (mail.operate_on_msgs_in_collapsed_threads), and there is a
+ * multi-selection, we just clear the display.
+ *
+ * @return true if we handled the selection notification and the nsMsgDBView
+ * should do nothing, false if we did not and the nsMsgDBView should use its
+ * logic to display a message.
+ */
+ onSelectedMessagesChanged:
+ function MessageDisplayWidget_onSelectedMessagesChanged() {
+ // If we are not active, we should not be touching things. pretend we are
+ // handling whatever is happening so the nsMsgDBView doesn't try and do
+ // anything. makeActive will trigger a fake SelectionChanged notification
+ // when we switch, which should put everything in its right place.
+ if (!this.active)
+ return true;
+
+ let selectedCount = this.folderDisplay.selectedCount;
+
+ if (selectedCount == 0) {
+ // davida, put your folder summary stuff here.
+ this.clearDisplay();
+ this.singleMessageDisplay = true;
+ }
+ else if (selectedCount == 1) {
+ // the display of the message is happening without us
+ this.singleMessageDisplay = true;
+
+ // This is the only case we don't handle and want the nsMsgDBView to
+ // take care of.
+ return false;
+ }
+ // we have a limit on the number of messages and if the pref is enabled
+ else if ((selectedCount < this.MAX_MESSAGES_TO_SUMMARIZE) &&
+ gPrefBranch.getBoolPref("mail.operate_on_msgs_in_collapsed_threads")) {
+ // _showSummary is responsible for handling the "don't resummarize too
+ // often" logic, as well as updating singleMessageDisplay.
+ this._showSummary();
+ }
+ // and so we clear things
+ else {
+ this.clearDisplay();
+ this.singleMessageDisplay = true;
+ }
+
+ return true;
+ },
+
+ /**
+ * If we are already summarized and we get a new request to summarize, require
+ * that the selection has been stable for at least this many milliseconds
+ * before resummarizing.
+ * Unit tests know about this variable and poke at it, so don't change the name
+ * without making sure you update the unit tests. (Not that you would commit
+ * code without first running all tests yourself...)
+ */
+ SUMMARIZATION_SELECTION_STABILITY_INTERVAL_MS: 100,
+ /**
+ * The timeout ID resulting from the call to window.setTimeout that we are
+ * using to require the selection be 'stabilized' before re-summarizing.
+ */
+ _summaryStabilityTimeout: null,
+
+ /**
+ * Updates message summaries with care to throttle the summarization when the
+ * selection is rapidly changing. We require that either we have not
+ * summarized anything 'recently', or that the selection has been stable for
+ * SUMMARIZATION_SELECTION_STABILITY_INTERVAL_MS ms before we update the
+ * summary. 'Recently' for our purposes means that it has been at least
+ * SUMMARIZATION_SELECTION_STABILITY_INTERVAL_MS ms since our last summary.
+ *
+ * Example event sequences (assuming a 100ms stability interval):
+ * - User selects a collapsed thread => we summarize the selection and set a
+ * 100ms timer set to call _clearSummaryTimer.
+ * - User extends the selection 50ms later => the timer has not elapsed so we
+ * reset it to 100ms to call _showSummary.
+ * - User extends the selection yet again 50ms later => timer has not elapsed
+ * so we reset it to 100ms to call _showSummary.
+ * - 100ms later, the timer elapses => we call _showSummary which updates the
+ * summary.
+ * - 2 seconds later, the user selects some other messages => we summarize the
+ * select and set a 100ms timer set to call _clearSummaryTimer.
+ * - 100ms later, _clearSummaryTimer clears _summaryStabilityTimeout so that
+ * the next selection change will immediately summarize.
+ *
+ * @param aIsCallback Is this a callback to ourselves? Callers should not set
+ * this, leaving it undefined.
+ */
+ _showSummary: function MessageDisplayWidget_showSummary(aIsCallback) {
+ // note: if we are in this function, we already know that summaries are
+ // enabled.
+
+ // If this is not a callback from the timeout and we currently have a
+ // timeout, that means that we need to wait for the selection to stabilize.
+ // The fact that we are getting called means the selection has just changed
+ // yet again and is not stable, so reset the timer for the full duration.
+ if (!aIsCallback && this._summaryStabilityTimeout != null) {
+ clearTimeout(this._summaryStabilityTimeout);
+ this._summaryStabilityTimeout =
+ setTimeout(this._wrapShowSummary,
+ this.SUMMARIZATION_SELECTION_STABILITY_INTERVAL_MS,
+ this);
+ return;
+ }
+
+ // Bail if our selection count has stabilized outside an acceptable range.
+ let selectedCount = this.folderDisplay.selectedCount;
+ if (selectedCount < 2 || selectedCount > this.MAX_MESSAGES_TO_SUMMARIZE)
+ return;
+
+ // Setup a timeout call to _clearSummaryTimer so that we don't try and
+ // summarize again within 100ms of now. Do this before calling
+ // the summarization logic in case it throws an exception.
+ this._summaryStabilityTimeout =
+ setTimeout(this._clearSummaryTimer,
+ this.SUMMARIZATION_SELECTION_STABILITY_INTERVAL_MS,
+ this);
+
+ // figure out if we're looking at one thread or more than one thread
+ let selectedMessages = this.folderDisplay.selectedMessages;
+ if (selectedMessages[0].threadId != selectedMessages[1].threadId)
+ summarizeMultipleSelection(selectedMessages);
+ else
+ summarizeThread(selectedMessages);
+ this.singleMessageDisplay = false;
+ },
+ _wrapShowSummary: function MessageDisplayWidget__wrapShowSummary(aThis) {
+ aThis._showSummary(true);
+ },
+ /**
+ * Just clears the _summaryStabilityTimeout attribute so we can use it as a
+ * means of checking if we are allowed to display the summary immediately.
+ */
+ _clearSummaryTimer: function MessageDisplayWidget__clearSummaryTimer(aThis) {
+ aThis._summaryStabilityTimeout = null;
+ },
+
+ /**
+ * Called by the FolderDisplayWidget when it is being made active again and
+ * it's time for us to step up and re-display or clear the message as
+ * demanded by our multiplexed tab implementation.
+ */
+ makeActive: function MessageDisplayWidget_makeActive() {
+ let wasInactive = !this._active;
+ this._active = true;
+
+ if (wasInactive) {
+ // (see our usage below)
+ let preDisplayedMessage = this.displayedMessage;
+ // Force a synthetic selection changed event. This will propagate through
+ // to a call to onSelectedMessagesChanged who will handle making sure the
+ // right message pane is in use, etc.
+ this.folderDisplay.view.dbView.selectionChanged();
+ // The one potential problem is that the message view only triggers message
+ // streaming if it doesn't think the message is already displayed. In that
+ // case we need to force a re-display by calling reloadMessage. We can
+ // detect that case by seeing if our displayedMessage value changes its
+ // value during this call (because we will receive a onDisplayingMessage
+ // notification). If we should be displaying a single message but the
+ // value does not change, we need to force a re-display.
+ if (this.singleMessageDisplay && this.displayedMessage &&
+ (this.displayedMessage == preDisplayedMessage))
+ this.folderDisplay.view.dbView.reloadMessage();
+ }
+
+ this._updateActiveMessagePane();
+ },
+
+ /**
+ * Called by the FolderDisplayWidget when it is being made inactive or no
+ * longer requires messages to be displayed.
+ */
+ makeInactive: function MessageDisplayWidget_makeInactive() {
+ this._active = false;
+ }
+};
+
+/**
+ * Display widget abstraction for the 3-pane message view's preview pane/message
+ * pane. Like the DisplayWidget, it is multiplexed.
+ */
+function MessagePaneDisplayWidget() {
+ MessageDisplayWidget.call(this);
+ this._visible = true;
+}
+MessagePaneDisplayWidget.prototype = {
+ __proto__: MessageDisplayWidget.prototype,
+
+ get visible MessageDisplayWidget_get_visible() {
+ return this._visible;
+ },
+ /**
+ * Tell us whether the message pane is visible or not; this should reflect
+ * reality and does not define reality. (Setting this to false does not
+ * hide the message pane, it merely makes us think it is hidden.)
+ */
+ set visible MessageDisplayWidget_set_visible(aVisible) {
+ // Ignore this if we are inactive. We don't want to get faked out by things
+ // happening after our tab has closed up shop.
+ if (!this._active)
+ return;
+
+ // no-op if it's the same
+ if (aVisible == this._visible)
+ return;
+
+ this._visible = aVisible;
+ // Update suppression. If we were not visible and now are visible, the db
+ // view itself will handle triggering the message display for us if the
+ // message was not currently being displayed...
+ let dbView = this.folderDisplay.view.dbView;
+ if (dbView) {
+ let treeSelection = this.folderDisplay.treeSelection;
+ // flag if we need to force the redisplay manually...
+ let needToReloadMessage = treeSelection.count &&
+ dbView.currentlyDisplayedMessage == treeSelection.currentIndex;
+ dbView.suppressMsgDisplay = !this._visible;
+ if (needToReloadMessage)
+ dbView.reloadMessage();
+ }
+ // But if we are no longer visible, it's on us to clear the display.
+ if (!aVisible)
+ this.clearDisplay();
+ },
+};
+
+/**
+ * Message display widget for the "message" tab that is the tab-based equivalent
+ * of the standalone message window.
+ */
+function MessageTabDisplayWidget() {
+ MessageDisplayWidget.call(this);
+}
+MessageTabDisplayWidget.prototype = {
+ __proto__: MessageDisplayWidget.prototype,
+
+ /**
+ * The message tab always has a visible message pane.
+ */
+ get visible() {
+ return true;
+ },
+ set visible(aIgnored) {
+ },
+
+ onSelectedMessagesChanged:
+ function MessageTabDisplayWidget_onSelectedMessagesChanged() {
+ this.__proto__.__proto__.onSelectedMessagesChanged.apply(this, arguments);
+ if (!this.closing)
+ document.getElementById('tabmail').setTabTitle(this.folderDisplay._tabInfo);
+ },
+
+ /**
+ * A message tab should never ever be blank. Close the tab if we become
+ * blank.
+ */
+ clearDisplay: function MessageTabDisplayWidget_clearDisplay() {
+ if (!this.closing) {
+ this.closing = true;
+ document.getElementById('tabmail').closeTab(this.folderDisplay._tabInfo);
+ }
+ }
+};
+
+/**
+ * The search dialog has no message preview pane, and so wants a message
+ * display widget that is never visible. No one other than the search
+ * dialog should use this because the search dialog is bad UI.
+ */
+function NeverVisisbleMessageDisplayWidget() {
+ MessageDisplayWidget.call(this);
+}
+NeverVisisbleMessageDisplayWidget.prototype = {
+ __proto__: MessageDisplayWidget.prototype,
+ get visible() {
+ return false;
+ },
+ onSelectedMessagesChanged: function() {
+ return false;
+ },
+ _updateActiveMessagePane: function() {
+ },
+};
\ No newline at end of file
--- a/mail/base/content/messageWindow.js
+++ b/mail/base/content/messageWindow.js
@@ -1,129 +1,242 @@
-# -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is Mozilla Communicator client code, released
-# March 31, 1998.
-#
-# The Initial Developer of the Original Code is
-# Netscape Communications Corporation.
-# Portions created by the Initial Developer are Copyright (C) 1998-1999
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-#
-# Alternatively, the contents of this file may be used under the terms of
-# either the GNU General Public License Version 2 or later (the "GPL"), or
-# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-# in which case the provisions of the GPL or the LGPL are applicable instead
-# of those above. If you wish to allow use of your version of this file only
-# under the terms of either the GPL or the LGPL, and not to allow others to
-# use your version of this file under the terms of the MPL, indicate your
-# decision by deleting the provisions above and replace them with the notice
-# and other provisions required by the GPL or the LGPL. If you do not delete
-# the provisions above, a recipient may use your version of this file under
-# the terms of any one of the MPL, the GPL or the LGPL.
-#
-# ***** END LICENSE BLOCK *****
+/** ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Communicator client code, released
+ * March 31, 1998.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1998-1999
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
/* This is where functions related to the standalone message window are kept */
+Components.utils.import("resource://app/modules/jsTreeSelection.js");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
// from MailNewsTypes.h
const nsMsgKey_None = 0xFFFFFFFF;
const nsMsgViewIndex_None = 0xFFFFFFFF;
/* globals for a particular window */
-var gCurrentMessageUri;
-var gCurrentFolderUri;
-var gThreadPaneCommandUpdater = null;
-var gNextMessageViewIndexAfterDelete = -2;
-var gCurrentFolderToRerootForStandAlone;
-var gRerootOnFolderLoadForStandAlone = false;
-var gNextMessageAfterLoad = null;
-var gMessageToLoad = nsMsgKey_None;
+/// we have no tree view; let people know that.
+var gFolderTreeView = null;
+
+var gFolderDisplay;
+var gMessageDisplay;
+
+/**
+ * We subclass FolderDisplayWidget:
+ * - Because it assumes some thread-pane things that do not apply to us and we
+ * want to no-op those things out.
+ * - To intercept queries about the selected message so that we can do the
+ * .eml file thing. We were originally trying to avoid involving the
+ * nsMsgDBView, but that might not be important anymore. (future work)
+ */
+function StandaloneFolderDisplayWidget(aMessageDisplayWidget) {
+ FolderDisplayWidget.call(this, null, aMessageDisplayWidget);
+ // do not set the actual treeBox variable or our superclass might try and do
+ // weird rooting things we don't want to have to think about right now.
+ this._magicTreeSelection = new JSTreeSelection(this.treeBox);
+}
+StandaloneFolderDisplayWidget.prototype = {
+ __proto__: FolderDisplayWidget.prototype,
+
+ /**
+ * If we have a displayed message, then we've got 1 message, otherwise 0.
+ */
+ get selectedCount() {
+ return this.messageDisplay.displayedMessage ? 1 : 0;
+ },
+
+ /**
+ * If we have a selected message, it's the one displayed! This is more
+ * straight-forward than having you trace through the tree selection and
+ * db view logic.
+ */
+ get selectedMessage() {
+ return this.messageDisplay.displayedMessage;
+ },
-// the folderListener object
-var folderListener = {
- OnItemAdded: function(parentItem, item) {},
+ /**
+ * If we have a selected message, it's the one displayed! This is more
+ * straight-forward than having you trace through the tree selection and
+ * db view logic.
+ */
+ get selectedMessages() {
+ return this.messageDisplay.displayedMessage ?
+ [this.messageDisplay.displayedMessage] : [];
+ },
- OnItemRemoved: function(parentItem, item) {},
- OnItemPropertyChanged: function(item, property, oldValue, newValue) {},
- OnItemIntPropertyChanged: function(item, property, oldValue, newValue) {
- if (item.URI == gCurrentFolderUri) {
- if (property.toString() == "TotalMessages" || property.toString() == "TotalUnreadMessages") {
- UpdateStandAloneMessageCounts();
- }
+ /**
+ * We never have a real treeview, so we always want to tell the view about
+ * the fake tree box so it will actually do something in NoteChange.
+ */
+ onCreatedView:
+ function StandaloneMessageDisplayWidget_onCreatedView() {
+ this._fakeTreeBox.view = this.view.dbView;
+ // only if we're not dealing with a dummy message (from .eml file /
+ // attachment should we try and hook up the selection object.) Otherwise
+ // the view will not operate in stand alone message mode.
+ // XXX the sequencing here may break re-using a message window that is
+ // showing an .eml file to go to a real message, at least in terms of
+ // having the selection object properly associated with the tree.
+ if (!this.messageDisplay.isDummy) {
+ this.view.dbView.setTree(this._fakeTreeBox);
+ this.view.dbView.selection = this._magicTreeSelection;
}
+ this.__proto__.__proto__.onCreatedView.call(this);
+ },
+
+ _superSelectedMessageUrisGetter:
+ FolderDisplayWidget.prototype.__lookupGetter__('selectedMessageUris'),
+ /**
+ * Check with the message display widget to see if it has a dummy; if so, just
+ * return the dummy's URI, as the nsMsgDBView logic that our superclass uses
+ * falls down in that case.
+ */
+ get selectedMessageUris() {
+ if (this.messageDisplay.displayedUri)
+ return [this.messageDisplay.displayedUri];
+ return this._superSelectedMessageUrisGetter.call(this);
},
- OnItemBoolPropertyChanged: function(item, property, oldValue, newValue) {},
- OnItemUnicharPropertyChanged: function(item, property, oldValue, newValue){},
- OnItemPropertyFlagChanged: function(item, property, oldFlag, newFlag) {},
+
+ /// folder display will want to show the thread pane; we need do nothing
+ _showThreadPane: function () {},
+ _showAccountCentral: function () {},
+
+ _updateThreadDisplay: function () {},
+
+ onMessageCountsChanged:
+ function StandaloneFolderDisplayWidget_onMessageCountsChaned() {
+ UpdateStatusMessageCounts();
+ },
+};
+
- OnItemEvent: function(folder, event) {
- var eventType = event.toString();
+/**
+ * Display widget abstraction for a standalone message display. Right now
+ * I think this means the standalone message window, and not the 'message in a
+ * tab' thing, which is really just a perverted configuration of the 3-pane
+ * format.
+ */
+function StandaloneMessageDisplayWidget() {
+ MessageDisplayWidget.call(this);
+ /**
+ * Indicate whether the message being displayed is a 'dummy' because it is
+ * backed not by an nsIMsgDBHdr but instead by a file on disk or an
+ * attachment on some mail message.
+ */
+ this.isDummy = false;
+ /**
+ * When displaying a dummy message, this is the URI of the message that we are
+ * displaying. If we are not displaying a dummy message, this is null.
+ */
+ this.displayedUri = null;
+}
+StandaloneMessageDisplayWidget.prototype = {
+ __proto__: MessageDisplayWidget.prototype,
- if (eventType == "DeleteOrMoveMsgCompleted")
- HandleDeleteOrMoveMsgCompleted(folder);
- else if (eventType == "DeleteOrMoveMsgFailed")
- HandleDeleteOrMoveMsgFailed(folder);
- else if (eventType == "FolderLoaded") {
- if (folder) {
- var uri = folder.URI;
- if (uri == gCurrentFolderToRerootForStandAlone) {
- gCurrentFolderToRerootForStandAlone = null;
- folder.endFolderLoading();
- if (gRerootOnFolderLoadForStandAlone) {
- RerootFolderForStandAlone(uri);
- }
- }
- }
+ /**
+ * The message pane is a standalone display widget is always visible.
+ */
+ get visible() {
+ return true;
+ },
+ set visible(aIgnored) {
+ },
+
+ /**
+ * Display the external message (from disk or attachment) named by the URI.
+ */
+ displayExternalMessage:
+ function StandaloneMessageDisplayWidget_displayExternalMessage(aUri) {
+ this.isDummy = true;
+ this.displayedUri = aUri;
+ this.onDisplayingMessage(messageHeaderSink.dummyMsgHeader);
+ UpdateMailToolbar("external message display");
+ // null out the selection on the view so it operates in stand alone mode
+ this.folderDisplay.view.dbView.selection = null;
+ this.folderDisplay.view.dbView.loadMessageByUrl(aUri);
+ },
+
+ clearDisplay: function () {
+ window.close();
+ },
+ _updateActiveMessagePane: function() {
+ // no-op. the message pane is always visible.
+ },
+
+ onDisplayingMessage:
+ function StandaloneMessageDisplayWidget_onDisplayingMessage(aMsgHdr) {
+ this.__proto__.__proto__.onDisplayingMessage.call(this, aMsgHdr);
+
+ // - set the window title to the message subject (and maybe the app name)
+ let title = aMsgHdr.mime2DecodedSubject;
+ if (!gPlatformOSX)
+ title += " - " + gBrandBundle.getString("brandFullName");
+ document.title = title;
+
+ this.isDummy = aMsgHdr.folder == null;
+ if (!this.isDummy)
+ this.displayedUri = null;
+ },
+
+ onSelectedMessagesChanged: function () {
+ if (this.folderDisplay.treeSelection.count == 0) {
+ window.close();
+ return true;
}
- else if (eventType == "JunkStatusChanged") {
- HandleJunkStatusChanged(folder);
- }
- }
-}
+ return false;
+ },
+};
var messagepaneObserver = {
canHandleMultipleItems: false,
onDrop: function (aEvent, aData, aDragSession)
{
var sourceUri = aData.data;
- if (sourceUri != gCurrentMessageUri)
+ if (!gFolderDisplay.selectedMessaage ||
+ sourceUri != gFolderDisplay.selectedMessageUris[0])
{
- var msgHdr = GetMsgHdrFromUri(sourceUri);
-
- // Reset the window's message uri and folder uri vars, and
- // update the command handlers to what's going to be used.
- // This has to be done before the call to CreateView().
- gCurrentMessageUri = sourceUri;
- gCurrentFolderUri = msgHdr.folder.URI;
- UpdateMailToolbar('onDrop');
-
- // even if the folder uri's match, we can't use the existing view
- // (msgHdr.folder.URI == windowID.gCurrentFolderUri)
- // the reason is quick search and mail views.
- // see bug #187673
- CreateView(aDragSession.sourceNode.ownerDocument.defaultView.gDBView);
- LoadMessageByMsgKey(msgHdr.messageKey);
+ var msgHdr = messenger.msgHdrFromURI(sourceUri);
+ let originGlobal = aDragSession.sourceNode.ownerDocument.defaultValue;
+ gFolderDisplay.cloneView(originGlobal.gFolderDisplay.view);
+ gFolderDisplay.selectMessage(msgHdr);
}
},
onDragOver: function (aEvent, aFlavour, aDragSession)
{
var messagepanebox = document.getElementById("messagepanebox");
messagepanebox.setAttribute("dragover", "true");
},
@@ -144,295 +257,144 @@ var messagepaneObserver = {
getSupportedFlavours: function ()
{
var flavourSet = new FlavourSet();
flavourSet.appendFlavour("text/x-moz-message");
return flavourSet;
}
};
-function nsMsgDBViewCommandUpdater()
-{}
-
-function UpdateStandAloneMessageCounts()
+function UpdateStatusMessageCounts()
{
// hook for extra toolbar items
var observerService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
observerService.notifyObservers(window, "mail:updateStandAloneMessageCounts", "");
}
-nsMsgDBViewCommandUpdater.prototype =
-{
- updateCommandStatus : function()
- {
- // the back end is smart and is only telling us to update command status
- // when the # of items in the selection has actually changed.
- UpdateMailToolbar("dbview, std alone window");
- },
-
- displayMessageChanged : function(aFolder, aSubject, aKeywords)
- {
- setTitleFromFolder(aFolder, aSubject);
- ClearPendingReadTimer(); // we are loading / selecting a new message so kill the mark as read timer for the currently viewed message
- gCurrentMessageUri = gDBView.URIForFirstSelectedMessage;
- UpdateStandAloneMessageCounts();
- goUpdateCommand("button_delete");
- goUpdateCommand("button_junk");
- goUpdateCommand("button_goBack");
- goUpdateCommand("button_goForward");
- goUpdateCommand("button_reply");
- goUpdateCommand("button_replyall");
- goUpdateCommand("button_replylist");
- },
-
- updateNextMessageAfterDelete : function()
- {
- SetNextMessageAfterDelete();
- },
-
- summarizeSelection : function() {return false},
-
- QueryInterface : function(iid)
- {
- if (iid.equals(Components.interfaces.nsIMsgDBViewCommandUpdater) ||
- iid.equals(Components.interfaces.nsISupports))
- return this;
-
- throw Components.results.NS_NOINTERFACE;
- }
-}
-
-function HandleDeleteOrMoveMsgCompleted(folder)
-{
- if ((folder.URI == gCurrentFolderUri))
- {
- gDBView.onDeleteCompleted(true);
- if (gNextMessageViewIndexAfterDelete != nsMsgViewIndex_None)
- {
- var nextMstKey = gDBView.getKeyAt(gNextMessageViewIndexAfterDelete);
-
- if (pref.getBoolPref("mail.close_message_window.on_delete")) {
- // Tell the main window to select the next message since we
- // won't be viewing it automatically in the standalone window.
- var treeView = window.opener.document.getElementById("threadTree").view;
- if (gDBView.removeRowOnMoveOrDelete && gNextMessageViewIndexAfterDelete >= 0) {
- gDBView.suppressCommandUpdating = true;
- treeView.selection.select(gNextMessageViewIndexAfterDelete);
- treeView.selectionChanged();
-
- window.opener.EnsureRowInThreadTreeIsVisible(gNextMessageViewIndexAfterDelete);
- gDBView.suppressCommandUpdating = false;
- }
-
- window.close();
- }
- else if (nextMstKey != nsMsgKey_None) {
- LoadMessageByViewIndex(gNextMessageViewIndexAfterDelete);
- }
- else {
- window.close();
- }
- }
- else
- {
- // close the stand alone window because there are no more messages in the folder
- window.close();
- }
- }
-}
-
-function HandleDeleteOrMoveMsgFailed(folder)
-{
- gDBView.onDeleteCompleted(false);
-}
-
-function IsCurrentLoadedFolder(folder)
-{
- return (folder.URI == gCurrentFolderUri);
-}
-
-
// we won't show the window until the onload() handler is finished
// so we do this trick (suggested by hyatt / blaker)
function OnLoadMessageWindow()
{
setTimeout(delayedOnLoadMessageWindow, 0); // when debugging, set this to 5000, so you can see what happens after the window comes up.
}
function delayedOnLoadMessageWindow()
{
HideMenus();
ShowMenus();
MailOfflineMgr.init();
CreateMailWindowGlobals();
verifyAccounts(null);
+ /**
+ * Create a message listener so that we can update the title once the message
+ * finishes streaming when it's a dummy.
+ */
+ gMessageListeners.push({
+ onStartHeaders: function () {},
+ onEndHeaders: function() {
+ if (gMessageDisplay.isDummy)
+ gMessageDisplay.onDisplayingMessage(messageHeaderSink.dummyMsgHeader);
+ UpdateMailToolbar(".eml/message from attachment finished loading");
+ },
+ onEndAttachments: function () {},
+ });
+
InitMsgWindow();
messenger.setWindow(window, msgWindow);
// FIX ME - later we will be able to use onload from the overlay
OnLoadMsgHeaderPane();
- var nsIFolderListener = Components.interfaces.nsIFolderListener;
- var notifyFlags = nsIFolderListener.removed | nsIFolderListener.event | nsIFolderListener.intPropertyChanged;
- Components.classes["@mozilla.org/messenger/services/session;1"]
- .getService(Components.interfaces.nsIMsgMailSession)
- .AddFolderListener(folderListener, notifyFlags);
-
- var originalView = null;
- var folder = null;
- var messageUri;
- var loadCustomMessage = false; //set to true when either loading a message/rfc822 attachment or a .eml file
- if (window.arguments)
- {
- if (window.arguments[0])
- {
- try
- {
- messageUri = window.arguments[0];
- if (messageUri instanceof Components.interfaces.nsIURI)
- {
- loadCustomMessage = /type=application\/x-message-display/.test(messageUri.spec);
- gCurrentMessageUri = messageUri.spec;
- if (messageUri.folder && messageUri instanceof Components.interfaces.nsIMsgMailNewsUrl)
- folder = messageUri.folder;
- }
- }
- catch(ex)
- {
- folder = null;
- dump("## ex=" + ex + "\n");
- }
-
- if (!gCurrentMessageUri)
- gCurrentMessageUri = window.arguments[0];
- }
- else
- gCurrentMessageUri = null;
-
- if (window.arguments[1])
- gCurrentFolderUri = window.arguments[1];
- else
- gCurrentFolderUri = folder ? folder.URI : null;
-
- if (window.arguments[2])
- originalView = window.arguments[2];
-
- }
-
- CreateView(originalView);
-
gPhishingDetector.init();
// initialize the customizeDone method on the customizeable toolbar
var toolbox = document.getElementById("mail-toolbox");
toolbox.customizeDone = function(aEvent) { MailToolboxCustomizeDone(aEvent, "CustomizeMailToolbar"); };
var toolbarset = document.getElementById('customToolbars');
toolbox.toolbarset = toolbarset;
- setTimeout(OnLoadMessageWindowDelayed, 0, loadCustomMessage);
+ SetupCommandUpdateHandlers();
- SetupCommandUpdateHandlers();
+ gMessageDisplay = new StandaloneMessageDisplayWidget();
+ gFolderDisplay = new StandaloneFolderDisplayWidget(gMessageDisplay);
+ gFolderDisplay.msgWindow = msgWindow;
+ gFolderDisplay.messenger = messenger;
+
+ setTimeout(actuallyLoadMessage, 0);
}
-function OnLoadMessageWindowDelayed(loadCustomMessage)
-{
- if (loadCustomMessage)
- {
- gDBView.suppressMsgDisplay = false;
- gDBView.loadMessageByUrl(gCurrentMessageUri);
- }
- else
+function actuallyLoadMessage() {
+ /*
+ * Our actual use cases that drive the arguments we take are:
+ * 1) Displaying a message from disk or that was an attachment on a message.
+ * Such messages have no (real) message header and must come in the form of
+ * a URI. (The message display code creates a 'dummy' header.)
+ * 2) Displaying a message that has a header available, either as a result of
+ * the user selecting a message in another window to spawn us or through
+ * some indirection like displaying a message by message-id. (The
+ * newsgroup UI exposes this, as well as the spotlight/vista indexers.)
+ *
+ * We clone views when possible for:
+ * - Consistency of navigation within the message display. Users would find
+ * it odd if they showed a message from a cross-folder view but ended up
+ * navigating around the message's actual folder.
+ * - Efficiency. It's faster to clone a view than open a new one.
+ *
+ * Our argument idioms for the use cases are thus:
+ * 1) [A Message URI] where the URI is an nsIURL corresponding to a message
+ * on disk or that is an attachment part on another message.
+ * 2) [A Message header, (optional) the origin DBViewWraper]
+ *
+ * Our original set of arguments, in case these get passed in and you're
+ * wondering why we explode, was:
+ * 0: A message URI, string or nsIURI.
+ * 1: A folder URI. If arg 0 was an nsIURI, it may have had a folder attribute.
+ * 2: The nsIMsgDBView used to open us.
+ */
+ if (window.arguments && window.arguments.length)
{
- var msgKey = extractMsgKeyFromURI(gCurrentMessageUri);
- var viewIndex = gDBView.findIndexFromKey(msgKey, true);
- // the message may not appear in the view if loaded from a search dialog
- if (viewIndex != nsMsgViewIndex_None)
- LoadMessageByViewIndex(viewIndex);
- else
- messenger.openURL(gCurrentMessageUri);
+ // message header?
+ if (window.arguments[0] instanceof Components.interfaces.nsIMsgDBHdr) {
+ let msgHdr = window.arguments[0];
+ let originViewWrapper = window.arguments.length > 1 ?
+ window.arguments[1] : null;
+ if (originViewWrapper)
+ gFolderDisplay.cloneView(originViewWrapper);
+ else
+ gFolderDisplay.show(msgHdr.folder);
+ gFolderDisplay.selectMessage(msgHdr);
+ }
+ // it must be a URI for a message lacking a backing header
+ else {
+ // Here's how this goes. nsMessenger::LoadURL checks out the URL we
+ // pass it, and if it sees that the URI starts with "file:" or contains
+ // "type=application/x-message-display" then it knows it needs to
+ // create a dummy header. It gets the 'dummyMsgHeader' property from
+ // the js message header sink.
+ // Additionally, nsMessenger::MsgHdrFromURI checks the URI we pass it
+ // and if it meets either of those same constraints (assuming it has a
+ // msgWindow), it will retrieve the header sink off the msgWindow, get
+ // the dummy header, and return that.
+ // so...
+ // - create a search view for the standalone dude
+ gFolderDisplay.view.openSearchView();
+ // - load the message
+ let messageURI = window.arguments[0];
+ if (messageURI instanceof Components.interfaces.nsIURI)
+ messageURI = messageURI.spec;
+ gMessageDisplay.displayExternalMessage(messageURI);
+ }
}
- gNextMessageViewIndexAfterDelete = gDBView.msgToSelectAfterDelete;
- UpdateStandAloneMessageCounts();
+
+ gFolderDisplay.makeActive();
// set focus to the message pane
window.content.focus();
-
- // since we just changed the pane with focus we need to update the toolbar to reflect this
- // XXX TODO
- // can we optimize
- // and just update cmd_delete and button_delete?
- UpdateMailToolbar("focus");
-}
-
-function CreateView(originalView)
-{
- var msgFolder = GetLoadedMsgFolder();
-
- // extract the sort type, the sort order,
- var sortType;
- var sortOrder;
- var viewFlags;
- var viewType;
-
- if (originalView)
- {
- viewType = originalView.viewType;
- viewFlags = originalView.viewFlags;
- sortType = originalView.sortType;
- sortOrder = originalView.sortOrder;
- }
- else if (msgFolder)
- {
- var msgDatabase = msgFolder.msgDatabase;
- if (msgDatabase)
- {
- var dbFolderInfo = msgDatabase.dBFolderInfo;
- sortType = dbFolderInfo.sortType;
- sortOrder = dbFolderInfo.sortOrder;
- viewFlags = dbFolderInfo.viewFlags;
- viewType = dbFolderInfo.viewType;
- msgDatabase = null;
- dbFolderInfo = null;
- }
- }
- else
- {
- // this is a hack to make opening a stand-alone msg window on a
- // .eml file work. We use a search view since its much more tolerant
- // of not having a folder.
- viewType = nsMsgViewType.eShowSearch;
- }
-
- // create a db view
- CreateBareDBView(originalView, msgFolder, viewType, viewFlags, sortType, sortOrder);
-
- var uri;
- if (gCurrentMessageUri)
- uri = gCurrentMessageUri;
- else if (gCurrentFolderUri)
- uri = gCurrentFolderUri;
- else
- uri = null;
-
- SetUpToolbarButtons(uri);
-
- // hook for extra toolbar items
- var observerService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
- observerService.notifyObservers(window, "mail:setupToolbarItems", uri);
-}
-
-function extractMsgKeyFromURI()
-{
- var msgKey = -1;
- var msgHdr = messenger.msgHdrFromURI(gCurrentMessageUri);
- if (msgHdr)
- msgKey = msgHdr.messageKey;
- return msgKey;
}
function ShowMenus()
{
var openMail3Pane_menuitem = document.getElementById('tasksMenuMail');
if (openMail3Pane_menuitem)
openMail3Pane_menuitem.removeAttribute("hidden");
}
@@ -529,256 +491,108 @@ function HideMenus()
var menuFileClose = document.getElementById('menu_close');
var menuFileQuit = document.getElementById('menu_FileQuitItem');
if (menuFileClose && menuFileQuit)
menuFileQuit.parentNode.replaceChild(menuFileClose, menuFileQuit);
}
function OnUnloadMessageWindow()
{
+ gFolderDisplay.close();
UnloadCommandUpdateHandlers();
// FIX ME - later we will be able to use onunload from the overlay
OnUnloadMsgHeaderPane();
gPhishingDetector.shutdown();
OnMailWindowUnload();
}
function GetSelectedMsgFolders()
{
- var folderArray = [];
- var msgFolder = GetLoadedMsgFolder();
- if (msgFolder)
- folderArray[0] = msgFolder;
-
- return folderArray;
-}
-
-function GetFirstSelectedMessage()
-{
- return GetLoadedMessage();
+ if (gFolderDisplay.displayedFolder)
+ return [gFolderDisplay.displayedFolder];
+ return [];
}
function GetNumSelectedMessages()
{
- if (gCurrentMessageUri)
- return 1;
- else
- return 0;
-}
-
-function GetSelectedMessages()
-{
- var messageArray = new Array(1);
- var message = GetLoadedMessage();
- if (message)
- messageArray[0] = message;
-
- return messageArray;
-}
-
-function GetSelectedIndices(dbView)
-{
- try {
- return dbView.getIndicesForSelection({});
- }
- catch (ex) {
- dump("ex = " + ex + "\n");
- return null;
- }
-}
-
-function GetLoadedMsgFolder()
-{
- return (gCurrentFolderUri) ? GetMsgFolderFromUri(gCurrentFolderUri) : null;
-}
-
-function GetSelectedFolderURI()
-{
- return gCurrentFolderUri;
-}
-
-function GetLoadedMessage()
-{
- return gCurrentMessageUri;
-}
-
-//Clear everything related to the current message. called after load start page.
-function ClearMessageSelection()
-{
- gCurrentMessageUri = null;
- gCurrentFolderUri = null;
- UpdateMailToolbar("clear msg, std alone window");
-}
-
-function SetNextMessageAfterDelete()
-{
- gNextMessageViewIndexAfterDelete = gDBView.msgToSelectAfterDelete;
-}
-
-function SelectFolder(folderUri)
-{
- if (folderUri == gCurrentFolderUri)
- return;
-
- var msgfolder = GetMsgFolderFromUri(folderUri)
- if (!msgfolder || msgfolder.isServer)
- return;
-
- // close old folder view
- var dbview = GetDBView();
- if (dbview)
- dbview.close();
-
- gCurrentFolderToRerootForStandAlone = folderUri;
- msgWindow.openFolder = msgfolder;
-
- if (msgfolder.manyHeadersToDownload)
- {
- gRerootOnFolderLoadForStandAlone = true;
- try
- {
- // accessing the db causes the folder loaded notification to get sent
- // for local folders.
- var db = msgfolder.msgDatabase;
- msgfolder.startFolderLoading();
- msgfolder.updateFolder(msgWindow);
- }
- catch(ex)
- {
- dump("Error loading with many headers to download: " + ex + "\n");
- }
- }
- else
- {
- RerootFolderForStandAlone(folderUri);
- gRerootOnFolderLoadForStandAlone = false;
- msgfolder.startFolderLoading();
-
- //Need to do this after rerooting folder. Otherwise possibility of receiving folder loaded
- //notification before folder has actually changed.
- msgfolder.updateFolder(msgWindow);
- }
-}
-
-function RerootFolderForStandAlone(uri)
-{
- gCurrentFolderUri = uri;
-
- // create new folder view
- CreateView(null);
-
- if (gMessageToLoad != nsMsgKey_None)
- {
- LoadMessageByMsgKey(gMessageToLoad);
- gMessageToLoad = nsMsgKey_None;
- }
- // now do the work to load the appropriate message
- else if (gNextMessageAfterLoad) {
- var type = gNextMessageAfterLoad;
- gNextMessageAfterLoad = null;
- LoadMessageByNavigationType(type);
- }
-
- SetUpToolbarButtons(gCurrentFolderUri);
-
- UpdateMailToolbar("reroot folder in stand alone window");
-
- // hook for extra toolbar items
- var observerService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
- observerService.notifyObservers(window, "mail:setupToolbarItems", uri);
-}
-
-function GetMsgHdrFromUri(messageUri)
-{
- return messenger.msgHdrFromURI(messageUri);
-}
-
-function SelectMessage(messageUri)
-{
- var msgHdr = GetMsgHdrFromUri(messageUri);
- LoadMessageByMsgKey(msgHdr.messageKey);
+ return gFolderDisplay.treeSelection.count;
}
function ReloadMessage()
{
- gDBView.reloadMessage();
+ gFolderDisplay.view.dbView.reloadMessage();
}
function MsgDeleteMessageFromMessageWindow(reallyDelete, fromToolbar)
{
// if from the toolbar, return right away if this is a news message
// only allow cancel from the menu: "Edit | Cancel / Delete Message"
- if (fromToolbar)
- {
- if (isNewsURI(gCurrentFolderUri))
- {
- // if news, don't delete
- return;
- }
- }
+ if (fromToolbar && gDisplayFolder.view.isNewsFolder)
+ return;
- // before we delete
- SetNextMessageAfterDelete();
+ gFolderDisplay.hintAboutToDeleteMessages();
if (reallyDelete)
- gDBView.doCommand(nsMsgViewCommandType.deleteNoTrash);
+ gFolderDisplay.doCommand(nsMsgViewCommandType.deleteNoTrash);
else
- gDBView.doCommand(nsMsgViewCommandType.deleteMsg);
+ gFolderDisplay.doCommand(nsMsgViewCommandType.deleteMsg);
}
// MessageWindowController object (handles commands when one of the trees does not have focus)
var MessageWindowController =
{
supportsCommand: function(command)
{
switch ( command )
{
+ // external messages cannot be deleted, mutated, or subjected to filtering
case "cmd_delete":
- case "cmd_undo":
- case "cmd_redo":
case "cmd_killThread":
case "cmd_killSubthread":
case "cmd_watchThread":
case "button_delete":
case "button_junk":
case "cmd_shiftDelete":
- case "cmd_saveAsFile":
- case "cmd_saveAsTemplate":
- case "cmd_viewPageSource":
- case "cmd_getMsgsForAuthAccounts":
case "cmd_tag":
case "button_mark":
case "cmd_markAsRead":
case "cmd_markAllRead":
case "cmd_markThreadAsRead":
case "cmd_markReadByDate":
case "cmd_markAsFlagged":
- case "button_file":
- case "cmd_file":
case "cmd_markAsJunk":
case "cmd_markAsNotJunk":
case "cmd_recalculateJunkScore":
case "cmd_applyFiltersToSelection":
case "cmd_applyFilters":
case "cmd_runJunkControls":
case "cmd_deleteJunk":
+ return !gMessageDisplay.isDummy;
+ case "cmd_undo":
+ case "cmd_redo":
+ case "cmd_saveAsFile":
+ case "cmd_saveAsTemplate":
+ case "cmd_viewPageSource":
+ case "cmd_getMsgsForAuthAccounts":
+ case "button_file":
+ case "cmd_file":
case "cmd_nextMsg":
case "button_next":
case "button_previous":
case "cmd_nextUnreadMsg":
case "cmd_nextFlaggedMsg":
case "cmd_nextUnreadThread":
case "cmd_previousMsg":
case "cmd_previousUnreadMsg":
case "cmd_previousFlaggedMsg":
case "cmd_goForward":
case "cmd_goBack":
case "button_goForward":
case "button_goBack":
- return !(gDBView.keyForFirstSelectedMessage == nsMsgKey_None);
+ return gFolderDisplay.selectedMessage != null;
case "cmd_reply":
case "button_reply":
case "cmd_replySender":
case "cmd_replyGroup":
case "cmd_replyall":
case "button_replyall":
case "cmd_replylist":
@@ -817,42 +631,45 @@ var MessageWindowController =
return MailOfflineMgr.isOnline();
default:
return false;
}
},
isCommandEnabled: function(command)
{
+ let loadedFolder;
switch ( command )
{
case "cmd_createFilterFromPopup":
case "cmd_createFilterFromMenu":
- var loadedFolder = GetLoadedMsgFolder();
+ loadedFolder = gFolderDisplay.displayedFolder;
if (!(loadedFolder && loadedFolder.server.canHaveFilters))
return false;
case "cmd_delete":
UpdateDeleteCommand();
// fall through
case "button_delete":
UpdateDeleteToolbarButton();
// fall through
case "cmd_shiftDelete":
- var loadedFolder = GetLoadedMsgFolder();
- return gCurrentMessageUri && loadedFolder && (loadedFolder.canDeleteMessages || isNewsURI(gCurrentFolderUri));
+ return gFolderDisplay.selectedMessage &&
+ gFolderDisplay.displayedFolder &&
+ (gFolderDisplay.displayedFolder.canDeleteMessages ||
+ gFolderDisplay.view.isNewsFolder);
case "button_junk":
UpdateJunkToolbarButton();
// fall through
case "cmd_markAsJunk":
case "cmd_markAsNotJunk":
case "cmd_recalculateJunkScore":
// can't do junk on news yet
- return (!isNewsURI(gCurrentFolderUri));
+ return (!gFolderDisplay.view.isNewsFolder);
case "button_archive":
- var folder = GetLoadedMsgFolder();
+ var folder = gFolderDisplay.displayedFolder;
return folder &&
!(IsSpecialFolder(folder, Components.interfaces.nsMsgFolderFlags.Archive,
true));
case "cmd_archive":
case "cmd_reply":
case "button_reply":
case "cmd_replySender":
case "cmd_replyGroup":
@@ -878,17 +695,17 @@ var MessageWindowController =
case "cmd_markAsRead":
case "cmd_markAllRead":
case "cmd_markThreadAsRead":
case "cmd_markReadByDate":
return(true);
case "cmd_markAsFlagged":
case "button_file":
case "cmd_file":
- return ( gCurrentMessageUri != null);
+ return ( gFolderDisplay.selectedMessage != null);
case "cmd_printSetup":
return true;
case "cmd_getNewMessages":
case "button_getNewMessages":
case "cmd_getMsgsForAuthAccounts":
// GetMsgs should always be enabled, see bugs 89404 and 111102.
return true;
case "cmd_getNextNMessages":
@@ -917,30 +734,29 @@ var MessageWindowController =
case "cmd_fullZoomEnlarge":
case "cmd_fullZoomReset":
case "cmd_fullZoomToggle":
return true;
case "button_goForward":
case "button_goBack":
case "cmd_goForward":
case "cmd_goBack":
- return gDBView &&
- gDBView.navigateStatus((command == "cmd_goBack" ||
- command == "button_goBack")
- ? nsMsgNavigationType.back : nsMsgNavigationType.forward);
+ return gFolderDisplay.navigateStatus(
+ (command == "cmd_goBack" || command == "button_goBack") ?
+ nsMsgNavigationType.back : nsMsgNavigationType.forward);
case "cmd_search":
- var loadedFolder = GetLoadedMsgFolder();