Rename `GearyApplication` => `Application.Client` and `geary-application.vala to `application-client.vala`. Update call sites and test cases.
524 lines
18 KiB
Vala
524 lines
18 KiB
Vala
/*
|
||
* 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, _("Don’t _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;
|
||
}
|
||
}
|