Bug 1267916 - part 2 - Add in UX for about:preferences#containers page, r=jaws
authorJonathan Kingston <jkt@mozilla.com>
Thu, 03 Nov 2016 19:31:35 +0100
changeset 363758 964223d37a2e59a28c4ee1df5a808252442ea94f
parent 363757 127379f03ad8a60918cfddf0db15d77c1fc32921
child 363759 4f09d9469e73adf32c7db6720504fcbe580516b3
child 363830 91bc0411b4f64a34dee35924e904ae01c96ed6e9
push id6795
push userjlund@mozilla.com
push dateMon, 23 Jan 2017 14:19:46 +0000
treeherdermozilla-beta@76101b503191 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1267916
milestone52.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1267916 - part 2 - Add in UX for about:preferences#containers page, r=jaws
browser/components/contextualidentity/content/usercontext.css
browser/components/preferences/containers.js
browser/components/preferences/containers.xul
browser/components/preferences/handlers.css
browser/components/preferences/handlers.xml
browser/components/preferences/in-content/containers.js
browser/components/preferences/in-content/containers.xul
browser/components/preferences/in-content/jar.mn
browser/components/preferences/in-content/preferences.js
browser/components/preferences/in-content/preferences.xul
browser/components/preferences/in-content/privacy.js
browser/components/preferences/in-content/privacy.xul
browser/components/preferences/jar.mn
browser/locales/en-US/chrome/browser/preferences/containers.dtd
browser/locales/en-US/chrome/browser/preferences/containers.properties
browser/locales/en-US/chrome/browser/preferences/preferences.dtd
browser/locales/en-US/chrome/browser/preferences/privacy.dtd
browser/locales/jar.mn
browser/themes/shared/incontentprefs/containers.css
browser/themes/shared/jar.inc.mn
browser/themes/shared/preferences/containers.css
--- a/browser/components/contextualidentity/content/usercontext.css
+++ b/browser/components/contextualidentity/content/usercontext.css
@@ -73,16 +73,17 @@
 }
 
 .tabbrowser-tab[usercontextid] {
   background-image: linear-gradient(to right, transparent 20%, var(--identity-tab-color) 30%, var(--identity-tab-color) 70%, transparent 80%);
   background-size: auto 2px;
   background-repeat: no-repeat;
 }
 
+.userContext-icon,
 .menuitem-iconic[data-usercontextid] > .menu-iconic-left > .menu-iconic-icon,
 .subviewbutton[usercontextid] > .toolbarbutton-icon,
 #userContext-indicator {
   background-image: var(--identity-icon);
   filter: url(chrome://browser/skin/filters.svg#fill);
   fill: var(--identity-icon-color);
   background-size: contain;
   background-repeat: no-repeat;
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/containers.js
@@ -0,0 +1,177 @@
+/* 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/ContextualIdentityService.jsm");
+
+const containersBundle = Services.strings.createBundle("chrome://browser/locale/preferences/containers.properties");
+
+const HTMLNS = "http://www.w3.org/1999/xhtml";
+
+let gContainersManager = {
+  icons: [
+    "fingerprint",
+    "briefcase",
+    "dollar",
+    "cart",
+    "circle"
+  ],
+
+  colors: [
+    "blue",
+    "turquoise",
+    "green",
+    "yellow",
+    "orange",
+    "red",
+    "pink",
+    "purple"
+  ],
+
+  onLoad() {
+    let params = window.arguments[0] || {};
+    this.init(params);
+  },
+
+  init(aParams) {
+    this.userContextId = aParams.userContextId || null;
+    this.identity = aParams.identity;
+
+    if (aParams.windowTitle) {
+      document.title = aParams.windowTitle;
+    }
+
+    const iconWrapper = document.getElementById("iconWrapper");
+    iconWrapper.appendChild(this.createIconButtons());
+
+    const colorWrapper = document.getElementById("colorWrapper");
+    colorWrapper.appendChild(this.createColorSwatches());
+
+    if (this.identity.name) {
+      const name = document.getElementById("name");
+      name.value = this.identity.name;
+      this.checkForm();
+    }
+
+    this.setLabelsMinWidth();
+
+    // This is to prevent layout jank caused by the svgs and outlines rendering at different times
+    document.getElementById("containers-content").removeAttribute("hidden");
+  },
+
+  setLabelsMinWidth() {
+    const labelMinWidth = containersBundle.GetStringFromName("containers.labelMinWidth");
+    const labels = [
+      document.getElementById("nameLabel"),
+      document.getElementById("iconLabel"),
+      document.getElementById("colorLabel")
+    ];
+    for (let label of labels) {
+      label.style.minWidth = labelMinWidth;
+    }
+  },
+
+  uninit() {
+  },
+
+  // Check if name string as to if the form can be submitted
+  checkForm() {
+    const name = document.getElementById("name");
+    let btnApplyChanges = document.getElementById("btnApplyChanges");
+    if (!name.value) {
+      btnApplyChanges.setAttribute("disabled", true);
+    } else {
+      btnApplyChanges.removeAttribute("disabled");
+    }
+  },
+
+  createIconButtons(defaultIcon) {
+    let radiogroup = document.createElement("radiogroup");
+    radiogroup.setAttribute("id", "icon");
+    radiogroup.className = "icon-buttons";
+
+    for (let icon of this.icons) {
+      let iconSwatch = document.createElement("radio");
+      iconSwatch.id = "iconbutton-" + icon;
+      iconSwatch.name = "icon";
+      iconSwatch.type = "radio";
+      iconSwatch.value = icon;
+
+      if (this.identity.icon && this.identity.icon == icon) {
+        iconSwatch.setAttribute("selected", true);
+      }
+
+      iconSwatch.setAttribute("label",
+        containersBundle.GetStringFromName(`containers.${icon}.label`));
+      let iconElement = document.createElement("hbox");
+      iconElement.className = 'userContext-icon';
+      iconElement.setAttribute("data-identity-icon", icon);
+
+      iconSwatch.appendChild(iconElement);
+      radiogroup.appendChild(iconSwatch);
+    }
+
+    return radiogroup;
+  },
+
+  createColorSwatches(defaultColor) {
+    let radiogroup = document.createElement("radiogroup");
+    radiogroup.setAttribute("id", "color");
+
+    for (let color of this.colors) {
+      let colorSwatch = document.createElement("radio");
+      colorSwatch.id = "colorswatch-" + color;
+      colorSwatch.name = "color";
+      colorSwatch.type = "radio";
+      colorSwatch.value = color;
+
+      if (this.identity.color && this.identity.color == color) {
+        colorSwatch.setAttribute("selected", true);
+      }
+
+      colorSwatch.setAttribute("label",
+        containersBundle.GetStringFromName(`containers.${color}.label`));
+      let iconElement = document.createElement("hbox");
+      iconElement.className = 'userContext-icon';
+      iconElement.setAttribute("data-identity-icon", "circle");
+      iconElement.setAttribute("data-identity-color", color);
+
+      colorSwatch.appendChild(iconElement);
+      radiogroup.appendChild(colorSwatch);
+    }
+    return radiogroup;
+  },
+
+  onApplyChanges() {
+    let icon = document.getElementById("icon").value;
+    let color = document.getElementById("color").value;
+    let name = document.getElementById("name").value;
+    let userContextId = false;
+
+    if (this.icons.indexOf(icon) == -1) {
+      throw "Internal error. The icon value doesn't match.";
+    }
+
+    if (this.colors.indexOf(color) == -1) {
+      throw "Internal error. The color value doesn't match.";
+    }
+
+    if (this.userContextId) {
+      ContextualIdentityService.update(this.userContextId,
+        name,
+        icon,
+        color);
+    } else {
+      ContextualIdentityService.create(name,
+        icon,
+        color);
+    }
+    window.parent.location.reload()
+  },
+
+  onWindowKeyPress(aEvent) {
+    if (aEvent.keyCode == KeyEvent.DOM_VK_ESCAPE)
+      window.close();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/containers.xul
@@ -0,0 +1,52 @@
+<?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://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/containers.css" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://browser/locale/preferences/containers.dtd" >
+
+<window id="ContainersDialog" class="windowDialog"
+        windowtype="Browser:Permissions"
+        title="&window.title;"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        style="width: &window.width;;"
+        onload="gContainersManager.onLoad();"
+        onunload="gContainersManager.uninit();"
+        persist="screenX screenY width height"
+        onkeypress="gContainersManager.onWindowKeyPress(event);">
+
+  <script src="chrome://global/content/treeUtils.js"/>
+  <script src="chrome://browser/content/preferences/containers.js"/>
+
+  <stringbundle id="bundlePreferences"
+                src="chrome://browser/locale/preferences/preferences.properties"/>
+
+  <keyset>
+    <key key="&windowClose.key;" modifiers="accel" oncommand="window.close();"/>
+  </keyset>
+
+  <vbox class="contentPane largeDialogContainer" flex="1" hidden="true" id="containers-content">
+    <description id="permissionsText" control="url"/>
+    <separator class="thin"/>
+    <hbox align="start">
+      <label id="nameLabel" control="url" value="&name.label;" accesskey="&name.accesskey;"/>
+      <textbox id="name" flex="1" onkeyup="gContainersManager.checkForm();" />
+    </hbox>
+    <hbox align="center" id="iconWrapper">
+      <label id="iconLabel" control="url" value="&icon.label;" accesskey="&icon.accesskey;"/>
+    </hbox>
+    <hbox align="center" id="colorWrapper">
+      <label id="colorLabel" control="url" value="&color.label;" accesskey="&color.accesskey;"/>
+    </hbox>
+  </vbox>
+  <vbox>
+    <hbox class="actionButtons" align="right" flex="1">
+      <button id="btnApplyChanges" disabled="true" oncommand="gContainersManager.onApplyChanges();" icon="save"
+              label="&button.ok.label;" accesskey="&button.ok.accesskey;"/>
+    </hbox>
+  </vbox>
+</window>
--- a/browser/components/preferences/handlers.css
+++ b/browser/components/preferences/handlers.css
@@ -5,16 +5,20 @@
 #handlersView > richlistitem {
   -moz-binding: url("chrome://browser/content/preferences/handlers.xml#handler");
 }
 
 #handlersView > richlistitem[selected="true"] {
   -moz-binding: url("chrome://browser/content/preferences/handlers.xml#handler-selected");
 }
 
+#containersView > richlistitem {
+  -moz-binding: url("chrome://browser/content/preferences/handlers.xml#container");
+}
+
 /**
  * Make the icons appear.
  * Note: we display the icon box for every item whether or not it has an icon
  * so the labels of all the items align vertically.
  */
 .actionsMenu > menupopup > menuitem > .menu-iconic-left {
   display: -moz-box;
   min-width: 16px;
--- a/browser/components/preferences/handlers.xml
+++ b/browser/components/preferences/handlers.xml
@@ -64,16 +64,39 @@
     <implementation>
       <constructor>
         gApplicationsPane.rebuildActionsMenu();
       </constructor>
     </implementation>
 
   </binding>
 
+  <binding id="container">
+    <content>
+      <xul:hbox flex="1" equalsize="always">
+        <xul:hbox flex="1" align="center">
+          <xul:hbox xbl:inherits="data-identity-icon=containerIcon,data-identity-color=containerColor" height="24" width="24" class="userContext-icon"/>
+          <xul:label flex="1" crop="end" xbl:inherits="value=containerName"/>
+        </xul:hbox>
+        <xul:hbox flex="1" align="right">
+          <xul:button anonid="preferencesButton"
+                      xbl:inherits="value=userContextId"
+                      onclick="gContainersPane.onPeferenceClick(event.originalTarget)">
+            Preferences
+          </xul:button>
+          <xul:button anonid="removeButton"
+                      xbl:inherits="value=userContextId"
+                      onclick="gContainersPane.onRemoveClick(event.originalTarget)">
+            Remove
+          </xul:button>
+        </xul:hbox>
+      </xul:hbox>
+    </content>
+  </binding>
+
   <binding id="offlineapp"
 	   extends="chrome://global/content/bindings/listbox.xml#listitem">
     <content>
       <children>
 	<xul:listcell xbl:inherits="label=origin"/>
 	<xul:listcell xbl:inherits="label=usage"/>
       </children>
     </content>
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/in-content/containers.js
@@ -0,0 +1,73 @@
+/* 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/AppConstants.jsm");
+Components.utils.import("resource://gre/modules/ContextualIdentityService.jsm");
+
+const containersBundle = Services.strings.createBundle("chrome://browser/locale/preferences/containers.properties");
+
+const defaultContainerIcon = "fingerprint";
+const defaultContainerColor = "blue";
+
+let gContainersPane = {
+
+  init() {
+    this._list = document.getElementById("containersView");
+
+    document.getElementById("backContainersLink").addEventListener("click", function () {
+      gotoPref("privacy");
+    });
+
+    this._rebuildView();
+  },
+
+  _rebuildView() {
+    const containers = ContextualIdentityService.getIdentities();
+    while (this._list.firstChild) {
+      this._list.firstChild.remove();
+    }
+    for (let container of containers) {
+      let item = document.createElement("richlistitem");
+      item.setAttribute("containerName", ContextualIdentityService.getUserContextLabel(container.userContextId));
+      item.setAttribute("containerIcon", container.icon);
+      item.setAttribute("containerColor", container.color);
+      item.setAttribute("userContextId", container.userContextId);
+
+      this._list.appendChild(item);
+    }
+  },
+
+  onRemoveClick(button) {
+    let userContextId = button.getAttribute("value");
+    ContextualIdentityService.remove(userContextId);
+    this._rebuildView();
+  },
+  onPeferenceClick(button) {
+    this.openPreferenceDialog(button.getAttribute("value"));
+  },
+
+  onAddButtonClick(button) {
+    this.openPreferenceDialog(null);
+  },
+
+  openPreferenceDialog(userContextId) {
+    let identity = {
+      name: "",
+      icon: defaultContainerIcon,
+      color: defaultContainerColor
+    };
+    let title;
+    if (userContextId) {
+      identity = ContextualIdentityService.getIdentityFromId(userContextId);
+      // This is required to get the translation string from defaults
+      identity.name = ContextualIdentityService.getUserContextLabel(identity.userContextId);
+      title = containersBundle.formatStringFromName("containers.updateContainerTitle", [identity.name], 1);
+    }
+
+    const params = { userContextId, identity, windowTitle: title };
+    gSubDialog.open("chrome://browser/content/preferences/containers.xul",
+                     null, params);
+  }
+
+};
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/in-content/containers.xul
@@ -0,0 +1,54 @@
+# 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/.
+
+<!-- Containers panel -->
+
+<script type="application/javascript"
+        src="chrome://browser/content/preferences/in-content/containers.js"/>
+
+<preferences id="containerPreferences" hidden="true" data-category="paneContainer">
+  <!-- Containers -->
+  <preference id="privacy.userContext.enabled"
+              name="privacy.userContext.enabled"
+              type="bool"/>
+
+</preferences>
+
+<hbox hidden="true"
+      class="container-header-links"
+      data-category="paneContainers">
+  <label class="text-link" id="backContainersLink" value="&backLink.label;" />
+</hbox>
+
+<hbox id="header-containers"
+      class="header"
+      hidden="true"
+      data-category="paneContainers">
+  <label class="header-name" flex="1">&paneContainers.title;</label>
+  <button class="help-button"
+          aria-label="&helpButton.label;"/>
+</hbox>
+
+<!-- Containers -->
+<groupbox id="browserContainersGroup" data-category="paneContainers" hidden="true">
+  <vbox id="browserContainersbox">
+
+    <richlistbox id="containersView" orient="vertical" persist="lastSelectedType"
+                 flex="1">
+      <listheader equalsize="always">
+          <treecol id="typeColumn" label="&label.label;" value="type"
+                   persist="sortDirection"
+                   flex="1" sortDirection="ascending"/>
+          <treecol id="actionColumn" value="action"
+                   persist="sortDirection"
+                   flex="1"/>
+      </listheader>
+    </richlistbox>
+  </vbox>
+  <vbox>
+    <hbox flex="1">
+      <button onclick="gContainersPane.onAddButtonClick();" accesskey="&addButton.accesskey;" label="&addButton.label;"/>
+    </hbox>
+  </vbox>
+</groupbox>
--- a/browser/components/preferences/in-content/jar.mn
+++ b/browser/components/preferences/in-content/jar.mn
@@ -4,14 +4,15 @@
 
 browser.jar:
    content/browser/preferences/in-content/preferences.js
 *  content/browser/preferences/in-content/preferences.xul
    content/browser/preferences/in-content/subdialogs.js
 
    content/browser/preferences/in-content/main.js
    content/browser/preferences/in-content/privacy.js
+   content/browser/preferences/in-content/containers.js
    content/browser/preferences/in-content/advanced.js
    content/browser/preferences/in-content/applications.js
    content/browser/preferences/in-content/content.js
    content/browser/preferences/in-content/sync.js
    content/browser/preferences/in-content/security.js
    content/browser/preferences/in-content/search.js
--- a/browser/components/preferences/in-content/preferences.js
+++ b/browser/components/preferences/in-content/preferences.js
@@ -56,16 +56,17 @@ addEventListener("DOMContentLoaded", fun
 
 function init_all() {
   document.documentElement.instantApply = true;
 
   gSubDialog.init();
   register_module("paneGeneral", gMainPane);
   register_module("paneSearch", gSearchPane);
   register_module("panePrivacy", gPrivacyPane);
+  register_module("paneContainers", gContainersPane);
   register_module("paneAdvanced", gAdvancedPane);
   register_module("paneApplications", gApplicationsPane);
   register_module("paneContent", gContentPane);
   register_module("paneSync", gSyncPane);
   register_module("paneSecurity", gSecurityPane);
 
   let categories = document.getElementById("categories");
   categories.addEventListener("select", event => gotoPref(event.target.value));
--- a/browser/components/preferences/in-content/preferences.xul
+++ b/browser/components/preferences/in-content/preferences.xul
@@ -8,29 +8,32 @@
 <?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
 <?xml-stylesheet href="chrome://global/skin/in-content/common.css"?>
 <?xml-stylesheet
   href="chrome://browser/skin/preferences/in-content/preferences.css"?>
 <?xml-stylesheet
   href="chrome://browser/content/preferences/handlers.css"?>
 <?xml-stylesheet href="chrome://browser/skin/preferences/applications.css"?>
 <?xml-stylesheet href="chrome://browser/skin/preferences/in-content/search.css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/in-content/containers.css"?>
 
 <!DOCTYPE page [
 <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
 <!ENTITY % globalPreferencesDTD SYSTEM "chrome://global/locale/preferences.dtd">
 <!ENTITY % preferencesDTD SYSTEM
   "chrome://browser/locale/preferences/preferences.dtd">
 <!ENTITY % privacyDTD SYSTEM "chrome://browser/locale/preferences/privacy.dtd">
 <!ENTITY % tabsDTD SYSTEM "chrome://browser/locale/preferences/tabs.dtd">
 <!ENTITY % searchDTD SYSTEM "chrome://browser/locale/preferences/search.dtd">
 <!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd">
 <!ENTITY % syncDTD SYSTEM "chrome://browser/locale/preferences/sync.dtd">
 <!ENTITY % securityDTD SYSTEM
   "chrome://browser/locale/preferences/security.dtd">
+<!ENTITY % containersDTD SYSTEM
+  "chrome://browser/locale/preferences/containers.dtd">
 <!ENTITY % sanitizeDTD SYSTEM "chrome://browser/locale/sanitize.dtd">
 <!ENTITY % mainDTD SYSTEM "chrome://browser/locale/preferences/main.dtd">
 <!ENTITY % aboutHomeDTD SYSTEM "chrome://browser/locale/aboutHome.dtd">
 <!ENTITY % contentDTD SYSTEM "chrome://browser/locale/preferences/content.dtd">
 <!ENTITY % applicationsDTD SYSTEM
   "chrome://browser/locale/preferences/applications.dtd">
 <!ENTITY % advancedDTD SYSTEM
   "chrome://browser/locale/preferences/advanced.dtd">
@@ -38,16 +41,17 @@
 %globalPreferencesDTD;
 %preferencesDTD;
 %privacyDTD;
 %tabsDTD;
 %searchDTD;
 %syncBrandDTD;
 %syncDTD;
 %securityDTD;
+%containersDTD;
 %sanitizeDTD;
 %mainDTD;
 %aboutHomeDTD;
 %contentDTD;
 %applicationsDTD;
 %advancedDTD;
 ]>
 
@@ -132,16 +136,22 @@
                     value="panePrivacy"
                     helpTopic="prefs-privacy"
                     tooltiptext="&panePrivacy.title;"
                     align="center">
         <image class="category-icon"/>
         <label class="category-name" flex="1">&panePrivacy.title;</label>
       </richlistitem>
 
+      <richlistitem id="category-containers"
+                    class="category"
+                    value="paneContainers"
+                    helpTopic="prefs-containers"
+                    hidden="true"/>
+
       <richlistitem id="category-security"
                     class="category"
                     value="paneSecurity"
                     helpTopic="prefs-security"
                     tooltiptext="&paneSecurity.title;"
                     align="center">
         <image class="category-icon"/>
         <label class="category-name" flex="1">&paneSecurity.title;</label>
@@ -168,16 +178,17 @@
       </richlistitem>
     </richlistbox>
 
     <vbox class="main-content" flex="1">
       <prefpane id="mainPrefPane">
 #include main.xul
 #include search.xul
 #include privacy.xul
+#include containers.xul
 #include advanced.xul
 #include applications.xul
 #include content.xul
 #include security.xul
 #include sync.xul
       </prefpane>
     </vbox>
 
--- a/browser/components/preferences/in-content/privacy.js
+++ b/browser/components/preferences/in-content/privacy.js
@@ -173,16 +173,18 @@ var gPrivacyPane = {
     setEventListener("trackingProtectionExceptions", "command",
                      gPrivacyPane.showTrackingProtectionExceptions);
     setEventListener("changeBlockList", "command",
                      gPrivacyPane.showBlockLists);
     setEventListener("changeBlockListPBM", "command",
                      gPrivacyPane.showBlockLists);
     setEventListener("browserContainersCheckbox", "command",
                      gPrivacyPane._checkBrowserContainers);
+    setEventListener("browserContainersSettings", "command",
+                     gPrivacyPane.showContainerSettings);
   },
 
   // TRACKING PROTECTION MODE
 
   /**
    * Selects the right item of the Tracking Protection radiogroup.
    */
   trackingProtectionReadPrefs() {
@@ -471,16 +473,23 @@ var gPrivacyPane = {
       windowTitle: bundlePreferences.getString("trackingprotectionpermissionstitle"),
       introText: bundlePreferences.getString("trackingprotectionpermissionstext"),
     };
     gSubDialog.open("chrome://browser/content/preferences/permissions.xul",
                     null, params);
   },
 
   /**
+   * Displays container panel for customising and adding containers.
+   */
+  showContainerSettings() {
+    gotoPref("containers");
+  },
+
+  /**
    * Displays the available block lists for tracking protection.
    */
   showBlockLists: function ()
   {
     var bundlePreferences = document.getElementById("bundlePreferences");
     let brandName = document.getElementById("bundleBrand")
                             .getString("brandShortName");
     var params = { brandShortName: brandName,
@@ -673,11 +682,31 @@ var gPrivacyPane = {
    * Enables or disables the "Settings..." button depending
    * on the privacy.sanitize.sanitizeOnShutdown preference value
    */
   _updateSanitizeSettingsButton: function () {
     var settingsButton = document.getElementById("clearDataSettings");
     var sanitizeOnShutdownPref = document.getElementById("privacy.sanitize.sanitizeOnShutdown");
 
     settingsButton.disabled = !sanitizeOnShutdownPref.value;
+   },
+
+  // CONTAINERS
+
+  /*
+   * preferences:
+   *
+   * privacy.userContext.enabled
+   * - true if containers is enabled
+   */
+
+   /**
+    * Enables/disables the Settings button used to configure containers
+    */
+   readBrowserContainersCheckbox: function ()
+   {
+     var pref = document.getElementById("privacy.userContext.enabled");
+     var settings = document.getElementById("browserContainersSettings");
+
+     settings.disabled = !pref.value;
    }
 
 };
--- a/browser/components/preferences/in-content/privacy.xul
+++ b/browser/components/preferences/in-content/privacy.xul
@@ -284,13 +284,25 @@
 
 <!-- Containers -->
 <groupbox id="browserContainersGroup" data-category="panePrivacy" hidden="true">
   <vbox id="browserContainersbox" hidden="true">
     <caption><label>&browserContainersHeader.label;
       <label id="browserContainersLearnMore" class="text-link"
              value="&browserContainersLearnMore.label;"/>
     </label></caption>
-    <checkbox id="browserContainersCheckbox"
-              label="&browserContainersEnabled.label;"
-              accesskey="&browserContainersEnabled.accesskey;" />
+    <hbox align="start">
+      <vbox>
+        <checkbox id="browserContainersCheckbox"
+                  label="&browserContainersEnabled.label;"
+                  accesskey="&browserContainersEnabled.accesskey;"
+                  preference="privacy.userContext.enabled"
+                  onsyncfrompreference="return gPrivacyPane.readBrowserContainersCheckbox();"/>
+      </vbox>
+      <spacer flex="1"/>
+      <vbox>
+        <button id="browserContainersSettings"
+                label="&browserContainersSettings.label;"
+                accesskey="&browserContainersSettings.accesskey;"/>
+      </vbox>
+    </hbox>
   </vbox>
 </groupbox>
--- a/browser/components/preferences/jar.mn
+++ b/browser/components/preferences/jar.mn
@@ -15,15 +15,17 @@ browser.jar:
     content/browser/preferences/donottrack.xul
 *   content/browser/preferences/fonts.xul
     content/browser/preferences/fonts.js
     content/browser/preferences/handlers.xml
     content/browser/preferences/handlers.css
 *   content/browser/preferences/languages.xul
     content/browser/preferences/languages.js
     content/browser/preferences/permissions.xul
+    content/browser/preferences/containers.xul
+    content/browser/preferences/containers.js
     content/browser/preferences/permissions.js
     content/browser/preferences/sanitize.xul
     content/browser/preferences/sanitize.js
     content/browser/preferences/selectBookmark.xul
     content/browser/preferences/selectBookmark.js
     content/browser/preferences/translation.xul
     content/browser/preferences/translation.js
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/preferences/containers.dtd
@@ -0,0 +1,24 @@
+<!-- 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/. -->
+
+<!ENTITY label.label          "Name">
+<!ENTITY addButton.label      "Add New Container">
+<!ENTITY addButton.accesskey  "A">
+<!-- &#171; is &laquo; however it's not defined in XML -->
+<!ENTITY backLink.label       "&#171; Go Back to Privacy">
+
+<!ENTITY window.title         "Add New Container">
+<!ENTITY window.width         "45em">
+
+<!ENTITY name.label           "Name:">
+<!ENTITY name.accesskey       "N">
+<!ENTITY icon.label           "Icon:">
+<!ENTITY icon.accesskey       "I">
+<!ENTITY color.label          "Color:">
+<!ENTITY color.accesskey      "o">
+<!ENTITY windowClose.key      "w">
+
+<!ENTITY button.ok.label      "Done">
+<!ENTITY button.ok.accesskey  "D">
+
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/preferences/containers.properties
@@ -0,0 +1,31 @@
+# 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/.
+
+containers.removeButton = Remove
+containers.preferencesButton = Preferences
+containers.colorHeading = Color:
+containers.labelMinWidth = 4rem
+containers.nameLabel = Name:
+containers.namePlaceholder = Enter a container name
+containers.submitButton = Done
+containers.iconHeading = Icon:
+containers.updateContainerTitle = %S Container Preferences
+
+containers.blue.label = Blue
+containers.turquoise.label = Turquoise
+containers.green.label = Green
+containers.yellow.label = Yellow
+containers.orange.label = Orange
+containers.red.label = Red
+containers.pink.label = Pink
+containers.purple.label = Purple
+
+containers.fingerprint.label = Fingerprint
+containers.briefcase.label = Briefcase
+# LOCALIZATION NOTE (containers.dollar.label)
+# String represents a money sign but currently uses a dollar sign so don't change to local currency
+# See Bug 1291672
+containers.dollar.label = Dollar sign
+containers.cart.label = Shopping cart
+containers.circle.label = Dot
--- a/browser/locales/en-US/chrome/browser/preferences/preferences.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/preferences.dtd
@@ -14,15 +14,16 @@
 <!ENTITY  prefWinMinSize.styleGNOME     "width: 45.5em; min-height: 40.5em;">
 
 <!ENTITY  paneGeneral.title       "General">
 <!ENTITY  paneTabs.title          "Tabs">
 <!ENTITY  paneSearch.title        "Search">
 <!ENTITY  paneContent.title       "Content">
 <!ENTITY  paneApplications.title  "Applications">
 <!ENTITY  panePrivacy.title       "Privacy">
+<!ENTITY  paneContainers.title    "Container Tabs">
 <!ENTITY  paneSecurity.title      "Security">
 <!ENTITY  paneAdvanced.title      "Advanced">
 
 <!-- LOCALIZATION NOTE (paneSync.title): This should match syncBrand.shortName.label in ../syncBrand.dtd -->
 <!ENTITY  paneSync.title          "Sync">
 
 <!ENTITY  helpButton.label        "Help">
--- a/browser/locales/en-US/chrome/browser/preferences/privacy.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/privacy.dtd
@@ -104,8 +104,10 @@
 
 <!ENTITY  clearOnCloseSettings.label     "Settingsā€¦">
 <!ENTITY  clearOnCloseSettings.accesskey "t">
 
 <!ENTITY  browserContainersHeader.label         "Container Tabs">
 <!ENTITY  browserContainersLearnMore.label      "Learn more">
 <!ENTITY  browserContainersEnabled.label        "Enable Container Tabs">
 <!ENTITY  browserContainersEnabled.accesskey    "n">
+<!ENTITY  browserContainersSettings.label        "Settingsā€¦">
+<!ENTITY  browserContainersSettings.accesskey    "i">
--- a/browser/locales/jar.mn
+++ b/browser/locales/jar.mn
@@ -70,18 +70,20 @@
     locale/browser/preferences/donottrack.dtd         (%chrome/browser/preferences/donottrack.dtd)
     locale/browser/preferences/applications.dtd       (%chrome/browser/preferences/applications.dtd)
     locale/browser/preferences/fonts.dtd              (%chrome/browser/preferences/fonts.dtd)
     locale/browser/preferences/main.dtd               (%chrome/browser/preferences/main.dtd)
     locale/browser/preferences/languages.dtd          (%chrome/browser/preferences/languages.dtd)
     locale/browser/preferences/permissions.dtd        (%chrome/browser/preferences/permissions.dtd)
     locale/browser/preferences/preferences.dtd        (%chrome/browser/preferences/preferences.dtd)
     locale/browser/preferences/preferences.properties (%chrome/browser/preferences/preferences.properties)
+    locale/browser/preferences/containers.properties  (%chrome/browser/preferences/containers.properties)
     locale/browser/preferences/privacy.dtd            (%chrome/browser/preferences/privacy.dtd)
     locale/browser/preferences/security.dtd           (%chrome/browser/preferences/security.dtd)
+    locale/browser/preferences/containers.dtd         (%chrome/browser/preferences/containers.dtd)
     locale/browser/preferences/sync.dtd               (%chrome/browser/preferences/sync.dtd)
     locale/browser/preferences/tabs.dtd               (%chrome/browser/preferences/tabs.dtd)
     locale/browser/preferences/search.dtd             (%chrome/browser/preferences/search.dtd)
     locale/browser/preferences/translation.dtd        (%chrome/browser/preferences/translation.dtd)
     locale/browser/syncBrand.dtd                (%chrome/browser/syncBrand.dtd)
     locale/browser/syncSetup.dtd                (%chrome/browser/syncSetup.dtd)
     locale/browser/syncSetup.properties         (%chrome/browser/syncSetup.properties)
     locale/browser/syncGenericChange.properties         (%chrome/browser/syncGenericChange.properties)
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/incontentprefs/containers.css
@@ -0,0 +1,32 @@
+/* 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/. */
+
+%include ../../../components/contextualidentity/content/usercontext.css
+
+.container-header-links {
+  margin-block-end: 15px;
+}
+
+[data-identity-icon] {
+  margin: 0;
+  margin-inline-end: 16px;
+}
+
+#containersView {
+  border: 0 none;
+  background: transparent;
+}
+
+#containersView richlistitem {
+  margin: 0px;
+  margin-inline-end: 8px;
+  padding: 0;
+  padding-block-end: 8px;
+  border-block-end: 1px solid var(--in-content-header-border-color);
+}
+
+#containersView richlistitem:last-of-type {
+  border-block-end: 0 none;
+  margin-block-end: 8px;
+}
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -72,16 +72,18 @@
 * skin/classic/browser/tracking-protection-16.svg              (../shared/identity-block/tracking-protection-16.svg)
   skin/classic/browser/newtab/close.png                        (../shared/newtab/close.png)
   skin/classic/browser/newtab/controls.svg                     (../shared/newtab/controls.svg)
   skin/classic/browser/newtab/whimsycorn.png                   (../shared/newtab/whimsycorn.png)
   skin/classic/browser/panel-icons.svg                         (../shared/panel-icons.svg)
   skin/classic/browser/preferences/in-content/favicon.ico      (../shared/incontentprefs/favicon.ico)
   skin/classic/browser/preferences/in-content/icons.svg        (../shared/incontentprefs/icons.svg)
   skin/classic/browser/preferences/in-content/search.css       (../shared/incontentprefs/search.css)
+* skin/classic/browser/preferences/in-content/containers.css   (../shared/incontentprefs/containers.css)
+* skin/classic/browser/preferences/containers.css              (../shared/preferences/containers.css)
   skin/classic/browser/fxa/default-avatar.svg                  (../shared/fxa/default-avatar.svg)
   skin/classic/browser/fxa/logo.png                            (../shared/fxa/logo.png)
   skin/classic/browser/fxa/logo@2x.png                         (../shared/fxa/logo@2x.png)
   skin/classic/browser/fxa/sync-illustration.png               (../shared/fxa/sync-illustration.png)
   skin/classic/browser/fxa/sync-illustration@2x.png            (../shared/fxa/sync-illustration@2x.png)
   skin/classic/browser/fxa/sync-illustration.svg               (../shared/fxa/sync-illustration.svg)
   skin/classic/browser/fxa/android.png                         (../shared/fxa/android.png)
   skin/classic/browser/fxa/android@2x.png                      (../shared/fxa/android@2x.png)
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/preferences/containers.css
@@ -0,0 +1,53 @@
+/* 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/. */
+
+%include ../../../components/contextualidentity/content/usercontext.css
+
+:root {
+  --preference-selected-color: #0996f8;
+  --preference-unselected-color: #333;
+  --preference-active-color: #858585;
+}
+
+radiogroup {
+  display: flex;
+  margin-inline-start: 0.35rem;
+}
+
+radio {
+  flex: auto;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  -moz-user-select: none;
+  outline: 2px solid transparent;
+  outline-offset: 4px;
+  -moz-outline-radius: 100%;
+  min-block-size: 24px;
+  min-inline-size: 24px;
+  border-radius: 50%;
+  padding: 2px;
+  margin: 10px;
+}
+
+.icon-buttons > radio > [data-identity-icon] {
+  fill: #4d4d4d;
+}
+
+radio > [data-identity-icon] {
+  inline-size: 22px;
+  block-size: 22px;
+}
+
+radio[selected=true] {
+  outline-color: var(--preference-unselected-color);
+}
+
+radio[focused=true] {
+  outline-color: var(--preference-selected-color);
+}
+
+radio:hover:active {
+  outline-color: var(--preference-active-color);
+}