Bug 646032 - Missing minus\plus icons on new feature Ability to collapse attachment pane; make attachment name clickable; r=bwinton, ui-r=bwinton
authorJim Porter <squibblyflabbetydoo@gmail.com>
Mon, 09 May 2011 19:30:12 -0500
changeset 7742 239f022b6dc8f4671a5a3272c46a3b53f15ad9a8
parent 7741 52d7aa578d9b3452609622791de1701ed23c06d2
child 7743 b29e89e118157cc7b5567e0d69af6f38d659ecf0
push id5943
push usersquibblyflabbetydoo@gmail.com
push dateTue, 10 May 2011 00:31:46 +0000
treeherdercomm-central@b29e89e11815 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbwinton, bwinton
bugs646032
Bug 646032 - Missing minus\plus icons on new feature Ability to collapse attachment pane; make attachment name clickable; r=bwinton, ui-r=bwinton
mail/base/content/mailCore.js
mail/base/content/msgHdrViewOverlay.js
mail/base/content/msgHdrViewOverlay.xul
mail/components/compose/content/MsgComposeCommands.js
mail/components/compose/content/messengercompose.xul
mail/locales/en-US/chrome/messenger/messenger.properties
mail/test/mozmill/attachment/test-attachment.js
mail/themes/gnomestripe/mail/messageHeader.css
mail/themes/pinstripe/mail/messageHeader.css
mail/themes/qute/mail/messageHeader-aero.css
mail/themes/qute/mail/messageHeader.css
--- a/mail/base/content/mailCore.js
+++ b/mail/base/content/mailCore.js
@@ -573,57 +573,53 @@ function getMostRecentMailWindow() {
 
     win = windowList.getNext();
   }
 #endif
 
   return win;
 }
 
-var attachmentAreaDNDObserver = {
-  onDragStart: function (aEvent, aAttachmentData, aDragAction)
-  {
-    var target = aEvent.target;
+/**
+ * Create a TransferData object for a message attachment, either from the
+ * message reader or the composer.
+ *
+ * @param aAttachment the attachment object
+ * @return the TransferData
+ */
+function CreateAttachmentTransferData(aAttachment)
+{
+  if (aAttachment.contentType == "text/x-moz-deleted")
+    return;
 
-    // The message reader will hold an attachment in a descriptionitem, while
-    // the compose window holds it in a listitem.
-    if (target.localName == "descriptionitem" ||
-        target.localName == "listitem")
-    {
-      var attachment = target.attachment;
-      if (attachment.contentType == "text/x-moz-deleted")
-        return;
-
-      var name = attachment.name || attachment.displayName;
+  var name = aAttachment.name || aAttachment.displayName;
 
-      var data = new TransferData();
-      if (attachment.url && name)
-      {
-        // Only add type/filename info for non-file URLs that don't already
-        // have it.
-        if (/(^file:|&filename=)/.test(attachment.url))
-          var info = attachment.url;
-        else
-          var info = attachment.url + "&type=" + attachment.contentType +
-                     "&filename=" + encodeURIComponent(name);
+  var data = new TransferData();
+  if (aAttachment.url && name)
+  {
+    // Only add type/filename info for non-file URLs that don't already
+    // have it.
+    if (/(^file:|&filename=)/.test(aAttachment.url))
+      var info = aAttachment.url;
+    else
+      var info = aAttachment.url + "&type=" + aAttachment.contentType +
+                 "&filename=" + encodeURIComponent(name);
 
-        data.addDataForFlavour("text/x-moz-url",
-                               info + "\n" + name + "\n" + attachment.size);
-        data.addDataForFlavour("text/x-moz-url-data", attachment.url);
-        data.addDataForFlavour("text/x-moz-url-desc", name);
-        data.addDataForFlavour("application/x-moz-file-promise-url",
-                               attachment.url);
-        data.addDataForFlavour("application/x-moz-file-promise",
-                               new nsFlavorDataProvider(), 0,
-                               Components.interfaces.nsISupports);
-      }
-      aAttachmentData.data = data;
-    }
+    data.addDataForFlavour("text/x-moz-url",
+                           info + "\n" + name + "\n" + aAttachment.size);
+    data.addDataForFlavour("text/x-moz-url-data", aAttachment.url);
+    data.addDataForFlavour("text/x-moz-url-desc", name);
+    data.addDataForFlavour("application/x-moz-file-promise-url",
+                           aAttachment.url);
+    data.addDataForFlavour("application/x-moz-file-promise",
+                           new nsFlavorDataProvider(), 0,
+                           Components.interfaces.nsISupports);
   }
-};
+  return data;
+}
 
 function nsFlavorDataProvider()
 {
 }
 
 nsFlavorDataProvider.prototype =
 {
   QueryInterface : function(iid)
--- a/mail/base/content/msgHdrViewOverlay.js
+++ b/mail/base/content/msgHdrViewOverlay.js
@@ -1720,45 +1720,61 @@ function CanDetachAttachments()
 /** Return true if the content type is an S/MIME one. */
 function ContentTypeIsSMIME(contentType)
 {
   // S/MIME is application/pkcs7-mime and application/pkcs7-signature
   // - also match application/x-pkcs7-mime and application/x-pkcs7-signature.
   return /application\/(x-)?pkcs7-(mime|signature)/.test(contentType);
 }
 
+/**
+ * Set up the attachment context menu, showing or hiding the appropriate menu
+ * items.
+ */
 function onShowAttachmentContextMenu()
 {
-  // if no attachments are selected, disable the Open and Save...
   var attachmentList = document.getElementById('attachmentList');
-  var selectedAttachments = attachmentList.selectedItems;
+  var attachmentName = document.getElementById('attachmentName');
+  var contextMenu = document.getElementById('attachmentListContext');
   var openMenu = document.getElementById('context-openAttachment');
   var saveMenu = document.getElementById('context-saveAttachment');
   var detachMenu = document.getElementById('context-detachAttachment');
   var deleteMenu = document.getElementById('context-deleteAttachment');
   var menuSeparator = document.getElementById('context-menu-separator');
   var saveAllMenu = document.getElementById('context-saveAllAttachments');
   var detachAllMenu = document.getElementById('context-detachAllAttachments');
   var deleteAllMenu = document.getElementById('context-deleteAllAttachments');
 
+  // If we opened the context menu from the attachmentName label, just grab
+  // the first (and only) attachment as our "selected" attachments.
+  var selectedAttachments;
+  if (contextMenu.triggerNode == attachmentName) {
+    selectedAttachments = [attachmentList.getItemAtIndex(0).attachment];
+    attachmentName.setAttribute('selected', true);
+  }
+  else
+    selectedAttachments = [item.attachment for each([, item] in
+                           Iterator(attachmentList.selectedItems))];
+  contextMenu.attachments = selectedAttachments;
+
   var canDetach = CanDetachAttachments();
   var deletedAmongSelected = false;
   var detachedAmongSelected = false;
   var anyDeleted = false; // at least one deleted attachment in the list
   var anyDetached = false; // at least one detached attachment in the list
   var selectNone = selectedAttachments.length == 0;
 
   // Check if one or more of the selected attachments are deleted.
   for (var i = 0; i < selectedAttachments.length && !deletedAmongSelected; i++)
     deletedAmongSelected =
-      (selectedAttachments[i].attachment.contentType == 'text/x-moz-deleted');
+      (selectedAttachments[i].contentType == 'text/x-moz-deleted');
 
   // Check if one or more of the selected attachments are detached.
   for (var i = 0; i < selectedAttachments.length && !detachedAmongSelected; i++)
-    detachedAmongSelected = selectedAttachments[i].attachment.isExternalAttachment;
+    detachedAmongSelected = selectedAttachments[i].isExternalAttachment;
 
   // Check if any attachments are deleted.
   for (var i = 0; i < currentAttachments.length && !anyDeleted; i++)
     anyDeleted = (currentAttachments[i].contentType == 'text/x-moz-deleted');
 
   // Check if any attachments are detached.
   for (var i = 0; i < currentAttachments.length && !anyDetached; i++)
     anyDetached = currentAttachments[i].isExternalAttachment;
@@ -1785,16 +1801,30 @@ function onShowAttachmentContextMenu()
   {
     saveAllMenu.setAttribute('disabled', anyDeleted);
     detachAllMenu.setAttribute('disabled', !canDetach || anyDeleted || anyDetached);
     deleteAllMenu.setAttribute('disabled', !canDetach || anyDeleted || anyDetached);
   }
 }
 
 /**
+ * Close the attachment context menu, performing any cleanup as necessary.
+ */
+function onHideAttachmentContextMenu()
+{
+  let attachmentName = document.getElementById('attachmentName');
+  let contextMenu = document.getElementById('attachmentListContext');
+
+  // If we opened the context menu from the attachmentName label, we need to
+  // get rid of the "selected" attribute.
+  if (contextMenu.triggerNode == attachmentName)
+    attachmentName.removeAttribute('selected');
+}
+
+/**
  * Enable/disable menu items as appropriate for the single-attachment save all
  * toolbar button.
  */
 function onShowSaveAttachmentMenuSingle()
 {
   let openItem   = document.getElementById('button-openAttachment');
   let saveItem   = document.getElementById('button-saveAttachment');
   let detachItem = document.getElementById('button-detachAttachment');
@@ -1931,72 +1961,76 @@ function displayAttachmentsForExpandedVi
       item.setAttribute("attachmentUrl", attachment.url);
       item.setAttribute("attachmentContentType", attachment.contentType);
       item.setAttribute("attachmentUri", attachment.uri);
       item.setAttribute("attachmentSize", attachment.size);
 
       attachmentList.appendChild(item);
     } // for each attachment
 
-    var words = gMessengerBundle.getString("attachmentCount");
-    var count = PluralForm.get(currentAttachments.length, words)
-                          .replace("#1", currentAttachments.length);
-
     // Show the appropriate toolbar button and label based on the number of
     // attachments.
-    if (numAttachments == 1)
-    {
-      var countAndName = gMessengerBundle.getFormattedString(
-        "attachmentCountAndName", [count, createAttachmentDisplayName(
-            currentAttachments[0])]);
+    let saveAllSingle   = document.getElementById("attachmentSaveAllSingle");
+    let saveAllMultiple = document.getElementById("attachmentSaveAllMultiple");
+    let attachmentCount = document.getElementById("attachmentCount");
+    let attachmentName  = document.getElementById("attachmentName");
+    let attachmentSize  = document.getElementById("attachmentSize");
+
+    if (numAttachments == 1) {
+      let count = gMessengerBundle.getString("attachmentCountSingle");
+      let name = createAttachmentDisplayName(currentAttachments[0]);
 
-      document.getElementById("attachmentSaveAllSingle").hidden = false;
-      document.getElementById("attachmentSaveAllMultiple").hidden = true;
-      document.getElementById("attachmentCount").setAttribute("value", countAndName);
+      saveAllSingle.hidden = false;
+      saveAllMultiple.hidden = true;
+      attachmentCount.setAttribute("value", count);
+      attachmentName.hidden = false;
+      attachmentName.setAttribute("value", name);
     }
-    else
-    {
-      document.getElementById("attachmentSaveAllSingle").hidden = true;
-      document.getElementById("attachmentSaveAllMultiple").hidden = false;
-      document.getElementById("attachmentCount").setAttribute("value", count);
+    else {
+      let words = gMessengerBundle.getString("attachmentCount");
+      let count = PluralForm.get(currentAttachments.length, words)
+                            .replace("#1", currentAttachments.length);
+
+      saveAllSingle.hidden = true;
+      saveAllMultiple.hidden = false;
+      attachmentCount.setAttribute("value", count);
+      attachmentName.hidden = true;
     }
 
     let sizeStr = messenger.formatFileSize(totalSize);
     if (unknownSize) {
       if (totalSize == 0)
         sizeStr = gMessengerBundle.getString("attachmentSizeUnknown");
       else
         sizeStr = gMessengerBundle.getFormattedString("attachmentSizeAtLeast",
                                                       [sizeStr]);
     }
-    document.getElementById("attachmentSize").setAttribute("value", sizeStr);
+    attachmentSize.setAttribute("value", sizeStr);
 
     gBuildAttachmentsForCurrentMsg = true;
   }
 }
 
 /**
  * Expand/collapse the attachment list. When expanding it, automatically resize
  * it to an appropriate height (1/4 the message pane or smaller).
  *
  * @param expanded True if the attachment list should be expanded, false
- *                 otherwise. If not specified, expand/collapse based on the
- *                 state of |attachmentToggle|.
+ *                 otherwise. If |expanded| is not specified, toggle the state.
  */
 function toggleAttachmentList(expanded)
 {
   var attachmentToggle      = document.getElementById("attachmentToggle");
   var attachmentView        = document.getElementById("attachmentView");
   var attachmentSplitter    = document.getElementById("attachment-splitter");
   var attachmentListWrapper = document.getElementById("attachmentListWrapper");
 
   if (expanded === undefined)
-    expanded = attachmentToggle.checked;
-  else
-    attachmentToggle.checked = expanded;
+    expanded = !attachmentToggle.checked;
+  attachmentToggle.checked = expanded;
 
   if (expanded) {
     attachmentListWrapper.collapsed = false;
     attachmentSplitter.collapsed = false;
 
     var attachmentHeight = attachmentView.boxObject.height;
 
     // If the attachments box takes up too much of the message pane, downsize:
@@ -2276,16 +2310,35 @@ function ClearAttachmentList()
   // clear selection
   var list = document.getElementById('attachmentList');
   list.selectedItems.length = 0;
 
   while (list.hasChildNodes())
     list.removeChild(list.lastChild);
 }
 
+var attachmentListDNDObserver = {
+  onDragStart: function (aEvent, aAttachmentData, aDragAction)
+  {
+    var target = aEvent.target;
+
+    if (target.localName == "descriptionitem")
+      aAttachmentData.data = CreateAttachmentTransferData(target.attachment);
+  }
+};
+
+var attachmentNameDNDObserver = {
+  onDragStart: function (aEvent, aAttachmentData, aDragAction)
+  {
+    var attachmentList = document.getElementById("attachmentList");
+    aAttachmentData.data = CreateAttachmentTransferData(
+      attachmentList.getItemAtIndex(0).attachment);
+  }
+};
+
 function ShowEditMessageBox()
 {
   // it would be nice if we passed in the msgHdr from the back end
   var msgHdr = gFolderDisplay.selectedMessage;
   if (!msgHdr || !msgHdr.folder)
     return;
   const nsMsgFolderFlags = Components.interfaces.nsMsgFolderFlags;
   if (msgHdr.folder.isSpecialFolder(nsMsgFolderFlags.Drafts, true))
--- a/mail/base/content/msgHdrViewOverlay.xul
+++ b/mail/base/content/msgHdrViewOverlay.xul
@@ -69,26 +69,28 @@
             oncommand="var messageId = GetMessageIdFromNode(document.popupNode, true);
                        OpenBrowserWithMessageId(messageId)"/>
   <menuitem id="messageIdContext-copyMessageId"
             label="&CopyMessageId.label;"
             oncommand="var messageId = GetMessageIdFromNode(document.popupNode, false);
                        CopyMessageId(messageId);"/>
 </menupopup>
 
-<menupopup id="attachmentListContext" onpopupshowing="return onShowAttachmentContextMenu();">
+<menupopup id="attachmentListContext"
+           onpopupshowing="return onShowAttachmentContextMenu();"
+           onpopuphiding="return onHideAttachmentContextMenu();">
   <menuitem id="context-openAttachment" label="&openAttachmentCmd.label;" accesskey="&openAttachmentCmd.accesskey;"
-            oncommand="HandleSelectedAttachments('open');"/>
+            oncommand="HandleMultipleAttachments(this.parentNode.attachments, 'open');"/>
   <menuitem id="context-saveAttachment" label="&saveAsAttachmentCmd.label;" accesskey="&saveAsAttachmentCmd.accesskey;"
-            oncommand="HandleSelectedAttachments('saveAs');"/>
+            oncommand="HandleMultipleAttachments(this.parentNode.attachments, 'saveAs');"/>
   <menuseparator id="context-menu-separator"/>
   <menuitem id="context-detachAttachment" label="&detachAttachmentCmd.label;" accesskey="&detachAttachmentCmd.accesskey;"
-            oncommand="HandleSelectedAttachments('detach');"/>
+            oncommand="HandleMultipleAttachments(this.parentNode.attachments, 'detach');"/>
   <menuitem id="context-deleteAttachment" label="&deleteAttachmentCmd.label;" accesskey="&deleteAttachmentCmd.accesskey;"
-            oncommand="HandleSelectedAttachments('delete');"/>
+            oncommand="HandleMultipleAttachments(this.parentNode.attachments, 'delete');"/>
   <menuitem id="context-saveAllAttachments" oncommand="HandleAllAttachments('save');"
     label="&saveAllAttachmentsCmd.label;" accesskey="&saveAllAttachmentsCmd.accesskey;"/>
   <menuitem id="context-detachAllAttachments" oncommand="HandleAllAttachments('detach');"
     label="&detachAllAttachmentsCmd.label;"/>
   <menuitem id="context-deleteAllAttachments" oncommand="HandleAllAttachments('delete');"
     label="&deleteAllAttachmentsCmd.label;"/>
 </menupopup>
 
@@ -452,20 +454,27 @@
          value="&editMessageDescription.label;"/>
   <button id="editMessageButton" label="&editMessageButton.label;"
           oncommand="MsgComposeDraftMessage()"/>
 </hbox>
 
 <!-- the message pane consists of 4 'boxes'. Box #4 is the attachment box which
      can be toggled into a slim or an expanded view -->
 <vbox id="attachmentView" collapsed="true">
-  <hbox align="center" context="attachment-toolbar-context-menu" width="200">
-    <button type="checkbox" id="attachmentToggle" oncommand="toggleAttachmentList();"/>
+  <hbox align="center" id="attachmentBar"
+        context="attachment-toolbar-context-menu"
+        onclick="if (event.button == 0 &amp;&amp; event.originalTarget.id == 'attachmentBar') toggleAttachmentList();">
+    <button type="checkbox" id="attachmentToggle"
+            oncommand="toggleAttachmentList(this.checked);"/>
     <image id="attachmentIcon"/>
-    <label id="attachmentCount" crop="center" flex="1"/>
+    <label id="attachmentCount"/>
+    <label id="attachmentName" crop="center" flex="1"
+           context="attachmentListContext"
+           onclick="if (event.button == 0) { HandleAllAttachments('open'); RestoreFocusAfterHdrButton(); }"
+           ondraggesture="nsDragAndDrop.startDrag(event,attachmentNameDNDObserver);"/>
     <label id="attachmentSize"/>
     <!-- Use a very large flex value here so that attachmentCount doesn't take
          up more space than necessary, but still crops itself if there's not
          enough space. -->
     <spacer flex="9999"/>
 
     <toolbox id="attachment-view-toolbox"
              class="inline-toolbox"
@@ -528,16 +537,17 @@
                customizable="true" mode="full"
                context="attachment-toolbar-context-menu"
                defaulticonsize="small" defaultmode="full"
                defaultset="attachmentSaveAll"/>
     </toolbox>
   </hbox>
   <box id="attachmentListWrapper" flex="1" collapsed="true">
     <description selectable="true" id="attachmentList" flex="1"
-                 seltype="multiple"
-                 onclick="attachmentListClick(event);" ondraggesture="nsDragAndDrop.startDrag(event,attachmentAreaDNDObserver);"
-                 ondragover="nsDragAndDrop.dragOver(event, attachmentAreaDNDObserver);" context="attachmentListContext">
+                 seltype="multiple" context="attachmentListContext"
+                 onclick="attachmentListClick(event);"
+                 ondraggesture="nsDragAndDrop.startDrag(event,attachmentListDNDObserver);"
+                 ondragover="nsDragAndDrop.dragOver(event, attachmentListDNDObserver);">
     </description>
   </box>
 </vbox>
 
 </overlay>
--- a/mail/components/compose/content/MsgComposeCommands.js
+++ b/mail/components/compose/content/MsgComposeCommands.js
@@ -3632,16 +3632,26 @@ var envelopeDragObserver = {
       flavourSet.appendFlavour("text/x-moz-message");
       flavourSet.appendFlavour("application/x-moz-file", "nsIFile");
       flavourSet.appendFlavour("text/x-moz-address");
       flavourSet.appendFlavour("text/x-moz-url");
       return flavourSet;
     }
 };
 
+var attachmentBucketDNDObserver = {
+  onDragStart: function (aEvent, aAttachmentData, aDragAction)
+  {
+    var target = aEvent.target;
+
+    if (target.localName == "listitem")
+      aAttachmentData.data = CreateAttachmentTransferData(target.attachment);
+  }
+};
+
 function DisplaySaveFolderDlg(folderURI)
 {
   try
   {
     var showDialog = gCurrentIdentity.showSaveMsgDlg;
   }
   catch (e)
   {
--- a/mail/components/compose/content/messengercompose.xul
+++ b/mail/components/compose/content/messengercompose.xul
@@ -754,17 +754,17 @@
                    crop="right" accesskey="&attachments.accesskey;"/>
             <label id="attachmentBucketSize"/>
           </hbox>
           <listbox seltype="multiple" id="attachmentBucket" flex="1" rows="4"
                    tabindex="0"
                    context="msgComposeAttachmentContext"
                    onkeypress="if (event.keyCode == 8 || event.keyCode == 46) RemoveSelectedAttachment();"
                    onclick="AttachmentBucketClicked(event);"
-                   ondraggesture="nsDragAndDrop.startDrag(event, attachmentAreaDNDObserver);"/>
+                   ondraggesture="nsDragAndDrop.startDrag(event, attachmentBucketDNDObserver);"/>
         </vbox>
       </hbox>
     </toolbar>   
 
     <!-- These toolbar items get filled out from the editorOverlay -->
     <hbox id="FormatToolbar-box">
       <hbox style="&headersSpace.style;"/>
       <toolbar class="chromeclass-toolbar" id="FormatToolbar" persist="collapsed"
--- a/mail/locales/en-US/chrome/messenger/messenger.properties
+++ b/mail/locales/en-US/chrome/messenger/messenger.properties
@@ -345,20 +345,21 @@ detachAttachments=The following attachme
 deleteAttachmentFailure=Failed to delete the selected attachments.
 emptyAttachment=This attachment appears to be empty.\nPlease check with the person who sent this.\nOften company firewalls or antivirus programs will destroy attachments.
 
 # LOCALIZATION NOTE (attachmentCount): Semi-colon list of plural forms.
 # See: http://developer.mozilla.org/en/Localization_and_Plurals
 # #1 number of attachments
 attachmentCount=#1 attachment;#1 attachments
 
-# LOCALIZATION NOTE (attachmentCountAndName): This is the format for the
-# attachment header when a message has only one attachment. %1$S is the
-# attachment count, and %2$S is the attachment's filename.
-attachmentCountAndName=%1$S: %2$S
+# LOCALIZATION NOTE (attachmentCountSingle): This is the format for the
+# attachment header when a message has only one attachment. This is separate
+# from attachmentCount above, since attachmentCountSingle typically ends with a
+# colon.
+attachmentCountSingle=1 attachment:
 
 # LOCALIZATION NOTE (attachmentSizeUnknown): The string to show for the total
 # size of all attachments when none of the attachments' sizes can be detected.
 attachmentSizeUnknown=size unknown
 
 # LOCALIZATION NOTE (attachmentSizeAtLeast): The string to show for the total
 # size of all attachments when at least one (but not all) of the attachments'
 # sizes can't be detected. %1$S is the formatted size.
--- a/mail/test/mozmill/attachment/test-attachment.js
+++ b/mail/test/mozmill/attachment/test-attachment.js
@@ -128,31 +128,52 @@ function test_attachment_view_expanded()
     assert_selected_and_displayed(i);
 
     if (mc.e("attachmentView").collapsed)
       throw new Error("Attachment pane collapsed (on message #"+i+
                       " when it shouldn't be!");
   }
 }
 
+function test_attachment_name_click() {
+  be_in_folder(folder);
+
+  select_click_row(1);
+  assert_selected_and_displayed(1);
+
+  // Ensure the context menu appears when right-clicking the attachment name
+  mc.rightClick(mc.eid("attachmentName"));
+  assert_equals(mc.e("attachmentListContext").state, "open");
+  close_popup(mc, mc.eid("attachmentListContext"));
+}
+
 function test_attachment_list_expansion() {
   be_in_folder(folder);
 
   select_click_row(1);
+  assert_selected_and_displayed(1);
 
   assert_true(mc.e("attachmentListWrapper").collapsed,
               "Attachment list should start out collapsed!");
 
   mc.click(mc.eid("attachmentToggle"));
   assert_true(!mc.e("attachmentListWrapper").collapsed,
-              "Attachment list should be expanded after toggling!");
+              "Attachment list should be expanded after clicking twisty!");
 
   mc.click(mc.eid("attachmentToggle"));
   assert_true(mc.e("attachmentListWrapper").collapsed,
-              "Attachment list should be collapsed after toggling again!");
+              "Attachment list should be collapsed after clicking twisty again!");
+
+  mc.click(mc.eid("attachmentBar"));
+  assert_true(!mc.e("attachmentListWrapper").collapsed,
+              "Attachment list should be expanded after clicking bar!");
+
+  mc.click(mc.eid("attachmentBar"));
+  assert_true(mc.e("attachmentListWrapper").collapsed,
+              "Attachment list should be collapsed after clicking bar again!");
 }
 
 function test_selected_attachments_are_cleared() {
   be_in_folder(folder);
 
   // First, select the message with two attachments.
   select_click_row(3);
 
--- a/mail/themes/gnomestripe/mail/messageHeader.css
+++ b/mail/themes/gnomestripe/mail/messageHeader.css
@@ -136,20 +136,44 @@ html|div#expandedHeadersTopBox {
 
 #attachmentIcon {
   list-style-image: url("chrome://messenger/skin/icons/filterbar.png");
   -moz-image-region: rect(0px, 80px, 16px, 64px);
   -moz-margin-start: 5px;
 }
 
 #attachmentCount {
+  margin: 0;
   -moz-margin-start: 2px;
+  -moz-margin-end: 1px;
+}
+
+#attachmentName {
+  -moz-user-focus: normal;
+  margin: 0;
+  -moz-margin-end: -3px;
+  padding: 1px 2px;
+  border-radius: 2px;
+  border: 1px dotted transparent;
+}
+
+#attachmentName:hover,
+#attachmentName[selected="true"] {
+  cursor: pointer;
+  color: HighlightText;
+  background-color: Highlight;
+}
+
+#attachmentName:focus {
+  border-color: Highlight;
 }
 
 #attachmentSize {
+  margin: 0;
+  -moz-margin-start: 8px;
   color: #888a85; /* the same color as .headerName */
 }
 
 #attachmentSaveAllSingle,
 #attachmentSaveAllMultiple {
   list-style-image: url("moz-icon://stock/gtk-save?size=menu");
 }
 
--- a/mail/themes/pinstripe/mail/messageHeader.css
+++ b/mail/themes/pinstripe/mail/messageHeader.css
@@ -158,21 +158,43 @@ html|div#expandedHeadersTopBox {
 }
 
 #attachmentIcon {
   list-style-image: url("chrome://messenger/skin/icons/attachment.png");
   -moz-margin-start: 5px;
 }
 
 #attachmentCount {
+  margin: 0;
   -moz-margin-start: 2px;
-  -moz-margin-end: 5px;
+  -moz-margin-end: 1px;
+}
+
+#attachmentName {
+  -moz-user-focus: normal;
+  margin: 0;
+  -moz-margin-end: -3px;
+  padding: 1px 2px;
+  border-radius: 2px;
+  border: 1px dotted transparent;
+}
+
+#attachmentName:hover,
+#attachmentName[selected="true"] {
+  cursor: pointer;
+  background-color: #fcaf3e; /* tango orange */
+}
+
+#attachmentName:focus {
+  border-color: Highlight;
 }
 
 #attachmentSize {
+  margin: 0;
+  -moz-margin-start: 8px;
   color: #888a85; /* the same color as .headerName */
 }
 
 #attachmentSaveAllSingle,
 #attachmentSaveAllMultiple {
   list-style-image: url("chrome://messenger/skin/icons/download.png");
 }
 
--- a/mail/themes/qute/mail/messageHeader-aero.css
+++ b/mail/themes/qute/mail/messageHeader-aero.css
@@ -171,20 +171,44 @@ html|div#expandedHeadersTopBox {
 }
 
 #attachmentIcon {
   list-style-image: url("chrome://messenger/skin/icons/attachment-col.png");
   -moz-margin-start: 5px;
 }
 
 #attachmentCount {
+  margin: 0;
   -moz-margin-start: 2px;
+  -moz-margin-end: 1px;
+}
+
+#attachmentName {
+  -moz-user-focus: normal;
+  margin: 0;
+  -moz-margin-end: -3px;
+  padding: 1px 2px;
+  border-radius: 2px;
+  border: 1px dotted transparent;
+}
+
+#attachmentName:hover,
+#attachmentName[selected="true"] {
+  cursor: pointer;
+  color: HighlightText;
+  background-color: Highlight;
+}
+
+#attachmentName:focus {
+  border-color: Highlight;
 }
 
 #attachmentSize {
+  margin: 0;
+  -moz-margin-start: 8px;
   color: #888a85; /* the same color as .headerName */
 }
 
 #attachmentSaveAllSingle,
 #attachmentSaveAllMultiple {
   list-style-image: url("chrome://messenger/skin/icons/download.png");
 }
 
--- a/mail/themes/qute/mail/messageHeader.css
+++ b/mail/themes/qute/mail/messageHeader.css
@@ -148,20 +148,44 @@ html|div#expandedHeadersTopBox {
 }
 
 #attachmentIcon {
   list-style-image: url("chrome://messenger/skin/icons/attachment-col.png");
   -moz-margin-start: 5px;
 }
 
 #attachmentCount {
+  margin: 0;
   -moz-margin-start: 2px;
+  -moz-margin-end: 1px;
+}
+
+#attachmentName {
+  -moz-user-focus: normal;
+  margin: 0;
+  -moz-margin-end: -3px;
+  padding: 1px 2px;
+  border-radius: 2px;
+  border: 1px dotted transparent;
+}
+
+#attachmentName:hover,
+#attachmentName[selected="true"] {
+  cursor: pointer;
+  color: HighlightText;
+  background-color: Highlight;
+}
+
+#attachmentName:focus {
+  border-color: Highlight;
 }
 
 #attachmentSize {
+  margin: 0;
+  -moz-margin-start: 8px;
   color: #888a85; /* the same color as .headerName */
 }
 
 #attachment-view-toolbox {
   background-color: transparent;
 }
 
 #attachmentSaveAllSingle,