diff --git a/po/POTFILES.in b/po/POTFILES.in index 5a9ebaf8..533634ab 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -20,9 +20,9 @@ src/client/application/application-certificate-manager.vala src/client/application/application-command.vala src/client/application/application-contact-store.vala src/client/application/application-contact.vala +src/client/application/application-controller.vala src/client/application/application-startup-manager.vala src/client/application/geary-application.vala -src/client/application/geary-controller.vala src/client/application/goa-mediator.vala src/client/application/main.vala src/client/application/secret-mediator.vala diff --git a/src/client/application/geary-controller.vala b/src/client/application/application-controller.vala similarity index 96% rename from src/client/application/geary-controller.vala rename to src/client/application/application-controller.vala index 2f0fba6d..c0d99449 100644 --- a/src/client/application/geary-controller.vala +++ b/src/client/application/application-controller.vala @@ -8,9 +8,11 @@ /** - * Primary controller for a Geary application instance. + * Primary controller for a application instance. + * + * @see GearyAplication */ -public class GearyController : Geary.BaseObject { +public class Application.Controller : Geary.BaseObject { // Named actions. public const string ACTION_REPLY_TO_MESSAGE = "reply-to-message"; @@ -55,7 +57,7 @@ public class GearyController : Geary.BaseObject { static construct { // Translators: File name used in save chooser when saving // attachments that do not otherwise have a name. - GearyController.untitled_file_name = _("Untitled"); + Controller.untitled_file_name = _("Untitled"); } @@ -109,49 +111,64 @@ public class GearyController : Geary.BaseObject { } - /** Determines if the controller is opening or is open. */ + /** Determines if the controller is open. */ public bool is_open { get { - return (this.open_cancellable != null); + return !this.open_cancellable.is_cancelled(); } } public weak GearyApplication application { get; private set; } // circular ref - public Accounts.Manager? account_manager { get; private set; default = null; } + /** Account management for the application. */ + public Accounts.Manager account_manager { get; private set; } - /** Application-wide {@link Application.CertificateManager} instance. */ - public Application.CertificateManager? certificate_manager { - get; private set; default = null; + /** Certificate management for the application. */ + public Application.CertificateManager certificate_manager { + get; private set; } - public MainWindow? main_window { get; private set; default = null; } + /** Desktop notifications for the application. */ + public Notification.Desktop notifications { get; private set; } - public Geary.App.ConversationMonitor? current_conversations { get; private set; default = null; } - - public Application.AvatarStore? avatars { + /** Avatar store for the application. */ + public Application.AvatarStore avatars { get; private set; default = new Application.AvatarStore(); } + /** Contact store cache for the application. */ public ContactListStoreCache contact_list_store_cache { get; private set; default = new ContactListStoreCache(); } - public Notification.Desktop? notifications { get; private set; default = null; } + /** Default main window */ + public MainWindow main_window { get; private set; } - private Geary.Account? current_account = null; + /** Conversations for the current folder, null if none selected */ + public Geary.App.ConversationMonitor? current_conversations { + get; private set; default = null; + } + + // Primary collection of the application's open accounts private Gee.Map accounts = new Gee.HashMap(); - // Created when controller is opened, cancelled and nulled out - // when closed. - private GLib.Cancellable? open_cancellable = null; + // Cancelled if the controller is closed + private GLib.Cancellable open_cancellable; - private Folks.IndividualAggregator? folks = null; - - private Canberra.Context? sound_context = null; + private UpgradeDialog upgrade_dialog; + private Folks.IndividualAggregator folks; + private Canberra.Context sound_context; + private NewMessagesMonitor new_messages_monitor; + private NewMessagesIndicator new_messages_indicator; + private UnityLauncher unity_launcher; + // Null if none selected private Geary.Folder? current_folder = null; + + // Null if no folder ever selected + private Geary.Account? current_account = null; + private Cancellable cancellable_folder = new Cancellable(); private Cancellable cancellable_search = new Cancellable(); private Cancellable cancellable_open_account = new Cancellable(); @@ -159,14 +176,10 @@ public class GearyController : Geary.BaseObject { private Gee.Set selected_conversations = new Gee.HashSet(); private Geary.App.Conversation? last_deleted_conversation = null; private Gee.LinkedList composer_widgets = new Gee.LinkedList(); - private NewMessagesMonitor? new_messages_monitor = null; - private NewMessagesIndicator? new_messages_indicator = null; - private UnityLauncher? unity_launcher = null; private uint select_folder_timeout_id = 0; private int64 next_folder_select_allowed_usec = 0; private Geary.Nonblocking.Mutex select_folder_mutex = new Geary.Nonblocking.Mutex(); private Geary.Folder? previous_non_search_folder = null; - private UpgradeDialog upgrade_dialog; private Gee.List pending_mailtos = new Gee.ArrayList(); private uint operation_count = 0; @@ -229,18 +242,11 @@ public class GearyController : Geary.BaseObject { /** * Constructs a new instance of the controller. */ - public GearyController(GearyApplication application) { + public async Controller(GearyApplication application, + GLib.Cancellable cancellable) { this.application = application; - } + this.open_cancellable = cancellable; - ~GearyController() { - assert(current_account == null); - } - - /** - * Starts the controller and brings up Geary. - */ - public async void open_async(GLib.Cancellable? cancellable) { Geary.Engine engine = this.application.engine; // This initializes the IconFactory, important to do before @@ -248,16 +254,14 @@ public class GearyController : Geary.BaseObject { // custom icons) IconFactory.instance.init(); - apply_app_menu_fix(); - - this.open_cancellable = new GLib.Cancellable(); - // Listen for attempts to close the application. this.application.exiting.connect(on_application_exiting); // Create DB upgrade dialog. - upgrade_dialog = new UpgradeDialog(); - upgrade_dialog.notify[UpgradeDialog.PROP_VISIBLE_NAME].connect(display_main_window_if_ready); + this.upgrade_dialog = new UpgradeDialog(); + this.upgrade_dialog.notify[UpgradeDialog.PROP_VISIBLE_NAME].connect( + display_main_window_if_ready + ); // Initialise WebKit and WebViews ClientWebView.init_web_context( @@ -324,7 +328,7 @@ public class GearyController : Geary.BaseObject { this.get_contact_store_for_account, this.should_notify_new_messages ); - main_window.folder_list.set_new_messages_monitor(new_messages_monitor); + main_window.folder_list.set_new_messages_monitor(this.new_messages_monitor); // New messages indicator (Ubuntuism) this.new_messages_indicator = NewMessagesIndicator.create( @@ -334,12 +338,12 @@ public class GearyController : Geary.BaseObject { this.new_messages_indicator.composer_activated.connect(on_indicator_activated_composer); this.new_messages_indicator.inbox_activated.connect(on_indicator_activated_inbox); - unity_launcher = new UnityLauncher(new_messages_monitor); + this.unity_launcher = new UnityLauncher(this.new_messages_monitor); this.notifications = new Notification.Desktop( this.new_messages_monitor, this.application, - this.open_cancellable + cancellable ); this.main_window.conversation_list_view.grab_focus(); @@ -416,50 +420,39 @@ public class GearyController : Geary.BaseObject { this.expunge_accounts.begin(); } - /** - * At the moment, this is non-reversible, i.e. once closed a GearyController cannot be - * re-opened. - */ + /** Closes all accounts and windows, releasing held resources. */ public async void close_async() { // Cancel internal processes early so they don't block // shutdown this.open_cancellable.cancel(); - this.open_cancellable = null; this.application.engine.account_available.disconnect(on_account_available); - if (this.main_window != null) { - // Release folder and conversations in the main window - on_conversations_selected(new Gee.HashSet()); - on_folder_selected(null); + // Release folder and conversations in the main window + on_conversations_selected(new Gee.HashSet()); + on_folder_selected(null); - // Disconnect from various UI signals. - this.main_window.conversation_list_view.conversations_selected.disconnect(on_conversations_selected); - this.main_window.conversation_list_view.conversation_activated.disconnect(on_conversation_activated); - this.main_window.conversation_list_view.load_more.disconnect(on_load_more); - this.main_window.conversation_list_view.mark_conversations.disconnect(on_mark_conversations); - this.main_window.conversation_list_view.visible_conversations_changed.disconnect(on_visible_conversations_changed); - this.main_window.folder_list.folder_selected.disconnect(on_folder_selected); - this.main_window.folder_list.copy_conversation.disconnect(on_copy_conversation); - this.main_window.folder_list.move_conversation.disconnect(on_move_conversation); - this.main_window.main_toolbar.copy_folder_menu.folder_selected.disconnect(on_copy_conversation); - this.main_window.main_toolbar.move_folder_menu.folder_selected.disconnect(on_move_conversation); - this.main_window.conversation_viewer.conversation_added.disconnect( - on_conversation_view_added - ); + // Disconnect from various UI signals. + this.main_window.conversation_list_view.conversations_selected.disconnect(on_conversations_selected); + this.main_window.conversation_list_view.conversation_activated.disconnect(on_conversation_activated); + this.main_window.conversation_list_view.load_more.disconnect(on_load_more); + this.main_window.conversation_list_view.mark_conversations.disconnect(on_mark_conversations); + this.main_window.conversation_list_view.visible_conversations_changed.disconnect(on_visible_conversations_changed); + this.main_window.folder_list.folder_selected.disconnect(on_folder_selected); + this.main_window.folder_list.copy_conversation.disconnect(on_copy_conversation); + this.main_window.folder_list.move_conversation.disconnect(on_move_conversation); + this.main_window.main_toolbar.copy_folder_menu.folder_selected.disconnect(on_copy_conversation); + this.main_window.main_toolbar.move_folder_menu.folder_selected.disconnect(on_move_conversation); + this.main_window.conversation_viewer.conversation_added.disconnect( + on_conversation_view_added + ); - // hide window while shutting down, as this can take a few - // seconds under certain conditions - this.main_window.hide(); - } + // hide window while shutting down, as this can take a few + // seconds under certain conditions + this.main_window.hide(); // Release monitoring early so held resources can be freed up - this.sound_context = null; - this.notifications = null; - this.new_messages_indicator = null; - this.unity_launcher = null; this.new_messages_monitor.clear_folders(); - this.new_messages_monitor = null; // drop the Revokable, which will commit it if necessary save_revokable(null, null); @@ -547,20 +540,16 @@ public class GearyController : Geary.BaseObject { this.account_manager.account_removed.disconnect( on_account_removed ); - this.account_manager = null; if (this.main_window != null) { this.application.remove_window(this.main_window); this.main_window.destroy(); - this.main_window = null; } - this.upgrade_dialog = null; + this.current_folder = null; + this.previous_non_search_folder = null; this.current_account = null; - this.current_folder = null; - - this.previous_non_search_folder = null; this.selected_conversations = new Gee.HashSet(); this.last_deleted_conversation = null; @@ -570,10 +559,8 @@ public class GearyController : Geary.BaseObject { this.waiting_to_close.clear(); this.avatars.close(); - this.avatars = null; - - debug("Closed GearyController"); + debug("Closed Application.Controller"); } /** @@ -605,28 +592,6 @@ public class GearyController : Geary.BaseObject { } } - // Fix for clients having both: - // * disabled Gtk/ShellShowsAppMenu setting - // * no 'menu' setting in Gtk/DecorationLayout - // See https://bugzilla.gnome.org/show_bug.cgi?id=770617 - private void apply_app_menu_fix() { - Gtk.Settings? settings = Gtk.Settings.get_default(); - - if (settings == null) { - warning("Couldn't fetch Gtk default settings"); - return; - } - - string decoration_layout = settings.gtk_decoration_layout ?? ""; - if (!decoration_layout.contains("menu")) { - string prefix = "menu:"; - if (decoration_layout.contains(":")) { - prefix = (decoration_layout.has_prefix(":")) ? "menu" : "menu,"; - } - settings.gtk_decoration_layout = prefix + settings.gtk_decoration_layout; - } - } - private void setup_actions() { this.main_window.add_action_entries(win_action_entries, this); @@ -1882,7 +1847,7 @@ public class GearyController : Geary.BaseObject { string? alt_text, GLib.Cancellable cancellable) { string alt_display_name = Geary.String.is_empty_or_whitespace(alt_text) - ? GearyController.untitled_file_name : alt_text; + ? Application.Controller.untitled_file_name : alt_text; string display_name = yield attachment.get_safe_file_name( alt_display_name ); @@ -1920,7 +1885,7 @@ public class GearyController : Geary.BaseObject { content = new Geary.Memory.FileBuffer(attachment.file, true); dest = dest_dir.get_child_for_display_name( yield attachment.get_safe_file_name( - GearyController.untitled_file_name + Application.Controller.untitled_file_name ) ); } catch (GLib.Error err) { @@ -3136,7 +3101,7 @@ public class GearyController : Geary.BaseObject { // chars anyway. string? display_name = source.get_basename(); if (Geary.String.is_empty_or_whitespace(display_name)) { - display_name = GearyController.untitled_file_name; + display_name = Application.Controller.untitled_file_name; } this.prompt_save_buffer.begin( diff --git a/src/client/application/geary-application.vala b/src/client/application/geary-application.vala index f229d30d..ece62b39 100644 --- a/src/client/application/geary-application.vala +++ b/src/client/application/geary-application.vala @@ -176,12 +176,14 @@ public class GearyApplication : Gtk.Application { /** - * The global UI controller for this app instance. + * The global controller for this application instance. + * + * This will be non-null in the primary application instance, only + * after initial activation, or after startup if {@link + * is_background_service} is true. */ - public GearyController controller { - get; - private set; - default = new GearyController(this); + public Application.Controller? controller { + get; private set; default = null; } /** @@ -253,6 +255,7 @@ public class GearyApplication : Gtk.Application { private bool exiting_fired = false; private int exitcode = 0; private bool is_destroyed = false; + private GLib.Cancellable controller_cancellable = new GLib.Cancellable(); private Components.Inspector? inspector = null; @@ -451,19 +454,7 @@ public class GearyApplication : Gtk.Application { public override void activate() { base.activate(); - - // Clear notifications immediately since we are showing a main - // window. If the controller isn't already open, need to wait - // for that to happen before doing so - - if (present()) { - this.controller.notifications.clear_all_notifications(); - } else { - this.create_controller.begin((obj, res) => { - this.create_controller.end(res); - this.controller.notifications.clear_all_notifications(); - }); - } + this.present.begin(); } public void add_window_accelerators(string action, @@ -560,6 +551,8 @@ public class GearyApplication : Gtk.Application { return; } + this.controller_cancellable.cancel(); + // Give asynchronous destroy_controller() a chance to // complete, but to avoid bug(s) where Geary hangs at exit, // shut the whole thing down if destroy_controller() takes too @@ -604,6 +597,15 @@ public class GearyApplication : Gtk.Application { Posix.exit(1); } + // Presents a main window. If the controller is not open, opens it + // first. + private async void present() { + if (this.controller == null) { + yield create_controller(); + } + this.controller.main_window.present(); + } + // Opens the controller private async void create_controller() { // Manually keep the main loop around for the duration of this @@ -616,11 +618,16 @@ public class GearyApplication : Gtk.Application { // this is only logged for the one user-visible instance, not // the other instances called when sending commands to the app // via the command-line) - message("%s %s prefix=%s exec_dir=%s is_installed=%s", NAME, VERSION, INSTALL_PREFIX, - exec_dir.get_path(), this.is_installed.to_string()); - - yield this.controller.open_async(null); + message( + "%s %s prefix=%s exec_dir=%s is_installed=%s", + NAME, VERSION, INSTALL_PREFIX, + exec_dir.get_path(), + this.is_installed.to_string() + ); + this.controller = yield new Application.Controller( + this, this.controller_cancellable + ); release(); } @@ -629,8 +636,9 @@ public class GearyApplication : Gtk.Application { // see create_controller() for reasoning hold/release is used hold(); - if (this.controller.is_open) { + if (this.controller != null) { yield this.controller.close_async(); + this.controller = null; } release(); @@ -714,15 +722,6 @@ public class GearyApplication : Gtk.Application { return -1; } - private bool present() { - bool ret = false; - if (this.controller.main_window != null) { - this.controller.main_window.present(); - ret = true; - } - return ret; - } - /** Removes and re-adds the autostart file if needed. */ private async void update_autostart_file() { try { diff --git a/src/client/components/main-toolbar.vala b/src/client/components/main-toolbar.vala index 92855f60..9a1b30cd 100644 --- a/src/client/components/main-toolbar.vala +++ b/src/client/components/main-toolbar.vala @@ -161,7 +161,7 @@ public class MainToolbar : Gtk.Box { ); if (this.show_trash_button) { - this.trash_delete_button.action_name = "win."+GearyController.ACTION_TRASH_CONVERSATION; + this.trash_delete_button.action_name = "win."+Application.Controller.ACTION_TRASH_CONVERSATION; this.trash_delete_button.image = trash_image; this.trash_delete_button.tooltip_text = ngettext( "Move conversation to Trash (Delete, Backspace)", @@ -169,7 +169,7 @@ public class MainToolbar : Gtk.Box { this.selected_conversations ); } else { - this.trash_delete_button.action_name = "win."+GearyController.ACTION_DELETE_CONVERSATION; + this.trash_delete_button.action_name = "win."+Application.Controller.ACTION_DELETE_CONVERSATION; this.trash_delete_button.image = delete_image; this.trash_delete_button.tooltip_text = ngettext( "Delete conversation (Shift+Delete)", diff --git a/src/client/components/main-window.vala b/src/client/components/main-window.vala index db91981e..3369c075 100644 --- a/src/client/components/main-window.vala +++ b/src/client/components/main-window.vala @@ -102,7 +102,7 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { load_config(application.config); restore_saved_window_state(); - application.controller.notify[GearyController.PROP_CURRENT_CONVERSATION] + application.controller.notify[Application.Controller.PROP_CURRENT_CONVERSATION] .connect(on_conversation_monitor_changed); application.controller.folder_selected.connect(on_folder_selected); this.application.engine.account_available.connect(on_account_available); @@ -126,7 +126,7 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { } public void open_composer_for_mailbox(Geary.RFC822.MailboxAddress to) { - GearyController controller = this.application.controller; + Application.Controller controller = this.application.controller; ComposerWidget composer = new ComposerWidget( this.current_folder.account, controller.contact_list_store_cache, @@ -225,7 +225,7 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface { new ComposerWindow(composer); } else { this.conversation_viewer.do_compose(composer); - get_action(GearyController.ACTION_FIND_IN_CONVERSATION).set_enabled(false); + get_action(Application.Controller.ACTION_FIND_IN_CONVERSATION).set_enabled(false); } } diff --git a/src/client/conversation-list/conversation-list-view.vala b/src/client/conversation-list/conversation-list-view.vala index dee8a33b..57390503 100644 --- a/src/client/conversation-list/conversation-list-view.vala +++ b/src/client/conversation-list/conversation-list-view.vala @@ -59,7 +59,7 @@ public class ConversationListView : Gtk.TreeView, Geary.BaseInterface { GearyApplication.instance.config.settings.changed[Configuration.DISPLAY_PREVIEW_KEY].connect( on_display_preview_changed); - GearyApplication.instance.controller.notify[GearyController.PROP_CURRENT_CONVERSATION]. + GearyApplication.instance.controller.notify[Application.Controller.PROP_CURRENT_CONVERSATION]. connect(on_conversation_monitor_changed); // Watch for mouse events. @@ -291,23 +291,23 @@ public class ConversationListView : Gtk.TreeView, Geary.BaseInterface { Geary.App.Conversation conversation = get_model().get_conversation_at_path(path); Menu context_menu_model = new Menu(); - context_menu_model.append(_("Delete conversation"), "win."+GearyController.ACTION_DELETE_CONVERSATION); + context_menu_model.append(_("Delete conversation"), "win."+Application.Controller.ACTION_DELETE_CONVERSATION); if (conversation.is_unread()) - context_menu_model.append(_("Mark as _Read"), "win."+GearyController.ACTION_MARK_AS_READ); + context_menu_model.append(_("Mark as _Read"), "win."+Application.Controller.ACTION_MARK_AS_READ); if (conversation.has_any_read_message()) - context_menu_model.append(_("Mark as _Unread"), "win."+GearyController.ACTION_MARK_AS_UNREAD); + context_menu_model.append(_("Mark as _Unread"), "win."+Application.Controller.ACTION_MARK_AS_UNREAD); if (conversation.is_flagged()) - context_menu_model.append(_("U_nstar"), "win."+GearyController.ACTION_MARK_AS_UNSTARRED); + context_menu_model.append(_("U_nstar"), "win."+Application.Controller.ACTION_MARK_AS_UNSTARRED); else - context_menu_model.append(_("_Star"), "win."+GearyController.ACTION_MARK_AS_STARRED); + context_menu_model.append(_("_Star"), "win."+Application.Controller.ACTION_MARK_AS_STARRED); Menu actions_section = new Menu(); - actions_section.append(_("_Reply"), "win."+GearyController.ACTION_REPLY_TO_MESSAGE); - actions_section.append(_("R_eply All"), "win."+GearyController.ACTION_REPLY_ALL_MESSAGE); - actions_section.append(_("_Forward"), "win."+GearyController.ACTION_FORWARD_MESSAGE); + actions_section.append(_("_Reply"), "win."+Application.Controller.ACTION_REPLY_TO_MESSAGE); + actions_section.append(_("R_eply All"), "win."+Application.Controller.ACTION_REPLY_ALL_MESSAGE); + actions_section.append(_("_Forward"), "win."+Application.Controller.ACTION_FORWARD_MESSAGE); context_menu_model.append_section(null, actions_section); Gtk.Menu context_menu = new Gtk.Menu.from_model(context_menu_model); diff --git a/src/client/meson.build b/src/client/meson.build index 9f8d9eaf..b4da6ff3 100644 --- a/src/client/meson.build +++ b/src/client/meson.build @@ -5,10 +5,10 @@ geary_client_vala_sources = files( 'application/application-command.vala', 'application/application-contact-store.vala', 'application/application-contact.vala', + 'application/application-controller.vala', 'application/application-startup-manager.vala', 'application/geary-application.vala', 'application/geary-config.vala', - 'application/geary-controller.vala', 'application/goa-mediator.vala', 'application/secret-mediator.vala',