[geary/wip/765516-gtk-widget-conversation-viewer: 60/169] Re-enable attachments context menu.



commit 12e59850a46763a1cdebf50dbe98735a82bc5699
Author: Michael James Gratton <mike vee net>
Date:   Sat Apr 23 17:02:32 2016 +1000

    Re-enable attachments context menu.
    
    Since we're using the Icon View, we have some more options in terms of
    user selction and the actions applicable to that. So make the attachment
    signals and their handlers all apply to collections of attachments and
    use the GAppInfo class for determining which app to open an attachment
    with.
    
    * src/client/application/geary-controller.vala: Chase signal changes.
      (GearyController::on_attachments_activated): Handle multiple
      attachments being activated at once. Use its GAppInfo for launching
      each attachment, prompt the user with an GtkAppChooserDialog if the
      info is unknown.
    
    * src/client/conversation-viewer/conversation-email.vala Use the new
      AttachmentInfo class to manage lists of all displayed and currently
      selected attachments and their associated GAppInfo objects. Add actions
      for attachment context menu items. Move attachment signals from
      ConversationViewer here, make all attachment signals have a collection
      of them as their param. Hook up appropriate GtkIconView callbacks to
      manage selection, activation, etc. Construct AttachmentInfo instances
      when loading attachments and use them in the icon view's model.
    
    * ui/conversation-email.ui: Define needed callbacks for the icon
      view. Update its model to accept a GObject for the attachment info
      class.
    
    * ui/conversation-message-menu.ui: Fix action name for save attachments
      menu item.

 src/client/application/geary-controller.vala       |   70 ++++++---
 .../conversation-viewer/conversation-email.vala    |  151 +++++++++++++------
 .../conversation-viewer/conversation-viewer.vala   |    6 -
 ui/conversation-email.ui                           |    6 +-
 ui/conversation-message-menu.ui                    |   20 +++-
 5 files changed, 174 insertions(+), 79 deletions(-)
---
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index fed10c3..54299c4 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -236,7 +236,6 @@ public class GearyController : Geary.BaseObject {
         main_window.conversation_viewer.email_row_added.connect(on_email_row_added);
         main_window.conversation_viewer.email_row_removed.connect(on_email_row_removed);
         main_window.conversation_viewer.mark_emails.connect(on_conversation_viewer_mark_emails);
-        main_window.conversation_viewer.save_attachments.connect(on_save_attachments);
         main_window.conversation_viewer.save_buffer_to_file.connect(on_save_buffer_to_file);
         new_messages_monitor = new NewMessagesMonitor(should_notify_new_messages);
         main_window.folder_list.set_new_messages_monitor(new_messages_monitor);
@@ -312,7 +311,6 @@ public class GearyController : Geary.BaseObject {
         main_window.conversation_viewer.email_row_added.disconnect(on_email_row_added);
         main_window.conversation_viewer.email_row_removed.disconnect(on_email_row_removed);
         main_window.conversation_viewer.mark_emails.disconnect(on_conversation_viewer_mark_emails);
-        main_window.conversation_viewer.save_attachments.disconnect(on_save_attachments);
         main_window.conversation_viewer.save_buffer_to_file.disconnect(on_save_buffer_to_file);
         // hide window while shutting down, as this can take a few seconds under certain conditions
         main_window.hide();
@@ -1987,28 +1985,50 @@ public class GearyController : Geary.BaseObject {
         }
     }
 
-    private void on_attachment_activated(Geary.Attachment attachment) {
+    private void on_attachments_activated(
+        Gee.Collection<ConversationEmail.AttachmentInfo> attachments) {
         if (GearyApplication.instance.config.ask_open_attachment) {
             QuestionDialog ask_to_open = new QuestionDialog.with_checkbox(main_window,
-                _("Are you sure you want to open \"%s\"?").printf(attachment.file.get_basename()),
+                _("Are you sure you want to open these attachments?"),
                 _("Attachments may cause damage to your system if opened.  Only open files from trusted 
sources."),
                 Stock._OPEN_BUTTON, Stock._CANCEL, _("Don't _ask me again"), false);
-            if (ask_to_open.run() != Gtk.ResponseType.OK)
+            if (ask_to_open.run() != Gtk.ResponseType.OK) {
                 return;
-
+            }
             // only save checkbox state if OK was selected
             GearyApplication.instance.config.ask_open_attachment = !ask_to_open.is_checked;
         }
 
-        // Open the attachment if we know what to do with it.
-        if (!open_uri(attachment.file.get_uri())) {
-            // Failing that, trigger a save dialog.
-            Gee.List<Geary.Attachment> attachment_list = new Gee.ArrayList<Geary.Attachment>();
-            attachment_list.add(attachment);
-            on_save_attachments(attachment_list);
+        foreach (ConversationEmail.AttachmentInfo info in attachments) {
+            if (info.app == null) {
+                string content_type = info.attachment.content_type.get_mime_type();
+                Gtk.AppChooserDialog app_chooser =
+                    new Gtk.AppChooserDialog.for_content_type(
+                        this.main_window,
+                        Gtk.DialogFlags.MODAL | Gtk.DialogFlags.USE_HEADER_BAR,
+                        content_type
+                    );
+                if (app_chooser.run() == Gtk.ResponseType.OK) {
+                    info.app = app_chooser.get_app_info();
+                }
+                app_chooser.hide();
+            }
+            if (info.app != null) {
+                List<File> files = new List<File>();
+                files.append(info.attachment.file);
+                try {
+                    info.app.launch(files, null);
+                } catch (Error error) {
+                    warning(
+                        "Failed to launch %s: %s\n",
+                        info.app.get_name(),
+                        error.message
+                    );
+                }
+            }
         }
     }
-    
+
     private bool do_overwrite_confirmation(File to_overwrite) {
         string primary = _("A file named \"%s\" already exists.  Do you want to replace it?").printf(
             to_overwrite.get_basename());
@@ -2025,11 +2045,12 @@ public class GearyController : Geary.BaseObject {
         return do_overwrite_confirmation(chooser.get_file()) ? Gtk.FileChooserConfirmation.ACCEPT_FILENAME
             : Gtk.FileChooserConfirmation.SELECT_AGAIN;
     }
-    
-    private void on_save_attachments(Gee.List<Geary.Attachment> attachments) {
+
+    private void on_save_attachments(
+        Gee.Collection<ConversationEmail.AttachmentInfo> attachments) {
         if (attachments.size == 0)
             return;
-        
+
         Gtk.FileChooserAction action = (attachments.size == 1)
             ? Gtk.FileChooserAction.SAVE
             : Gtk.FileChooserAction.SELECT_FOLDER;
@@ -2038,7 +2059,10 @@ public class GearyController : Geary.BaseObject {
         if (last_save_directory != null)
             dialog.set_current_folder(last_save_directory.get_path());
         if (attachments.size == 1) {
-            dialog.set_current_name(attachments[0].file.get_basename());
+            Gee.Iterator<ConversationEmail.AttachmentInfo> it = attachments.iterator();
+            it.next();
+            ConversationEmail.AttachmentInfo info = it.get();
+            dialog.set_current_name(info.attachment.file.get_basename());
             dialog.set_do_overwrite_confirmation(true);
             // use custom overwrite confirmation so it looks consistent whether one or many
             // attachments are being saved
@@ -2063,9 +2087,9 @@ public class GearyController : Geary.BaseObject {
         debug("Saving attachments to %s", destination.get_path());
         
         // Save each one, checking for overwrite only if multiple attachments are being written
-        foreach (Geary.Attachment attachment in attachments) {
-            File source_file = attachment.file;
-            File dest_file = (attachments.size == 1) ? destination : 
destination.get_child(attachment.file.get_basename());
+        foreach (ConversationEmail.AttachmentInfo info in attachments) {
+            File source_file = info.attachment.file;
+            File dest_file = (attachments.size == 1) ? destination : 
destination.get_child(info.attachment.file.get_basename());
             
             if (attachments.size > 1 && dest_file.query_exists() && !do_overwrite_confirmation(dest_file))
                 return;
@@ -2668,7 +2692,8 @@ public class GearyController : Geary.BaseObject {
         message.reply_all_message.connect(on_reply_all_message);
         message.forward_message.connect(on_forward_message);
         message.link_activated.connect(on_link_activated);
-        message.attachment_activated.connect(on_attachment_activated);
+        message.attachments_activated.connect(on_attachments_activated);
+        message.save_attachments.connect(on_save_attachments);
         message.edit_draft.connect(on_edit_draft);
         message.view_source.connect(on_view_source);
     }
@@ -2678,7 +2703,8 @@ public class GearyController : Geary.BaseObject {
         message.reply_all_message.disconnect(on_reply_all_message);
         message.forward_message.disconnect(on_forward_message);
         message.link_activated.disconnect(on_link_activated);
-        message.attachment_activated.disconnect(on_attachment_activated);
+        message.attachments_activated.disconnect(on_attachments_activated);
+        message.save_attachments.disconnect(on_save_attachments);
         message.edit_draft.disconnect(on_edit_draft);
         message.view_source.disconnect(on_view_source);
     }
diff --git a/src/client/conversation-viewer/conversation-email.vala 
b/src/client/conversation-viewer/conversation-email.vala
index 687dd83..ca3aee8 100644
--- a/src/client/conversation-viewer/conversation-email.vala
+++ b/src/client/conversation-viewer/conversation-email.vala
@@ -18,6 +18,24 @@
 [GtkTemplate (ui = "/org/gnome/Geary/conversation-email.ui")]
 public class ConversationEmail : Gtk.Box {
 
+
+    /**
+     * Information related to a specific attachment.
+     */
+    public class AttachmentInfo : GLib.Object {
+        // Extends GObject since we put it in a ListStore
+
+        public Geary.Attachment attachment { get; private set; }
+        public AppInfo? app { get; internal set; default = null; }
+
+
+        internal AttachmentInfo(Geary.Attachment attachment) {
+            this.attachment = attachment;
+        }
+
+    }
+
+
     private const int ATTACHMENT_ICON_SIZE = 32;
     private const int ATTACHMENT_PREVIEW_SIZE = 64;
 
@@ -25,9 +43,12 @@ public class ConversationEmail : Gtk.Box {
     private const string ACTION_MARK_READ = "mark_read";
     private const string ACTION_MARK_UNREAD = "mark_unread";
     private const string ACTION_MARK_UNREAD_DOWN = "mark_unread_down";
+    private const string ACTION_OPEN_ATTACHMENTS = "open_attachments";
     private const string ACTION_PRINT = "print";
     private const string ACTION_REPLY_SENDER = "reply_sender";
     private const string ACTION_REPLY_ALL = "reply_all";
+    private const string ACTION_SAVE_ATTACHMENTS = "save_attachments";
+    private const string ACTION_SAVE_ALL_ATTACHMENTS = "save_all_attachments";
     private const string ACTION_STAR = "star";
     private const string ACTION_UNSTAR = "unstar";
     private const string ACTION_VIEW_SOURCE = "view_source";
@@ -52,6 +73,15 @@ public class ConversationEmail : Gtk.Box {
     // Attachment ids that have been displayed inline
     private Gee.HashSet<string> inlined_content_ids = new Gee.HashSet<string>();
 
+    // A subset of the message's attachments that are displayed in the
+    // attachments view
+    Gee.List<AttachmentInfo> displayed_attachments =
+        new Gee.LinkedList<AttachmentInfo>();
+
+    // A subset of the message's attachments selected by the user
+    Gee.Set<AttachmentInfo> selected_attachments =
+        new Gee.HashSet<AttachmentInfo>();
+
     // Message-specific actions
     private SimpleActionGroup message_actions = new SimpleActionGroup();
 
@@ -83,8 +113,13 @@ public class ConversationEmail : Gtk.Box {
     private Gtk.Box attachments_box;
 
     [GtkChild]
+    private Gtk.IconView attachments_view;
+
+    [GtkChild]
     private Gtk.ListStore attachments_model;
 
+    private Gtk.Menu attachments_menu;
+
     // Fired when the user clicks "reply" in the message menu.
     public signal void reply_to_message(Geary.Email message);
 
@@ -104,11 +139,15 @@ public class ConversationEmail : Gtk.Box {
         Geary.Email email, Geary.NamedFlag? to_add, Geary.NamedFlag? to_remove
     );
 
+
     // Fired on link activation in the web_view
     public signal void link_activated(string link);
 
     // Fired on attachment activation
-    public signal void attachment_activated(Geary.Attachment attachment);
+    public signal void attachments_activated(Gee.Collection<AttachmentInfo> attachments);
+
+    // Fired when the save attachments action is activated
+    public signal void save_attachments(Gee.Collection<AttachmentInfo> attachments);
 
     // Fired the edit draft button is clicked.
     public signal void edit_draft(Geary.Email email);
@@ -138,12 +177,21 @@ public class ConversationEmail : Gtk.Box {
         add_action(ACTION_MARK_UNREAD_DOWN).activate.connect(() => {
                 mark_email_from(this.email, Geary.EmailFlags.UNREAD, null);
             });
+        add_action(ACTION_OPEN_ATTACHMENTS).activate.connect(() => {
+                attachments_activated(selected_attachments);
+            });
         add_action(ACTION_REPLY_ALL).activate.connect(() => {
                 reply_all_message(this.email);
             });
         add_action(ACTION_REPLY_SENDER).activate.connect(() => {
                 reply_to_message(this.email);
             });
+        add_action(ACTION_SAVE_ATTACHMENTS).activate.connect(() => {
+                save_attachments(selected_attachments);
+            });
+        add_action(ACTION_SAVE_ALL_ATTACHMENTS).activate.connect(() => {
+                save_attachments(displayed_attachments);
+            });
         add_action(ACTION_STAR).activate.connect(() => {
                 mark_email(this.email, Geary.EmailFlags.FLAGGED, null);
             });
@@ -184,6 +232,11 @@ public class ConversationEmail : Gtk.Box {
         email_menubutton.set_menu_model((MenuModel) builder.get_object("email_menu"));
         email_menubutton.set_sensitive(false);
 
+        attachments_menu = new Gtk.Menu.from_model(
+            (MenuModel) builder.get_object("attachments_menu")
+        );
+        attachments_menu.attach_to_widget(this, null);
+
         primary_message.infobar_box.pack_start(draft_infobar, false, false, 0);
         if (is_draft) {
             draft_infobar.show();
@@ -341,78 +394,80 @@ public class ConversationEmail : Gtk.Box {
 
     [GtkCallback]
     private void on_attachments_view_activated(Gtk.IconView view, Gtk.TreePath path) {
-        Gtk.TreeIter iter;
-        Value attachment_id;
+        AttachmentInfo attachment_info = attachment_info_for_view_path(path);
+        attachments_activated(
+            Geary.iterate<AttachmentInfo>(attachment_info).to_array_list()
+        );
+    }
 
-        attachments_model.get_iter(out iter, path);
-        attachments_model.get_value(iter, 2, out attachment_id);
+    [GtkCallback]
+    private void on_attachments_view_selection_changed() {
+        selected_attachments.clear();
+        List<Gtk.TreePath> selected = attachments_view.get_selected_items();
+        selected.foreach((path) => {
+                selected_attachments.add(attachment_info_for_view_path(path));
+            });
+    }
 
-        Geary.Attachment? attachment = null;
-        try {
-            attachment = email.get_attachment(attachment_id.get_string());
-        } catch (Error error) {
-            warning("Error getting attachment: %s", error.message);
+    [GtkCallback]
+    private bool on_attachments_view_button_press_event(Gdk.EventButton event) {
+        if (event.button != Gdk.BUTTON_SECONDARY) {
+            return false;
         }
 
-        if (attachment != null) {
-            attachment_activated(attachment);
+        Gtk.TreePath path = attachments_view.get_path_at_pos(
+            (int) event.x, (int) event.y
+            );
+        AttachmentInfo attachment = attachment_info_for_view_path(path);
+        if (!selected_attachments.contains(attachment)) {
+            attachments_view.unselect_all();
+            attachments_view.select_path(path);
         }
+        attachments_menu.popup(null, null, null, event.button, event.time);
+        return false;
     }
 
-    // private void save_attachment(Geary.Attachment attachment) {
-    //     Gee.List<Geary.Attachment> attachments = new Gee.ArrayList<Geary.Attachment>();
-    //     attachments.add(attachment);
-    //     get_viewer().save_attachments(attachments);
-    // }
-
-    // private void show_attachment_menu(Geary.Email email, Geary.Attachment attachment) {
-    //     attachment_menu = build_attachment_menu(email, attachment);
-    //     attachment_menu.show_all();
-    //     attachment_menu.popup(null, null, null, 0, Gtk.get_current_event_time());
-    // }
-
-    // private Gtk.Menu build_attachment_menu(Geary.Email email, Geary.Attachment attachment) {
-    //     Gtk.Menu menu = new Gtk.Menu();
-    //     menu.selection_done.connect(on_attachment_menu_selection_done);
-
-    //     Gtk.MenuItem save_attachment_item = new Gtk.MenuItem.with_mnemonic(_("_Save As..."));
-    //     save_attachment_item.activate.connect(() => save_attachment(attachment));
-    //     menu.append(save_attachment_item);
-
-    //     if (displayed_attachments(email) > 1) {
-    //         Gtk.MenuItem save_all_item = new Gtk.MenuItem.with_mnemonic(_("Save All A_ttachments..."));
-    //         save_all_item.activate.connect(() => save_attachments(email.attachments));
-    //         menu.append(save_all_item);
-    //     }
-
-    //     return menu;
-    // }
+    private AttachmentInfo attachment_info_for_view_path(Gtk.TreePath path) {
+        Gtk.TreeIter iter;
+        attachments_model.get_iter(out iter, path);
+        Value info_value;
+        attachments_model.get_value(iter, 2, out info_value);
+        AttachmentInfo info = (AttachmentInfo) info_value.dup_object();
+        info_value.unset();
+        return info;
+    }
 
     private async void load_attachments(Cancellable load_cancelled) {
-        Gee.List<Geary.Attachment> displayed_attachments =
-            new Gee.LinkedList<Geary.Attachment>();
-
-        // Do we have any attachments to display?
+        // Do we have any attachments to be displayed?
         foreach (Geary.Attachment attachment in email.attachments) {
             if (!(attachment.content_id in inlined_content_ids) &&
                 attachment.content_disposition.disposition_type ==
                     Geary.Mime.DispositionType.ATTACHMENT) {
-                displayed_attachments.add(attachment);
+                displayed_attachments.add(new AttachmentInfo(attachment));
             }
         }
 
         if (displayed_attachments.is_empty) {
+            set_action_enabled(ACTION_OPEN_ATTACHMENTS, false);
+            set_action_enabled(ACTION_SAVE_ATTACHMENTS, false);
+            set_action_enabled(ACTION_SAVE_ALL_ATTACHMENTS, false);
             return;
         }
 
         // Show attachments container. Would like to do this in the
         // ctor but we don't know at that point if any attachments
-        // will be displayed inline
+        // will be displayed inline.
         attachment_icon.set_visible(true);
         primary_message.body_box.pack_start(attachments_box, false, false, 0);
 
         // Add each displayed attachment to the icon view
-        foreach (Geary.Attachment attachment in displayed_attachments) {
+        foreach (AttachmentInfo attachment_info in displayed_attachments) {
+            Geary.Attachment attachment = attachment_info.attachment;
+
+            attachment_info.app = AppInfo.get_default_for_type(
+                attachment.content_type.get_mime_type(), false
+            );
+
             Gdk.Pixbuf? icon =
                 yield load_attachment_icon(attachment, load_cancelled);
             string file_name = null;
@@ -441,7 +496,7 @@ public class ConversationEmail : Gtk.Box {
                 iter,
                 0, icon,
                 1, Markup.printf_escaped("%s\n%s", file_name, file_size),
-                2, attachment.id,
+                2, attachment_info,
                 -1
             );
         }
diff --git a/src/client/conversation-viewer/conversation-viewer.vala 
b/src/client/conversation-viewer/conversation-viewer.vala
index 9cdaef7..485dc6a 100644
--- a/src/client/conversation-viewer/conversation-viewer.vala
+++ b/src/client/conversation-viewer/conversation-viewer.vala
@@ -62,12 +62,6 @@ public class ConversationViewer : Gtk.Stack {
     public signal void mark_emails(Gee.Collection<Geary.EmailIdentifier> emails,
         Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove);
 
-    // Fired when the user opens an attachment.
-    public signal void open_attachment(Geary.Attachment attachment);
-
-    // Fired when the user wants to save one or more attachments.
-    public signal void save_attachments(Gee.List<Geary.Attachment> attachment);
-    
     // Fired when the user wants to save an image buffer to disk
     public signal void save_buffer_to_file(string? filename, Geary.Memory.Buffer buffer);
     
diff --git a/ui/conversation-email.ui b/ui/conversation-email.ui
index 5119148..d867aed 100644
--- a/ui/conversation-email.ui
+++ b/ui/conversation-email.ui
@@ -112,8 +112,8 @@
       <column type="GdkPixbuf"/>
       <!-- column-name label -->
       <column type="gchararray"/>
-      <!-- column-name attachment_id -->
-      <column type="gchararray"/>
+      <!-- column-name attachment_info -->
+      <column type="GObject"/>
     </columns>
   </object>
   <object class="GtkBox" id="attachments_box">
@@ -140,7 +140,9 @@
         <property name="item_orientation">horizontal</property>
         <property name="model">attachments_model</property>
         <property name="spacing">6</property>
+        <signal name="button-press-event" handler="on_attachments_view_button_press_event" swapped="no"/>
         <signal name="item-activated" handler="on_attachments_view_activated" swapped="no"/>
+        <signal name="selection-changed" handler="on_attachments_view_selection_changed" swapped="no"/>
         <child>
           <object class="GtkCellRendererPixbuf" id="icon"/>
           <attributes>
diff --git a/ui/conversation-message-menu.ui b/ui/conversation-message-menu.ui
index f2ae478..fc18731 100644
--- a/ui/conversation-message-menu.ui
+++ b/ui/conversation-message-menu.ui
@@ -5,7 +5,7 @@
     <section>
       <item>
         <attribute name="label" translatable="yes">_Save Attachments</attribute>
-        <attribute name="action">msg.selected</attribute>
+        <attribute name="action">msg.save_all_attachments</attribute>
       </item>
     </section>
     <section>
@@ -47,4 +47,22 @@
       </item>
     </section>
   </menu>
+  <menu id="attachments_menu">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Open</attribute>
+        <attribute name="action">msg.open_attachments</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Save</attribute>
+        <attribute name="action">msg.save_attachments</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Save All</attribute>
+        <attribute name="action">msg.save_all_attachments</attribute>
+      </item>
+    </section>
+  </menu>
 </interface>


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]