[geary/wip/713150-conversations] Further simplifications to ConversationMonitor



commit 2445fefe0a110b66a9aeda6fa967ecd4fc960c32
Author: Jim Nelson <jim yorba org>
Date:   Wed Mar 18 15:29:02 2015 -0700

    Further simplifications to ConversationMonitor
    
    Also, client now accurately sets min_window_count to the number
    of conversations required to generate a scrollbar w/o estimating.

 src/CMakeLists.txt                                 |    2 +-
 src/client/application/geary-controller.vala       |   45 ++++++++++++--
 .../conversation-list-cell-dimensions.vala         |   63 ++++++++++++++++++++
 .../formatted-conversation-data.vala               |   45 ++++++++------
 src/engine/app/app-conversation-monitor.vala       |   36 +++++++-----
 .../app-local-load-operation.vala                  |   15 -----
 6 files changed, 152 insertions(+), 54 deletions(-)
---
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index ec586c9..db6c896 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -56,7 +56,6 @@ engine/app/conversation-monitor/app-conversation-operation.vala
 engine/app/conversation-monitor/app-external-append-operation.vala
 engine/app/conversation-monitor/app-external-remove-operation.vala
 engine/app/conversation-monitor/app-fill-window-operation.vala
-engine/app/conversation-monitor/app-local-load-operation.vala
 engine/app/conversation-monitor/app-remove-operation.vala
 engine/app/conversation-monitor/app-terminate-operation.vala
 
@@ -357,6 +356,7 @@ client/composer/email-entry.vala
 client/composer/scrollable-overlay.vala
 client/composer/webview-edit-fixer.vala
 
+client/conversation-list/conversation-list-cell-dimensions.vala
 client/conversation-list/conversation-list-cell-renderer.vala
 client/conversation-list/conversation-list-store.vala
 client/conversation-list/conversation-list-view.vala
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index aa3923e..db5cc7b 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -57,8 +57,8 @@ public class GearyController : Geary.BaseObject {
     
     public const string PROP_CURRENT_CONVERSATION ="current-conversations";
     
-    public const int MIN_CONVERSATION_COUNT = 50;
-    public const int LOAD_MORE_CONVERSATION_COUNT = 10;
+    private const int INITIAL_CONVERSATION_COUNT = 0;
+    private const int LOAD_MORE_CONVERSATION_COUNT = 10;
     
     private const string DELETE_MESSAGE_TOOLTIP_SINGLE = _("Delete conversation (Shift+Delete)");
     private const string DELETE_MESSAGE_TOOLTIP_MULTIPLE = _("Delete conversations (Shift+Delete)");
@@ -98,6 +98,8 @@ public class GearyController : Geary.BaseObject {
     
     public LoginDialog? login_dialog { get; private set; default = null; }
     
+    public ConversationListCellDimensions cell_dimensions = new ConversationListCellDimensions();
+    
     private Geary.Account? current_account = null;
     private Gee.HashMap<Geary.Account, Geary.App.EmailStore> email_stores
         = new Gee.HashMap<Geary.Account, Geary.App.EmailStore>();
@@ -164,6 +166,11 @@ public class GearyController : Geary.BaseObject {
     public signal void search_text_changed(string keywords);
     
     public GearyController() {
+        // when the ConversationListStore cell dimensions are updated, ensure that the
+        // ConversationMonitor's window count is set to a number that will load just enough
+        // conversations to create scrollbars
+        cell_dimensions.notify[ConversationListCellDimensions.PROP_CELL_HEIGHT].connect(
+            update_min_window_count);
     }
     
     ~GearyController() {
@@ -206,7 +213,7 @@ public class GearyController : Geary.BaseObject {
         main_window.conversation_list_view.load_more.connect(on_load_more);
         main_window.conversation_list_view.mark_conversations.connect(on_mark_conversations);
         
main_window.conversation_list_view.visible_conversations_changed.connect(on_visible_conversations_changed);
-        main_window.conversation_list_height_changed.connect(on_check_conversation_list_scrollbars);
+        main_window.conversation_list_height_changed.connect(on_conversation_list_view_height_changed);
         main_window.folder_list.folder_selected.connect(on_folder_selected);
         main_window.folder_list.copy_conversation.connect(on_copy_conversation);
         main_window.folder_list.move_conversation.connect(on_move_conversation);
@@ -283,7 +290,7 @@ public class GearyController : Geary.BaseObject {
         main_window.conversation_list_view.load_more.disconnect(on_load_more);
         main_window.conversation_list_view.mark_conversations.disconnect(on_mark_conversations);
         
main_window.conversation_list_view.visible_conversations_changed.disconnect(on_visible_conversations_changed);
-        main_window.conversation_list_height_changed.disconnect(on_check_conversation_list_scrollbars);
+        main_window.conversation_list_height_changed.disconnect(on_conversation_list_view_height_changed);
         main_window.folder_list.folder_selected.disconnect(on_folder_selected);
         main_window.folder_list.copy_conversation.disconnect(on_copy_conversation);
         main_window.folder_list.move_conversation.disconnect(on_move_conversation);
@@ -1447,7 +1454,7 @@ public class GearyController : Geary.BaseObject {
         update_ui();
         
         current_conversations = new Geary.App.ConversationMonitor(current_folder, 
Geary.Folder.OpenFlags.NO_DELAY,
-            ConversationListStore.REQUIRED_FIELDS, MIN_CONVERSATION_COUNT);
+            ConversationListStore.REQUIRED_FIELDS, INITIAL_CONVERSATION_COUNT);
         
         if (inboxes.values.contains(current_folder)) {
             // Inbox selected, clear new messages if visible
@@ -1465,6 +1472,10 @@ public class GearyController : Geary.BaseObject {
         current_conversations.conversation_removed.connect(on_conversation_count_changed);
         current_conversations.conversation_removed.connect(on_conversation_removed);
         
+        // update current_conversations min_window_count for enough conversations to create
+        // scrollbars
+        update_min_window_count();
+        
         if (!current_conversations.is_monitoring)
             yield current_conversations.start_monitoring_async(conversation_cancellable);
         
@@ -1477,6 +1488,11 @@ public class GearyController : Geary.BaseObject {
         debug("Scan error: %s", err.message);
     }
     
+    private void on_conversation_list_view_height_changed() {
+        update_min_window_count();
+        on_check_conversation_list_scrollbars();
+    }
+    
     private void on_check_conversation_list_scrollbars() {
         if (scrollbar_check_id != 0)
             Source.remove(scrollbar_check_id);
@@ -1497,6 +1513,11 @@ public class GearyController : Geary.BaseObject {
         if (main_window.conversation_list_has_scrollbar())
             return false;
         
+        // if conversation count is below min_window_count, give the conversation monitor a chance
+        // to keep filling until it reaches that level
+        if (current_conversations.get_conversation_count() < current_conversations.min_window_count)
+            return false;
+        
         debug("Not enough conversations (%d/%d) for scrollbars, loading more from folder %s",
             current_conversations.get_conversation_count(), current_conversations.min_window_count,
             current_folder.to_string());
@@ -1545,6 +1566,20 @@ public class GearyController : Geary.BaseObject {
         main_window.folder_list.select_folder(folder);
     }
     
+    // Update the ConversationMonitor's min_window_count to be at least enough to create scrollbars
+    // when all conversations are loaded
+    private void update_min_window_count() {
+        if (!cell_dimensions.valid || current_conversations == null)
+            return;
+        
+        // determine the minimum (floor) of the window count that is enough to show a scrollbar
+        int list_height = main_window.conversation_list_view.get_allocated_height();
+        int cells = (list_height / cell_dimensions.cell_height) + 1;
+        
+        if (current_conversations.min_window_count < cells)
+            current_conversations.min_window_count = cells;
+    }
+    
     private void on_load_more() {
         if (current_conversations == null || current_conversations.all_mail_loaded)
             return;
diff --git a/src/client/conversation-list/conversation-list-cell-dimensions.vala 
b/src/client/conversation-list/conversation-list-cell-dimensions.vala
new file mode 100644
index 0000000..052952e
--- /dev/null
+++ b/src/client/conversation-list/conversation-list-cell-dimensions.vala
@@ -0,0 +1,63 @@
+/* Copyright 2015 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+public class ConversationListCellDimensions : Geary.BaseObject {
+    public const string PROP_CELL_HEIGHT = "cell-height";
+    public const string PROP_PREVIEW_HEIGHT = "preview-height";
+    public const string PROP_VALID = "valid";
+    
+    /**
+     * The height in pixels of each cell in the { link ConversationListStore}.
+     *
+     * { link update} ensures that the notify signal for this property is only fired when the
+     * value changes.
+     *
+     * Defaults to -1.
+     */
+    public int cell_height { get; private set; default = -1; }
+    
+    /**
+     * The height in pixels of each cell in the { link ConversationListStore}.
+     *
+     * { link update} ensures that the notify signal for this property is only fired when the
+     * value changes.
+     *
+     * Defaults to -1.
+     */
+    public int preview_height { get; private set; default = -1; }
+    
+    /**
+     * Returns true when { link cell_height} and { link preview_height} are valid (non-negative)
+     * values.
+     *
+     * This does not actually check if the values are valid for screen or window dimensions, etc.
+     */
+    public bool valid { get; private set; default = false; }
+    
+    public ConversationListCellDimensions() {
+    }
+    
+    /**
+     * Update the dimensions of cells in the { link ConversationListStore}.
+     *
+     * @see cell_height
+     * @see preview_height
+     */
+    public void update(int cell_height, int preview_height) {
+        // don't fire notify signal(s) until all sets are complete
+        freeze_notify();
+        
+        if (this.cell_height != cell_height)
+            this.cell_height = cell_height;
+        
+        if (this.preview_height != preview_height)
+            this.preview_height = preview_height;
+        
+        valid = this.cell_height >= 0 && this.preview_height >= 0;
+        
+        thaw_notify();
+    }
+}
diff --git a/src/client/conversation-list/formatted-conversation-data.vala 
b/src/client/conversation-list/formatted-conversation-data.vala
index 4cd56d2..69912e8 100644
--- a/src/client/conversation-list/formatted-conversation-data.vala
+++ b/src/client/conversation-list/formatted-conversation-data.vala
@@ -72,9 +72,6 @@ public class FormattedConversationData : Geary.BaseObject {
         }
     }
     
-    private static int cell_height = -1;
-    private static int preview_height = -1;
-    
     public bool is_unread { get; set; }
     public bool is_flagged { get; set; }
     public string date { get; private set; }
@@ -87,6 +84,7 @@ public class FormattedConversationData : Geary.BaseObject {
     private Gee.List<Geary.RFC822.MailboxAddress>? account_owner_emails = null;
     private bool use_to = true;
     private CountBadge count_badge = new CountBadge(2);
+    private ConversationListCellDimensions cell_dimensions;
     
     // Creates a formatted message data from an e-mail.
     public FormattedConversationData(Geary.App.Conversation conversation, Geary.Email preview,
@@ -97,6 +95,10 @@ public class FormattedConversationData : Geary.BaseObject {
         this.account_owner_emails = account_owner_emails;
         use_to = (folder != null) && folder.special_folder_type.is_outgoing();
         
+        // take a reference to the global ConversationListCellDimensions object, which is held by
+        // the GearyController
+        cell_dimensions = GearyApplication.instance.controller.cell_dimensions;
+        
         // Load preview-related data.
         update_date_string();
         this.subject = EmailUtil.strip_subject_prefixes(preview);
@@ -109,6 +111,20 @@ public class FormattedConversationData : Geary.BaseObject {
         this.num_emails = conversation.get_count();
     }
     
+    // Creates an example message (used interally for styling calculations.)
+    public FormattedConversationData.create_example() {
+        this.is_unread = false;
+        this.is_flagged = false;
+        this.date = STYLE_EXAMPLE;
+        this.subject = STYLE_EXAMPLE;
+        this.body = STYLE_EXAMPLE + "\n" + STYLE_EXAMPLE;
+        this.num_emails = 1;
+        
+        // take a reference to the global ConversationListCellDimensions object, which is held by
+        // the GearyController
+        cell_dimensions = GearyApplication.instance.controller.cell_dimensions;
+    }
+    
     public bool update_date_string() {
         // get latest email *in folder* for the conversation's date, fall back on out-of-folder
         Geary.Email? latest = 
conversation.get_latest_recv_email(Geary.App.Conversation.Location.IN_FOLDER_OUT_OF_FOLDER);
@@ -127,16 +143,6 @@ public class FormattedConversationData : Geary.BaseObject {
         return true;
     }
     
-    // Creates an example message (used interally for styling calculations.)
-    public FormattedConversationData.create_example() {
-        this.is_unread = false;
-        this.is_flagged = false;
-        this.date = STYLE_EXAMPLE;
-        this.subject = STYLE_EXAMPLE;
-        this.body = STYLE_EXAMPLE + "\n" + STYLE_EXAMPLE;
-        this.num_emails = 1;
-    }
-    
     private uint8 gdk_to_rgb(double gdk) {
         return (uint8) (gdk.clamp(0.0, 1.0) * 255.0);
     }
@@ -232,7 +238,8 @@ public class FormattedConversationData : Geary.BaseObject {
     // Must call calculate_sizes() first.
     public void get_size(Gtk.Widget widget, Gdk.Rectangle? cell_area, out int x_offset, 
         out int y_offset, out int width, out int height) {
-        assert(cell_height != -1); // ensures calculate_sizes() was called.
+        // ensure calculate_sizes() was called
+        assert(cell_dimensions.valid);
         
         x_offset = 0;
         y_offset = 0;
@@ -240,7 +247,7 @@ public class FormattedConversationData : Geary.BaseObject {
         // conversation list to be shown as "squished":
         // https://bugzilla.gnome.org/show_bug.cgi?id=713954
         width = 1;
-        height = cell_height;
+        height = cell_dimensions.cell_height;
     }
     
     // Can be used for rendering or calculating height.
@@ -300,8 +307,7 @@ public class FormattedConversationData : Geary.BaseObject {
         }
         
         if (recalc_dims) {
-            FormattedConversationData.preview_height = preview_height;
-            FormattedConversationData.cell_height = y + preview_height;
+            cell_dimensions.update(y + preview_height, preview_height);
         } else {
             int unread_y = display_preview ? cell_area.y + LINE_SPACING * 2 : cell_area.y +
                 LINE_SPACING;
@@ -393,6 +399,9 @@ public class FormattedConversationData : Geary.BaseObject {
     
     private Pango.Rectangle render_preview(Gtk.Widget widget, Gdk.Rectangle? cell_area,
         Cairo.Context? ctx, int y, bool selected, int counter_width = 0) {
+        // the dimensions should have been calculated before calling this
+        assert(cell_dimensions.valid);
+        
         double dim = selected ? DIM_TEXT_AMOUNT : DIM_PREVIEW_TEXT_AMOUNT;
         string preview_markup = "<span foreground='%s'>%s</span>".printf(
             rgba_to_markup(dim_rgba(get_foreground_rgba(widget, selected), dim)),
@@ -409,7 +418,7 @@ public class FormattedConversationData : Geary.BaseObject {
         layout_preview.set_ellipsize(Pango.EllipsizeMode.END);
         if (ctx != null && cell_area != null) {
             layout_preview.set_width((cell_area.width - TEXT_LEFT - counter_width - LINE_SPACING) * 
Pango.SCALE);
-            layout_preview.set_height(preview_height * Pango.SCALE);
+            layout_preview.set_height(cell_dimensions.preview_height * Pango.SCALE);
             
             ctx.move_to(cell_area.x + TEXT_LEFT, y);
             Pango.cairo_show_layout(ctx, layout_preview);
diff --git a/src/engine/app/app-conversation-monitor.vala b/src/engine/app/app-conversation-monitor.vala
index d69c558..c74695a 100644
--- a/src/engine/app/app-conversation-monitor.vala
+++ b/src/engine/app/app-conversation-monitor.vala
@@ -4,6 +4,16 @@
  * (version 2.1 or later).  See the COPYING file in this distribution.
  */
 
+/**
+ * A helper class that automatically loads and sorts { link Email} in a { link Folder} into
+ * { link Conversation}s ordered by date received (descending).
+ *
+ * Before starting, the application should subscribe to ConversationMonitor's various signals to
+ * be informed of conversations being added and removed from the folder.  The application can
+ * specify the minimum number of conversations to be held by ConversationMonitor by adjusting the
+ * { link min_window_count} property.
+ */
+
 public class Geary.App.ConversationMonitor : BaseObject {
     /**
      * These are the fields Conversations require to thread emails together.  These fields will
@@ -12,8 +22,13 @@ public class Geary.App.ConversationMonitor : BaseObject {
     public const Geary.Email.Field REQUIRED_FIELDS = Geary.Email.Field.REFERENCES |
         Geary.Email.Field.FLAGS | Geary.Email.Field.DATE;
     
+    // An approximate multipler of messages-in-folder-to-conversation ... because the Engine doesn't
+    // offer a way to directly load conversations in a folder as a vector unto itself, this class
+    // loads a number of messages equal to the number of needed conversations times this multiplier
+    private const int MSG_CONV_MULTIPLIER = 2;
+    
     // # of messages to load at a time as we attempt to fill the min window.
-    private const int MIN_FILL_MESSAGE_COUNT = 5;
+    private const int MIN_FILL_MESSAGE_COUNT = 5 * MSG_CONV_MULTIPLIER;
     
     private const Geary.SpecialFolderType[] BLACKLISTED_FOLDER_TYPES = {
         Geary.SpecialFolderType.SPAM,
@@ -323,9 +338,8 @@ public class Geary.App.ConversationMonitor : BaseObject {
             yield operation_queue.stop_processing_async(cancellable_monitor);
         operation_queue.clear();
         
-        // Add the necessary initial operations ahead of anything the Folder might notify us of
-        // (additions, removals, etc.)
-        operation_queue.add(new LocalLoadOperation(this));
+        // Add the initial local load ahead of anything the Folder might notify us of
+        // (additions, removals, etc.) to prime the pump
         operation_queue.add(new FillWindowOperation(this, false));
         
         connect_to_folder();
@@ -348,15 +362,6 @@ public class Geary.App.ConversationMonitor : BaseObject {
         return true;
     }
     
-    internal async void local_load_async() {
-        debug("ConversationMonitor seeding with local email for %s", folder.to_string());
-        yield load_by_id_async(null, min_window_count, required_fields, Folder.ListFlags.LOCAL_ONLY,
-            cancellable_monitor);
-        debug("ConversationMonitor seeded for %s", folder.to_string());
-        
-        operation_queue.add(new FillWindowOperation(this, false));
-    }
-    
     /**
      * Halt monitoring of the Folder and, if specified, close it.  Note that the Cancellable
      * supplied to start_monitoring_async() is used during monitoring but *not* for this method.
@@ -834,7 +839,7 @@ public class Geary.App.ConversationMonitor : BaseObject {
         Geary.EmailIdentifier? low_id = yield get_lowest_email_id_async(null);
         if (low_id != null && !is_insert) {
             // Load at least as many messages as remaining conversations.
-            int num_to_load = Numeric.int_floor(min_window_count - conversations.size,
+            int num_to_load = Numeric.int_floor((min_window_count - conversations.size) * 
MSG_CONV_MULTIPLIER,
                 MIN_FILL_MESSAGE_COUNT);
             
             yield load_by_id_async(low_id, num_to_load, Email.Field.NONE, flags, cancellable_monitor);
@@ -844,7 +849,8 @@ public class Geary.App.ConversationMonitor : BaseObject {
         } else {
             // No existing messages or an insert invalidated our existing list,
             // need to start from scratch.
-            yield load_by_id_async(null, min_window_count, Email.Field.NONE, flags, cancellable_monitor);
+            yield load_by_id_async(null, min_window_count * MSG_CONV_MULTIPLIER, Email.Field.NONE,
+                flags, cancellable_monitor);
             
             // in this case, it's possible no new email is loaded, but that shouldn't stop a
             // reschedule


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