diff --git a/src/client/components/main-window.vala b/src/client/components/main-window.vala index 6f510205..5617f6a1 100644 --- a/src/client/components/main-window.vala +++ b/src/client/components/main-window.vala @@ -1390,27 +1390,13 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { } private void on_conversation_view_added(ConversationListBox list) { - list.email_added.connect(on_conversation_viewer_email_added); - list.mark_emails.connect(on_mark_messages); - } - - private void on_conversation_viewer_email_added(ConversationEmail view) { - view.forward_message.connect(on_forward_message); - view.reply_all_message.connect(on_reply_all_message); - view.reply_to_message.connect(on_reply_to_message); - view.edit_draft.connect(on_edit_draft); - - Geary.App.Conversation conversation = this.conversation_viewer.current_list.conversation; - bool in_selected_folder = ( - conversation.is_in_base_folder(view.email.id) && - conversation.base_folder == selected_folder - ); - bool supports_trash = in_selected_folder && selected_folder_supports_trash(); - bool supports_delete = in_selected_folder && selected_folder is Geary.FolderSupport.Remove; - view.trash_message.connect(on_trash_message); - view.delete_message.connect(on_delete_message); - view.set_folder_actions_enabled(supports_trash, supports_delete); - this.on_shift_key.connect(view.shift_key_changed); + list.mark_email.connect(on_email_mark); + list.reply_to_all_email.connect(on_email_reply_to_all); + list.reply_to_sender_email.connect(on_email_reply_to_sender); + list.forward_email.connect(on_email_forward); + list.edit_email.connect(on_email_edit); + list.trash_email.connect(on_email_trash); + list.delete_email.connect(on_email_delete); } // Window-level action callbacks @@ -1768,20 +1754,30 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { } } - // Individual message view action callbacks + // Individual conversation email view action callbacks - private void on_mark_messages(Geary.App.Conversation conversation, - Gee.Collection messages, - Geary.EmailFlags? to_add, - Geary.EmailFlags? to_remove) { + private void on_email_mark(ConversationListBox view, + Gee.Collection messages, + Geary.NamedFlag? to_add, + Geary.NamedFlag? to_remove) { Geary.Account? target = this.selected_account; if (target != null) { + Geary.EmailFlags add_flags = null; + if (to_add != null) { + add_flags = new Geary.EmailFlags(); + add_flags.add(to_add); + } + Geary.EmailFlags remove_flags = null; + if (to_remove != null) { + remove_flags = new Geary.EmailFlags(); + remove_flags.add(to_remove); + } this.application.controller.mark_messages.begin( target, - Geary.Collection.single(conversation), + Geary.Collection.single(view.conversation), messages, - to_add, - to_remove, + add_flags, + remove_flags, (obj, res) => { try { this.application.controller.mark_messages.end(res); @@ -1793,58 +1789,49 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { } } - private void on_reply_to_message(ConversationEmail target_view) { - Geary.Account? account = this.selected_account; - if (account != null) { - target_view.get_selection_for_quoting.begin((obj, res) => { - string? quote = target_view.get_selection_for_quoting.end(res); - this.application.controller.compose_with_context_email( - account, REPLY, target_view.email, quote - ); - }); - } - } - - private void on_reply_all_message(ConversationEmail target_view) { - Geary.Account? account = this.selected_account; - if (account != null) { - target_view.get_selection_for_quoting.begin((obj, res) => { - string? quote = target_view.get_selection_for_quoting.end(res); - this.application.controller.compose_with_context_email( - account, REPLY_ALL, target_view.email, quote - ); - }); - } - } - - private void on_forward_message(ConversationEmail target_view) { - Geary.Account? account = this.selected_account; - if (account != null) { - target_view.get_selection_for_quoting.begin((obj, res) => { - string? quote = target_view.get_selection_for_quoting.end(res); - this.application.controller.compose_with_context_email( - account, FORWARD, target_view.email, quote - ); - }); - } - } - - private void on_edit_draft(ConversationEmail target_view) { + private void on_email_reply_to_sender(Geary.Email target, string? quote) { Geary.Account? account = this.selected_account; if (account != null) { this.application.controller.compose_with_context_email( - account, NEW_MESSAGE, target_view.email, null + account, REPLY, target, quote ); } } - private void on_trash_message(ConversationEmail target_view) { + private void on_email_reply_to_all(Geary.Email target, string? quote) { + Geary.Account? account = this.selected_account; + if (account != null) { + this.application.controller.compose_with_context_email( + account, REPLY_ALL, target, quote + ); + } + } + + private void on_email_forward(Geary.Email target, string? quote) { + Geary.Account? account = this.selected_account; + if (account != null) { + this.application.controller.compose_with_context_email( + account, FORWARD, target, quote + ); + } + } + + private void on_email_edit(Geary.Email target) { + Geary.Account? account = this.selected_account; + if (account != null) { + this.application.controller.compose_with_context_email( + account, NEW_MESSAGE, target, null + ); + } + } + + private void on_email_trash(Geary.Email target) { Geary.Folder? source = this.selected_folder; if (source != null) { this.application.controller.move_messages_special.begin( source, TRASH, - Geary.Collection.single(target_view.email.id), + Geary.Collection.single(target.id), (obj, res) => { try { this.application.controller.move_messages_special.end(res); @@ -1856,13 +1843,13 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { } } - private void on_delete_message(ConversationEmail target_view) { + private void on_email_delete(Geary.Email target) { Geary.FolderSupport.Remove? source = this.selected_folder as Geary.FolderSupport.Remove; if (source != null && prompt_delete_messages(1)) { this.application.controller.delete_messages.begin( source, - Geary.Collection.single(target_view.email.id), + Geary.Collection.single(target.id), (obj, res) => { try { this.application.controller.delete_messages.end(res); diff --git a/src/client/conversation-viewer/conversation-email.vala b/src/client/conversation-viewer/conversation-email.vala index e77e6bcb..3e34b304 100644 --- a/src/client/conversation-viewer/conversation-email.vala +++ b/src/client/conversation-viewer/conversation-email.vala @@ -20,6 +20,10 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { // This isn't a Gtk.Grid since when added to a Gtk.ListBoxRow the // hover style isn't applied to it. + private const string MANUAL_READ_CLASS = "geary-manual-read"; + private const string SENT_CLASS = "geary-sent"; + private const string STARRED_CLASS = "geary-starred"; + private const string UNREAD_CLASS = "geary-unread"; /** Fields that must be available for constructing the view. */ internal const Geary.Email.Field REQUIRED_FOR_CONSTRUCT = ( @@ -127,24 +131,20 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { } - private const string ACTION_FORWARD = "forward"; - private const string ACTION_MARK_READ = "mark_read"; - private const string ACTION_MARK_UNREAD = "mark_unread"; - private const string ACTION_MARK_UNREAD_DOWN = "mark_unread_down"; - private const string ACTION_TRASH_MESSAGE = "trash_msg"; - private const string ACTION_DELETE_MESSAGE = "delete_msg"; - private const string ACTION_PRINT = "print"; - private const string ACTION_REPLY_SENDER = "reply_sender"; - private const string ACTION_REPLY_ALL = "reply_all"; - private const string ACTION_SAVE_ALL_ATTACHMENTS = "save_all_attachments"; - private const string ACTION_STAR = "star"; - private const string ACTION_UNSTAR = "unstar"; - private const string ACTION_VIEW_SOURCE = "view_source"; + private static GLib.MenuModel email_menu_template; + private static GLib.MenuModel email_menu_trash_section; + private static GLib.MenuModel email_menu_delete_section; + + + static construct { + Gtk.Builder builder = new Gtk.Builder.from_resource( + "/org/gnome/Geary/conversation-email-menus.ui" + ); + email_menu_template = (GLib.MenuModel) builder.get_object("email_menu"); + email_menu_trash_section = (GLib.MenuModel) builder.get_object("email_menu_trash"); + email_menu_delete_section = (GLib.MenuModel) builder.get_object("email_menu_delete"); + } - private const string MANUAL_READ_CLASS = "geary-manual-read"; - private const string SENT_CLASS = "geary-sent"; - private const string STARRED_CLASS = "geary-starred"; - private const string UNREAD_CLASS = "geary-unread"; /** * The specific email that is displayed by this view. @@ -156,6 +156,22 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { */ public Geary.Email email { get; private set; } + /** Determines if this email currently flagged as unread. */ + public bool is_unread { + get { + Geary.EmailFlags? flags = this.email.email_flags; + return (flags != null && flags.is_unread()); + } + } + + /** Determines if this email currently flagged as starred. */ + public bool is_starred { + get { + Geary.EmailFlags? flags = this.email.email_flags; + return (flags != null && flags.is_flagged()); + } + } + /** Determines if the email is showing a preview or the full message. */ public bool is_collapsed = true; @@ -177,6 +193,10 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { /** The view displaying the email's primary message headers and body. */ public ConversationMessage primary_message { get; private set; } + public Components.AttachmentPane? attachments_pane { + get; private set; default = null; + } + /** Views for attached messages. */ public Gee.List attached_messages { owned get { return this._attached_messages.read_only_view; } @@ -187,6 +207,8 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { /** Determines the message body loading state. */ public LoadState message_body_state { get; private set; default = NOT_STARTED; } + public Geary.App.Conversation conversation; + // Store from which to load message content, if needed private Geary.App.EmailStore email_store; @@ -200,7 +222,6 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { private Geary.TimeoutManager body_loading_timeout; - /** Determines if all message's web views have finished loading. */ private Geary.Nonblocking.Spinlock message_bodies_loaded_lock; @@ -212,8 +233,9 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { private Gee.List displayed_attachments = new Gee.LinkedList(); - // Message-specific actions - private SimpleActionGroup message_actions = new SimpleActionGroup(); + // Tracks if Shift key handler has been installed on the main + // window, for updating email menu trash/delete actions. + private bool shift_handler_installed = false; [GtkChild] private Gtk.Grid actions; @@ -239,45 +261,9 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { [GtkChild] private Gtk.Grid sub_messages; - private Components.AttachmentPane? attachments_pane = null; - - private Menu email_menu; - private Menu email_menu_model; - private Menu email_menu_trash; - private Menu email_menu_delete; - private bool shift_key_down; - - - /** Fired when the user clicks "reply" in the message menu. */ - public signal void reply_to_message(); - - /** Fired when the user clicks "reply all" in the message menu. */ - public signal void reply_all_message(); - - /** Fired when the user clicks "forward" in the message menu. */ - public signal void forward_message(); - - /** Fired when the user updates the email's flags. */ - public signal void mark_email( - Geary.NamedFlag? to_add, Geary.NamedFlag? to_remove - ); - - /** Fired when the user updates flags for this email and all others down. */ - public signal void mark_email_from_here( - Geary.NamedFlag? to_add, Geary.NamedFlag? to_remove - ); - - /** Fired when the user clicks "trash" in the message menu. */ - public signal void trash_message(); - - /** Fired when the user clicks "delete" in the message menu. */ - public signal void delete_message(); - - /** Fired the edit draft button is clicked. */ - public signal void edit_draft(); /** Fired when a internal link is activated */ - public signal void internal_link_activated(int y); + internal signal void internal_link_activated(int y); /** Fired when the user selects text in a message. */ internal signal void body_selection_changed(bool has_selection); @@ -290,7 +276,8 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { * the complete email, but does not attempt any possibly * long-running loading processes. */ - public ConversationEmail(Geary.Email email, + public ConversationEmail(Geary.App.Conversation conversation, + Geary.Email email, Geary.App.EmailStore email_store, Application.ContactStore contacts, Configuration config, @@ -298,6 +285,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { bool is_draft, GLib.Cancellable load_cancellable) { base_ref(); + this.conversation = conversation; this.email = email; this.is_draft = is_draft; this.email_store = email_store; @@ -311,45 +299,6 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { get_style_context().add_class(SENT_CLASS); } - add_action(ACTION_FORWARD).activate.connect(() => { - forward_message(); - }); - add_action(ACTION_PRINT).activate.connect(() => { - print.begin(); - }); - add_action(ACTION_MARK_READ).activate.connect(() => { - mark_email(null, Geary.EmailFlags.UNREAD); - }); - add_action(ACTION_MARK_UNREAD).activate.connect(() => { - mark_email(Geary.EmailFlags.UNREAD, null); - }); - add_action(ACTION_MARK_UNREAD_DOWN).activate.connect(() => { - mark_email_from_here(Geary.EmailFlags.UNREAD, null); - }); - add_action(ACTION_TRASH_MESSAGE).activate.connect(() => { - trash_message(); - }); - add_action(ACTION_DELETE_MESSAGE).activate.connect(() => { - delete_message(); - }); - add_action(ACTION_REPLY_ALL).activate.connect(() => { - reply_all_message(); - }); - add_action(ACTION_REPLY_SENDER).activate.connect(() => { - reply_to_message(); - }); - add_action(ACTION_SAVE_ALL_ATTACHMENTS).activate.connect(() => { - this.attachments_pane.save_all(); - }); - add_action(ACTION_STAR).activate.connect(() => { - mark_email(Geary.EmailFlags.FLAGGED, null); - }); - add_action(ACTION_UNSTAR).activate.connect(() => { - mark_email(null, Geary.EmailFlags.FLAGGED); - }); - add_action(ACTION_VIEW_SOURCE).activate.connect(on_view_source); - insert_action_group("eml", message_actions); - // Construct the view for the primary message, hook into it this.primary_message = new ConversationMessage.from_email( @@ -361,30 +310,19 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { connect_message_view_signals(this.primary_message); this.primary_message.summary.add(this.actions); - - // Wire up the rest of the UI - - Gtk.Builder builder = new Gtk.Builder.from_resource( - "/org/gnome/Geary/conversation-email-menus.ui" - ); - this.email_menu = new Menu(); - this.email_menu_model = (Menu) builder.get_object("email_menu"); - this.email_menu_trash = (Menu) builder.get_object("email_menu_trash"); - this.email_menu_delete = (Menu) builder.get_object("email_menu_delete"); - this.email_menubutton.set_menu_model(this.email_menu); - this.email_menubutton.set_sensitive(false); - this.email_menubutton.toggled.connect(this.on_email_menu); - this.primary_message.infobars.add(this.draft_infobar); if (is_draft) { this.draft_infobar.show(); this.draft_infobar.response.connect((infobar, response_id) => { - if (response_id == 1) { edit_draft(); } + if (response_id == 1) { + activate_email_action(ConversationListBox.ACTION_EDIT); + } }); } - this.primary_message.infobars.add(this.not_saved_infobar); + // Wire up the rest of the UI + email_store.account.incoming.notify["current-status"].connect( on_service_status_change ); @@ -483,23 +421,6 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { } } - /** - * Enables or disables actions that require folder support. - */ - public void set_folder_actions_enabled(bool supports_trash, bool supports_delete) { - set_action_enabled(ACTION_TRASH_MESSAGE, supports_trash); - set_action_enabled(ACTION_DELETE_MESSAGE, supports_delete); - } - - /** - * Substitutes the "Delete Message" button for the "Move Message to Trash" - * button if the Shift key is pressed. - */ - public void shift_key_changed(bool pressed) { - this.shift_key_down = pressed; - this.on_email_menu(); - } - /** * Shows the complete message: headers, body and attachments. */ @@ -507,7 +428,16 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { this.is_collapsed = false; update_email_state(); this.attachments_button.set_sensitive(true); - this.email_menubutton.set_sensitive(true); + // Needs at least some menu set otherwise it won't be enabled, + // also has the side effect of making it sensitive + this.email_menubutton.set_menu_model(new GLib.Menu()); + + // Set targets to enable the actions + GLib.Variant email_target = email.id.to_variant(); + this.attachments_button.set_action_target_value(email_target); + this.star_button.set_action_target_value(email_target); + this.unstar_button.set_action_target_value(email_target); + foreach (ConversationMessage message in this) { message.show_message_body(include_transitions); } @@ -521,6 +451,12 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { update_email_state(); attachments_button.set_sensitive(false); email_menubutton.set_sensitive(false); + + // Clear targets to disable the actions + this.attachments_button.set_action_target_value(null); + this.star_button.set_action_target_value(null); + this.unstar_button.set_action_target_value(null); + primary_message.hide_message_body(); foreach (ConversationMessage attached in this._attached_messages) { attached.hide_message_body(); @@ -567,6 +503,115 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { return selection; } + /** Displays the raw RFC 822 source for this email. */ + public async void view_source() { + MainWindow? main = get_toplevel() as MainWindow; + if (main != null) { + Geary.Email email = this.email; + try { + yield Geary.Nonblocking.Concurrent.global.schedule_async( + () => { + string source = ( + email.header.buffer.to_string() + + email.body.buffer.to_string() + ); + string temporary_filename; + int temporary_handle = GLib.FileUtils.open_tmp( + "geary-message-XXXXXX.txt", + out temporary_filename + ); + GLib.FileUtils.set_contents(temporary_filename, source); + GLib.FileUtils.close(temporary_handle); + + // ensure this file is only readable by the + // user ... this needs to be done after the + // file is closed + GLib.FileUtils.chmod( + temporary_filename, + (int) (Posix.S_IRUSR | Posix.S_IWUSR) + ); + + string temporary_uri = GLib.Filename.to_uri( + temporary_filename, null + ); + main.application.show_uri.begin(temporary_uri); + }, + null + ); + } catch (GLib.Error error) { + main.application.controller.report_problem( + new Geary.ProblemReport(error) + ); + } + } + } + + /** Print this view's email. */ + public async void print() throws Error { + Json.Builder builder = new Json.Builder(); + builder.begin_object(); + if (this.email.from != null) { + builder.set_member_name(_("From:")); + builder.add_string_value(this.email.from.to_string()); + } + if (this.email.to != null) { + // Translators: Human-readable version of the RFC 822 To header + builder.set_member_name(_("To:")); + builder.add_string_value(this.email.to.to_string()); + } + if (this.email.cc != null) { + // Translators: Human-readable version of the RFC 822 CC header + builder.set_member_name(_("Cc:")); + builder.add_string_value(this.email.cc.to_string()); + } + if (this.email.bcc != null) { + // Translators: Human-readable version of the RFC 822 BCC header + builder.set_member_name(_("Bcc:")); + builder.add_string_value(this.email.bcc.to_string()); + } + if (this.email.date != null) { + // Translators: Human-readable version of the RFC 822 Date header + builder.set_member_name(_("Date:")); + builder.add_string_value( + Util.Date.pretty_print_verbose( + this.email.date.value.to_local(), + this.config.clock_format + ) + ); + } + if (this.email.subject != null) { + // Translators: Human-readable version of the RFC 822 Subject header + builder.set_member_name(_("Subject:")); + builder.add_string_value(this.email.subject.to_string()); + } + builder.end_object(); + Json.Generator generator = new Json.Generator(); + generator.set_root(builder.get_root()); + string js = "geary.addPrintHeaders(" + generator.to_data(null) + ");"; + yield this.primary_message.web_view.run_javascript(js, null); + + Gtk.Window? window = get_toplevel() as Gtk.Window; + WebKit.PrintOperation op = new WebKit.PrintOperation( + this.primary_message.web_view + ); + Gtk.PrintSettings settings = new Gtk.PrintSettings(); + + if (this.email.subject != null) { + string file_name = Geary.String.reduce_whitespace(this.email.subject.value); + file_name = file_name.replace("/", "_"); + if (file_name.char_count() > 128) { + file_name = Geary.String.safe_byte_substring(file_name, 128); + } + + if (!Geary.String.is_empty(file_name)) { + settings.set(Gtk.PRINT_SETTINGS_OUTPUT_BASENAME, file_name); + } + } + + op.set_print_settings(settings); + op.run_dialog(window); + } + /** * Returns a new Iterable over all message views in this email view */ @@ -574,31 +619,6 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { return new MessageViewIterator(this); } - 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; - } - - private bool get_action_enabled(string name) { - SimpleAction? action = - this.message_actions.lookup_action(name) as SimpleAction; - if (action != null) { - return action.get_enabled(); - } else { - return false; - } - } - - private void set_action_enabled(string name, bool enabled) { - SimpleAction? action = - this.message_actions.lookup_action(name) as SimpleAction; - if (action != null) { - action.set_enabled(enabled); - } - } - private void connect_message_view_signals(ConversationMessage view) { view.flag_remote_images.connect(on_flag_remote_images); view.internal_link_activated.connect((y) => { @@ -715,37 +735,103 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { } private void update_email_state() { - Geary.EmailFlags? flags = this.email.email_flags; Gtk.StyleContext style = get_style_context(); - bool is_unread = (flags != null && flags.is_unread()); - set_action_enabled(ACTION_MARK_READ, is_unread); - set_action_enabled(ACTION_MARK_UNREAD, !is_unread); - set_action_enabled(ACTION_MARK_UNREAD_DOWN, !is_unread); - if (is_unread) { + if (this.is_unread) { style.add_class(UNREAD_CLASS); } else { style.remove_class(UNREAD_CLASS); } - bool is_flagged = (flags != null && flags.is_flagged()); - set_action_enabled(ACTION_STAR, !this.is_collapsed && !is_flagged); - set_action_enabled(ACTION_UNSTAR, !this.is_collapsed && is_flagged); - if (is_flagged) { + if (this.is_starred) { style.add_class(STARRED_CLASS); - star_button.hide(); - unstar_button.show(); + this.star_button.hide(); + this.unstar_button.show(); } else { style.remove_class(STARRED_CLASS); - star_button.show(); - unstar_button.hide(); + this.star_button.show(); + this.unstar_button.hide(); } - if (flags != null && flags.is_outbox_sent()) { + if (this.email.email_flags != null && + this.email.email_flags.is_outbox_sent()) { this.not_saved_infobar.show(); } + + update_email_menu(); } + private void update_email_menu() { + if (this.email_menubutton.active) { + bool in_base_folder = this.conversation.is_in_base_folder( + this.email.id + ); + bool supports_trash = ( + in_base_folder && + Application.Controller.does_folder_support_trash( + this.conversation.base_folder + ) + ); + bool supports_delete = ( + in_base_folder && + this.conversation.base_folder is Geary.FolderSupport.Remove + ); + bool is_shift_down = false; + MainWindow? main = get_toplevel() as MainWindow; + if (main != null) { + is_shift_down = main.is_shift_down; + + if (!this.shift_handler_installed) { + this.shift_handler_installed = true; + main.notify["is-shift-down"].connect(on_shift_changed); + } + } + + string[] blacklist = {}; + if (this.is_unread) { + blacklist += ( + ConversationListBox.EMAIL_ACTION_GROUP_NAME + "." + + ConversationListBox.ACTION_MARK_UNREAD + ); + blacklist += ( + ConversationListBox.EMAIL_ACTION_GROUP_NAME + "." + + ConversationListBox.ACTION_MARK_UNREAD_DOWN + ); + } else { + blacklist += ( + ConversationListBox.EMAIL_ACTION_GROUP_NAME + "." + + ConversationListBox.ACTION_MARK_READ + ); + } + + bool show_trash = !is_shift_down && supports_trash; + bool show_delete = !show_trash && supports_delete; + GLib.Variant email_target = email.id.to_variant(); + GLib.Menu new_model = Util.Gtk.construct_menu( + email_menu_template, + (menu, submenu, action, item) => { + bool accept = true; + if (submenu == email_menu_trash_section && !show_trash) { + accept = false; + } + if (submenu == email_menu_delete_section && !show_delete) { + accept = false; + } + if (action != null && !(action in blacklist)) { + item.set_action_and_target_value( + action, email_target + ); + } + return accept; + } + ); + + this.email_menubutton.popover.bind_model(new_model, null); + this.email_menubutton.popover.grab_focus(); + } + } + + private void update_displayed_attachments() { bool has_attachments = !this.displayed_attachments.is_empty; this.attachments_button.set_visible(has_attachments); @@ -787,130 +873,22 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { return (this.email_store.account.incoming.current_status == CONNECTED); } - /** - * Updates the email menu if it is open. - */ - private void on_email_menu() { - if (this.email_menubutton.active) { - this.email_menu.remove_all(); - - bool supports_trash = get_action_enabled(ACTION_TRASH_MESSAGE); - bool supports_delete = get_action_enabled(ACTION_DELETE_MESSAGE); - bool show_trash_button = !this.shift_key_down && (supports_trash || !supports_delete); - Util.Gtk.menu_foreach(this.email_menu_model, (label, name, target, section) => { - if ((section != this.email_menu_trash || show_trash_button) && - (section != this.email_menu_delete || !show_trash_button)) { - this.email_menu.append_item(new MenuItem.section(label, section)); - } - }); - } - } - - private async void view_source() { - MainWindow? main = get_toplevel() as MainWindow; - if (main != null) { - Geary.Email email = this.email; - try { - yield Geary.Nonblocking.Concurrent.global.schedule_async( - () => { - string source = ( - email.header.buffer.to_string() + - email.body.buffer.to_string() - ); - string temporary_filename; - int temporary_handle = GLib.FileUtils.open_tmp( - "geary-message-XXXXXX.txt", - out temporary_filename - ); - GLib.FileUtils.set_contents(temporary_filename, source); - GLib.FileUtils.close(temporary_handle); - - // ensure this file is only readable by the - // user ... this needs to be done after the - // file is closed - GLib.FileUtils.chmod( - temporary_filename, - (int) (Posix.S_IRUSR | Posix.S_IWUSR) - ); - - string temporary_uri = GLib.Filename.to_uri( - temporary_filename, null - ); - main.application.show_uri.begin(temporary_uri); - }, - null - ); - } catch (GLib.Error error) { - main.application.controller.report_problem( - new Geary.ProblemReport(error) - ); - } - } - } - - private async void print() throws Error { - Json.Builder builder = new Json.Builder(); - builder.begin_object(); - if (this.email.from != null) { - builder.set_member_name(_("From:")); - builder.add_string_value(this.email.from.to_string()); - } - if (this.email.to != null) { - // Translators: Human-readable version of the RFC 822 To header - builder.set_member_name(_("To:")); - builder.add_string_value(this.email.to.to_string()); - } - if (this.email.cc != null) { - // Translators: Human-readable version of the RFC 822 CC header - builder.set_member_name(_("Cc:")); - builder.add_string_value(this.email.cc.to_string()); - } - if (this.email.bcc != null) { - // Translators: Human-readable version of the RFC 822 BCC header - builder.set_member_name(_("Bcc:")); - builder.add_string_value(this.email.bcc.to_string()); - } - if (this.email.date != null) { - // Translators: Human-readable version of the RFC 822 Date header - builder.set_member_name(_("Date:")); - builder.add_string_value( - Util.Date.pretty_print_verbose( - this.email.date.value.to_local(), - this.config.clock_format - ) - ); - } - if (this.email.subject != null) { - // Translators: Human-readable version of the RFC 822 Subject header - builder.set_member_name(_("Subject:")); - builder.add_string_value(this.email.subject.to_string()); - } - builder.end_object(); - Json.Generator generator = new Json.Generator(); - generator.set_root(builder.get_root()); - string js = "geary.addPrintHeaders(" + generator.to_data(null) + ");"; - yield this.primary_message.web_view.run_javascript(js, null); - - Gtk.Window? window = get_toplevel() as Gtk.Window; - WebKit.PrintOperation op = new WebKit.PrintOperation( - this.primary_message.web_view + private void activate_email_action(string name) { + GLib.ActionGroup? email_actions = get_action_group( + ConversationListBox.EMAIL_ACTION_GROUP_NAME ); - Gtk.PrintSettings settings = new Gtk.PrintSettings(); - - if (this.email.subject != null) { - string file_name = Geary.String.reduce_whitespace(this.email.subject.value); - file_name = file_name.replace("/", "_"); - if (file_name.char_count() > 128) { - file_name = Geary.String.safe_byte_substring(file_name, 128); - } - - if (!Geary.String.is_empty(file_name)) { - settings.set(Gtk.PRINT_SETTINGS_OUTPUT_BASENAME, file_name); - } + if (email_actions != null) { + email_actions.activate_action(name, this.email.id.to_variant()); } + } - op.set_print_settings(settings); - op.run_dialog(window); + [GtkCallback] + private void on_email_menu() { + update_email_menu(); + } + + private void on_shift_changed() { + update_email_menu(); } private void on_body_loading_timeout() { @@ -921,12 +899,8 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { this.body_loading_timeout.reset(); } - private void on_flag_remote_images(ConversationMessage view) { - if (!email.email_flags.contains(Geary.EmailFlags.LOAD_REMOTE_IMAGES)) { - // Don't pass a cancellable in to make sure the flag is - // always saved - mark_email(Geary.EmailFlags.LOAD_REMOTE_IMAGES, null); - } + private void on_flag_remote_images() { + activate_email_action(ConversationListBox.ACTION_MARK_LOAD_REMOTE); } private void on_save_image(string uri, @@ -1000,10 +974,6 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface { } } - private void on_view_source() { - this.view_source.begin(); - } - private void on_service_status_change() { if (this.message_body_state == FAILED && !this.load_cancellable.is_cancelled() && diff --git a/src/client/conversation-viewer/conversation-list-box.vala b/src/client/conversation-viewer/conversation-list-box.vala index a2a0f62b..52ca7abf 100644 --- a/src/client/conversation-viewer/conversation-list-box.vala +++ b/src/client/conversation-viewer/conversation-list-box.vala @@ -30,6 +30,24 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { Geary.Email.Field.ORIGINATORS ); + internal const string EMAIL_ACTION_GROUP_NAME = "eml"; + + internal const string ACTION_DELETE = "delete"; + internal const string ACTION_EDIT = "edit"; + internal const string ACTION_FORWARD = "forward"; + internal const string ACTION_MARK_LOAD_REMOTE = "mark-load-remote"; + internal const string ACTION_MARK_READ = "mark-read"; + internal const string ACTION_MARK_STARRED = "mark-starred"; + internal const string ACTION_MARK_UNREAD = "mark-unread"; + internal const string ACTION_MARK_UNREAD_DOWN = "mark-unread-down"; + internal const string ACTION_MARK_UNSTARRED = "mark-unstarred"; + internal const string ACTION_PRINT = "print"; + internal const string ACTION_REPLY_ALL = "reply-all"; + internal const string ACTION_REPLY_SENDER = "reply-sender"; + internal const string ACTION_SAVE_ALL_ATTACHMENTS = "save-all-attachments"; + internal const string ACTION_TRASH = "trash"; + internal const string ACTION_VIEW_SOURCE = "view-source"; + // Offset from the top of the list box which emails views will // scrolled to, so the user can see there are additional messages // above it. XXX This is currently approx 0.5 times the height of @@ -47,6 +65,27 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { // mark it as read private const int MARK_READ_PADDING = 50; + private const string ACTION_TARGET_TYPE = ( + Geary.EmailIdentifier.BASE_VARIANT_TYPE + ); + private const ActionEntry[] email_action_entries = { + { ACTION_DELETE, on_email_delete, ACTION_TARGET_TYPE }, + { ACTION_EDIT, on_email_edit, ACTION_TARGET_TYPE }, + { ACTION_FORWARD, on_email_forward, ACTION_TARGET_TYPE }, + { ACTION_MARK_LOAD_REMOTE, on_email_load_remote, ACTION_TARGET_TYPE }, + { ACTION_MARK_READ, on_email_mark_read, ACTION_TARGET_TYPE }, + { ACTION_MARK_STARRED, on_email_mark_starred, ACTION_TARGET_TYPE }, + { ACTION_MARK_UNREAD, on_email_mark_unread, ACTION_TARGET_TYPE }, + { ACTION_MARK_UNREAD_DOWN, on_email_mark_unread_down, ACTION_TARGET_TYPE }, + { ACTION_MARK_UNSTARRED, on_email_mark_unstarred, ACTION_TARGET_TYPE }, + { ACTION_PRINT, on_email_print, ACTION_TARGET_TYPE }, + { ACTION_REPLY_ALL, on_email_reply_all, ACTION_TARGET_TYPE }, + { ACTION_REPLY_SENDER, on_email_reply_sender, ACTION_TARGET_TYPE }, + { ACTION_SAVE_ALL_ATTACHMENTS, on_email_save_all_attachments, ACTION_TARGET_TYPE }, + { ACTION_TRASH, on_email_trash, ACTION_TARGET_TYPE }, + { ACTION_VIEW_SOURCE, on_email_view_source, ACTION_TARGET_TYPE }, + }; + /** Manages find/search term matching in a conversation. */ public class SearchManager : Geary.BaseObject { @@ -492,6 +531,8 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { private Geary.TimeoutManager mark_read_timer; + private GLib.SimpleActionGroup email_actions = new GLib.SimpleActionGroup(); + /** Keyboard action to scroll the conversation. */ [Signal (action=true)] @@ -536,19 +577,28 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { this.mark_read_timer.start(); } - /** Fired when an email view is added to the conversation list. */ - public signal void email_added(ConversationEmail email); + /** Fired when the user clicks "reply" in the message menu. */ + public signal void reply_to_sender_email(Geary.Email email, string? quote); - /** Fired when an email view is removed from the conversation list. */ - public signal void email_removed(ConversationEmail email); + /** Fired when the user clicks "reply all" in the message menu. */ + public signal void reply_to_all_email(Geary.Email email, string? quote); - /** Fired when the user updates the flags for a set of emails. */ - public signal void mark_emails( - Geary.App.Conversation conversation, - Gee.Collection emails, - Geary.EmailFlags? flags_to_add, - Geary.EmailFlags? flags_to_remove - ); + /** Fired when the user clicks "forward" in the message menu. */ + public signal void forward_email(Geary.Email email, string? quote); + + /** Emitted when email message flags are to be updated. */ + public signal void mark_email(Gee.Collection email, + Geary.NamedFlag? to_add, + Geary.NamedFlag? to_remove); + + /** Fired when the user clicks "trash" in the message menu. */ + public signal void trash_email(Geary.Email email); + + /** Fired when the user clicks "delete" in the message menu. */ + public signal void delete_email(Geary.Email email); + + /** Fired the edit draft button is clicked. */ + public signal void edit_email(Geary.Email email); /** @@ -579,6 +629,9 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { set_adjustment(adjustment); set_sort_func(ConversationListBox.on_sort); + this.email_actions.add_action_entries(email_action_entries, this); + insert_action_group(EMAIL_ACTION_GROUP_NAME, this.email_actions); + this.row_activated.connect(on_row_activated); this.conversation.appended.connect(on_conversation_appended); @@ -923,6 +976,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { } ConversationEmail view = new ConversationEmail( + conversation, email, this.email_store, this.contacts, @@ -931,8 +985,6 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { is_draft(email), this.cancellable ); - view.mark_email.connect(on_mark_email); - view.mark_email_from_here.connect(on_mark_email_from_here); view.internal_link_activated.connect(on_internal_link_activated); view.body_selection_changed.connect((email, has_selection) => { this.body_selected_view = has_selection ? email : null; @@ -957,7 +1009,6 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { } else { insert(row, 0); } - email_added(view); return row; } @@ -967,7 +1018,6 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { EmailRow? row = null; if (this.email_rows.unset(email.id, out row)) { remove(row); - email_removed(row.view); } } @@ -1048,9 +1098,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { }); if (email_ids.size > 0) { - Geary.EmailFlags flags = new Geary.EmailFlags(); - flags.add(Geary.EmailFlags.UNREAD); - mark_emails(this.conversation, email_ids, null, flags); + mark_email(email_ids, null, Geary.EmailFlags.UNREAD); } } @@ -1098,6 +1146,19 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { ); } + private ConversationEmail action_target_to_view(GLib.Variant target) { + Geary.EmailIdentifier? id = null; + try { + id = this.conversation.base_folder.account.to_email_identifier(target); + } catch (Geary.EngineError err) { + debug("Failed to get email id for action target: %s", err.message); + } + debug("XXX have id? %s", (id != null).to_string()); + EmailRow? row = (id != null) ? this.email_rows[id] : null; + debug("XXX have row? %s", (row != null).to_string()); + return (row != null) ? row.view : null; + } + private void on_conversation_appended(Geary.App.Conversation conversation, Geary.Email email) { on_conversation_appended_async.begin(conversation, email); @@ -1135,44 +1196,6 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { row.view.update_flags(email); } - private void on_mark_email(ConversationEmail view, - Geary.NamedFlag? to_add, - Geary.NamedFlag? to_remove) { - Gee.Collection ids = - new Gee.LinkedList(); - ids.add(view.email.id); - mark_emails( - this.conversation, - ids, - flag_to_flags(to_add), - flag_to_flags(to_remove) - ); - } - - private void on_mark_email_from_here(ConversationEmail view, - Geary.NamedFlag? to_add, - Geary.NamedFlag? to_remove) { - Geary.Email email = view.email; - Gee.Collection ids = - new Gee.LinkedList(); - ids.add(email.id); - this.foreach((row) => { - if (row.get_visible()) { - Geary.Email other = ((EmailRow) row).view.email; - if (Geary.Email.compare_sent_date_ascending( - email, other) < 0) { - ids.add(other.id); - } - } - }); - mark_emails( - this.conversation, - ids, - flag_to_flags(to_add), - flag_to_flags(to_remove) - ); - } - private void on_message_body_state_notify(GLib.Object obj, GLib.ParamSpec param) { ConversationEmail? view = obj as ConversationEmail; @@ -1181,15 +1204,6 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { } } - private Geary.EmailFlags? flag_to_flags(Geary.NamedFlag? flag) { - Geary.EmailFlags flags = null; - if (flag != null) { - flags = new Geary.EmailFlags(); - flags.add(flag); - } - return flags; - } - private void on_row_activated(Gtk.ListBoxRow widget) { EmailRow? row = widget as EmailRow; if (row != null) { @@ -1212,4 +1226,169 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface { scroll_to_anchor(row, y); } + // Email action callbacks + + private void on_email_reply_sender(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + view.get_selection_for_quoting.begin((obj, res) => { + string? quote = view.get_selection_for_quoting.end(res); + reply_to_sender_email(view.email, quote); + }); + } + } + + private void on_email_reply_all(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + view.get_selection_for_quoting.begin((obj, res) => { + string? quote = view.get_selection_for_quoting.end(res); + reply_to_all_email(view.email, quote); + }); + } + } + + private void on_email_forward(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + view.get_selection_for_quoting.begin((obj, res) => { + string? quote = view.get_selection_for_quoting.end(res); + forward_email(view.email, quote); + }); + } + } + + private void on_email_mark_read(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + mark_email( + Geary.Collection.single(view.email.id), + null, + Geary.EmailFlags.UNREAD + ); + } + } + + private void on_email_mark_unread(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + mark_email( + Geary.Collection.single(view.email.id), + Geary.EmailFlags.UNREAD, + null + ); + } + } + + private void on_email_mark_unread_down(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + Geary.Email email = view.email; + var ids = new Gee.LinkedList(); + ids.add(email.id); + this.foreach((row) => { + if (row.get_visible()) { + Geary.Email other = ((EmailRow) row).view.email; + if (Geary.Email.compare_sent_date_ascending( + email, other) < 0) { + ids.add(other.id); + } + } + }); + mark_email(ids, Geary.EmailFlags.UNREAD, null); + } + } + + private void on_email_mark_starred(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + mark_email( + Geary.Collection.single(view.email.id), + Geary.EmailFlags.FLAGGED, + null + ); + } + } + + private void on_email_mark_unstarred(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + mark_email( + Geary.Collection.single(view.email.id), + null, + Geary.EmailFlags.FLAGGED + ); + } + } + + private void on_email_load_remote(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + mark_email( + Geary.Collection.single(view.email.id), + Geary.EmailFlags.LOAD_REMOTE_IMAGES, + null + ); + } + } + + private void on_email_edit(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + edit_email(view.email); + } + } + + private void on_email_trash(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + trash_email(view.email); + } + } + + private void on_email_delete(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + delete_email(view.email); + } + } + + private void on_email_save_all_attachments(GLib.SimpleAction action, + GLib.Variant? param) { + debug("XXX save all: %s", param.print(true)); + ConversationEmail? view = action_target_to_view(param); + if (view != null && view.attachments_pane != null) { + debug("XXX really save all"); + view.attachments_pane.save_all(); + } + } + + private void on_email_print(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + view.print.begin(); + } + } + + private void on_email_view_source(GLib.SimpleAction action, + GLib.Variant? param) { + ConversationEmail? view = action_target_to_view(param); + if (view != null) { + view.view_source.begin(); + } + } + } diff --git a/src/client/util/util-gtk.vala b/src/client/util/util-gtk.vala index e25c319c..798ad0ed 100644 --- a/src/client/util/util-gtk.vala +++ b/src/client/util/util-gtk.vala @@ -84,6 +84,83 @@ namespace Util.Gtk { return widget.get_allocated_height() - margin.top - margin.bottom; } + /** + * Constructs a frozen GMenu from an existing model using a visitor. + * + * The visitor is applied to the given template model to each of + * its items, or recursively for any section or submenu. If the + * visitor returns false when passed an item, section or submenu + * then it will be skipped, otherwise it will be added to a new + * menu. + * + * The constructed menu will be returned frozen. + * + * @see MenuVisitor + */ + public GLib.Menu construct_menu(GLib.MenuModel template, + MenuVisitor visitor) { + GLib.Menu model = new GLib.Menu(); + for (int i = 0; i < template.get_n_items(); i++) { + GLib.MenuItem item = new GLib.MenuItem.from_model(template, i); + string? action = null; + GLib.Variant? action_value = item.get_attribute_value( + GLib.Menu.ATTRIBUTE_ACTION, GLib.VariantType.STRING + ); + if (action_value != null) { + action = (string) action_value; + } + GLib.Menu? section = (GLib.Menu) item.get_link( + GLib.Menu.LINK_SECTION + ); + GLib.Menu? submenu = (GLib.Menu) item.get_link( + GLib.Menu.LINK_SUBMENU + ); + + bool append = false; + if (section != null) { + if (visitor(template, section, action, item)) { + append = true; + section = construct_menu(section, visitor); + item.set_section(section); + } + } else if (submenu != null) { + if (visitor(template, submenu, action, item)) { + append = true; + submenu = construct_menu(submenu, visitor); + item.set_submenu(submenu); + } + } else { + append = visitor(template, null, action, item); + } + + if (append) { + model.append_item(item); + } + } + model.freeze(); + return model; + } + + /** + * Visitor for {@link construct_menu}. + * + * Implementations should return true to accept the given child + * menu or menu item, causing it to be included in the new model, + * or false to reject it and cause it to be skipped. + * + * @param existing_menu - current menu or submenu being visited + * @param existing_child_menu - if not null, a child menu that is + * about to be descended into + * @param existing_action - existing fully qualified action name + * of the curent item, if any + * @param new_item - copy of the menu item being visited, which if + * accepted will be added to the new model + */ + public delegate bool MenuVisitor(GLib.MenuModel existing_menu, + GLib.MenuModel? existing_child_menu, + string? existing_action, + GLib.MenuItem? new_item); + /** Copies a GLib menu, setting targets for the given actions. */ public GLib.Menu copy_menu_with_targets(GLib.Menu template, string group, diff --git a/ui/conversation-email-menus.ui b/ui/conversation-email-menus.ui index ecf3176d..5681cec0 100644 --- a/ui/conversation-email-menus.ui +++ b/ui/conversation-email-menus.ui @@ -7,13 +7,13 @@ _Reply - eml.reply_sender + eml.reply-sender mail-reply-sender-symbolic Reply to _All - eml.reply_all + eml.reply-all mail-reply-all-symbolic @@ -28,19 +28,19 @@ _Mark Read - eml.mark_read + eml.mark-read _Mark Unread - eml.mark_unread + eml.mark-unread Mark Unread From _Here - eml.mark_unread_down + eml.mark-unread-down
@@ -48,14 +48,14 @@ Move message to _Trash - eml.trash_msg + eml.trash
_Delete message… - eml.delete_msg + eml.delete
@@ -67,7 +67,7 @@ _View Source - eml.view_source + eml.view-source
diff --git a/ui/conversation-email.ui b/ui/conversation-email.ui index 156f1b0f..f3955dd0 100644 --- a/ui/conversation-email.ui +++ b/ui/conversation-email.ui @@ -26,7 +26,7 @@ True Save all attachments start - eml.save_all_attachments + eml.save-all-attachments none @@ -49,7 +49,7 @@ True Mark this message as starred start - eml.star + eml.mark-starred none @@ -71,7 +71,7 @@ True Mark this message as not starred start - eml.unstar + eml.mark-unstarred none @@ -92,9 +92,9 @@ False True True - Display the message menu start none + True