[geary/mjog/mutiple-main-windows: 7/14] Move Application.MainWindow handling from Controller to Client



commit 3ca121f06909d46eedc016fdf659d92561ae2716
Author: Michael Gratton <mike vee net>
Date:   Thu Nov 14 12:17:05 2019 +1100

    Move Application.MainWindow handling from Controller to Client
    
    Add Application.Client::last_active_main_window, ::get_main_windows and
    ::get_active_main_window members to allow the controller easy access to
    MainWindow instances. Remove the Application.Controller::main_window
    property in favour of using these, generalise it to work with with
    arbirary numbers of open main windows.

 src/client/application/application-client.vala     |  78 +++++-
 src/client/application/application-controller.vala | 279 +++++++++++----------
 .../application/application-main-window.vala       | 160 +++++++++---
 .../conversation-list/conversation-list-view.vala  |  14 +-
 src/client/folder-list/folder-list-tree.vala       |   2 +
 5 files changed, 342 insertions(+), 191 deletions(-)
---
diff --git a/src/client/application/application-client.vala b/src/client/application/application-client.vala
index fb7e4d8e..822799bd 100644
--- a/src/client/application/application-client.vala
+++ b/src/client/application/application-client.vala
@@ -178,6 +178,17 @@ public class Application.Client : Gtk.Application {
         get; private set; default = null;
     }
 
+    /**
+     * The last active main window.
+     *
+     * This will be null if no main windows exist, see {@link
+     * get_active_main_window} if you want to be guaranteed an
+     * instance.
+     */
+    public MainWindow? last_active_main_window {
+        get; private set; default = null;
+    }
+
     /**
      * Manages the autostart desktop file.
      *
@@ -335,6 +346,7 @@ public class Application.Client : Gtk.Application {
             )
         );
         this.add_main_option_entries(OPTION_ENTRIES);
+        this.window_removed.connect_after(on_window_removed);
     }
 
     public override bool local_command_line(ref unowned string[] args,
@@ -458,6 +470,38 @@ public class Application.Client : Gtk.Application {
         }
     }
 
+    /**
+     * Returns a collection of open main windows.
+     */
+    public Gee.Collection<MainWindow> get_main_windows() {
+        var windows = new Gee.LinkedList<MainWindow>();
+        foreach (Gtk.Window window in get_windows()) {
+            MainWindow? main = window as MainWindow;
+            if (main != null) {
+                windows.add(main);
+            }
+        }
+        return windows;
+    }
+
+    /**
+     * Returns the mostly recently active main window or a new instance.
+     *
+     * This returns the value of {@link last_active_main_window} if
+     * not null, else it constructs a new MainWindow instance and
+     * shows it.
+     */
+    public MainWindow get_active_main_window() {
+        MainWindow? active = this.last_active_main_window;
+        if (active == null) {
+            active = new MainWindow(this);
+            this.controller.register_window(active);
+            this.last_active_main_window = active;
+            active.show();
+        }
+        return active;
+    }
+
     public void add_window_accelerators(string action,
                                         string[] accelerators,
                                         Variant? param = null) {
@@ -512,17 +556,13 @@ public class Application.Client : Gtk.Application {
 
     public async void show_email(Geary.Folder? folder,
                                  Geary.EmailIdentifier id) {
-        yield this.present();
-        this.controller.main_window.show_email.begin(
-            folder,
-            Geary.Collection.single(id),
-            true
-        );
+        MainWindow main = yield this.present();
+        main.show_email.begin(folder, Geary.Collection.single(id), true);
     }
 
     public async void show_folder(Geary.Folder? folder) {
-        yield this.present();
-        yield this.controller.main_window.select_folder(folder, true);
+        MainWindow main = yield this.present();
+        yield main.select_folder(folder, true);
     }
 
     public async void show_inspector() {
@@ -699,11 +739,13 @@ public class Application.Client : Gtk.Application {
         withdraw_notification(ERROR_NOTIFICATION_ID);
     }
 
-    // Presents a main window. If the controller is not open, opens it
-    // first.
-    private async void present() {
+    // Presents a main window, opening the controller and window if
+    // needed.
+    private async MainWindow present() {
         yield create_controller();
-        this.controller.main_window.present();
+        MainWindow main = get_active_main_window();
+        main.present();
+        return main;
     }
 
     // Opens the controller
@@ -991,4 +1033,16 @@ public class Application.Client : Gtk.Application {
         }
     }
 
+    private void on_window_removed(Gtk.Window window) {
+        MainWindow? main = window as MainWindow;
+        if (main != null) {
+            this.controller.unregister_window(main);
+            if (this.last_active_main_window == main) {
+                this.last_active_main_window = Geary.Collection.get_first(
+                    get_main_windows()
+                );
+            }
+        }
+    }
+
 }
diff --git a/src/client/application/application-controller.vala 
b/src/client/application/application-controller.vala
index 0c5889c7..b094f236 100644
--- a/src/client/application/application-controller.vala
+++ b/src/client/application/application-controller.vala
@@ -30,6 +30,25 @@ internal class Application.Controller : Geary.BaseObject {
         );
     }
 
+    /** Determines if folders should be added to main windows. */
+    public static bool should_add_folder(Gee.Collection<Geary.Folder>? all,
+                                         Geary.Folder folder) {
+        // if folder is openable, add it
+        if (folder.properties.is_openable != Geary.Trillian.FALSE)
+            return true;
+        else if (folder.properties.has_children == Geary.Trillian.FALSE)
+            return false;
+
+        // if folder contains children, we must ensure that there is at least one of the same type
+        Geary.SpecialFolderType type = folder.special_folder_type;
+        foreach (Geary.Folder other in all) {
+            if (other.special_folder_type == type && other.path.parent == folder.path)
+                return true;
+        }
+
+        return false;
+    }
+
 
     /** Determines if the controller is open. */
     public bool is_open {
@@ -54,9 +73,6 @@ internal class Application.Controller : Geary.BaseObject {
         get; private set; default = new Application.AvatarStore();
     }
 
-    /** Default main window */
-    public MainWindow main_window { get; private set; }
-
     // Primary collection of the application's open accounts
     private Gee.Map<Geary.AccountInformation,AccountContext> accounts =
         new Gee.HashMap<Geary.AccountInformation,AccountContext>();
@@ -110,8 +126,6 @@ internal class Application.Controller : Geary.BaseObject {
         this.application = application;
         this.controller_open = cancellable;
 
-        Geary.Engine engine = this.application.engine;
-
         // This initializes the IconFactory, important to do before
         // the actions are created (as they refer to some of Geary's
         // custom icons)
@@ -162,18 +176,6 @@ internal class Application.Controller : Geary.BaseObject {
         );
         this.plugin_manager.load();
 
-        this.main_window = new MainWindow(application, this);
-        this.main_window.retry_service_problem.connect(on_retry_service_problem);
-
-        engine.account_available.connect(on_account_available);
-
-        // Connect to various UI signals.
-        this.main_window.folder_list.set_new_messages_monitor(
-            this.plugin_manager.notifications
-        );
-
-        this.main_window.conversation_list_view.grab_focus();
-
         // Migrate configuration if necessary.
         try {
             Migrate.xdg_config_dir(this.application.get_user_data_directory(),
@@ -196,6 +198,8 @@ internal class Application.Controller : Geary.BaseObject {
             error("Error opening libsecret: %s", err.message);
         }
 
+        application.engine.account_available.connect(on_account_available);
+
         this.account_manager = new Accounts.Manager(
             libsecret,
             this.application.get_user_config_directory(),
@@ -222,7 +226,7 @@ internal class Application.Controller : Geary.BaseObject {
 
         // Start the engine and load our accounts
         try {
-            yield engine.open_async(
+            yield application.engine.open_async(
                 this.application.get_resource_directory(), cancellable
             );
             yield this.account_manager.load_accounts(cancellable);
@@ -251,7 +255,9 @@ internal class Application.Controller : Geary.BaseObject {
             on_account_available
         );
 
-        this.main_window.set_sensitive(false);
+        foreach (MainWindow window in this.application.get_main_windows()) {
+            window.set_sensitive(false);
+        }
 
         // Close any open composers up-front before anything else is
         // shut down so any pending operations have a chance to
@@ -289,12 +295,11 @@ internal class Application.Controller : Geary.BaseObject {
         // first so they don't block shutdown.
         this.controller_open.cancel();
 
-        // Release folder and conversations in the main window
-        yield this.main_window.select_folder(null, false);
-
-        // hide window while shutting down, as this can take a few
-        // seconds under certain conditions
-        this.main_window.hide();
+        // Release folder and conversations in main windows
+        foreach (MainWindow window in this.application.get_main_windows()) {
+            yield window.select_folder(null, false);
+            window.hide();
+        }
 
         // Release notification monitoring early so held resources can
         // be freed up
@@ -348,9 +353,8 @@ internal class Application.Controller : Geary.BaseObject {
             on_account_removed
         );
 
-        if (this.main_window != null) {
-            this.application.remove_window(this.main_window);
-            this.main_window.destroy();
+        foreach (MainWindow window in this.application.get_main_windows()) {
+            window.destroy();
         }
 
         this.pending_mailtos.clear();
@@ -365,26 +369,35 @@ internal class Application.Controller : Geary.BaseObject {
      * Opens or queues a new composer addressed to a specific email address.
      */
     public void compose(string? mailto = null) {
-        Geary.Account? selected = this.main_window.selected_account;
-        if (selected == null) {
-            // Schedule the send for after we have an account open.
-            this.pending_mailtos.add(mailto);
-        } else {
+        MainWindow? window = this.application.last_active_main_window;
+        if (window != null && window.selected_account != null) {
             create_compose_widget(
-                selected, NEW_MESSAGE, mailto, null, null, false
+                window,
+                window.selected_account,
+                NEW_MESSAGE,
+                mailto,
+                null,
+                null,
+                false
             );
+        } else {
+            // Schedule the send for after we have an account open.
+            this.pending_mailtos.add(mailto);
         }
     }
 
     /**
      * Opens new composer with an existing message as context.
      */
-    public void compose_with_context_email(Geary.Account account,
+    public void compose_with_context_email(MainWindow to_show,
+                                           Geary.Account account,
                                            Composer.Widget.ComposeType type,
                                            Geary.Email context,
                                            string? quote,
                                            bool is_draft) {
-        create_compose_widget(account, type, null, context, quote, is_draft);
+        create_compose_widget(
+            to_show, account, type, null, context, quote, is_draft
+        );
     }
 
     /** Adds a new composer to be kept track of. */
@@ -470,7 +483,7 @@ internal class Application.Controller : Geary.BaseObject {
             !(report.error.thrown is IOError.CANCELLED)) {
             MainWindowInfoBar info_bar = new MainWindowInfoBar.for_problem(report);
             info_bar.retry.connect(on_retry_problem);
-            this.main_window.show_infobar(info_bar);
+            this.application.get_active_main_window().show_infobar(info_bar);
         }
 
         Geary.ServiceProblemReport? service_report =
@@ -879,6 +892,17 @@ internal class Application.Controller : Geary.BaseObject {
         }
     }
 
+    internal void register_window(MainWindow window) {
+        window.retry_service_problem.connect(on_retry_service_problem);
+        window.folder_list.set_new_messages_monitor(
+            this.plugin_manager.notifications
+        );
+    }
+
+    internal void unregister_window(MainWindow window) {
+        window.retry_service_problem.disconnect(on_retry_service_problem);
+    }
+
     /** Returns the first open account, sorted by ordinal. */
     internal Geary.AccountInformation? get_first_account() {
         return this.accounts.keys.iterator().fold<Geary.AccountInformation?>(
@@ -1048,16 +1072,13 @@ internal class Application.Controller : Geary.BaseObject {
             has_cert_error |= context.tls_validation_failed;
         }
 
-        foreach (Gtk.Window window in this.application.get_windows()) {
-            MainWindow? main = window as MainWindow;
-            if (main != null) {
-                main.update_account_status(
-                    effective_status,
-                    has_auth_error,
-                    has_cert_error,
-                    service_problem_source
-                );
-            }
+        foreach (MainWindow window in this.application.get_main_windows()) {
+            window.update_account_status(
+                effective_status,
+                has_auth_error,
+                has_cert_error,
+                service_problem_source
+            );
         }
     }
 
@@ -1184,7 +1205,7 @@ internal class Application.Controller : Geary.BaseObject {
         context.tls_validation_prompting = true;
         try {
             yield this.certificate_manager.prompt_pin_certificate(
-                this.main_window,
+                this.application.get_active_main_window(),
                 context.account.information,
                 service,
                 endpoint,
@@ -1212,19 +1233,26 @@ internal class Application.Controller : Geary.BaseObject {
         update_account_status();
     }
 
-    private void on_account_email_removed(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
-        if (folder.special_folder_type == Geary.SpecialFolderType.OUTBOX) {
-            main_window.status_bar.deactivate_message(StatusBar.Message.OUTBOX_SEND_FAILURE);
-            main_window.status_bar.deactivate_message(StatusBar.Message.OUTBOX_SAVE_SENT_MAIL_FAILED);
+    private void on_account_email_removed(Geary.Folder folder,
+                                          Gee.Collection<Geary.EmailIdentifier> ids) {
+        if (folder.special_folder_type == OUTBOX) {
+            foreach (MainWindow window in this.application.get_main_windows()) {
+                window.status_bar.deactivate_message(StatusBar.Message.OUTBOX_SEND_FAILURE);
+                window.status_bar.deactivate_message(StatusBar.Message.OUTBOX_SAVE_SENT_MAIL_FAILED);
+            }
         }
     }
 
     private void on_sending_started() {
-        main_window.status_bar.activate_message(StatusBar.Message.OUTBOX_SENDING);
+        foreach (MainWindow window in this.application.get_main_windows()) {
+            window.status_bar.activate_message(StatusBar.Message.OUTBOX_SENDING);
+        }
     }
 
     private void on_sending_finished() {
-        main_window.status_bar.deactivate_message(StatusBar.Message.OUTBOX_SENDING);
+        foreach (MainWindow window in this.application.get_main_windows()) {
+            window.status_bar.deactivate_message(StatusBar.Message.OUTBOX_SENDING);
+        }
     }
 
     // Returns true if the caller should try opening the account again
@@ -1234,7 +1262,8 @@ internal class Application.Controller : Geary.BaseObject {
         // give the user two options: reset the Account local store, or exit Geary.  A third
         // could be done to leave the Account in an unopened state, but we don't currently
         // have provisions for that.
-        QuestionDialog dialog = new QuestionDialog(main_window,
+        QuestionDialog dialog = new QuestionDialog(
+            this.application.get_active_main_window(),
             _("Unable to open the database for %s").printf(account.information.id),
             _("There was an error opening the local mail database for this account. This is possibly due to 
corruption of the database file in this directory:\n\n%s\n\nGeary can rebuild the database and re-synchronize 
with the server or exit.\n\nRebuilding the database will destroy all local email and its attachments. <b>The 
mail on the your server will not be affected.</b>")
                 .printf(account.information.data_dir.get_path()),
@@ -1246,7 +1275,8 @@ internal class Application.Controller : Geary.BaseObject {
                 try {
                     yield account.rebuild_async();
                 } catch (Error err) {
-                    ErrorDialog errdialog = new ErrorDialog(main_window,
+                    ErrorDialog errdialog = new ErrorDialog(
+                        this.application.get_active_main_window(),
                         _("Unable to rebuild database for ā€œ%sā€").printf(account.information.id),
                         _("Error during rebuild:\n\n%s").printf(err.message));
                     errdialog.run();
@@ -1287,8 +1317,9 @@ internal class Application.Controller : Geary.BaseObject {
         if (did_attempt_open_all_accounts() &&
             !this.upgrade_dialog.visible &&
             !this.controller_open.is_cancelled() &&
-            !this.application.is_background_service)
-            main_window.show();
+            !this.application.is_background_service) {
+            this.application.get_active_main_window().present();
+        }
     }
 
     /**
@@ -1320,28 +1351,12 @@ internal class Application.Controller : Geary.BaseObject {
     private void on_special_folder_type_changed(Geary.Folder folder,
                                                 Geary.SpecialFolderType old_type,
                                                 Geary.SpecialFolderType new_type) {
-        Geary.AccountInformation info = folder.account.information;
-
-        // Update the main window
-        this.main_window.folder_list.remove_folder(folder);
-        this.main_window.folder_list.add_folder(folder);
-        // Since removing the folder will also remove its children
-        // from the folder list, we need to check for any and re-add
-        // them. See issue #11.
-        try {
-            foreach (Geary.Folder child in
-                     folder.account.list_matching_folders(folder.path)) {
-                main_window.folder_list.add_folder(child);
-            }
-        } catch (Error err) {
-            // Oh well
-        }
-
         // Update notifications
         this.plugin_manager.notifications.remove_folder(folder);
         if (folder.special_folder_type == Geary.SpecialFolderType.INBOX ||
             (folder.special_folder_type == Geary.SpecialFolderType.NONE &&
              is_inbox_descendant(folder))) {
+            Geary.AccountInformation info = folder.account.information;
             this.plugin_manager.notifications.add_folder(
                 folder, this.accounts.get(info).cancellable
             );
@@ -1356,32 +1371,20 @@ internal class Application.Controller : Geary.BaseObject {
 
         if (available != null && available.size > 0) {
             foreach (Geary.Folder folder in available) {
-                if (!should_add_folder(available, folder)) {
+                if (!Controller.should_add_folder(available, folder)) {
                     continue;
                 }
-                folder.special_folder_type_changed.connect(on_special_folder_type_changed);
-                this.main_window.add_folder(folder);
+                folder.special_folder_type_changed.connect(
+                    on_special_folder_type_changed
+                );
 
                 GLib.Cancellable cancellable = context.cancellable;
                 switch (folder.special_folder_type) {
                 case Geary.SpecialFolderType.INBOX:
-                    // Special case handling of inboxes
                     if (context.inbox == null) {
                         context.inbox = folder;
-
-                        // Select this inbox if there isn't an
-                        // existing folder selected and it is the
-                        // inbox for the first account
-                        if (!this.main_window.folder_list.is_any_selected() &&
-                            folder.account.information == get_first_account()) {
-                            // 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(folder.account))
-                                main_window.folder_list.select_folder(folder);
-                        }
                     }
-
-                    folder.open_async.begin(Geary.Folder.OpenFlags.NO_DELAY, cancellable);
+                    folder.open_async.begin(NO_DELAY, cancellable);
 
                     // Always notify for new messages in the Inbox
                     this.plugin_manager.notifications.add_folder(
@@ -1408,8 +1411,9 @@ internal class Application.Controller : Geary.BaseObject {
             bool has_prev = unavailable_iterator.last();
             while (has_prev) {
                 Geary.Folder folder = unavailable_iterator.get();
-                folder.special_folder_type_changed.disconnect(on_special_folder_type_changed);
-                this.main_window.remove_folder(folder);
+                folder.special_folder_type_changed.disconnect(
+                    on_special_folder_type_changed
+                );
 
                 switch (folder.special_folder_type) {
                 case Geary.SpecialFolderType.INBOX:
@@ -1438,10 +1442,12 @@ internal class Application.Controller : Geary.BaseObject {
         // 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
+        MainWindow? window = this.application.last_active_main_window;
         return (
-            folder != this.main_window.selected_folder ||
-            this.main_window.conversation_list_view.vadjustment.value != 0.0 ||
-            !this.main_window.has_toplevel_focus
+            window != null &&
+            (folder != window.selected_folder ||
+             window.conversation_list_view.vadjustment.value != 0.0 ||
+             !window.has_toplevel_focus)
         );
     }
 
@@ -1449,14 +1455,17 @@ internal class Application.Controller : Geary.BaseObject {
     // false and the supplied visible messages are visible in the conversation list view
     public void clear_new_messages(string caller,
                                    Gee.Set<Geary.App.Conversation>? supplied) {
-        Geary.Folder? selected = this.main_window.selected_folder;
+        MainWindow? window = this.application.last_active_main_window;
+        Geary.Folder? selected = (
+            (window != null) ? window.selected_folder : null
+        );
         NotificationContext notifications = this.plugin_manager.notifications;
         if (selected != null && (
                 !notifications.get_folders().contains(selected) ||
                 should_notify_new_messages(selected))) {
 
             Gee.Set<Geary.App.Conversation> visible =
-                supplied ?? main_window.conversation_list_view.get_visible_conversations();
+                supplied ?? window.conversation_list_view.get_visible_conversations();
 
             foreach (Geary.App.Conversation conversation in visible) {
                 try {
@@ -1475,8 +1484,14 @@ internal class Application.Controller : Geary.BaseObject {
 
     /** Displays a composer on the last active main window. */
     internal void show_composer(Composer.Widget composer,
-                                Gee.Collection<Geary.EmailIdentifier>? refers_to) {
-        this.main_window.show_composer(composer, refers_to);
+                                Gee.Collection<Geary.EmailIdentifier>? refers_to,
+                                MainWindow? show_on) {
+        var target = show_on;
+        if (target == null) {
+            target = this.application.get_active_main_window();
+        }
+
+        target.show_composer(composer, refers_to);
         composer.set_focus();
     }
 
@@ -1506,7 +1521,8 @@ internal class Application.Controller : Geary.BaseObject {
      * @param is_draft - Whether we're starting from a draft (true) or
      * a new mail (false)
      */
-    private void create_compose_widget(Geary.Account account,
+    private void create_compose_widget(MainWindow show_on,
+                                       Geary.Account account,
                                        Composer.Widget.ComposeType compose_type,
                                        string? mailto,
                                        Geary.Email? referred,
@@ -1516,22 +1532,24 @@ internal class Application.Controller : Geary.BaseObject {
         // composer, check for these first.
         if (compose_type == NEW_MESSAGE && !is_draft) {
             // We're creating a new message that isn't a draft, if
-            // there's already a composer open, just use that
-            Composer.Widget? existing =
-                this.main_window.conversation_viewer.current_composer;
-            if (existing != null &&
-                existing.current_mode == PANED &&
-                existing.is_blank) {
-                existing.present();
-                return;
+            // there's already an empty composer open, just use
+            // that
+            foreach (Composer.Widget existing in this.composer_widgets) {
+                if (existing != null &&
+                    existing.current_mode == PANED &&
+                    existing.is_blank) {
+                    existing.present();
+                    return;
+                }
             }
         } else if (compose_type != NEW_MESSAGE && referred != null) {
             // A reply/forward was requested, see whether there is
-            // already an inline message that is either a
-            // reply/forward for that message, or there is a quote
-            // to insert into it.
+            // already an inline message in the target window that is
+            // either a reply/forward for that message, or there is a
+            // quote to insert into it.
             foreach (Composer.Widget existing in this.composer_widgets) {
-                if ((existing.current_mode == INLINE ||
+                if (existing.get_toplevel() == show_on &&
+                    (existing.current_mode == INLINE ||
                      existing.current_mode == INLINE_COMPACT) &&
                     (referred.id in existing.get_referred_ids() ||
                      quote != null)) {
@@ -1549,7 +1567,7 @@ internal class Application.Controller : Geary.BaseObject {
             // new one. Replies must open inline in the main window,
             // so we need to ensure there are no composers open there
             // first.
-            if (!this.main_window.close_composer(true)) {
+            if (!show_on.close_composer(true)) {
                 return;
             }
         }
@@ -1568,7 +1586,8 @@ internal class Application.Controller : Geary.BaseObject {
         add_composer(widget);
         show_composer(
             widget,
-            referred != null ? Geary.Collection.single(referred.id) : null
+            referred != null ? Geary.Collection.single(referred.id) : null,
+            show_on
         );
 
         this.load_composer.begin(
@@ -1629,7 +1648,9 @@ internal class Application.Controller : Geary.BaseObject {
         ).printf(Util.Email.to_short_recipient_display(sent));
         Components.InAppNotification notification =
             new Components.InAppNotification(message);
-        this.main_window.add_notification(notification);
+        foreach (MainWindow window in this.application.get_main_windows()) {
+            window.add_notification(notification);
+        }
 
         AccountContext? context = this.accounts.get(service.account);
         if (context != null) {
@@ -1637,24 +1658,6 @@ internal class Application.Controller : Geary.BaseObject {
         }
     }
 
-    private bool should_add_folder(Gee.Collection<Geary.Folder>? all,
-                                   Geary.Folder folder) {
-        // if folder is openable, add it
-        if (folder.properties.is_openable != Geary.Trillian.FALSE)
-            return true;
-        else if (folder.properties.has_children == Geary.Trillian.FALSE)
-            return false;
-
-        // if folder contains children, we must ensure that there is at least one of the same type
-        Geary.SpecialFolderType type = folder.special_folder_type;
-        foreach (Geary.Folder other in all) {
-            if (other.special_folder_type == type && other.path.parent == folder.path)
-                return true;
-        }
-
-        return false;
-    }
-
     private Gee.Collection<Geary.EmailIdentifier>
         to_in_folder_email_ids(Gee.Collection<Geary.App.Conversation> conversations) {
         Gee.Collection<Geary.EmailIdentifier> messages =
@@ -2730,7 +2733,7 @@ private class Application.SendComposerCommand : ComposerCommand {
         this.saved = null;
 
         this.composer.set_enabled(true);
-        this.application.controller.show_composer(this.composer, null);
+        this.application.controller.show_composer(this.composer, null, null);
         clear_composer();
     }
 
@@ -2784,7 +2787,7 @@ private class Application.SaveComposerCommand : ComposerCommand {
         if (this.composer != null) {
             this.destroy_timer.reset();
             this.composer.set_enabled(true);
-            this.controller.show_composer(this.composer, null);
+            this.controller.show_composer(this.composer, null, null);
             clear_composer();
         } else {
             /// Translators: A label for an in-app notification.
@@ -2842,7 +2845,7 @@ private class Application.DiscardComposerCommand : ComposerCommand {
         if (this.composer != null) {
             this.destroy_timer.reset();
             this.composer.set_enabled(true);
-            this.controller.show_composer(this.composer, null);
+            this.controller.show_composer(this.composer, null, null);
             clear_composer();
         } else {
             /// Translators: A label for an in-app notification.
diff --git a/src/client/application/application-main-window.vala 
b/src/client/application/application-main-window.vala
index b262a3b0..fcf643e9 100644
--- a/src/client/application/application-main-window.vala
+++ b/src/client/application/application-main-window.vala
@@ -274,7 +274,7 @@ public class Application.MainWindow :
     public signal void retry_service_problem(Geary.ClientService.Status problem);
 
 
-    internal MainWindow(Client application, Controller controller) {
+    internal MainWindow(Client application) {
         Object(
             application: application,
             show_menubar: false
@@ -306,17 +306,18 @@ public class Application.MainWindow :
         this.update_ui_timeout.repetition = FOREVER;
 
         // Add future and existing accounts to the main window
-        controller.account_available.connect(
+        this.application.controller.account_available.connect(
             on_account_available
         );
-        controller.account_unavailable.connect(
+        this.application.controller.account_unavailable.connect(
             on_account_unavailable
         );
-        foreach (AccountContext context in controller.get_account_contexts()) {
+        foreach (AccountContext context in
+                 this.application.controller.get_account_contexts()) {
             add_account(context);
         }
 
-        this.main_layout.show_all();
+        this.conversation_list_view.grab_focus();
     }
 
     ~MainWindow() {
@@ -687,24 +688,6 @@ public class Application.MainWindow :
         }
     }
 
-    /** Adds a folder to the window. */
-    internal void add_folder(Geary.Folder to_add) {
-        this.folder_list.add_folder(to_add);
-        if (to_add.account == this.selected_account) {
-            this.main_toolbar.copy_folder_menu.add_folder(to_add);
-            this.main_toolbar.move_folder_menu.add_folder(to_add);
-        }
-    }
-
-    /** Removes a folder from the window. */
-    internal void remove_folder(Geary.Folder to_remove) {
-        if (to_remove.account == this.selected_account) {
-            this.main_toolbar.copy_folder_menu.remove_folder(to_remove);
-            this.main_toolbar.move_folder_menu.remove_folder(to_remove);
-        }
-        this.folder_list.remove_folder(to_remove);
-    }
-
     private void add_account(AccountContext to_add) {
         if (!this.accounts.contains(to_add)) {
             this.folder_list.set_user_folders_root_name(
@@ -723,6 +706,15 @@ public class Application.MainWindow :
             to_add.commands.undone.connect(on_command_undo);
             to_add.commands.redone.connect(on_command_redo);
 
+            to_add.account.folders_available_unavailable.connect(
+                on_folders_available_unavailable
+            );
+
+            folders_available(
+                to_add.account,
+                Geary.Account.sort_by_path(to_add.account.list_folders())
+            );
+
             this.accounts.add(to_add);
         }
     }
@@ -755,6 +747,10 @@ public class Application.MainWindow :
                 }
             }
 
+            to_remove.account.folders_available_unavailable.disconnect(
+                on_folders_available_unavailable
+            );
+
             to_remove.commands.executed.disconnect(on_command_execute);
             to_remove.commands.undone.disconnect(on_command_undo);
             to_remove.commands.redone.disconnect(on_command_redo);
@@ -773,6 +769,30 @@ public class Application.MainWindow :
         }
     }
 
+    /** Adds a folder to the window. */
+    private void add_folder(Geary.Folder to_add) {
+        this.folder_list.add_folder(to_add);
+        if (to_add.account == this.selected_account) {
+            this.main_toolbar.copy_folder_menu.add_folder(to_add);
+            this.main_toolbar.move_folder_menu.add_folder(to_add);
+        }
+        to_add.special_folder_type_changed.connect(
+            on_special_folder_type_changed
+        );
+    }
+
+    /** Removes a folder from the window. */
+    private void remove_folder(Geary.Folder to_remove) {
+        to_remove.special_folder_type_changed.disconnect(
+            on_special_folder_type_changed
+        );
+        if (to_remove.account == this.selected_account) {
+            this.main_toolbar.copy_folder_menu.remove_folder(to_remove);
+            this.main_toolbar.move_folder_menu.remove_folder(to_remove);
+        }
+        this.folder_list.remove_folder(to_remove);
+    }
+
     private AccountContext? get_selected_account_context() {
         AccountContext? context = null;
         if (this.selected_account != null) {
@@ -962,13 +982,6 @@ public class Application.MainWindow :
         this.status_bar.add(this.spinner);
     }
 
-    // Returns true when there's a conversation list scrollbar visible, i.e. the list is tall
-    // enough to need one.  Otherwise returns false.
-    public bool conversation_list_has_scrollbar() {
-        Gtk.Scrollbar? scrollbar = this.conversation_list_scrolled.get_vscrollbar() as Gtk.Scrollbar;
-        return scrollbar != null && scrollbar.get_visible();
-    }
-
     /** {@inheritDoc} */
     public override bool key_press_event(Gdk.EventKey event) {
         check_shift_event(event);
@@ -1272,6 +1285,44 @@ public class Application.MainWindow :
         }
     }
 
+    private void folders_available(Geary.Account account,
+                                   Gee.BidirSortedSet<Geary.Folder> available) {
+        foreach (Geary.Folder folder in available) {
+            if (Controller.should_add_folder(available, folder)) {
+                add_folder(folder);
+
+                if (folder.special_folder_type == INBOX) {
+                    // Select this inbox if there isn't an existing
+                    // folder selected and it is the inbox for the
+                    // first account
+                    Geary.AccountInformation? first_account =
+                        this.application.controller.get_first_account();
+                    if (!this.folder_list.is_any_selected() &&
+                        folder.account.information == first_account) {
+                        // First we try to select the Inboxes branch
+                        // inbox if it's there, falling back to the
+                        // main folder list.
+                        if (!this.folder_list.select_inbox(folder.account)) {
+                            this.folder_list.select_folder(folder);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private void folders_unavailable(Geary.Account account,
+                                     Gee.BidirSortedSet<Geary.Folder> unavailable) {
+        var unavailable_iterator = unavailable.bidir_iterator();
+        bool has_prev = unavailable_iterator.last();
+        while (has_prev) {
+            Geary.Folder folder = unavailable_iterator.get();
+            remove_folder(folder);
+
+            has_prev = unavailable_iterator.previous();
+        }
+    }
+
     private async void open_conversation_monitor(Geary.App.ConversationMonitor to_open,
                                                  GLib.Cancellable cancellable) {
         to_open.scan_completed.connect(on_scan_completed);
@@ -1331,6 +1382,7 @@ public class Application.MainWindow :
             email_view.get_selection_for_quoting.begin((obj, res) => {
                     string? quote = email_view.get_selection_for_quoting.end(res);
                     this.application.controller.compose_with_context_email(
+                        this,
                         account,
                         compose_type,
                         email_view.email,
@@ -1576,8 +1628,11 @@ public class Application.MainWindow :
     private void on_scan_completed(Geary.App.ConversationMonitor monitor) {
         // Done scanning.  Check if we have enough messages to fill
         // the conversation list; if not, trigger a load_more();
+        Gtk.Scrollbar? scrollbar = (
+            this.conversation_list_scrolled.get_vscrollbar() as Gtk.Scrollbar
+        );
         if (is_visible() &&
-            !conversation_list_has_scrollbar() &&
+            (scrollbar == null || !scrollbar.get_visible()) &&
             monitor == this.conversations &&
             monitor.can_load_more) {
             debug("Not enough messages, loading more for folder %s",
@@ -1690,6 +1745,40 @@ public class Application.MainWindow :
 
         this.remove_account.begin(account, to_select);
     }
+
+    private void on_folders_available_unavailable(
+        Geary.Account account,
+        Gee.BidirSortedSet<Geary.Folder>? available,
+        Gee.BidirSortedSet<Geary.Folder>? unavailable
+    ) {
+        if (available != null) {
+            folders_available(account, available);
+        }
+        if (unavailable != null) {
+            folders_unavailable(account, unavailable);
+        }
+    }
+
+    private void on_special_folder_type_changed(Geary.Folder folder,
+                                                Geary.SpecialFolderType old_type,
+                                                Geary.SpecialFolderType new_type) {
+        // Update the main window
+        this.folder_list.remove_folder(folder);
+        this.folder_list.add_folder(folder);
+
+        // Since removing the folder will also remove its children
+        // from the folder list, we need to check for any and re-add
+        // them. See issue #11.
+        try {
+            foreach (Geary.Folder child in
+                     folder.account.list_matching_folders(folder.path)) {
+                this.folder_list.add_folder(child);
+            }
+        } catch (Error err) {
+            // Oh well
+        }
+    }
+
     private void on_command_execute(Command command) {
         if (!(command is TrivialCommand)) {
             // Only show an execute notification for non-trivial
@@ -1801,6 +1890,7 @@ public class Application.MainWindow :
 
             if (!already_open) {
                 this.application.controller.compose_with_context_email(
+                    this,
                     activated.base_folder.account,
                     NEW_MESSAGE,
                     draft,
@@ -2191,7 +2281,7 @@ public class Application.MainWindow :
         Geary.Account? account = this.selected_account;
         if (account != null) {
             this.application.controller.compose_with_context_email(
-                account, REPLY, target, quote, false
+                this, account, REPLY, target, quote, false
             );
         }
     }
@@ -2200,7 +2290,7 @@ public class Application.MainWindow :
         Geary.Account? account = this.selected_account;
         if (account != null) {
             this.application.controller.compose_with_context_email(
-                account, REPLY_ALL, target, quote, false
+                this, account, REPLY_ALL, target, quote, false
             );
         }
     }
@@ -2209,7 +2299,7 @@ public class Application.MainWindow :
         Geary.Account? account = this.selected_account;
         if (account != null) {
             this.application.controller.compose_with_context_email(
-                account, FORWARD, target, quote, false
+                this, account, FORWARD, target, quote, false
             );
         }
     }
@@ -2218,7 +2308,7 @@ public class Application.MainWindow :
         Geary.Account? account = this.selected_account;
         if (account != null) {
             this.application.controller.compose_with_context_email(
-                account, NEW_MESSAGE, target, null, true
+                this, account, NEW_MESSAGE, target, null, true
             );
         }
     }
diff --git a/src/client/conversation-list/conversation-list-view.vala 
b/src/client/conversation-list/conversation-list-view.vala
index fb90cd09..2697554e 100644
--- a/src/client/conversation-list/conversation-list-view.vala
+++ b/src/client/conversation-list/conversation-list-view.vala
@@ -47,9 +47,10 @@ public class ConversationListView : Gtk.TreeView, Geary.BaseInterface {
         Gtk.TreeSelection selection = get_selection();
         selection.set_mode(Gtk.SelectionMode.MULTIPLE);
         style_updated.connect(on_style_changed);
-        show.connect(on_show);
         row_activated.connect(on_row_activated);
 
+        notify["vadjustment"].connect(on_vadjustment_changed);
+
         button_press_event.connect(on_button_press);
 
         // Set up drag and drop.
@@ -72,6 +73,8 @@ public class ConversationListView : Gtk.TreeView, Geary.BaseInterface {
 
         this.selection_update = new Geary.IdleManager(do_selection_changed);
         this.selection_update.priority = Geary.IdleManager.Priority.LOW;
+
+        this.visible = true;
     }
 
     ~ConversationListView() {
@@ -415,11 +418,6 @@ public class ConversationListView : Gtk.TreeView, Geary.BaseInterface {
         schedule_visible_conversations_changed();
     }
 
-    private void on_show() {
-        // Wait until we're visible to set this signal up.
-        ((Gtk.Scrollable) this).get_vadjustment().value_changed.connect(on_value_changed);
-    }
-
     private void on_value_changed() {
         if (this.enable_load_more) {
             check_load_more();
@@ -584,4 +582,8 @@ public class ConversationListView : Gtk.TreeView, Geary.BaseInterface {
 
     }
 
+    private void on_vadjustment_changed() {
+        this.vadjustment.value_changed.connect(on_value_changed);
+    }
+
 }
diff --git a/src/client/folder-list/folder-list-tree.vala b/src/client/folder-list/folder-list-tree.vala
index 1c164f0f..95d4da63 100644
--- a/src/client/folder-list/folder-list-tree.vala
+++ b/src/client/folder-list/folder-list-tree.vala
@@ -38,6 +38,8 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface {
         unowned Gtk.BindingSet? binding_set = Gtk.BindingSet.find("GtkTreeView");
         assert(binding_set != null);
         Gtk.BindingEntry.remove(binding_set, Gdk.Key.N, Gdk.ModifierType.CONTROL_MASK);
+
+        this.visible = true;
     }
 
     ~Tree() {



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]