[geary/wip/730682-refine-convo-list] Update action state based on conversation state, not folder state.
- From: Michael Gratton <mjog src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/wip/730682-refine-convo-list] Update action state based on conversation state, not folder state.
- Date: Wed, 3 Jan 2018 09:03:36 +0000 (UTC)
commit d8a9b3a5b68ed33745a3b0d5c52f5e3523d647bf
Author: Michael James Gratton <mike vee net>
Date: Sun Dec 31 19:09:05 2017 +1100
Update action state based on conversation state, not folder state.
* src/client/application/geary-controller.vala (GearyController): Expose
new query_supported_operations() method, rework
enable_context_dependent_buttons_async() to use that.
* src/client/components/main-window.vala (MainWindow): When the selected
or marked conversations changes, query what folder operations are
supported for the set and update action state accordingly.
* src/client/conversation-list/conversation-list.vala
(ConversationListBox): Replace item_marked signal with items_marked, so
if multiple items are marked the same time, only one supported
operations query gets run.
* src/engine/api/geary-folder.vala (Folder): Add get_support_types()
method to return a folder's supported types.o
* src/engine/app/app-email-store.vala
(EmailStore::get_supported_operations_async): Never return null
src/client/application/geary-controller.vala | 54 ++++++---
src/client/components/main-window.vala | 129 +++++++++++++++++---
.../conversation-list/conversation-list.vala | 53 +++++++-
src/engine/api/geary-folder.vala | 28 ++++-
src/engine/app/app-email-store.vala | 83 +++++++------
5 files changed, 269 insertions(+), 78 deletions(-)
---
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 6d25c6f..3bb6b9f 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -1136,6 +1136,34 @@ public class GearyController : Geary.BaseObject {
}
/**
+ * Determines the common supported operations for a set of email.
+ */
+ internal async Gee.Set<Type> query_supported_operations(Gee.Collection<Geary.EmailIdentifier> ids,
+ Cancellable? cancellable)
+ throws Error {
+ Gee.Set<Type>? supported = null;
+ if (ids.size > 1) {
+ Gee.MultiMap<Geary.EmailIdentifier,Type>? selected = null;
+ Geary.App.EmailStore? store = this.email_stores.get(this.current_account);
+ if (store != null) {
+ selected = yield store.get_supported_operations_async(
+ ids, cancellable
+ );
+ }
+
+ supported = new Gee.HashSet<Type>();
+ if (selected != null) {
+ supported.add_all(selected.get_values());
+ }
+ } else {
+ // Heuristic: If there is only one email, assume it is in
+ // the current folder and return the folder's support
+ supported = this.current_folder.get_support_types();
+ }
+ return supported;
+ }
+
+ /**
* Adds and removes flags from a set of emails.
*/
internal async void mark_email(Gee.Collection<Geary.EmailIdentifier> ids,
@@ -2615,37 +2643,31 @@ public class GearyController : Geary.BaseObject {
get_window_action(ACTION_DELETE_CONVERSATION).set_enabled(sensitive && (current_folder is
Geary.FolderSupport.Remove));
cancel_context_dependent_buttons();
- enable_context_dependent_buttons_async.begin(sensitive, cancellable_context_dependent_buttons);
+ if (this.current_folder != null) {
+ enable_context_dependent_buttons_async.begin(sensitive, cancellable_context_dependent_buttons);
+ }
}
private async void enable_context_dependent_buttons_async(bool sensitive, Cancellable? cancellable) {
- Gee.MultiMap<Geary.EmailIdentifier, Type>? selected_operations = null;
+ Gee.Set<Type>? supported = null;
try {
- if (current_folder != null) {
- Geary.App.EmailStore? store = email_stores.get(current_folder.account);
- if (store != null) {
- selected_operations = yield store
- .get_supported_operations_async(get_selected_email_ids(false), cancellable);
- }
- }
+ supported = yield query_supported_operations(
+ get_selected_email_ids(false), cancellable
+ );
} catch (Error e) {
debug("Error checking for what operations are supported in the selected conversations: %s",
e.message);
}
-
+
// Exit here if the user has cancelled.
if (cancellable != null && cancellable.is_cancelled())
return;
-
- Gee.HashSet<Type> supported_operations = new Gee.HashSet<Type>();
- if (selected_operations != null)
- supported_operations.add_all(selected_operations.get_values());
get_window_action(ACTION_SHOW_MARK_MENU).set_enabled(
- sensitive && (typeof(Geary.FolderSupport.Mark) in supported_operations)
+ sensitive && (typeof(Geary.FolderSupport.Mark) in supported)
);
get_window_action(ACTION_COPY_MENU).set_enabled(
- sensitive && (supported_operations.contains(typeof(Geary.FolderSupport.Copy)))
+ sensitive && (supported.contains(typeof(Geary.FolderSupport.Copy)))
);
}
diff --git a/src/client/components/main-window.vala b/src/client/components/main-window.vala
index 3d329c0..8585b6d 100644
--- a/src/client/components/main-window.vala
+++ b/src/client/components/main-window.vala
@@ -53,6 +53,32 @@ public class MainWindow : Gtk.ApplicationWindow {
};
+ private class SupportedOperations {
+
+ internal bool supports_archive = false;
+ internal bool supports_copy = false;
+ internal bool supports_delete = false;
+ internal bool supports_mark = false;
+ internal bool supports_move = false;
+ internal bool supports_trash = false;
+
+
+ internal SupportedOperations(Geary.Folder base_folder, Gee.Set<Type>? supports) {
+ this.supports_archive = supports.contains(typeof(Geary.FolderSupport.Archive));
+ this.supports_copy = supports.contains(typeof(Geary.FolderSupport.Copy));
+ this.supports_delete = supports.contains(typeof(Geary.FolderSupport.Remove));
+ this.supports_mark = supports.contains(typeof(Geary.FolderSupport.Mark));
+ this.supports_move = supports.contains(typeof(Geary.FolderSupport.Move));
+ this.supports_trash = (
+ this.supports_move &&
+ base_folder.special_folder_type != Geary.SpecialFolderType.TRASH &&
+ !base_folder.properties.is_local_only
+ );
+ }
+
+ }
+
+
public new GearyApplication application {
get { return (GearyApplication) base.get_application(); }
set { base.set_application(value); }
@@ -77,6 +103,7 @@ public class MainWindow : Gtk.ApplicationWindow {
public Geary.Folder? current_folder { get; private set; default = null; }
public Geary.App.ConversationMonitor? current_conversations { get; private set; default = null; }
private Cancellable load_cancellable = new Cancellable();
+ private SupportedOperations? folder_operations = null;
private ConversationActionBar conversation_list_actions =
new ConversationActionBar();
@@ -122,7 +149,7 @@ public class MainWindow : Gtk.ApplicationWindow {
this.conversation_list = new ConversationList(application.config);
this.conversation_list.conversation_selection_changed.connect(on_conversation_selection_changed);
this.conversation_list.conversation_activated.connect(on_conversation_activated);
- this.conversation_list.item_marked.connect(on_conversation_item_marked);
+ this.conversation_list.items_marked.connect(on_conversation_items_marked);
this.conversation_list.marked_conversations_evaporated.connect(on_selection_mode_disabled);
this.conversation_list.selection_mode_enabled.connect(on_selection_mode_enabled);
this.conversation_list.visible_conversations_changed.connect(on_visible_conversations_changed);
@@ -417,14 +444,79 @@ public class MainWindow : Gtk.ApplicationWindow {
this.main_toolbar.folder = this.current_folder.get_display_name();
}
- private void update_conversation_actions(Geary.App.Conversation? target) {
- bool is_unread = target.is_unread();
- get_action(ACTION_MARK_READ).set_enabled(is_unread);
- get_action(ACTION_MARK_UNREAD).set_enabled(!is_unread);
+ // Queries the supported actions for the currently highlighted
+ // conversations then updates them.
+ private void query_supported_actions() {
+ // Update actions up-front using folder defaults, even when
+ // actually doing a query, so the operations are vaguely correct.
+ update_conversation_actions(this.folder_operations);
+
+ Gee.Collection<Geary.EmailIdentifier> highlighted = get_highlighted_email();
+ if (!highlighted.is_empty && highlighted.size >= 1) {
+ this.application.controller.query_supported_operations.begin(
+ highlighted,
+ this.load_cancellable,
+ (obj, res) => {
+ Gee.Set<Type>? supported = null;
+ try {
+ supported = this.application.controller.query_supported_operations.end(res);
+ } catch (Error err) {
+ debug("Error querying supported actions: %s", err.message);
+ }
+ update_conversation_actions(
+ new SupportedOperations(this.current_folder, supported)
+ );
+ });
+ }
+ }
+
+ // Updates conversation action enabled state based on those that
+ // are currently supported.
+ private void update_conversation_actions(SupportedOperations ops) {
+ Gee.Collection<Geary.App.Conversation> highlighted =
+ this.conversation_list.get_highlighted_conversations();
+ bool has_highlighted = !highlighted.is_empty;
+
+ get_action(ACTION_ARCHIVE).set_enabled(has_highlighted && ops.supports_archive);
+ get_action(ACTION_COPY).set_enabled(has_highlighted && ops.supports_copy);
+ get_action(ACTION_DELETE).set_enabled(has_highlighted && ops.supports_delete);
+ get_action(ACTION_JUNK).set_enabled(has_highlighted && ops.supports_move);
+ get_action(ACTION_MOVE).set_enabled(has_highlighted && ops.supports_move);
+ get_action(ACTION_RESTORE).set_enabled(has_highlighted && ops.supports_move);
+ get_action(ACTION_TRASH).set_enabled(has_highlighted && ops.supports_trash);
+
+ get_action(ACTION_SHOW_COPY).set_enabled(has_highlighted && ops.supports_copy);
+ get_action(ACTION_SHOW_MOVE).set_enabled(has_highlighted && ops.supports_move);
+
+ SimpleAction read = get_action(ACTION_MARK_READ);
+ SimpleAction unread = get_action(ACTION_MARK_UNREAD);
+ SimpleAction starred = get_action(ACTION_MARK_STARRED);
+ SimpleAction unstarred = get_action(ACTION_MARK_UNSTARRED);
+ if (has_highlighted && ops.supports_mark) {
+ bool has_read = false;
+ bool has_unread = false;
+ bool has_starred = false;
+
+ foreach (Geary.App.Conversation convo in highlighted) {
+ has_read |= convo.has_any_read_message();
+ has_unread |= convo.is_unread();
+ has_starred |= convo.is_flagged();
+
+ if (has_starred && has_unread && has_starred) {
+ break;
+ }
+ }
- bool is_starred = target.is_flagged();
- get_action(ACTION_MARK_UNSTARRED).set_enabled(is_starred);
- get_action(ACTION_MARK_STARRED).set_enabled(!is_starred);
+ read.set_enabled(has_unread);
+ unread.set_enabled(has_read);
+ starred.set_enabled(!has_starred);
+ unstarred.set_enabled(has_starred);
+ } else {
+ read.set_enabled(false);
+ unread.set_enabled(false);
+ starred.set_enabled(false);
+ unstarred.set_enabled(false);
+ }
}
private inline void check_shift_event(Gdk.EventKey event) {
@@ -455,6 +547,9 @@ public class MainWindow : Gtk.ApplicationWindow {
folder.properties.notify.connect(update_headerbar);
this.current_folder = folder;
+ this.folder_operations = new SupportedOperations(
+ folder, folder.get_support_types()
+ );
// Set up a new conversation monitor for the folder
Geary.App.ConversationMonitor monitor = new Geary.App.ConversationMonitor(
@@ -480,6 +575,7 @@ public class MainWindow : Gtk.ApplicationWindow {
this.conversation_list_actions.update_location(this.current_folder);
update_headerbar();
set_selection_mode_enabled(false);
+ update_conversation_actions(this.folder_operations);
this.progress_monitor.add(folder.opening_monitor);
this.progress_monitor.add(monitor.progress_monitor);
@@ -551,6 +647,7 @@ public class MainWindow : Gtk.ApplicationWindow {
this.main_toolbar.set_selection_mode_enabled(enabled);
this.conversation_list.set_selection_mode_enabled(enabled);
this.conversation_viewer.show_none_selected();
+ update_conversation_actions(this.folder_operations);
}
private void report_problem(Action action, Variant? param, Error? err = null) {
@@ -637,7 +734,7 @@ public class MainWindow : Gtk.ApplicationWindow {
private void on_conversation_selection_changed(Geary.App.Conversation? selection) {
show_conversation(selection);
- update_conversation_actions(selection);
+ query_supported_actions();
}
private void on_conversation_activated(Geary.App.Conversation activated) {
@@ -647,22 +744,24 @@ public class MainWindow : Gtk.ApplicationWindow {
// TODO: Determine how to map between conversations and drafts correctly.
Geary.Email draft = activated.get_latest_recv_email(
Geary.App.Conversation.Location.IN_FOLDER
- );
+ );
this.application.controller.create_compose_widget(
ComposerWidget.ComposeType.NEW_MESSAGE, draft, null, null, true
);
}
}
- private void on_conversation_item_marked(ConversationListItem item, bool marked) {
- if (marked) {
- show_conversation(item.conversation);
+ private void on_conversation_items_marked(Gee.List<ConversationListItem> marked,
+ Gee.List<ConversationListItem> unmarked) {
+ if (!marked.is_empty) {
+ show_conversation(marked.last().conversation);
} else {
this.conversation_viewer.show_none_selected();
}
this.main_toolbar.update_selection_count(
this.conversation_list.get_marked_items().size
);
+ query_supported_actions();
}
private void on_initial_conversation_load() {
@@ -713,8 +812,8 @@ public class MainWindow : Gtk.ApplicationWindow {
}
private void on_conversation_flags_changed(Geary.App.Conversation changed) {
- if (this.conversation_list.selected == changed) {
- update_conversation_actions(changed);
+ if (this.conversation_list.is_highlighted(changed)) {
+ update_conversation_actions(this.folder_operations);
}
}
diff --git a/src/client/conversation-list/conversation-list.vala
b/src/client/conversation-list/conversation-list.vala
index 522cc40..c84923c 100644
--- a/src/client/conversation-list/conversation-list.vala
+++ b/src/client/conversation-list/conversation-list.vala
@@ -51,6 +51,7 @@ public class ConversationList : Gtk.ListBox {
private Gee.Map<Geary.App.Conversation,ConversationListItem> marked =
new Gee.HashMap<Geary.App.Conversation,ConversationListItem>();
private ConversationListItem? last_marked = null;
+ private bool is_marking = false;
private Gee.Set<Geary.App.Conversation>? visible_conversations = null;
private Geary.Scheduler.Scheduled? update_visible_scheduled = null;
private bool enable_load_more = true;
@@ -86,9 +87,10 @@ public class ConversationList : Gtk.ListBox {
public signal void marked_conversations_evaporated();
/**
- * Fired when a list item was marked as selected in selection mode.
+ * Fired when list items are marked or unmarked in selection mode.
*/
- public signal void item_marked(ConversationListItem item, bool marked);
+ public signal void items_marked(Gee.List<ConversationListItem> marked,
+ Gee.List<ConversationListItem> unmarked);
public ConversationList(Configuration config) {
@@ -247,14 +249,26 @@ public class ConversationList : Gtk.ListBox {
} else {
anchor = last_marked;
}
+
+ this.is_marking = true;
+ Gee.List<ConversationListItem> marked =
+ new Gee.LinkedList<ConversationListItem>();
+ Gee.List<ConversationListItem> unmarked =
+ Gee.List.empty<ConversationListItem>();
+
int index = int.min(clicked.get_index(), anchor.get_index());
int end = index + (clicked.get_index() - anchor.get_index()).abs();
while (index <= end) {
ConversationListItem? row = get_item_at_index(index++);
if (row != null) {
row.set_marked(true);
+ marked.add(row);
}
}
+
+ items_marked(marked, unmarked);
+ this.is_marking = false;
+
ret = Gdk.EVENT_STOP;
}
}
@@ -279,13 +293,23 @@ public class ConversationList : Gtk.ListBox {
if (enabled) {
freeze_selection();
} else {
+ this.is_marking = true;
+ Gee.List<ConversationListItem> marked =
+ Gee.List.empty<ConversationListItem>();
+ Gee.List<ConversationListItem> unmarked =
+ new Gee.LinkedList<ConversationListItem>();
+
// Call to_array here to get a copy of the value
// collection, since unmarking the items will cause the
// underlying map to be modified
foreach (ConversationListItem item in this.marked.values.to_array()) {
item.set_marked(false);
+ unmarked.add(item);
}
- this.marked.clear();
+
+ items_marked(marked, unmarked);
+ this.is_marking = false;
+
thaw_selection();
}
this.is_selection_mode_enabled = enabled;
@@ -467,7 +491,7 @@ public class ConversationList : Gtk.ListBox {
}
}
- private void on_item_marked(ConversationListItem item, bool marked) {
+ private void on_item_marked(ConversationListItem item, bool is_marked) {
if (!this.is_selection_mode_enabled) {
// Selection mode not enabled, so the item would have
// been Ctrl-activated and we need to enable it
@@ -475,13 +499,30 @@ public class ConversationList : Gtk.ListBox {
selection_mode_enabled();
}
- if (marked) {
+ if (is_marked) {
this.marked.set(item.conversation, item);
this.last_marked = item;
} else {
this.marked.remove(item.conversation);
}
- item_marked(item, marked);
+
+ // Only fire the event for a single item if we aren't doing a
+ // mass-marking elsewhere
+ if (!this.is_marking) {
+ Gee.List<ConversationListItem> marked =
+ Gee.List.empty<ConversationListItem>();
+ Gee.List<ConversationListItem> unmarked =
+ Gee.List.empty<ConversationListItem>();
+
+ if (is_marked) {
+ marked = new Gee.LinkedList<ConversationListItem>();
+ marked.add(item);
+ } else {
+ unmarked = new Gee.LinkedList<ConversationListItem>();
+ unmarked.add(item);
+ }
+ items_marked(marked, unmarked);
+ }
}
}
diff --git a/src/engine/api/geary-folder.vala b/src/engine/api/geary-folder.vala
index e33852d..7dde63e 100644
--- a/src/engine/api/geary-folder.vala
+++ b/src/engine/api/geary-folder.vala
@@ -412,7 +412,33 @@ public abstract class Geary.Folder : BaseObject {
return (special_folder_type == Geary.SpecialFolderType.NONE)
? path.basename : special_folder_type.get_display_name();
}
-
+
+ /**
+ * Returns this folder's supported {@link FolderSupport} types.
+ */
+ public Gee.Set<Type> get_support_types() {
+ Gee.Set<Type>? ops = new Gee.HashSet<Type>();
+ if (this is Geary.FolderSupport.Archive) {
+ ops.add(typeof(Geary.FolderSupport.Archive));
+ }
+ if (this is Geary.FolderSupport.Copy) {
+ ops.add(typeof(Geary.FolderSupport.Copy));
+ }
+ if (this is Geary.FolderSupport.Create) {
+ ops.add(typeof(Geary.FolderSupport.Create));
+ }
+ if (this is Geary.FolderSupport.Mark) {
+ ops.add(typeof(Geary.FolderSupport.Mark));
+ }
+ if (this is Geary.FolderSupport.Move) {
+ ops.add(typeof(Geary.FolderSupport.Move));
+ }
+ if (this is Geary.FolderSupport.Remove) {
+ ops.add(typeof(Geary.FolderSupport.Remove));
+ }
+ return ops;
+ }
+
/**
* Returns the state of the Folder's connections to the local and remote stores.
*/
diff --git a/src/engine/app/app-email-store.vala b/src/engine/app/app-email-store.vala
index f74ede3..38d3467 100644
--- a/src/engine/app/app-email-store.vala
+++ b/src/engine/app/app-email-store.vala
@@ -10,57 +10,60 @@ public class Geary.App.EmailStore : BaseObject {
public EmailStore(Geary.Account account) {
this.account = account;
}
-
+
/**
+ * Determines the supported operations for a set of email.
+ *
* Return a map of EmailIdentifiers to the special Geary.FolderSupport
* interfaces each one supports. For example, if an EmailIdentifier comes
* back mapped to typeof(Geary.FolderSupport.Mark), it can be marked via
* mark_email_async(). If an EmailIdentifier doesn't appear in the
* returned map, no operations are supported on it.
*/
- public async Gee.MultiMap<Geary.EmailIdentifier, Type>? get_supported_operations_async(
- Gee.Collection<Geary.EmailIdentifier> emails, Cancellable? cancellable = null) throws Error {
- Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath>? folders
- = yield account.get_containing_folders_async(emails, cancellable);
- if (folders == null)
- return null;
-
- Gee.HashSet<Type> all_support = new Gee.HashSet<Type>();
- all_support.add(typeof(Geary.FolderSupport.Archive));
- all_support.add(typeof(Geary.FolderSupport.Copy));
- all_support.add(typeof(Geary.FolderSupport.Create));
- all_support.add(typeof(Geary.FolderSupport.Mark));
- all_support.add(typeof(Geary.FolderSupport.Move));
- all_support.add(typeof(Geary.FolderSupport.Remove));
-
- Gee.HashMultiMap<Geary.EmailIdentifier, Type> map
- = new Gee.HashMultiMap<Geary.EmailIdentifier, Type>();
- foreach (Geary.EmailIdentifier email in folders.get_keys()) {
- Gee.HashSet<Type> support = new Gee.HashSet<Type>();
-
- foreach (Geary.FolderPath path in folders.get(email)) {
- Geary.Folder folder;
- try {
- folder = yield account.fetch_folder_async(path, cancellable);
- } catch (Error e) {
- debug("Error getting a folder from path %s: %s", path.to_string(), e.message);
- continue;
- }
-
- foreach (Type type in all_support) {
- if (folder.get_type().is_a(type))
- support.add(type);
+ public async Gee.MultiMap<Geary.EmailIdentifier,Type>
+ get_supported_operations_async(Gee.Collection<Geary.EmailIdentifier> emails,
+ Cancellable? cancellable = null)
+ throws Error {
+ Gee.HashMultiMap<Geary.EmailIdentifier,Type> supported =
+ new Gee.HashMultiMap<Geary.EmailIdentifier,Type>();
+ Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath>? folders =
+ yield account.get_containing_folders_async(emails, cancellable);
+ if (folders != null) {
+ Gee.HashSet<Type> all_support = new Gee.HashSet<Type>();
+ all_support.add(typeof(Geary.FolderSupport.Archive));
+ all_support.add(typeof(Geary.FolderSupport.Copy));
+ all_support.add(typeof(Geary.FolderSupport.Create));
+ all_support.add(typeof(Geary.FolderSupport.Mark));
+ all_support.add(typeof(Geary.FolderSupport.Move));
+ all_support.add(typeof(Geary.FolderSupport.Remove));
+
+ foreach (Geary.EmailIdentifier email in folders.get_keys()) {
+ Gee.HashSet<Type> support = new Gee.HashSet<Type>();
+
+ foreach (Geary.FolderPath path in folders.get(email)) {
+ Geary.Folder folder;
+ try {
+ folder = yield account.fetch_folder_async(path, cancellable);
+ } catch (Error e) {
+ debug("Error getting a folder from path %s: %s", path.to_string(), e.message);
+ continue;
+ }
+
+ foreach (Type type in all_support) {
+ if (folder.get_type().is_a(type))
+ support.add(type);
+ }
+ if (support.contains_all(all_support))
+ break;
}
- if (support.contains_all(all_support))
- break;
+
+ Geary.Collection.multi_map_set_all<Geary.EmailIdentifier, Type>(supported, email, support);
}
-
- Geary.Collection.multi_map_set_all<Geary.EmailIdentifier, Type>(map, email, support);
}
-
- return (map.size > 0 ? map : null);
+
+ return supported;
}
-
+
/**
* Lists any set of EmailIdentifiers as if they were all in one folder.
*/
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]