[geary/wip/765516-gtk-widget-conversation-viewer] Replace Gtk.IconView with FlowBox for displaying email attachments.



commit 36227c59da273f8a3d8031737707abeb0ca3778f
Author: Michael James Gratton <mike vee net>
Date:   Tue Aug 16 18:08:21 2016 +1000

    Replace Gtk.IconView with FlowBox for displaying email attachments.

 po/POTFILES.in                                     |    1 +
 src/client/application/geary-controller.vala       |   50 ++--
 .../conversation-viewer/conversation-email.vala    |  423 +++++++++-----------
 ui/CMakeLists.txt                                  |    1 +
 ui/conversation-email-attachment-view.ui           |   50 +++
 ui/conversation-email.ui                           |  119 ++++--
 6 files changed, 363 insertions(+), 281 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 99797ad..1b43efc 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -390,6 +390,7 @@ src/mailer/main.vala
 [type: gettext/glade]ui/composer_accelerators.ui
 [type: gettext/glade]ui/composer.glade
 [type: gettext/glade]ui/conversation-email.ui
+[type: gettext/glade]ui/conversation-email-attachment-view.ui
 [type: gettext/glade]ui/conversation-email-menus.ui
 [type: gettext/glade]ui/conversation-message.ui
 [type: gettext/glade]ui/conversation-message-menus.ui
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index c0dd4b8..afb2313 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -2041,8 +2041,7 @@ public class GearyController : Geary.BaseObject {
         }
     }
 
-    private void on_attachments_activated(
-        Gee.Collection<ConversationEmail.AttachmentInfo> attachments) {
+    private void on_attachments_activated(Gee.Collection<Geary.Attachment> 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 these attachments?"),
@@ -2055,9 +2054,17 @@ public class GearyController : Geary.BaseObject {
             GearyApplication.instance.config.ask_open_attachment = !ask_to_open.is_checked;
         }
 
-        foreach (ConversationEmail.AttachmentInfo info in attachments) {
-            if (info.app == null) {
-                string content_type = info.attachment.content_type.get_mime_type();
+        foreach (Geary.Attachment attachment in attachments) {
+            string gio_content_type = ContentType.from_mime_type(
+                attachment.content_type.get_mime_type()
+            );
+            AppInfo? app = null;
+            if (!ContentType.can_be_executable(gio_content_type) &&
+                !ContentType.is_unknown(gio_content_type)) {
+                app = AppInfo.get_default_for_type(gio_content_type, false);
+            }
+            if (app == null) {
+                string content_type = attachment.content_type.get_mime_type();
                 Gtk.AppChooserDialog app_chooser =
                     new Gtk.AppChooserDialog.for_content_type(
                         this.main_window,
@@ -2065,21 +2072,18 @@ public class GearyController : Geary.BaseObject {
                         content_type
                     );
                 if (app_chooser.run() == Gtk.ResponseType.OK) {
-                    info.app = app_chooser.get_app_info();
+                    app = app_chooser.get_app_info();
                 }
                 app_chooser.hide();
             }
-            if (info.app != null) {
+            if (app != null) {
                 List<File> files = new List<File>();
-                files.append(info.attachment.file);
+                files.append(attachment.file);
                 try {
-                    info.app.launch(files, null);
+                    app.launch(files, null);
                 } catch (Error error) {
-                    warning(
-                        "Failed to launch %s: %s\n",
-                        info.app.get_name(),
-                        error.message
-                    );
+                    warning("Failed to launch %s: %s\n",
+                            app.get_name(), error.message);
                 }
             }
         }
@@ -2102,11 +2106,7 @@ public class GearyController : Geary.BaseObject {
             : Gtk.FileChooserConfirmation.SELECT_AGAIN;
     }
 
-    private void on_save_attachments(
-        Gee.Collection<ConversationEmail.AttachmentInfo> attachments) {
-        if (attachments.size == 0)
-            return;
-
+    private void on_save_attachments(Gee.Collection<Geary.Attachment> attachments) {
         Gtk.FileChooserAction action = (attachments.size == 1)
             ? Gtk.FileChooserAction.SAVE
             : Gtk.FileChooserAction.SELECT_FOLDER;
@@ -2115,10 +2115,10 @@ public class GearyController : Geary.BaseObject {
         if (last_save_directory != null)
             dialog.set_current_folder(last_save_directory.get_path());
         if (attachments.size == 1) {
-            Gee.Iterator<ConversationEmail.AttachmentInfo> it = attachments.iterator();
+            Gee.Iterator<Geary.Attachment> it = attachments.iterator();
             it.next();
-            ConversationEmail.AttachmentInfo info = it.get();
-            dialog.set_current_name(info.attachment.file.get_basename());
+            Geary.Attachment attachment = it.get();
+            dialog.set_current_name(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
@@ -2143,9 +2143,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 (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());
+        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());
             
             if (attachments.size > 1 && dest_file.query_exists() && !do_overwrite_confirmation(dest_file))
                 return;
diff --git a/src/client/conversation-viewer/conversation-email.vala 
b/src/client/conversation-viewer/conversation-email.vala
index 3582b37..14f136a 100644
--- a/src/client/conversation-viewer/conversation-email.vala
+++ b/src/client/conversation-viewer/conversation-email.vala
@@ -17,10 +17,10 @@
  */
 [GtkTemplate (ui = "/org/gnome/Geary/conversation-email.ui")]
 public class ConversationEmail : Gtk.Box {
-
     // This isn't a Gtk.Grid since when added to a Gtk.ListBoxRow the
     // hover style isn't applied to it.
 
+
     /**
      * Iterator that returns all message views in an email view.
      */
@@ -88,18 +88,117 @@ 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
+
+    // Displays an attachment's icon and details
+    [GtkTemplate (ui = "/org/gnome/Geary/conversation-email-attachment-view.ui")]
+    private class AttachmentView : Gtk.Grid {
 
         public Geary.Attachment attachment { get; private set; }
-        public AppInfo? app { get; internal set; default = null; }
 
+        [GtkChild]
+        private Gtk.Image icon;
 
-        internal AttachmentInfo(Geary.Attachment attachment) {
+        [GtkChild]
+        private Gtk.Label filename;
+
+        [GtkChild]
+        private Gtk.Label description;
+
+        private string gio_content_type;
+
+        public AttachmentView(Geary.Attachment attachment) {
             this.attachment = attachment;
+            string mime_content_type = attachment.content_type.get_mime_type();
+            this.gio_content_type = ContentType.from_mime_type(
+                mime_content_type
+            );
+
+            string file_name = null;
+            if (attachment.has_supplied_filename) {
+                file_name = attachment.file.get_basename();
+            }
+            string file_desc = ContentType.get_description(gio_content_type);
+            if (ContentType.is_unknown(gio_content_type)) {
+                // Translators: This is the file type displayed for
+                // attachments with unknown file types.
+                file_desc = _("Unknown");
+            }
+            string file_size = Files.get_filesize_as_string(attachment.filesize);
+
+            // XXX Geary.ImapDb.Attachment will use "none" when
+            // saving attachments with no filename to disk, this
+            // seems to be getting saved to be the filename and
+            // passed back, breaking the has_supplied_filename
+            // test - so check for it here.
+            if (file_name == null ||
+                file_name == "" ||
+                file_name == "none") {
+                // XXX Check for unknown types here and try to guess
+                // using attachment data.
+                file_name = file_desc;
+                file_desc = file_size;
+            } else {
+                // Translators: The first argument will be a
+                // description of the document type, the second will
+                // be a human-friendly size string. For example:
+                // Document (100.9MB)
+                file_desc = _("%s (%s)".printf(file_desc, file_size));
+            }
+            this.filename.set_text(file_name);
+            this.description.set_text(file_desc);
+        }
+
+        internal async void load_icon(Cancellable load_cancelled) {
+            Gdk.Pixbuf? pixbuf = null;
+
+            // XXX We need to hook up to GtkWidget::style-set and
+            // reload the icon when the theme changes.
+
+            int window_scale = get_scale_factor();
+            try {
+                // If the file is an image, use it. Otherwise get the
+                // icon for this mime_type.
+                if (this.attachment.content_type.has_media_type("image")) {
+                    // Get a thumbnail for the image.
+                    // TODO Generate and save the thumbnail when
+                    // extracting the attachments rather than when showing
+                    // them in the viewer.
+                    int preview_size = ATTACHMENT_PREVIEW_SIZE * window_scale;
+                    InputStream stream = yield this.attachment.file.read_async(
+                        Priority.DEFAULT,
+                        load_cancelled
+                    );
+                    pixbuf = yield new Gdk.Pixbuf.from_stream_at_scale_async(
+                        stream, preview_size, preview_size, true, load_cancelled
+                    );
+                    pixbuf = pixbuf.apply_embedded_orientation();
+                } else {
+                    // Load the icon for this mime type
+                    Icon icon = ContentType.get_icon(this.gio_content_type);
+                    Gtk.IconTheme theme = Gtk.IconTheme.get_default();
+                    Gtk.IconLookupFlags flags = Gtk.IconLookupFlags.DIR_LTR;
+                    if (get_direction() == Gtk.TextDirection.RTL) {
+                        flags = Gtk.IconLookupFlags.DIR_RTL;
+                    }
+                    Gtk.IconInfo? icon_info = theme.lookup_by_gicon_for_scale(
+                        icon, ATTACHMENT_ICON_SIZE, window_scale, flags
+                    );
+                    if (icon_info != null) {
+                        pixbuf = yield icon_info.load_icon_async(load_cancelled);
+                    }
+                }
+            } catch (Error error) {
+                debug("Failed to load icon for attachment '%s': %s",
+                      this.attachment.id,
+                      error.message);
+            }
+
+            if (pixbuf != null) {
+                Cairo.Surface surface = Gdk.cairo_surface_create_from_pixbuf(
+                    pixbuf, window_scale, get_window()
+                );
+                this.icon.set_from_surface(surface);
+            }
         }
 
     }
@@ -118,6 +217,7 @@ public class ConversationEmail : Gtk.Box {
     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_SELECT_ALL_ATTACHMENTS = "select_all_attachments";
     private const string ACTION_STAR = "star";
     private const string ACTION_UNSTAR = "unstar";
     private const string ACTION_VIEW_SOURCE = "view_source";
@@ -168,12 +268,8 @@ public class ConversationEmail : Gtk.Box {
 
     // 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>();
+    Gee.Collection<Geary.Attachment> displayed_attachments =
+         new Gee.LinkedList<Geary.Attachment>();
 
     // Message-specific actions
     private SimpleActionGroup message_actions = new SimpleActionGroup();
@@ -206,13 +302,14 @@ public class ConversationEmail : Gtk.Box {
     private Gtk.Grid attachments;
 
     [GtkChild]
-    private Gtk.IconView attachments_view;
+    private Gtk.FlowBox attachments_view;
 
     [GtkChild]
-    private Gtk.ListStore attachments_model;
+    private Gtk.Button select_all_attachments;
 
     private Gtk.Menu attachments_menu;
 
+
     /** Fired when the user clicks "reply" in the message menu. */
     public signal void reply_to_message();
 
@@ -239,10 +336,14 @@ public class ConversationEmail : Gtk.Box {
     public signal void link_activated(string link);
 
     /** Fired when the user activates an attachment. */
-    public signal void attachments_activated(Gee.Collection<AttachmentInfo> attachments);
+    public signal void attachments_activated(
+        Gee.Collection<Geary.Attachment> attachments
+    );
 
     /** Fired when the user saves an attachment. */
-    public signal void save_attachments(Gee.Collection<AttachmentInfo> attachments);
+    public signal void save_attachments(
+        Gee.Collection<Geary.Attachment> attachments
+    );
 
     /** Fired the edit draft button is clicked. */
     public signal void edit_draft();
@@ -253,6 +354,7 @@ public class ConversationEmail : Gtk.Box {
     /** Fired when the user selects text in a message. */
     internal signal void body_selection_changed(bool has_selection);
 
+
     /**
      * Constructs a new view to display an email.
      *
@@ -281,8 +383,8 @@ public class ConversationEmail : Gtk.Box {
         add_action(ACTION_MARK_UNREAD_DOWN).activate.connect(() => {
                 mark_email_from_here(Geary.EmailFlags.UNREAD, null);
             });
-        add_action(ACTION_OPEN_ATTACHMENTS).activate.connect(() => {
-                attachments_activated(selected_attachments);
+        add_action(ACTION_OPEN_ATTACHMENTS, false).activate.connect(() => {
+                attachments_activated(get_selected_attachments());
             });
         add_action(ACTION_REPLY_ALL).activate.connect(() => {
                 reply_all_message();
@@ -290,11 +392,14 @@ public class ConversationEmail : Gtk.Box {
         add_action(ACTION_REPLY_SENDER).activate.connect(() => {
                 reply_to_message();
             });
-        add_action(ACTION_SAVE_ATTACHMENTS).activate.connect(() => {
-                save_attachments(selected_attachments);
+        add_action(ACTION_SAVE_ATTACHMENTS, false).activate.connect(() => {
+                save_attachments(get_selected_attachments());
             });
-        add_action(ACTION_SAVE_ALL_ATTACHMENTS).activate.connect(() => {
-                save_attachments(displayed_attachments);
+        add_action(ACTION_SAVE_ALL_ATTACHMENTS, false).activate.connect(() => {
+                save_attachments(this.displayed_attachments);
+            });
+        add_action(ACTION_SELECT_ALL_ATTACHMENTS, false).activate.connect(() => {
+                this.attachments_view.select_all();
             });
         add_action(ACTION_STAR).activate.connect(() => {
                 mark_email(Geary.EmailFlags.FLAGGED, null);
@@ -465,8 +570,9 @@ public class ConversationEmail : Gtk.Box {
         return new MessageViewIterator(this);
     }
 
-    private SimpleAction add_action(string name) {
+    private SimpleAction add_action(string name, bool enabled = true) {
         SimpleAction action = new SimpleAction(name, null);
+        action.set_enabled(enabled);
         message_actions.add_action(action);
         return action;
     }
@@ -527,8 +633,63 @@ public class ConversationEmail : Gtk.Box {
         }
     }
 
+    private async void load_attachments(Cancellable load_cancelled) {
+        // Do we have any attachments to be displayed? This relies on
+        // the primary and any attached message bodies having being
+        // already loaded, so that we know which attachments have been
+        // shown inline and hence do not need to be included here.
+        foreach (Geary.Attachment attachment in email.attachments) {
+            if (!(attachment.content_id in inlined_content_ids)) {
+                Geary.Mime.DispositionType? disposition = null;
+                if (attachment.content_disposition != null) {
+                    disposition = attachment.content_disposition.disposition_type;
+                }
+                // Display both any attachment and inline parts that
+                // have already not been inlined. Although any inline
+                // parts should be referred to by other content in a
+                // multipart/related or multipart/alternative
+                // container, or inlined if in a multipart/mixed
+                // container, this cannot be not guaranteed. C.f. Bug
+                // 769868.
+                if (disposition != null &&
+                    disposition == Geary.Mime.DispositionType.ATTACHMENT ||
+                    disposition == Geary.Mime.DispositionType.INLINE) {
+                    this.displayed_attachments.add(attachment);
+                }
+            }
+        }
+
+        if (!this.displayed_attachments.is_empty) {
+            this.attachments_button.show();
+            this.attachments_button.set_sensitive(!this.is_collapsed);
+            this.primary_message.body.add(this.attachments);
+
+            if (this.displayed_attachments.size > 1) {
+                this.select_all_attachments.show();
+                set_action_enabled(ACTION_SELECT_ALL_ATTACHMENTS, true);
+            }
+
+            foreach (Geary.Attachment attachment in this.displayed_attachments) {
+                AttachmentView view = new AttachmentView(attachment);
+                this.attachments_view.add(view);
+                yield view.load_icon(load_cancelled);
+            }
+        }
+    }
+
+    internal Gee.Collection<Geary.Attachment> get_selected_attachments() {
+        Gee.LinkedList<Geary.Attachment> selected =
+            new Gee.LinkedList<Geary.Attachment>();
+        foreach (Gtk.FlowBoxChild child in
+                 this.attachments_view.get_selected_children()) {
+            selected.add(((AttachmentView) child.get_child()).attachment);
+        }
+        return selected;
+    }
+
     private void print() {
-        // XXX this isn't anywhere near good enough
+        // XXX This isn't anywhere near good enough - headers aren't
+        // being printed.
         primary_message.web_view.get_main_frame().print();
     }
 
@@ -573,209 +734,23 @@ public class ConversationEmail : Gtk.Box {
     }
 
     [GtkCallback]
-    private void on_attachments_view_activated(Gtk.IconView view, Gtk.TreePath path) {
-        AttachmentInfo attachment_info = attachment_info_for_view_path(path);
+    private void on_attachments_child_activated(Gtk.FlowBox view,
+                                                Gtk.FlowBoxChild child) {
         attachments_activated(
-            Geary.iterate<AttachmentInfo>(attachment_info).to_array_list()
+            Geary.iterate<Geary.Attachment>(
+                ((AttachmentView) child.get_child()).attachment
+            ).to_array_list()
         );
     }
 
     [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));
-            });
-    }
-
-    [GtkCallback]
-    private bool on_attachments_view_button_press_event(Gdk.EventButton event) {
-        if (event.button != Gdk.BUTTON_SECONDARY) {
-            return false;
-        }
-
-        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 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) {
-        // Do we have any attachments to be displayed?
-        foreach (Geary.Attachment attachment in email.attachments) {
-            if (!(attachment.content_id in inlined_content_ids)) {
-                Geary.Mime.DispositionType? disposition = null;
-                if (attachment.content_disposition != null) {
-                    disposition = attachment.content_disposition.disposition_type;
-                }
-                // Display both any attachment and inline parts that
-                // have already not been inlined. Although any inline
-                // parts should be referred to by other content in a
-                // multipart/related or multipart/alternative
-                // container, or inlined if in a multipart/mixed
-                // container, this cannot be not guaranteed. C.f. Bug
-                // 769868.
-                if (disposition != null &&
-                    disposition == Geary.Mime.DispositionType.ATTACHMENT ||
-                    disposition == Geary.Mime.DispositionType.INLINE) {
-                    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 attachment widgets. Would like to do this in the
-        // ctor but we don't know at that point if any attachments
-        // will be displayed inline.
-        this.attachments_button.show();
-        this.attachments_button.set_sensitive(!this.is_collapsed);
-        this.primary_message.body.add(this.attachments);
-
-        // Add each displayed attachment to the icon view
-        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;
-            if (attachment.has_supplied_filename) {
-                file_name = attachment.file.get_basename();
-            }
-            // XXX Geary.ImapDb.Attachment will use "none" when
-            // saving attachments with no filename to disk, this
-            // seems to be getting saved to be the filename and
-            // passed back, breaking the has_supplied_filename
-            // test - so check for it here.
-            if (file_name == null ||
-                file_name == "" ||
-                file_name == "none") {
-                // XXX Check for unknown types here and try to guess
-                // using attachment data.
-                file_name = ContentType.get_description(
-                    attachment.content_type.get_mime_type()
-                );
-            }
-            string file_size = Files.get_filesize_as_string(attachment.filesize);
-
-            Gtk.TreeIter iter;
-            attachments_model.append(out iter);
-            attachments_model.set(
-                iter,
-                0, icon,
-                1, Markup.printf_escaped("%s\n%s", file_name, file_size),
-                2, attachment_info,
-                -1
-            );
-        }
-    }
-
-    private async Gdk.Pixbuf? load_attachment_icon(Geary.Attachment attachment,
-                                                   Cancellable load_cancelled) {
-        Geary.Mime.ContentType content_type = attachment.content_type;
-        Gdk.Pixbuf? pixbuf = null;
-
-        // Due to Bug 65167, for retina/highdpi displays with
-        // window_scale == 2, GtkCellRendererPixbuf will draw the
-        // pixbuf twice as large and blurry, so clamp it to 1 for now
-        // - this at least gives is the correct size icons, but still
-        // blurry.
-        //int window_scale = get_scale_factor();
-        int window_scale = 1;
-        try {
-            // If the file is an image, use it. Otherwise get the icon
-            // for this mime_type.
-            if (content_type.has_media_type("image")) {
-                // Get a thumbnail for the image.
-                // TODO Generate and save the thumbnail when
-                // extracting the attachments rather than when showing
-                // them in the viewer.
-                int preview_size = ATTACHMENT_PREVIEW_SIZE * window_scale;
-                InputStream stream = yield attachment.file.read_async(
-                    Priority.DEFAULT,
-                    load_cancelled
-                );
-                pixbuf = yield new Gdk.Pixbuf.from_stream_at_scale_async(
-                    stream, preview_size, preview_size, true, load_cancelled
-                );
-                pixbuf = pixbuf.apply_embedded_orientation();
-            } else {
-                // Load the icon for this mime type.
-                string gio_content_type =
-                   ContentType.from_mime_type(content_type.get_mime_type());
-                Icon icon = ContentType.get_icon(gio_content_type);
-                Gtk.IconTheme theme = Gtk.IconTheme.get_default();
-
-                // XXX GTK 3.14 We should be able to replace the
-                // ThemedIcon/LoadableIcon/other cases below with
-                // simply this:
-                // Gtk.IconInfo? icon_info = theme.lookup_by_gicon_for_scale(
-                //     icon, ATTACHMENT_ICON_SIZE, window_scale
-                // );
-                // pixbuf = yield icon_info.load_icon_async(load_cancelled);
-
-                if (icon is ThemedIcon) {
-                    Gtk.IconInfo? icon_info = null;
-                    foreach (string name in ((ThemedIcon) icon).names) {
-                        icon_info = theme.lookup_icon_for_scale(
-                            name, ATTACHMENT_ICON_SIZE, window_scale, 0
-                        );
-                        if (icon_info != null) {
-                            break;
-                        }
-                    }
-                    if (icon_info == null) {
-                        icon_info = theme.lookup_icon_for_scale(
-                            "x-office-document", ATTACHMENT_ICON_SIZE, window_scale, 0
-                        );
-                    }
-                    pixbuf = yield icon_info.load_icon_async(load_cancelled);
-                } else if (icon is LoadableIcon) {
-                    InputStream stream = yield ((LoadableIcon) icon).load_async(
-                        ATTACHMENT_ICON_SIZE, load_cancelled
-                    );
-                    int icon_size = ATTACHMENT_ICON_SIZE * window_scale;
-                    pixbuf = yield new Gdk.Pixbuf.from_stream_at_scale_async(
-                        stream, icon_size, icon_size, true, load_cancelled
-                    );
-                } else {
-                    debug("Unsupported attachment icon type: %s\n",
-                            icon.get_type().name());
-                }
-            }
-        } catch (Error error) {
-            debug("Failed to load icon for attachment '%s': %s",
-                    attachment.id,
-                    error.message);
-        }
-
-        return pixbuf;
+    private void on_attachments_selected_changed(Gtk.FlowBox view) {
+        uint len = view.get_selected_children().length();
+        bool not_empty = len > 0;
+        set_action_enabled(ACTION_OPEN_ATTACHMENTS, not_empty);
+        set_action_enabled(ACTION_SAVE_ATTACHMENTS, not_empty);
+        set_action_enabled(ACTION_SELECT_ALL_ATTACHMENTS,
+                           len < this.displayed_attachments.size);
     }
 
 }
diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt
index c631a9c..72d6302 100644
--- a/ui/CMakeLists.txt
+++ b/ui/CMakeLists.txt
@@ -9,6 +9,7 @@ set(RESOURCE_LIST
   STRIPBLANKS "composer.glade"
   STRIPBLANKS "composer_accelerators.ui"
   STRIPBLANKS "conversation-email.ui"
+  STRIPBLANKS "conversation-email-attachment-view.ui"
   STRIPBLANKS "conversation-email-menus.ui"
   STRIPBLANKS "conversation-message.ui"
   STRIPBLANKS "conversation-message-menus.ui"
diff --git a/ui/conversation-email-attachment-view.ui b/ui/conversation-email-attachment-view.ui
new file mode 100644
index 0000000..92a5756
--- /dev/null
+++ b/ui/conversation-email-attachment-view.ui
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.0 -->
+<interface>
+  <requires lib="gtk+" version="3.20"/>
+  <template class="ConversationEmailAttachmentView" parent="GtkGrid">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="column_spacing">6</property>
+    <child>
+      <object class="GtkImage" id="icon">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="pixel_size">32</property>
+        <property name="icon_name">x-office-document</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">0</property>
+        <property name="height">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="filename">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="valign">end</property>
+        <property name="label">filename.ext</property>
+        <property name="ellipsize">middle</property>
+        <property name="xalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="description">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="valign">start</property>
+        <property name="label">type (size)</property>
+        <property name="xalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">1</property>
+      </packing>
+    </child>
+  </template>
+</interface>
diff --git a/ui/conversation-email.ui b/ui/conversation-email.ui
index 4ce1e99..cad69a1 100644
--- a/ui/conversation-email.ui
+++ b/ui/conversation-email.ui
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!-- Generated with glade 3.20.0 -->
 <interface>
-  <requires lib="gtk+" version="3.12"/>
+  <requires lib="gtk+" version="3.14"/>
   <template class="ConversationEmail" parent="GtkBox">
     <property name="visible">True</property>
     <property name="can_focus">False</property>
@@ -109,17 +109,8 @@
       </packing>
     </child>
   </object>
-  <object class="GtkListStore" id="attachments_model">
-    <columns>
-      <!-- column-name icon -->
-      <column type="GdkPixbuf"/>
-      <!-- column-name label -->
-      <column type="gchararray"/>
-      <!-- column-name attachment_info -->
-      <column type="GObject"/>
-    </columns>
-  </object>
   <object class="GtkGrid" id="attachments">
+    <property name="name">box</property>
     <property name="visible">True</property>
     <property name="can_focus">False</property>
     <property name="hexpand">True</property>
@@ -136,41 +127,105 @@
       </packing>
     </child>
     <child>
-      <object class="GtkIconView" id="attachments_view">
+      <object class="GtkFlowBox" id="attachments_view">
         <property name="visible">True</property>
         <property name="can_focus">True</property>
-        <property name="margin">6</property>
+        <property name="margin_left">6</property>
+        <property name="margin_right">6</property>
+        <property name="margin_top">6</property>
+        <property name="margin_bottom">6</property>
         <property name="hexpand">True</property>
+        <property name="homogeneous">True</property>
+        <property name="column_spacing">6</property>
+        <property name="row_spacing">6</property>
+        <property name="max_children_per_line">4</property>
         <property name="selection_mode">multiple</property>
-        <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"/>
+        <property name="activate_on_single_click">False</property>
+        <signal name="child-activated" handler="on_attachments_child_activated" swapped="no"/>
+        <signal name="selected-children-changed" handler="on_attachments_selected_changed" swapped="no"/>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkActionBar" id="attachments_actions">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <style>
+          <class name="background"/>
+        </style>
+        <child>
+          <object class="GtkButton" id="open_attachments">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="tooltip_text" translatable="yes">Open selected attachments</property>
+            <property name="action_name">eml.open_attachments</property>
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="icon_name">document-open-symbolic</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+          </packing>
+        </child>
         <child>
-          <object class="GtkCellRendererPixbuf" id="icon"/>
-          <attributes>
-            <attribute name="pixbuf">0</attribute>
-          </attributes>
+          <object class="GtkButton" id="save_attachments">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="tooltip_text" translatable="yes">Save selected attachments</property>
+            <property name="action_name">eml.save_attachments</property>
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="icon_name">document-save-symbolic</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+          </packing>
         </child>
         <child>
-          <object class="GtkCellRendererText" id="file_name">
-            <property name="xpad">6</property>
+          <object class="GtkButton" id="select_all_attachments">
+            <property name="visible">False</property>
+            <property name="can_focus">True</property>
+            <property name="tooltip_text" translatable="yes">Select all attachments</property>
+            <property name="action_name">eml.select_all_attachments</property>
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="icon_name">edit-select-all-symbolic</property>
+              </object>
+            </child>
           </object>
-          <attributes>
-            <attribute name="text">1</attribute>
-          </attributes>
+          <packing>
+          </packing>
         </child>
-        <style>
-          <class name="geary-attachments"/>
-        </style>
       </object>
       <packing>
         <property name="left_attach">0</property>
-        <property name="top_attach">1</property>
+        <property name="top_attach">2</property>
       </packing>
     </child>
+    <style>
+      <class name="view"/>
+    </style>
+  </object>
+  <object class="GtkListStore" id="attachments_model">
+    <columns>
+      <!-- column-name icon -->
+      <column type="GdkPixbuf"/>
+      <!-- column-name label -->
+      <column type="gchararray"/>
+      <!-- column-name attachment_info -->
+      <column type="GObject"/>
+    </columns>
   </object>
   <object class="GtkInfoBar" id="draft_infobar">
     <property name="app_paintable">True</property>



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