Replace Gtk.IconView with FlowBox for displaying email attachments.
This commit is contained in:
parent
a2644f243a
commit
9c813eaacb
6 changed files with 367 additions and 285 deletions
|
|
@ -390,6 +390,7 @@ src/mailer/main.vala
|
|||
[type: gettext/glade]ui/composer-menus.ui
|
||||
[type: gettext/glade]ui/composer-widget.ui
|
||||
[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
|
||||
|
|
|
|||
|
|
@ -1955,8 +1955,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?"),
|
||||
|
|
@ -1969,9 +1968,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,
|
||||
|
|
@ -1979,21 +1986,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2016,11 +2020,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;
|
||||
|
|
@ -2029,10 +2029,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
|
||||
|
|
@ -2057,9 +2057,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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ set(RESOURCE_LIST
|
|||
STRIPBLANKS "composer-menus.ui"
|
||||
STRIPBLANKS "composer-widget.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"
|
||||
|
|
|
|||
50
ui/conversation-email-attachment-view.ui
Normal file
50
ui/conversation-email-attachment-view.ui
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.20.0 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.14"/>
|
||||
<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>
|
||||
|
|
@ -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"/>
|
||||
<child>
|
||||
<object class="GtkCellRendererPixbuf" id="icon"/>
|
||||
<attributes>
|
||||
<attribute name="pixbuf">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="file_name">
|
||||
<property name="xpad">6</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<style>
|
||||
<class name="geary-attachments"/>
|
||||
</style>
|
||||
<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="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="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>
|
||||
<packing>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue