Bug 1491562 - Port bug 1488467 to TB: Support adding and removing installed languages in Alternatives subdialog. r=jorgk
authorRichard Marti <richard.marti@gmail.com>
Sat, 15 Sep 2018 19:35:29 +0200
changeset 33165 e385d269fb2511eb0c3299f72d07903c3c230e0c
parent 33164 553c535bc354e1276a66d299ba3efd2d76b49674
child 33166 c6d36dbf6916a586fb2482c9c31f97fbaae40e04
push id387
push userclokep@gmail.com
push dateMon, 10 Dec 2018 21:30:47 +0000
reviewersjorgk
bugs1491562, 1488467
Bug 1491562 - Port bug 1488467 to TB: Support adding and removing installed languages in Alternatives subdialog. r=jorgk
mail/components/preferences/messengerLanguages.js
mail/components/preferences/messengerLanguages.xul
mail/components/preferences/subdialogs.js
mail/locales/en-US/messenger/preferences/languages.ftl
mail/themes/shared/mail/incontentprefs/aboutPreferences.css
--- a/mail/components/preferences/messengerLanguages.js
+++ b/mail/components/preferences/messengerLanguages.js
@@ -1,41 +1,40 @@
 /* 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/. */
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 class OrderedListBox {
-  constructor({richlistbox, upButton, downButton}) {
+  constructor({richlistbox, upButton, downButton, removeButton, onRemove}) {
     this.richlistbox = richlistbox;
     this.upButton = upButton;
     this.downButton = downButton;
+    this.removeButton = removeButton;
+    this.onRemove = onRemove;
 
     this.items = [];
 
     this.richlistbox.addEventListener("select", () => this.setButtonState());
     this.upButton.addEventListener("command", () => this.moveUp());
     this.downButton.addEventListener("command", () => this.moveDown());
+    this.removeButton.addEventListener("command", () => this.removeItem());
+  }
+
+  get selectedItem() {
+    return this.items[this.richlistbox.selectedIndex];
   }
 
   setButtonState() {
-    let { upButton, downButton } = this;
-    switch (this.richlistbox.selectedCount) {
-    case 0:
-      upButton.disabled = downButton.disabled = true;
-      break;
-    case 1:
-      upButton.disabled = this.richlistbox.selectedIndex == 0;
-      downButton.disabled = this.richlistbox.selectedIndex == this.richlistbox.childNodes.length - 1;
-      break;
-    default:
-      upButton.disabled = true;
-      downButton.disabled = true;
-    }
+    let {upButton, downButton, removeButton} = this;
+    let {selectedIndex, itemCount} = this.richlistbox;
+    upButton.disabled = selectedIndex == 0;
+    downButton.disabled = selectedIndex == itemCount - 1;
+    removeButton.disabled = itemCount == 1 || !this.selectedItem.canRemove;
   }
 
   moveUp() {
     let {selectedIndex} = this.richlistbox;
     if (selectedIndex == 0) {
       return;
     }
     let {items} = this;
@@ -62,64 +61,193 @@ class OrderedListBox {
     items[selectedIndex] = nextItem;
     let nextEl = document.getElementById(nextItem.id);
     let selectedEl = document.getElementById(selectedItem.id);
     this.richlistbox.insertBefore(nextEl, selectedEl);
     this.richlistbox.ensureElementIsVisible(selectedEl);
     this.setButtonState();
   }
 
+  removeItem() {
+    let {selectedIndex} = this.richlistbox;
+
+    if (selectedIndex == -1) {
+      return;
+    }
+
+    let [item] = this.items.splice(selectedIndex, 1);
+    this.richlistbox.selectedItem.remove();
+    this.richlistbox.selectedIndex = Math.min(
+      selectedIndex, this.richlistbox.itemCount - 1);
+    this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem);
+    this.onRemove(item);
+  }
+
   setItems(items) {
     this.items = items;
     this.populate();
     this.setButtonState();
   }
 
+  /**
+   * Add an item to the top of the ordered list.
+   *
+   * @param {object} item The item to insert.
+   */
+  addItem(item) {
+    this.items.unshift(item);
+    this.richlistbox.insertBefore(
+      this.createItem(item),
+      this.richlistbox.firstElementChild);
+    this.richlistbox.selectedIndex = 0;
+    this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem);
+  }
+
   populate() {
     this.richlistbox.textContent = "";
 
-    for (let {id, label, value} of this.items) {
-      let listitem = document.createElement("richlistitem");
-      listitem.setAttribute("value", value);
-      let labelEl = document.createElement("label");
-      listitem.id = id;
-      labelEl.textContent = label;
-      listitem.appendChild(labelEl);
-      this.richlistbox.appendChild(listitem);
+    let frag = document.createDocumentFragment();
+    for (let item of this.items) {
+      frag.appendChild(this.createItem(item));
     }
+    this.richlistbox.appendChild(frag);
 
     this.richlistbox.selectedIndex = 0;
+    this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem);
+  }
+
+  createItem({id, label, value}) {
+    let listitem = document.createElement("richlistitem");
+    listitem.id = id;
+    listitem.setAttribute("value", value);
+
+    let labelEl = document.createElement("label");
+    labelEl.textContent = label;
+    listitem.appendChild(labelEl);
+
+    return listitem;
+  }
+}
+
+class SortedItemSelectList {
+  constructor({menulist, button, onSelect}) {
+    this.menulist = menulist;
+    this.popup = menulist.firstElementChild;
+    this.items = [];
+
+    menulist.addEventListener("command", () => {
+      button.disabled = !menulist.selectedItem;
+    });
+    button.addEventListener("command", () => {
+      if (!menulist.selectedItem) return;
+
+      let [item] = this.items.splice(menulist.selectedIndex, 1);
+      menulist.selectedItem.remove();
+      menulist.setAttribute("label", menulist.getAttribute("placeholder"));
+      button.disabled = true;
+      menulist.disabled = menulist.itemCount == 0;
+
+      onSelect(item);
+    });
+  }
+
+  setItems(items) {
+    this.items = items.sort((a, b) => a.label > b.label);
+    this.populate();
+  }
+
+  populate() {
+    let {items, menulist, popup} = this;
+    popup.textContent = "";
+
+    let frag = document.createDocumentFragment();
+    for (let item of items) {
+      frag.appendChild(this.createItem(item));
+    }
+    popup.appendChild(frag);
+
+    menulist.setAttribute("label", menulist.getAttribute("placeholder"));
+    menulist.disabled = menulist.itemCount == 0;
+  }
+
+  /**
+   * Add an item to the list sorted by the label.
+   *
+   * @param {object} item The item to insert.
+   */
+  addItem(item) {
+    let {items, menulist, popup} = this;
+    let i;
+
+    // Find the index of the item to insert before.
+    for (i = 0; i < items.length && items[i].label < item.label; i++)
+      ;
+
+    items.splice(i, 0, item);
+    popup.insertBefore(this.createItem(item), menulist.getItemAtIndex(i));
+    menulist.disabled = menulist.itemCount == 0;
+  }
+
+  createItem({label, value}) {
+    let item = document.createElement("menuitem");
+    item.value = value;
+    item.setAttribute("label", label);
+    return item;
   }
 }
 
 function getLocaleDisplayInfo(localeCodes) {
+  let packagedLocales = new Set(Services.locale.getPackagedLocales());
   let localeNames = Services.intl.getLocaleDisplayNames(undefined, localeCodes);
   return localeCodes.map((code, i) => {
     return {
       id: "locale-" + code,
       label: localeNames[i],
       value: code,
+      canRemove: !packagedLocales.has(code),
     };
   });
 }
 
 var gMessengerLanguagesDialog = {
-  _orderedListBox: null,
+  _availableLocales: null,
+  _requestedLocales: null,
   requestedLocales: null,
 
   beforeAccept() {
-    this.requestedLocales = this._orderedListBox.items.map(item => item.value);
+    this.requestedLocales = this._requestedLocales.items.map(item => item.value);
     return true;
   },
 
   onLoad() {
-    this._orderedListBox = new OrderedListBox({
-      richlistbox: document.getElementById("activeLocales"),
+    // Maintain the previously requested locales even if we cancel out.
+    this.requestedLocales = window.arguments[0];
+
+    let requested = this.requestedLocales || Services.locale.getRequestedLocales();
+    let requestedSet = new Set(requested);
+    let available = Services.locale.getAvailableLocales()
+      .filter(locale => !requestedSet.has(locale));
+
+    this.initRequestedLocales(requested);
+    this.initAvailableLocales(available);
+    this.initialized = true;
+  },
+
+  initRequestedLocales(requested) {
+    this._requestedLocales = new OrderedListBox({
+      richlistbox: document.getElementById("requestedLocales"),
       upButton: document.getElementById("up"),
       downButton: document.getElementById("down"),
+      removeButton: document.getElementById("remove"),
+      onRemove: (item) => this._availableLocales.addItem(item),
     });
-    // Maintain the previously requested locales even if we cancel out.
-    this.requestedLocales = window.arguments[0];
-    let locales = window.arguments[0]
-      || Services.locale.getRequestedLocales();
-    this._orderedListBox.setItems(getLocaleDisplayInfo(locales));
+    this._requestedLocales.setItems(getLocaleDisplayInfo(requested));
+  },
+
+  initAvailableLocales(available) {
+    this._availableLocales = new SortedItemSelectList({
+      menulist: document.getElementById("availableLocales"),
+      button: document.getElementById("add"),
+      onSelect: (item) => this._requestedLocales.addItem(item),
+    });
+    this._availableLocales.setItems(getLocaleDisplayInfo(available));
   },
 };
--- a/mail/components/preferences/messengerLanguages.xul
+++ b/mail/components/preferences/messengerLanguages.xul
@@ -1,47 +1,61 @@
 <?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/"?>
-<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
 
-<dialog id="BrowserLanguagesDialog" type="child" class="prefwindow"
+<dialog id="MessengerLanguagesDialog" type="child" class="prefwindow"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         data-l10n-id="messenger-languages-window"
         data-l10n-attrs="title, style"
         buttons="accept,cancel"
         role="dialog"
         onload="gMessengerLanguagesDialog.onLoad();"
         onbeforeaccept="return gMessengerLanguagesDialog.beforeAccept();">
 
   <linkset>
     <link rel="localization" href="branding/brand.ftl"/>
     <link rel="localization" href="messenger/preferences/languages.ftl"/>
   </linkset>
 
-  <script type="application/javascript" src="chrome://messnger/content/utilityOverlay.js"/>
   <script type="application/javascript" src="chrome://global/content/preferencesBindings.js"/>
   <script type="application/javascript" src="chrome://messenger/content/preferences/messengerLanguages.js"/>
 
-  <description data-l10n-id="messenger-languages-description"/>
+  <vbox id="messengerLanguagesDialogPane"
+        class="prefpane largeDialogContainer"
+        flex="1">
+    <description data-l10n-id="messenger-languages-description"/>
 
-  <grid flex="1">
-    <columns>
-      <column flex="1"/>
-      <column/>
-    </columns>
-    <rows>
-      <row flex="1">
-        <vbox>
-          <richlistbox id="activeLocales" flex="1"/>
-        </vbox>
-        <vbox>
-          <button id="up" disabled="true" data-l10n-id="languages-customize-moveup"/>
-          <button id="down" disabled="true" data-l10n-id="languages-customize-movedown"/>
-        </vbox>
-      </row>
-    </rows>
-  </grid>
+    <grid flex="1">
+      <columns>
+        <column flex="1"/>
+        <column/>
+      </columns>
+      <rows>
+        <row flex="1">
+            <richlistbox id="requestedLocales" flex="1"/>
+          <vbox>
+            <button id="up" disabled="true" data-l10n-id="languages-customize-moveup"/>
+            <button id="down" disabled="true" data-l10n-id="languages-customize-movedown"/>
+            <button id="remove" disabled="true" data-l10n-id="languages-customize-remove"/>
+          </vbox>
+        </row>
+        <row>
+          <menulist id="availableLocales"
+                    class="available-locales-list"
+                    data-l10n-id="languages-customize-select-language"
+                    data-l10n-attrs="placeholder">
+            <menupopup/>
+          </menulist>
+          <button id="add"
+                  class="add-browser-language"
+                  data-l10n-id="languages-customize-add"
+                  disabled="true"/>
+        </row>
+      </rows>
+    </grid>
+    <separator/>
+  </vbox>
 </dialog>
--- a/mail/components/preferences/subdialogs.js
+++ b/mail/components/preferences/subdialogs.js
@@ -252,21 +252,27 @@ SubDialog.prototype = {
 
     // XXX: Hack to make focus during the dialog's load functions work. Make the element visible
     // sooner in DOMContentLoaded but mostly invisible instead of changing visibility just before
     // the dialog's load event.
     this._overlay.style.visibility = "visible";
     this._overlay.style.opacity = "0.01";
   },
 
-  _onLoad(aEvent) {
+  async _onLoad(aEvent) {
     if (aEvent.target.contentWindow.location == "about:blank") {
       return;
     }
 
+    // In order to properly calculate the sizing of the subdialog, we need to
+    // ensure that all of the l10n is done.
+    if (aEvent.target.contentDocument.l10n) {
+      await aEvent.target.contentDocument.l10n.ready;
+    }
+
     // Do this on load to wait for the CSS to load and apply before calculating the size.
     let docEl = this._frame.contentDocument.documentElement;
 
     let groupBoxTitle = document.getAnonymousElementByAttribute(this._box, "class", "groupbox-title");
     let groupBoxTitleHeight = groupBoxTitle.clientHeight +
                               parseFloat(getComputedStyle(groupBoxTitle).borderBottomWidth);
 
     let groupBoxBody = document.getAnonymousElementByAttribute(this._box, "class", "groupbox-body");
--- a/mail/locales/en-US/messenger/preferences/languages.ftl
+++ b/mail/locales/en-US/messenger/preferences/languages.ftl
@@ -5,13 +5,24 @@
 languages-customize-moveup =
     .label = Move Up
     .accesskey = U
 
 languages-customize-movedown =
     .label = Move Down
     .accesskey = D
 
+languages-customize-remove =
+    .label = Remove
+    .accesskey = R
+
+languages-customize-select-language =
+    .placeholder = Select a language to add…
+
+languages-customize-add =
+    .label = Add
+    .accesskey = A
+
 messenger-languages-window =
     .title = { -brand-short-name } Language Settings
     .style = width: 40em
 
 messenger-languages-description = { -brand-short-name } will display the first language as your default and will display alternate languages if necessary in the order they appear.
--- a/mail/themes/shared/mail/incontentprefs/aboutPreferences.css
+++ b/mail/themes/shared/mail/incontentprefs/aboutPreferences.css
@@ -407,21 +407,34 @@ richlistbox richlistitem:hover {
   background-color: var(--in-content-item-hover);
 }
 
 richlistbox:focus > richlistitem[selected="true"] {
   background-color: var(--in-content-item-selected);
   color: var(--in-content-selected-text) !important;
 }
 
+#messengerLanguagesDialogPane {
+  min-height: 360px;
+}
+
 #defaultMessengerLanguage {
   margin-inline-start: 0;
   min-width: 20em;
 }
 
+#availableLocales {
+  margin: 0;
+  margin-inline-end: 4px;
+}
+
+.add-browser-language {
+  margin: 0 4px;
+}
+
 /**
  * Dialog
  */
 
 .dialogOverlay {
   visibility: hidden;
 }
 .dialogOverlay[topmost="true"] {