geary/src/client/components/components-attachment-pane.vala
Michael Gratton 56876365ba Rename GearyApplication class and source to match code style
Rename `GearyApplication` => `Application.Client` and
`geary-application.vala to `application-client.vala`. Update call sites
and test cases.
2019-11-17 21:38:29 +11:00

524 lines
18 KiB
Vala
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright 2019 Michael Gratton <mike@vee.net>
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* Displays the attachment parts for an email.
*
* This can be used in an editable or non-editable context, the UI
* shown will differ slightly based on which is selected.
*/
[GtkTemplate (ui = "/org/gnome/Geary/components-attachment-pane.ui")]
public class Components.AttachmentPane : Gtk.Grid {
private const string GROUP_NAME = "cap";
private const string ACTION_OPEN = "open";
private const string ACTION_OPEN_SELECTED = "open-selected";
private const string ACTION_REMOVE = "remove";
private const string ACTION_REMOVE_SELECTED = "remove-selected";
private const string ACTION_SAVE = "save";
private const string ACTION_SAVE_ALL = "save-all";
private const string ACTION_SAVE_SELECTED = "save-selected";
private const string ACTION_SELECT_ALL = "select-all";
private const ActionEntry[] action_entries = {
{ ACTION_OPEN, on_open, "s" },
{ ACTION_OPEN_SELECTED, on_open_selected },
{ ACTION_REMOVE, on_remove, "s" },
{ ACTION_REMOVE_SELECTED, on_remove_selected },
{ ACTION_SAVE, on_save, "s" },
{ ACTION_SAVE_ALL, on_save_all },
{ ACTION_SAVE_SELECTED, on_save_selected },
{ ACTION_SELECT_ALL, on_select_all },
};
// This exists purely to be able to set key bindings on it.
private class FlowBox : Gtk.FlowBox {
/** Keyboard action to open the currently selected attachments. */
[Signal (action=true)]
public signal void open_attachments();
/** Keyboard action to save the currently selected attachments. */
[Signal (action=true)]
public signal void save_attachments();
/** Keyboard action to remove the currently selected attachments. */
[Signal (action=true)]
public signal void remove_attachments();
}
// Displays an attachment's icon and details
[GtkTemplate (ui = "/org/gnome/Geary/components-attachment-view.ui")]
private class View : Gtk.Grid {
private const int ATTACHMENT_ICON_SIZE = 32;
private const int ATTACHMENT_PREVIEW_SIZE = 64;
public Geary.Attachment attachment { get; private set; }
[GtkChild]
private Gtk.Image icon;
[GtkChild]
private Gtk.Label filename;
[GtkChild]
private Gtk.Label description;
private string gio_content_type;
public View(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 = attachment.content_filename;
string file_desc = GLib.ContentType.get_description(gio_content_type);
if (GLib.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
);
if (Geary.String.is_empty(file_name)) {
// 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(GLib.Cancellable load_cancelled) {
if (load_cancelled.is_cancelled()) {
return;
}
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;
GLib.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
GLib.Icon icon = GLib.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 (GLib.Error error) {
debug("Failed to load icon for attachment '%s': %s",
this.attachment.file.get_path(),
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);
}
}
}
static construct {
// Set up custom keybindings
unowned Gtk.BindingSet bindings = Gtk.BindingSet.by_class(
(ObjectClass) typeof(FlowBox).class_ref()
);
Gtk.BindingEntry.add_signal(
bindings, Gdk.Key.O, Gdk.ModifierType.CONTROL_MASK, "open-attachments", 0
);
Gtk.BindingEntry.add_signal(
bindings, Gdk.Key.S, Gdk.ModifierType.CONTROL_MASK, "save-attachments", 0
);
Gtk.BindingEntry.add_signal(
bindings, Gdk.Key.BackSpace, 0, "remove-attachments", 0
);
Gtk.BindingEntry.add_signal(
bindings, Gdk.Key.Delete, 0, "remove-attachments", 0
);
Gtk.BindingEntry.add_signal(
bindings, Gdk.Key.KP_Delete, 0, "remove-attachments", 0
);
}
/** Determines if this pane's contents can be modified. */
public bool edit_mode { get; private set; }
private Gee.List<Geary.Attachment> attachments =
new Gee.LinkedList<Geary.Attachment>();
private Application.AttachmentManager manager;
private GLib.SimpleActionGroup actions = new GLib.SimpleActionGroup();
[GtkChild]
private Gtk.Grid attachments_container;
[GtkChild]
private Gtk.Button save_button;
[GtkChild]
private Gtk.Button remove_button;
private FlowBox attachments_view;
public AttachmentPane(bool edit_mode,
Application.AttachmentManager manager) {
this.edit_mode = edit_mode;
if (edit_mode) {
save_button.hide();
} else {
remove_button.hide();
}
this.manager = manager;
this.attachments_view = new FlowBox();
this.attachments_view.open_attachments.connect(on_open_selected);
this.attachments_view.remove_attachments.connect(on_remove_selected);
this.attachments_view.save_attachments.connect(on_save_selected);
this.attachments_view.child_activated.connect(on_child_activated);
this.attachments_view.selected_children_changed.connect(on_selected_changed);
this.attachments_view.button_press_event.connect(on_attachment_button_press);
this.attachments_view.popup_menu.connect(on_attachment_popup_menu);
this.attachments_view.activate_on_single_click = false;
this.attachments_view.max_children_per_line = 3;
this.attachments_view.column_spacing = 6;
this.attachments_view.row_spacing = 6;
this.attachments_view.selection_mode = Gtk.SelectionMode.MULTIPLE;
this.attachments_view.hexpand = true;
this.attachments_view.show();
this.attachments_container.add(this.attachments_view);
this.actions.add_action_entries(action_entries, this);
insert_action_group(GROUP_NAME, this.actions);
}
public void add_attachment(Geary.Attachment attachment,
GLib.Cancellable? cancellable) {
View view = new View(attachment);
this.attachments_view.add(view);
this.attachments.add(attachment);
view.load_icon.begin(cancellable);
update_actions();
}
public void open_attachment(Geary.Attachment attachment) {
open_attachments(Geary.Collection.single(attachment));
}
public void save_attachment(Geary.Attachment attachment) {
this.manager.save_attachment.begin(
attachment,
null,
null // No cancellable for the moment, need UI for it
);
}
public void remove_attachment(Geary.Attachment attachment) {
this.attachments.remove(attachment);
this.attachments_view.foreach(child => {
Gtk.FlowBoxChild flow_child = (Gtk.FlowBoxChild) child;
if (((View) flow_child.get_child()).attachment == attachment) {
this.attachments_view.remove(child);
}
});
}
public bool save_all() {
bool ret = false;
if (!this.attachments.is_empty) {
var all = new Gee.ArrayList<Geary.Attachment>();
all.add_all(this.attachments);
this.manager.save_attachments.begin(
all,
null // No cancellable for the moment, need UI for it
);
}
return ret;
}
private Geary.Attachment? get_attachment(GLib.Variant param) {
Geary.Attachment? ret = null;
string path = (string) param;
foreach (var attachment in this.attachments) {
if (attachment.file.get_path() == path) {
ret = attachment;
break;
}
}
return ret;
}
private Gee.Collection<Geary.Attachment> get_selected_attachments() {
var selected = new Gee.LinkedList<Geary.Attachment>();
this.attachments_view.selected_foreach((box, child) => {
selected.add(
((View) child.get_child()).attachment
);
});
return selected;
}
private bool open_selected() {
bool ret = false;
var selected = get_selected_attachments();
if (!selected.is_empty) {
open_attachments(selected);
ret = true;
}
return ret;
}
private bool save_selected() {
bool ret = false;
var selected = get_selected_attachments();
if (!this.edit_mode && !selected.is_empty) {
this.manager.save_attachments.begin(
selected,
null // No cancellable for the moment, need UI for it
);
ret = true;
}
return ret;
}
private bool remove_selected() {
bool ret = false;
GLib.List<unowned Gtk.FlowBoxChild> children =
this.attachments_view.get_selected_children();
if (this.edit_mode && children.length() > 0) {
children.foreach(child => {
this.attachments_view.remove(child);
this.attachments.remove(
((View) child.get_child()).attachment
);
});
ret = true;
}
return ret;
}
private void update_actions() {
uint len = this.attachments_view.get_selected_children().length();
bool not_empty = len > 0;
set_action_enabled(ACTION_OPEN_SELECTED, not_empty);
set_action_enabled(ACTION_REMOVE_SELECTED, not_empty && this.edit_mode);
set_action_enabled(ACTION_SAVE_SELECTED, not_empty && !this.edit_mode);
set_action_enabled(ACTION_SELECT_ALL, len < this.attachments.size);
}
private void open_attachments(Gee.Collection<Geary.Attachment> attachments) {
MainWindow? main = this.get_toplevel() as MainWindow;
if (main != null) {
Application.Client app = main.application;
bool confirmed = true;
if (app.config.ask_open_attachment) {
QuestionDialog ask_to_open = new QuestionDialog.with_checkbox(
main,
_("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, _("Dont _ask me again"), false
);
if (ask_to_open.run() == Gtk.ResponseType.OK) {
app.config.ask_open_attachment = !ask_to_open.is_checked;
} else {
confirmed = false;
}
}
if (confirmed) {
foreach (var attachment in attachments) {
app.show_uri.begin(attachment.file.get_uri());
}
}
}
}
private void set_action_enabled(string name, bool enabled) {
SimpleAction? action = this.actions.lookup_action(name) as SimpleAction;
if (action != null) {
action.set_enabled(enabled);
}
}
private void show_popup(View view, Gdk.EventButton? event) {
Gtk.Builder builder = new Gtk.Builder.from_resource(
"/org/gnome/Geary/components-attachment-pane-menus.ui"
);
var targets = new Gee.HashMap<string,GLib.Variant>();
GLib.Variant target = view.attachment.file.get_path();
targets[ACTION_OPEN] = target;
targets[ACTION_REMOVE] = target;
targets[ACTION_SAVE] = target;
GLib.Menu model = Util.Gtk.copy_menu_with_targets(
(GLib.Menu) builder.get_object("attachments_menu"),
GROUP_NAME,
targets
);
Gtk.Menu menu = new Gtk.Menu.from_model(model);
menu.attach_to_widget(view, null);
if (event != null) {
menu.popup_at_pointer(event);
} else {
menu.popup_at_widget(view, CENTER, SOUTH, null);
}
}
private void beep() {
Gtk.Widget? toplevel = get_toplevel();
if (toplevel == null) {
Gdk.Window? window = toplevel.get_window();
if (window != null) {
window.beep();
}
}
}
private void on_open(GLib.SimpleAction action, GLib.Variant? param) {
var target = get_attachment(param);
if (target != null) {
open_attachment(target);
}
}
private void on_open_selected() {
if (!open_selected()) {
beep();
}
}
private void on_save(GLib.SimpleAction action, GLib.Variant? param) {
var target = get_attachment(param);
if (target != null) {
save_attachment(target);
}
}
private void on_save_all() {
debug("save all!");
if (!save_all()) {
beep();
}
}
private void on_save_selected() {
if (!save_selected()) {
beep();
}
}
private void on_remove(GLib.SimpleAction action, GLib.Variant? param) {
var target = get_attachment(param);
if (target != null) {
remove_attachment(target);
}
}
private void on_remove_selected() {
if (!remove_selected()) {
beep();
}
}
private void on_select_all() {
this.attachments_view.select_all();
}
private void on_child_activated() {
open_selected();
}
private void on_selected_changed() {
update_actions();
}
private bool on_attachment_popup_menu(Gtk.Widget widget) {
bool ret = Gdk.EVENT_PROPAGATE;
Gtk.Window parent = get_toplevel() as Gtk.Window;
if (parent != null) {
Gtk.FlowBoxChild? focus = parent.get_focus() as Gtk.FlowBoxChild;
if (focus != null && focus.parent == this.attachments_view) {
show_popup((View) focus.get_child(), null);
ret = Gdk.EVENT_STOP;
}
}
return ret;
}
private bool on_attachment_button_press(Gtk.Widget widget,
Gdk.EventButton event) {
bool ret = Gdk.EVENT_PROPAGATE;
if (event.triggers_context_menu()) {
Gtk.FlowBoxChild? child = this.attachments_view.get_child_at_pos(
(int) event.x,
(int) event.y
);
if (child != null) {
show_popup((View) child.get_child(), event);
ret = Gdk.EVENT_STOP;
}
}
return ret;
}
}