[geary/wip/730682-refine-convo-list] Implement actions for selected conversations in the conversation list.
- From: Michael Gratton <mjog src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/wip/730682-refine-convo-list] Implement actions for selected conversations in the conversation list.
- Date: Wed, 3 Jan 2018 09:03:06 +0000 (UTC)
commit 2a30b12f896ea19a5a72f6762521d1f4b1f322e6
Author: Michael James Gratton <mike vee net>
Date: Wed Dec 27 00:38:33 2017 +1030
Implement actions for selected conversations in the conversation list.
src/client/application/geary-controller.vala | 154 ++++++++++---
src/client/components/main-window.vala | 257 +++++++++++++++++++-
.../conversation-list/conversation-list.vala | 38 +++-
3 files changed, 416 insertions(+), 33 deletions(-)
---
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 0b327dc..4317efa 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -1,6 +1,6 @@
/*
* Copyright 2016 Software Freedom Conservancy Inc.
- * Copyright 2016, 2017 Michael Gratton <mike vee net>
+ * Copyright 2016-2017 Michael Gratton <mike vee net>
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
@@ -1155,7 +1155,121 @@ public class GearyController : Geary.BaseObject {
message("Error enumerating accounts: %s", e.message);
}
}
-
+
+ /**
+ * Adds and removes flags from a set of emails.
+ */
+ internal async void mark_email(Gee.Collection<Geary.EmailIdentifier> ids,
+ Geary.EmailFlags? flags_to_add,
+ Geary.EmailFlags? flags_to_remove)
+ throws Error {
+ if (ids.size > 0) {
+ Geary.App.EmailStore store =
+ this.email_stores.get(this.current_folder.account);
+ yield store.mark_email_async(
+ ids, flags_to_add, flags_to_remove, this.cancellable_folder
+ );
+ }
+ }
+
+ /**
+ * Moves a set of conversations to a special folder.
+ */
+ internal async void move_conversations(Gee.Collection<Geary.App.Conversation> targets,
+ Geary.SpecialFolderType type)
+ throws Error {
+ Gee.List<Geary.EmailIdentifier> ids = get_ids_in_folder(targets);
+ if (type == Geary.SpecialFolderType.ARCHIVE) {
+ Geary.FolderSupport.Archive? archivable =
+ this.current_folder as Geary.FolderSupport.Archive;
+ if (archivable == null) {
+ throw new Geary.ImapError.NOT_SUPPORTED(
+ "Folder %s doesn't support archive",
+ this.current_folder.to_string()
+ );
+ }
+ save_revokable(
+ yield archivable.archive_email_async(
+ ids, this.cancellable_folder
+ ),
+ _("Undo archive (Ctrl+Z)")
+ );
+ } else {
+ Geary.Folder dest = this.current_account.get_special_folder(type);
+ Geary.FolderSupport.Move? movable =
+ this.current_folder as Geary.FolderSupport.Move;
+ if (movable == null) {
+ throw new Geary.ImapError.NOT_SUPPORTED(
+ "Folder %s doesn't support moving",
+ this.current_folder.to_string()
+ );
+ }
+ string tooltip = "";
+ switch (type) {
+ case Geary.SpecialFolderType.INBOX:
+ tooltip = _("Undo restore (Ctrl+Z)");
+ break;
+ case Geary.SpecialFolderType.TRASH:
+ tooltip = _("Undo trash (Ctrl+Z)");
+ break;
+ case Geary.SpecialFolderType.SPAM:
+ tooltip = _("Undo junk (Ctrl+Z)");
+ break;
+ default:
+ tooltip = _("Undo move (Ctrl+Z)");
+ break;
+ }
+ save_revokable(
+ yield movable.move_email_async(
+ ids, dest.path, this.cancellable_folder
+ ),
+ tooltip
+ );
+ }
+ }
+
+ /**
+ * Restores a set of messages to their original location.
+ */
+ internal async void restore_conversations(Gee.Collection<Geary.App.Conversation> targets)
+ throws Error {
+ yield move_conversations(targets, Geary.SpecialFolderType.INBOX);
+ }
+
+ /**
+ * Permanently deletes a set of conversations, without prompting.
+ */
+ internal async void delete_conversations(Gee.Collection<Geary.App.Conversation> targets)
+ throws Error {
+ Gee.List<Geary.EmailIdentifier> ids = get_ids_in_folder(targets);
+ Geary.FolderSupport.Remove? removable =
+ this.current_folder as Geary.FolderSupport.Remove;
+ if (removable == null) {
+ throw new Geary.ImapError.NOT_SUPPORTED(
+ "Folder %s doesn't support deletion",
+ this.current_folder.to_string()
+ );
+ }
+
+ yield removable.remove_email_async(ids, this.cancellable_folder);
+ }
+
+ /**
+ * Returns conversation's email ids that are in-folder.
+ */
+ private Gee.List<Geary.EmailIdentifier> get_ids_in_folder(Gee.Collection<Geary.App.Conversation>
targets) {
+ Gee.LinkedList<Geary.EmailIdentifier> ids =
+ new Gee.LinkedList<Geary.EmailIdentifier>();
+ foreach (Geary.App.Conversation convo in targets) {
+ foreach (Geary.Email email in
+ convo.get_emails(Geary.App.Conversation.Ordering.NONE,
+ Geary.App.Conversation.Location.ANYWHERE)) {
+ ids.add(email.id);
+ }
+ }
+ return ids;
+ }
+
/**
* Returns true if we've attempted to open all accounts at this point.
*/
@@ -1568,14 +1682,6 @@ public class GearyController : Geary.BaseObject {
return ids;
}
- private void mark_email(Gee.Collection<Geary.EmailIdentifier> ids,
- Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove) {
- if (ids.size > 0) {
- email_stores.get(current_folder.account).mark_email_async.begin(
- ids, flags_to_add, flags_to_remove, cancellable_folder);
- }
- }
-
private void on_show_mark_menu() {
Geary.App.Conversation conversation =
this.main_window.conversation_list.selected;
@@ -1645,7 +1751,7 @@ public class GearyController : Geary.BaseObject {
private void on_conversation_viewer_mark_emails(Gee.Collection<Geary.EmailIdentifier> emails,
Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove) {
- mark_email(emails, flags_to_add, flags_to_remove);
+ mark_email.begin(emails, flags_to_add, flags_to_remove);
}
private void on_mark_as_read(SimpleAction action) {
@@ -1653,7 +1759,7 @@ public class GearyController : Geary.BaseObject {
flags.add(Geary.EmailFlags.UNREAD);
Gee.ArrayList<Geary.EmailIdentifier> ids = get_selected_email_ids(false);
- mark_email(ids, null, flags);
+ mark_email.begin(ids, null, flags);
ConversationListBox? list =
main_window.conversation_viewer.current_list;
@@ -1668,7 +1774,7 @@ public class GearyController : Geary.BaseObject {
flags.add(Geary.EmailFlags.UNREAD);
Gee.ArrayList<Geary.EmailIdentifier> ids = get_selected_email_ids(true);
- mark_email(ids, flags, null);
+ mark_email.begin(ids, flags, null);
ConversationListBox? list =
main_window.conversation_viewer.current_list;
@@ -1681,21 +1787,21 @@ public class GearyController : Geary.BaseObject {
private void on_mark_as_starred(SimpleAction action) {
Geary.EmailFlags flags = new Geary.EmailFlags();
flags.add(Geary.EmailFlags.FLAGGED);
- mark_email(get_selected_email_ids(true), flags, null);
+ mark_email.begin(get_selected_email_ids(true), flags, null);
}
private void on_mark_as_unstarred(SimpleAction action) {
Geary.EmailFlags flags = new Geary.EmailFlags();
flags.add(Geary.EmailFlags.FLAGGED);
- mark_email(get_selected_email_ids(false), null, flags);
+ mark_email.begin(get_selected_email_ids(false), null, flags);
}
private void on_show_move_menu(SimpleAction? action) {
- this.main_window.main_toolbar.copy_conversation_button.clicked();
+ this.main_window.main_toolbar.copy_conversation_button.activate();
}
private void on_show_copy_menu(SimpleAction? action) {
- this.main_window.main_toolbar.move_conversation_button.clicked();
+ this.main_window.main_toolbar.move_conversation_button.activate();
}
private async void mark_as_spam_toggle_async(Cancellable? cancellable) {
@@ -2274,17 +2380,7 @@ public class GearyController : Geary.BaseObject {
&& !current_folder.properties.is_local_only && current_account != null
&& (current_folder as Geary.FolderSupport.Move) != null);
}
-
- public bool confirm_delete(int num_messages) {
- main_window.present();
- ConfirmationDialog dialog = new ConfirmationDialog(main_window, ngettext(
- "Do you want to permanently delete this message?",
- "Do you want to permanently delete these messages?", num_messages),
- null, _("Delete"), "destructive-action");
-
- return (dialog.run() == Gtk.ResponseType.OK);
- }
-
+
private async void archive_or_delete_selection_async(bool archive, bool trash,
Cancellable? cancellable) throws Error {
if (!can_switch_conversation_view())
@@ -2346,7 +2442,7 @@ public class GearyController : Geary.BaseObject {
if (supports_remove == null) {
debug("Folder %s doesn't support remove", current_folder.to_string());
} else {
- if (confirm_delete(ids.size))
+ if (this.main_window.confirm_delete())
yield supports_remove.remove_email_async(ids, cancellable);
else
last_deleted_conversation = null;
diff --git a/src/client/components/main-window.vala b/src/client/components/main-window.vala
index 8d8c318..97f0e09 100644
--- a/src/client/components/main-window.vala
+++ b/src/client/components/main-window.vala
@@ -10,15 +10,34 @@
public class MainWindow : Gtk.ApplicationWindow {
+ public const string ACTION_ARCHIVE = "conversation-archive";
+ public const string ACTION_DELETE = "conversation-delete";
+ public const string ACTION_JUNK = "conversation-junk";
+ public const string ACTION_MARK_READ = "conversation-mark-read";
+ public const string ACTION_MARK_STARRED = "conversation-mark-starred";
+ public const string ACTION_MARK_UNREAD = "conversation-mark-unread";
+ public const string ACTION_MARK_UNSTARRED = "conversation-mark-unstarred";
+ public const string ACTION_RESTORE = "conversation-restore";
+ public const string ACTION_TRASH = "conversation-trash";
+
public const string ACTION_SELECTION_MODE_DISABLE = "selection-mode-disable";
public const string ACTION_SELECTION_MODE_ENABLE = "selection-mode-enable";
-
private const int STATUS_BAR_HEIGHT = 18;
private const ActionEntry[] action_entries = {
- {ACTION_SELECTION_MODE_DISABLE, on_selection_mode_disabled },
- {ACTION_SELECTION_MODE_ENABLE, on_selection_mode_enabled }
+ { ACTION_ARCHIVE, on_conversation_archive },
+ { ACTION_DELETE, on_conversation_delete },
+ { ACTION_JUNK, on_conversation_junk },
+ { ACTION_MARK_READ, on_conversation_mark_read },
+ { ACTION_MARK_STARRED, on_conversation_mark_starred },
+ { ACTION_MARK_UNREAD, on_conversation_mark_unread },
+ { ACTION_MARK_UNSTARRED, on_conversation_mark_unstarred },
+ { ACTION_RESTORE, on_conversation_restore },
+ { ACTION_TRASH, on_conversation_trash },
+
+ { ACTION_SELECTION_MODE_DISABLE, on_selection_mode_disabled },
+ { ACTION_SELECTION_MODE_ENABLE, on_selection_mode_enabled }
};
@@ -298,6 +317,51 @@ public class MainWindow : Gtk.ApplicationWindow {
return handled;
}
+ /**
+ * Prompts the user to confirm deleting conversations.
+ */
+ internal bool confirm_delete() {
+ present();
+ ConfirmationDialog dialog = new ConfirmationDialog(
+ this,
+ _("Do you want to permanently delete conversation messages in this folder?"),
+ null,
+ _("Delete"),
+ "destructive-action"
+ );
+ return (dialog.run() == Gtk.ResponseType.OK);
+ }
+
+ /**
+ * Returns email ids from all highlighted conversations, if any.
+ */
+ private Gee.List<Geary.EmailIdentifier> get_highlighted_email() {
+ Gee.LinkedList<Geary.EmailIdentifier> ids =
+ new Gee.LinkedList<Geary.EmailIdentifier>();
+ foreach (Geary.App.Conversation convo in
+ this.conversation_list.get_highlighted_conversations()) {
+ ids.add_all(convo.get_email_ids());
+ }
+ return ids;
+ }
+
+ /**
+ * Returns id of most latest email in all highlighted conversations.
+ */
+ private Gee.List<Geary.EmailIdentifier> get_highlighted_latest_email() {
+ Gee.LinkedList<Geary.EmailIdentifier> ids =
+ new Gee.LinkedList<Geary.EmailIdentifier>();
+ foreach (Geary.App.Conversation convo in
+ this.conversation_list.get_highlighted_conversations()) {
+ Geary.Email? latest = convo.get_latest_sent_email(
+ Geary.App.Conversation.Location.IN_FOLDER_OUT_OF_FOLDER);
+ if (latest != null) {
+ ids.add(latest.id);
+ }
+ }
+ return ids;
+ }
+
private void setup_actions() {
add_action_entries(action_entries, this);
add_window_accelerators(ACTION_SELECTION_MODE_DISABLE, { "Escape", });
@@ -337,6 +401,16 @@ 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);
+
+ bool is_starred = target.is_flagged();
+ get_action(ACTION_MARK_UNSTARRED).set_enabled(is_starred);
+ get_action(ACTION_MARK_STARRED).set_enabled(!is_starred);
+ }
+
private inline void check_shift_event(Gdk.EventKey event) {
// FIXME: it's possible the user will press two shift keys. We want
// the shift key to report as released when they release ALL of them.
@@ -397,6 +471,13 @@ public class MainWindow : Gtk.ApplicationWindow {
this.conversation_viewer.show_none_selected();
}
+ private void report_problem(Action action, Variant? param, Error err) {
+ // XXX
+ debug("Client problem reported: %s: %s",
+ action.get_name(),
+ err.message);
+ }
+
private void on_conversation_monitor_changed() {
this.conversation_list.freeze_selection();
ConversationListModel? old_model = this.conversation_list.model;
@@ -412,6 +493,7 @@ public class MainWindow : Gtk.ApplicationWindow {
old_monitor.scan_completed.disconnect(on_conversation_count_changed);
old_monitor.conversations_added.disconnect(on_conversation_count_changed);
old_monitor.conversations_removed.disconnect(on_conversation_count_changed);
+ old_monitor.email_flags_changed.disconnect(on_conversation_flags_changed);
}
Geary.App.ConversationMonitor? new_monitor =
@@ -430,6 +512,7 @@ public class MainWindow : Gtk.ApplicationWindow {
new_monitor.scan_completed.connect(on_conversation_count_changed);
new_monitor.conversations_added.connect(on_conversation_count_changed);
new_monitor.conversations_removed.connect(on_conversation_count_changed);
+ new_monitor.email_flags_changed.connect(on_conversation_flags_changed);
}
this.conversation_list.thaw_selection();
}
@@ -514,6 +597,7 @@ public class MainWindow : Gtk.ApplicationWindow {
private void on_conversation_selection_changed(Geary.App.Conversation? selection) {
show_conversation(selection);
+ update_conversation_actions(selection);
}
private void on_conversation_activated(Geary.App.Conversation activated) {
@@ -586,6 +670,12 @@ 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);
+ }
+ }
+
[GtkCallback]
private bool on_delete_event() {
if (this.application.is_background_service) {
@@ -619,6 +709,167 @@ public class MainWindow : Gtk.ApplicationWindow {
}
}
+ private void on_conversation_archive(Action action, Variant? param) {
+ this.application.controller.move_conversations.begin(
+ this.conversation_list.get_highlighted_conversations(),
+ Geary.SpecialFolderType.ARCHIVE,
+ (obj, ret) => {
+ try {
+ this.application.controller.move_conversations.end(ret);
+ } catch (Error err) {
+ report_problem(action, param, err);
+ }
+ }
+ );
+ }
+
+ private void on_conversation_delete(Action action, Variant? param) {
+ if (confirm_delete()) {
+ this.application.controller.delete_conversations.begin(
+ this.conversation_list.get_highlighted_conversations(),
+ (obj, ret) => {
+ try {
+ this.application.controller.delete_conversations.end(ret);
+ } catch (Error err) {
+ report_problem(action, param, err);
+ }
+ }
+ );
+ }
+ }
+
+ private void on_conversation_junk(Action action, Variant? param) {
+ this.application.controller.move_conversations.begin(
+ this.conversation_list.get_highlighted_conversations(),
+ Geary.SpecialFolderType.SPAM,
+ (obj, ret) => {
+ try {
+ this.application.controller.move_conversations.end(ret);
+ } catch (Error err) {
+ report_problem(action, param, err);
+ }
+ }
+ );
+ }
+
+ private void on_conversation_mark_read(Action action, Variant? param) {
+ Geary.EmailFlags flags = new Geary.EmailFlags();
+ flags.add(Geary.EmailFlags.UNREAD);
+
+ Gee.List<Geary.EmailIdentifier> ids = get_highlighted_email();
+ ConversationListBox? list = this.conversation_viewer.current_list;
+ this.application.controller.mark_email.begin(
+ ids, null, flags,
+ (obj, ret) => {
+ try {
+ this.application.controller.mark_email.end(ret);
+ } catch (Error err) {
+ report_problem(action, param, err);
+ // undo the manual marking
+ if (list != null) {
+ foreach (Geary.EmailIdentifier id in ids) {
+ list.mark_manual_unread(id);
+ }
+ }
+ }
+ }
+ );
+
+ if (list != null) {
+ foreach (Geary.EmailIdentifier id in ids) {
+ list.mark_manual_read(id);
+ }
+ }
+ }
+
+ private void on_conversation_mark_unread(Action action, Variant? param) {
+ Geary.EmailFlags flags = new Geary.EmailFlags();
+ flags.add(Geary.EmailFlags.UNREAD);
+
+ Gee.List<Geary.EmailIdentifier> ids = get_highlighted_latest_email();
+ ConversationListBox? list = this.conversation_viewer.current_list;
+ this.application.controller.mark_email.begin(
+ ids, flags, null,
+ (obj, ret) => {
+ try {
+ this.application.controller.mark_email.end(ret);
+ } catch (Error err) {
+ report_problem(action, param, err);
+ // undo the manual marking
+ if (list != null) {
+ foreach (Geary.EmailIdentifier id in ids) {
+ list.mark_manual_read(id);
+ }
+ }
+ }
+ }
+ );
+
+ if (list != null) {
+ foreach (Geary.EmailIdentifier id in ids) {
+ list.mark_manual_unread(id);
+ }
+ }
+ }
+
+ private void on_conversation_mark_starred(Action action, Variant? param) {
+ Geary.EmailFlags flags = new Geary.EmailFlags();
+ flags.add(Geary.EmailFlags.FLAGGED);
+ this.application.controller.mark_email.begin(
+ get_highlighted_latest_email(), flags, null,
+ (obj, ret) => {
+ try {
+ this.application.controller.mark_email.end(ret);
+ } catch (Error err) {
+ report_problem(action, param, err);
+ }
+ }
+ );
+ }
+
+ private void on_conversation_mark_unstarred(Action action, Variant? param) {
+ Geary.EmailFlags flags = new Geary.EmailFlags();
+ flags.add(Geary.EmailFlags.FLAGGED);
+ this.application.controller.mark_email.begin(
+ get_highlighted_email(), null, flags,
+ (obj, ret) => {
+ try {
+ this.application.controller.mark_email.end(ret);
+ } catch (Error err) {
+ report_problem(action, param, err);
+ }
+ }
+ );
+ }
+
+ private void on_conversation_restore(Action action, Variant? param) {
+ this.application.controller.restore_conversations.begin(
+ this.conversation_list.get_highlighted_conversations(),
+ (obj, ret) => {
+ try {
+ this.application.controller.restore_conversations.end(ret);
+ } catch (Error err) {
+ report_problem(action, param, err);
+ }
+ }
+ );
+ }
+
+ private void on_conversation_trash(Action action, Variant? param) {
+ this.application.controller.move_conversations.begin(
+ this.conversation_list.get_highlighted_conversations(),
+ Geary.SpecialFolderType.TRASH,
+ (obj, ret) => {
+ try {
+ this.application.controller.move_conversations.end(ret);
+ } catch (Error err) {
+ report_problem(action, param, err);
+ }
+ }
+ );
+ }
+
+
private void on_selection_mode_enabled() {
set_selection_mode_enabled(true);
}
diff --git a/src/client/conversation-list/conversation-list.vala
b/src/client/conversation-list/conversation-list.vala
index 8c2f410..02139fc 100644
--- a/src/client/conversation-list/conversation-list.vala
+++ b/src/client/conversation-list/conversation-list.vala
@@ -12,7 +12,9 @@
* This class uses the GtkListBox's selection system for selecting and
* displaying individual conversations, and supports the GNOME3 HIG
* selection mode pattern for to allow multiple conversations to be
- * marked, independent of the list's selection.
+ * marked, independent of the list's selection. These conversations
+ * are referred to `selected` and `marked`, respectively, or
+ * `highlighted` if referring to either.
*/
public class ConversationList : Gtk.ListBox {
@@ -34,6 +36,15 @@ public class ConversationList : Gtk.ListBox {
/** Determines if selection mode is enabled for the list. */
public bool is_selection_mode_enabled { get; private set; default = false; }
+ /** Determines if the list has selected or marked conversations. */
+ public bool has_highlighted_conversations {
+ get {
+ return this.is_selection_mode_enabled
+ ? !this.marked.is_empty
+ : this.selected != null;
+ }
+ }
+
private Configuration config;
private int selected_index = -1;
private bool selection_frozen = false;
@@ -96,6 +107,31 @@ public class ConversationList : Gtk.ListBox {
}
/**
+ * Returns current selected or marked conversations, if any.
+ */
+ public bool is_highlighted(Geary.App.Conversation target) {
+ return this.is_selection_mode_enabled
+ ? this.marked.has_key(target)
+ : this.selected == target;
+ }
+
+ /**
+ * Returns current selected or marked conversations, if any.
+ */
+ public Gee.Collection<Geary.App.Conversation> get_highlighted_conversations() {
+ Gee.Collection<Geary.App.Conversation>? highlighted = null;
+ if (this.is_selection_mode_enabled) {
+ highlighted = this.get_marked_items();
+ } else {
+ highlighted = new Gee.LinkedList<Geary.App.Conversation>();
+ if (this.selected != null) {
+ highlighted.add(this.selected);
+ }
+ }
+ return highlighted;
+ }
+
+ /**
* Returns a read-only collection of currently marked items.
*
* This is distinct to the conversations marked via the list's
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]