Bug 501163 - "New attachment preference pane lacks option to remove wrong file-type associations" [r=philringnalda ui-review=clarkbw]
authorBlake Winton <bwinton@latte.ca>
Mon, 31 Aug 2009 10:06:38 +0100
changeset 3454 072d7b84a131419c26feee6d68f81aa21d7cd676
parent 3453 b6392cba3b7d16a3ef9709eb7de6ac9d8f608184
child 3455 0eb7935fcb9bab1edec17619e0b8eba88289c8cd
push idunknown
push userunknown
push dateunknown
reviewersphilringnalda
bugs501163
Bug 501163 - "New attachment preference pane lacks option to remove wrong file-type associations" [r=philringnalda ui-review=clarkbw]
mail/components/preferences/applications.js
mail/components/preferences/applications.xul
mail/components/preferences/handlers.xml
mail/locales/en-US/chrome/messenger/preferences/preferences.properties
mail/themes/gnomestripe/mail/preferences/applications.css
mail/themes/pinstripe/mail/preferences/applications.css
mail/themes/qute/mail/preferences/applications.css
--- a/mail/components/preferences/applications.js
+++ b/mail/components/preferences/applications.js
@@ -406,16 +406,20 @@ HandlerInfoWrapper.prototype = {
 
   //**************************************************************************//
   // Storage
 
   store: function() {
     this._handlerSvc.store(this.wrappedHandlerInfo);
   },
 
+  remove: function() {
+    this._handlerSvc.remove(this.wrappedHandlerInfo);
+  },
+
 
   //**************************************************************************//
   // Icons
 
   get smallIcon() {
     return this._getIcon(16);
   },
 
@@ -702,27 +706,28 @@ var gApplicationsPane = {
         this._visibleTypeDescriptionCount[handlerInfo.description] = 1;
     }
   },
 
   rebuildView: function() {
     // Clear the list of entries.
     while (this._list.childNodes.length > 1)
       this._list.removeChild(this._list.lastChild);
-
     var visibleTypes = this._visibleTypes;
 
     // If the user is filtering the list, then only show matching types.
     if (this._filter.value)
       visibleTypes = visibleTypes.filter(this._matchesFilter, this);
 
     for each (let visibleType in visibleTypes) {
       let item = document.createElement("richlistitem");
       item.setAttribute("type", visibleType.type);
       item.setAttribute("typeDescription", this._describeType(visibleType));
+      item.setAttribute("shortTypeDescription", visibleType.description);
+      item.setAttribute("shortTypeDetails", this._typeDetails(visibleType));
       if (visibleType.smallIcon)
         item.setAttribute("typeIcon", visibleType.smallIcon);
       item.setAttribute("actionDescription",
                         this._describePreferredAction(visibleType));
 
       if (!this._setIconClassForPreferredAction(visibleType, item)) {
         item.setAttribute("actionIcon",
                           this._getIconURLForPreferredAction(visibleType));
@@ -746,25 +751,58 @@ var gApplicationsPane = {
    * the info object, but if more than one object presents the same description,
    * then we annotate the duplicate descriptions with the type itself to help
    * users distinguish between those types.
    *
    * @param aHandlerInfo {nsIHandlerInfo} the type being described
    * @return {string} a description of the type
    */
   _describeType: function(aHandlerInfo) {
-    if (this._visibleTypeDescriptionCount[aHandlerInfo.description] > 1)
-      return this._prefsBundle.getFormattedString("typeDescriptionWithType",
+    let details = this._typeDetails(aHandlerInfo);
+
+    if (details)
+      return this._prefsBundle.getFormattedString("typeDescriptionWithDetails",
                                                   [aHandlerInfo.description,
-                                                   aHandlerInfo.type]);
-
+                                                   details]);
     return aHandlerInfo.description;
   },
 
   /**
+   * Get the details for the type represented by the given handler info
+   * object.
+   *
+   * @param aHandlerInfo {nsIHandlerInfo} the type to get the extensions for.
+   * @return {string} the extensions for the type
+   */
+  _typeDetails: function(aHandlerInfo) {
+    let exts = [];
+    if (aHandlerInfo.wrappedHandlerInfo instanceof Components.interfaces.nsIMIMEInfo) {
+      let extIter = aHandlerInfo.wrappedHandlerInfo.getFileExtensions();
+      while(extIter.hasMore()) {
+        let ext = "."+extIter.getNext();
+        if (exts.indexOf(ext) == -1)
+          exts.push(ext);
+      }
+    }
+    exts.sort();
+    exts = exts.join(", ");
+    if (this._visibleTypeDescriptionCount[aHandlerInfo.description] > 0) {
+      if (exts)
+        return this._prefsBundle.getFormattedString("typeDetailsWithTypeAndExt",
+                                                    [aHandlerInfo.type,
+                                                     exts]);
+      return this._prefsBundle.getFormattedString("typeDetailsWithTypeOrExt",
+                                                  [ aHandlerInfo.type]);
+    }
+    if (exts)
+      return this._prefsBundle.getFormattedString("typeDescriptionWithExt",
+                                                  [exts]);
+    return exts;
+  },
+  /**
    * Describe, in a human-readable fashion, the preferred action to take on
    * the type represented by the given handler info object.
    *
    * XXX Should this be part of the HandlerInfoWrapper interface?  It would
    * violate the separation of model and view, but it might make more sense
    * nonetheless (f.e. it would make sortTypes easier).
    *
    * @param aHandlerInfo {nsIHandlerInfo} the type whose preferred action
@@ -995,16 +1033,23 @@ var gApplicationsPane = {
       let menuItem = document.createElement("menuseparator");
       menuPopup.appendChild(menuItem);
       menuItem = document.createElement("menuitem");
       menuItem.setAttribute("oncommand", "gApplicationsPane.manageApp(event)");
       menuItem.setAttribute("label", this._prefsBundle.getString("manageApp"));
       menuPopup.appendChild(menuItem);
     }
 
+    let menuItem = document.createElement("menuseparator");
+    menuPopup.appendChild(menuItem);
+    menuItem = document.createElement("menuitem");
+    menuItem.setAttribute("oncommand", "gApplicationsPane.confirmDelete(event)");
+    menuItem.setAttribute("label", this._prefsBundle.getString("delete"));
+    menuPopup.appendChild(menuItem);
+
     // Select the item corresponding to the preferred action.  If the always
     // ask flag is set, it overrides the preferred action.  Otherwise we pick
     // the item identified by the preferred action (when the preferred action
     // is to use a helper app, we have to pick the specific helper app item).
     if (handlerInfo.alwaysAskBeforeHandling)
       menu.selectedItem = askMenuItem;
     else switch (handlerInfo.preferredAction) {
       case Components.interfaces.nsIHandlerInfo.handleInternally:
@@ -1020,16 +1065,20 @@ var gApplicationsPane = {
         break;
       case kActionUsePlugin:
         menu.selectedItem = pluginMenuItem;
         break;
       case Components.interfaces.nsIHandlerInfo.saveToDisk:
         menu.selectedItem = saveMenuItem;
         break;
     }
+    // menu.selectedItem may be null if the preferredAction is
+    // useSystemDefault, but handlerInfo.hasDefaultHandler returns false.
+    // For now, we'll just use the askMenuItem to avoid ugly exceptions.
+    menu.previousSelectedItem = menu.selectedItem || askMenuItem;
   },
 
 
   //**************************************************************************//
   // Sorting & Filtering
 
   _sortColumn: null,
 
@@ -1094,16 +1143,20 @@ var gApplicationsPane = {
   },
 
   //**************************************************************************//
   // Changes
 
   onSelectAction: function(aActionItem) {
     this._storingAction = true;
 
+    let typeItem = this._list.selectedItem;
+    let menu = document.getAnonymousElementByAttribute(typeItem, "class",
+                                                       "actionsMenu");
+    menu.previousSelectedItem = aActionItem;
     try {
       this._storeAction(aActionItem);
     }
     finally {
       this._storingAction = false;
     }
   },
 
@@ -1255,16 +1308,50 @@ var gApplicationsPane = {
   // Mark which item in the list was last selected so we can reselect it
   // when we rebuild the list or when the user returns to the prefpane.
   onSelectionChanged: function() {
     if (this._list.selectedItem)
       this._list.setAttribute("lastSelectedType",
                               this._list.selectedItem.getAttribute("type"));
   },
 
+  confirmDelete: function(aEvent) {
+    aEvent.stopPropagation();
+    let promptSvc = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
+                              .getService(Components.interfaces.nsIPromptService);
+    if (promptSvc.confirm(null,
+                          this._prefsBundle.getString("confirmDeleteTitle"),
+                          this._prefsBundle.getString("confirmDeleteText")))
+      this.onDelete(aEvent);
+    else {
+      // They hit cancel, so return them to the previously selected item.
+      let typeItem = this._list.selectedItem;
+      let menu = document.getAnonymousElementByAttribute(this._list.selectedItem,
+                                                         "class", "actionsMenu");
+      menu.selectedItem = menu.previousSelectedItem;
+    }
+  },
+
+  onDelete: function(aEvent) {
+    // We want to delete if either the request came from the confirmDelete
+    // method (which is the only thing that populates the aEvent parameter),
+    // or we've hit the delete/backspace key while the list has focus.
+    if ((aEvent || document.commandDispatcher.focusedElement == this._list) &&
+        this._list.selectedIndex != -1) {
+      let typeItem = this._list.getItemAtIndex(this._list.selectedIndex);
+      let handlerInfo = this._handledTypes[typeItem.type];
+      this._list.removeItemAt(this._list.selectedIndex);
+      let index = this._visibleTypes.indexOf(handlerInfo);
+      if (index != -1)
+        this._visibleTypes.splice(index, 1);
+      handlerInfo.remove();
+      delete this._handledTypes[typeItem.type];
+    }
+  },
+
   _setIconClassForPreferredAction: function(aHandlerInfo, aElement) {
     // If this returns true, the attribute that CSS sniffs for was set to something
     // so you shouldn't manually set an icon URI.
     // This removes the existing actionIcon attribute if any, even if returning false.
     aElement.removeAttribute("actionIcon");
 
     if (aHandlerInfo.alwaysAskBeforeHandling) {
       aElement.setAttribute(APP_ICON_ATTR_NAME, "ask");
--- a/mail/components/preferences/applications.xul
+++ b/mail/components/preferences/applications.xul
@@ -71,16 +71,26 @@
       <preference id="pref.downloads.disable_button.edit_actions"
                   name="pref.downloads.disable_button.edit_actions"
                   type="bool"/>
     </preferences>
 
     <script type="application/javascript"
             src="chrome://messenger/content/preferences/applications.js"/>
 
+    <commandset id="appPaneCommandSet">
+      <command id="cmd_delete"
+               oncommand="gApplicationsPane.onDelete();"/>
+    </commandset>
+
+    <keyset id="appPaneKeyset">
+      <key keycode="VK_BACK" modifiers="any" command="cmd_delete"/>
+      <key keycode="VK_DELETE" modifiers="any" command="cmd_delete"/>
+    </keyset>
+
     <keyset>
       <key key="&focusSearch1.key;" modifiers="accel"
            oncommand="gApplicationsPane.focusFilterBox();"/>
       <key key="&focusSearch2.key;" modifiers="accel"
            oncommand="gApplicationsPane.focusFilterBox();"/>
     </keyset>
 
     <hbox>
@@ -95,17 +105,17 @@
 
     <richlistbox id="handlersView" orient="vertical" persist="lastSelectedType"
                  preference="pref.downloads.disable_button.edit_actions"
                  onselect="gApplicationsPane.onSelectionChanged();">
       <listheader equalsize="always" style="border: 0; padding: 0; -moz-appearance: none;">
         <treecol id="typeColumn" label="&typeColumn.label;" value="type"
                  accesskey="&typeColumn.accesskey;" persist="sortDirection"
                  flex="1" onclick="gApplicationsPane.sort(event);"
-                 sortDirection="ascending"/>
+                 sortDirection="ascending" sort="typeDescription"/>
         <treecol id="actionColumn" label="&actionColumn2.label;" value="action"
                  accesskey="&actionColumn2.accesskey;" persist="sortDirection"
                  flex="1" onclick="gApplicationsPane.sort(event);"/>
       </listheader>
     </richlistbox>
 
     <separator class="thin"/>
 
--- a/mail/components/preferences/handlers.xml
+++ b/mail/components/preferences/handlers.xml
@@ -58,33 +58,39 @@
   </binding>
 
   <binding id="handler" extends="chrome://messenger/content/preferences/handlers.xml#handler-base">
     <content>
       <xul:hbox flex="1" equalsize="always">
         <xul:hbox flex="1" align="center" xbl:inherits="tooltiptext=typeDescription">
           <xul:image src="moz-icon://goat?size=16" class="typeIcon"
                      xbl:inherits="src=typeIcon" height="16" width="16"/>
-          <xul:label flex="1" crop="end" xbl:inherits="value=typeDescription"/>
+          <xul:label flex="1" class="shortDescription"
+                     xbl:inherits="value=shortTypeDescription"/>
+          <xul:label flex="1" crop="end" class="shortDetails"
+                     xbl:inherits="value=shortTypeDetails"/>
         </xul:hbox>
         <xul:hbox flex="1" align="center" xbl:inherits="tooltiptext=actionDescription">
           <xul:image xbl:inherits="src=actionIcon" height="16" width="16" class="actionIcon"/>
           <xul:label flex="1" crop="end" xbl:inherits="value=actionDescription"/>
         </xul:hbox>
       </xul:hbox>
     </content>
   </binding>
 
   <binding id="handler-selected" extends="chrome://messenger/content/preferences/handlers.xml#handler-base">
     <content>
       <xul:hbox flex="1" equalsize="always">
         <xul:hbox flex="1" align="center" xbl:inherits="tooltiptext=typeDescription">
           <xul:image src="moz-icon://goat?size=16" class="typeIcon"
                      xbl:inherits="src=typeIcon" height="16" width="16"/>
-          <xul:label flex="1" crop="end" xbl:inherits="value=typeDescription"/>
+          <xul:label flex="1" class="shortDescription selected"
+                     xbl:inherits="value=shortTypeDescription"/>
+          <xul:label flex="1" crop="end" class="shortDetails selected"
+                     xbl:inherits="value=shortTypeDetails"/>
         </xul:hbox>
         <xul:hbox flex="1">
           <xul:menulist class="actionsMenu" flex="1" crop="end" selectedIndex="1"
                         xbl:inherits="tooltiptext=actionDescription"
                         oncommand="gApplicationsPane.onSelectAction(event.originalTarget)">
             <xul:menupopup/>
           </xul:menulist>
         </xul:hbox>
--- a/mail/locales/en-US/chrome/messenger/preferences/preferences.properties
+++ b/mail/locales/en-US/chrome/messenger/preferences/preferences.properties
@@ -25,26 +25,38 @@ saveFile=Save File
 # LOCALIZATION NOTE (useApp, useDefault): %S = Application name
 useApp=Use %S
 useDefault=Use %S (default)
 
 useOtherApp=Use other…
 fpTitleChooseApp=Select Helper Application
 manageApp=Application Details…
 alwaysAsk=Always ask
+delete=Delete Action
+confirmDeleteTitle=Delete Action
+confirmDeleteText=Are you sure you want to delete this action?
 
 # LOCALIZATION NOTE (usePluginIn):
 # %1$S = plugin name (for example "QuickTime Plugin-in 7.2")
 # %2$S = brandShortName from brand.properties (for example "Shredder")
 usePluginIn=Use %1$S (in %2$S)
 
-# LOCALIZATION NOTE (typeDescriptionWithType):
+# LOCALIZATION NOTE (typeDescriptionWithDetails):
 # %1$S = type description (for example "Portable Document Format")
-# %2$S = type (for example "application/pdf")
-typeDescriptionWithType=%1$S (%2$S)
+# %2$S = details (see below, for example "(application/pdf: .pdf, .pdfx)")
+typeDescriptionWithDetails=%1$S %2$S
+
+# LOCALIZATION NOTE (typeDetailsWithTypeOrExt):
+# %1$S = type or extensions (for example "application/pdf", or ".pdf, .pdfx")
+typeDetailsWithTypeOrExt=(%1$S)
+
+# LOCALIZATION NOTE (typeDetailsWithTypeAndExt):
+# %1$S = type (for example "application/pdf")
+# %2$S = extensions (for example ".pdf, .pdfx")
+typeDetailsWithTypeAndExt=(%1$S: %2$S)
 
 #### Sound Notifications
 soundFilePickerTitle=Choose Sound
 
 #### Shell Service
 alreadyDefaultClientTitle=Default Client
 alreadyDefault=%S is already set as your default mail client.
 
--- a/mail/themes/gnomestripe/mail/preferences/applications.css
+++ b/mail/themes/gnomestripe/mail/preferences/applications.css
@@ -85,8 +85,17 @@ menuitem[appHandlerIcon="plugin"] {
 .actionsMenu > menupopup > menuitem > .menu-iconic-left {
   -moz-padding-start: 0;
   -moz-padding-end: 4px !important;
 }
 
 .actionsMenu > menupopup > menuitem {
   -moz-padding-start: 3px;
 }
+
+.shortDetails {
+  text-align: right;
+  color: GrayText;
+}
+
+richlistbox:focus .shortDetails.selected {
+  color: inherit;
+}
--- a/mail/themes/pinstripe/mail/preferences/applications.css
+++ b/mail/themes/pinstripe/mail/preferences/applications.css
@@ -78,8 +78,17 @@ menuitem[appHandlerIcon="plugin"] {
 .actionsMenu .menulist-icon {
   -moz-margin-end: 1px;
 }
 
 .actionsMenu > menupopup > menuitem > .menu-iconic-left {
   -moz-padding-start: 3px;
   -moz-padding-end: 1px;
 }
+
+.shortDetails {
+  text-align: right;
+  color: GrayText;
+}
+
+richlistbox:focus .shortDetails.selected {
+  color: inherit;
+}
--- a/mail/themes/qute/mail/preferences/applications.css
+++ b/mail/themes/qute/mail/preferences/applications.css
@@ -83,8 +83,17 @@ menuitem[appHandlerIcon="plugin"] {
 .actionsMenu > menupopup > menuitem > .menu-iconic-left {
   -moz-padding-start: 0px;
   -moz-padding-end: 2px;
 }
 
 .actionsMenu > menupopup > menuitem {
   -moz-padding-start: 4px;
 }
+
+.shortDetails {
+  text-align: right;
+  color: GrayText;
+}
+
+richlistbox:focus .shortDetails.selected {
+  color: inherit;
+}