Bug 1637668 - Implement colour customization option for Folder Pane icons. r=mkmelin, ui-r=paenglab
authorAlessandro Castellani <alessandro@thunderbird.net>
Fri, 22 May 2020 12:53:51 +0300
changeset 29640 f8310228f5b2c75ff65bbb4e182eb6ce693c6e28
parent 29639 a15cee0df19fc84a95e32cb0e1ae1f438001c917
child 29641 6a077945683b32ef107dca9ceb7ed9959a5bc5c7
push id17468
push usermkmelin@iki.fi
push dateFri, 22 May 2020 09:55:36 +0000
treeherdercomm-central@6a077945683b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmkmelin, paenglab
bugs1637668
Bug 1637668 - Implement colour customization option for Folder Pane icons. r=mkmelin, ui-r=paenglab
mail/base/content/folderPane.js
mail/base/content/messenger.xhtml
mail/locales/en-US/chrome/messenger/folderProps.dtd
mail/themes/shared/jar.inc.mn
mail/themes/shared/mail/icons/folder.svg
mail/themes/shared/mail/icons/forget.svg
mail/themes/shared/mail/input-fields.css
mailnews/base/content/folderProps.js
mailnews/base/content/folderProps.xhtml
mailnews/base/content/msgSelectOfflineFolders.xhtml
mailnews/base/content/virtualFolderListEdit.xhtml
mailnews/base/content/virtualFolderProperties.js
mailnews/base/content/virtualFolderProperties.xhtml
--- a/mail/base/content/folderPane.js
+++ b/mail/base/content/folderPane.js
@@ -145,16 +145,18 @@ var gFolderTreeView = {
 
   /**
    * Called when the window is initially loaded. This function initializes the
    * folder-pane to the view last shown before the application was closed.
    */
   load(aTree, aJSONFile) {
     this._treeElement = aTree;
     this.messengerBundle = document.getElementById("bundle_messenger");
+    this.inlineStyle = document.getElementById("inlineStyle");
+    this.previewStyle = document.getElementById("previewStyle");
 
     // The folder pane can be used for other trees which may not have these
     // elements. Collapse them if no account is currently available.
     let hasAccounts = MailServices.accounts.accounts.length > 0;
     if (document.getElementById("folderpane_splitter")) {
       document.getElementById("folderpane_splitter").collapsed = !hasAccounts;
     }
     if (document.getElementById("folderPaneBox")) {
@@ -1679,16 +1681,42 @@ var gFolderTreeView = {
     for (let folder of selectedFolders) {
       if (folder) {
         let index = this.getIndexOfFolder(folder);
         if (index != null) {
           this.selection.toggleSelect(index);
         }
       }
     }
+
+    // Restore custom icon colors.
+    for (let i = 0; i < this._rowMap.length; i++) {
+      let folder = this._rowMap[i]._folder;
+      let msgDatabase;
+      try {
+        // This will throw an exception if the .msf file is missing,
+        // out of date (e.g., the local folder has changed), or corrupted.
+        msgDatabase = folder.msgDatabase;
+      } catch (e) {}
+
+      if (msgDatabase) {
+        // Get the previously stored color from the Folder Database.
+        let iconColor = folder.msgDatabase.dBFolderInfo.getCharProperty(
+          "folderIconColor"
+        );
+        // Store the color in the cache property so we can use this for
+        // properties changes and updates.
+        gFolderTreeView.setFolderCacheProperty(
+          this._rowMap[i]._folder,
+          "folderIconColor",
+          iconColor
+        );
+        this.appendColor(iconColor);
+      }
+    }
   },
 
   _sortedAccounts() {
     let accounts = allAccountsSorted(true);
 
     // Don't show deferred pop accounts.
     accounts = accounts.filter(function(a) {
       let server = a.incomingServer;
@@ -2586,16 +2614,32 @@ var gFolderTreeView = {
 
   OnItemPropertyFlagChanged(aItem, aProperty, aOld, aNew) {},
   OnItemEvent(aFolder, aEvent) {
     let index = this.getIndexOfFolder(aFolder);
     if (index != null) {
       this._tree.invalidateRow(index);
     }
   },
+
+  /**
+   * Append inline CSS style for those icons where a custom color was defined.
+   *
+   * @param {string} iconColor - The hash color.
+   */
+  appendColor(iconColor) {
+    if (!this.inlineStyle || !iconColor) {
+      return;
+    }
+
+    let selector = `customColor-${iconColor.replace("#", "")}`;
+
+    // Append the inline CSS styling.
+    this.inlineStyle.textContent += `treechildren::-moz-tree-image(folderNameCol, ${selector}) {fill: ${iconColor};}`;
+  },
 };
 
 /**
  * The ftvItem object represents a single row in the tree view. Because I'm lazy
  * I'm just going to define the expected interface here.  You are free to return
  * an alternative object, provided that it matches this interface:
  *
  * id (attribute) - a unique string for this object. Must persist over sessions
@@ -2786,16 +2830,25 @@ ftvItem.prototype = {
       properties += " specialFolder-" + this._folder.name.replace(/\s+/g, "");
     }
     // if there is a smartFolder name property, add it
     let smartFolderName = getSmartFolderName(this._folder);
     if (smartFolderName) {
       properties += " specialFolder-" + smartFolderName.replace(/\s+/g, "");
     }
 
+    let customColor = gFolderTreeView.getFolderCacheProperty(
+      this._folder,
+      "folderIconColor"
+    );
+    // Add the property if a custom color was defined for this folder.
+    if (customColor) {
+      properties += ` customColor-${customColor.replace("#", "")}`;
+    }
+
     if (FeedMessageHandler.isFeedFolder(this._folder)) {
       properties += FeedUtils.getFolderProperties(this._folder, null);
       gFolderTreeView.setFolderCacheProperty(
         this._folder,
         "properties",
         properties
       );
     }
@@ -2923,17 +2976,17 @@ var gFolderTreeController = {
       if (aNewName != aOldName) {
         folder.rename(aNewName, msgWindow);
       }
     }
 
     // xxx useless param
     function rebuildSummary(aFolder) {
       // folder is already introduced in our containing function and is
-      //  lexically captured and available to us.
+      // lexically captured and available to us.
       if (folder.locked) {
         folder.throwAlertMsg("operationFailedFolderBusy", msgWindow);
         return;
       }
       if (folder.supportsOffline) {
         // Remove the offline store, if any.
         let offlineStore = folder.filePath;
         // XXX todo: figure out how to delete a maildir directory async. This
@@ -2974,23 +3027,29 @@ var gFolderTreeController = {
     }
 
     window.openDialog(
       "chrome://messenger/content/folderProps.xhtml",
       "",
       "chrome,modal,centerscreen",
       {
         folder,
+        treeView: gFolderTreeView,
         serverType: folder.server.type,
         msgWindow,
         title,
         okCallback: editFolderCallback,
         tabID: aTabID,
         name: folder.prettyName,
         rebuildSummaryCallback: rebuildSummary,
+        previewSelectedColorCallback:
+          gFolderTreeController.previewSelectedColor,
+        clearFolderSelectionCallback:
+          gFolderTreeController.clearFolderSelection,
+        updateColorCallback: gFolderTreeController.updateColor,
       }
     );
   },
 
   /**
    * Opens the dialog to rename a particular folder, and does the renaming if
    * the user clicks OK in that dialog
    *
@@ -3235,18 +3294,24 @@ var gFolderTreeController = {
       }
     }
     window.openDialog(
       "chrome://messenger/content/virtualFolderProperties.xhtml",
       "",
       "chrome,modal,centerscreen",
       {
         folder,
+        treeView: gFolderTreeView,
         editExistingFolder: true,
         onOKCallback: editVirtualCallback,
+        previewSelectedColorCallback:
+          gFolderTreeController.previewSelectedColor,
+        clearFolderSelectionCallback:
+          gFolderTreeController.clearFolderSelection,
+        updateColorCallback: gFolderTreeController.updateColor,
         msgWindow,
       }
     );
   },
 
   /**
    * Opens a search window with the given folder, or the selected one if none
    * is given.
@@ -3310,16 +3375,89 @@ var gFolderTreeController = {
     return true;
   },
 
   get _tree() {
     let tree = document.getElementById("folderTree");
     delete this._tree;
     return (this._tree = tree);
   },
+
+  /**
+   * Update the inline preview style in the messagener.xhtml file to show
+   * users a preview of the defined color.
+   *
+   * @param {ftvItem} folder - The folder where the color is defined.
+   * @param {string} newColor - The new hash color to preview.
+   */
+  previewSelectedColor(folder, newColor) {
+    // If the color is null, it measn we're resetting to the default value.
+    if (!newColor) {
+      gFolderTreeView.setFolderCacheProperty(folder, "folderIconColor", "");
+
+      // Clear the preview CSS.
+      gFolderTreeView.previewStyle.textContent = "";
+
+      // Force the folder update to see the new color.
+      gFolderTreeView._tree.invalidateRow(
+        gFolderTreeView.getIndexOfFolder(folder)
+      );
+      return;
+    }
+
+    // Add the new color property.
+    gFolderTreeView.setFolderCacheProperty(folder, "folderIconColor", newColor);
+
+    let selector = `customColor-${newColor.replace("#", "")}`;
+    // Add the inline CSS styling.
+    gFolderTreeView.previewStyle.textContent = `treechildren::-moz-tree-image(folderNameCol, ${selector}) {fill: ${newColor};}`;
+
+    // Force the folder update to set the new color.
+    gFolderTreeView._tree.invalidateRow(
+      gFolderTreeView.getIndexOfFolder(folder)
+    );
+  },
+
+  /**
+   * Clear the preview style and add the new selected color to the persistent
+   * inline style in the messenger.xhtml file.
+   *
+   * @param {ftvItem} folder - The folder where the new color was defined.
+   */
+  updateColor(folder) {
+    // Clear the preview CSS.
+    gFolderTreeView.previewStyle.textContent = "";
+
+    let newColor = gFolderTreeView.getFolderCacheProperty(
+      folder,
+      "folderIconColor"
+    );
+
+    // Append new incline color if defined.
+    gFolderTreeView.appendColor(newColor);
+
+    // Store the new color in the Folder database.
+    folder.msgDatabase.dBFolderInfo.setCharProperty(
+      "folderIconColor",
+      newColor
+    );
+
+    // Force the folder update to set the new color.
+    gFolderTreeView._tree.invalidateRow(
+      gFolderTreeView.getIndexOfFolder(folder)
+    );
+  },
+
+  /**
+   * Force the clear of the selection when the color picker is opened to allow
+   * users to see the color preview.
+   */
+  clearFolderSelection() {
+    gFolderTreeView.selection.clearSelection();
+  },
 };
 
 /**
  * Constructor for ftv_SmartItem. This is a top level item in the "smart"
  * (a.k.a. "Unified") folder mode.
  */
 function ftv_SmartItem(aFolder) {
   ftvItem.call(this, aFolder); // call super constructor
--- a/mail/base/content/messenger.xhtml
+++ b/mail/base/content/messenger.xhtml
@@ -39,16 +39,20 @@
 
 <?xml-stylesheet href="chrome://calendar/skin/calendar-views.css" type="text/css"?>
 <?xml-stylesheet href="chrome://calendar/skin/shared/calendar-alarms.css" type="text/css"?>
 <?xml-stylesheet href="chrome://calendar/skin/shared/widgets/minimonth.css" type="text/css"?>
 <?xml-stylesheet href="chrome://calendar/skin/widgets/calendar-widgets.css" type="text/css"?>
 
 <?xml-stylesheet href="chrome://calendar/skin/lightning-toolbar.css" type="text/css"?>
 
+<!-- NEEDED FOR FOLDER COLOR CUSTOMIZATION -->
+<?xml-stylesheet href="#inlineStyle" type="text/css"?>
+<?xml-stylesheet href="#previewStyle" type="text/css"?>
+
 # All DTD information is stored in a separate file so that it can be shared by
 # hiddenWindowMac.xhtml.
 <!DOCTYPE window [
 #include messenger-doctype.inc.dtd
 ]>
 
 <!--
   - The 'what you think of when you think of thunderbird' window;
@@ -244,16 +248,20 @@
 <script src="chrome://calendar/content/calendar-menus.js"/>
 
 <!-- NEEDED FOR CALENDAR VIEWS -->
 <script src="chrome://calendar/content/calendar-event-gripbar.js"/>
 
 <!-- NEEDED FOR MIGRATION CHECK AT INSTALL -->
 <script src="chrome://calendar/content/calendar-migration.js"/>
 
+<!-- NEEDED FOR FOLDER COLOR CUSTOMIZATION -->
+<html:style type="text/css" id="inlineStyle"></html:style>
+<html:style type="text/css" id="previewStyle"></html:style>
+
 <commandset id="mailCommands">
 #include mainCommandSet.inc.xhtml
   <commandset id="mailSearchMenuItems"/>
   <commandset id="globalEditMenuItems"
               commandupdater="true"
               events="create-menu-edit"
               oncommandupdate="goUpdateGlobalEditMenuItems()"/>
   <commandset id="selectEditMenuItems"
--- a/mail/locales/en-US/chrome/messenger/folderProps.dtd
+++ b/mail/locales/en-US/chrome/messenger/folderProps.dtd
@@ -44,16 +44,19 @@
 
 <!ENTITY selectofflineNewsgroup.check.label      "Select this newsgroup for offline use">
 <!ENTITY selectofflineNewsgroup.check.accesskey  "o">
 <!ENTITY offlineNewsgroup.button.label           "Download Now">
 <!ENTITY offlineNewsgroup.button.accesskey       "D">
 
 <!ENTITY folderProps.name.label                  "Name:">
 <!ENTITY folderProps.name.accesskey              "N">
+<!ENTITY folderProps.color.label                 "Icon Color:">
+<!ENTITY folderProps.color.accesskey             "I">
+<!ENTITY folderProps.reset.tooltip               "Restore default color">
 <!ENTITY folderProps.location.label              "Location:">
 <!ENTITY folderProps.location.accesskey          "L">
 
 <!ENTITY folderSharingTab.label                  "Sharing">
 <!ENTITY privileges.button.label                 "Privileges…">
 <!ENTITY privileges.button.accesskey             "P">
 <!ENTITY permissionsDesc.label                   "You have the following permissions:">
 <!ENTITY folderType.label                        "Folder Type:">
--- a/mail/themes/shared/jar.inc.mn
+++ b/mail/themes/shared/jar.inc.mn
@@ -71,16 +71,17 @@
   skin/classic/messenger/icons/feeds-folder.svg               (../shared/mail/icons/feeds-folder.svg)
   skin/classic/messenger/icons/file.svg                       (../shared/mail/icons/file.svg)
   skin/classic/messenger/icons/file-item.svg                  (../shared/mail/icons/file-item.svg)
   skin/classic/messenger/icons/filter.svg                     (../shared/mail/icons/filter.svg)
   skin/classic/messenger/icons/flag-col.svg                   (../shared/mail/icons/flag-col.svg)
   skin/classic/messenger/icons/flagged.svg                    (../shared/mail/icons/flagged.svg)
   skin/classic/messenger/icons/folder.svg                     (../shared/mail/icons/folder.svg)
   skin/classic/messenger/icons/folder-local.svg               (../shared/mail/icons/folder-local.svg)
+  skin/classic/messenger/icons/forget.svg                     (../shared/mail/icons/forget.svg)
   skin/classic/messenger/icons/forward.svg                    (../shared/mail/icons/forward.svg)
   skin/classic/messenger/icons/get-all.svg                    (../shared/mail/icons/get-all.svg)
   skin/classic/messenger/icons/getmsg.svg                     (../shared/mail/icons/getmsg.svg)
   skin/classic/messenger/icons/goback.svg                     (../shared/mail/icons/goback.svg)
   skin/classic/messenger/icons/goforward.svg                  (../shared/mail/icons/goforward.svg)
   skin/classic/messenger/icons/globe.svg                      (../shared/mail/icons/globe.svg)
   skin/classic/messenger/icons/globe-secure.svg               (../shared/mail/icons/globe-secure.svg)
   skin/classic/messenger/icons/help.svg                       (../shared/mail/icons/help.svg)
--- a/mail/themes/shared/mail/icons/folder.svg
+++ b/mail/themes/shared/mail/icons/folder.svg
@@ -1,4 +1,6 @@
 <!-- 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/. -->
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M14 3H8.151L6.584 1.538A2 2 0 0 0 5.219 1H2a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zM5.219 3l1.072 1H2V3zM14 13H2V5h6v-.014c.05 0 .1.014.151.014H14z"></path></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <path fill="context-fill" d="M13 4H7.85L6.38 2.54A1.93 1.93 0 005.02 2H2a2 2 0 00-2 2v9c0 1.1.9 2 2 2h11a2 2 0 002-2V6a2 2 0 00-2-2zM5 4l1 1H2V4zm8 9H2V6h5.8v-.01c0-.05.1.01.15.01H13z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/mail/themes/shared/mail/icons/forget.svg
@@ -0,0 +1,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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill" d="M6.854 10.854l2-2A.5.5 0 0 0 9 8.5v-4a.5.5 0 0 0-1 0v3.793l-1.854 1.853a.5.5 0 1 0 .707.707zM8 0a8.011 8.011 0 0 0-7 4.184V1.5a.5.5 0 0 0-1 0v5a.5.5 0 0 0 .5.5h5a.5.5 0 0 0 0-1H2.344a.938.938 0 0 0 .056-.085 6 6 0 1 1 0 4.184 1 1 0 0 0-1.873.7A7.991 7.991 0 1 0 8 0z"></path></svg>
\ No newline at end of file
--- a/mail/themes/shared/mail/input-fields.css
+++ b/mail/themes/shared/mail/input-fields.css
@@ -55,8 +55,24 @@ html|input[type="number"].input-number-i
   flex: 1 !important;
   padding: 2px 2px 3px;
   margin-inline-start: 2px;
 }
 
 html|input[type="number"]::-moz-number-spin-box {
   margin-inline-start: 4px;
 }
+
+/* Buttons */
+.btn-reset {
+  -moz-appearance: none;
+  list-style-image: url("chrome://messenger/skin/icons/forget.svg");
+  -moz-context-properties: fill;
+  fill: currentColor;
+  min-width: 16px;
+  min-height: 16px;
+  padding: 2px !important;
+  margin-inline-end: 4px;
+}
+
+.btn-reset .button-icon {
+  margin-inline-end: 0;
+}
--- a/mailnews/base/content/folderProps.js
+++ b/mailnews/base/content/folderProps.js
@@ -2,20 +2,24 @@
  * 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/. */
 
 /* import-globals-from retention.js */
 
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 var { Gloda } = ChromeUtils.import("resource:///modules/gloda/Gloda.jsm");
 
+var gFolderTreeView;
 var gMsgFolder;
 var gLockedPref = null;
+var kCurrentColor = "";
+var kDefaultColor = "#363959";
 
 document.addEventListener("dialogaccept", folderPropsOKButton);
+document.addEventListener("dialogcancel", folderCancelButton);
 
 // The folderPropsSink is the class that gets notified of an imap folder's properties
 
 var gFolderPropsSink = {
   setFolderType(folderTypeString) {
     var typeLabel = document.getElementById("folderType.text");
     if (typeLabel) {
       typeLabel.setAttribute("value", folderTypeString);
@@ -109,16 +113,32 @@ var gFolderPropsSink = {
 
 function doEnabling() {
   var nameTextbox = document.getElementById("name");
   document
     .querySelector("dialog")
     .getButton("accept").disabled = !nameTextbox.value;
 }
 
+/**
+ * Clear the tree selection if the user opens the color picker.
+ */
+function inputColorClicked() {
+  window.arguments[0].clearFolderSelectionCallback();
+}
+
+/**
+ * Reset the folder color to the default value.
+ */
+function resetColor() {
+  inputColorClicked();
+  document.getElementById("color").value = kDefaultColor;
+  window.arguments[0].previewSelectedColorCallback(gMsgFolder, null);
+}
+
 function folderPropsOKButton(event) {
   if (gMsgFolder) {
     // set charset attributes
     var folderCharsetList = document.getElementById("folderCharsetList");
 
     // Log to the Error Console the charset value for the folder
     // if it is unknown to us. Value will be preserved by the menu-item.
     if (folderCharsetList.selectedIndex == -1) {
@@ -166,44 +186,63 @@ function folderPropsOKButton(event) {
 
     var retentionSettings = saveCommonRetentionSettings(
       gMsgFolder.retentionSettings
     );
     retentionSettings.useServerDefaults = document.getElementById(
       "retention.useDefault"
     ).checked;
     gMsgFolder.retentionSettings = retentionSettings;
+
+    // Check if the icon color was updated.
+    if (
+      kCurrentColor !=
+      gFolderTreeView.getFolderCacheProperty(gMsgFolder, "folderIconColor")
+    ) {
+      window.arguments[0].updateColorCallback(gMsgFolder);
+    }
   }
 
   try {
     // This throws an exception when an illegal folder name was entered.
     top.okCallback(
       document.getElementById("name").value,
       window.arguments[0].name,
       gMsgFolder.URI
     );
   } catch (e) {
     event.preventDefault();
   }
 }
 
+function folderCancelButton(event) {
+  // Restore the icon to the previous color and discard edits.
+  window.arguments[0].previewSelectedColorCallback(gMsgFolder, kCurrentColor);
+}
+
 function folderPropsOnLoad() {
   // look in arguments[0] for parameters
   if (window.arguments && window.arguments[0]) {
     if (window.arguments[0].title) {
       document.title = window.arguments[0].title;
     }
     if (window.arguments[0].okCallback) {
       top.okCallback = window.arguments[0].okCallback;
     }
   }
 
-  // fill in folder name, based on what they selected in the folder pane
   if (window.arguments[0].folder) {
+    // Fill in folder name, based on what they selected in the folder pane.
     gMsgFolder = window.arguments[0].folder;
+    gFolderTreeView = window.arguments[0].treeView;
+    // Store the current icon color to allow discarding edits.
+    kCurrentColor = gFolderTreeView.getFolderCacheProperty(
+      gMsgFolder,
+      "folderIconColor"
+    );
   } else {
     dump("passed null for folder, do nothing\n");
   }
 
   if (window.arguments[0].name) {
     // Initialize name textbox with the given name and remember this
     // value so we can tell whether the folder needs to be renamed
     // when the dialog is accepted.
@@ -224,16 +263,25 @@ function folderPropsOnLoad() {
     // We really need a functioning database, so we'll detect problems
     // and create one if we have to.
     try {
       gMsgFolder.getDatabase(null);
     } catch (e) {
       gMsgFolder.updateFolder(window.arguments[0].msgWindow);
     }
 
+    let colorInput = document.getElementById("color");
+    colorInput.value = kCurrentColor ? kCurrentColor : kDefaultColor;
+    colorInput.addEventListener("input", event => {
+      window.arguments[0].previewSelectedColorCallback(
+        gMsgFolder,
+        event.target.value
+      );
+    });
+
     var locationTextbox = document.getElementById("location");
 
     // Decode the displayed mailbox:// URL as it's useful primarily for debugging,
     // whereas imap and news urls are sent around.
     locationTextbox.value =
       serverType == "imap" || serverType == "nntp"
         ? gMsgFolder.folderURL
         : decodeURI(gMsgFolder.folderURL);
--- a/mailnews/base/content/folderProps.xhtml
+++ b/mailnews/base/content/folderProps.xhtml
@@ -39,16 +39,28 @@
         <label id="nameLabel" value="&folderProps.name.label;" control="name"
                accesskey="&folderProps.name.accesskey;"/>
         <html:input id="name"
                     type="text"
                     readonly="readonly"
                     oninput="doEnabling();"
                     class="input-inline"
                     aria-labelledby="nameLabel"/>
+        <label id="colorLabel" value="&folderProps.color.label;" control="color"
+               accesskey="&folderProps.color.accesskey;"/>
+        <html:input id="color"
+                    type="color"
+                    value=""
+                    class="input-inline-color"
+                    onclick="inputColorClicked();"
+                    aria-labelledby="colorLabel"/>
+        <button id="resetColor"
+                tooltiptext="&folderProps.reset.tooltip;"
+                class="toolbarbutton-1 btn-flat btn-reset"
+                onclick="resetColor();"/>
       </hbox>
       <hbox align="center" class="input-container">
         <label id="locationLabel" value="&folderProps.location.label;"
                control="location" accesskey="&folderProps.location.accesskey;"/>
         <html:input id="location"
                     type="text"
                     readonly="readonly"
                     class="uri-element input-inline"
--- a/mailnews/base/content/msgSelectOfflineFolders.xhtml
+++ b/mailnews/base/content/msgSelectOfflineFolders.xhtml
@@ -1,30 +1,35 @@
 <?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/msgSelectOffline.css" type="text/css"?>
+<!-- NEEDED FOR FOLDER COLOR CUSTOMIZATION -->
+<?xml-stylesheet href="#inlineStyle" type="text/css"?>
 
 <!DOCTYPE window SYSTEM "chrome://messenger/locale/msgSynchronize.dtd" >
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml"
         windowtype="mailnews:selectOffline"
         title="&MsgSelect.label;"
         width="450" height="400"
         persist="width height"
         onload="gSelectOffline.load();">
 <dialog id="select-offline">
   <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
   <stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/>
 
   <script src="chrome://messenger/content/msgSelectOfflineFolders.js"/>
   <script src="chrome://messenger/content/folderPane.js"/>
 
+  <!-- NEEDED FOR FOLDER COLOR CUSTOMIZATION -->
+  <html:style type="text/css" id="inlineStyle"></html:style>
+
   <label class="desc" control="synchronizeTree">&MsgSelectDesc.label;</label>
 
   <tree id="synchronizeTree"
         flex="1"
         hidecolumnpicker="true"
         seltype="multiple"
         disableKeyNavigation="true"
         simplelist="true"
--- a/mailnews/base/content/virtualFolderListEdit.xhtml
+++ b/mailnews/base/content/virtualFolderListEdit.xhtml
@@ -1,28 +1,33 @@
 <?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/mailWindow1.css" type="text/css"?>
+<!-- NEEDED FOR FOLDER COLOR CUSTOMIZATION -->
+<?xml-stylesheet href="#inlineStyle" type="text/css"?>
 
 <!DOCTYPE window SYSTEM "chrome://messenger/locale/virtualFolderListDialog.dtd">
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml"
         title="&virtualFolderListTitle.title;"
         windowtype="mailnews:virtualFolderList"
         style="width: 27em; height: 25em;"
         persist="width height screenX screenY"
         onload="gSelectVirtual.load();">
 <dialog id="searchFolderWindow">
   <script src="chrome://messenger/content/virtualFolderListEdit.js"/>
   <script src="chrome://messenger/content/folderPane.js"/>
 
+  <!-- NEEDED FOR FOLDER COLOR CUSTOMIZATION -->
+  <html:style type="text/css" id="inlineStyle"></html:style>
+
   <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
 
   <label control="folderPickerTree">&virtualFolderDesc.label;</label>
 
     <tree id="folderPickerTree"
           flex="1"
           hidecolumnpicker="true"
           seltype="multiple"
--- a/mailnews/base/content/virtualFolderProperties.js
+++ b/mailnews/base/content/virtualFolderProperties.js
@@ -1,21 +1,24 @@
 /* -*- 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/. */
 
 /* import-globals-from ../search/content/searchTerm.js */
 
+var gFolderTreeView;
 var gPickedFolder;
 var gMailView = null;
 var msgWindow; // important, don't change the name of this variable. it's really a global used by commandglue.js
 var gSearchTermSession; // really an in memory temporary filter we use to read in and write out the search terms
 var gSearchFolderURIs = "";
 var gMessengerBundle = null;
+var kCurrentColor = "";
+var kDefaultColor = "#363959";
 
 var nsMsgSearchScope = Ci.nsMsgSearchScope;
 
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 var { PluralForm } = ChromeUtils.import(
   "resource://gre/modules/PluralForm.jsm"
 );
 var { MailServices } = ChromeUtils.import(
@@ -25,16 +28,17 @@ var { VirtualFolderHelper } = ChromeUtil
   "resource:///modules/VirtualFolderWrapper.jsm"
 );
 var { fixIterator } = ChromeUtils.import(
   "resource:///modules/iteratorUtils.jsm"
 );
 var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
 
 document.addEventListener("dialogaccept", onOK);
+document.addEventListener("dialogcancel", onCancel);
 
 function onLoad() {
   var windowArgs = window.arguments[0];
   var acceptButton = document.querySelector("dialog").getButton("accept");
 
   gMessengerBundle = document.getElementById("bundle_messenger");
 
   // call this when OK is pressed
@@ -146,16 +150,34 @@ function InitDialogWithVirtualFolder(aVi
   );
 
   // when editing an existing folder, hide the folder picker that stores the parent location of the folder
   document.getElementById("msgNewFolderPicker").collapsed = true;
   document.getElementById("chooseFolderLocationLabel").collapsed = true;
   let folderNameField = document.getElementById("existingName");
   folderNameField.removeAttribute("hidden");
 
+  // Show the icon color options.
+  document.getElementById("iconColorContainer").collapsed = false;
+  // Store the current icon color to allow discarding edits.
+  gFolderTreeView = window.arguments[0].treeView;
+  kCurrentColor = gFolderTreeView.getFolderCacheProperty(
+    aVirtualFolder,
+    "folderIconColor"
+  );
+
+  let colorInput = document.getElementById("color");
+  colorInput.value = kCurrentColor ? kCurrentColor : kDefaultColor;
+  colorInput.addEventListener("input", event => {
+    window.arguments[0].previewSelectedColorCallback(
+      aVirtualFolder,
+      event.target.value
+    );
+  });
+
   gSearchFolderURIs = virtualFolderWrapper.searchFolderURIs;
   updateFoldersCount();
   document.getElementById("searchOnline").checked =
     virtualFolderWrapper.onlineSearch;
   gSearchTermSession = virtualFolderWrapper.searchTermsSession;
 
   setupSearchRows(gSearchTermSession.searchTerms);
 
@@ -201,16 +223,27 @@ function onOK(event) {
     );
     virtualFolderWrapper.searchTerms = gSearchTermSession.searchTerms;
     virtualFolderWrapper.searchFolders = gSearchFolderURIs;
     virtualFolderWrapper.onlineSearch = searchOnline;
     virtualFolderWrapper.cleanUpMessageDatabase();
 
     MailServices.accounts.saveVirtualFolders();
 
+    // Check if the icon color was updated.
+    if (
+      kCurrentColor !=
+      gFolderTreeView.getFolderCacheProperty(
+        window.arguments[0].folder,
+        "folderIconColor"
+      )
+    ) {
+      window.arguments[0].updateColorCallback(window.arguments[0].folder);
+    }
+
     if (window.arguments[0].onOKCallback) {
       window.arguments[0].onOKCallback(virtualFolderWrapper.virtualFolder.URI);
     }
     return;
   }
   var uri = gPickedFolder.URI;
   if (name && uri) {
     // create a new virtual folder
@@ -243,16 +276,26 @@ function onOK(event) {
       parentFolder,
       gSearchFolderURIs,
       gSearchTermSession.searchTerms,
       searchOnline
     );
   }
 }
 
+function onCancel(event) {
+  if (window.arguments[0].folder) {
+    // Restore the icon to the previous color and discard edits.
+    window.arguments[0].previewSelectedColorCallback(
+      window.arguments[0].folder,
+      kCurrentColor
+    );
+  }
+}
+
 function doEnabling() {
   var acceptButton = document.querySelector("dialog").getButton("accept");
   acceptButton.disabled = !document.getElementById("name").value;
 }
 
 function chooseFoldersToSearch() {
   // if we have some search folders already, then root the folder picker dialog off the account
   // for those folders. Otherwise fall back to the preselectedfolderURI which is the parent folder
@@ -298,8 +341,27 @@ function updateFoldersCount() {
     foldersList.removeAttribute("tooltiptext");
   }
 }
 
 function onEnterInSearchTerm() {
   // stub function called by the core search widget code...
   // nothing for us to do here
 }
+
+/**
+ * Clear the tree selection if the user opens the color picker.
+ */
+function inputColorClicked() {
+  window.arguments[0].clearFolderSelectionCallback();
+}
+
+/**
+ * Reset the folder color to the default value.
+ */
+function resetColor() {
+  inputColorClicked();
+  document.getElementById("color").value = kDefaultColor;
+  window.arguments[0].previewSelectedColorCallback(
+    window.arguments[0].folder,
+    null
+  );
+}
--- a/mailnews/base/content/virtualFolderProperties.xhtml
+++ b/mailnews/base/content/virtualFolderProperties.xhtml
@@ -6,16 +6,18 @@
 
 <?xml-stylesheet href="chrome://messenger/skin/searchDialog.css" type="text/css"?>
 <?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
 <?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
 
 <!DOCTYPE window [
   <!ENTITY % folderDTD SYSTEM "chrome://messenger/locale/virtualFolderProperties.dtd">
   %folderDTD;
+  <!ENTITY % folderPropsDTD SYSTEM "chrome://messenger/locale/folderProps.dtd">
+  %folderPropsDTD;
   <!ENTITY % searchTermDTD SYSTEM "chrome://messenger/locale/searchTermOverlay.dtd">
   %searchTermDTD;
 ]>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         xmlns:html="http://www.w3.org/1999/xhtml"
         title="&virtualFolderProperties.title;"
         onload="onLoad();"
@@ -50,26 +52,42 @@
         <label value="&description.label;" accesskey="&description.accesskey;"
                control="msgNewFolderPicker"/>
       </hbox>
       <hbox flex="1" align="center">
         <label value="&folderSelectionCaption.label;"/>
       </hbox>
     </vbox>
     <vbox flex="1">
-      <html:input id="name"
-                  hidden="true"
-                  class="input-inline"
-                  aria-labelledby="nameLabel"
-                  oninput="doEnabling();"/>
-      <html:input id="existingName"
-                  readonly="readonly"
-                  hidden="hidden"
-                  class="input-inline"
-                  tabindex="0"/>
+      <hbox class="input-container">
+        <html:input id="name"
+                    hidden="hidden"
+                    class="input-inline"
+                    aria-labelledby="nameLabel"
+                    oninput="doEnabling();"/>
+        <html:input id="existingName"
+                    readonly="readonly"
+                    hidden="hidden"
+                    class="input-inline"
+                    tabindex="0"/>
+        <hbox id="iconColorContainer" align="center" collapsed="true">
+          <label id="colorLabel" value="&folderProps.color.label;" control="color"
+                 accesskey="&folderProps.color.accesskey;"/>
+          <html:input id="color"
+                      type="color"
+                      value=""
+                      class="input-inline-color"
+                      onclick="inputColorClicked();"
+                      aria-labelledby="colorLabel"/>
+          <button id="resetColor"
+                  tooltiptext="&folderProps.reset.tooltip;"
+                  class="toolbarbutton-1 btn-flat btn-reset"
+                  onclick="resetColor();"/>
+        </hbox>
+      </hbox>
       <menulist id="msgNewFolderPicker" class="folderMenuItem" align="center"
                 displayformat="verbose">
         <menupopup is="folder-menupopup" id="msgNewFolderPopup" class="menulist-menupopup"
                    mode="newFolder" showFileHereLabel="true" oncommand="onFolderPick(event);"/>
       </menulist>
       <hbox align="center">
         <label id="chosenFoldersCount"/>
         <spacer flex="1"/>