Bug 865916: create a Character Encoding widget and subview. r=gkruitbosch,dao
authorMike de Boer <mdeboer@mozilla.com>
Tue, 20 Aug 2013 14:52:29 +0200
changeset 143717 5a7f22213ce9280f30856e039ce0c067c1a0bc51
parent 143716 3d94af4f67dfc5c2346d1666a90f3143a7d712f0
child 143718 d23d5cda96669e2b8169f669c399e7cb39f3ac96
push id345
push usermdeboer@mozilla.com
push dateTue, 20 Aug 2013 12:57:45 +0000
treeherderux@5a7f22213ce9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgkruitbosch, dao
bugs865916
milestone26.0a1
Bug 865916: create a Character Encoding widget and subview. r=gkruitbosch,dao
browser/components/customizableui/content/panelUI.inc.xul
browser/components/customizableui/content/panelUI.js
browser/components/customizableui/src/CustomizableUI.jsm
browser/components/customizableui/src/CustomizableWidgets.jsm
browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties
browser/themes/linux/browser.css
browser/themes/osx/browser.css
browser/themes/shared/browser.inc
browser/themes/shared/customizableui/panelUIOverlay.inc.css
browser/themes/shared/menupanel.inc.css
browser/themes/shared/toolbarbuttons.inc.css
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -101,16 +101,30 @@
       <vbox id="PanelUI-helpItems"/>
     </panelview>
 
     <panelview id="PanelUI-developer" flex="1">
       <label value="&webDeveloperMenu.label;"/>
       <vbox id="PanelUI-developerItems"/>
     </panelview>
 
+    <panelview id="PanelUI-characterEncodingView" flex="1">
+      <label value="&charsetMenu.label;"/>
+      <toolbarbutton label="&charsetCustomize.label;"
+                     oncommand="PanelUI.onCharsetCustomizeCommand();"/>
+
+      <vbox id="PanelUI-characterEncodingView-customlist"
+            class="PanelUI-characterEncodingView-list"/>
+      <vbox>
+        <label value="&charsetMenuAutodet.label;"/>
+        <vbox id="PanelUI-characterEncodingView-autodetect"
+              class="PanelUI-characterEncodingView-list"/>
+      </vbox>
+    </panelview>
+
   </panelmultiview>
   <popupset id="customizationContextMenus">
     <menupopup id="customizationContextMenu">
       <menuitem oncommand="gCustomizeMode.addToToolbar(document.popupNode)"
                 accesskey="&customizeMenu.addToToolbar.accesskey;"
                 label="&customizeMenu.addToToolbar.label;"/>
       <menuitem oncommand="gCustomizeMode.removeFromPanel(document.popupNode)"
                 accesskey="&customizeMenu.removeFromMenu.accesskey;"
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -275,16 +275,27 @@ const PanelUI = {
    * so that the panel knows if and when to close itself.
    */
   onCommandHandler: function(aEvent) {
     if (!aEvent.originalTarget.hasAttribute("noautoclose")) {
       PanelUI.hide();
     }
   },
 
+  /**
+   * Open a dialog window that allow the user to customize listed character sets.
+   */
+  onCharsetCustomizeCommand: function() {
+    this.hide();
+    window.openDialog("chrome://global/content/customizeCharset.xul",
+                      "PrefWindow",
+                      "chrome,modal=yes,resizable=yes",
+                      "browser");
+  },
+
   /** 
    * Signal that we're about to make a lot of changes to the contents of the
    * panels all at once. For performance, we ignore the mutations.
    */
   beginBatchUpdate: function() {
     this._ensureEventListenersAdded();
     this.multiView.ignoreMutations = true;
   },
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -124,32 +124,41 @@ let gModuleName = "[CustomizableUI]";
 let CustomizableUIInternal = {
   initialize: function() {
     LOG("Initializing");
 
     this.addListener(this);
     this._defineBuiltInWidgets();
     this.loadSavedState();
 
+    let panelPlacements = [
+      "edit-controls",
+      "zoom-controls",
+      "new-window-button",
+      "privatebrowsing-button",
+      "save-page-button",
+      "print-button",
+      "history-panelmenu",
+      "fullscreen-button",
+      "find-button",
+      "preferences-button",
+      "add-ons-button",
+    ];
+    let showCharacterEncoding = Services.prefs.getComplexValue(
+      "browser.menu.showCharacterEncoding",
+      Ci.nsIPrefLocalizedString
+    ).data;
+    if (showCharacterEncoding == "true") {
+      panelPlacements.push("characterencoding-button");
+    }
+
     this.registerArea(CustomizableUI.AREA_PANEL, {
       anchor: "PanelUI-menu-button",
       type: CustomizableUI.TYPE_MENU_PANEL,
-      defaultPlacements: [
-        "edit-controls",
-        "zoom-controls",
-        "new-window-button",
-        "privatebrowsing-button",
-        "save-page-button",
-        "print-button",
-        "history-panelmenu",
-        "fullscreen-button",
-        "find-button",
-        "preferences-button",
-        "add-ons-button",
-      ]
+      defaultPlacements: panelPlacements
     });
     this.registerArea(CustomizableUI.AREA_NAVBAR, {
       legacy: true,
       anchor: "nav-bar-overflow-button",
       type: CustomizableUI.TYPE_TOOLBAR,
       overflowable: true,
       defaultPlacements: [
         "urlbar-container",
@@ -1510,16 +1519,17 @@ let CustomizableUIInternal = {
       return false;
     }
     return gAreas.get(aArea).has("legacy");
   },
 
   //XXXunf Log some warnings here, when the data provided isn't up to scratch.
   normalizeWidget: function(aData, aSource) {
     let widget = {
+      implementation: aData,
       source: aSource || "addon",
       instances: new Map(),
       currentArea: null,
       removable: false,
       nooverflow: false,
       defaultArea: null,
       allowedAreas: [],
       shortcut: null,
@@ -1527,16 +1537,19 @@ let CustomizableUIInternal = {
       showInPrivateBrowsing: true,
     };
 
     if (typeof aData.id != "string" || !/^[a-z0-9-_]{1,}$/i.test(aData.id)) {
       ERROR("Given an illegal id in normalizeWidget: " + aData.id);
       return null;
     }
 
+    delete widget.implementation.currentArea;
+    widget.implementation.__defineGetter__("currentArea", function() widget.currentArea);
+
     const kReqStringProps = ["id"];
     for (let prop of kReqStringProps) {
       if (typeof aData[prop] != "string") {
         ERROR("Missing required property '" + prop + "' in normalizeWidget: "
               + aData.id);
         return null;
       }
       widget[prop] = aData[prop];
@@ -1568,51 +1581,65 @@ let CustomizableUIInternal = {
     if ("type" in aData && gSupportedWidgetTypes.has(aData.type)) {
       widget.type = aData.type;
     } else {
       widget.type = "button";
     }
 
     widget.disabled = aData.disabled === true;
 
-    widget.onClick = typeof aData.onClick == "function" ? aData.onClick : null;
-
-    widget.onCreated = typeof aData.onCreated == "function" ? aData.onCreated : null;
+    this.wrapWidgetEventHandler("onClick", widget);
+    this.wrapWidgetEventHandler("onCreated", widget);
 
     if (widget.type == "button") {
       widget.onCommand = typeof aData.onCommand == "function" ?
                            aData.onCommand :
                            null;
     } else if (widget.type == "view") {
       if (typeof aData.viewId != "string") {
         ERROR("Expected a string for widget " + widget.id + " viewId, but got "
               + aData.viewId);
         return null;
       }
       widget.viewId = aData.viewId;
 
-      widget.onViewShowing = typeof aData.onViewShowing == "function" ?
-                                 aData.onViewShowing :
-                                 null;
-      widget.onViewHiding = typeof aData.onViewHiding == "function" ? 
-                                 aData.onViewHiding :
-                                 null;
+      this.wrapWidgetEventHandler("onViewShowing", widget);
+      this.wrapWidgetEventHandler("onViewHiding", widget);
     } else if (widget.type == "custom") {
-      widget.onBuild = typeof aData.onBuild == "function" ?
-                                 aData.onBuild :
-                                 null;
+      this.wrapWidgetEventHandler("onBuild", widget);
     }
 
     if (gPalette.has(widget.id)) {
       return null;
     }
 
     return widget;
   },
 
+  wrapWidgetEventHandler: function(aEventName, aWidget) {
+    if (typeof aWidget.implementation[aEventName] != "function") {
+      aWidget[aEventName] = null;
+      return;
+    }
+    aWidget[aEventName] = function(...aArgs) {
+      // Wrap inside a try...catch to properly log errors, until bug 862627 is
+      // fixed, which in turn might help bug 503244.
+      try {
+        // Don't copy the function to the normalized widget object, instead
+        // keep it on the original object provided to the API so that
+        // additional methods can be implemented and used by the event
+        // handlers.
+        return aWidget.implementation[aEventName].apply(aWidget.implementation,
+                                                        aArgs);
+      } catch (e) {
+        Cu.reportError(e);
+      }
+    };
+  },
+
   destroyWidget: function(aWidgetId) {
     let widget = gPalette.get(aWidgetId);
     if (!widget) {
       return;
     }
 
     // Remove it from the default placements of an area if it was added there:
     if (widget.defaultArea) {
@@ -1943,17 +1970,17 @@ this.CustomizableUI = {
   isSpecialWidget: function(aWidgetId) {
     return CustomizableUIInternal.isSpecialWidget(aWidgetId);
   },
   addPanelCloseListeners: function(aPanel) {
     CustomizableUIInternal.addPanelCloseListeners(aPanel);
   },
   removePanelCloseListeners: function(aPanel) {
     CustomizableUIInternal.removePanelCloseListeners(aPanel);
-  },
+  }
 };
 Object.freeze(this.CustomizableUI);
 
 
 /**
  * All external consumers of widgets are really interacting with these wrappers
  * which provide a common interface.
  */
--- a/browser/components/customizableui/src/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/src/CustomizableWidgets.jsm
@@ -7,16 +7,19 @@ const {classes: Cc, interfaces: Ci, util
 
 this.EXPORTED_SYMBOLS = ["CustomizableWidgets"];
 
 Cu.import("resource:///modules/CustomizableUI.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
   "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "CharsetManager",
+                                   "@mozilla.org/charset-converter-manager;1",
+                                   "nsICharsetConverterManager");
 
 const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 const kPrefCustomizationDebug = "browser.uiCustomization.debug";
 const kWidePanelItemClass = "panel-combined-item";
 
 let gModuleName = "[CustomizableWidgets]";
 #include logging.js
 
@@ -584,9 +587,219 @@ const CustomizableWidgets = [{
     onCreated: function(node) {
       let win = node.ownerDocument.defaultView;
       let selectedBrowser = win.gBrowser.selectedBrowser;
       let feeds = selectedBrowser && selectedBrowser.feeds;
       if (!feeds || !feeds.length) {
         node.setAttribute("disabled", "true");
       }
     }
+  }, {
+    id: "characterencoding-button",
+    type: "view",
+    viewId: "PanelUI-characterEncodingView",
+    removable: true,
+    defaultArea: CustomizableUI.AREA_PANEL,
+    allowedAreas: [CustomizableUI.AREA_PANEL],
+    maybeDisableMenu: function(aDocument) {
+      let window = aDocument.defaultView;
+      return !(window.gBrowser &&
+               window.gBrowser.docShell &&
+               window.gBrowser.docShell.mayEnableCharacterEncodingMenu);
+    },
+    getCharsetList: function(aSection, aDocument) {
+      let currCharset = aDocument.defaultView.content.document.characterSet;
+
+      let list = "";
+      try {
+        let pref = "intl.charsetmenu.browser." + aSection;
+        list = Services.prefs.getComplexValue(pref,
+                                              Ci.nsIPrefLocalizedString).data;
+      } catch (e) {}
+
+      list = list.trim();
+      if (!list)
+        return [];
+
+      list = list.split(",");
+
+      let items = [];
+      for (let charset of list) {
+        charset = charset.trim();
+
+        let notForBrowser = false;
+        try {
+          notForBrowser = CharsetManager.getCharsetData(charset,
+                                                        "notForBrowser");
+        } catch (e) {}
+
+        if (notForBrowser)
+          continue;
+
+        let title = charset;
+        try {
+          title = CharsetManager.getCharsetTitle(charset);
+        } catch (e) {}
+
+        items.push({value: charset, name: title, current: charset == currCharset});
+      }
+
+      return items;
+    },
+    getAutoDetectors: function(aDocument) {
+      let detectorEnum = CharsetManager.GetCharsetDetectorList();
+      let currDetector;
+      try {
+        currDetector = Services.prefs.getComplexValue(
+          "intl.charset.detector", Ci.nsIPrefLocalizedString).data;
+      } catch (e) {}
+      if (!currDetector)
+        currDetector = "off";
+      currDetector = "chardet." + currDetector;
+
+      let items = [];
+
+      while (detectorEnum.hasMore()) {
+        let detector = detectorEnum.getNext();
+
+        let title = detector;
+        try {
+          title = CharsetManager.getCharsetTitle(detector);
+        } catch (e) {}
+
+        items.push({value: detector, name: title, current: detector == currDetector});
+      }
+
+      items.sort((aItem1, aItem2) => {
+        return aItem1.name.localeCompare(aItem2.name);
+      });
+
+      return items;
+    },
+    populateList: function(aDocument, aContainerId, aSection) {
+      let containerElem = aDocument.getElementById(aContainerId);
+
+      while (containerElem.firstChild) {
+        containerElem.removeChild(containerElem.firstChild);
+      }
+
+      containerElem.addEventListener("command", this.onCommand, false);
+
+      let list = [];
+      if (aSection == "autodetect") {
+        list = this.getAutoDetectors(aDocument);
+      } else if (aSection == "browser") {
+        let staticList = this.getCharsetList("static", aDocument);
+        let cacheList = this.getCharsetList("cache", aDocument);
+        // Combine lists, and de-duplicate.
+        let checkedIn = new Set();
+        for (let item of staticList.concat(cacheList)) {
+          let itemName = item.name.toLowerCase();
+          if (!checkedIn.has(itemName)) {
+            list.push(item);
+            checkedIn.add(itemName);
+          }
+        }
+      }
+
+      // Update the appearance of the buttons when it's not possible to
+      // customize encoding.
+      let disabled = this.maybeDisableMenu(aDocument);
+      for (let item of list) {
+        let elem = aDocument.createElementNS(kNSXUL, "toolbarbutton");
+        elem.setAttribute("label", item.name);
+        elem.section = aSection;
+        elem.value = item.value;
+        if (item.current)
+          elem.setAttribute("current", "true");
+        if (disabled)
+          elem.setAttribute("disabled", "true");
+        containerElem.appendChild(elem);
+      }
+    },
+    onViewShowing: function(aEvent) {
+      let document = aEvent.target.ownerDocument;
+
+      this.populateList(document,
+                        "PanelUI-characterEncodingView-customlist",
+                        "browser");
+      this.populateList(document,
+                        "PanelUI-characterEncodingView-autodetect",
+                        "autodetect");
+    },
+    onCommand: function(aEvent) {
+      let node = aEvent.target;
+      if (!node.hasAttribute || !node.section) {
+        return;
+      }
+
+      CustomizableUI.hidePanelForNode(node);
+      let window = node.ownerDocument.defaultView;
+      let section = node.section;
+      let value = node.value;
+
+      // The behavior as implemented here is directly based off of the
+      // `MultiplexHandler()` method in browser.js.
+      if (section == "browser") {
+        window.BrowserSetForcedCharacterSet(value);
+      } else if (section == "autodetect") {
+        value = value.replace(/^chardet\./, "");
+        if (value == "off") {
+          value = "";
+        }
+        // Set the detector pref.
+        try {
+          let str = Cc["@mozilla.org/supports-string;1"]
+                      .createInstance(Ci.nsISupportsString);
+          str.data = value;
+          Services.prefs.setComplexValue("intl.charset.detector", Ci.nsISupportsString, str);
+        } catch (e) {
+          Cu.reportError("Failed to set the intl.charset.detector preference.");
+        }
+        // Prepare a browser page reload with a changed charset.
+        window.BrowserCharsetReload();
+      }
+    },
+    onCreated: function(aNode) {
+      const kPanelId = "PanelUI-popup";
+      let document = aNode.ownerDocument;
+
+      let updateButton = () => {
+        if (this.maybeDisableMenu(document))
+          aNode.setAttribute("disabled", "true");
+        else
+          aNode.removeAttribute("disabled");
+      };
+
+      if (this.currentArea == CustomizableUI.AREA_PANEL) {
+        let panel = document.getElementById(kPanelId);
+        panel.addEventListener("popupshowing", updateButton);
+      }
+
+      let listener = {
+        onWidgetAdded: (aWidgetId, aArea) => {
+          if (aWidgetId != this.id)
+            return;
+          if (aArea == CustomizableUI.AREA_PANEL) {
+            let panel = document.getElementById(kPanelId);
+            panel.addEventListener("popupshowing", updateButton);
+          }
+        },
+        onWidgetRemoved: (aWidgetId, aPrevArea) => {
+          if (aWidgetId != this.id)
+            return;
+          if (aPrevArea == CustomizableUI.AREA_PANEL) {
+            let panel = document.getElementById(kPanelId);
+            panel.removeEventListener("popupshowing", updateButton);
+          }
+        },
+        onWidgetInstanceRemoved: (aWidgetId, aDoc) => {
+          if (aWidgetId != this.id || aDoc != document)
+            return;
+
+          CustomizableUI.removeListener(listener);
+          let panel = aDoc.getElementById(kPanelId);
+          panel.removeEventListener("popupshowing", updateButton);
+        }
+      };
+      CustomizableUI.addListener(listener);
+    }
   }];
--- a/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties
+++ b/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties
@@ -58,10 +58,15 @@ cut-button.label = Cut
 cut-button.tooltiptext = Cut
 
 copy-button.label = Copy
 copy-button.tooltiptext = Copy
 
 paste-button.label = Paste
 paste-button.tooltiptext = Paste
 
+# LOCALIZATION NOTE (feed-button.tooltiptext): Use the unicode ellipsis char,
+# \u2026, or use "..." if \u2026 doesn't suit traditions in your locale.
 feed-button.label = Subscribe
 feed-button.tooltiptext = Subscribe to this page…
+
+characterencoding-button.label = Character Encoding
+characterencoding-button.tooltiptext = Character encoding
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -710,28 +710,28 @@ toolbar > .customization-target > toolba
 }
 #home-button.bookmark-item {
   list-style-image: url("moz-icon://stock/gtk-home?size=menu");
 }
 #home-button.bookmark-item[disabled="true"] {
   list-style-image: url("moz-icon://stock/gtk-home?size=menu&state=disabled");
 }
 
-#characterencoding-panelmenu[customizableui-areatype="toolbar"],
+#characterencoding-button[customizableui-areatype="toolbar"],
 #developer-button[customizableui-areatype="toolbar"] {
   list-style-image: url(chrome://browser/skin/menuPanel.png);
 }
 
-#characterencoding-panelmenu[customizableui-areatype="toolbar"] > .toolbarbutton-icon,
+#characterencoding-button[customizableui-areatype="toolbar"] > .toolbarbutton-icon,
 #developer-button[customizableui-areatype="toolbar"] > .toolbarbutton-icon {
   height: 24px;
 }
 
-#characterencoding-panelmenu[customizableui-areatype="toolbar"] {
-  -moz-image-region: rect(0px, 216px, 24px, 192px);
+#characterencoding-button[customizableui-areatype="toolbar"] {
+  -moz-image-region: rect(0px, 480px, 32px, 448px);
 }
 
 #developer-button[customizableui-areatype="toolbar"] {
   -moz-image-region: rect(0px, 736px, 32px, 704px);
 }
 
 /* Menu panel buttons */
 
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -470,20 +470,24 @@ toolbarbutton.bookmark-item > menupopup 
   #feed-button@toolbarButtonPressed@ {
     -moz-image-region: rect(18px, 288px, 36px, 270px);
   }
 
   #share-button@toolbarButtonPressed@ {
     -moz-image-region: rect(18px, 306px, 36px, 288px);
   }
 
-  #charset-button@toolbarButtonPressed@ {
+  #characterencoding-button@toolbarButtonPressed@ {
     -moz-image-region: rect(18px, 324px, 36px, 306px);
   }
 
+  #characterencoding-button[open] {
+    -moz-image-region: rect(36px, 324px, 54px, 306px);
+  }
+
   #new-window-button@toolbarButtonPressed@ {
     -moz-image-region: rect(18px, 342px, 36px, 324px);
   }
 
   #new-tab-button@toolbarButtonPressed@ {
     -moz-image-region: rect(18px, 360px, 36px, 342px);
   }
 
@@ -691,16 +695,28 @@ toolbarbutton.bookmark-item > menupopup 
   #feed-button[customizableui-areatype="toolbar"] {
     -moz-image-region: rect(0, 576px, 36px, 540px);
   }
 
   #feed-button[customizableui-areatype="toolbar"]:hover:active:not([disabled="true"]) {
     -moz-image-region: rect(36px, 576px, 72px, 540px);
   }
 
+  #characterencoding-button[customizableui-areatype="toolbar"] {
+    -moz-image-region: rect(0, 648px, 36px, 612px);
+  }
+
+  #characterencoding-button[customizableui-areatype="toolbar"]:hover:active:not([disabled="true"]) {
+    -moz-image-region: rect(36px, 648px, 72px, 612px);
+  }
+
+  #characterencoding-button[customizableui-areatype="toolbar"][open] {
+    -moz-image-region: rect(72px, 648px, 108px, 612px);
+  }
+
   #new-window-button[customizableui-areatype="toolbar"] {
     -moz-image-region: rect(0, 684px, 36px, 648px);
   }
 
   #new-window-button[customizableui-areatype="toolbar"]:hover:active:not([disabled="true"]) {
     -moz-image-region: rect(36px, 684px, 72px, 648px);
   }
 
@@ -913,16 +929,21 @@ toolbarbutton.bookmark-item > menupopup 
     -moz-image-region: rect(0px, 832px, 64px, 768px);
   }
 
   #social-share-button[customizableui-areatype="menu-panel"],
   toolbarpaletteitem[place="palette"] > #social-share-button {
     -moz-image-region: rect(0px, 896px, 64px, 832px);
   }
 
+  #characterencoding-button[customizableui-areatype="menu-panel"],
+  toolbarpaletteitem[place="palette"] > #characterencoding-button {
+    -moz-image-region: rect(0, 960px, 64px, 896px);
+  }
+
   #new-window-button[customizableui-areatype="menu-panel"],
   toolbarpaletteitem[place="palette"] > #new-window-button {
     -moz-image-region: rect(0px, 1024px, 64px, 960px);
   }
 
   #new-tab-button[customizableui-areatype="menu-panel"],
   toolbarpaletteitem[place="palette"] > #new-tab-button {
     -moz-image-region: rect(0px, 1088px, 64px, 1024px);
--- a/browser/themes/shared/browser.inc
+++ b/browser/themes/shared/browser.inc
@@ -1,3 +1,3 @@
 %filter substitution
 
-%define primaryToolbarButtons #back-button, #forward-button, #home-button, #print-button, #downloads-button, #downloads-indicator, #bookmarks-menu-button, #new-tab-button, #new-window-button, #cut-button, #copy-button, #paste-button, #fullscreen-button, #zoom-out-button, #zoom-reset-button, #zoom-in-button, #sync-button, #feed-button, #alltabs-button, #tabview-button, #webrtc-status-button, #social-share-button, #open-file-button, #find-button, #developer-button, #preferences-button, #privatebrowsing-button, #save-page-button, #add-ons-button, #history-panelmenu, #nav-bar-overflow-button, #PanelUI-menu-button
+%define primaryToolbarButtons #back-button, #forward-button, #home-button, #print-button, #downloads-button, #downloads-indicator, #bookmarks-menu-button, #new-tab-button, #new-window-button, #cut-button, #copy-button, #paste-button, #fullscreen-button, #zoom-out-button, #zoom-reset-button, #zoom-in-button, #sync-button, #feed-button, #alltabs-button, #tabview-button, #webrtc-status-button, #social-share-button, #open-file-button, #find-button, #developer-button, #preferences-button, #privatebrowsing-button, #save-page-button, #add-ons-button, #history-panelmenu, #nav-bar-overflow-button, #PanelUI-menu-button, #characterencoding-button
--- a/browser/themes/shared/customizableui/panelUIOverlay.inc.css
+++ b/browser/themes/shared/customizableui/panelUIOverlay.inc.css
@@ -347,8 +347,22 @@ toolbarbutton.panel-multiview-anchor {
 #PanelUI-contents #cut-button:-moz-locale-dir(rtl),
 #PanelUI-contents #paste-button:-moz-locale-dir(ltr),
 #PanelUI-contents #zoom-out-button:-moz-locale-dir(rtl),
 #PanelUI-contents #zoom-in-button:-moz-locale-dir(ltr) {
   border-top-left-radius: 2px;
   border-bottom-left-radius: 2px;
 }
 
+.PanelUI-characterEncodingView-list > toolbarbutton[current] {
+  -moz-padding-start: 2px;
+}
+
+.PanelUI-characterEncodingView-list > toolbarbutton[current] > .toolbarbutton-text,
+#customizationui-widget-panel .PanelUI-characterEncodingView-list > toolbarbutton[current] > .toolbarbutton-text {
+  -moz-padding-start: 0px;
+}
+
+.PanelUI-characterEncodingView-list > toolbarbutton[current]::before {
+  content: "✓";
+  display: -moz-box;
+  width: 12px;
+}
--- a/browser/themes/shared/menupanel.inc.css
+++ b/browser/themes/shared/menupanel.inc.css
@@ -55,16 +55,21 @@ toolbarpaletteitem[place="palette"] > #f
   -moz-image-region: rect(0px, 416px, 32px, 384px);
 }
 
 #social-share-button[customizableui-areatype="menu-panel"],
 toolbarpaletteitem[place="palette"] > #social-share-button {
   -moz-image-region: rect(0px, 448px, 32px, 416px);
 }
 
+#characterencoding-button[customizableui-areatype="menu-panel"],
+toolbarpaletteitem[place="palette"] > #characterencoding-button {
+  -moz-image-region: rect(0px, 480px, 32px, 448px);
+}
+
 #new-window-button[customizableui-areatype="menu-panel"],
 toolbarpaletteitem[place="palette"] > #new-window-button {
   -moz-image-region: rect(0px, 512px, 32px, 480px);
 }
 
 #new-tab-button[customizableui-areatype="menu-panel"],
 toolbarpaletteitem[place="palette"] > #new-tab-button {
   -moz-image-region: rect(0px, 544px, 32px, 512px);
--- a/browser/themes/shared/toolbarbuttons.inc.css
+++ b/browser/themes/shared/toolbarbuttons.inc.css
@@ -61,16 +61,20 @@
 #sync-button[customizableui-areatype="toolbar"] {
   -moz-image-region: rect(0, 270px, 18px, 252px);
 }
 
 #feed-button[customizableui-areatype="toolbar"] {
   -moz-image-region: rect(0, 288px, 18px, 270px);
 }
 
+#characterencoding-button[customizableui-areatype="toolbar"]{
+  -moz-image-region: rect(0, 324px, 18px, 306px);
+}
+
 #new-window-button[customizableui-areatype="toolbar"] {
   -moz-image-region: rect(0, 342px, 18px, 324px);
 }
 
 #new-tab-button[customizableui-areatype="toolbar"] {
   -moz-image-region: rect(0, 360px, 18px, 342px);
 }