[geary/mjog/account-command-stacks: 70/77] Move folder selection and related handling to MainWindow



commit eadf98e9552cd17cbd38fc8cf039051d05099135
Author: Michael Gratton <mike vee net>
Date:   Sun Nov 3 06:34:36 2019 +1100

    Move folder selection and related handling to MainWindow
    
    Move more main-window specific code out of Application.Controller,
    allowing consolidation of all folder selection code in the one place
    and allowing it to be revamped so as to support robust programmatic
    selection of folders.

 src/client/application/application-controller.vala | 266 ++-------------
 src/client/application/geary-application.vala      |   6 +-
 src/client/components/main-window.vala             | 361 ++++++++++++++-------
 src/client/folder-list/folder-list-tree.vala       |  37 ++-
 4 files changed, 299 insertions(+), 371 deletions(-)
---
diff --git a/src/client/application/application-controller.vala 
b/src/client/application/application-controller.vala
index 6fcf7bbb..fec08470 100644
--- a/src/client/application/application-controller.vala
+++ b/src/client/application/application-controller.vala
@@ -17,7 +17,6 @@ public class Application.Controller : Geary.BaseObject {
 
 
     private const string PROP_ATTEMPT_OPEN_ACCOUNT = "attempt-open-account";
-    private const int SELECT_FOLDER_TIMEOUT_USEC = 100 * 1000;
     private const uint MAX_AUTH_ATTEMPTS = 3;
 
 
@@ -163,17 +162,8 @@ public class Application.Controller : Geary.BaseObject {
 
     private PluginManager plugin_manager;
 
-    // Null if none selected
-    private Geary.Folder? current_folder = null;
-
-    private Cancellable cancellable_folder = new Cancellable();
-    private Cancellable cancellable_search = new Cancellable();
     private Cancellable cancellable_open_account = new Cancellable();
     private Gee.LinkedList<ComposerWidget> composer_widgets = new Gee.LinkedList<ComposerWidget>();
-    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 Gee.List<string?> pending_mailtos = new Gee.ArrayList<string>();
 
     // List of windows we're waiting to close before Geary closes.
@@ -246,15 +236,10 @@ public class Application.Controller : Geary.BaseObject {
         // Create the main window (must be done after creating actions.)
         main_window = new MainWindow(this.application);
         main_window.retry_service_problem.connect(on_retry_service_problem);
-        main_window.notify["has-toplevel-focus"].connect(on_has_toplevel_focus);
 
         engine.account_available.connect(on_account_available);
 
         // Connect to various UI signals.
-        main_window.conversation_list_view.conversation_activated.connect(on_conversation_activated);
-        
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.search_bar.search_text_changed.connect((text) => { do_search(text); });
         this.main_window.folder_list.set_new_messages_monitor(
             this.plugin_manager.notifications
         );
@@ -336,12 +321,7 @@ public class Application.Controller : Geary.BaseObject {
         this.application.engine.account_available.disconnect(on_account_available);
 
         // Release folder and conversations in the main window
-        on_folder_selected(null);
-
-        // Disconnect from various UI signals.
-        this.main_window.conversation_list_view.conversation_activated.disconnect(on_conversation_activated);
-        
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);
+        yield this.main_window.select_folder(null);
 
         // hide window while shutting down, as this can take a few
         // seconds under certain conditions
@@ -423,9 +403,6 @@ public class Application.Controller : Geary.BaseObject {
             this.main_window.destroy();
         }
 
-        this.current_folder = null;
-        this.previous_non_search_folder = null;
-
         this.pending_mailtos.clear();
         this.composer_widgets.clear();
         this.waiting_to_close.clear();
@@ -442,7 +419,7 @@ public class Application.Controller : Geary.BaseObject {
         Geary.Account? selected = this.main_window.selected_account;
         if (selected == null) {
             // Schedule the send for after we have an account open.
-            pending_mailtos.add(mailto);
+            this.pending_mailtos.add(mailto);
         } else {
             create_compose_widget(selected, NEW_MESSAGE, null, null, mailto);
         }
@@ -465,6 +442,19 @@ public class Application.Controller : Geary.BaseObject {
         this.composer_widgets.add(widget);
     }
 
+    /** Returns a read-only collection of currently open composers .*/
+    public Gee.Collection<ComposerWidget> get_composers() {
+        return this.composer_widgets.read_only_view;
+    }
+
+    /** Opens any pending composers. */
+    public void process_pending_composers() {
+        foreach (string? mailto in this.pending_mailtos) {
+            compose(mailto);
+        }
+        this.pending_mailtos.clear();
+    }
+
     /** Displays a problem report when an error has been encountered. */
     public void report_problem(Geary.ProblemReport report) {
         debug("Problem reported: %s", report.to_string());
@@ -909,8 +899,6 @@ public class Application.Controller : Geary.BaseObject {
             Geary.Account account = context.account;
             if (this.main_window.selected_account == account) {
                 this.main_window.deselect_account();
-                cancel_folder();
-                this.previous_non_search_folder = null;
             }
 
             // Stop updating status and showing errors when closing
@@ -1311,136 +1299,6 @@ public class Application.Controller : Geary.BaseObject {
         return is_descendent;
     }
 
-    private void on_folder_selected(Geary.Folder? folder) {
-        debug("Folder %s selected", folder != null ? folder.to_string() : "(null)");
-        if (folder == null) {
-            this.current_folder = null;
-            main_window.conversation_list_view.set_model(null);
-            main_window.main_toolbar.folder = null;
-            this.main_window.folder_selected(null, null);
-        } else if (folder != this.current_folder) {
-            // To prevent the user from selecting folders too quickly,
-            // we prevent additional selection changes to occur until
-            // after a timeout has expired from the last one
-            int64 now = get_monotonic_time();
-            int64 diff = now - this.next_folder_select_allowed_usec;
-            if (diff < SELECT_FOLDER_TIMEOUT_USEC) {
-                // only start timeout if another timeout is not
-                // running ... this means the user can click madly and
-                // will see the last clicked-on folder 100ms after the
-                // first one was clicked on
-                if (this.select_folder_timeout_id == 0) {
-                    this.select_folder_timeout_id = Timeout.add(
-                        (uint) (diff / 1000),
-                        () => {
-                            this.select_folder_timeout_id = 0;
-                            this.next_folder_select_allowed_usec = 0;
-                            if (folder != this.current_folder) {
-                                do_select_folder.begin(
-                                    folder, on_select_folder_completed
-                                );
-                            }
-                            return false;
-                        });
-                }
-            } else {
-                do_select_folder.begin(folder, on_select_folder_completed);
-                this.next_folder_select_allowed_usec =
-                    now + SELECT_FOLDER_TIMEOUT_USEC;
-            }
-        }
-    }
-
-    private async void do_select_folder(Geary.Folder folder) throws Error {
-        debug("Switching to %s...", folder.to_string());
-
-        closed_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);
-
-        // re-enable copy/move to the last selected folder
-        if (current_folder != null) {
-            main_window.main_toolbar.copy_folder_menu.enable_disable_folder(current_folder, true);
-            main_window.main_toolbar.move_folder_menu.enable_disable_folder(current_folder, true);
-        }
-
-        this.current_folder = folder;
-
-        if (this.main_window.selected_account != folder.account) {
-            // If we were waiting for an account to be selected before
-            // issuing mailtos, do that now.
-            if (pending_mailtos.size > 0) {
-                foreach(string? mailto in pending_mailtos)
-                    compose(mailto);
-
-                pending_mailtos.clear();
-            }
-        }
-
-        if (!(current_folder is Geary.SearchFolder))
-            previous_non_search_folder = current_folder;
-
-        // disable copy/move to the new folder
-        main_window.main_toolbar.copy_folder_menu.enable_disable_folder(current_folder, false);
-        main_window.main_toolbar.move_folder_menu.enable_disable_folder(current_folder, false);
-
-        this.main_window.folder_selected(folder, this.cancellable_folder);
-
-        clear_new_messages("do_select_folder", null);
-
-        select_folder_mutex.release(ref mutex_token);
-
-        debug("Switched to %s", folder.to_string());
-    }
-
-    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_conversation_activated(Geary.App.Conversation activated) {
-        // Currently activating a conversation is only available for drafts folders.
-        if (current_folder == null || current_folder.special_folder_type !=
-            Geary.SpecialFolderType.DRAFTS)
-            return;
-
-        // TODO: Determine how to map between conversations and drafts correctly.
-        Geary.Email draft = activated.get_latest_recv_email(
-            Geary.App.Conversation.Location.IN_FOLDER
-        );
-
-        // Check all known composers since the draft may be open in a
-        // detached composer
-        bool already_open = false;
-        foreach (ComposerWidget composer in this.composer_widgets) {
-            if (composer.draft_id != null &&
-                composer.draft_id.equal_to(draft.id)) {
-                already_open = true;
-                composer.present();
-                composer.set_focus();
-                break;
-            }
-        }
-
-        if (!already_open) {
-            create_compose_widget(
-                activated.base_folder.account,
-                NEW_MESSAGE,
-                draft,
-                null,
-                null,
-                true
-            );
-        }
-    }
-
     private void on_special_folder_type_changed(Geary.Folder folder,
                                                 Geary.SpecialFolderType old_type,
                                                 Geary.SpecialFolderType new_type) {
@@ -1583,26 +1441,6 @@ public class Application.Controller : Geary.BaseObject {
         }
     }
 
-    private void cancel_folder() {
-        Cancellable old_cancellable = cancellable_folder;
-        cancellable_folder = new Cancellable();
-
-        old_cancellable.cancel();
-    }
-
-    // Like cancel_folder() but doesn't cancel outstanding operations, allowing them to complete
-    // in the background
-    private void closed_folder() {
-        cancellable_folder = new Cancellable();
-    }
-
-    private void cancel_search() {
-        Cancellable old_cancellable = this.cancellable_search;
-        this.cancellable_search = new Cancellable();
-
-        old_cancellable.cancel();
-    }
-
     // We need to include the second parameter, or valac doesn't recognize the function as matching
     // GearyApplication.exiting's signature.
     private bool on_application_exiting(GearyApplication sender, bool panicked) {
@@ -1612,42 +1450,36 @@ public class Application.Controller : Geary.BaseObject {
         return sender.cancel_exit();
     }
 
-    // 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_visible_conversations_changed(Gee.Set<Geary.App.Conversation> 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;
+        return (
+            folder != this.main_window.selected_folder ||
+            this.main_window.conversation_list_view.vadjustment.value != 0.0 ||
+            !this.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<Geary.App.Conversation>? supplied) {
+    public void clear_new_messages(string caller,
+                                   Gee.Set<Geary.App.Conversation>? supplied) {
+        Geary.Folder? selected = this.main_window.selected_folder;
         NotificationContext notifications = this.plugin_manager.notifications;
-        if (current_folder != null && (
-                !notifications.get_folders().contains(current_folder) ||
-                should_notify_new_messages(current_folder))) {
+        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();
 
             foreach (Geary.App.Conversation conversation in visible) {
                 try {
-                    if (notifications.are_any_new_messages(current_folder,
+                    if (notifications.are_any_new_messages(selected,
                                                            conversation.get_email_ids())) {
                         debug("Clearing new messages: %s", caller);
-                        notifications.clear_new_messages(current_folder);
+                        notifications.clear_new_messages(selected);
                         break;
                     }
                 } catch (Geary.EngineError.NOT_FOUND err) {
@@ -1795,20 +1627,20 @@ public class Application.Controller : Geary.BaseObject {
             account,
             widget,
             referred,
-            quote,
-            this.cancellable_folder
+            quote
         );
     }
 
     private async void load_composer(Geary.Account account,
                                      ComposerWidget widget,
                                      Geary.Email? referred = null,
-                                     string? quote = null,
-                                     GLib.Cancellable? cancellable) {
+                                     string? quote = null) {
         Geary.Email? full = null;
+        GLib.Cancellable? cancellable = null;
         if (referred != null) {
             AccountContext? context = this.accounts.get(account.information);
             if (context != null) {
+                cancellable = context.cancellable;
                 try {
                     full = yield context.emails.fetch_email_async(
                         referred.id,
@@ -1863,40 +1695,6 @@ public class Application.Controller : Geary.BaseObject {
         return ret.size >= 1 ? ret : null;
     }
 
-    private void do_search(string search_text) {
-        Geary.SearchFolder? search_folder = null;
-        if (this.main_window.selected_account != null) {
-            search_folder = this.main_window.selected_account.get_special_folder(
-                Geary.SpecialFolderType.SEARCH
-            ) as Geary.SearchFolder;
-        }
-
-        if (Geary.String.is_empty_or_whitespace(search_text)) {
-            if (this.previous_non_search_folder != null &&
-                this.current_folder is Geary.SearchFolder) {
-                this.main_window.folder_list.select_folder(
-                    this.previous_non_search_folder
-                );
-            }
-
-            this.main_window.folder_list.remove_search();
-
-            if (search_folder !=  null) {
-                search_folder.clear();
-            }
-        } else if (search_folder != null) {
-            cancel_search(); // Stop any search in progress
-
-            search_folder.search(
-                search_text,
-                this.application.config.get_search_strategy(),
-                this.cancellable_search
-            );
-
-            this.main_window.folder_list.set_search(search_folder);
-        }
-    }
-
     private bool should_add_folder(Gee.Collection<Geary.Folder>? all,
                                    Geary.Folder folder) {
         // if folder is openable, add it
diff --git a/src/client/application/geary-application.vala b/src/client/application/geary-application.vala
index d5de58a1..7859a615 100644
--- a/src/client/application/geary-application.vala
+++ b/src/client/application/geary-application.vala
@@ -535,14 +535,12 @@ public class GearyApplication : Gtk.Application {
     public async void show_email(Geary.Folder? folder,
                                  Geary.EmailIdentifier id) {
         yield this.present();
-
-        this.controller.main_window.show_email(folder, id);
+        yield this.controller.main_window.show_email(folder, id);
     }
 
     public async void show_folder(Geary.Folder? folder) {
         yield this.present();
-
-        this.controller.main_window.show_folder(folder);
+        yield this.controller.main_window.select_folder(folder);
     }
 
     public async void show_inspector() {
diff --git a/src/client/components/main-window.vala b/src/client/components/main-window.vala
index 285df3c1..a8afb549 100644
--- a/src/client/components/main-window.vala
+++ b/src/client/components/main-window.vala
@@ -209,10 +209,15 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
 
     private Application.Controller.AccountContext? context = null;
 
+    // Caches the last non-search folder so it can be re-selected on
+    // the search folder closing
+    private Geary.Folder? previous_non_search_folder = null;
+
     private Geary.AggregateProgressMonitor progress_monitor = new Geary.AggregateProgressMonitor();
 
     private GLib.Cancellable action_update_cancellable = new GLib.Cancellable();
-
+    private GLib.Cancellable folder_open = new GLib.Cancellable();
+    private GLib.Cancellable search_open = new GLib.Cancellable();
 
     private Geary.TimeoutManager update_ui_timeout;
     private int64 update_ui_last = 0;
@@ -344,21 +349,114 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
         update_infobar_frame();
     }
 
-    /** Selects the given account and folder. */
-    public void show_folder(Geary.Folder folder) {
-        this.folder_list.select_folder(folder);
+    /** Selects and open the given folder. */
+    public async void select_folder(Geary.Folder? to_select) {
+        if (this.selected_folder != to_select) {
+            // Cancel any existing folder loading
+            this.folder_open.cancel();
+            var cancellable = this.folder_open = new GLib.Cancellable();
+
+            // Dispose of all existing objects for the currently
+            // selected model.
+
+            if (this.selected_folder != null) {
+                this.main_toolbar.copy_folder_menu.enable_disable_folder(
+                    this.selected_folder, true
+                );
+                this.main_toolbar.move_folder_menu.enable_disable_folder(
+                    this.selected_folder, true
+                );
+
+                this.progress_monitor.remove(this.selected_folder.opening_monitor);
+                this.selected_folder.properties.notify.disconnect(update_headerbar);
+                this.selected_folder = null;
+            }
+            if (this.conversations != null) {
+                this.progress_monitor.remove(this.conversations.progress_monitor);
+                close_conversation_monitor(this.conversations);
+                this.conversations = null;
+            }
+            var conversations_model = this.conversation_list_view.get_model();
+            if (conversations_model != null) {
+                this.progress_monitor.remove(conversations_model.preview_monitor);
+                this.conversation_list_view.set_model(null);
+            }
+
+            // With everything disposed of, update existing window
+            // state
+
+            select_account(to_select != null ? to_select.account : null);
+            this.selected_folder = to_select;
+            // Ensure that the folder is selected in the UI if
+            // this was called by something other than the
+            // selection changed callback. That will check to
+            // ensure that we're not setting it again.
+            if (to_select != null) {
+                this.folder_list.select_folder(to_select);
+            } else {
+                this.folder_list.deselect_folder();
+            }
+            if (!(to_select is Geary.SearchFolder)) {
+                this.previous_non_search_folder = to_select;
+            }
+            update_conversation_actions(NONE);
+            this.main_toolbar.update_trash_button(
+                !this.is_shift_down && this.selected_folder_supports_trash
+            );
+            this.conversation_viewer.show_loading();
+
+            debug("Folder selected: %s",
+                  (to_select != null) ? to_select.to_string() : "(null)");
+
+            // Finally, hook up the new folder if any and start
+            // loading conversations.
+
+            if (to_select != null) {
+                this.progress_monitor.add(to_select.opening_monitor);
+                to_select.properties.notify.connect(update_headerbar);
+
+                this.conversations = new Geary.App.ConversationMonitor(
+                    to_select,
+                    NO_DELAY,
+                    // Include fields for the conversation viewer as well so
+                    // conversations can be displayed without having to go
+                    // back to the db
+                    ConversationListStore.REQUIRED_FIELDS |
+                    ConversationListBox.REQUIRED_FIELDS |
+                    ConversationEmail.REQUIRED_FOR_CONSTRUCT,
+                    MIN_CONVERSATION_COUNT
+                );
+                this.progress_monitor.add(this.conversations.progress_monitor);
+
+                conversations_model = new ConversationListStore(
+                    this.conversations
+                );
+                this.progress_monitor.add(conversations_model.preview_monitor);
+                this.conversation_list_view.set_model(conversations_model);
+
+                // disable copy/move to the new folder
+                this.main_toolbar.copy_folder_menu.enable_disable_folder(
+                    to_select, false
+                );
+                this.main_toolbar.move_folder_menu.enable_disable_folder(
+                    to_select, false
+                );
+
+                yield open_conversation_monitor(this.conversations, cancellable);
+                this.application.controller.clear_new_messages(
+                    GLib.Log.METHOD, null
+                );
+
+                this.application.controller.process_pending_composers();
+            }
+        }
+
+        update_headerbar();
     }
 
     /** Selects the given account, folder and email. */
-    public void show_email(Geary.Folder folder, Geary.EmailIdentifier id) {
-        // XXX this is broken in the case of the email's folder not
-        // being currently selected and loaded, since changing folders
-        // and loading the email in the conversation monitor won't
-        // have completed until well after is it obtained
-        // below. However, it should work in the only case where this
-        // currently used, that is when a user clicks on a
-        // notification for new mail in the current folder.
-        show_folder(folder);
+    public async void show_email(Geary.Folder folder, Geary.EmailIdentifier id) {
+        yield select_folder(folder);
         Geary.App.Conversation? conversation =
             this.conversations.get_by_email_identifier(id);
         if (conversation != null) {
@@ -385,6 +483,7 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
         // XXX do other things like select the first/next most highest
         // account's inbox?
         this.search_bar.set_search_text(""); // Reset search.
+        this.select_folder.begin(null);
     }
 
     /** Displays a composer addressed to a specific email address. */
@@ -433,6 +532,35 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
         return closed;
     }
 
+    public void search(string text) {
+        Geary.SearchFolder? search_folder = null;
+        if (this.selected_account != null) {
+            search_folder = this.selected_account.get_special_folder(
+                SEARCH
+            ) as Geary.SearchFolder;
+        }
+
+        // Stop any search in progress
+        this.search_open.cancel();
+        var cancellable = this.search_open = new GLib.Cancellable();
+
+        if (Geary.String.is_empty_or_whitespace(text)) {
+            if (this.previous_non_search_folder != null &&
+                this.selected_folder is Geary.SearchFolder) {
+                this.select_folder.begin(this.previous_non_search_folder);
+            }
+            this.folder_list.remove_search();
+            if (search_folder !=  null) {
+                search_folder.clear();
+            }
+        } else if (search_folder != null) {
+            search_folder.search(
+                text, this.application.config.get_search_strategy(), cancellable
+            );
+            this.folder_list.set_search(search_folder);
+        }
+    }
+
     private void load_config(Configuration config) {
         // This code both loads AND saves the pane positions with live updating. This is more
         // resilient against crashes because the value in dconf changes *immediately*, and
@@ -545,11 +673,28 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
     }
 
     private void setup_layout(Configuration config) {
+        this.notify["has-toplevel-focus"].connect(on_has_toplevel_focus);
+
+        // Search bar
+        this.search_bar.search_text_changed.connect(do_search);
+        this.search_bar_box.pack_start(this.search_bar, false, false, 0);
+
+        // Folder list
+        this.folder_list.folder_selected.connect(on_folder_selected);
+        this.folder_list.move_conversation.connect(on_move_conversation);
+        this.folder_list.copy_conversation.connect(on_copy_conversation);
+        this.folder_list_scrolled.add(this.folder_list);
+
+        // Conversation list
         this.conversation_list_view = new ConversationListView(this);
         this.conversation_list_view.load_more.connect(on_load_more);
         this.conversation_list_view.mark_conversations.connect(on_mark_conversations);
         this.conversation_list_view.conversations_selected.connect(on_conversations_selected);
+        this.conversation_list_view.conversation_activated.connect(on_conversation_activated);
+        this.conversation_list_view.visible_conversations_changed.connect(on_visible_conversations_changed);
+        this.conversation_list_scrolled.add(conversation_list_view);
 
+        // Conversation viewer
         this.conversation_viewer = new ConversationViewer(
             this.application.config
         );
@@ -557,6 +702,9 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
             on_conversation_view_added
         );
 
+        this.conversations_paned.pack2(this.conversation_viewer, true, true);
+
+        // Main toolbar
         this.main_toolbar = new MainToolbar(config);
         this.main_toolbar.move_folder_menu.folder_selected.connect(on_move_conversation);
         this.main_toolbar.copy_folder_menu.folder_selected.connect(on_copy_conversation);
@@ -581,20 +729,6 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
             set_titlebar(main_toolbar);
         }
 
-        // Search bar
-        this.search_bar_box.pack_start(this.search_bar, false, false, 0);
-
-        // Folder list
-        this.folder_list_scrolled.add(this.folder_list);
-        this.folder_list.move_conversation.connect(on_move_conversation);
-        this.folder_list.copy_conversation.connect(on_copy_conversation);
-
-        // Conversation list
-        this.conversation_list_scrolled.add(this.conversation_list_view);
-
-        // Conversation viewer
-        this.conversations_paned.pack2(this.conversation_viewer, true, true);
-
         // Status bar
         this.status_bar.set_size_request(-1, STATUS_BAR_HEIGHT);
         this.status_bar.set_border_width(2);
@@ -681,36 +815,6 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
         return base.key_release_event(event);
     }
 
-    public void folder_selected(Geary.Folder? folder,
-                                GLib.Cancellable? cancellable) {
-        if (this.selected_folder != folder) {
-            Geary.Account? previous_account = this.selected_account;
-            if (this.selected_folder != null) {
-                previous_account = this.selected_folder.account;
-                this.progress_monitor.remove(this.selected_folder.opening_monitor);
-                this.selected_folder.properties.notify.disconnect(update_headerbar);
-                close_conversation_monitor();
-            }
-
-            account_selected(folder != null ? folder.account : null);
-            this.selected_folder = folder;
-
-            update_conversation_actions(NONE);
-            this.conversation_viewer.show_loading();
-            this.main_toolbar.update_trash_button(
-                !this.is_shift_down && this.selected_folder_supports_trash
-            );
-
-            if (folder != null) {
-                this.progress_monitor.add(folder.opening_monitor);
-                folder.properties.notify.connect(update_headerbar);
-                open_conversation_monitor.begin(cancellable);
-            }
-        }
-
-        update_headerbar();
-    }
-
     /** Un-does the last executed application command, if any. */
     private async void undo() {
         Application.Controller.AccountContext? selected = this.context;
@@ -827,7 +931,7 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
         }
     }
 
-    private void account_selected(Geary.Account? account) {
+    private void select_account(Geary.Account? account) {
         if (this.selected_account != account) {
             if (this.selected_account != null) {
                 this.main_toolbar.copy_folder_menu.clear();
@@ -865,91 +969,44 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
         }
     }
 
-    private async void open_conversation_monitor(GLib.Cancellable cancellable) {
-        this.conversations = new Geary.App.ConversationMonitor(
-            this.selected_folder,
-            Geary.Folder.OpenFlags.NO_DELAY,
-            // Include fields for the conversation viewer as well so
-            // conversations can be displayed without having to go
-            // back to the db
-            ConversationListStore.REQUIRED_FIELDS |
-            ConversationListBox.REQUIRED_FIELDS |
-            ConversationEmail.REQUIRED_FOR_CONSTRUCT,
-            MIN_CONVERSATION_COUNT
-        );
-
-        this.conversations.scan_completed.connect(on_scan_completed);
-        this.conversations.scan_error.connect(on_scan_error);
+    private async void open_conversation_monitor(Geary.App.ConversationMonitor to_open,
+                                                 GLib.Cancellable cancellable) {
+        to_open.scan_completed.connect(on_scan_completed);
+        to_open.scan_error.connect(on_scan_error);
 
-        this.conversations.scan_completed.connect(
-            on_conversation_count_changed
-        );
-        this.conversations.conversations_added.connect(
-            on_conversation_count_changed
-        );
-        this.conversations.conversations_removed.connect(
-            on_conversation_count_changed
-        );
+        to_open.scan_completed.connect(on_conversation_count_changed);
+        to_open.conversations_added.connect(on_conversation_count_changed);
+        to_open.conversations_removed.connect(on_conversation_count_changed);
 
-        ConversationListStore new_model = new ConversationListStore(
-            this.conversations
-        );
-        this.progress_monitor.add(new_model.preview_monitor);
-        this.progress_monitor.add(conversations.progress_monitor);
-        this.conversation_list_view.set_model(new_model);
-
-        // Work on a local copy since the main window's copy may
-        // change if a folder is selected while closing.
-        Geary.App.ConversationMonitor conversations = this.conversations;
-        conversations.start_monitoring_async.begin(
+        to_open.start_monitoring_async.begin(
             cancellable,
             (obj, res) => {
                 try {
-                    conversations.start_monitoring_async.end(res);
-                } catch (Error err) {
-                    Geary.AccountInformation account =
-                        conversations.base_folder.account.information;
-                    this.application.controller.report_problem(
-                        new Geary.ServiceProblemReport(account, account.incoming, err)
-                    );
+                    to_open.start_monitoring_async.end(res);
+                } catch (GLib.Error err) {
+                    handle_error(to_open.base_folder.account.information, err);
                 }
             }
         );
     }
 
-    private void close_conversation_monitor() {
-        ConversationListStore? old_model =
-            this.conversation_list_view.get_model();
-        if (old_model != null) {
-            this.progress_monitor.remove(old_model.preview_monitor);
-            this.progress_monitor.remove(old_model.conversations.progress_monitor);
-        }
+    private void close_conversation_monitor(Geary.App.ConversationMonitor to_close) {
+        to_close.scan_completed.disconnect(on_scan_completed);
+        to_close.scan_error.disconnect(on_scan_error);
 
-        this.conversations.scan_completed.disconnect(on_scan_completed);
-        this.conversations.scan_error.disconnect(on_scan_error);
+        to_close.scan_completed.disconnect(on_conversation_count_changed);
+        to_close.conversations_added.disconnect(on_conversation_count_changed);
+        to_close.conversations_removed.disconnect(on_conversation_count_changed);
 
-        this.conversations.scan_completed.disconnect(
-            on_conversation_count_changed
-        );
-        this.conversations.conversations_added.disconnect(
-            on_conversation_count_changed
-        );
-        this.conversations.conversations_removed.disconnect(
-            on_conversation_count_changed
-        );
-
-        // Work on a local copy since the main window's copy may
-        // change if a folder is selected while closing.
-        Geary.App.ConversationMonitor conversations = this.conversations;
-        conversations.stop_monitoring_async.begin(
+        to_close.stop_monitoring_async.begin(
             null,
             (obj, res) => {
                 try {
-                    conversations.stop_monitoring_async.end(res);
-                } catch (Error err) {
+                    to_close.stop_monitoring_async.end(res);
+                } catch (GLib.Error err) {
                     warning(
                         "Error closing conversation monitor %s: %s",
-                        this.conversations.base_folder.to_string(),
+                        to_close.base_folder.to_string(),
                         err.message
                     );
                 }
@@ -1413,6 +1470,58 @@ public class MainWindow : Gtk.ApplicationWindow, Geary.BaseInterface {
         close();
     }
 
+    // this signal does not necessarily indicate that the application
+    // previously didn't have focus and now it does
+    private void on_has_toplevel_focus() {
+        this.application.controller.clear_new_messages(GLib.Log.METHOD, null);
+    }
+
+    private void on_folder_selected(Geary.Folder? folder) {
+        this.select_folder.begin(folder);
+    }
+
+    private void do_search(string text) {
+        search(text);
+    }
+
+    private void on_visible_conversations_changed(Gee.Set<Geary.App.Conversation> visible) {
+        this.application.controller.clear_new_messages(GLib.Log.METHOD, visible);
+    }
+
+    private void on_conversation_activated(Geary.App.Conversation activated) {
+        // Currently activating a conversation is only available for
+        // drafts folders.
+        if (this.selected_folder != null &&
+            this.selected_folder.special_folder_type == DRAFTS) {
+            // TODO: Determine how to map between conversations and
+            // drafts correctly.
+            Geary.Email draft = activated.get_latest_recv_email(IN_FOLDER);
+
+            // Check all known composers since the draft may be open
+            // in a detached composer
+            bool already_open = false;
+            foreach (ComposerWidget composer
+                     in this.application.controller.get_composers()) {
+                if (composer.draft_id != null &&
+                    composer.draft_id.equal_to(draft.id)) {
+                    already_open = true;
+                    composer.present();
+                    composer.set_focus();
+                    break;
+                }
+            }
+
+            if (!already_open) {
+                this.application.controller.compose_with_context_email(
+                    activated.base_folder.account,
+                    NEW_MESSAGE,
+                    draft,
+                    null
+                );
+            }
+        }
+    }
+
     private void on_conversation_list() {
         this.conversation_list_view.grab_focus();
     }
diff --git a/src/client/folder-list/folder-list-tree.vala b/src/client/folder-list/folder-list-tree.vala
index dd85275d..f919caf6 100644
--- a/src/client/folder-list/folder-list-tree.vala
+++ b/src/client/folder-list/folder-list-tree.vala
@@ -16,6 +16,8 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface {
     public signal void copy_conversation(Geary.Folder folder);
     public signal void move_conversation(Geary.Folder folder);
 
+    public Geary.Folder? selected { get ; private set; default = null; }
+
     private Gee.HashMap<Geary.Account, AccountBranch> account_branches
         = new Gee.HashMap<Geary.Account, AccountBranch>();
     private InboxesBranch inboxes_branch = new InboxesBranch();
@@ -64,8 +66,10 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface {
 
     private void on_entry_selected(Sidebar.SelectableEntry selectable) {
         AbstractFolderEntry? abstract_folder_entry = selectable as AbstractFolderEntry;
-        if (abstract_folder_entry != null)
+        if (abstract_folder_entry != null) {
+            this.selected = abstract_folder_entry.folder;
             folder_selected(abstract_folder_entry.folder);
+        }
     }
 
     private void on_new_messages_changed(Geary.Folder folder, int count) {
@@ -131,8 +135,10 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface {
         }
 
         // if found and selected, report nothing is selected in preparation for its removal
-        if (entry != null && is_selected(entry))
+        if (entry != null && is_selected(entry)) {
+            this.selected = null;
             folder_selected(null);
+        }
 
         // if Inbox, remove from inboxes branch, selected or not
         if (folder.special_folder_type == Geary.SpecialFolderType.INBOX)
@@ -148,6 +154,7 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface {
             // If a folder on this account is selected, unselect it.
             foreach (FolderEntry entry in account_branch.folder_entries.values) {
                 if (is_selected(entry)) {
+                    this.selected = null;
                     folder_selected(null);
                     break;
                 }
@@ -159,8 +166,10 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface {
         }
 
         Sidebar.Entry? entry = inboxes_branch.get_entry_for_account(account);
-        if (entry != null && is_selected(entry))
+        if (entry != null && is_selected(entry)) {
+            this.selected = null;
             folder_selected(null);
+        }
 
         inboxes_branch.remove_inbox(account);
 
@@ -168,10 +177,20 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface {
             prune(inboxes_branch);
     }
 
-    public void select_folder(Geary.Folder folder) {
-        FolderEntry? entry = get_folder_entry(folder);
-        if (entry != null)
-            place_cursor(entry, false);
+    public void select_folder(Geary.Folder to_select) {
+        if (this.selected != to_select) {
+            bool selected = false;
+            if (to_select.special_folder_type == INBOX) {
+                selected = select_inbox(to_select.account);
+            }
+
+            if (!selected) {
+                FolderEntry? entry = get_folder_entry(to_select);
+                if (entry != null) {
+                    place_cursor(entry, false);
+                }
+            }
+        }
     }
 
     public bool select_inbox(Geary.Account account) {
@@ -186,6 +205,10 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface {
         return true;
     }
 
+    public void deselect_folder() {
+        get_selection().unselect_all();
+    }
+
     public override bool drag_motion(Gdk.DragContext context, int x, int y, uint time) {
         // Run the base version first.
         bool ret = base.drag_motion(context, x, y, time);



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