diff options
author | Matt A. Tobin <email@mattatobin.com> | 2019-11-03 00:17:46 -0400 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2019-11-03 00:17:46 -0400 |
commit | 302bf1b523012e11b60425d6eee1221ebc2724eb (patch) | |
tree | b191a895f8716efcbe42f454f37597a545a6f421 /mailnews/base/search/content | |
parent | 21b3f6247403c06f85e1f45d219f87549862198f (diff) | |
download | UXP-302bf1b523012e11b60425d6eee1221ebc2724eb.tar UXP-302bf1b523012e11b60425d6eee1221ebc2724eb.tar.gz UXP-302bf1b523012e11b60425d6eee1221ebc2724eb.tar.lz UXP-302bf1b523012e11b60425d6eee1221ebc2724eb.tar.xz UXP-302bf1b523012e11b60425d6eee1221ebc2724eb.zip |
Issue #1258 - Part 1: Import mailnews, ldap, and mork from comm-esr52.9.1
Diffstat (limited to 'mailnews/base/search/content')
-rw-r--r-- | mailnews/base/search/content/CustomHeaders.js | 203 | ||||
-rw-r--r-- | mailnews/base/search/content/CustomHeaders.xul | 57 | ||||
-rw-r--r-- | mailnews/base/search/content/FilterEditor.js | 854 | ||||
-rw-r--r-- | mailnews/base/search/content/FilterEditor.xul | 125 | ||||
-rw-r--r-- | mailnews/base/search/content/searchTermOverlay.js | 536 | ||||
-rw-r--r-- | mailnews/base/search/content/searchTermOverlay.xul | 66 | ||||
-rw-r--r-- | mailnews/base/search/content/searchWidgets.xml | 738 | ||||
-rw-r--r-- | mailnews/base/search/content/viewLog.js | 36 | ||||
-rw-r--r-- | mailnews/base/search/content/viewLog.xul | 49 |
9 files changed, 2664 insertions, 0 deletions
diff --git a/mailnews/base/search/content/CustomHeaders.js b/mailnews/base/search/content/CustomHeaders.js new file mode 100644 index 000000000..174ba100e --- /dev/null +++ b/mailnews/base/search/content/CustomHeaders.js @@ -0,0 +1,203 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource://gre/modules/Services.jsm"); + +var gAddButton; +var gRemoveButton; +var gHeaderInputElement; +var gArrayHdrs; +var gHdrsList; +var gContainer; +var gFilterBundle=null; +var gCustomBundle=null; + +function onLoad() +{ + let hdrs = Services.prefs.getCharPref("mailnews.customHeaders"); + gHeaderInputElement = document.getElementById("headerInput"); + gHeaderInputElement.focus(); + + gHdrsList = document.getElementById("headerList"); + gArrayHdrs = new Array(); + gAddButton = document.getElementById("addButton"); + gRemoveButton = document.getElementById("removeButton"); + + initializeDialog(hdrs); + updateAddButton(true); + updateRemoveButton(); +} + +function initializeDialog(hdrs) +{ + if (hdrs) + { + hdrs = hdrs.replace(/\s+/g,''); //remove white spaces before splitting + gArrayHdrs = hdrs.split(":"); + for (var i = 0; i < gArrayHdrs.length; i++) + if (!gArrayHdrs[i]) + gArrayHdrs.splice(i,1); //remove any null elements + initializeRows(); + } +} + +function initializeRows() +{ + for (var i = 0; i < gArrayHdrs.length; i++) + addRow(TrimString(gArrayHdrs[i])); +} + +function onTextInput() +{ + // enable the add button if the user has started to type text + updateAddButton( (gHeaderInputElement.value == "") ); +} + +function onOk() +{ + if (gArrayHdrs.length) + { + var hdrs; + if (gArrayHdrs.length == 1) + hdrs = gArrayHdrs; + else + hdrs = gArrayHdrs.join(": "); + Services.prefs.setCharPref("mailnews.customHeaders", hdrs); + // flush prefs to disk, in case we crash, to avoid dataloss and problems with filters that use the custom headers + Services.prefs.savePrefFile(null); + } + else + { + Services.prefs.clearUserPref("mailnews.customHeaders"); //clear the pref, no custom headers + } + + window.arguments[0].selectedVal = gHdrsList.selectedItem ? gHdrsList.selectedItem.label : null; + return true; +} + +function customHeaderOverflow() +{ + var nsMsgSearchAttrib = Components.interfaces.nsMsgSearchAttrib; + if (gArrayHdrs.length >= (nsMsgSearchAttrib.kNumMsgSearchAttributes - nsMsgSearchAttrib.OtherHeader - 1)) + { + if (!gFilterBundle) + gFilterBundle = document.getElementById("bundle_filter"); + + var alertText = gFilterBundle.getString("customHeaderOverflow"); + Services.prompt.alert(window, null, alertText); + return true; + } + return false; +} + +function onAddHeader() +{ + var newHdr = TrimString(gHeaderInputElement.value); + + if (!isRFC2822Header(newHdr)) // if user entered an invalid rfc822 header field name, bail out. + { + if (!gCustomBundle) + gCustomBundle = document.getElementById("bundle_custom"); + + var alertText = gCustomBundle.getString("colonInHeaderName"); + Services.prompt.alert(window, null, alertText); + return; + } + + gHeaderInputElement.value = ""; + if (!newHdr || customHeaderOverflow()) + return; + if (!duplicateHdrExists(newHdr)) + { + gArrayHdrs[gArrayHdrs.length] = newHdr; + var newItem = addRow(newHdr); + gHdrsList.selectItem (newItem); // make sure the new entry is selected in the tree + // now disable the add button + updateAddButton(true); + gHeaderInputElement.focus(); // refocus the input field for the next custom header + } +} + +function isRFC2822Header(hdr) +{ + var charCode; + for (var i = 0; i < hdr.length; i++) + { + charCode = hdr.charCodeAt(i); + //58 is for colon and 33 and 126 are us-ascii bounds that should be used for header field name, as per rfc2822 + + if (charCode < 33 || charCode == 58 || charCode > 126) + return false; + } + return true; +} + +function duplicateHdrExists(hdr) +{ + for (var i = 0;i < gArrayHdrs.length; i++) + { + if (gArrayHdrs[i] == hdr) + return true; + } + return false; +} + +function onRemoveHeader() +{ + var listitem = gHdrsList.selectedItems[0] + if (!listitem) return; + listitem.remove(); + var selectedHdr = GetListItemAttributeStr(listitem); + var j=0; + for (var i = 0; i < gArrayHdrs.length; i++) + { + if (gArrayHdrs[i] == selectedHdr) + { + gArrayHdrs.splice(i,1); + break; + } + } +} + +function GetListItemAttributeStr(listitem) +{ + if (listitem) + return TrimString(listitem.getAttribute("label")); + + return ""; +} + +function addRow(newHdr) +{ + var listitem = document.createElement("listitem"); + listitem.setAttribute("label", newHdr); + gHdrsList.appendChild(listitem); + return listitem; +} + +function updateAddButton(aDisable) +{ + // only update the button if the disabled state changed + if (aDisable == gAddButton.disabled) + return; + + gAddButton.disabled = aDisable; + document.documentElement.defaultButton = aDisable ? "accept" : "extra1"; +} + +function updateRemoveButton() +{ + var headerSelected = (gHdrsList.selectedItems.length > 0); + gRemoveButton.disabled = !headerSelected; + if (gRemoveButton.disabled) + gHeaderInputElement.focus(); +} + +//Remove whitespace from both ends of a string +function TrimString(string) +{ + if (!string) return ""; + return string.trim(); +} diff --git a/mailnews/base/search/content/CustomHeaders.xul b/mailnews/base/search/content/CustomHeaders.xul new file mode 100644 index 000000000..c13a8dba8 --- /dev/null +++ b/mailnews/base/search/content/CustomHeaders.xul @@ -0,0 +1,57 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?> + +<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/CustomHeaders.dtd"> +<dialog id="customHeadersDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="onLoad();" + ondialogaccept="return onOk();" + ondialogextra1="onAddHeader();" + ondialogextra2="onRemoveHeader();" + style="width: 30em; height: 25em;" + persist="width height screenX screenY" + title="&window.title;" + buttons="accept,cancel,extra1,extra2"> + + <stringbundleset id="stringbundleset"> + <stringbundle id="bundle_filter" src="chrome://messenger/locale/filter.properties"/> + <stringbundle id="bundle_custom" src="chrome://messenger/locale/custom.properties"/> + </stringbundleset> + + <script type="application/javascript" src="chrome://messenger/content/CustomHeaders.js"/> + + <grid flex="1"> + <columns> + <column flex="1"/> + <column/> + </columns> + <rows> + <row> + <label accesskey="&newMsgHeader.accesskey;" control="headerInput" value="&newMsgHeader.label;"/> + </row> + <row> + <textbox id="headerInput" onfocus="this.select();" oninput="onTextInput();"/> + </row> + + <row flex="1"> + <vbox> + <listbox id="headerList" flex="1" onselect="updateRemoveButton();" /> + </vbox> + + <vbox> + <button id="addButton" + label="&addButton.label;" + accesskey="&addButton.accesskey;" + dlgtype="extra1"/> + <button id="removeButton" + label="&removeButton.label;" + accesskey="&removeButton.accesskey;" + dlgtype="extra2"/> + </vbox> + </row> + </rows> + </grid> +</dialog> diff --git a/mailnews/base/search/content/FilterEditor.js b/mailnews/base/search/content/FilterEditor.js new file mode 100644 index 000000000..986185d34 --- /dev/null +++ b/mailnews/base/search/content/FilterEditor.js @@ -0,0 +1,854 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource:///modules/mailServices.js"); +Components.utils.import("resource:///modules/MailUtils.js"); + +// The actual filter that we're editing if it is a _saved_ filter or prefill; +// void otherwise. +var gFilter; +// cache the key elements we need +var gFilterList; +// The filter name as it appears in the "Filter Name" field of dialog. +var gFilterNameElement; +var gFilterTypeSelector; +var gFilterBundle; +var gPreFillName; +var gSessionFolderListenerAdded = false; +var gFilterActionList; +var gCustomActions = null; +var gFilterType; +var gFilterPosition = 0; + +var gFilterActionStrings = ["none", "movemessage", "setpriorityto", "deletemessage", + "markasread", "ignorethread", "watchthread", "markasflagged", + "label", "replytomessage", "forwardmessage", "stopexecution", + "deletefrompopserver", "leaveonpopserver", "setjunkscore", + "fetchfrompopserver", "copymessage", "addtagtomessage", + "ignoresubthread", "markasunread"]; + +// A temporary filter with the current state of actions in the UI. +var gTempFilter = null; +// A nsIArray of the currently defined actions in the order they will be run. +var gActionListOrdered = null; + +var gFilterEditorMsgWindow = null; + +var nsMsgFilterAction = Components.interfaces.nsMsgFilterAction; +var nsMsgFilterType = Components.interfaces.nsMsgFilterType; +var nsIMsgRuleAction = Components.interfaces.nsIMsgRuleAction; +var nsMsgSearchScope = Components.interfaces.nsMsgSearchScope; + +function filterEditorOnLoad() +{ + getCustomActions(); + initializeSearchWidgets(); + initializeFilterWidgets(); + + gFilterBundle = document.getElementById("bundle_filter"); + + if ("arguments" in window && window.arguments[0]) + { + var args = window.arguments[0]; + + if ("filterList" in args) + { + gFilterList = args.filterList; + // the postPlugin filters cannot be applied to servers that are + // deferred, (you must define them on the deferredTo server instead). + let server = gFilterList.folder.server; + if (server.rootFolder != server.rootMsgFolder) + gFilterTypeSelector.disableDeferredAccount(); + } + + if ("filterPosition" in args) + { + gFilterPosition = args.filterPosition; + } + + if ("filter" in args) + { + // editing a filter + gFilter = window.arguments[0].filter; + initializeDialog(gFilter); + } + else + { + if (gFilterList) + setSearchScope(getScopeFromFilterList(gFilterList)); + // if doing prefill filter create a new filter and populate it. + if ("filterName" in args) + { + gPreFillName = args.filterName; + + // Passing null as the parameter to createFilter to keep the name empty + // until later where we assign the name. + gFilter = gFilterList.createFilter(null); + + var term = gFilter.createTerm(); + + term.attrib = Components.interfaces.nsMsgSearchAttrib.Default; + if (("fieldName" in args) && args.fieldName) { + // fieldName should contain the name of the field in which to search, + // from nsMsgSearchTerm.cpp::SearchAttribEntryTable, e.g. "to" or "cc" + try { + term.attrib = term.getAttributeFromString(args.fieldName); + } catch (e) { /* Invalid string is fine, just ignore it. */ } + } + if (term.attrib == Components.interfaces.nsMsgSearchAttrib.Default) + term.attrib = Components.interfaces.nsMsgSearchAttrib.Sender; + + term.op = Components.interfaces.nsMsgSearchOp.Is; + term.booleanAnd = gSearchBooleanRadiogroup.value == "and"; + + var termValue = term.value; + termValue.attrib = term.attrib; + termValue.str = gPreFillName; + + term.value = termValue; + + gFilter.appendTerm(term); + + // the default action for news filters is Delete + // for everything else, it's MoveToFolder + var filterAction = gFilter.createAction(); + filterAction.type = (getScopeFromFilterList(gFilterList) == + nsMsgSearchScope.newsFilter) ? + nsMsgFilterAction.Delete : nsMsgFilterAction.MoveToFolder; + gFilter.appendAction(filterAction); + initializeDialog(gFilter); + } + else if ("copiedFilter" in args) + { + // we are copying a filter + var copiedFilter = args.copiedFilter; + var copiedName = gFilterBundle.getFormattedString("copyToNewFilterName", + [copiedFilter.filterName]); + let newFilter = gFilterList.createFilter(copiedName); + + // copy the actions + for (let i = 0; i < copiedFilter.actionCount; i++) + { + let filterAction = copiedFilter.getActionAt(i); + newFilter.appendAction(filterAction); + } + + // copy the search terms + for (let i = 0; i < copiedFilter.searchTerms.Count(); i++) + { + var searchTerm = copiedFilter.searchTerms.QueryElementAt(i, + Components.interfaces.nsIMsgSearchTerm); + + var newTerm = newFilter.createTerm(); + newTerm.attrib = searchTerm.attrib; + newTerm.op = searchTerm.op; + newTerm.booleanAnd = searchTerm.booleanAnd; + newTerm.value = searchTerm.value; + newFilter.appendTerm(newTerm); + }; + + gPreFillName = copiedName; + gFilter = newFilter; + + initializeDialog(gFilter); + + // We reset the filter name, because otherwise the saveFilter() + // function thinks we are editing a filter, and will thus skip the name + // uniqueness check. + gFilter.filterName = ""; + } + else + { + // fake the first more button press + onMore(null); + } + } + } + + if (!gFilter) + { + // This is a new filter. Set to both Incoming and Manual contexts. + gFilterTypeSelector.setType(nsMsgFilterType.Incoming | nsMsgFilterType.Manual); + } + + // in the case of a new filter, we may not have an action row yet. + ensureActionRow(); + gFilterType = gFilterTypeSelector.getType(); + + gFilterNameElement.select(); + // This call is required on mac and linux. It has no effect under win32. See bug 94800. + gFilterNameElement.focus(); +} + +function filterEditorOnUnload() +{ + if (gSessionFolderListenerAdded) + MailServices.mailSession.RemoveFolderListener(gFolderListener); +} + +function onEnterInSearchTerm(event) +{ + if (event.ctrlKey || (Services.appinfo.OS == "Darwin" && event.metaKey)) { + // If accel key (Ctrl on Win/Linux, Cmd on Mac) was held too, accept the dialog. + document.getElementById("FilterEditor").acceptDialog(); + } else { + // If only plain Enter was pressed, add a new rule line. + onMore(event); + } +} + +function onAccept() +{ + try { + if (!saveFilter()) + return false; + } catch(e) {Components.utils.reportError(e); return false;} + + // parent should refresh filter list.. + // this should REALLY only happen when some criteria changes that + // are displayed in the filter dialog, like the filter name + window.arguments[0].refresh = true; + window.arguments[0].newFilter = gFilter; + return true; +} + +// the folderListener object +var gFolderListener = { + OnItemAdded: function(parentItem, item) {}, + + OnItemRemoved: function(parentItem, item){}, + + 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) {}, + + OnItemEvent: function(folder, event) + { + var eventType = event.toString(); + + if (eventType == "FolderCreateCompleted") + { + gActionTargetElement.selectFolder(folder); + SetBusyCursor(window, false); + } + else if (eventType == "FolderCreateFailed") + SetBusyCursor(window, false); + } +} + +function duplicateFilterNameExists(filterName) +{ + if (gFilterList) + for (var i = 0; i < gFilterList.filterCount; i++) + if (filterName == gFilterList.getFilterAt(i).filterName) + return true; + return false; +} + +function getScopeFromFilterList(filterList) +{ + if (!filterList) + { + dump("yikes, null filterList\n"); + return nsMsgSearchScope.offlineMail; + } + return filterList.folder.server.filterScope; +} + +function getScope(filter) +{ + return getScopeFromFilterList(filter.filterList); +} + +function initializeFilterWidgets() +{ + gFilterNameElement = document.getElementById("filterName"); + gFilterActionList = document.getElementById("filterActionList"); + initializeFilterTypeSelector(); +} + +function initializeFilterTypeSelector() +{ + /** + * This object controls code interaction with the widget allowing specifying + * the filter type (event when the filter is run). + */ + gFilterTypeSelector = { + checkBoxManual: document.getElementById("runManual"), + checkBoxIncoming : document.getElementById("runIncoming"), + + menulistIncoming: document.getElementById("pluginsRunOrder"), + + menuitemBeforePlugins: document.getElementById("runBeforePlugins"), + menuitemAfterPlugins: document.getElementById("runAfterPlugins"), + + checkBoxArchive: document.getElementById("runArchive"), + checkBoxOutgoing: document.getElementById("runOutgoing"), + + /** + * Returns the currently set filter type (checkboxes) in terms + * of a Components.interfaces.nsMsgFilterType value. + */ + getType: function() + { + let type = nsMsgFilterType.None; + + if (this.checkBoxManual.checked) + type |= nsMsgFilterType.Manual; + + if (this.checkBoxIncoming.checked) { + if (this.menulistIncoming.selectedItem == this.menuitemAfterPlugins) { + type |= nsMsgFilterType.PostPlugin; + } else { + // this.menuitemBeforePlugins selected + if (getScopeFromFilterList(gFilterList) == + nsMsgSearchScope.newsFilter) + type |= nsMsgFilterType.NewsRule; + else + type |= nsMsgFilterType.InboxRule; + } + } + + if (this.checkBoxArchive.checked) + type |= nsMsgFilterType.Archive; + + if (this.checkBoxOutgoing.checked) + type |= nsMsgFilterType.PostOutgoing; + + return type; + }, + + /** + * Sets the checkboxes to represent the filter type passed in. + * + * @param aType the filter type to set in terms + * of Components.interfaces.nsMsgFilterType values. + */ + setType: function(aType) + { + // If there is no type (event) requested, force "when manually run" + if (aType == nsMsgFilterType.None) + aType = nsMsgFilterType.Manual; + + this.checkBoxManual.checked = aType & nsMsgFilterType.Manual; + + this.checkBoxIncoming.checked = aType & (nsMsgFilterType.PostPlugin | + nsMsgFilterType.Incoming); + + this.menulistIncoming.selectedItem = aType & nsMsgFilterType.PostPlugin ? + this.menuitemAfterPlugins : this.menuitemBeforePlugins; + + this.checkBoxArchive.checked = aType & nsMsgFilterType.Archive; + + this.checkBoxOutgoing.checked = aType & nsMsgFilterType.PostOutgoing; + + this.updateClassificationMenu(); + }, + + /** + * Enable the "before/after classification" menulist depending on + * whether "run when incoming mail" is selected. + */ + updateClassificationMenu: function() + { + this.menulistIncoming.disabled = !this.checkBoxIncoming.checked; + updateFilterType(); + }, + + /** + * Disable the options unsuitable for deferred accounts. + */ + disableDeferredAccount: function() + { + this.menuitemAfterPlugins.disabled = true; + this.checkBoxOutgoing.disabled = true; + } + }; +} + +function initializeDialog(filter) +{ + gFilterNameElement.value = filter.filterName; + let filterType = filter.filterType; + gFilterTypeSelector.setType(filter.filterType); + + let numActions = filter.actionCount; + for (let actionIndex = 0; actionIndex < numActions; actionIndex++) + { + let filterAction = filter.getActionAt(actionIndex); + + var newActionRow = document.createElement('listitem'); + newActionRow.setAttribute('initialActionIndex', actionIndex); + newActionRow.className = 'ruleaction'; + gFilterActionList.appendChild(newActionRow); + newActionRow.setAttribute('value', + filterAction.type == nsMsgFilterAction.Custom ? + filterAction.customId : gFilterActionStrings[filterAction.type]); + newActionRow.setAttribute('onfocus', 'this.storeFocus();'); + } + + var gSearchScope = getFilterScope(getScope(filter), filter.filterType, filter.filterList); + initializeSearchRows(gSearchScope, filter.searchTerms); + setFilterScope(filter.filterType, filter.filterList); +} + +function ensureActionRow() +{ + // make sure we have at least one action row visible to the user + if (!gFilterActionList.getRowCount()) + { + var newActionRow = document.createElement('listitem'); + newActionRow.className = 'ruleaction'; + gFilterActionList.appendChild(newActionRow); + newActionRow.mRemoveButton.disabled = true; + } +} + +// move to overlay +function saveFilter() +{ + // See if at least one filter type (activation event) is selected. + if (gFilterType == nsMsgFilterType.None) { + Services.prompt.alert(window, + gFilterBundle.getString("mustHaveFilterTypeTitle"), + gFilterBundle.getString("mustHaveFilterTypeMessage")); + return false; + } + + let filterName = gFilterNameElement.value; + // If we think have a duplicate, then we need to check that if we + // have an original filter name (i.e. we are editing a filter), then + // we must check that the original is not the current as that is what + // the duplicateFilterNameExists function will have picked up. + if ((!gFilter || gFilter.filterName != filterName) && duplicateFilterNameExists(filterName)) + { + Services.prompt.alert(window, + gFilterBundle.getString("cannotHaveDuplicateFilterTitle"), + gFilterBundle.getString("cannotHaveDuplicateFilterMessage")); + return false; + } + + // Check that all of the search attributes and operators are valid. + function rule_desc(index, obj) { + return (index + 1) + " (" + obj.searchattribute.label + ", " + obj.searchoperator.label + ")"; + } + + let invalidRule = false; + for (let index = 0; index < gSearchTerms.length; index++) + { + let obj = gSearchTerms[index].obj; + // We don't need to check validity of matchAll terms + if (obj.matchAll) + continue; + + // the term might be an offscreen one that we haven't initialized yet + let searchTerm = obj.searchTerm; + if (!searchTerm && !gSearchTerms[index].initialized) + continue; + + if (isNaN(obj.searchattribute.value)) // is this a custom term? + { + let customTerm = MailServices.filters.getCustomTerm(obj.searchattribute.value); + if (!customTerm) + { + invalidRule = true; + Components.utils.reportError("Filter not saved because custom search term '" + + obj.searchattribute.value + "' in rule " + rule_desc(index, obj) + " not found"); + } + else + { + if (!customTerm.getAvailable(obj.searchScope, obj.searchattribute.value)) + { + invalidRule = true; + Components.utils.reportError("Filter not saved because custom search term '" + + customTerm.name + "' in rule " + rule_desc(index, obj) + " not available"); + } + } + } + else + { + let otherHeader = Components.interfaces.nsMsgSearchAttrib.OtherHeader; + let attribValue = (obj.searchattribute.value > otherHeader) ? + otherHeader : obj.searchattribute.value; + if (!obj.searchattribute + .validityTable + .getAvailable(attribValue, obj.searchoperator.value)) + { + invalidRule = true; + Components.utils.reportError("Filter not saved because standard search term '" + + attribValue + "' in rule " + rule_desc(index, obj) + " not available in this context"); + } + } + + if (invalidRule) { + Services.prompt.alert(window, + gFilterBundle.getString("searchTermsInvalidTitle"), + gFilterBundle.getFormattedString("searchTermsInvalidRule", + [obj.searchattribute.label, + obj.searchoperator.label])); + return false; + } + + } + + // before we go any further, validate each specified filter action, abort the save + // if any of the actions is invalid... + for (let index = 0; index < gFilterActionList.itemCount; index++) + { + var listItem = gFilterActionList.getItemAtIndex(index); + if (!listItem.validateAction()) + return false; + } + + // if we made it here, all of the actions are valid, so go ahead and save the filter + let isNewFilter; + if (!gFilter) + { + // This is a new filter + gFilter = gFilterList.createFilter(filterName); + isNewFilter = true; + gFilter.enabled = true; + } + else + { + // We are working with an existing filter object, + // either editing or using prefill + gFilter.filterName = filterName; + //Prefilter is treated as a new filter. + if (gPreFillName) + { + isNewFilter = true; + gFilter.enabled = true; + } + else + isNewFilter = false; + + gFilter.clearActionList(); + } + + // add each filteraction to the filter + for (let index = 0; index < gFilterActionList.itemCount; index++) + gFilterActionList.getItemAtIndex(index).saveToFilter(gFilter); + + // If we do not have a filter name at this point, generate one. + if (!gFilter.filterName) + AssignMeaningfulName(); + + gFilter.filterType = gFilterType; + saveSearchTerms(gFilter.searchTerms, gFilter); + + if (isNewFilter) + { + // new filter - insert into gFilterList + gFilterList.insertFilterAt(gFilterPosition, gFilter); + } + + // success! + return true; +} + +/** + * Check if the list of actions the user created will be executed in a different order. + * Exposes a note to the user if that is the case. + */ +function checkActionsReorder() +{ + setTimeout(_checkActionsReorder, 0); +} + +/** + * This should be called from setTimeout otherwise some of the elements calling + * may not be fully initialized yet (e.g. we get ".saveToFilter is not a function"). + * It is OK to schedule multiple timeouts with this function. + */ +function _checkActionsReorder() { + // Create a temporary disposable filter and add current actions to it. + if (!gTempFilter) + gTempFilter = gFilterList.createFilter(""); + else + gTempFilter.clearActionList(); + + for (let index = 0; index < gFilterActionList.itemCount; index++) + gFilterActionList.getItemAtIndex(index).saveToFilter(gTempFilter); + + // Now get the actions out of the filter in the order they will be executed in. + gActionListOrdered = gTempFilter.sortedActionList; + + // Compare the two lists. + let statusBar = document.getElementById("statusbar"); + for (let index = 0; index < gActionListOrdered.length; index++) { + if (index != gTempFilter.getActionIndex( + gActionListOrdered.queryElementAt(index, nsIMsgRuleAction))) + { + // If the lists are not the same unhide the status bar and show warning. + statusBar.style.visibility = "visible"; + return; + } + } + + statusBar.style.visibility = "hidden"; +} + +/** + * Show a dialog with the ordered list of actions. + * The fetching of action label and argument is separated from checkActionsReorder + * function to make that one more lightweight. The list is built only upon + * user request. + */ +function showActionsOrder() +{ + // Fetch the actions and arguments as a string. + let actionStrings = []; + for (let index = 0; index < gFilterActionList.itemCount; index++) + gFilterActionList.getItemAtIndex(index).getActionStrings(actionStrings); + + // Present a nicely formatted list of action names and arguments. + let actionList = gFilterBundle.getString("filterActionOrderExplanation"); + for (let i = 0; i < gActionListOrdered.length; i++) { + let actionIndex = gTempFilter.getActionIndex( + gActionListOrdered.queryElementAt(i, nsIMsgRuleAction)); + let action = actionStrings[actionIndex]; + actionList += gFilterBundle.getFormattedString("filterActionItem", + [(i + 1), action.label, action.argument]); + } + + Services.prompt.confirmEx(window, + gFilterBundle.getString("filterActionOrderTitle"), + actionList, Services.prompt.BUTTON_TITLE_OK, + null, null, null, null, {value:false}); +} + +function AssignMeaningfulName() +{ + // termRoot points to the first search object, which is the one we care about. + let termRoot = gSearchTerms[0].obj; + // stub is used as the base name for a filter. + let stub; + + // If this is a Match All Messages Filter, we already know the name to assign. + if (termRoot.matchAll) + stub = gFilterBundle.getString( "matchAllFilterName" ); + else + { + // Assign a name based on the first search term. + let searchValue = termRoot.searchvalue; + let selIndex = searchValue.getAttribute( "selectedIndex" ); + let children = document.getAnonymousNodes(searchValue); + let activeItem = children[selIndex]; + let attribs = Components.interfaces.nsMsgSearchAttrib; + + // Term, Operator and Value are the three parts of a filter match + // Term and Operator are easy to retrieve + let term = termRoot.searchattribute.label; + let operator = termRoot.searchoperator.label; + + // Values are either popup menu items or edit fields. + // For popup menus use activeItem.label; for + // edit fields, activeItem.value + let value; + switch (Number(termRoot.searchattribute.value)) + { + case attribs.Priority: + case attribs.MsgStatus: + case attribs.Keywords: + case attribs.HasAttachmentStatus: + case attribs.JunkStatus: + case attribs.JunkScoreOrigin: + if (activeItem) + value = activeItem.label; + else + value = ""; + break; + + default: + try + { + value = activeItem.value; + } + catch (ex) + { + // We should never get here, but for safety's sake, + // let's name the filter "Untitled Filter". + stub = gFilterBundle.getString( "untitledFilterName" ); + // Do not 'Return'. Instead fall through and deal with the untitled filter below. + } + break; + } + // We are now ready to name the filter. + // If at this point stub is empty, we know that this is not a Match All Filter + // and is not an "untitledFilterName" Filter, so assign it a name using + // a string format from the Filter Bundle. + if (!stub) + stub = gFilterBundle.getFormattedString("filterAutoNameStr", [term, operator, value]); + } + + // Whatever name we have used, 'uniquify' it. + let tempName = stub; + let count = 1; + while (duplicateFilterNameExists(tempName)) + { + count++; + tempName = stub + " " + count; + } + gFilter.filterName = tempName; +} + + +function GetFirstSelectedMsgFolder() +{ + var selectedFolder = gActionTargetElement.getAttribute("uri"); + if (!selectedFolder) + return null; + + var msgFolder = MailUtils.getFolderForURI(selectedFolder, true); + return msgFolder; +} + +function SearchNewFolderOkCallback(name, uri) +{ + var msgFolder = MailUtils.getFolderForURI(uri, true); + var imapFolder = null; + try + { + imapFolder = msgFolder.QueryInterface(Components.interfaces.nsIMsgImapMailFolder); + } + catch(ex) {} + if (imapFolder) //imapFolder creation is asynchronous. + { + if (!gSessionFolderListenerAdded) { + try + { + let notifyFlags = Components.interfaces.nsIFolderListener.event; + MailServices.mailSession.AddFolderListener(gFolderListener, notifyFlags); + gSessionFolderListenerAdded = true; + } + catch (ex) + { + Components.utils.reportError("Error adding to session: " + ex + "\n"); + } + } + } + + var msgWindow = GetFilterEditorMsgWindow(); + + if (imapFolder) + SetBusyCursor(window, true); + + msgFolder.createSubfolder(name, msgWindow); + + if (!imapFolder) + { + var curFolder = uri+"/"+encodeURIComponent(name); + let folder = MailUtils.getFolderForURI(curFolder); + gActionTargetElement.selectFolder(folder); + } +} + +function UpdateAfterCustomHeaderChange() +{ + updateSearchAttributes(); +} + +//if you use msgWindow, please make sure that destructor gets called when you close the "window" +function GetFilterEditorMsgWindow() +{ + if (!gFilterEditorMsgWindow) + { + var msgWindowContractID = "@mozilla.org/messenger/msgwindow;1"; + var nsIMsgWindow = Components.interfaces.nsIMsgWindow; + gFilterEditorMsgWindow = Components.classes[msgWindowContractID].createInstance(nsIMsgWindow); + gFilterEditorMsgWindow.domWindow = window; + gFilterEditorMsgWindow.rootDocShell.appType = Components.interfaces.nsIDocShell.APP_TYPE_MAIL; + } + return gFilterEditorMsgWindow; +} + +function SetBusyCursor(window, enable) +{ + // setCursor() is only available for chrome windows. + // However one of our frames is the start page which + // is a non-chrome window, so check if this window has a + // setCursor method + if ("setCursor" in window) + { + if (enable) + window.setCursor("wait"); + else + window.setCursor("auto"); + } +} + +function doHelpButton() +{ + openHelp("mail-filters"); +} + +function getCustomActions() +{ + if (!gCustomActions) + { + gCustomActions = []; + let customActionsEnum = MailServices.filters.getCustomActions(); + while (customActionsEnum.hasMoreElements()) + gCustomActions.push(customActionsEnum.getNext().QueryInterface( + Components.interfaces.nsIMsgFilterCustomAction)); + } +} + +function updateFilterType() +{ + gFilterType = gFilterTypeSelector.getType(); + setFilterScope(gFilterType, gFilterList); + + // set valid actions + var ruleActions = gFilterActionList.getElementsByAttribute('class', 'ruleaction'); + for (var i = 0; i < ruleActions.length; i++) + ruleActions[i].mRuleActionType.hideInvalidActions(); +} + +// Given a filter type, set the global search scope to the filter scope +function setFilterScope(aFilterType, aFilterList) +{ + let filterScope = getFilterScope(getScopeFromFilterList(aFilterList), + aFilterType, aFilterList); + setSearchScope(filterScope); +} + +// +// Given the base filter scope for a server, and the filter +// type, return the scope used for filter. This assumes a +// hierarchy of contexts, with incoming the most restrictive, +// followed by manual and post-plugin. +function getFilterScope(aServerFilterScope, aFilterType, aFilterList) +{ + if (aFilterType & nsMsgFilterType.Incoming) + return aServerFilterScope; + + // Manual or PostPlugin + // local mail allows body and junk types + if (aServerFilterScope == nsMsgSearchScope.offlineMailFilter) + return nsMsgSearchScope.offlineMail; + // IMAP and NEWS online don't allow body + return nsMsgSearchScope.onlineManual; +} + +/** + * Re-focus the action that was focused before focus was lost. + */ +function setLastActionFocus() { + let lastAction = gFilterActionList.getAttribute("focusedAction"); + if (!lastAction || lastAction < 0) + lastAction = 0; + if (lastAction >= gFilterActionList.itemCount) + lastAction = gFilterActionList.itemCount - 1; + + gFilterActionList.getItemAtIndex(lastAction).mRuleActionType.menulist.focus(); +} diff --git a/mailnews/base/search/content/FilterEditor.xul b/mailnews/base/search/content/FilterEditor.xul new file mode 100644 index 000000000..0e322264d --- /dev/null +++ b/mailnews/base/search/content/FilterEditor.xul @@ -0,0 +1,125 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/filterDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/folderPane.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?> + +<?xul-overlay href="chrome://messenger/content/searchTermOverlay.xul"?> + +<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/FilterEditor.dtd"> + +<dialog id="FilterEditor" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&window.title;" + style="&filterEditorDialog.dimensions;" + windowtype="mailnews:filtereditor" + persist="width height screenX screenY" + buttons="accept,cancel" + onload="filterEditorOnLoad();" + onunload="filterEditorOnUnload();" + ondialogaccept="return onAccept();"> + + <dummy class="usesMailWidgets"/> + <stringbundleset id="stringbundleset"> + <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/> + <stringbundle id="bundle_filter" src="chrome://messenger/locale/filter.properties"/> + <stringbundle id="bundle_search" src="chrome://messenger/locale/search.properties"/> + </stringbundleset> + + <script type="application/javascript" src="chrome://messenger/content/mailWindowOverlay.js"/> + <script type="application/javascript" src="chrome://messenger/content/mailCommands.js"/> + <script type="application/javascript" src="chrome://messenger/content/FilterEditor.js"/> + + <commandset> + <command id="cmd_updateFilterType" oncommand="updateFilterType();"/> + <command id="cmd_updateClassificationMenu" oncommand="gFilterTypeSelector.updateClassificationMenu();"/> + </commandset> + + <vbox> + <hbox align="center"> + <label value="&filterName.label;" accesskey="&filterName.accesskey;" control="filterName"/> + <textbox flex="1" id="filterName"/> + <spacer flex="1"/> + </hbox> + </vbox> + + <separator class="thin"/> + + <vbox flex="1"> + <groupbox> + <caption label="&contextDesc.label;"/> + <grid> + <columns> + <column/> + <column/> + </columns> + <rows> + <row> + <checkbox id="runManual" + label="&contextManual.label;" + accesskey="&contextManual.accesskey;" + command="cmd_updateFilterType"/> + </row> + <row> + <checkbox id="runIncoming" + label="&contextIncomingMail.label;" + accesskey="&contextIncomingMail.accesskey;" + command="cmd_updateClassificationMenu"/> + <menulist id="pluginsRunOrder" + command="cmd_updateFilterType"> + <menupopup> + <menuitem id="runBeforePlugins" + label="&contextBeforeCls.label;"/> + <menuitem id="runAfterPlugins" + label="&contextAfterCls.label;"/> + </menupopup> + </menulist> + </row> + <row> + <checkbox id="runArchive" + label="&contextArchive.label;" + accesskey="&contextArchive.accesskey;" + command="cmd_updateFilterType"/> + </row> + <row> + <checkbox id="runOutgoing" + label="&contextOutgoing.label;" + accesskey="&contextOutgoing.accesskey;" + command="cmd_updateFilterType"/> + </row> + </rows> + </grid> + </groupbox> + + <vbox id="searchTermListBox" flex="1"/> + </vbox> + + <splitter id="gray_horizontal_splitter" persist="state"/> + + <vbox flex="1"> + <label value="&filterActionDesc.label;" + accesskey="&filterActionDesc.accesskey;" + control="filterActionList"/> + <listbox id="filterActionList" flex="1" rows="4" minheight="35%" + onfocus="setLastActionFocus();" focusedAction="0"> + <listcols> + <listcol flex="&filterActionTypeFlexValue;"/> + <listcol flex="&filterActionTargetFlexValue;"/> + <listcol class="filler"/> + </listcols> + </listbox> + </vbox> + + <vbox id="statusbar" style="visibility: hidden;"> + <hbox align="center"> + <label> + &filterActionOrderWarning.label; + </label> + <label class="text-link" onclick="showActionsOrder();">&filterActionOrder.label;</label> + </hbox> + </vbox> +</dialog> diff --git a/mailnews/base/search/content/searchTermOverlay.js b/mailnews/base/search/content/searchTermOverlay.js new file mode 100644 index 000000000..faf4ba91c --- /dev/null +++ b/mailnews/base/search/content/searchTermOverlay.js @@ -0,0 +1,536 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var gTotalSearchTerms=0; +var gSearchTermList; +var gSearchTerms = new Array; +var gSearchRemovedTerms = new Array; +var gSearchScope; +var gSearchBooleanRadiogroup; + +var gUniqueSearchTermCounter = 0; // gets bumped every time we add a search term so we can always + // dynamically generate unique IDs for the terms. + +// cache these so we don't have to hit the string bundle for them +var gMoreButtonTooltipText; +var gLessButtonTooltipText; +var gLoading = true; + + +function searchTermContainer() {} + +searchTermContainer.prototype = { + internalSearchTerm : '', + internalBooleanAnd : '', + + // this.searchTerm: the actual nsIMsgSearchTerm object + get searchTerm() { return this.internalSearchTerm; }, + set searchTerm(val) { + this.internalSearchTerm = val; + + var term = val; + // val is a nsIMsgSearchTerm + var searchAttribute=this.searchattribute; + var searchOperator=this.searchoperator; + var searchValue=this.searchvalue; + + // now reflect all attributes of the searchterm into the widgets + if (searchAttribute) + { + // for custom, the value is the custom id, not the integer attribute + if (term.attrib == Components.interfaces.nsMsgSearchAttrib.Custom) + searchAttribute.value = term.customId; + else + searchAttribute.value = term.attrib; + } + if (searchOperator) searchOperator.value = val.op; + if (searchValue) searchValue.value = term.value; + + this.booleanAnd = val.booleanAnd; + this.matchAll = val.matchAll; + return val; + }, + + // searchscope - just forward to the searchattribute + get searchScope() { + if (this.searchattribute) + return this.searchattribute.searchScope; + return undefined; + }, + set searchScope(val) { + var searchAttribute = this.searchattribute; + if (searchAttribute) searchAttribute.searchScope=val; + return val; + }, + + saveId: function (element, slot) { + this[slot] = element.id; + }, + + getElement: function (slot) { + return document.getElementById(this[slot]); + }, + + // three well-defined properties: + // searchattribute, searchoperator, searchvalue + // the trick going on here is that we're storing the Element's Id, + // not the element itself, because the XBL object may change out + // from underneath us + get searchattribute() { return this.getElement("internalSearchAttributeId"); }, + set searchattribute(val) { + this.saveId(val, "internalSearchAttributeId"); + return val; + }, + get searchoperator() { return this.getElement("internalSearchOperatorId"); }, + set searchoperator(val) { + this.saveId(val, "internalSearchOperatorId"); + return val; + }, + get searchvalue() { return this.getElement("internalSearchValueId"); }, + set searchvalue(val) { + this.saveId(val, "internalSearchValueId"); + return val; + }, + + booleanNodes: null, + get booleanAnd() { return this.internalBooleanAnd; }, + set booleanAnd(val) { + this.internalBooleanAnd = val; + return val; + }, + + save: function () { + var searchTerm = this.searchTerm; + var nsMsgSearchAttrib = Components.interfaces.nsMsgSearchAttrib; + + if (isNaN(this.searchattribute.value)) // is this a custom term? + { + searchTerm.attrib = nsMsgSearchAttrib.Custom; + searchTerm.customId = this.searchattribute.value; + } + else + { + searchTerm.attrib = this.searchattribute.value; + } + + if (this.searchattribute.value > nsMsgSearchAttrib.OtherHeader && this.searchattribute.value < nsMsgSearchAttrib.kNumMsgSearchAttributes) + searchTerm.arbitraryHeader = this.searchattribute.label; + searchTerm.op = this.searchoperator.value; + if (this.searchvalue.value) + this.searchvalue.save(); + else + this.searchvalue.saveTo(searchTerm.value); + searchTerm.value = this.searchvalue.value; + searchTerm.booleanAnd = this.booleanAnd; + searchTerm.matchAll = this.matchAll; + }, + // if you have a search term element with no search term + saveTo: function(searchTerm) { + this.internalSearchTerm = searchTerm; + this.save(); + } +} + +var nsIMsgSearchTerm = Components.interfaces.nsIMsgSearchTerm; + +function initializeSearchWidgets() +{ + gSearchBooleanRadiogroup = document.getElementById("booleanAndGroup"); + gSearchTermList = document.getElementById("searchTermList"); + + // initialize some strings + var bundle = document.getElementById('bundle_search'); + gMoreButtonTooltipText = bundle.getString('moreButtonTooltipText'); + gLessButtonTooltipText = bundle.getString('lessButtonTooltipText'); +} + +function initializeBooleanWidgets() +{ + var booleanAnd = true; + var matchAll = false; + // get the boolean value from the first term + var firstTerm = gSearchTerms[0].searchTerm; + if (firstTerm) + { + // If there is a second term, it should actually define whether we're + // using 'and' or not. Note that our UI is not as rich as the + // underlying search model, so there's the potential to lose here when + // grouping is involved. + booleanAnd = (gSearchTerms.length > 1) ? + gSearchTerms[1].searchTerm.booleanAnd : firstTerm.booleanAnd; + matchAll = firstTerm.matchAll; + } + // target radio items have value="and" or value="or" or "all" + gSearchBooleanRadiogroup.value = matchAll + ? "matchAll" + : (booleanAnd ? "and" : "or") + var searchTerms = document.getElementById("searchTermList"); + if (searchTerms) + updateSearchTermsListbox(matchAll); +} + +function initializeSearchRows(scope, searchTerms) +{ + for (var i = 0; i < searchTerms.Count(); i++) { + var searchTerm = searchTerms.QueryElementAt(i, nsIMsgSearchTerm); + createSearchRow(i, scope, searchTerm, false); + gTotalSearchTerms++; + } + initializeBooleanWidgets(); + updateRemoveRowButton(); +} + +/** + * Enables/disables all the visible elements inside the search terms listbox. + * + * @param matchAllValue boolean value from the first search term + */ +function updateSearchTermsListbox(matchAllValue) +{ + var searchTerms = document.getElementById("searchTermList"); + searchTerms.setAttribute("disabled", matchAllValue); + var searchAttributeList = searchTerms.getElementsByTagName("searchattribute"); + var searchOperatorList = searchTerms.getElementsByTagName("searchoperator"); + var searchValueList = searchTerms.getElementsByTagName("searchvalue"); + for (var i = 0; i < searchAttributeList.length; i++) { + searchAttributeList[i].setAttribute("disabled", matchAllValue); + searchOperatorList[i].setAttribute("disabled", matchAllValue); + searchValueList[i].setAttribute("disabled", matchAllValue); + if (!matchAllValue) + searchValueList[i].removeAttribute("disabled"); + } + var moreOrLessButtonsList = searchTerms.getElementsByTagName("button"); + for (var i = 0; i < moreOrLessButtonsList.length; i++) { + moreOrLessButtonsList[i].setAttribute("disabled", matchAllValue); + } + if (!matchAllValue) + updateRemoveRowButton(); +} + +// enables/disables the less button for the first row of search terms. +function updateRemoveRowButton() +{ + var firstListItem = gSearchTermList.getItemAtIndex(0); + if (firstListItem) + firstListItem.lastChild.lastChild.setAttribute("disabled", gTotalSearchTerms == 1); +} + +// Returns the actual list item row index in the list of search rows +// that contains the passed in element id. +function getSearchRowIndexForElement(aElement) +{ + var listItem = aElement; + + while (listItem && listItem.localName != "listitem") + listItem = listItem.parentNode; + + return gSearchTermList.getIndexOfItem(listItem); +} + +function onMore(event) +{ + // if we have an event, extract the list row index and use that as the row number + // for our insertion point. If there is no event, append to the end.... + var rowIndex; + + if (event) + rowIndex = getSearchRowIndexForElement(event.target) + 1; + else + rowIndex = gSearchTermList.getRowCount(); + + createSearchRow(rowIndex, gSearchScope, null, event != null); + gTotalSearchTerms++; + updateRemoveRowButton(); + + // the user just added a term, so scroll to it + gSearchTermList.ensureIndexIsVisible(rowIndex); +} + +function onLess(event) +{ + if (event && gTotalSearchTerms > 1) + { + removeSearchRow(getSearchRowIndexForElement(event.target)); + --gTotalSearchTerms; + } + + updateRemoveRowButton(); +} + +// set scope on all visible searchattribute tags +function setSearchScope(scope) +{ + gSearchScope = scope; + for (var i = 0; i < gSearchTerms.length; i++) + { + // don't set element attributes if XBL hasn't loaded + if (!(gSearchTerms[i].obj.searchattribute.searchScope === undefined)) + { + gSearchTerms[i].obj.searchattribute.searchScope = scope; + // act like the user "selected" this, see bug #202848 + gSearchTerms[i].obj.searchattribute.onSelect(null /* no event */); + } + gSearchTerms[i].scope = scope; + } +} + +function updateSearchAttributes() +{ + for (var i=0; i<gSearchTerms.length; i++) + gSearchTerms[i].obj.searchattribute.refreshList(); + } + +function booleanChanged(event) { + // when boolean changes, we have to update all the attributes on the search terms + var newBoolValue = (event.target.getAttribute("value") == "and"); + var matchAllValue = (event.target.getAttribute("value") == "matchAll"); + if (document.getElementById("abPopup")) { + var selectedAB = document.getElementById("abPopup").selectedItem.value; + setSearchScope(GetScopeForDirectoryURI(selectedAB)); + } + for (var i=0; i<gSearchTerms.length; i++) { + let searchTerm = gSearchTerms[i].obj; + // If term is not yet initialized in the UI, change the original object. + if (!searchTerm || !gSearchTerms[i].initialized) + searchTerm = gSearchTerms[i].searchTerm; + + searchTerm.booleanAnd = newBoolValue; + searchTerm.matchAll = matchAllValue; + } + var searchTerms = document.getElementById("searchTermList"); + if (searchTerms) + { + if (!matchAllValue && searchTerms.hidden && !gTotalSearchTerms) + onMore(null); // fake to get empty row. + updateSearchTermsListbox(matchAllValue); + } +} + +/** + * Create a new search row with all the needed elements. + * + * @param index index of the position in the menulist where to add the row + * @param scope a nsMsgSearchScope constant indicating scope of this search rule + * @param searchTerm nsIMsgSearchTerm object to hold the search term + * @param aUserAdded boolean indicating if the row addition was initiated by the user + * (e.g. via the '+' button) + */ +function createSearchRow(index, scope, searchTerm, aUserAdded) +{ + var searchAttr = document.createElement("searchattribute"); + var searchOp = document.createElement("searchoperator"); + var searchVal = document.createElement("searchvalue"); + + var moreButton = document.createElement("button"); + var lessButton = document.createElement("button"); + moreButton.setAttribute("class", "small-button"); + moreButton.setAttribute("oncommand", "onMore(event);"); + moreButton.setAttribute('label', '+'); + moreButton.setAttribute('tooltiptext', gMoreButtonTooltipText); + lessButton.setAttribute("class", "small-button"); + lessButton.setAttribute("oncommand", "onLess(event);"); + lessButton.setAttribute('label', '\u2212'); + lessButton.setAttribute('tooltiptext', gLessButtonTooltipText); + + // now set up ids: + searchAttr.id = "searchAttr" + gUniqueSearchTermCounter; + searchOp.id = "searchOp" + gUniqueSearchTermCounter; + searchVal.id = "searchVal" + gUniqueSearchTermCounter; + + searchAttr.setAttribute("for", searchOp.id + "," + searchVal.id); + searchOp.setAttribute("opfor", searchVal.id); + + var rowdata = [searchAttr, searchOp, searchVal, + [moreButton, lessButton] ]; + var searchrow = constructRow(rowdata); + searchrow.id = "searchRow" + gUniqueSearchTermCounter; + + var searchTermObj = new searchTermContainer; + searchTermObj.searchattribute = searchAttr; + searchTermObj.searchoperator = searchOp; + searchTermObj.searchvalue = searchVal; + + // now insert the new search term into our list of terms + gSearchTerms.splice(index, 0, {obj:searchTermObj, scope:scope, searchTerm:searchTerm, initialized:false}); + + var editFilter = null; + try { editFilter = gFilter; } catch(e) { } + + var editMailView = null; + try { editMailView = gMailView; } catch(e) { } + + if ((!editFilter && !editMailView) || + (editFilter && index == gTotalSearchTerms) || + (editMailView && index == gTotalSearchTerms)) + gLoading = false; + + // index is index of new row + // gTotalSearchTerms has not been updated yet + if (gLoading || index == gTotalSearchTerms) { + gSearchTermList.appendChild(searchrow); + } + else { + var currentItem = gSearchTermList.getItemAtIndex(index); + gSearchTermList.insertBefore(searchrow, currentItem); + } + + // If this row was added by user action, focus the value field. + if (aUserAdded) { + document.commandDispatcher.advanceFocusIntoSubtree(searchVal); + searchrow.setAttribute("highlight", "true"); + } + + // bump our unique search term counter + gUniqueSearchTermCounter++; +} + +function initializeTermFromId(id) +{ + initializeTermFromIndex(getSearchRowIndexForElement(document.getElementById(id))); +} + +function initializeTermFromIndex(index) +{ + var searchTermObj = gSearchTerms[index].obj; + + searchTermObj.searchScope = gSearchTerms[index].scope; + // the search term will initialize the searchTerm element, including + // .booleanAnd + if (gSearchTerms[index].searchTerm) + searchTermObj.searchTerm = gSearchTerms[index].searchTerm; + // here, we don't have a searchTerm, so it's probably a new element - + // we'll initialize the .booleanAnd from the existing setting in + // the UI + else + { + searchTermObj.booleanAnd = (gSearchBooleanRadiogroup.value == "and"); + if (index) + { + // If we weren't pre-initialized with a searchTerm then steal the + // search attribute and operator from the previous row. + searchTermObj.searchattribute.value = gSearchTerms[index - 1].obj.searchattribute.value; + searchTermObj.searchoperator.value = gSearchTerms[index - 1].obj.searchoperator.value; + } + } + + gSearchTerms[index].initialized = true; +} + +/** + * Creates a <listitem> using the array children as the children + * of each listcell. + * @param aChildren An array of XUL elements to put into the listitem. + * Each array member is put into a separate listcell. + * If the member itself is an array of elements, + * all of them are put into the same listcell. + */ +function constructRow(aChildren) +{ + let listitem = document.createElement("listitem"); + listitem.setAttribute("allowevents", "true"); + for (let i = 0; i < aChildren.length; i++) { + let listcell = document.createElement("listcell"); + let child = aChildren[i]; + + if (child instanceof Array) { + for (let j = 0; j < child.length; j++) + listcell.appendChild(child[j]); + } else { + child.setAttribute("flex", "1"); + listcell.appendChild(child); + } + listitem.appendChild(listcell); + } + return listitem; +} + +function removeSearchRow(index) +{ + var searchTermObj = gSearchTerms[index].obj; + if (!searchTermObj) { + return; + } + + // if it is an existing (but offscreen) term, + // make sure it is initialized before we remove it. + if (!gSearchTerms[index].searchTerm && !gSearchTerms[index].initialized) + initializeTermFromIndex(index); + + // need to remove row from list, so walk upwards from the + // searchattribute to find the first <listitem> + var listitem = searchTermObj.searchattribute; + + while (listitem) { + if (listitem.localName == "listitem") break; + listitem = listitem.parentNode; + } + + if (!listitem) { + dump("Error: couldn't find parent listitem!\n"); + return; + } + + + if (searchTermObj.searchTerm) { + gSearchRemovedTerms[gSearchRemovedTerms.length] = searchTermObj.searchTerm; + } else { + //dump("That wasn't real. ignoring \n"); + } + + listitem.remove(); + + // now remove the item from our list of terms + gSearchTerms.splice(index, 1); +} + +// save the search terms from the UI back to the actual search terms +// searchTerms: nsISupportsArray of terms +// termOwner: object which can contain and create the terms +// (will be unnecessary if we just make terms creatable +// via XPCOM) +function saveSearchTerms(searchTerms, termOwner) +{ + var matchAll = gSearchBooleanRadiogroup.value == 'matchAll'; + var i; + for (i = 0; i < gSearchRemovedTerms.length; i++) + searchTerms.RemoveElement(gSearchRemovedTerms[i]); + + for (i = 0; i < gSearchTerms.length; i++) { + try { + gSearchTerms[i].obj.matchAll = matchAll; + var searchTerm = gSearchTerms[i].obj.searchTerm; + if (searchTerm) { + gSearchTerms[i].obj.save(); + } else if (!gSearchTerms[i].initialized) { + // the term might be an offscreen one we haven't initialized yet + searchTerm = gSearchTerms[i].searchTerm; + } else { + // need to create a new searchTerm, and somehow save it to that + searchTerm = termOwner.createTerm(); + gSearchTerms[i].obj.saveTo(searchTerm); + // this might not be the right place for the term, + // but we need to make the array longer anyway + termOwner.appendTerm(searchTerm); + } + searchTerms.SetElementAt(i, searchTerm); + } catch (ex) { + dump("** Error saving element " + i + ": " + ex + "\n"); + } + } +} + +function onReset(event) +{ + while (gTotalSearchTerms>0) + removeSearchRow(--gTotalSearchTerms); + onMore(null); +} + +function hideMatchAllItem() +{ + var allItems = document.getElementById('matchAllItem'); + if (allItems) + allItems.hidden = true; +} diff --git a/mailnews/base/search/content/searchTermOverlay.xul b/mailnews/base/search/content/searchTermOverlay.xul new file mode 100644 index 000000000..c14e05cb0 --- /dev/null +++ b/mailnews/base/search/content/searchTermOverlay.xul @@ -0,0 +1,66 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/searchTermOverlay.dtd"> + +<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://messenger/content/searchTermOverlay.js"/> + <script type="application/javascript" + src="chrome://messenger/content/dateFormat.js"/> + + <vbox id="searchTermListBox"> + + <radiogroup id="booleanAndGroup" orient="horizontal" value="and" + oncommand="booleanChanged(event);"> + <radio value="and" label="&matchAll.label;" + accesskey="&matchAll.accesskey;"/> + <radio value="or" label="&matchAny.label;" + accesskey="&matchAny.accesskey;"/> + <radio value="matchAll" id="matchAllItem" label="&matchAllMsgs.label;" + accesskey="&matchAllMsgs.accesskey;"/> + </radiogroup> + + <hbox flex="1"> + <hbox id="searchterms"/> + <listbox flex="1" id="searchTermList" rows="4" minheight="35%"> + <listcols> + <listcol flex="&searchTermListAttributesFlexValue;"/> + <listcol flex="&searchTermListOperatorsFlexValue;"/> + <listcol flex="&searchTermListValueFlexValue;"/> + <listcol class="filler"/> + </listcols> + + <!-- this is what the listitems will look like: + <listitem id="searchListItem"> + <listcell allowevents="true"> + <searchattribute id="searchAttr1" for="searchOp1,searchValue1" flex="1"/> + </listcell> + <listcell allowevents="true"> + <searchoperator id="searchOp1" opfor="searchValue1" flex="1"/> + </listcell> + <listcell allowevents="true" > + <searchvalue id="searchValue1" flex="1"/> + </listcell> + <listcell> + <button label="add"/> + <button label="remove"/> + </listcell> + </listitem> + <listitem> + <listcell label="the.."/> + <listcell label="contains.."/> + <listcell label="text here"/> + <listcell label="+/-"/> + </listitem> + --> + </listbox> + + </hbox> + </vbox> + +</overlay> diff --git a/mailnews/base/search/content/searchWidgets.xml b/mailnews/base/search/content/searchWidgets.xml new file mode 100644 index 000000000..80ebe38c4 --- /dev/null +++ b/mailnews/base/search/content/searchWidgets.xml @@ -0,0 +1,738 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!-- + This file has the following external dependencies: + -gFilterActionStrings from FilterEditor.js + -gFilterList from FilterEditor.js + -gFilter from FilterEditor.js + -gCustomActions from FilterEditor.js + -gFilterType from FilterEditor.js + -checkActionsReorder from FilterEditor.js +--> + +<!DOCTYPE dialog [ + <!ENTITY % filterEditorDTD SYSTEM "chrome://messenger/locale/FilterEditor.dtd" > +%filterEditorDTD; + <!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd" > +%messengerDTD; +]> + +<bindings id="filterBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:nc="http://home.netscape.com/NC-rdf#" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="ruleactiontype-menulist"> + <content> + <xul:menulist class="ruleaction-type"> + <xul:menupopup> + <xul:menuitem label="&moveMessage.label;" value="movemessage" enablefornews="false"/> + <xul:menuitem label="©Message.label;" value="copymessage"/> + <xul:menuseparator enablefornews="false"/> + <xul:menuitem label="&forwardTo.label;" value="forwardmessage" enablefornews="false"/> + <xul:menuitem label="&replyWithTemplate.label;" value="replytomessage" enablefornews="false"/> + <xul:menuseparator/> + <xul:menuitem label="&markMessageRead.label;" value="markasread"/> + <xul:menuitem label="&markMessageUnread.label;" value="markasunread"/> + <xul:menuitem label="&markMessageStarred.label;" value="markasflagged"/> + <xul:menuitem label="&setPriority.label;" value="setpriorityto"/> + <xul:menuitem label="&addTag.label;" value="addtagtomessage"/> + <xul:menuitem label="&setJunkScore.label;" value="setjunkscore" enablefornews="false"/> + <xul:menuseparator enableforpop3="true"/> + <xul:menuitem label="&deleteMessage.label;" value="deletemessage"/> + <xul:menuitem label="&deleteFromPOP.label;" value="deletefrompopserver" enableforpop3="true"/> + <xul:menuitem label="&fetchFromPOP.label;" value="fetchfrompopserver" enableforpop3="true"/> + <xul:menuseparator/> + <xul:menuitem label="&ignoreThread.label;" value="ignorethread"/> + <xul:menuitem label="&ignoreSubthread.label;" value="ignoresubthread"/> + <xul:menuitem label="&watchThread.label;" value="watchthread"/> + <xul:menuseparator/> + <xul:menuitem label="&stopExecution.label;" value="stopexecution"/> + </xul:menupopup> + </xul:menulist> + </content> + + <implementation> + <constructor> + <![CDATA[ + this.addCustomActions(); + this.hideInvalidActions(); + // differentiate between creating a new, next available action, + // and creating a row which will be initialized with an action + if (!this.parentNode.hasAttribute('initialActionIndex')) + { + var unavailableActions = this.usedActionsList(); + // select the first one that's not in the list + for (var index = 0; index < this.menuitems.length; index++) + { + var menu = this.menuitems[index]; + if (!(menu.value in unavailableActions) && !menu.hidden) + { + this.menulist.value = menu.value; + this.parentNode.setAttribute('value', menu.value); + break; + } + } + } + else + { + this.parentNode.mActionTypeInitialized = true; + this.parentNode.clearInitialActionIndex(); + } + ]]> + </constructor> + + <field name="menulist">document.getAnonymousNodes(this)[0]</field> + <field name="menuitems">this.menulist.getElementsByTagNameNS(this.menulist.namespaceURI, 'menuitem')</field> + + <method name="hideInvalidActions"> + <body> + <![CDATA[ + let menupopup = this.menulist.menupopup; + let scope = getScopeFromFilterList(gFilterList); + + // walk through the list of filter actions and hide any actions which aren't valid + // for our given scope (news, imap, pop, etc) and context + let elements, i; + + // disable / enable all elements in the "filteractionlist" + // based on the scope and the "enablefornews" attribute + elements = menupopup.getElementsByAttribute("enablefornews", "true"); + for (i = 0; i < elements.length; i++) + elements[i].hidden = scope != Components.interfaces.nsMsgSearchScope.newsFilter; + + elements = menupopup.getElementsByAttribute("enablefornews", "false"); + for (i = 0; i < elements.length; i++) + elements[i].hidden = scope == Components.interfaces.nsMsgSearchScope.newsFilter; + + elements = menupopup.getElementsByAttribute("enableforpop3", "true"); + for (i = 0; i < elements.length; i++) + elements[i].hidden = !((gFilterList.folder.server.type == "pop3") || + (gFilterList.folder.server.type == "none")); + + elements = menupopup.getElementsByAttribute("isCustom", "true"); + // Note there might be an additional element here as a placeholder + // for a missing action, so we iterate over the known actions + // instead of the elements. + for (i = 0; i < gCustomActions.length; i++) + elements[i].hidden = !gCustomActions[i] + .isValidForType(gFilterType, scope); + + // Disable "Reply with Template" if there are no templates. + if (!this.getTemplates(false)) { + elements = menupopup.getElementsByAttribute("value", "replytomessage"); + if (elements.length == 1) + elements[0].hidden = true; + } + ]]> + </body> + </method> + + <method name="addCustomActions"> + <body> + <![CDATA[ + var menupopup = this.menulist.menupopup; + for (var i = 0; i < gCustomActions.length; i++) + { + var customAction = gCustomActions[i]; + var menuitem = document.createElementNS( + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "xul:menuitem"); + menuitem.setAttribute("label", customAction.name); + menuitem.setAttribute("value", customAction.id); + menuitem.setAttribute("isCustom", "true"); + menupopup.appendChild(menuitem); + } + ]]> + </body> + </method> + + <!-- returns a hash containing all of the filter actions which are currently being used by other filteractionrows --> + <method name="usedActionsList"> + <body> + <![CDATA[ + var usedActions = {}; + var currentFilterActionRow = this.parentNode; + var listBox = currentFilterActionRow.mListBox; // need to account for the list item + // now iterate over each list item in the list box + for (var index = 0; index < listBox.getRowCount(); index++) + { + var filterActionRow = listBox.getItemAtIndex(index); + if (filterActionRow != currentFilterActionRow) + { + var actionValue = filterActionRow.getAttribute('value'); + + // let custom actions decide if dups are allowed + var isCustom = false; + for (var i = 0; i < gCustomActions.length; i++) + { + if (gCustomActions[i].id == actionValue) + { + isCustom = true; + if (!gCustomActions[i].allowDuplicates) + usedActions[actionValue] = true; + break; + } + } + + if (!isCustom) { + // The following actions can appear more than once in a single filter + // so do not set them as already used. + if (actionValue != 'addtagtomessage' && + actionValue != 'forwardmessage' && + actionValue != 'copymessage') + usedActions[actionValue] = true; + // If either Delete message or Move message exists, disable the other one. + // It does not make sense to apply both to the same message. + if (actionValue == 'deletemessage') + usedActions['movemessage'] = true; + else if (actionValue == 'movemessage') + usedActions['deletemessage'] = true; + // The same with Mark as read/Mark as Unread. + else if (actionValue == 'markasread') + usedActions['markasunread'] = true; + else if (actionValue == 'markasunread') + usedActions['markasread'] = true; + } + } + } + return usedActions; + ]]> + </body> + </method> + + <!-- + - Check if there exist any templates in this account. + - + - @param populateTemplateList If true, create menuitems representing + - the found templates. + - @param templateMenuList The menulist element to create items in. + - + - @return True if at least one template was found, otherwise false. + --> + <method name="getTemplates"> + <parameter name="populateTemplateList"/> + <parameter name="templateMenuList"/> + <body> + <![CDATA[ + Components.utils.import("resource:///modules/iteratorUtils.jsm", this); + let identitiesRaw = MailServices.accounts + .getIdentitiesForServer(gFilterList.folder.server); + let identities = Array.from(this.fixIterator(identitiesRaw, + Components.interfaces.nsIMsgIdentity)); + + if (!identities.length) // typically if this is Local Folders + identities.push(MailServices.accounts.defaultAccount.defaultIdentity); + + let templateFound = false; + let foldersScanned = []; + + for (let identity of identities) { + let enumerator = null; + let msgFolder; + try { + msgFolder = Components.classes["@mozilla.org/rdf/rdf-service;1"] + .getService(Components.interfaces.nsIRDFService) + .GetResource(identity.stationeryFolder) + .QueryInterface(Components.interfaces.nsIMsgFolder); + // If we already processed this folder, do not set enumerator + // so that we skip this identity. + if (foldersScanned.indexOf(msgFolder) == -1) { + foldersScanned.push(msgFolder); + enumerator = msgFolder.msgDatabase.EnumerateMessages(); + } + } catch (e) { + // The Templates folder may not exist, that is OK. + } + + if (!enumerator) + continue; + + while (enumerator.hasMoreElements()) { + let header = enumerator.getNext(); + if (header instanceof Components.interfaces.nsIMsgDBHdr) { + templateFound = true; + if (!populateTemplateList) + return true; + let msgTemplateUri = msgFolder.URI + "?messageId=" + + header.messageId + '&subject=' + header.mime2DecodedSubject; + let newItem = templateMenuList.appendItem(header.mime2DecodedSubject, + msgTemplateUri); + } + } + } + + return templateFound; + ]]> + </body> + </method> + + </implementation> + + <handlers> + <handler event="command"> + <![CDATA[ + this.parentNode.setAttribute('value', this.menulist.value); + checkActionsReorder(); + ]]> + </handler> + + <handler event="popupshowing"> + <![CDATA[ + var unavailableActions = this.usedActionsList(); + for (var index = 0; index < this.menuitems.length; index++) + { + var menu = this.menuitems[index]; + menu.setAttribute('disabled', menu.value in unavailableActions); + } + ]]> + </handler> + </handlers> + </binding> + + <!-- This binding exists to disable the default binding of a listitem + in the search terms. --> + <binding id="listitem"> + <implementation> + <method name="_fireEvent"> + <parameter name="aName"/> + <body> + <![CDATA[ + /* This provides a dummy _fireEvent function that + the listbox expects to be able to call. + See bug 202036. */ + ]]> + </body> + </method> + </implementation> + </binding> + + <binding id="ruleaction" extends="#listitem"> + <content allowevents="true"> + <xul:listcell class="ruleactiontype" + orient="vertical" align="stretch" pack="center"/> + <xul:listcell class="ruleactiontarget" xbl:inherits="type=value" + orient="vertical" align="stretch" pack="center"/> + <xul:listcell> + <xul:button class="small-button" + label="+" + tooltiptext="&addAction.tooltip;" + oncommand="this.parentNode.parentNode.addRow();"/> + <xul:button class="small-button" + label="−" + tooltiptext="&removeAction.tooltip;" + oncommand="this.parentNode.parentNode.removeRow();" + anonid="removeButton"/> + </xul:listcell> + </content> + + <implementation> + <field name="mListBox">this.parentNode</field> + <field name="mRemoveButton">document.getAnonymousElementByAttribute(this, "anonid", "removeButton")</field> + <field name="mActionTypeInitialized">false</field> + <field name="mRuleActionTargetInitialized">false</field> + <field name="mRuleActionType">document.getAnonymousNodes(this)[0]</field> + + <method name="clearInitialActionIndex"> + <body> + <![CDATA[ + // we should only remove the initialActionIndex after we have been told that + // both the rule action type and the rule action target have both been built since they both need + // this piece of information. This complication arises because both of these child elements are getting + // bound asynchronously after the search row has been constructed + + if (this.mActionTypeInitialized && this.mRuleActionTargetInitialized) + this.removeAttribute('initialActionIndex'); + ]]> + </body> + </method> + + <method name="initWithAction"> + <parameter name="aFilterAction"/> + <body> + <![CDATA[ + var filterActionStr; + var actionTarget = document.getAnonymousNodes(this)[1]; + var actionItem = document.getAnonymousNodes(actionTarget); + var nsMsgFilterAction = Components.interfaces.nsMsgFilterAction; + switch (aFilterAction.type) + { + case nsMsgFilterAction.Custom: + filterActionStr = aFilterAction.customId; + if (actionItem) + actionItem[0].value = aFilterAction.strValue; + + // Make sure the custom action has been added. If not, it + // probably was from an extension that has been removed. We'll + // show a dummy menuitem to warn the user. + var needCustomLabel = true; + for (var i = 0; i < gCustomActions.length; i++) + { + if (gCustomActions[i].id == filterActionStr) + { + needCustomLabel = false; + break; + } + } + if (needCustomLabel) + { + var menuitem = document.createElementNS( + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "xul:menuitem"); + menuitem.setAttribute("label", + gFilterBundle.getString("filterMissingCustomAction")); + menuitem.setAttribute("value", filterActionStr); + menuitem.disabled = true; + this.mRuleActionType.menulist.menupopup.appendChild(menuitem); + var scriptError = Components.classes["@mozilla.org/scripterror;1"] + .createInstance(Components.interfaces.nsIScriptError); + scriptError.init("Missing custom action " + filterActionStr, + null, null, 0, 0, + Components.interfaces.nsIScriptError.errorFlag, + "component javascript"); + Services.console.logMessage(scriptError); + } + break; + case nsMsgFilterAction.MoveToFolder: + case nsMsgFilterAction.CopyToFolder: + actionItem[0].value = aFilterAction.targetFolderUri; + break; + case nsMsgFilterAction.Reply: + case nsMsgFilterAction.Forward: + actionItem[0].value = aFilterAction.strValue; + break; + case nsMsgFilterAction.Label: + actionItem[0].value = aFilterAction.label; + break; + case nsMsgFilterAction.ChangePriority: + actionItem[0].value = aFilterAction.priority; + break; + case nsMsgFilterAction.JunkScore: + actionItem[0].value = aFilterAction.junkScore; + break; + case nsMsgFilterAction.AddTag: + actionItem[0].value = aFilterAction.strValue; + break; + default: + break; + } + if (aFilterAction.type != nsMsgFilterAction.Custom) + filterActionStr = gFilterActionStrings[aFilterAction.type]; + document.getAnonymousNodes(this.mRuleActionType)[0] + .value = filterActionStr; + this.mRuleActionTargetInitialized = true; + this.clearInitialActionIndex(); + checkActionsReorder(); + ]]> + </body> + </method> + + <method name="validateAction"> + <body> + <![CDATA[ + // returns true if this row represents a valid filter action and false otherwise. + // This routine also prompts the user. + Components.utils.import("resource:///modules/MailUtils.js", this); + var filterActionString = this.getAttribute('value'); + var actionTarget = document.getAnonymousNodes(this)[1]; + var errorString, customError; + + switch (filterActionString) + { + case "movemessage": + case "copymessage": + let msgFolder = document.getAnonymousNodes(actionTarget)[0].value ? + this.MailUtils.getFolderForURI(document.getAnonymousNodes(actionTarget)[0].value) : null; + if (!msgFolder || !msgFolder.canFileMessages) + errorString = "mustSelectFolder"; + break; + case "forwardmessage": + if (document.getAnonymousNodes(actionTarget)[0].value.length < 3 || + document.getAnonymousNodes(actionTarget)[0].value.indexOf('@') < 1) + errorString = "enterValidEmailAddress"; + break; + case "replytomessage": + if (!document.getAnonymousNodes(actionTarget)[0].selectedItem) + errorString = "pickTemplateToReplyWith"; + break; + default: + // some custom actions have no action value node + if (!document.getAnonymousNodes(actionTarget)) + return true; + // locate the correct custom action, and check validity + for (var i = 0; i < gCustomActions.length; i++) + if (gCustomActions[i].id == filterActionString) + { + customError = + gCustomActions[i].validateActionValue( + document.getAnonymousNodes(actionTarget)[0].value, + gFilterList.folder, gFilterType); + break; + } + break; + } + + errorString = errorString ? + gFilterBundle.getString(errorString) : + customError; + if (errorString) + Services.prompt.alert(window, null, errorString); + + return !errorString; + ]]> + </body> + </method> + + <method name="saveToFilter"> + <parameter name="aFilter"/> + <body> + <![CDATA[ + // create a new filter action, fill it in, and then append it to the filter + var filterAction = aFilter.createAction(); + var filterActionString = this.getAttribute('value'); + filterAction.type = gFilterActionStrings.indexOf(filterActionString); + var actionTarget = document.getAnonymousNodes(this)[1]; + var actionItem = document.getAnonymousNodes(actionTarget); + var nsMsgFilterAction = Components.interfaces.nsMsgFilterAction; + switch (filterAction.type) + { + case nsMsgFilterAction.Label: + filterAction.label = actionItem[0].getAttribute("value"); + break; + case nsMsgFilterAction.ChangePriority: + filterAction.priority = actionItem[0].getAttribute("value"); + break; + case nsMsgFilterAction.MoveToFolder: + case nsMsgFilterAction.CopyToFolder: + filterAction.targetFolderUri = actionItem[0].value; + break; + case nsMsgFilterAction.JunkScore: + filterAction.junkScore = actionItem[0].value; + break; + case nsMsgFilterAction.Custom: + filterAction.customId = filterActionString; + // fall through to set the value + default: + if (actionItem) + filterAction.strValue = actionItem[0].value; + break; + } + aFilter.appendAction(filterAction); + ]]> + </body> + </method> + + <method name="getActionStrings"> + <parameter name="aActionStrings"/> + <body> + <![CDATA[ + // Collect the action names and arguments in a plain string form. + let actionTarget = document.getAnonymousNodes(this)[1]; + let actionItem = document.getAnonymousNodes(actionTarget); + + aActionStrings.push({ + label: document.getAnonymousNodes(this.mRuleActionType)[0].label, + argument: actionItem ? + (actionItem[0].label ? + actionItem[0].label : actionItem[0].value) : "" + }); + ]]> + </body> + </method> + + <method name="updateRemoveButton"> + <body> + <![CDATA[ + // if we only have one row of actions, then disable the remove button for that row + this.mListBox.getItemAtIndex(0).mRemoveButton.disabled = this.mListBox.getRowCount() == 1; + ]]> + </body> + </method> + + <method name="addRow"> + <body> + <![CDATA[ + let listItem = document.createElement('listitem'); + listItem.className = 'ruleaction'; + listItem.setAttribute('onfocus','this.storeFocus();'); + this.mListBox.insertBefore(listItem, this.nextSibling); + this.mListBox.ensureElementIsVisible(listItem); + + // make sure the first remove button is enabled + this.updateRemoveButton(); + checkActionsReorder(); + ]]> + </body> + </method> + + <method name="removeRow"> + <body> + <![CDATA[ + // this.mListBox will fail after the row is removed, so save it + let listBox = this.mListBox; + if (listBox.getRowCount() > 1) + this.remove(); + // can't use 'this' as it is destroyed now + listBox.getItemAtIndex(0).updateRemoveButton(); + checkActionsReorder(); + ]]> + </body> + </method> + + <method name="storeFocus"> + <body> + <![CDATA[ + // When this action row is focused, store its index in the parent listbox. + this.mListBox.setAttribute("focusedAction", this.mListBox.getIndexOfItem(this)); + ]]> + </body> + </method> + + </implementation> + </binding> + + <binding id="ruleactiontarget-base"> + <implementation> + <constructor> + <![CDATA[ + if (this.parentNode.hasAttribute('initialActionIndex')) + { + let actionIndex = this.parentNode.getAttribute('initialActionIndex'); + let filterAction = gFilter.getActionAt(actionIndex); + this.parentNode.initWithAction(filterAction); + } + this.parentNode.updateRemoveButton(); + ]]> + </constructor> + </implementation> + </binding> + + <binding id="ruleactiontarget-tag" extends="chrome://messenger/content/searchWidgets.xml#ruleactiontarget-base"> + <content> + <xul:menulist class="ruleactionitem"> + <xul:menupopup> + </xul:menupopup> + </xul:menulist> + </content> + + <implementation> + <constructor> + <![CDATA[ + let menuPopup = document.getAnonymousNodes(this)[0].menupopup; + let tagArray = MailServices.tags.getAllTags({}); + for (let i = 0; i < tagArray.length; ++i) + { + var taginfo = tagArray[i]; + var newMenuItem = document.createElement('menuitem'); + newMenuItem.setAttribute('label', taginfo.tag); + newMenuItem.setAttribute('value', taginfo.key); + menuPopup.appendChild(newMenuItem); + } + // propagating a pre-existing hack to make the tag get displayed correctly in the menulist + // now that we've changed the tags for each menu list. We need to use the current selectedIndex + // (if its defined) to handle the case where we were initialized with a filter action already. + var currentItem = document.getAnonymousNodes(this)[0].selectedItem; + document.getAnonymousNodes(this)[0].selectedItem = null; + document.getAnonymousNodes(this)[0].selectedItem = currentItem; + ]]> + </constructor> + </implementation> + </binding> + + <binding id="ruleactiontarget-priority" extends="chrome://messenger/content/searchWidgets.xml#ruleactiontarget-base"> + <content> + <xul:menulist class="ruleactionitem"> + <xul:menupopup> + <xul:menuitem value="6" label="&highestPriorityCmd.label;"/> + <xul:menuitem value="5" label="&highPriorityCmd.label;"/> + <xul:menuitem value="4" label="&normalPriorityCmd.label;"/> + <xul:menuitem value="3" label="&lowPriorityCmd.label;"/> + <xul:menuitem value="2" label="&lowestPriorityCmd.label;"/> + </xul:menupopup> + </xul:menulist> + </content> + </binding> + + <binding id="ruleactiontarget-junkscore" extends="chrome://messenger/content/searchWidgets.xml#ruleactiontarget-base"> + <content> + <xul:menulist class="ruleactionitem"> + <xul:menupopup> + <xul:menuitem value="100" label="&junk.label;"/> + <xul:menuitem value="0" label="¬Junk.label;"/> + </xul:menupopup> + </xul:menulist> + </content> + </binding> + + <binding id="ruleactiontarget-replyto" extends="chrome://messenger/content/searchWidgets.xml#ruleactiontarget-base"> + <content> + <xul:menulist class="ruleactionitem"> + <xul:menupopup> + </xul:menupopup> + </xul:menulist> + </content> + + <implementation> + <constructor> + <![CDATA[ + document.getAnonymousElementByAttribute( + this.parentNode, "class", "ruleactiontype") + .getTemplates(true, document.getAnonymousNodes(this)[0]); + ]]> + </constructor> + </implementation> + </binding> + + <binding id="ruleactiontarget-forwardto" extends="chrome://messenger/content/searchWidgets.xml#ruleactiontarget-base"> + <content> + <xul:textbox class="ruleactionitem" flex="1"/> + </content> + </binding> + + <binding id="ruleactiontarget-folder" extends="chrome://messenger/content/searchWidgets.xml#ruleactiontarget-base"> + <content> + <xul:menulist class="ruleactionitem folderMenuItem" + displayformat="verbose" + oncommand="this.parentNode.setPicker(event);"> + <xul:menupopup type="folder" + mode="filing" + class="menulist-menupopup" + showRecent="true" + recentLabel="&recentFolders.label;" + showFileHereLabel="true"/> + </xul:menulist> + </content> + + <implementation> + <constructor> + <![CDATA[ + Components.utils.import("resource:///modules/MailUtils.js", this); + let folder = this.menulist.value ? + this.MailUtils.getFolderForURI(this.menulist.value) : + gFilterList.folder; + // An account folder is not a move/copy target; show "Choose Folder". + folder = folder.isServer ? null : folder; + let menupopup = this.menulist.menupopup; + // The menupopup constructor needs to finish first. + setTimeout(function() { menupopup.selectFolder(folder); }, 0); + ]]> + </constructor> + + <field name="menulist">document.getAnonymousNodes(this)[0]</field> + <method name="setPicker"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + this.menulist.menupopup.selectFolder(aEvent.target._folder); + ]]> + </body> + </method> + </implementation> + </binding> + +</bindings> diff --git a/mailnews/base/search/content/viewLog.js b/mailnews/base/search/content/viewLog.js new file mode 100644 index 000000000..61ae44df7 --- /dev/null +++ b/mailnews/base/search/content/viewLog.js @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var gFilterList; +var gLogFilters; +var gLogView; + +function onLoad() +{ + gFilterList = window.arguments[0].filterList; + + gLogFilters = document.getElementById("logFilters"); + gLogFilters.checked = gFilterList.loggingEnabled; + + gLogView = document.getElementById("logView"); + + // for security, disable JS + gLogView.docShell.allowJavascript = false; + + gLogView.setAttribute("src", gFilterList.logURL); +} + +function toggleLogFilters() +{ + gFilterList.loggingEnabled = gLogFilters.checked; +} + +function clearLog() +{ + gFilterList.clearLog(); + + // reload the newly truncated file + gLogView.reload(); +} + diff --git a/mailnews/base/search/content/viewLog.xul b/mailnews/base/search/content/viewLog.xul new file mode 100644 index 000000000..4ee766395 --- /dev/null +++ b/mailnews/base/search/content/viewLog.xul @@ -0,0 +1,49 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?> + +<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/viewLog.dtd"> + +<dialog id="viewLogWindow" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="onLoad();" + title="&viewLog.title;" + windowtype="mailnews:filterlog" + buttons="accept" + buttonlabelaccept="&closeLog.label;" + buttonaccesskeyaccept="&closeLog.accesskey;" + ondialogaccept="window.close();" + persist="screenX screenY width height" + style="width: 40em; height: 25em;"> + + <script type="application/javascript" src="chrome://messenger/content/viewLog.js"/> + + <vbox flex="1"> + <description>&viewLogInfo.text;</description> + <hbox> + <checkbox id="logFilters" + label="&enableLog.label;" + accesskey="&enableLog.accesskey;" + oncommand="toggleLogFilters();"/> + <spacer flex="1"/> + <button label="&clearLog.label;" + accesskey="&clearLog.accesskey;" + oncommand="clearLog();"/> + </hbox> + <separator class="thin"/> + <hbox flex="1"> + <browser id="logView" + class="inset" + type="content" + disablehistory="true" + disablesecurity="true" + src="about:blank" + autofind="false" + flex="1"/> + </hbox> + </vbox> +</dialog> |