/* Copyright 2011-2013 Yorba Foundation * * This software is licensed under the GNU Lesser General Public License * (version 2.1 or later). See the COPYING file in this distribution. */ // Primary controller object for Geary. public class GearyController { // Named actions. public const string ACTION_HELP = "GearyHelp"; public const string ACTION_ABOUT = "GearyAbout"; public const string ACTION_QUIT = "GearyQuit"; public const string ACTION_NEW_MESSAGE = "GearyNewMessage"; public const string ACTION_REPLY_TO_MESSAGE = "GearyReplyToMessage"; public const string ACTION_REPLY_ALL_MESSAGE = "GearyReplyAllMessage"; public const string ACTION_FORWARD_MESSAGE = "GearyForwardMessage"; public const string ACTION_DELETE_MESSAGE = "GearyDeleteMessage"; public const string ACTION_ZOOM_IN = "GearyZoomIn"; public const string ACTION_ZOOM_OUT = "GearyZoomOut"; public const string ACTION_ZOOM_NORMAL = "GearyZoomNormal"; public const string ACTION_ACCOUNTS = "GearyAccounts"; public const string ACTION_PREFERENCES = "GearyPreferences"; public const string ACTION_MARK_AS_MENU = "GearyMarkAsMenuButton"; public const string ACTION_MARK_AS_READ = "GearyMarkAsRead"; public const string ACTION_MARK_AS_UNREAD = "GearyMarkAsUnread"; public const string ACTION_MARK_AS_STARRED = "GearyMarkAsStarred"; public const string ACTION_MARK_AS_UNSTARRED = "GearyMarkAsUnStarred"; public const string ACTION_MARK_AS_SPAM = "GearyMarkAsSpam"; public const string ACTION_COPY_MENU = "GearyCopyMenuButton"; public const string ACTION_MOVE_MENU = "GearyMoveMenuButton"; public const string ACTION_GEAR_MENU = "GearyGearMenuButton"; public const int FETCH_EMAIL_CHUNK_COUNT = 200; private const string DELETE_MESSAGE_LABEL = _("_Delete"); private const string DELETE_MESSAGE_TOOLTIP_SINGLE = _("Delete conversation (Delete, Backspace, A)"); private const string DELETE_MESSAGE_TOOLTIP_MULTIPLE = _("Delete conversations (Delete, Backspace, A)"); private const string DELETE_MESSAGE_ICON_NAME = "user-trash-full"; private const string ARCHIVE_MESSAGE_LABEL = _("_Archive"); private const string ARCHIVE_MESSAGE_TOOLTIP_SINGLE = _("Archive conversation (Delete, Backspace, A)"); private const string ARCHIVE_MESSAGE_TOOLTIP_MULTIPLE = _("Archive conversations (Delete, Backspace, A)"); private const string ARCHIVE_MESSAGE_ICON_NAME = "mail-archive"; private const string MARK_AS_SPAM_LABEL = _("Mark as s_pam"); private const string MARK_AS_NOT_SPAM_LABEL = _("Mark as not s_pam"); private const string MARK_MESSAGE_MENU_TOOLTIP_SINGLE = _("Mark conversation"); private const string MARK_MESSAGE_MENU_TOOLTIP_MULTIPLE = _("Mark conversations"); private const string LABEL_MESSAGE_TOOLTIP_SINGLE = _("Add label to conversation"); private const string LABEL_MESSAGE_TOOLTIP_MULTIPLE = _("Add label to conversations"); private const string MOVE_MESSAGE_TOOLTIP_SINGLE = _("Move conversation"); private const string MOVE_MESSAGE_TOOLTIP_MULTIPLE = _("Move conversations"); private const int SELECT_FOLDER_TIMEOUT_MSEC = 100; public MainWindow main_window { get; private set; } private Geary.Account? current_account = null; private Gee.HashMap inboxes = new Gee.HashMap(); private Geary.Folder? current_folder = null; private Geary.ConversationMonitor? current_conversations = null; private Cancellable cancellable_folder = new Cancellable(); private Cancellable cancellable_message = new Cancellable(); private Gee.HashMap inbox_cancellables = new Gee.HashMap(); private int busy_count = 0; private Gee.Set selected_conversations = new Gee.HashSet(); private Geary.Conversation? last_deleted_conversation = null; private Gee.LinkedList composer_windows = new Gee.LinkedList(); private File? last_save_directory = null; private NewMessagesMonitor? new_messages_monitor = null; private NewMessagesIndicator? new_messages_indicator = null; private UnityLauncher? unity_launcher = null; private NotificationBubble? notification_bubble = null; private uint select_folder_timeout_id = 0; private Geary.Folder? folder_to_select = null; private Geary.NonblockingMutex select_folder_mutex = new Geary.NonblockingMutex(); private Geary.Account? account_to_select = null; public GearyController() { // This initializes the IconFactory, important to do before the actions are created (as they // refer to some of Geary's custom icons) IconFactory.instance.init(); // Setup actions. GearyApplication.instance.actions.add_actions(create_actions(), this); GearyApplication.instance.actions.add_toggle_actions(create_toggle_actions(), this); GearyApplication.instance.ui_manager.insert_action_group( GearyApplication.instance.actions, 0); GearyApplication.instance.load_ui_file("accelerators.ui"); // some actions need a little extra help prepare_actions(); // Listen for attempts to close the application. GearyApplication.instance.exiting.connect(on_application_exiting); // Create the main window (must be done after creating actions.) main_window = new MainWindow(); main_window.notify["has-toplevel-focus"].connect(on_has_toplevel_focus); enable_message_buttons(false); // Connect to various UI signals. main_window.conversation_list_view.conversations_selected.connect(on_conversations_selected); main_window.conversation_list_view.load_more.connect(on_load_more); main_window.conversation_list_view.mark_conversation.connect(on_mark_conversation); main_window.conversation_list_view.visible_conversations_changed.connect(on_visible_conversations_changed); main_window.folder_list.folder_selected.connect(on_folder_selected); main_window.folder_list.copy_conversation.connect(on_copy_conversation); main_window.folder_list.move_conversation.connect(on_move_conversation); main_window.main_toolbar.copy_folder_menu.folder_selected.connect(on_copy_conversation); main_window.main_toolbar.move_folder_menu.folder_selected.connect(on_move_conversation); main_window.conversation_viewer.link_selected.connect(on_link_selected); main_window.conversation_viewer.reply_to_message.connect(on_reply_to_message); main_window.conversation_viewer.reply_all_message.connect(on_reply_all_message); main_window.conversation_viewer.forward_message.connect(on_forward_message); main_window.conversation_viewer.mark_message.connect(on_conversation_viewer_mark_message); main_window.conversation_viewer.open_attachment.connect(on_open_attachment); main_window.conversation_viewer.save_attachments.connect(on_save_attachments); new_messages_monitor = new NewMessagesMonitor(should_notify_new_messages); main_window.folder_list.set_new_messages_monitor(new_messages_monitor); // New messages indicator (Ubuntuism) new_messages_indicator = NewMessagesIndicator.create(new_messages_monitor); new_messages_indicator.application_activated.connect(on_indicator_activated_application); new_messages_indicator.composer_activated.connect(on_indicator_activated_composer); new_messages_indicator.inbox_activated.connect(on_indicator_activated_inbox); unity_launcher = new UnityLauncher(new_messages_monitor); // libnotify notification_bubble = new NotificationBubble(new_messages_monitor); notification_bubble.invoked.connect(on_notification_bubble_invoked); // This is fired after the accounts are ready. Geary.Engine.instance.opened.connect(on_engine_opened); main_window.conversation_list_view.grab_focus(); set_busy(false); main_window.show_all(); } ~GearyController() { assert(current_account == null); } private void add_accelerator(string accelerator, string action) { GtkUtil.add_accelerator(GearyApplication.instance.ui_manager, GearyApplication.instance.actions, accelerator, action); } private Gtk.ActionEntry[] create_actions() { Gtk.ActionEntry[] entries = new Gtk.ActionEntry[0]; Gtk.ActionEntry accounts = { ACTION_ACCOUNTS, null, TRANSLATABLE, "M", null, on_accounts }; accounts.label = _("A_ccounts"); entries += accounts; Gtk.ActionEntry prefs = { ACTION_PREFERENCES, Gtk.Stock.PREFERENCES, TRANSLATABLE, "E", null, on_preferences }; prefs.label = _("_Preferences"); entries += prefs; Gtk.ActionEntry help = { ACTION_HELP, Gtk.Stock.HELP, TRANSLATABLE, "F1", null, on_help }; help.label = _("_Help"); entries += help; Gtk.ActionEntry about = { ACTION_ABOUT, Gtk.Stock.ABOUT, TRANSLATABLE, null, null, on_about }; about.label = _("_About"); entries += about; Gtk.ActionEntry quit = { ACTION_QUIT, Gtk.Stock.QUIT, TRANSLATABLE, "Q", null, on_quit }; quit.label = _("_Quit"); entries += quit; Gtk.ActionEntry mark_menu = { ACTION_MARK_AS_MENU, null, TRANSLATABLE, null, null, on_show_mark_menu }; mark_menu.label = _("_Mark as..."); mark_menu.tooltip = MARK_MESSAGE_MENU_TOOLTIP_SINGLE; entries += mark_menu; Gtk.ActionEntry mark_read = { ACTION_MARK_AS_READ, "mail-mark-read", TRANSLATABLE, "I", null, on_mark_as_read }; mark_read.label = _("Mark as _read"); entries += mark_read; add_accelerator("I", ACTION_MARK_AS_READ); Gtk.ActionEntry mark_unread = { ACTION_MARK_AS_UNREAD, "mail-mark-unread", TRANSLATABLE, "U", null, on_mark_as_unread }; mark_unread.label = _("Mark as _unread"); entries += mark_unread; add_accelerator("U", ACTION_MARK_AS_UNREAD); Gtk.ActionEntry mark_starred = { ACTION_MARK_AS_STARRED, "starred", TRANSLATABLE, "S", null, on_mark_as_starred }; mark_starred.label = _("_Star"); entries += mark_starred; Gtk.ActionEntry mark_unstarred = { ACTION_MARK_AS_UNSTARRED, "non-starred", TRANSLATABLE, "D", null, on_mark_as_unstarred }; mark_unstarred.label = _("U_nstar"); entries += mark_unstarred; Gtk.ActionEntry mark_spam = { ACTION_MARK_AS_SPAM, null, TRANSLATABLE, "J", null, on_mark_as_spam }; mark_spam.label = MARK_AS_SPAM_LABEL; entries += mark_spam; add_accelerator("exclam", ACTION_MARK_AS_SPAM); // Exclamation mark (!) Gtk.ActionEntry copy_menu = { ACTION_COPY_MENU, null, TRANSLATABLE, "L", null, null }; copy_menu.label = _("_Label"); entries += copy_menu; Gtk.ActionEntry move_menu = { ACTION_MOVE_MENU, null, TRANSLATABLE, "M", null, null }; move_menu.label = _("_Move"); entries += move_menu; Gtk.ActionEntry new_message = { ACTION_NEW_MESSAGE, null, TRANSLATABLE, "N", null, on_new_message }; new_message.label = _("_New Message"); entries += new_message; add_accelerator("N", ACTION_NEW_MESSAGE); Gtk.ActionEntry reply_to_message = { ACTION_REPLY_TO_MESSAGE, null, TRANSLATABLE, "R", null, on_reply_to_message_action }; reply_to_message.label = _("_Reply"); entries += reply_to_message; add_accelerator("R", ACTION_REPLY_TO_MESSAGE); Gtk.ActionEntry reply_all_message = { ACTION_REPLY_ALL_MESSAGE, null, TRANSLATABLE, "R", null, on_reply_all_message_action }; reply_all_message.label = _("Reply _all"); entries += reply_all_message; add_accelerator("R", ACTION_REPLY_ALL_MESSAGE); Gtk.ActionEntry forward_message = { ACTION_FORWARD_MESSAGE, null, TRANSLATABLE, "L", null, on_forward_message_action }; forward_message.label = _("_Forward"); entries += forward_message; add_accelerator("F", ACTION_FORWARD_MESSAGE); // although this action changes according to Geary.Folder capabilities, set to Archive // until they're known so the "translatable" string doesn't first appear Gtk.ActionEntry delete_message = { ACTION_DELETE_MESSAGE, ARCHIVE_MESSAGE_ICON_NAME, ARCHIVE_MESSAGE_LABEL, "A", null, on_delete_message }; delete_message.tooltip = ARCHIVE_MESSAGE_TOOLTIP_SINGLE; entries += delete_message; add_accelerator("Delete", ACTION_DELETE_MESSAGE); add_accelerator("BackSpace", ACTION_DELETE_MESSAGE); Gtk.ActionEntry zoom_in = { ACTION_ZOOM_IN, null, null, "equal", null, on_zoom_in }; entries += zoom_in; add_accelerator("equal", ACTION_ZOOM_IN); Gtk.ActionEntry zoom_out = { ACTION_ZOOM_OUT, null, null, "minus", null, on_zoom_out }; entries += zoom_out; add_accelerator("minus", ACTION_ZOOM_OUT); Gtk.ActionEntry zoom_normal = { ACTION_ZOOM_NORMAL, null, null, "0", null, on_zoom_normal }; entries += zoom_normal; add_accelerator("0", ACTION_ZOOM_NORMAL); return entries; } private Gtk.ToggleActionEntry[] create_toggle_actions() { Gtk.ToggleActionEntry[] entries = new Gtk.ToggleActionEntry[0]; Gtk.ToggleActionEntry gear_menu = { ACTION_GEAR_MENU, null, null, "F10", null, null, false }; entries += gear_menu; return entries; } private void prepare_actions() { GearyApplication.instance.get_action(ACTION_NEW_MESSAGE).is_important = true; GearyApplication.instance.get_action(ACTION_REPLY_TO_MESSAGE).is_important = true; GearyApplication.instance.get_action(ACTION_REPLY_ALL_MESSAGE).is_important = true; GearyApplication.instance.get_action(ACTION_FORWARD_MESSAGE).is_important = true; GearyApplication.instance.get_action(ACTION_DELETE_MESSAGE).is_important = true; } public async void connect_account_async(Geary.Account account, Cancellable? cancellable = null) { account.folders_available_unavailable.connect(on_folders_available_unavailable); try { yield account.open_async(cancellable); } catch (Error open_err) { // TODO: Better error reporting to user debug("Unable to open account %s: %s", account.to_string(), open_err.message); GearyApplication.instance.panic(); } inbox_cancellables.set(account, new Cancellable()); account.email_sent.connect(on_sent); main_window.folder_list.set_user_folders_root_name(account, _("Labels")); } public async void disconnect_account_async(Geary.Account account, Cancellable? cancellable = null) { cancel_inbox(account); if (current_account == account) { cancel_folder(); cancel_message(); } account.folders_available_unavailable.disconnect(on_folders_available_unavailable); if (main_window.conversation_list_store.account_owner_email == account.information.email) main_window.conversation_list_store.account_owner_email = null; main_window.folder_list.remove_account(account); if (inboxes.has_key(account)) { try { yield inboxes.get(account).close_async(cancellable); } catch (Error close_inbox_err) { debug("Unable to close monitored inbox: %s", close_inbox_err.message); } inboxes.unset(account); } try { yield account.close_async(cancellable); } catch (Error close_err) { debug("Unable to close account %s: %s", account.to_string(), close_err.message); } inbox_cancellables.unset(account); // If there are no accounts available, exit. (This can happen if the user declines to // enter a password on their account.) try { if (get_num_open_accounts() == 0) GearyApplication.instance.exit(); } catch (Error e) { message("Error enumerating accounts: %s", e.message); } } // Returns the number of open accounts. private int get_num_open_accounts() throws Error { int num = 0; foreach (Geary.AccountInformation info in Geary.Engine.instance.get_accounts().values) { Geary.Account a = Geary.Engine.instance.get_account_instance(info); if (a.is_open()) num++; } return num; } private bool is_viewed_conversation(Geary.Conversation? conversation) { return conversation != null && selected_conversations.size > 0 && Geary.Collection.get_first(selected_conversations) == conversation; } // Update widgets and such to match capabilities of the current folder ... sensitivity is handled // by other utility methods private void update_ui() { update_tooltips(); Gtk.Action delete_message = GearyApplication.instance.actions.get_action(ACTION_DELETE_MESSAGE); if (current_folder is Geary.FolderSupportsArchive) { delete_message.label = ARCHIVE_MESSAGE_LABEL; delete_message.icon_name = ARCHIVE_MESSAGE_ICON_NAME; } else { // even if not Geary.FolderSupportsrRemove, use delete icons and label, although they // may be insensitive the entire time delete_message.label = DELETE_MESSAGE_LABEL; delete_message.icon_name = DELETE_MESSAGE_ICON_NAME; } } private void on_folder_selected(Geary.Folder? folder) { debug("Folder %s selected", folder != null ? folder.to_string() : "(null)"); // If the folder is being unset, clear the message list and exit here. if (folder == null) { current_folder = null; main_window.conversation_list_store.clear(); main_window.conversation_viewer.clear(null, null); return; } // To prevent the user from selecting folders too quickly, we actually // schedule the action to happen after a timeout instead of acting // directly. If the user selects another folder during the timeout, // we nix the original timeout and start a new one. if (select_folder_timeout_id != 0) Source.remove(select_folder_timeout_id); folder_to_select = folder; select_folder_timeout_id = Timeout.add(SELECT_FOLDER_TIMEOUT_MSEC, on_select_folder_timeout); } private bool on_select_folder_timeout() { assert(folder_to_select != null); select_folder_timeout_id = 0; do_select_folder.begin(folder_to_select, on_select_folder_completed); folder_to_select = null; return false; } private async void do_select_folder(Geary.Folder folder) throws Error { set_busy(true); cancel_folder(); // This function is not reentrant. It should be, because it can be // called reentrant-ly if you select folders quickly enough. This // mutex lock is a bandaid solution to make the function safe to // reenter. int mutex_token = yield select_folder_mutex.claim_async(cancellable_folder); bool current_is_inbox = inboxes.values.contains(current_folder); Cancellable? conversation_cancellable = (current_is_inbox ? inbox_cancellables.get(folder.account) : cancellable_folder); // stop monitoring for conversations and close the folder (but only if not an inbox, // which we leave open for notifications) if (current_conversations != null) { yield current_conversations.stop_monitoring_async(!current_is_inbox, null); current_conversations = null; } else if (current_folder != null && !current_is_inbox) { yield current_folder.close_async(); } if (folder != null) debug("switching to %s", folder.to_string()); current_folder = folder; current_account = folder.account; main_window.conversation_list_store.set_current_folder(current_folder, conversation_cancellable); main_window.conversation_list_store.account_owner_email = current_account.information.email; main_window.main_toolbar.copy_folder_menu.clear(); main_window.main_toolbar.move_folder_menu.clear(); foreach(Geary.Folder f in current_folder.account.list_folders()) { main_window.main_toolbar.copy_folder_menu.add_folder(f); main_window.main_toolbar.move_folder_menu.add_folder(f); } update_ui(); current_conversations = new Geary.ConversationMonitor(current_folder, false, ConversationListStore.REQUIRED_FIELDS); if (inboxes.values.contains(current_folder)) { // Inbox selected, clear new messages if visible clear_new_messages("do_select_folder (inbox)", null); } current_conversations.scan_started.connect(on_scan_started); current_conversations.scan_error.connect(on_scan_error); current_conversations.scan_completed.connect(on_scan_completed); current_conversations.seed_completed.connect(on_seed_completed); current_conversations.conversation_appended.connect(on_conversation_appended); current_conversations.conversation_trimmed.connect(on_conversation_trimmed); current_conversations.email_flags_changed.connect(on_email_flags_changed); main_window.conversation_list_store.set_conversation_monitor(current_conversations); main_window.conversation_list_view.set_conversation_monitor(current_conversations); if (!current_conversations.is_monitoring) yield current_conversations.start_monitoring_async(FETCH_EMAIL_CHUNK_COUNT, conversation_cancellable); select_folder_mutex.release(ref mutex_token); set_busy(false); } private void on_scan_started() { set_busy(true); } private void on_scan_error(Error err) { set_busy(false); } private void on_scan_completed() { set_busy(false); } private void on_seed_completed() { // Done scanning. Check if we have enough messages to fill the conversation list; if not, // trigger a load_more(); if (!main_window.conversation_list_has_scrollbar()) { debug("Not enough messages, loading more for folder %s", current_folder.to_string()); on_load_more(); } } private void on_notification_bubble_invoked(Geary.Folder? folder, Geary.Email? email) { new_messages_monitor.clear_all_new_messages(); if (folder == null || email == null) return; main_window.folder_list.select_folder(folder); Geary.Conversation? conversation = current_conversations.get_conversation_for_email(email.id); if (conversation != null) main_window.conversation_list_view.select_conversation(conversation); } private void on_indicator_activated_application(uint32 timestamp) { main_window.present_with_time(timestamp); } private void on_indicator_activated_composer(uint32 timestamp) { main_window.present_with_time(timestamp); on_new_message(); } private void on_indicator_activated_inbox(Geary.Folder folder, uint32 timestamp) { main_window.present_with_time(timestamp); main_window.folder_list.select_folder(folder); } private void on_conversation_appended(Geary.Conversation conversation, Gee.Collection email) { if (is_viewed_conversation(conversation)) { do_show_message.begin(conversation.get_emails(Geary.Conversation.Ordering.NONE), cancellable_message, false, on_show_message_completed); } } private void on_conversation_trimmed(Geary.Conversation conversation, Geary.Email email) { if (is_viewed_conversation(conversation)) main_window.conversation_viewer.remove_message(email); } private void on_load_more() { debug("on_load_more"); Geary.EmailIdentifier? low_id = main_window.conversation_list_store.get_lowest_email_id(); if (low_id == null) return; set_busy(true); current_conversations.load_by_id_async.begin(low_id, - FETCH_EMAIL_CHUNK_COUNT, Geary.Folder.ListFlags.EXCLUDING_ID, cancellable_folder, on_load_more_completed); } private void on_load_more_completed(Object? source, AsyncResult result) { debug("on load more completed"); try { current_conversations.load_by_id_async.end(result); } catch (Error err) { debug("Error, unable to load conversations: %s", err.message); } set_busy(false); } private void on_email_flags_changed(Geary.Conversation conversation, Geary.Email email) { main_window.conversation_viewer.update_flags(email); } private void on_select_folder_completed(Object? source, AsyncResult result) { try { do_select_folder.end(result); } catch (Error err) { debug("Unable to select folder: %s", err.message); } } private void on_conversations_selected(Gee.Set selected) { cancel_message(); selected_conversations = selected; // Disable message buttons until conversation loads. enable_message_buttons(false); if (selected.size == 1 && current_folder != null) { Geary.Conversation conversation = Geary.Collection.get_first(selected); do_show_message.begin(conversation.get_emails(Geary.Conversation.Ordering.DATE_ASCENDING), cancellable_message, true, on_conversation_selected_completed); } else if (current_folder != null) { main_window.conversation_viewer.show_multiple_selected(selected.size); if (selected.size > 1) { enable_multiple_message_buttons(); } else { enable_message_buttons(false); } } } private async void do_show_message(Gee.Collection messages, Cancellable? cancellable = null, bool clear_view = true) throws Error { set_busy(true); // Clear view before we yield, to make sure it happens if (clear_view) { main_window.conversation_viewer.clear(current_folder, current_account.information); main_window.conversation_viewer.scroll_reset(); } // Fetch full messages. Gee.Collection messages_to_add = new Gee.HashSet(); foreach (Geary.Email email in messages) { Geary.Email.Field required_fields = ConversationViewer.REQUIRED_FIELDS | Geary.ComposedEmail.REQUIRED_REPLY_FIELDS; Geary.Email full_email; if (email.id.get_folder_path() == null) { full_email = yield current_folder.account.local_fetch_email_async( email.id, required_fields, cancellable); } else { full_email = yield current_folder.fetch_email_async(email.id, required_fields, Geary.Folder.ListFlags.NONE, cancellable); } if (cancellable.is_cancelled()) throw new IOError.CANCELLED("do_select_message cancelled"); messages_to_add.add(full_email); } // Add messages. conversation_viewer.add_message only adds new messages foreach (Geary.Email email in messages_to_add) main_window.conversation_viewer.add_message(email); main_window.conversation_viewer.unhide_last_email(); main_window.conversation_viewer.compress_emails(); } private void on_show_message_completed(Object? source, AsyncResult result) { try { do_show_message.end(result); enable_message_buttons(true); } catch (Error err) { if (!(err is IOError.CANCELLED)) debug("Unable to show message: %s", err.message); } set_busy(false); } private void on_conversation_selected_completed(Object? source, AsyncResult result) { on_show_message_completed(source, result); main_window.conversation_viewer.mark_read(); } private void on_special_folder_type_changed(Geary.Folder folder, Geary.SpecialFolderType old_type, Geary.SpecialFolderType new_type) { main_window.folder_list.remove_folder(folder); main_window.folder_list.add_folder(folder); } private void on_engine_opened() { // Locate the first account so we can select its inbox when available. try { Gee.ArrayList all_accounts = new Gee.ArrayList(); all_accounts.add_all(Geary.Engine.instance.get_accounts().values); if (all_accounts.size == 0) { debug("No accounts found."); return; } all_accounts.sort(Geary.AccountInformation.compare_ascending); account_to_select = Geary.Engine.instance.get_account_instance(all_accounts.get(0)); } catch (Error e) { debug("Error selecting first inbox: %s", e.message); } } // Meant to be called inside the available block of on_folders_available_unavailable, // after we've located the first account. private Geary.Folder? get_initial_selection_folder(Geary.Folder folder_being_added) { if (folder_being_added.account == account_to_select && !main_window.folder_list.is_any_selected() && inboxes.has_key(account_to_select)) { return inboxes.get(account_to_select); } else if (account_to_select == null) { // This is the first account being added, so select the inbox. return inboxes.get(folder_being_added.account); } return null; } private void on_folders_available_unavailable(Gee.Collection? available, Gee.Collection? unavailable) { if (available != null && available.size > 0) { foreach (Geary.Folder folder in available) { main_window.folder_list.add_folder(folder); if (folder.account == current_account) { if (!main_window.main_toolbar.copy_folder_menu.has_folder(folder)) main_window.main_toolbar.copy_folder_menu.add_folder(folder); if (!main_window.main_toolbar.move_folder_menu.has_folder(folder)) main_window.main_toolbar.move_folder_menu.add_folder(folder); } // monitor the Inbox for notifications if (folder.get_special_folder_type() == Geary.SpecialFolderType.INBOX && !inboxes.has_key(folder.account)) { inboxes.set(folder.account, folder); Geary.Folder? select_folder = get_initial_selection_folder(folder); if (select_folder != null) { // First we try to select the Inboxes branch inbox if // it's there, falling back to the main folder list. if (!main_window.folder_list.select_inbox(select_folder.account)) main_window.folder_list.select_folder(select_folder); } folder.open_async.begin(false, inbox_cancellables.get(folder.account)); new_messages_monitor.add_folder(folder, inbox_cancellables.get(folder.account)); } folder.special_folder_type_changed.connect(on_special_folder_type_changed); } } if (unavailable != null) { foreach (Geary.Folder folder in unavailable) { if (folder.get_special_folder_type() == Geary.SpecialFolderType.INBOX && inboxes.has_key(folder.account)) { new_messages_monitor.remove_folder(folder); } } } } private void cancel_folder() { Cancellable old_cancellable = cancellable_folder; cancellable_folder = new Cancellable(); cancel_message(); old_cancellable.cancel(); } private void cancel_inbox(Geary.Account account) { if (!inbox_cancellables.has_key(account)) { debug("Unable to cancel inbox operation for %s", account.to_string()); return; } Cancellable old_cancellable = inbox_cancellables.get(account); inbox_cancellables.set(account, new Cancellable()); old_cancellable.cancel(); } private void cancel_message() { Cancellable old_cancellable = cancellable_message; cancellable_message = new Cancellable(); set_busy(false); old_cancellable.cancel(); } // We need to include the second parameter, or valac doesn't recognize the function as matching // YorbaApplication.exiting's signature. private bool on_application_exiting(YorbaApplication sender, bool panicked) { if (close_composition_windows()) return true; return sender.cancel_exit(); } private void on_quit() { GearyApplication.instance.exit(); } private void on_help() { try { if (GearyApplication.instance.is_installed()) { Gtk.show_uri(null, "ghelp:geary", Gdk.CURRENT_TIME); } else { Pid pid; File exec_dir = GearyApplication.instance.get_exec_dir(); string[] argv = new string[3]; argv[0] = "gnome-help"; argv[1] = GearyApplication.SOURCE_ROOT_DIR + "/help/C/"; argv[2] = null; if (!Process.spawn_async(exec_dir.get_path(), argv, null, SpawnFlags.SEARCH_PATH | SpawnFlags.STDERR_TO_DEV_NULL, null, out pid)) { debug("Failed to launch help locally."); } } } catch (Error error) { debug("Error showing help: %s", error.message); Gtk.Dialog dialog = new Gtk.Dialog.with_buttons("Error", null, Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.Stock.CLOSE, Gtk.ResponseType.CLOSE, null); dialog.response.connect(() => { dialog.destroy(); }); dialog.get_content_area().add(new Gtk.Label("Error showing help: %s".printf(error.message))); dialog.show_all(); dialog.run(); } } private void on_about() { Gtk.show_about_dialog(main_window, "program-name", GearyApplication.NAME, "comments", GearyApplication.DESCRIPTION, "authors", GearyApplication.AUTHORS, "copyright", GearyApplication.COPYRIGHT, "license", GearyApplication.LICENSE, "version", GearyApplication.VERSION, "website", GearyApplication.WEBSITE, "website-label", GearyApplication.WEBSITE_LABEL, "title", _("About %s").printf(GearyApplication.NAME), /// Translators: add your name and email address to receive credit in the About dialog /// For example: Yamada Taro "translator-credits", _("translator-credits") ); } // this signal does not necessarily indicate that the application previously didn't have // focus and now it does private void on_has_toplevel_focus() { clear_new_messages("on_has_toplevel_focus", null); } private void on_accounts() { AccountDialog dialog = new AccountDialog(); dialog.run(); dialog.destroy(); } private void on_preferences() { PreferencesDialog dialog = new PreferencesDialog(GearyApplication.instance.config); dialog.run(); } private Gee.List get_selected_folder_email_ids( bool preview_message_only = false) { Gee.ArrayList ids = new Gee.ArrayList(); foreach (Geary.Conversation conversation in selected_conversations) ids.add_all(get_conversation_email_ids(conversation, true, preview_message_only)); return ids; } private Gee.Collection get_conversation_email_ids(Geary.Conversation conversation, bool folder_email_ids_only = false, bool preview_message_only = false) { if (preview_message_only) { Gee.ArrayList id = new Gee.ArrayList(); Geary.Email? preview_message = conversation.get_latest_email(folder_email_ids_only); if (preview_message != null) id.add(preview_message.id); return id; } else { return conversation.get_email_ids(folder_email_ids_only); } } private void mark_selected_conversations(Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove, bool preview_message_only = false) { Geary.FolderSupportsMark? supports_mark = current_folder as Geary.FolderSupportsMark; if (supports_mark == null) return; // Mark the emails. Gee.List ids = get_selected_folder_email_ids(preview_message_only); if (ids.size > 0) { set_busy(true); supports_mark.mark_email_async.begin(ids, flags_to_add, flags_to_remove, cancellable_message, on_mark_complete); } } private void on_show_mark_menu() { bool unread_selected = false; bool read_selected = false; bool starred_selected = false; bool unstarred_selected = false; foreach (Geary.Conversation conversation in selected_conversations) { if (conversation.is_unread()) unread_selected = true; if (conversation.has_any_read_message()) read_selected = true; if (conversation.is_flagged()) { starred_selected = true; } else { unstarred_selected = true; } } var actions = GearyApplication.instance.actions; actions.get_action(ACTION_MARK_AS_READ).set_visible(unread_selected); actions.get_action(ACTION_MARK_AS_UNREAD).set_visible(read_selected); actions.get_action(ACTION_MARK_AS_STARRED).set_visible(unstarred_selected); actions.get_action(ACTION_MARK_AS_UNSTARRED).set_visible(starred_selected); Geary.Folder? spam_folder = null; try { spam_folder = current_account.get_special_folder(Geary.SpecialFolderType.SPAM); } catch (Error e) { debug("Could not locate special spam folder: %s", e.message); } if (spam_folder != null && current_folder.get_special_folder_type() != Geary.SpecialFolderType.DRAFTS && current_folder.get_special_folder_type() != Geary.SpecialFolderType.OUTBOX) { if (current_folder.get_special_folder_type() == Geary.SpecialFolderType.SPAM) { // We're in the spam folder. actions.get_action(ACTION_MARK_AS_SPAM).sensitive = true; actions.get_action(ACTION_MARK_AS_SPAM).label = MARK_AS_NOT_SPAM_LABEL; } else { // We're not in the spam folder, but we are in a folder that allows mark-as-spam. actions.get_action(ACTION_MARK_AS_SPAM).sensitive = true; actions.get_action(ACTION_MARK_AS_SPAM).label = MARK_AS_SPAM_LABEL; } } else { // No Spam folder, or we're in Drafts/Outbox, so gray-out the option. actions.get_action(ACTION_MARK_AS_SPAM).sensitive = false; actions.get_action(ACTION_MARK_AS_SPAM).label = MARK_AS_SPAM_LABEL; } } private void on_visible_conversations_changed(Gee.Set visible) { clear_new_messages("on_visible_conversations_changed", visible); } private bool should_notify_new_messages(Geary.Folder folder) { // A monitored folder must be selected to squelch notifications; // if conversation list is at top of display, don't display // and don't display if main window has top-level focus return folder != current_folder || main_window.conversation_list_view.vadjustment.value != 0.0 || !main_window.has_toplevel_focus; } // Clears messages if conditions are true: anything in should_notify_new_messages() is // false and the supplied visible messages are visible in the conversation list view private void clear_new_messages(string caller, Gee.Set? supplied) { if (current_folder == null || !new_messages_monitor.get_folders().contains(current_folder) || should_notify_new_messages(current_folder)) return; Gee.Set visible = supplied ?? main_window.conversation_list_view.get_visible_conversations(); foreach (Geary.Conversation conversation in visible) { if (new_messages_monitor.are_any_new_messages(current_folder, conversation.get_email_ids())) { debug("Clearing new messages: %s", caller); new_messages_monitor.clear_new_messages(current_folder); break; } } } private void on_mark_conversation(Geary.Conversation conversation, Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove, bool only_mark_preview = false) { Geary.FolderSupportsMark? supports_mark = current_folder as Geary.FolderSupportsMark; if (supports_mark == null) return; Gee.Collection ids = get_conversation_email_ids(conversation, true, only_mark_preview); if (ids.size > 0) { set_busy(true); supports_mark.mark_email_async.begin(Geary.Collection.to_array_list(ids), flags_to_add, flags_to_remove, cancellable_message, on_mark_complete); } } private void on_conversation_viewer_mark_message(Geary.Email message, Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove) { Geary.FolderSupportsMark? supports_mark = current_folder as Geary.FolderSupportsMark; if (supports_mark == null) return; set_busy(true); supports_mark.mark_single_email_async.begin(message.id, flags_to_add, flags_to_remove, cancellable_message, on_mark_complete); } private void on_mark_as_read() { Geary.EmailFlags flags = new Geary.EmailFlags(); flags.add(Geary.EmailFlags.UNREAD); mark_selected_conversations(null, flags); foreach (Geary.EmailIdentifier id in get_selected_folder_email_ids()) main_window.conversation_viewer.mark_manual_read(id); } private void on_mark_as_unread() { Geary.EmailFlags flags = new Geary.EmailFlags(); flags.add(Geary.EmailFlags.UNREAD); mark_selected_conversations(flags, null); foreach (Geary.EmailIdentifier id in get_selected_folder_email_ids()) main_window.conversation_viewer.mark_manual_read(id); } private void on_mark_as_starred() { Geary.EmailFlags flags = new Geary.EmailFlags(); flags.add(Geary.EmailFlags.FLAGGED); mark_selected_conversations(flags, null, true); } private void on_mark_as_unstarred() { Geary.EmailFlags flags = new Geary.EmailFlags(); flags.add(Geary.EmailFlags.FLAGGED); mark_selected_conversations(null, flags); } private void on_mark_complete() { set_busy(false); } private void on_mark_as_spam() { Geary.Folder? destination_folder = null; if (current_folder.get_special_folder_type() != Geary.SpecialFolderType.SPAM) { // Move to spam folder. try { destination_folder = current_account.get_special_folder(Geary.SpecialFolderType.SPAM); } catch (Error e) { debug("Error getting spam folder: %s", e.message); } } else { // Move out of spam folder, back to inbox. try { destination_folder = current_account.get_special_folder(Geary.SpecialFolderType.INBOX); } catch (Error e) { debug("Error getting inbox folder: %s", e.message); } } if (destination_folder != null) on_move_conversation(destination_folder); } private void on_copy_conversation(Geary.Folder destination) { // Nothing to do if nothing selected. if (selected_conversations == null || selected_conversations.size == 0) return; Gee.List ids = get_selected_folder_email_ids(); if (ids.size == 0) return; Geary.FolderSupportsCopy? supports_copy = current_folder as Geary.FolderSupportsCopy; if (supports_copy == null) return; set_busy(true); supports_copy.copy_email_async.begin(ids, destination.get_path(), cancellable_message, on_copy_complete); } private void on_copy_complete() { set_busy(false); } private void on_move_conversation(Geary.Folder destination) { // Nothing to do if nothing selected. if (selected_conversations == null || selected_conversations.size == 0) return; Gee.List ids = get_selected_folder_email_ids(); if (ids.size == 0) return; Geary.FolderSupportsMove? supports_move = current_folder as Geary.FolderSupportsMove; if (supports_move == null) return; set_busy(true); supports_move.move_email_async.begin(ids, destination.get_path(), cancellable_message, on_move_complete); } private void on_move_complete() { set_busy(false); } private void on_open_attachment(Geary.Attachment attachment) { if (GearyApplication.instance.config.ask_open_attachment) { QuestionDialog ask_to_open = new QuestionDialog.with_checkbox(main_window, _("Are you sure you want to open \"%s\"?").printf(attachment.filename), _("Attachments may cause damage to your system if opened. Only open files from trusted sources."), Gtk.Stock.OPEN, Gtk.Stock.CANCEL, _("Don't _ask me again"), false); if (ask_to_open.run() != Gtk.ResponseType.OK) return; // only save checkbox state if OK was selected GearyApplication.instance.config.ask_open_attachment = !ask_to_open.is_checked; } open_uri("file://" + attachment.filepath); } private bool do_overwrite_confirmation(File to_overwrite) { string primary = _("A file named \"%s\" already exists. Do you want to replace it?").printf( to_overwrite.get_basename()); string secondary = _("The file already exists in \"%s\". Replacing it will overwrite its contents.").printf( to_overwrite.get_parent().get_basename()); ConfirmationDialog dialog = new ConfirmationDialog(main_window, primary, secondary, _("_Replace")); return (dialog.run() == Gtk.ResponseType.OK); } private Gtk.FileChooserConfirmation on_confirm_overwrite(Gtk.FileChooser chooser) { // this is only called when choosing one file return do_overwrite_confirmation(chooser.get_file()) ? Gtk.FileChooserConfirmation.ACCEPT_FILENAME : Gtk.FileChooserConfirmation.SELECT_AGAIN; } private void on_save_attachments(Gee.List attachments) { if (attachments.size == 0) return; Gtk.FileChooserAction action = (attachments.size == 1) ? Gtk.FileChooserAction.SAVE : Gtk.FileChooserAction.SELECT_FOLDER; Gtk.FileChooserDialog dialog = new Gtk.FileChooserDialog(null, main_window, action, Gtk.Stock.CANCEL, Gtk.ResponseType.CANCEL, Gtk.Stock.SAVE, Gtk.ResponseType.ACCEPT, null); if (last_save_directory != null) dialog.set_current_folder(last_save_directory.get_path()); if (attachments.size == 1) { dialog.set_current_name(attachments[0].filename); dialog.set_do_overwrite_confirmation(true); // use custom overwrite confirmation so it looks consistent whether one or many // attachments are being saved dialog.confirm_overwrite.connect(on_confirm_overwrite); } dialog.set_create_folders(true); dialog.set_local_only(false); bool accepted = (dialog.run() == Gtk.ResponseType.ACCEPT); File destination = File.new_for_path(dialog.get_filename()); dialog.destroy(); if (!accepted) return; // Proceeding, save this as last destination directory last_save_directory = (attachments.size == 1) ? destination.get_parent() : destination; debug("Saving attachments to %s", destination.get_path()); // Save each one, checking for overwrite only if multiple attachments are being written foreach (Geary.Attachment attachment in attachments) { File source_file = File.new_for_path(attachment.filepath); File dest_file = (attachments.size == 1) ? destination : destination.get_child(attachment.filename); if (attachments.size > 1 && dest_file.query_exists() && !do_overwrite_confirmation(dest_file)) return; debug("Copying %s to %s...", source_file.get_path(), dest_file.get_path()); source_file.copy_async.begin(dest_file, FileCopyFlags.OVERWRITE, Priority.DEFAULT, null, null, on_save_completed); } } private void on_save_completed(Object? source, AsyncResult result) { try { ((File) source).copy_async.end(result); } catch (Error error) { warning("Failed to copy attachment %s to destination: %s", ((File) source).get_path(), error.message); } } // Opens a link in an external browser. private void open_uri(string _link) { string link = _link; // Support web URLs that ommit the protocol. if (!link.contains(":")) link = "http://" + link; try { Gtk.show_uri(main_window.get_screen(), link, Gdk.CURRENT_TIME); } catch (Error err) { debug("Unable to open URL. %s", err.message); } } private bool close_composition_windows() { // We want to allow the user to cancel a quit when they have unsent text. // We are modifying the list as we go, so we can't simply iterate through it. while (composer_windows.size > 0) { ComposerWindow composer_window = composer_windows.first(); if (!composer_window.should_close()) return false; // This will remove composer_window from composer_windows. // See GearyController.on_composer_window_destroy. composer_window.destroy(); } // If we deleted all composer windows without the user cancelling, we can exit. return true; } private void create_compose_window(ComposerWindow.ComposeType compose_type, Geary.Email? referred = null, string? mailto = null) { if (current_account == null) return; ComposerWindow window; if (mailto != null) window = new ComposerWindow.from_mailto(current_account, mailto); else window = new ComposerWindow(current_account, compose_type, referred); window.set_position(Gtk.WindowPosition.CENTER); window.send.connect(on_send); // We want to keep track of the open composer windows, so we can allow the user to cancel // an exit without losing their data. composer_windows.add(window); window.destroy.connect(on_composer_window_destroy); window.show_all(); } private void on_composer_window_destroy(Gtk.Widget sender) { composer_windows.remove((ComposerWindow) sender); } private void on_new_message() { create_compose_window(ComposerWindow.ComposeType.NEW_MESSAGE); } private void on_reply_to_message(Geary.Email message) { create_compose_window(ComposerWindow.ComposeType.REPLY, message); } private void on_reply_to_message_action() { Geary.Email? message = main_window.conversation_viewer.get_last_message(); if (message != null) on_reply_to_message(message); } private void on_reply_all_message(Geary.Email message) { create_compose_window(ComposerWindow.ComposeType.REPLY_ALL, message); } private void on_reply_all_message_action() { Geary.Email? message = main_window.conversation_viewer.get_last_message(); if (message != null) on_reply_all_message(message); } private void on_forward_message(Geary.Email message) { create_compose_window(ComposerWindow.ComposeType.FORWARD, message); } private void on_forward_message_action() { Geary.Email? message = main_window.conversation_viewer.get_last_message(); if (message != null) on_forward_message(message); } // This method is used for both removing and archive a message; currently Geary only supports // one or the other in a folder private void on_delete_message() { // Prevent deletes of the same conversation from repeating. if (is_viewed_conversation(last_deleted_conversation)) { debug("not archiving/deleting, viewed conversation is last deleted conversation"); return; } // There should always be at least one conversation selected here, otherwise the archive // button is disabled, but better safe than segfaulted. last_deleted_conversation = selected_conversations.size > 0 ? Geary.Collection.get_first(selected_conversations) : null; // If the user clicked the toolbar button, we want to move focus back to the message list. main_window.conversation_list_view.grab_focus(); set_busy(true); delete_messages.begin(get_selected_folder_email_ids(), cancellable_folder, on_delete_messages_completed); } // This method is used for both removing and archive a message; currently Geary only supports // one or the other in a folder. This will try archiving first, then remove. private async void delete_messages(Gee.List ids, Cancellable? cancellable) throws Error { Geary.FolderSupportsArchive? supports_archive = current_folder as Geary.FolderSupportsArchive; if (supports_archive != null) { yield supports_archive.archive_email_async(ids, cancellable); return; } Geary.FolderSupportsRemove? supports_remove = current_folder as Geary.FolderSupportsRemove; if (supports_remove != null) { yield supports_remove.remove_email_async(ids, cancellable); return; } debug("Folder %s supports neither remove nor archive", current_folder.to_string()); } private void on_delete_messages_completed(Object? source, AsyncResult result) { try { delete_messages.end(result); } catch (Error err) { debug("Error, unable to delete messages: %s", err.message); } set_busy(false); } private void on_zoom_in() { main_window.conversation_viewer.web_view.zoom_in(); } private void on_zoom_out() { main_window.conversation_viewer.web_view.zoom_out(); } private void on_zoom_normal() { main_window.conversation_viewer.web_view.zoom_level = 1.0f; } private void on_send(ComposerWindow composer_window) { composer_window.account.send_email_async.begin(composer_window.get_composed_email()); composer_window.destroy(); } private void on_sent(Geary.RFC822.Message rfc822) { NotificationBubble.play_sound("message-sent-email"); } public void set_busy(bool is_busy) { busy_count += is_busy ? 1 : -1; if (busy_count < 0) busy_count = 0; main_window.set_busy(busy_count > 0); } private void on_link_selected(string link) { if (link.down().has_prefix(Geary.ComposedEmail.MAILTO_SCHEME)) { compose_mailto(link); } else { open_uri(link); } } // Disables all single-message buttons and enables all multi-message buttons. public void enable_multiple_message_buttons() { update_tooltips(); // Single message only buttons. GearyApplication.instance.actions.get_action(ACTION_REPLY_TO_MESSAGE).sensitive = false; GearyApplication.instance.actions.get_action(ACTION_REPLY_ALL_MESSAGE).sensitive = false; GearyApplication.instance.actions.get_action(ACTION_FORWARD_MESSAGE).sensitive = false; // Mutliple message buttons. GearyApplication.instance.actions.get_action(ACTION_DELETE_MESSAGE).sensitive = (current_folder is Geary.FolderSupportsRemove) || (current_folder is Geary.FolderSupportsArchive); GearyApplication.instance.actions.get_action(ACTION_MARK_AS_MENU).sensitive = current_folder is Geary.FolderSupportsMark; GearyApplication.instance.actions.get_action(ACTION_COPY_MENU).sensitive = current_folder is Geary.FolderSupportsCopy; GearyApplication.instance.actions.get_action(ACTION_MOVE_MENU).sensitive = current_folder is Geary.FolderSupportsMove; } // Enables or disables the message buttons on the toolbar. public void enable_message_buttons(bool sensitive) { update_tooltips(); GearyApplication.instance.actions.get_action(ACTION_REPLY_TO_MESSAGE).sensitive = sensitive; GearyApplication.instance.actions.get_action(ACTION_REPLY_ALL_MESSAGE).sensitive = sensitive; GearyApplication.instance.actions.get_action(ACTION_FORWARD_MESSAGE).sensitive = sensitive; GearyApplication.instance.actions.get_action(ACTION_DELETE_MESSAGE).sensitive = sensitive && ((current_folder is Geary.FolderSupportsRemove) || (current_folder is Geary.FolderSupportsArchive)); GearyApplication.instance.actions.get_action(ACTION_MARK_AS_MENU).sensitive = sensitive && (current_folder is Geary.FolderSupportsMark); GearyApplication.instance.actions.get_action(ACTION_COPY_MENU).sensitive = sensitive && (current_folder is Geary.FolderSupportsCopy); GearyApplication.instance.actions.get_action(ACTION_MOVE_MENU).sensitive = sensitive && (current_folder is Geary.FolderSupportsMove); } // Updates tooltip text depending on number of conversations selected. private void update_tooltips() { bool single = selected_conversations.size == 1; GearyApplication.instance.actions.get_action(ACTION_MARK_AS_MENU).tooltip = single ? MARK_MESSAGE_MENU_TOOLTIP_SINGLE : MARK_MESSAGE_MENU_TOOLTIP_MULTIPLE; GearyApplication.instance.actions.get_action(ACTION_COPY_MENU).tooltip = single ? LABEL_MESSAGE_TOOLTIP_SINGLE : LABEL_MESSAGE_TOOLTIP_MULTIPLE; GearyApplication.instance.actions.get_action(ACTION_MOVE_MENU).tooltip = single ? MOVE_MESSAGE_TOOLTIP_SINGLE : MOVE_MESSAGE_TOOLTIP_MULTIPLE; if (current_folder is Geary.FolderSupportsArchive) { GearyApplication.instance.actions.get_action(ACTION_DELETE_MESSAGE).tooltip = single ? ARCHIVE_MESSAGE_TOOLTIP_SINGLE : ARCHIVE_MESSAGE_TOOLTIP_MULTIPLE; } else { GearyApplication.instance.actions.get_action(ACTION_DELETE_MESSAGE).tooltip = single ? DELETE_MESSAGE_TOOLTIP_SINGLE : DELETE_MESSAGE_TOOLTIP_MULTIPLE; } } public void compose_mailto(string mailto) { create_compose_window(ComposerWindow.ComposeType.NEW_MESSAGE, null, mailto); } // Returns a list of composer windows for an account, or null if none. public Gee.List? get_composer_windows_for_account(Geary.AccountInformation account) { Gee.List ret = new Gee.LinkedList(); foreach (ComposerWindow cw in composer_windows) { if (cw.account.information == account) ret.add(cw); } return ret.size >= 1 ? ret : null; } }