Bug 450302: There is no "filter searching" (to find a filter). ui-r=bwinton, r=mkmelin
authorAxel Grude [:realRaven] <axelg@gofree.indigo.ie>
Sun, 05 Aug 2012 13:09:58 +0300
changeset 10811 6856915ce62ba31ae73713896dba686bdd74b00f
parent 10810 25e716ecbc45fc3b28b69275c5cdbdb2cc85653b
child 10812 423cb0440fca93faab9e466582f9d289da49324c
push id8123
push usermkmelin@iki.fi
push dateSun, 05 Aug 2012 10:12:24 +0000
treeherdercomm-central@6856915ce62b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbwinton, mkmelin
bugs450302
Bug 450302: There is no "filter searching" (to find a filter). ui-r=bwinton, r=mkmelin
aclocal.m4
mail/base/content/FilterListDialog.js
mail/base/content/FilterListDialog.xul
mail/locales/en-US/chrome/messenger/FilterListDialog.dtd
mail/locales/en-US/chrome/messenger/filter.properties
--- a/aclocal.m4
+++ b/aclocal.m4
@@ -1,12 +1,12 @@
 dnl
 dnl Local autoconf macros used with mozilla
 dnl The contents of this file are under the Public Domain.
-dnl 
+dnl
 
 builtin(include, mozilla/build/autoconf/nspr.m4)dnl
 builtin(include, mozilla/build/autoconf/nss.m4)dnl
 builtin(include, mozilla/build/autoconf/pkg.m4)dnl
 builtin(include, mozilla/build/autoconf/codeset.m4)dnl
 builtin(include, mozilla/build/autoconf/altoptions.m4)dnl
 builtin(include, mozilla/build/autoconf/mozprog.m4)dnl
 builtin(include, mozilla/build/autoconf/acwinpaths.m4)dnl
--- a/mail/base/content/FilterListDialog.js
+++ b/mail/base/content/FilterListDialog.js
@@ -1,19 +1,28 @@
 /* -*- 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");
+Components.utils.import("resource://gre/modules/PluralForm.jsm");
 
 var gFilterListMsgWindow = null;
 var gCurrentFilterList;
 var gCurrentFolder;
 
+const msgMoveMotion = {
+  Up     : 0,
+  Down   : 1,
+  Top    : 2,
+  Bottom : 3
+}
+
+
 var gStatusFeedback = {
   progressMeterVisible : false,
 
   showStatusString: function(status)
   {
     document.getElementById("statusText").setAttribute("value", status);
   },
   startMeteors: function()
@@ -75,48 +84,51 @@ function onLoad()
     gFilterListMsgWindow.domWindow = window;
     gFilterListMsgWindow.rootDocShell.appType = Components.interfaces.nsIDocShell.APP_TYPE_MAIL;
     gFilterListMsgWindow.statusFeedback = gStatusFeedback;
 
     updateButtons();
 
     // Get the folder where filters should be defined, if that server
     // can accept filters.
-    var firstItem = getFilterFolderForSelection();
+    let firstItem = getFilterFolderForSelection();
 
-    // if the selected server cannot have filters, get the default server
-    // if the default server cannot have filters, check all accounts
+    // If the selected server cannot have filters, get the default server
+    // If the default server cannot have filters, check all accounts
     // and get a server that can have filters.
     if (!firstItem)
         firstItem = getServerThatCanHaveFilters().rootFolder;
 
     if (firstItem) {
         selectFolder(firstItem);
     }
 
     Services.obs.addObserver(filterEditorQuitObserver,
                              "quit-application-requested", false);
 }
 
 /**
- * Called when a user selects a folder in the list, so we can update the filters
- * that are displayed
+ * Called when a user selects a folder in the list, so we can update the 
+ * filters that are displayed
+ * note the function name 'onFilterFolderClick' is misleading, it would be
+ * better named 'onServerSelect' => file follow up bug later.
  *
  * @param aFolder  the nsIMsgFolder that was selected
  */
 function onFilterFolderClick(aFolder)
 {
     if (!aFolder || aFolder == gCurrentFolder)
       return;
 
     // Save the current filters to disk before switching because
     // the dialog may be closed and we'll lose current filters.
     gCurrentFilterList.saveToDefaultFile();
 
     selectFolder(aFolder);
+    onFindFilter(false);
 }
 
 function CanRunFiltersAfterTheFact(aServer)
 {
   // filter after the fact is implement using search
   // so if you can't search, you can't filter after the fact
   return aServer.canSearchMessages;
 }
@@ -165,16 +177,17 @@ function setFolder(msgFolder)
      document.getElementById("runFiltersFolder").setAttribute("hidden", "true");
      document.getElementById("runFiltersButton").setAttribute("hidden", "true");
      document.getElementById("folderPickerPrefix").setAttribute("hidden", "true");
    }
 
    // Get the first folder for this server. INBOX for
    // imap and pop accts and 1st news group for news.
    updateButtons();
+   updateCountBox();
 }
 
 function toggleFilter(aFilter, aIndex)
 {
     if (aFilter.unparseable)
     {
       var bundle = document.getElementById("bundle_filter");
       var promptSvc = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
@@ -207,53 +220,71 @@ function currentFilter()
 
 function onEditFilter()
 {
   var selectedFilter = currentFilter();
   var args = {filter: selectedFilter, filterList: gCurrentFilterList};
 
   window.openDialog("chrome://messenger/content/FilterEditor.xul", "FilterEditor", "chrome,modal,titlebar,resizable,centerscreen", args);
 
-  if ("refresh" in args && args.refresh)
+  if ("refresh" in args && args.refresh) {
+    // reset search if edit was okay (name change might lead to hidden entry!)
+    document.getElementById("searchBox").value = "";
     rebuildFilterList(gCurrentFilterList);
+  }
 }
 
 function onNewFilter(emailAddress)
 {
   let list = document.getElementById("filterList");
   let filterNodes = list.childNodes;
   let selectedFilter = currentFilter();
   // if no filter is selected use the first position, starting at 1
   let position = 1;
   if (selectedFilter) {
+    // Get the position in the unfiltered list.
+    // - this is where the new filter should be inserted!
+    rebuildFilterList(gCurrentFilterList);
+
     // The filterNodes[0] item is the list header, skip it.
     for (let i = 1; i < filterNodes.length; i++) {
       if (filterNodes[i]._filter == selectedFilter) {
         position = i;
         break;
       }
     }
   }
-
   // The returned position is offset by 1 (due to the list header)
   // compared to filter indexes in gCurrentFilterList.
   let args = {filterList: gCurrentFilterList, filterPosition: position - 1};
 
   window.openDialog("chrome://messenger/content/FilterEditor.xul", "FilterEditor", "chrome,modal,titlebar,resizable,centerscreen", args);
 
   if ("refresh" in args && args.refresh) {
+    // On success: reset the search box!
+    document.getElementById("searchBox").value = "";
     rebuildFilterList(gCurrentFilterList);
 
-    // select the new filter, it is at the position of previous selection
+    // Select the new filter, it is at the position of previous selection.
     list.clearSelection();
     list.addItemToSelection(list.childNodes[position]);
     updateViewPosition(position);
+    updateCountBox();
   }
+  else {
+    // If no filter created, let's search again.
+    onFindFilter(false);
+  }
+
 }
 
+/**
+ * Delete selected filters.
+ *  'Selected' is not to be confused with active (checkbox checked)
+ */
 function onDeleteFilter()
 {
   let list = document.getElementById("filterList");
   let items = list.selectedItems;
   if (!items.length)
     return;
 
   let checkValue = {value:false};
@@ -270,45 +301,156 @@ function onDeleteFilter()
                            '', '', '',
                            bundle.getString('dontWarnAboutDeleteCheckbox'),
                            checkValue)))
     return;
 
   if (checkValue.value)
      prefBranch.setBoolPref("mailnews.filters.confirm_delete", false);
 
-  // save filter position before the first selected one
+  // Save filter position before the first selected one.
   let newSelection = items[0].previousElementSibling;
   if (newSelection == list.childNodes[0])
     newSelection = null;
 
-  // must reverse the loop, as the items list shrinks when we delete
+  // Must reverse the loop, as the items list shrinks when we delete.
   for (let index = items.length - 1; index >= 0; --index) {
     let item = items[index];
     gCurrentFilterList.removeFilter(item._filter);
     document.getElementById("filterList").removeChild(item);
   }
 
-  // select filter above previously selected if one existed, otherwise the first one
+  // Select filter above previously selected if one existed, otherwise the first one.
   if (!newSelection && list.itemCount)
     newSelection = list.childNodes[1];
   if (newSelection) {
     list.addItemToSelection(newSelection);
     updateViewPosition(-1);
   }
+  updateCountBox();
+}
+
+/**
+ * Move filter one step up in visible list.
+ */
+function onUp(event) {
+  moveFilter(msgMoveMotion.Up);
+}
+
+/**
+ * Move filter one step down in visible list.
+ */
+function onDown(event) {
+  moveFilter(msgMoveMotion.Down);
+}
+
+/**
+ * Move filter to bottom for long filter lists.
+ */
+ function onTop(evt) {
+  moveFilter(msgMoveMotion.Top);
+}
+
+/**
+ * Move filter to top for long filter lists.
+ */
+function onBottom(evt) {
+  moveFilter(msgMoveMotion.Bottom);
 }
 
-function onUp(event)
-{
-    moveCurrentFilter(Components.interfaces.nsMsgFilterMotion.up);
-}
+/**
+ * Moves a singular selected filter up or down either 1 increment or to the 
+ * top/bottom. This acts on the visible filter list only which means that:
+ *
+ * - when moving up or down "1" the filter may skip one or more other
+ *   filters (which are currently not visible) - this will also lead
+ *   to the "related" filters (e.g search filters containing 'moz')
+ *   being grouped more closely together
+ * - moveTop / moveBottom
+ *   this is currently moving to the top/bottom of the absolute list
+ *   but it would be better if it moved "just as far as necessary"
+ *   which would further "compact" related filters
+ *
+ * @param motion
+ *   msgMoveMotion.Up, msgMoveMotion.Down, msgMoveMotion.Top, msgMoveMotion.Bottom
+ */
+function moveFilter(motion) {
+  // At the moment, do not allow moving groups of filters.
+  var list = document.getElementById("filterList");
+  if (list.selectedItems.length != 1)
+    return;
+  var activeFilter = list.selectedItems[0]._filter;
+  var relativeStep = 0;
+  var moveFilterNative = null;
 
-function onDown(event)
-{
-    moveCurrentFilter(Components.interfaces.nsMsgFilterMotion.down);
+  switch(motion) {
+    case msgMoveMotion.Top:
+      if (activeFilter) {
+        gCurrentFilterList.removeFilter(activeFilter);
+        gCurrentFilterList.insertFilterAt(0, activeFilter);
+        rebuildFilterList(gCurrentFilterList);
+        onFindFilter(false); // re-filter list
+        document.getElementById("reorderTopButton").disabled = true;
+      }
+      return;
+    case msgMoveMotion.Bottom:
+      if (activeFilter) {
+        gCurrentFilterList.removeFilter(activeFilter);
+        gCurrentFilterList.insertFilterAt(gCurrentFilterList.filterCount, activeFilter);
+        rebuildFilterList(gCurrentFilterList);
+        onFindFilter(false);
+        document.getElementById("reorderBottomButton").disabled = true;
+      }
+      return;
+    case msgMoveMotion.Up:
+      relativeStep = -1;
+      moveFilterNative = Components.interfaces.nsMsgFilterMotion.up;
+      break;
+    case msgMoveMotion.Down:
+      relativeStep = +1;
+      moveFilterNative = Components.interfaces.nsMsgFilterMotion.down;
+      break;
+  }
+
+  var searchBox = document.getElementById("searchBox");
+  if (searchBox.value) {
+    if (activeFilter) {
+      let nextIndex = list.selectedIndex + relativeStep;
+      let nextFilter = list.getItemAtIndex(nextIndex)._filter;
+      rebuildFilterList(gCurrentFilterList);
+
+      // assumption: item stays selected even after removing the search condition
+      let newIndex = list.selectedIndex + relativeStep;
+      gCurrentFilterList.removeFilter(activeFilter);
+
+      // insert after/before next visible item
+      switch(motion) {
+        case msgMoveMotion.Up:
+          // go up from selected index until finding the correct filter name
+          while (nextFilter.filterName != list.getItemAtIndex(newIndex)._filter.filterName && nextIndex < list.itemCount)
+            newIndex--;
+          break;
+        case msgMoveMotion.Down:
+          // go down from selected index until finding the correct filter name
+          while (nextFilter.filterName != list.getItemAtIndex(newIndex)._filter.filterName && nextIndex < list.itemCount)
+            newIndex++;
+          break;
+        case msgMoveMotion.Top: break; // obsolete, dealt with above
+        case msgMoveMotion.Bottom: break; // obsolete, dealt with above
+      }
+      gCurrentFilterList.insertFilterAt(newIndex, activeFilter);
+      rebuildFilterList(gCurrentFilterList);
+      list.selectedIndex = newIndex;
+    }
+    onFindFilter(false);
+  }
+  else {
+    // use legacy move filter code: up, down; only if searchBox is empty
+    moveCurrentFilter(moveFilterNative);
+  }
 }
 
 function viewLog()
 {
   var args = {filterList: gCurrentFilterList};
 
   window.openDialog("chrome://messenger/content/viewLog.xul", "FilterLog", "chrome,modal,titlebar,resizable,centerscreen", args);
 }
@@ -392,16 +534,17 @@ function rebuildFilterList(aFilterList)
   var list = document.getElementById("filterList");
 
   // Make a note of which filters were previously selected
   var selectedNames = [];
   for (var i = 0; i < list.selectedItems.length; i++)
     selectedNames.push(list.selectedItems[i]._filter.filterName);
 
   // Save scroll position so we can try to restore it later.
+  // Doesn't work when the list is rebuilt after search box condition changed.
   let firstVisibleRowIndex = list.getIndexOfFirstVisibleRow();
 
   // Remove any existing child nodes, but not our headers
   for (var i = list.childNodes.length - 1; i > 0; i--) {
     list.removeChild(list.childNodes[i]);
   }
 
   // listbox.xml seems to cache the value of the first selected item in a
@@ -430,23 +573,25 @@ function rebuildFilterList(aFilterList)
     listitem.addEventListener("dblclick", onFilterDoubleClick, true);
     listitem._filter = filter;
     list.appendChild(listitem);
 
     if (selectedNames.indexOf(filter.filterName) != -1)
       list.addItemToSelection(listitem);
   }
   updateViewPosition(firstVisibleRowIndex);
+  updateCountBox();
 }
 
 function updateViewPosition(firstVisibleRowIndex)
 {
   let list = document.getElementById("filterList");
   if (firstVisibleRowIndex == -1)
     firstVisibleRowIndex = list.getIndexOfFirstVisibleRow();
+
   // Restore to the extent possible the scroll position.
   if (firstVisibleRowIndex && list.itemCount)
     list.scrollToIndex(Math.min(firstVisibleRowIndex, list.itemCount - 1));
 
   if (list.selectedCount) {
     // Make sure that at least the first selected item is visible.
     list.ensureElementIsVisible(list.selectedItems[0]);
 
@@ -454,54 +599,68 @@ function updateViewPosition(firstVisible
     // selection extension can work.
     list.currentItem = list.selectedItems[0];
   }
 
   updateButtons();
   list.focus();
 }
 
+/**
+ * Try to only enable buttons that make sense
+ *  - moving filters is currently only enabled for single selection
+ *    also movement is restricted by searchBox and current selection position
+ *  - edit only for single filters
+ *  - delete / run only for one or more selected filters
+ */
 function updateButtons()
 {
     var list = document.getElementById("filterList");
     var numFiltersSelected = list.selectedItems.length;
     var oneFilterSelected = (numFiltersSelected == 1);
 
     var filter = currentFilter();
     // "edit" only enabled when one filter selected or if we couldn't parse the filter
     var disabled = !oneFilterSelected || filter.unparseable
     document.getElementById("editButton").disabled = disabled;
-    
+
     // "delete" only disabled when no filters are selected
     document.getElementById("deleteButton").disabled = !numFiltersSelected;
 
     // we can run multiple filters on a folder
     // so only disable this UI if no filters are selected
     document.getElementById("folderPickerPrefix").disabled = !numFiltersSelected;
     document.getElementById("runFiltersFolder").disabled = !numFiltersSelected;
     document.getElementById("runFiltersButton").disabled = !numFiltersSelected;
 
     // "up" enabled only if one filter selected, and it's not the first
     // don't use list.currentIndex here, it's buggy when we've just changed the
     // children in the list (via rebuildFilterList)
-    var upDisabled = !(oneFilterSelected && 
+    var upDisabled = !(oneFilterSelected &&
                        list.selectedItems[0] != list.childNodes[1]);
     document.getElementById("reorderUpButton").disabled = upDisabled
     // "down" enabled only if one filter selected, and it's not the last
     var downDisabled = !(oneFilterSelected && list.currentIndex < list.getRowCount()-1);
     document.getElementById("reorderDownButton").disabled = downDisabled;
+
+    // special buttons
+    var buttonTop = document.getElementById("reorderTopButton");
+    var buttonBottom = document.getElementById("reorderBottomButton");
+
+    buttonTop.disabled = upDisabled;
+    buttonBottom.disabled = downDisabled;
 }
 
 /**
-  * Given a selected folder, returns the folder where filters should
-  *  be defined (the root folder except for news) if the server can
-  *  accept filters.
-  *
-  * @returns an nsIMsgFolder where the filter is defined
-  */
+ * Given a selected folder, returns the folder where filters should
+ *  be defined (the root folder except for news) if the server can
+ *  accept filters.
+ *
+ * @returns an nsIMsgFolder where the filter is defined
+ */
 function getFilterFolderForSelection()
 {
     var args = window.arguments;
 
     if (args && args[0] && args[0].folder)
     {
         var selectedFolder = args[0].folder;
         var msgFolder = selectedFolder.QueryInterface(Components.interfaces.nsIMsgFolder);
@@ -518,22 +677,23 @@ function getFilterFolderForSelection()
         catch (ex)
         {
         }
     }
 
     return null;
 }
 
-/** if the selected server cannot have filters, get the default server
-  * if the default server cannot have filters, check all accounts
-  * and get a server that can have filters.
-  *
-  * @returns an nsIMsgIncomingServer
-  */
+/**
+ * If the selected server cannot have filters, get the default server.
+ * If the default server cannot have filters, check all accounts
+ * and get a server that can have filters.
+ *
+ * @returns an nsIMsgIncomingServer
+ */
 function getServerThatCanHaveFilters()
 {
     var firstItem = null;
 
     var accountManager
         = Components.classes["@mozilla.org/messenger/account-manager;1"].
             getService(Components.interfaces.nsIMsgAccountManager);
 
@@ -617,19 +777,19 @@ function onFilterListKeyPress(event)
 
 function onTargetSelect(event) {
   var menu = document.getElementById("runFiltersFolder");
   menu._folder = event.target._folder;
   menu.setAttribute("label", event.target._folder.prettyName);
 }
 
 /**
-  * For a given server folder, get the first folder. For imap and pop it's INBOX
-  * and it's the very first group for news accounts.
-  */
+ * For a given server folder, get the first folder. For imap and pop it's INBOX
+ * and it's the very first group for news accounts.
+ */
 function getFirstFolder(msgFolder)
 {
   // Sanity check.
   if (! msgFolder.isServer)
     return msgFolder;
 
   try {
     // Find Inbox for imap and pop
@@ -647,8 +807,73 @@ function getFirstFolder(msgFolder)
       // XXX TODO: For news, we should find the 1st group/folder off the news groups. For now use server.
       return msgFolder;
   }
   catch (ex) {
     dump(ex + "\n");
   }
   return msgFolder;
 }
+
+
+
+/**
+ * Called when the search button is clicked, this will narrow down the amount
+ * of filters displayed in the list, using the search term to filter the names
+ *
+ * @param focusSearchBox  if called from the button click event, return to searchbox
+ */
+function onFindFilter(focusSearchBox)
+{
+  let searchBox = document.getElementById("searchBox");
+  let filterList = document.getElementById("filterList");
+  let keyWord = searchBox.value.toLocaleLowerCase();
+
+  // simplest case: if filter was added or removed and searchbox is empty
+  if (!keyWord && !focusSearchBox) {
+    updateCountBox();
+    return;
+  }
+  rebuildFilterList(gCurrentFilterList); // creates the unfiltered list
+  if (!keyWord) {
+    if (focusSearchBox)
+      searchBox.focus();
+    updateCountBox();
+    return;
+  }
+
+  // rematch everything in the list, remove what doesn't match the search box
+  let rows = filterList.getRowCount();
+
+  for(let i = rows - 1; i >= 0; i--) {
+    let matched = true;
+    let item = filterList.getItemAtIndex(i);
+    let title = item.firstChild.getAttribute("label");
+    if (title.toLocaleLowerCase().indexOf(keyWord) == -1)
+    {
+      matched = false;
+      filterList.removeChild(item);
+    }
+  }
+  updateCountBox();
+  if (focusSearchBox)
+    searchBox.focus();
+}
+
+/**
+ * Display "1 item",  "11 items" or "4 of 10" if list is filtered via search box.
+ */
+function updateCountBox()
+{
+  let countBox = document.getElementById("countBox");
+  let sum = gCurrentFilterList.filterCount;
+  let filterList = document.getElementById("filterList");
+  let len = filterList.getRowCount();
+
+  let bundle = document.getElementById("bundle_filter");
+
+  if (len == sum) // "N items"
+    countBox.value = PluralForm.get(len, bundle.getString("filterCountItems"))
+                               .replace("#1",[len]);
+  else // "N of M"
+    countBox.value = bundle.getFormattedString("filterCountVisibleOfTotal", [len, sum]);
+}
+
--- a/mail/base/content/FilterListDialog.xul
+++ b/mail/base/content/FilterListDialog.xul
@@ -31,109 +31,141 @@
   <keyset id="dialogKeys"/>
 
   <keyset>
     <key key="&closeCmd.key;" modifiers="accel" oncommand="if (onFilterClose()) window.close();"/>
     <key keycode="VK_ESCAPE" oncommand="if (onFilterClose()) window.close();"/>
   </keyset>
 
   <hbox align="center">
-    <label value="&filtersForPrefix.label;" 
+    <label value="&filtersForPrefix.label;"
            accesskey="&filtersForPrefix.accesskey;" control="serverMenu"/>
 
-      <menulist id="serverMenu"
-                class="folderMenuItem"
-                IsServer="true"
-                IsSecure="false"
-                ServerType="none"
-                oncommand="onFilterFolderClick(event.target._folder);">
-        <menupopup id="serverMenuPopup" type="folder" mode="filters"
-          expandFolders="nntp" headlabels="&choosethisnewsserver.label;" />
-      </menulist>
-      <spacer flex="1"/> 
-      <vbox>
-        <button label="&viewLogButton.label;" accesskey="&viewLogButton.accesskey;" oncommand="viewLog();"/>
-      </vbox>
+    <menulist id="serverMenu"
+              class="folderMenuItem"
+              IsServer="true"
+              IsSecure="false"
+              ServerType="none"
+              oncommand="onFilterFolderClick(event.target._folder);">
+      <menupopup id="serverMenuPopup" type="folder" mode="filters"
+                 expandFolders="nntp" headlabels="&choosethisnewsserver.label;"/>
+    </menulist>
+    <textbox id="searchBox"
+             flex="1"
+             type="search"
+             oncommand="onFindFilter(true);"
+             emptytext="&searchBox.emptyText;"
+             isempty="true"/>
   </hbox>
 
   <grid flex="1">
     <columns>
       <column flex="1"/>
       <column/>
     </columns>
     <rows>
       <row>
         <separator class="thin"/>
       </row>
       <row>
-        <label control="filterTree">&filterHeader.label;</label>
+        <hbox>
+          <label id="filterListLabel"
+                 control="filterList"
+                 value="&filterHeader.label;"
+                 crop="end"
+                 flex="1"/>
+          <label id="countBox"/>
+        </hbox>
       </row>
 
       <row flex="1">
         <vbox>
           <listbox id="filterList" flex="1" onselect="updateButtons();"
                    seltype="multiple"
                    onkeypress="onFilterListKeyPress(event);">
             <listhead>
               <listheader id="nameColumn" label="&nameColumn.label;" flex="1"/>
               <listheader id="activeColumn" label="&activeColumn.label;" minwidth="40px"/>
             </listhead>
           </listbox>
         </vbox>
 
         <vbox>
-          <button id="newButton" label="&newButton.label;" accesskey="&newButton.accesskey;"
+          <button id="newButton"
+                  label="&newButton.label;"
+                  accesskey="&newButton.accesskey;"
                   oncommand="onNewFilter(null);"/>
-          <button id="editButton" label="&editButton.label;" accesskey="&editButton.accesskey;" 
+          <button id="editButton" label="&editButton.label;"
+                  accesskey="&editButton.accesskey;"
                   oncommand="onEditFilter();"/>
-          <button id="deleteButton" label="&deleteButton.label;" accesskey="&deleteButton.accesskey;" 
+          <button id="deleteButton"
+                  label="&deleteButton.label;"
+                  accesskey="&deleteButton.accesskey;"
                   oncommand="onDeleteFilter();"/>
           <spacer flex="1"/>
-          <button id="reorderUpButton" label="&reorderUpButton.label;" accesskey="&reorderUpButton.accesskey;" 
+          <button id="reorderTopButton"
+                  label="&reorderTopButton;"
+                  accesskey="&reorderTopButton.accessKey;"
+                  tooltiptext="&reorderTopButton.toolTip;"
+                  oncommand="onTop(event);"/>
+          <button id="reorderUpButton"
+                  label="&reorderUpButton.label;"
+                  accesskey="&reorderUpButton.accesskey;"
                   class="up"
                   oncommand="onUp(event);"/>
-          <button id="reorderDownButton" label="&reorderDownButton.label;" accesskey="&reorderDownButton.accesskey;" 
+          <button id="reorderDownButton"
+                  label="&reorderDownButton.label;"
+                  accesskey="&reorderDownButton.accesskey;"
                   class="down"
                   oncommand="onDown(event);"/>
+          <button id="reorderBottomButton"
+                  label="&reorderBottomButton;"
+                  accesskey="&reorderBottomButton.accessKey;"
+                  tooltiptext="&reorderBottomButton.toolTip;"
+                  oncommand="onBottom(event);"/>
           <spacer flex="1"/>
         </vbox>
       </row>
       <row>
         <vbox>
           <separator class="thin"/>
           <hbox align="center">
-            <label id="folderPickerPrefix" value="&folderPickerPrefix.label;" 
+            <label id="folderPickerPrefix" value="&folderPickerPrefix.label;"
                    accesskey="&folderPickerPrefix.accesskey;"
                    disabled="true" control="runFiltersFolder"/>
             <menulist id="runFiltersFolder" disabled="true" flex="1"
                       oncommand="onTargetSelect(event);">
               <menupopup id="runFiltersPopup" type="folder"
                          showFileHereLabel="true"
                          fileHereLabel="&filemessageschoosethis.label;"/>
             </menulist>
+          <button id="runFiltersButton"
+                  label="&runFilters.label;"
+                  accesskey="&runFilters.accesskey;"
+                  runlabel="&runFilters.label;"
+                  runaccesskey="&runFilters.accesskey;"
+                  stoplabel="&stopFilters.label;"
+                  stopaccesskey="&stopFilters.accesskey;"
+                  oncommand="runSelectedFilters();" disabled="true"/>
           </hbox>
         </vbox>
         <vbox>
           <separator class="thin"/>
-          <button id="runFiltersButton" 
-                  label="&runFilters.label;" 
-                  accesskey="&runFilters.accesskey;" 
-                  runlabel="&runFilters.label;" 
-                  runaccesskey="&runFilters.accesskey;" 
-                  stoplabel="&stopFilters.label;" 
-                  stopaccesskey="&stopFilters.accesskey;"
-                  oncommand="runSelectedFilters();" disabled="true"/>
+            <button label="&viewLogButton.label;"
+                    accesskey="&viewLogButton.accesskey;"
+                    oncommand="viewLog();"/>
+
         </vbox>
       </row>
     </rows>
   </grid>
 
   <separator class="thin"/>
 
   <statusbar class="chromeclass-status" id="status-bar">
     <statusbarpanel id="statusText" flex="1" crop="right"/>
     <statusbarpanel class="statusbarpanel-progress" collapsed="true" id="statusbar-progresspanel">
       <progressmeter class="progressmeter-statusbar" id="statusbar-icon" mode="normal" value="0"/>
     </statusbarpanel>
-  </statusbar> 
+  </statusbar>
 
 </window>
 
--- a/mail/locales/en-US/chrome/messenger/FilterListDialog.dtd
+++ b/mail/locales/en-US/chrome/messenger/FilterListDialog.dtd
@@ -6,28 +6,38 @@
 <!ENTITY nameColumn.label "Filter Name">
 <!ENTITY activeColumn.label "Enabled">
 <!ENTITY newButton.label "New…">
 <!ENTITY newButton.accesskey "N">
 <!ENTITY editButton.label "Edit…">
 <!ENTITY editButton.accesskey "E">
 <!ENTITY deleteButton.label "Delete">
 <!ENTITY deleteButton.accesskey "t">
+<!ENTITY reorderTopButton "Move to Top">
+<!ENTITY reorderTopButton.accessKey "o">
+<!ENTITY reorderTopButton.toolTip "Rearrange filter so it executes before all others">
 <!ENTITY reorderUpButton.label "Move Up">
 <!ENTITY reorderUpButton.accesskey "U">
 <!ENTITY reorderDownButton.label "Move Down">
 <!ENTITY reorderDownButton.accesskey "D">
+<!ENTITY reorderBottomButton "Move to Bottom">
+<!ENTITY reorderBottomButton.accessKey "B">
+<!ENTITY reorderBottomButton.toolTip "Rearrange filter so it executes after all others">
 <!ENTITY filterHeader.label "Enabled filters are run automatically in the order shown below.">
 <!ENTITY filtersForPrefix.label "Filters for:">
 <!ENTITY filtersForPrefix.accesskey "F">
 <!ENTITY viewLogButton.label "Filter Log">
 <!ENTITY viewLogButton.accesskey "L">
 <!ENTITY runFilters.label "Run Now">
 <!ENTITY runFilters.accesskey "R">
 <!ENTITY stopFilters.label "Stop">
 <!ENTITY stopFilters.accesskey "S">
 <!ENTITY folderPickerPrefix.label "Run selected filter(s) on:">
 <!ENTITY folderPickerPrefix.accesskey "c">
 <!ENTITY choosethis.label "choose this folder">
 <!ENTITY choosethisnewsserver.label "choose this news server">
 <!ENTITY helpButton.label "Help">
 <!ENTITY helpButton.accesskey "H">
 <!ENTITY closeCmd.key "W"> 
+<!ENTITY searchBox.emptyText "Search filters by name…">
+
+
+
--- a/mail/locales/en-US/chrome/messenger/filter.properties
+++ b/mail/locales/en-US/chrome/messenger/filter.properties
@@ -19,17 +19,21 @@ continueFilterExecution=Applying filter 
 promptTitle=Running Filters
 promptMsg=You are currently in the process of filtering messages.\nWould you like to continue applying filters?
 stopButtonLabel=Stop
 continueButtonLabel=Continue
 cannotEnableFilter=This filter was probably created by future version of mozilla/netscape. You cannot enable this filter because we don't know how to apply it.
 dontWarnAboutDeleteCheckbox=Don't ask me again
 searchTermsInvalidTitle=Search Terms Invalid
 searchTermsInvalidMessage=This filter cannot be saved because some search terms are invalid in the current context.
-
+filterCountVisibleOfTotal=%1$S of %2$S
+## LOCALIZATION NOTE(filterCountItems): Semi-colon list of plural forms.
+## See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+## #1 is the count of items in the list.# filterCountItems is a pluralForm - see 
+filterCountItems=#1 item; #1 items
 # for junk mail logging / mail filter logging
 # LOCALIZATION NOTE(junkLogDetectStr)
 # %1$S=author, %2$S=subject, %3$S=date
 junkLogDetectStr=Detected junk message from %1$S - %2$S at %3$S
 # LOCALIZATION NOTE(logMoveStr)
 # %1$S=message id, %2$S=folder URI
 logMoveStr=moved message id = %1$S to %2$S
 # LOCALIZATION NOTE(logCopyStr)