[geary/gnumdk/stable: 2/2] client: conversation-list: Migrate from `TreeView` to `ListBox`
- From: Cédric Bellegarde <cbellegarde src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/gnumdk/stable: 2/2] client: conversation-list: Migrate from `TreeView` to `ListBox`
- Date: Tue, 20 Sep 2022 15:20:32 +0000 (UTC)
commit 0e4984e98fa76bc45cc3ac366a33788ca7c8c894
Author: Cédric Bellegarde <cedric bellegarde adishatz org>
Date: Wed Sep 14 15:12:49 2022 +0200
client: conversation-list: Migrate from `TreeView` to `ListBox`
- Replace ConversationListStore with ConversationListModel
- Replace GtkTreeView with GtkListBox
- Implement proper multiselection for ListBox
- Rework navigation to be touch friendly
Fork of John Renner <john jrenner net> merge request !698
po/POTFILES.in | 8 +-
src/client/application/application-client.vala | 13 +-
.../application/application-main-window.vala | 176 ++--
.../components-conversation-actions.vala | 7 +
.../components-headerbar-conversation-list.vala | 7 +
src/client/components/count-badge.vala | 5 +-
.../conversation-list-cell-renderer.vala | 73 --
.../conversation-list/conversation-list-model.vala | 139 +++
.../conversation-list-participant.vala | 68 ++
.../conversation-list/conversation-list-row.vala | 211 ++++
.../conversation-list/conversation-list-store.vala | 494 ---------
.../conversation-list/conversation-list-view.vala | 1061 ++++++++++----------
.../formatted-conversation-data.vala | 476 ---------
.../conversation-viewer/conversation-viewer.vala | 6 +-
src/client/meson.build | 6 +-
.../sidebar/sidebar-count-cell-renderer.vala | 2 +-
src/engine/util/util-numeric.vala | 14 +
ui/application-main-window.ui | 43 +-
ui/components-conversation-actions.ui | 21 +-
ui/components-headerbar-conversation-list.ui | 19 +-
ui/conversation-list-row.ui | 214 ++++
ui/conversation-list-view.ui | 25 +
ui/geary.css | 122 ++-
ui/org.gnome.Geary.gresource.xml | 2 +
24 files changed, 1453 insertions(+), 1759 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index d8301ec4f..20c6005f6 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -70,10 +70,10 @@ src/client/composer/composer-widget.vala
src/client/composer/composer-window.vala
src/client/composer/contact-entry-completion.vala
src/client/composer/spell-check-popover.vala
-src/client/conversation-list/conversation-list-cell-renderer.vala
-src/client/conversation-list/conversation-list-store.vala
+src/client/conversation-list/conversation-list-model.vala
+src/client/conversation-list/conversation-list-row.vala
src/client/conversation-list/conversation-list-view.vala
-src/client/conversation-list/formatted-conversation-data.vala
+src/client/conversation-list/conversation-list-participant.vala
src/client/conversation-viewer/conversation-email.vala
src/client/conversation-viewer/conversation-list-box.vala
src/client/conversation-viewer/conversation-message.vala
@@ -468,6 +468,8 @@ ui/components-placeholder-pane.ui
ui/conversation-contact-popover.ui
ui/conversation-email.ui
ui/conversation-email-menus.ui
+ui/conversation-list-row.ui
+ui/conversation-list-view.ui
ui/conversation-message-link-popover.ui
ui/conversation-message-menus.ui
ui/conversation-message.ui
diff --git a/src/client/application/application-client.vala b/src/client/application/application-client.vala
index 04b73f8d0..a5ac04223 100644
--- a/src/client/application/application-client.vala
+++ b/src/client/application/application-client.vala
@@ -399,17 +399,6 @@ public class Application.Client : Gtk.Application {
add_edit_accelerators(Action.Edit.REDO, { "<Ctrl><Shift>Z" });
add_edit_accelerators(Action.Edit.UNDO, { "<Ctrl>Z" });
- // Set up custom keybindings
- unowned Gtk.BindingSet bindings = Gtk.BindingSet.by_class(
- (ObjectClass) typeof(Gtk.ListBoxRow).class_ref()
- );
- Gtk.BindingEntry.add_signal(
- bindings, Gdk.Key.Right, MOD1_MASK, "activate", 0
- );
- Gtk.BindingEntry.add_signal(
- bindings, Gdk.Key.Forward, 0, "activate", 0
- );
-
// Load Geary GTK CSS
var provider = new Gtk.CssProvider();
Gtk.StyleContext.add_provider_for_screen(
@@ -1203,7 +1192,7 @@ public class Application.Client : Gtk.Application {
MainWindow? current = this.last_active_main_window;
if (current != null) {
folder = current.selected_folder;
- conversations = current.conversation_list_view.copy_selected();
+ conversations = current.conversation_list_view.get_selected();
}
this.new_window.begin(folder, conversations);
}
diff --git a/src/client/application/application-main-window.vala
b/src/client/application/application-main-window.vala
index 49a882a7f..6b9be281a 100644
--- a/src/client/application/application-main-window.vala
+++ b/src/client/application/application-main-window.vala
@@ -207,6 +207,12 @@ public class Application.MainWindow :
"navigate", 1,
typeof(Gtk.ScrollType), Gtk.ScrollType.PAGE_LEFT
);
+ Gtk.BindingEntry.add_signal(
+ bindings,
+ Gdk.Key.Escape, 0,
+ "navigate", 1,
+ typeof(Gtk.ScrollType), Gtk.ScrollType.PAGE_LEFT
+ );
Gtk.BindingEntry.add_signal(
bindings,
Gdk.Key.Back, 0,
@@ -355,7 +361,7 @@ public class Application.MainWindow :
// Widget descendants
public FolderList.Tree folder_list { get; private set; default = new FolderList.Tree(); }
public SearchBar search_bar { get; private set; }
- public ConversationListView conversation_list_view { get; private set; }
+ public ConversationList.View conversation_list_view { get; private set; }
public ConversationViewer conversation_viewer { get; private set; }
public Components.InfoBarStack conversation_list_info_bars {
@@ -402,7 +408,6 @@ public class Application.MainWindow :
[GtkChild] private unowned Gtk.ScrolledWindow folder_list_scrolled;
[GtkChild] private unowned Gtk.Box conversation_list_box;
- [GtkChild] private unowned Gtk.ScrolledWindow conversation_list_scrolled;
[GtkChild] private unowned Gtk.Revealer conversation_list_actions_revealer;
[GtkChild] private unowned Components.ConversationActions conversation_list_actions;
@@ -659,7 +664,9 @@ public class Application.MainWindow :
cert_retry.clicked.connect(on_cert_problem_retry);
this.cert_problem_infobar.get_action_area().add(cert_retry);
- this.conversation_list_view.grab_focus();
+ this.map.connect(() => {
+ this.folder_list.grab_focus();
+ });
foreach (var actions in this.folder_conversation_actions) {
actions.mark_message_button_toggled.connect(on_show_mark_menu);
@@ -760,6 +767,8 @@ public class Application.MainWindow :
this.folder_open.cancel();
var cancellable = this.folder_open = new GLib.Cancellable();
+ this.conversation_list_headerbar.selection_open = false;
+
// Dispose of all existing objects for the currently
// selected model.
@@ -776,11 +785,7 @@ public class Application.MainWindow :
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);
+ this.conversation_list_view.set_monitor(null);
}
this.conversation_list_info_bars.remove_all();
@@ -829,22 +834,17 @@ public class Application.MainWindow :
// Include fields for the conversation viewer as well so
// conversations can be displayed without having to go
// back to the db
- ConversationListStore.REQUIRED_FIELDS |
+ ConversationList.View.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.application.config
-
- );
- this.progress_monitor.add(conversations_model.preview_monitor);
if (inhibit_autoselect) {
this.conversation_list_view.inhibit_next_autoselect();
}
- this.conversation_list_view.set_model(conversations_model);
+ this.conversation_list_view.set_monitor(this.conversations);
// disable copy/move to the new folder
foreach (var menu in this.folder_popovers) {
@@ -930,7 +930,6 @@ public class Application.MainWindow :
Gee.Collection.empty<Geary.EmailIdentifier>(),
is_interactive
);
- } else {
}
}
}
@@ -1335,15 +1334,16 @@ public class Application.MainWindow :
this.conversation_list_box.pack_start(
this.conversation_list_info_bars, false, false, 0
);
- this.conversation_list_view = new ConversationListView(
- this.application.config
- );
- this.conversation_list_view.load_more.connect(on_load_more);
+
+ this.conversation_list_view = new ConversationList.View(this.application.config);
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);
+ this.conversation_list_view.conversation_alt_action.connect(on_conversation_alt_action);
+ this.conversation_list_view.visible_conversations.notify.connect(on_visible_conversations_changed);
+
+ this.conversation_list_box.pack_start(
+ this.conversation_list_view, true, true, 0
+ );
// Conversation viewer
this.conversation_viewer = new ConversationViewer(
@@ -1361,11 +1361,25 @@ public class Application.MainWindow :
this.search_bar, "search-mode-enabled",
SYNC_CREATE | BIDIRECTIONAL
);
+ this.conversation_list_headerbar.bind_property(
+ "selection-open",
+ this.conversation_list_view, "selection-mode-enabled",
+ SYNC_CREATE | BIDIRECTIONAL
+ );
this.conversation_headerbar.bind_property(
"find-open",
this.conversation_viewer.conversation_find_bar, "search-mode-enabled",
SYNC_CREATE | BIDIRECTIONAL
);
+ this.conversation_list_headerbar.notify["selection-open"].connect(
+ () => {
+ if (this.conversation_list_view.selection_mode_enabled)
+ this.conversation_list_actions_revealer.reveal_child = (
+ this.outer_leaflet.folded);
+ else
+ this.conversation_list_actions_revealer.reveal_child = false;
+ }
+ );
this.conversation_headerbar.notify["shown-actions"].connect(
() => {
this.conversation_viewer_actions_revealer.reveal_child = (
@@ -1383,6 +1397,8 @@ public class Application.MainWindow :
this.status_bar.add(this.spinner);
this.status_bar.show_all();
+ this.conversation_list_actions.set_mark_inverted();
+
this.folder_conversation_actions = {
this.conversation_headerbar.full_actions,
this.conversation_list_actions
@@ -1552,11 +1568,7 @@ public class Application.MainWindow :
this.conversation_viewer.current_list.update_display();
}
- ConversationListStore? list_store =
- this.conversation_list_view.get_model() as ConversationListStore;
- if (list_store != null) {
- list_store.update_display();
- }
+ this.conversation_list_view.refresh_times();
}
}
@@ -1646,6 +1658,9 @@ public class Application.MainWindow :
context.contacts,
start_mark_timer
);
+ if (is_interactive) {
+ focus_next_pane(true);
+ }
} catch (Geary.EngineError.NOT_FOUND err) {
// The first interesting email from the
// conversation wasn't found. If the
@@ -1750,15 +1765,14 @@ public class Application.MainWindow :
}
}
- private void load_more() {
- if (this.is_conversation_list_shown &&
- this.conversations != null) {
- this.conversations.min_window_count += MIN_CONVERSATION_COUNT;
- }
- }
-
- private void on_conversations_selected(Gee.Set<Geary.App.Conversation> selected) {
- this.select_conversations.begin(selected, Gee.Collection.empty(), true);
+ private void on_conversations_selected(Gee.Set<Geary.App.Conversation> selected, bool is_interactive) {
+ this.select_conversations.begin(selected, Gee.Collection.empty(), is_interactive);
+ if (this.conversation_list_view.selection_mode_enabled)
+ if (selected.size > 0)
+ this.conversation_list_actions_revealer.reveal_child = (
+ this.outer_leaflet.folded);
+ else
+ this.conversation_list_actions_revealer.reveal_child = false;
}
private void on_conversation_count_changed() {
@@ -1778,7 +1792,7 @@ public class Application.MainWindow :
// conversations_selected firing from the convo list,
// so we need to stop the loading spinner here.
if (!this.application.config.autoselect &&
- this.conversation_list_view.get_selection().count_selected_rows() == 0) {
+ this.conversation_list_view.get_selected().size == 0) {
this.conversation_viewer.show_none_selected();
update_conversation_actions(NONE);
}
@@ -1863,20 +1877,6 @@ public class Application.MainWindow :
sensitive && (this.selected_folder is Geary.FolderSupport.Remove)
);
- switch (count) {
- case NONE:
- this.conversation_list_actions_revealer.reveal_child = false;
- break;
- case SINGLE:
- this.conversation_list_actions_revealer.reveal_child = (
- this.outer_leaflet.folded
- );
- break;
- case MULTIPLE:
- this.conversation_list_actions_revealer.reveal_child = true;
- break;
- }
-
this.update_context_dependent_actions.begin(sensitive);
}
@@ -1954,7 +1954,7 @@ public class Application.MainWindow :
}
}
- private void focus_next_pane() {
+ private void focus_next_pane(bool no_conversations_focus=false) {
var focus = get_focus();
if (this.outer_leaflet.folded) {
@@ -1976,8 +1976,9 @@ public class Application.MainWindow :
if (focus == this.folder_list ||
focus.is_ancestor(this.folder_list)) {
focus = this.conversation_list_view;
- } else if (focus == this.conversation_list_view ||
- focus.is_ancestor(this.conversation_list_view)) {
+ } else if (!no_conversations_focus && (
+ focus == this.conversation_list_view ||
+ focus.is_ancestor(this.conversation_list_view))) {
focus = this.conversation_viewer.visible_child;
} else if (focus == this.conversation_viewer ||
focus.is_ancestor(this.conversation_viewer)) {
@@ -1994,7 +1995,6 @@ public class Application.MainWindow :
private void focus_previous_pane() {
var focus = get_focus();
-
if (this.outer_leaflet.folded) {
if (this.outer_leaflet.visible_child_name == INNER_LEAFLET) {
if (this.inner_leaflet.folded) {
@@ -2003,7 +2003,8 @@ public class Application.MainWindow :
focus = this.folder_list;
}
} else {
- if (focus == this.conversation_list_view)
+ if (focus == this.conversation_list_view ||
+ focus.is_ancestor(this.conversation_list_view))
focus = this.folder_list;
else
focus = this.conversation_list_view;
@@ -2053,7 +2054,7 @@ public class Application.MainWindow :
// Done scanning. Check if we have enough messages to fill
// the conversation list; if not, trigger a load_more();
Gtk.Scrollbar? scrollbar = (
- this.conversation_list_scrolled.get_vscrollbar() as Gtk.Scrollbar
+ this.conversation_list_view.get_vscrollbar() as Gtk.Scrollbar
);
if (is_visible() &&
(scrollbar == null || !scrollbar.get_visible()) &&
@@ -2061,7 +2062,7 @@ public class Application.MainWindow :
monitor.can_load_more) {
debug("Not enough messages, loading more for folder %s",
this.selected_folder.to_string());
- load_more();
+ this.conversation_list_view.load_more(MIN_CONVERSATION_COUNT);
}
}
@@ -2074,10 +2075,6 @@ public class Application.MainWindow :
);
}
- private void on_load_more() {
- load_more();
- }
-
[GtkCallback]
private void on_map() {
this.update_ui_timeout.start();
@@ -2319,7 +2316,7 @@ public class Application.MainWindow :
if (this.selected_folder != null) {
this.controller.clear_new_messages(
this.selected_folder,
- this.conversation_list_view.get_visible_conversations()
+ this.conversation_list_view.visible_conversations
);
}
}
@@ -2353,27 +2350,24 @@ public class Application.MainWindow :
}
}
- private void on_visible_conversations_changed(Gee.Set<Geary.App.Conversation> visible) {
+ private void on_visible_conversations_changed() {
if (this.selected_folder != null) {
- this.controller.clear_new_messages(this.selected_folder, visible);
+ this.controller.clear_new_messages(this.selected_folder,
this.conversation_list_view.visible_conversations);
}
}
private void on_folder_activated(Geary.Folder? folder) {
- if (folder != null)
+ if (folder != null) {
focus_next_pane();
+ }
}
- private void on_conversation_activated(Geary.App.Conversation activated, bool single) {
- if (single) {
- if (this.outer_leaflet.folded) {
- focus_next_pane();
- }
- } else if (this.selected_folder != null) {
+ private void on_conversation_alt_action(Geary.App.Conversation activated) {
+ if (this.selected_folder != null) {
if (this.selected_folder.used_as != DRAFTS) {
this.application.new_window.begin(
this.selected_folder,
- this.conversation_list_view.copy_selected()
+ this.conversation_list_view.get_selected()
);
} else {
// TODO: Determine how to map between conversations
@@ -2527,7 +2521,7 @@ public class Application.MainWindow :
if (location != null) {
this.controller.mark_conversations.begin(
location,
- this.conversation_list_view.copy_selected(),
+ this.conversation_list_view.get_selected(),
Geary.EmailFlags.UNREAD,
false,
(obj, res) => {
@@ -2539,6 +2533,7 @@ public class Application.MainWindow :
}
);
}
+ this.conversation_list_view.selection_mode_enabled = false;
}
private void on_mark_as_unread() {
@@ -2546,7 +2541,7 @@ public class Application.MainWindow :
if (location != null) {
this.controller.mark_conversations.begin(
location,
- this.conversation_list_view.copy_selected(),
+ this.conversation_list_view.get_selected(),
Geary.EmailFlags.UNREAD,
true,
(obj, res) => {
@@ -2558,6 +2553,7 @@ public class Application.MainWindow :
}
);
}
+ this.conversation_list_view.selection_mode_enabled = false;
}
private void on_mark_as_starred() {
@@ -2565,7 +2561,7 @@ public class Application.MainWindow :
if (location != null) {
this.controller.mark_conversations.begin(
location,
- this.conversation_list_view.copy_selected(),
+ this.conversation_list_view.get_selected(),
Geary.EmailFlags.FLAGGED,
true,
(obj, res) => {
@@ -2577,6 +2573,7 @@ public class Application.MainWindow :
}
);
}
+ this.conversation_list_view.selection_mode_enabled = false;
}
private void on_mark_as_unstarred() {
@@ -2584,7 +2581,7 @@ public class Application.MainWindow :
if (location != null) {
this.controller.mark_conversations.begin(
location,
- this.conversation_list_view.copy_selected(),
+ this.conversation_list_view.get_selected(),
Geary.EmailFlags.FLAGGED,
false,
(obj, res) => {
@@ -2596,6 +2593,7 @@ public class Application.MainWindow :
}
);
}
+ this.conversation_list_view.selection_mode_enabled = false;
}
private void on_mark_as_junk_toggle() {
@@ -2608,7 +2606,7 @@ public class Application.MainWindow :
this.controller.move_conversations_special.begin(
source,
destination,
- this.conversation_list_view.copy_selected(),
+ this.conversation_list_view.get_selected(),
(obj, res) => {
try {
this.controller.move_conversations_special.end(res);
@@ -2618,6 +2616,7 @@ public class Application.MainWindow :
}
);
}
+ this.conversation_list_view.selection_mode_enabled = false;
}
private void on_move_conversation(Geary.Folder destination) {
@@ -2627,7 +2626,7 @@ public class Application.MainWindow :
this.controller.move_conversations.begin(
source,
destination,
- this.conversation_list_view.copy_selected(),
+ this.conversation_list_view.get_selected(),
(obj, res) => {
try {
this.controller.move_conversations.end(res);
@@ -2638,6 +2637,7 @@ public class Application.MainWindow :
);
}
+ this.conversation_list_view.selection_mode_enabled = false;
}
private void on_copy_conversation(Geary.Folder destination) {
@@ -2647,7 +2647,7 @@ public class Application.MainWindow :
this.controller.copy_conversations.begin(
source,
destination,
- this.conversation_list_view.copy_selected(),
+ this.conversation_list_view.get_selected(),
(obj, res) => {
try {
this.controller.copy_conversations.end(res);
@@ -2658,6 +2658,7 @@ public class Application.MainWindow :
);
}
+ this.conversation_list_view.selection_mode_enabled = false;
}
private void on_archive_conversation() {
@@ -2666,7 +2667,7 @@ public class Application.MainWindow :
this.controller.move_conversations_special.begin(
source,
ARCHIVE,
- this.conversation_list_view.copy_selected(),
+ this.conversation_list_view.get_selected(),
(obj, res) => {
try {
this.controller.move_conversations_special.end(res);
@@ -2676,6 +2677,7 @@ public class Application.MainWindow :
}
);
}
+ this.conversation_list_view.selection_mode_enabled = false;
}
private void on_trash_conversation() {
@@ -2684,7 +2686,7 @@ public class Application.MainWindow :
this.controller.move_conversations_special.begin(
source,
TRASH,
- this.conversation_list_view.copy_selected(),
+ this.conversation_list_view.get_selected(),
(obj, res) => {
try {
this.controller.move_conversations_special.end(res);
@@ -2694,13 +2696,14 @@ public class Application.MainWindow :
}
);
}
+ // No need to disable selection mode, handled by model change
}
private void on_delete_conversation() {
Geary.FolderSupport.Remove target =
this.selected_folder as Geary.FolderSupport.Remove;
Gee.Collection<Geary.App.Conversation> conversations =
- this.conversation_list_view.copy_selected();
+ this.conversation_list_view.get_selected();
if (target != null && this.prompt_delete_conversations(conversations.size)) {
this.controller.delete_conversations.begin(
target,
@@ -2714,6 +2717,7 @@ public class Application.MainWindow :
}
);
}
+ // No need to disable selection mode, handled by model change
}
private void on_email_loaded(ConversationListBox view,
@@ -2755,6 +2759,7 @@ public class Application.MainWindow :
}
);
}
+ this.conversation_list_view.selection_mode_enabled = false;
}
private void on_email_reply_to_sender(Geary.Email target, string? quote) {
@@ -2763,6 +2768,7 @@ public class Application.MainWindow :
this.selected_account, REPLY_SENDER, target, quote
);
}
+ this.conversation_list_view.selection_mode_enabled = false;
}
private void on_email_reply_to_all(Geary.Email target, string? quote) {
@@ -2771,6 +2777,7 @@ public class Application.MainWindow :
this.selected_account, REPLY_ALL, target, quote
);
}
+ this.conversation_list_view.selection_mode_enabled = false;
}
private void on_email_forward(Geary.Email target, string? quote) {
@@ -2779,6 +2786,7 @@ public class Application.MainWindow :
this.selected_account, FORWARD, target, quote
);
}
+ this.conversation_list_view.selection_mode_enabled = false;
}
private void on_email_trash(ConversationListBox view, Geary.Email target) {
diff --git a/src/client/components/components-conversation-actions.vala
b/src/client/components/components-conversation-actions.vala
index 7adb6afc3..0483b9f37 100644
--- a/src/client/components/components-conversation-actions.vala
+++ b/src/client/components/components-conversation-actions.vala
@@ -98,6 +98,13 @@ public class Components.ConversationActions : Gtk.Box {
this.copy_message_button.clicked();
}
+ public void set_mark_inverted() {
+ var image = new Gtk.Image.from_icon_name(
+ "pan-up-symbolic", Gtk.IconSize.BUTTON
+ );
+ this.mark_message_button.set_image(image);
+ }
+
public void update_trash_button(bool show_trash) {
this.show_trash_button = show_trash;
update_conversation_buttons();
diff --git a/src/client/components/components-headerbar-conversation-list.vala
b/src/client/components/components-headerbar-conversation-list.vala
index 319b2a4ef..78f24d351 100644
--- a/src/client/components/components-headerbar-conversation-list.vala
+++ b/src/client/components/components-headerbar-conversation-list.vala
@@ -19,8 +19,10 @@ public class Components.ConversationListHeaderBar : Hdy.HeaderBar {
public string account { get; set; }
public string folder { get; set; }
public bool search_open { get; set; default = false; }
+ public bool selection_open { get; set; default = false; }
[GtkChild] private unowned Gtk.ToggleButton search_button;
+ [GtkChild] private unowned Gtk.ToggleButton selection_button;
[GtkChild] public unowned Gtk.Button back_button;
@@ -33,5 +35,10 @@ public class Components.ConversationListHeaderBar : Hdy.HeaderBar {
this.search_button, "active",
SYNC_CREATE | BIDIRECTIONAL
);
+ this.bind_property(
+ "selection-open",
+ this.selection_button, "active",
+ SYNC_CREATE | BIDIRECTIONAL
+ );
}
}
diff --git a/src/client/components/count-badge.vala b/src/client/components/count-badge.vala
index a0a1963d3..b5a833b01 100644
--- a/src/client/components/count-badge.vala
+++ b/src/client/components/count-badge.vala
@@ -9,6 +9,7 @@
*/
public class CountBadge : Geary.BaseObject {
public const string UNREAD_BG_COLOR = "#888888";
+ public const int SPACING = 6;
private const int FONT_SIZE_MESSAGE_COUNT = 8;
@@ -63,7 +64,7 @@ public class CountBadge : Geary.BaseObject {
Pango.Rectangle? logical_rect;
layout_num.get_pixel_extents(out ink_rect, out logical_rect);
if (ctx != null) {
- double bg_width = logical_rect.width + FormattedConversationData.SPACING;
+ double bg_width = logical_rect.width + SPACING;
double bg_height = logical_rect.height;
double radius = bg_height / 2.0;
double degrees = Math.PI / 180.0;
@@ -87,7 +88,7 @@ public class CountBadge : Geary.BaseObject {
Pango.cairo_show_layout(ctx, layout_num);
}
- width = logical_rect.width + FormattedConversationData.SPACING;
+ width = logical_rect.width + SPACING;
height = logical_rect.height;
}
}
diff --git a/src/client/conversation-list/conversation-list-model.vala
b/src/client/conversation-list/conversation-list-model.vala
new file mode 100644
index 000000000..4f2850f71
--- /dev/null
+++ b/src/client/conversation-list/conversation-list-model.vala
@@ -0,0 +1,139 @@
+/*
+ * Copyright © 2022 John Renner <john jrenner net>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+// The whole goal of this class to wrap the ConversationMonitor with a view that presents a sorted list
+public class ConversationList.Model {
+ internal ListStore store = new ListStore(typeof(Geary.App.Conversation));
+ internal Geary.App.ConversationMonitor monitor { get; set; }
+
+ private bool scanning = false;
+
+ internal Model (Geary.App.ConversationMonitor monitor) {
+ this.monitor = monitor;
+ foreach (Geary.App.Conversation convo in monitor.read_only_view) {
+ insert(convo);
+ }
+
+ monitor.conversations_added.connect(on_conversations_added);
+ monitor.conversation_appended.connect(on_conversation_updated);
+ monitor.conversation_trimmed.connect(on_conversation_updated);
+ monitor.conversations_removed.connect(on_conversations_removed);
+ monitor.scan_started.connect(on_scan_started);
+ monitor.scan_completed.connect(on_scan_completed);
+ }
+
+ ~Model() {
+ this.monitor.conversations_added.disconnect(on_conversations_added);
+ this.monitor.conversation_appended.disconnect(on_conversation_updated);
+ this.monitor.conversation_trimmed.disconnect(on_conversation_updated);
+ this.monitor.conversations_removed.disconnect(on_conversations_removed);
+ this.monitor.scan_started.disconnect(on_scan_started);
+ this.monitor.scan_completed.disconnect(on_scan_completed);
+ }
+
+ public signal void conversations_added(bool start);
+ public signal void conversations_removed(bool start);
+ public signal void conversations_loaded();
+
+ private static int compare(Object a, Object b) {
+ return Util.Email.compare_conversation_descending(a as Geary.App.Conversation, b as
Geary.App.Conversation);
+ }
+
+ private void insert(Geary.App.Conversation convo) {
+ store.insert_sorted(convo, compare);
+ }
+
+ // ------------------------
+ // Scanning and load_more
+ // ------------------------
+
+
+ private void on_scan_started(Geary.App.ConversationMonitor source) {
+ this.scanning = true;
+ }
+ private void on_scan_completed(Geary.App.ConversationMonitor source) {
+ this.scanning = false;
+ conversations_loaded();
+ }
+
+ public bool load_more(int amount) {
+ if (this.scanning) {
+ return false;
+ }
+
+ this.monitor.min_window_count += amount;
+ return true;
+ }
+
+
+ // Monitor Lifecycle handles
+ private void on_conversations_added(Gee.Collection<Geary.App.Conversation> conversations) {
+ debug("Adding %d conversations.", conversations.size);
+ if (!this.scanning) {
+ conversations_added(true);
+ }
+ int added = 0;
+ foreach (Geary.App.Conversation conversation in conversations) {
+ if (upsert_conversation(conversation)) {
+ added++;
+ }
+ }
+ if (!this.scanning) {
+ conversations_added(false);
+ }
+ debug("Added %d/%d conversations.", added, conversations.size);
+ }
+
+ private void on_conversations_removed(Gee.Collection<Geary.App.Conversation> conversations) {
+ debug("Removing %d conversations.", conversations.size);
+
+ if (!this.scanning) {
+ conversations_removed(true);
+ }
+ int removed = 0;
+ foreach (Geary.App.Conversation conversation in conversations) {
+ if (remove_conversation(conversation)) {
+ removed++;
+ }
+ }
+ if (!this.scanning) {
+ conversations_removed(false);
+ }
+ debug("Removed %d/%d conversations.", removed, conversations.size);
+ }
+
+ private void on_conversation_updated(Geary.App.ConversationMonitor sender, Geary.App.Conversation convo,
Gee.Collection<Geary.Email> emails) {
+ upsert_conversation(convo);
+ }
+
+ // Monitor helpers
+ private bool upsert_conversation(Geary.App.Conversation convo) {
+ // The conversation may be bogus, if so don't do anything
+ Geary.Email? last_email = convo.get_latest_recv_email(Geary.App.Conversation.Location.ANYWHERE);
+
+ if (last_email == null) {
+ debug("Cannot add conversation: last email is null");
+ return false;
+ }
+
+ remove_conversation(convo);
+ insert(convo);
+
+ return true;
+ }
+
+ private bool remove_conversation(Geary.App.Conversation conversation) {
+ uint index;
+ if (store.find(conversation, out index)) {
+ store.remove(index);
+ return true;
+ }
+
+ return false;
+ }
+
+}
diff --git a/src/client/conversation-list/conversation-list-participant.vala
b/src/client/conversation-list/conversation-list-participant.vala
new file mode 100644
index 000000000..9219d09c9
--- /dev/null
+++ b/src/client/conversation-list/conversation-list-participant.vala
@@ -0,0 +1,68 @@
+/*
+ * Copyright © 2022 John Renner <john jrenner net>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+internal class ConversationList.Participant : Geary.BaseObject, Gee.Hashable<Participant> {
+ private const string ME = "Me";
+ public Geary.RFC822.MailboxAddress address;
+
+ public Participant(Geary.RFC822.MailboxAddress address) {
+ this.address = address;
+ }
+
+ public string get_full_markup(Gee.List<Geary.RFC822.MailboxAddress> account_mailboxes) {
+ return get_as_markup((address in account_mailboxes) ? ME : address.to_short_display());
+ }
+
+ public string get_short_markup(Gee.List<Geary.RFC822.MailboxAddress> account_mailboxes) {
+ if (address in account_mailboxes)
+ return get_as_markup(ME);
+
+ if (address.is_spoofed()) {
+ return get_full_markup(account_mailboxes);
+ }
+
+ string short_address = Markup.escape_text(address.to_short_display());
+
+ if (", " in short_address) {
+ // assume address is in Last, First format
+ string[] tokens = short_address.split(", ", 2);
+ short_address = tokens[1].strip();
+ if (Geary.String.is_empty(short_address))
+ return get_full_markup(account_mailboxes);
+ }
+
+ // use first name as delimited by a space
+ string[] tokens = short_address.split(" ", 2);
+ if (tokens.length < 1)
+ return get_full_markup(account_mailboxes);
+
+ string first_name = tokens[0].strip();
+ if (Geary.String.is_empty_or_whitespace(first_name))
+ return get_full_markup(account_mailboxes);
+
+ return get_as_markup(first_name);
+ }
+
+ private string get_as_markup(string participant) {
+ string markup = Geary.HTML.escape_markup(participant);
+
+ if (this.address.is_spoofed()) {
+ markup = "<s>%s</s>".printf(markup);
+ }
+
+ return markup;
+ }
+
+ public bool equal_to(Participant other) {
+ return address.equal_to(other.address)
+ && address.name == other.address.name;
+ }
+
+ public uint hash() {
+ return address.hash();
+ }
+}
diff --git a/src/client/conversation-list/conversation-list-row.vala
b/src/client/conversation-list/conversation-list-row.vala
new file mode 100644
index 000000000..9e24cbdd3
--- /dev/null
+++ b/src/client/conversation-list/conversation-list-row.vala
@@ -0,0 +1,211 @@
+/*
+ * Copyright © 2022 John Renner <john jrenner net>
+ * Copyright © 2022 Cédric Bellegarde <cedric bellegarde adishatz org>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+
+/**
+ * A conversation list row displaying an email summary
+ */
+[GtkTemplate (ui = "/org/gnome/Geary/conversation-list-row.ui")]
+internal class ConversationList.Row : Gtk.ListBoxRow {
+
+ private Gee.List<Geary.RFC822.MailboxAddress>? user_accounts {
+ owned get {
+ return conversation.base_folder.account.information.sender_mailboxes;
+ }
+ }
+
+ [GtkChild] unowned Gtk.Label preview;
+ [GtkChild] unowned Gtk.Box preview_row;
+ [GtkChild] unowned Gtk.Label subject;
+ [GtkChild] unowned Gtk.Label participants;
+ [GtkChild] unowned Gtk.Label date;
+ [GtkChild] unowned Gtk.Label count_badge;
+
+ [GtkChild] unowned Gtk.Image read_icon;
+ [GtkChild] unowned Gtk.Image flagged_icon;
+
+ [GtkChild] unowned Gtk.Stack stack;
+ [GtkChild] unowned Gtk.CheckButton selected_button;
+
+ internal Geary.App.Conversation conversation;
+ private Application.Configuration config;
+ private DateTime? recv_time;
+
+ internal signal void toggle_flag(ConversationList.Row row,
+ Geary.NamedFlag flag);
+
+ internal Row(Application.Configuration config,
+ Geary.App.Conversation conversation,
+ bool selection_mode_enabled) {
+ this.config = config;
+ this.conversation = conversation;
+
+ Geary.Email? last_email = conversation.get_latest_recv_email(
+ Geary.App.Conversation.Location.ANYWHERE
+ );
+ if (last_email != null) {
+ var text = Util.Email.strip_subject_prefixes(last_email);
+ this.subject.set_text(text);
+ this.preview.set_text(last_email.get_preview_as_string());
+ this.recv_time = last_email.properties.date_received.to_local();
+ refresh_time();
+ }
+
+ this.participants.set_markup(get_participants());
+
+ var count = conversation.get_count();
+ if (count > 1) {
+ this.count_badge.set_text(conversation.get_count().to_string());
+ } else {
+ this.count_badge.hide();
+ }
+
+ conversation.email_flags_changed.connect(update_flags);
+ update_flags(null);
+
+ config.bind(Application.Configuration.DISPLAY_PREVIEW_KEY,
+ this.preview_row, "visible");
+
+ if (selection_mode_enabled) {
+ set_selection_enabled(true);
+ }
+ }
+
+ internal void set_selection_enabled(bool enabled) {
+ if (enabled) {
+ this.selected_button.show();
+ set_button_active(this.is_selected());
+ this.state_flags_changed.connect(update_button);
+ this.selected_button.toggled.connect(update_state_flags);
+ this.stack.set_visible_child_name("selection-button");
+ } else {
+ this.stack.set_visible_child_name("buttons");
+ this.state_flags_changed.disconnect(update_button);
+ this.selected_button.toggled.disconnect(update_state_flags);
+ set_button_active(false);
+ this.selected_button.hide();
+ }
+ }
+
+ internal void refresh_time() {
+ if (this.recv_time != null) {
+ // conversation list store sorts by date-received, so display that
+ // instead of the sent time
+ this.date.set_text(Util.Date.pretty_print(
+ this.recv_time,
+ this.config.clock_format
+ ));
+ }
+ }
+
+ private void set_button_active(bool active) {
+ this.selected_button.set_active(active);
+ if (active) {
+ this.get_style_context().add_class("selected");
+ this.set_state_flags(Gtk.StateFlags.SELECTED, false);
+ } else {
+ this.get_style_context().remove_class("selected");
+ this.unset_state_flags(Gtk.StateFlags.SELECTED);
+ }
+ }
+ private void update_button() {
+ bool is_selected = (
+ this.get_state_flags() & Gtk.StateFlags.SELECTED
+ ) == Gtk.StateFlags.SELECTED;
+
+ this.selected_button.toggled.disconnect(update_state_flags);
+ set_button_active(is_selected);
+ this.selected_button.toggled.connect(update_state_flags);
+
+ }
+
+ private void update_state_flags() {
+ this.state_flags_changed.disconnect(update_button);
+
+ if (this.selected_button.get_active()) {
+ this.set_state_flags(Gtk.StateFlags.SELECTED, false);
+ this.get_style_context().add_class("selected");
+ } else {
+ this.unset_state_flags(Gtk.StateFlags.SELECTED);
+ this.get_style_context().remove_class("selected");
+ }
+
+ this.state_flags_changed.connect(update_button);
+ }
+
+ private void update_flags(Geary.Email? email) {
+ if (conversation.is_unread()) {
+ get_style_context().add_class("unread");
+ read_icon.set_from_icon_name("mail-unread-symbolic", Gtk.IconSize.BUTTON);
+ } else {
+ get_style_context().remove_class("unread");
+ read_icon.set_from_icon_name("mail-read-symbolic", Gtk.IconSize.BUTTON);
+ }
+
+ if (conversation.is_flagged()) {
+ flagged_icon.set_from_icon_name("starred-symbolic", Gtk.IconSize.BUTTON);
+ } else {
+ flagged_icon.set_from_icon_name("non-starred-symbolic", Gtk.IconSize.BUTTON);
+ }
+ }
+
+ [GtkCallback] private void on_unread_button_clicked() {
+ toggle_flag(this, Geary.EmailFlags.UNREAD);
+ }
+
+ [GtkCallback] private void on_flagged_button_clicked() {
+ toggle_flag(this, Geary.EmailFlags.FLAGGED);
+ }
+
+ private string get_participants() {
+ var participants = new Gee.ArrayList<Participant>();
+ Gee.List<Geary.Email> emails = conversation.get_emails(
+ Geary.App.Conversation.Ordering.RECV_DATE_ASCENDING);
+
+ foreach (Geary.Email message in emails) {
+ Geary.RFC822.MailboxAddresses? addresses =
+ conversation.base_folder.used_as.is_outgoing()
+ ? new Geary.RFC822.MailboxAddresses.single(Util.Email.get_primary_originator(message))
+ : message.from;
+
+ if (addresses == null) {
+ continue;
+ }
+
+ foreach (Geary.RFC822.MailboxAddress address in addresses) {
+ Participant participant_display = new Participant(address);
+ int existing_index = participants.index_of(participant_display);
+ if (existing_index < 0) {
+ participants.add(participant_display);
+ continue;
+ }
+ }
+ }
+
+ if (participants.size == 0) {
+ return "";
+ }
+
+ if(participants.size == 1) {
+ return participants[0].get_full_markup(this.user_accounts);
+ }
+
+ StringBuilder builder = new StringBuilder();
+ bool first = true;
+ foreach (Participant participant in participants) {
+ if (!first) {
+ builder.append(", ");
+ }
+
+ builder.append(participant.get_short_markup(this.user_accounts));
+ first = false;
+ }
+
+ return builder.str;
+ }
+}
diff --git a/src/client/conversation-list/conversation-list-view.vala
b/src/client/conversation-list/conversation-list-view.vala
index 37a2a6401..1a953fe71 100644
--- a/src/client/conversation-list/conversation-list-view.vala
+++ b/src/client/conversation-list/conversation-list-view.vala
@@ -1,666 +1,639 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
+/*
+ * Copyright © 2022 John Renner <john jrenner net>
+ * Copyright © 2022 Cédric Bellegarde <cedric bellegarde adishatz org>
*
* This software is licensed under the GNU Lesser General Public License
- * (version 2.1 or later). See the COPYING file in this distribution.
+ * (version 2.1 or later). See the COPYING file in this distribution.
*/
-public class ConversationListView : Gtk.TreeView, Geary.BaseInterface {
- const int LOAD_MORE_HEIGHT = 100;
-
+/**
+ * Represents in folder conversations list.
+ *
+ */
+[GtkTemplate (ui = "/org/gnome/Geary/conversation-list-view.ui")]
+public class ConversationList.View : Gtk.ScrolledWindow, Geary.BaseInterface {
+ /**
+ * The fields that must be available on any ConversationMonitor
+ * passed to ConversationList.View
+ */
+ public const Geary.Email.Field REQUIRED_FIELDS = (
+ Geary.Email.Field.ENVELOPE |
+ Geary.Email.Field.FLAGS |
+ Geary.Email.Field.PROPERTIES
+ );
+
+ public bool selection_mode_enabled { get; set; default = false; }
private Application.Configuration config;
+ private Gtk.GestureMultiPress press_gesture;
+ private Gtk.GestureLongPress long_press_gesture;
+ private Gtk.EventControllerKey key_event_controller;
- private bool enable_load_more = true;
-
- private bool reset_adjustment = false;
- private Gee.Set<Geary.App.Conversation>? current_visible_conversations = null;
- private Geary.Scheduler.Scheduled? scheduled_update_visible_conversations = null;
- private Gee.Set<Geary.App.Conversation> selected = new Gee.HashSet<Geary.App.Conversation>();
- private Geary.IdleManager selection_update;
- private Gtk.GestureMultiPress gesture;
-
- // Determines if the next folder scan should avoid selecting a
- // conversation when autoselect is enabled
- private bool should_inhibit_autoselect = false;
-
-
- public signal void conversations_selected(Gee.Set<Geary.App.Conversation> selected);
-
- // Signal for when a conversation has been double-clicked, or selected and enter is pressed.
- public signal void conversation_activated(Geary.App.Conversation activated, bool single = false);
-
- public virtual signal void load_more() {
- enable_load_more = false;
- }
-
- public signal void mark_conversations(Gee.Collection<Geary.App.Conversation> conversations,
- Geary.NamedFlag flag);
-
- public signal void visible_conversations_changed(Gee.Set<Geary.App.Conversation> visible);
+ [GtkChild] private unowned Gtk.ListBox list;
+ /*
+ * Use to restore selected row when exiting selection/edition
+ */
+ private Gtk.ListBoxRow? to_restore_row = null;
- public ConversationListView(Application.Configuration config) {
- base_ref();
- set_show_expanders(false);
- set_headers_visible(false);
- set_grid_lines(Gtk.TreeViewGridLines.HORIZONTAL);
-
+ public View(Application.Configuration config) {
this.config = config;
- append_column(create_column(ConversationListStore.Column.CONVERSATION_DATA,
- new ConversationListCellRenderer(), ConversationListStore.Column.CONVERSATION_DATA.to_string(),
- 0));
-
- Gtk.TreeSelection selection = get_selection();
- selection.set_mode(Gtk.SelectionMode.MULTIPLE);
- style_updated.connect(on_style_changed);
-
- notify["vadjustment"].connect(on_vadjustment_changed);
-
- key_press_event.connect(on_key_press);
- button_press_event.connect(on_button_press);
- gesture = new Gtk.GestureMultiPress(this);
- gesture.pressed.connect(on_gesture_pressed);
-
- // Set up drag and drop.
- Gtk.drag_source_set(this, Gdk.ModifierType.BUTTON1_MASK, FolderList.Tree.TARGET_ENTRY_LIST,
- Gdk.DragAction.COPY | Gdk.DragAction.MOVE);
-
- this.config.settings.changed[
- Application.Configuration.DISPLAY_PREVIEW_KEY
- ].connect(on_display_preview_changed);
-
- // Watch for mouse events.
- motion_notify_event.connect(on_motion_notify_event);
- leave_notify_event.connect(on_leave_notify_event);
+ this.notify["selection-mode-enabled"].connect(on_selection_mode_changed);
- // GtkTreeView binds Ctrl+N to "move cursor to next". Not so interested in that, so we'll
- // remove it.
- unowned Gtk.BindingSet? binding_set = Gtk.BindingSet.find("GtkTreeView");
- assert(binding_set != null);
- Gtk.BindingEntry.remove(binding_set, Gdk.Key.N, Gdk.ModifierType.CONTROL_MASK);
+ this.list.selected_rows_changed.connect(on_selected_rows_changed);
+ this.list.row_activated.connect(on_row_activated);
- this.selection_update = new Geary.IdleManager(do_selection_changed);
- this.selection_update.priority = Geary.IdleManager.Priority.LOW;
+ this.list.set_header_func(header_func);
- this.visible = true;
- }
-
- ~ConversationListView() {
- base_unref();
- }
-
- public override void destroy() {
- this.selection_update.reset();
- base.destroy();
- }
-
- public new ConversationListStore? get_model() {
- return base.get_model() as ConversationListStore;
- }
-
- public new void set_model(ConversationListStore? new_store) {
- ConversationListStore? old_store = get_model();
- if (old_store != null) {
- old_store.conversations.scan_started.disconnect(on_scan_started);
- old_store.conversations.scan_completed.disconnect(on_scan_completed);
-
- old_store.conversations_added.disconnect(on_conversations_added);
- old_store.conversations_removed.disconnect(on_conversations_removed);
- old_store.row_inserted.disconnect(on_rows_changed);
- old_store.rows_reordered.disconnect(on_rows_changed);
- old_store.row_changed.disconnect(on_rows_changed);
- old_store.row_deleted.disconnect(on_rows_changed);
- old_store.destroy();
- }
-
- if (new_store != null) {
- new_store.conversations.scan_started.connect(on_scan_started);
- new_store.conversations.scan_completed.connect(on_scan_completed);
-
- new_store.row_inserted.connect(on_rows_changed);
- new_store.rows_reordered.connect(on_rows_changed);
- new_store.row_changed.connect(on_rows_changed);
- new_store.row_deleted.connect(on_rows_changed);
- new_store.conversations_removed.connect(on_conversations_removed);
- new_store.conversations_added.connect(on_conversations_added);
- }
-
- // Disconnect the selection handler since we don't want to
- // fire selection signals while changing the model.
- Gtk.TreeSelection selection = get_selection();
- selection.changed.disconnect(on_selection_changed);
- base.set_model(new_store);
- this.selected.clear();
- selection.changed.connect(on_selection_changed);
- }
+ this.vadjustment.value_changed.connect(maybe_load_more);
+ this.vadjustment.value_changed.connect(update_visible_conversations);
- /** Returns a read-only iteration of the current selection. */
- public Gee.Set<Geary.App.Conversation> get_selected() {
- return this.selected.read_only_view;
- }
+ this.press_gesture = new Gtk.GestureMultiPress(this.list);
+ this.press_gesture.set_button(0);
+ this.press_gesture.released.connect(on_press_gesture_released);
- /** Returns a copy of the current selection. */
- public Gee.Set<Geary.App.Conversation> copy_selected() {
- var copy = new Gee.HashSet<Geary.App.Conversation>();
- copy.add_all(this.selected);
- return copy;
- }
-
- public void inhibit_next_autoselect() {
- this.should_inhibit_autoselect = true;
- }
-
- public void scroll(Gtk.ScrollType where) {
- Gtk.TreeSelection selection = get_selection();
- weak Gtk.TreeModel model;
- GLib.List<Gtk.TreePath> selected = selection.get_selected_rows(out model);
- Gtk.TreePath? target_path = null;
- Gtk.TreeIter? target_iter = null;
- if (selected.length() > 0) {
- switch (where) {
- case STEP_UP:
- target_path = selected.first().data;
- model.get_iter(out target_iter, target_path);
- if (model.iter_previous(ref target_iter)) {
- target_path = model.get_path(target_iter);
- } else {
- this.get_window().beep();
- }
- break;
-
- case STEP_DOWN:
- target_path = selected.last().data;
- model.get_iter(out target_iter, target_path);
- if (model.iter_next(ref target_iter)) {
- target_path = model.get_path(target_iter);
- } else {
- this.get_window().beep();
- }
- break;
-
- default:
- // no-op
- break;
+ this.long_press_gesture = new Gtk.GestureLongPress(this.list);
+ this.long_press_gesture.propagation_phase = CAPTURE;
+ this.long_press_gesture.pressed.connect((n_press, x, y) => {
+ Row? row = (Row) this.list.get_row_at_y((int) y);
+ if (row != null) {
+ context_menu(row);
}
+ });
- set_cursor(target_path, null, false);
- }
- }
-
- private void check_load_more() {
- ConversationListStore? model = get_model();
- Geary.App.ConversationMonitor? conversations = (model != null)
- ? model.conversations
- : null;
- if (conversations != null) {
- // Check if we're at the very bottom of the list. If we
- // are, it's time to issue a load_more signal.
- Gtk.Adjustment adjustment = ((Gtk.Scrollable) this).get_vadjustment();
- double upper = adjustment.get_upper();
- double threshold = upper - adjustment.page_size - LOAD_MORE_HEIGHT;
- if (this.is_visible() &&
- conversations.can_load_more &&
- adjustment.get_value() >= threshold) {
- load_more();
- }
+ this.key_event_controller = new Gtk.EventControllerKey(this.list);
+ this.key_event_controller.key_pressed.connect(on_key_event_controller_key_pressed);
- schedule_visible_conversations_changed();
- }
+ Gtk.drag_source_set(this.list, Gdk.ModifierType.BUTTON1_MASK, FolderList.Tree.TARGET_ENTRY_LIST,
+ Gdk.DragAction.COPY | Gdk.DragAction.MOVE);
+ this.list.drag_begin.connect(on_drag_begin);
+ this.list.drag_end.connect(on_drag_end);
}
- private void on_scan_started() {
- this.enable_load_more = false;
+ static construct {
+ set_css_name("conversation-list");
}
- private void on_scan_completed() {
- this.enable_load_more = true;
- check_load_more();
-
- // Select the first conversation, if autoselect is enabled,
- // nothing has been selected yet and we're not showing a
- // composer.
- if (this.config.autoselect &&
- !this.should_inhibit_autoselect &&
- get_selection().count_selected_rows() == 0) {
- var parent = get_toplevel() as Application.MainWindow;
- if (parent != null && !parent.has_composer) {
- set_cursor(new Gtk.TreePath.from_indices(0, -1), null, false);
- }
+ // -------
+ // UI
+ // -------
+ private void header_func(Gtk.ListBoxRow row, Gtk.ListBoxRow? before) {
+ if (before != null) {
+ var sep = new Gtk.Separator(Gtk.Orientation.HORIZONTAL);
+ sep.show();
+ row.set_header(sep);
}
-
- this.should_inhibit_autoselect = false;
}
- private void on_conversations_added(bool start) {
- Gtk.Adjustment? adjustment = get_adjustment();
- if (start) {
- // If we were at the top, we want to stay there after
- // conversations are added.
- this.reset_adjustment = adjustment != null && adjustment.get_value() == 0;
- } else if (this.reset_adjustment && adjustment != null) {
- // Pump the loop to make sure the new conversations are
- // taking up space in the window. Without this, setting
- // the adjustment here is a no-op because as far as it's
- // concerned, it's already at the top.
- while (Gtk.events_pending())
- Gtk.main_iteration();
-
- adjustment.set_value(0);
+ /**
+ * Updates the display of the received time on each list row.
+ *
+ * Because the received time is displayed as relative to the current time,
+ * it must be periodically updated. ConversationList.View does not do this
+ * automatically but instead it must be externally scheduled
+ */
+ public void refresh_times() {
+ this.list.foreach((child) => {
+ var row = (Row) child;
+ row.refresh_time();
+ });
+ }
+
+ // -------------------
+ // Model Management
+ // -------------------
+
+ /**
+ * The currently bound model
+ */
+ private Model? model;
+
+ /**
+ * Set the conversation monitor which the listview is displaying
+ */
+ public void set_monitor(Geary.App.ConversationMonitor? monitor) {
+ if (this.model != null) {
+ this.model.conversations_loaded.disconnect(on_conversations_loaded);
+ this.model.conversations_removed.disconnect(on_conversations_removed);
}
- this.reset_adjustment = false;
- }
-
- private void on_conversations_removed(bool start) {
- if (!this.config.autoselect) {
- Gtk.SelectionMode mode = start
- // Stop GtkTreeView from automatically selecting the
- // next row after the removed rows
- ? Gtk.SelectionMode.NONE
- // Allow the user to make selections again
- : Gtk.SelectionMode.MULTIPLE;
- get_selection().set_mode(mode);
+ if (monitor == null) {
+ this.model = null;
+ this.list.bind_model(null, row_factory);
+ } else {
+ this.model = new Model(monitor);
+ this.list.bind_model(this.model.store, row_factory);
+ this.model.conversations_loaded.connect(on_conversations_loaded);
+ this.model.conversations_removed.connect(on_conversations_removed);
}
}
- private Gtk.Adjustment? get_adjustment() {
- Gtk.ScrolledWindow? parent = get_parent() as Gtk.ScrolledWindow;
- if (parent == null) {
- debug("Parent was not scrolled window");
- return null;
+ /**
+ * Attempt to load more conversations from the current monitor
+ */
+ public void load_more(int request) {
+ if (model != null) {
+ model.load_more(request);
}
-
- return parent.get_vadjustment();
}
- private void on_gesture_pressed(int n_press, double x, double y) {
- if (gesture.get_current_button() != Gdk.BUTTON_PRIMARY)
- return;
-
- Gtk.TreePath? path;
- get_path_at_pos((int) x, (int) y, out path, null, null, null);
+ public void scroll(Gtk.ScrollType scroll_type) {
+ Gtk.ListBoxRow row = this.list.get_selected_row();
- // If the user clicked in an empty area, do nothing.
- if (path == null)
+ if (row == null) {
return;
+ }
- Geary.App.Conversation? c = get_model().get_conversation_at_path(path);
- if (c == null)
- return;
-
- Gdk.Event event = gesture.get_last_event(gesture.get_current_sequence());
- Gdk.ModifierType modifiers = Gtk.accelerator_get_default_mod_mask();
-
- Gdk.ModifierType state_mask;
- event.get_state(out state_mask);
+ int index = row.get_index();
+ if (scroll_type == Gtk.ScrollType.STEP_UP) {
+ row = this.list.get_row_at_index(index + 1);
+ } else {
+ row = this.list.get_row_at_index(index - 1);
+ }
- if ((state_mask & modifiers) == 0 && n_press == 1) {
- conversation_activated(c, true);
- } else if ((state_mask & modifiers) == Gdk.ModifierType.SHIFT_MASK && n_press == 2) {
- conversation_activated(c);
+ if (row != null) {
+ this.list.select_row(row);
}
}
- private bool on_key_press(Gdk.EventKey event) {
- if (this.selected.size != 1)
- return false;
-
- Geary.App.Conversation? c = this.selected.to_array()[0];
- if (c == null)
- return false;
-
- Gdk.ModifierType modifiers = Gtk.accelerator_get_default_mod_mask();
-
- if (event.keyval == Gdk.Key.Return ||
- event.keyval == Gdk.Key.ISO_Enter ||
- event.keyval == Gdk.Key.KP_Enter ||
- event.keyval == Gdk.Key.space ||
- event.keyval == Gdk.Key.KP_Space)
- conversation_activated(c, !((event.state & modifiers) == Gdk.ModifierType.SHIFT_MASK));
- return false;
+ private Gtk.Widget row_factory(Object convo_obj) {
+ var convo = (Geary.App.Conversation) convo_obj;
+ var row = new Row(config, convo, this.selection_mode_enabled);
+ row.toggle_flag.connect(on_toggle_flags);
+ return row;
}
- private bool on_button_press(Gdk.EventButton event) {
- // Get the coordinates on the cell as well as the clicked path.
- int cell_x;
- int cell_y;
- Gtk.TreePath? path;
- get_path_at_pos((int) event.x, (int) event.y, out path, null, out cell_x, out cell_y);
-
- // If the user clicked in an empty area, do nothing.
- if (path == null)
- return false;
-
- // Handle clicks to toggle read and starred status.
- if ((event.state & Gdk.ModifierType.SHIFT_MASK) == 0 &&
- (event.state & Gdk.ModifierType.CONTROL_MASK) == 0 &&
- event.type == Gdk.EventType.BUTTON_PRESS) {
-
- // Click positions depend on whether the preview is enabled.
- bool read_clicked = false;
- bool star_clicked = false;
- if (this.config.display_preview) {
- read_clicked = cell_x < 25 && cell_y >= 14 && cell_y <= 30;
- star_clicked = cell_x < 25 && cell_y >= 40 && cell_y <= 62;
- } else {
- read_clicked = cell_x < 25 && cell_y >= 8 && cell_y <= 22;
- star_clicked = cell_x < 25 && cell_y >= 28 && cell_y <= 43;
- }
- // Get the current conversation. If it's selected, we'll apply the mark operation to
- // all selected conversations; otherwise, it just applies to this one.
- Geary.App.Conversation conversation = get_model().get_conversation_at_path(path);
- Gee.Collection<Geary.App.Conversation> to_mark = (
- this.selected.contains(conversation)
- ? copy_selected()
- : Geary.Collection.single(conversation)
- );
-
- if (read_clicked) {
- mark_conversations(to_mark, Geary.EmailFlags.UNREAD);
- return true;
- } else if (star_clicked) {
- mark_conversations(to_mark, Geary.EmailFlags.FLAGGED);
- return true;
- }
+ // --------------------
+ // Right-click Popup
+ // --------------------
+ private void context_menu(Row row, Gdk.Rectangle? rect=null) {
+ if (!row.is_selected()) {
+ this.list.unselect_all();
+ this.list.select_row(row);
}
- // Check if changing the selection will require any composers
- // to be closed, but only on the first click of a
- // double/triple click, so that double-clicking a draft
- // doesn't attempt to load it then close it straight away.
- if (event.type == Gdk.EventType.BUTTON_PRESS &&
- !get_selection().path_is_selected(path)) {
- var parent = get_toplevel() as Application.MainWindow;
- if (parent != null && !parent.close_composer(false)) {
- return true;
- }
+ var popup_menu = construct_popover(row, this.list.get_selected_rows().length());
+ if (rect != null) {
+ popup_menu.set_pointing_to(rect);
}
+ popup_menu.popup();
+ }
- if (event.button == 3 && event.type == Gdk.EventType.BUTTON_PRESS) {
- Geary.App.Conversation conversation = get_model().get_conversation_at_path(path);
-
- GLib.Menu context_menu_model = new GLib.Menu();
- var main = get_toplevel() as Application.MainWindow;
- if (main != null) {
- if (!main.is_shift_down) {
- context_menu_model.append(
- /// Translators: Context menu item
- ngettext(
- "Move conversation to _Trash",
- "Move conversations to _Trash",
- this.selected.size
- ),
- Action.Window.prefix(
- Application.MainWindow.ACTION_TRASH_CONVERSATION
- )
- );
- } else {
- context_menu_model.append(
- /// Translators: Context menu item
- ngettext(
- "_Delete conversation",
- "_Delete conversations",
- this.selected.size
- ),
- Action.Window.prefix(
- Application.MainWindow.ACTION_DELETE_CONVERSATION
- )
- );
- }
- }
-
- if (conversation.is_unread())
- context_menu_model.append(
- _("Mark as _Read"),
- Action.Window.prefix(
- Application.MainWindow.ACTION_MARK_AS_READ
- )
- );
-
- if (conversation.has_any_read_message())
- context_menu_model.append(
- _("Mark as _Unread"),
- Action.Window.prefix(
- Application.MainWindow.ACTION_MARK_AS_UNREAD
- )
- );
+ private Gtk.Popover construct_popover(Row row, uint selection_size) {
+ GLib.Menu context_menu_model = new GLib.Menu();
+ var main = get_toplevel() as Application.MainWindow;
- if (conversation.is_flagged()) {
+ if (main != null) {
+ if (!main.is_shift_down) {
context_menu_model.append(
- _("U_nstar"),
+ /// Translators: Context menu item
+ ngettext(
+ "Move conversation to _Trash",
+ "Move conversations to _Trash",
+ selection_size
+ ),
Action.Window.prefix(
- Application.MainWindow.ACTION_MARK_AS_UNSTARRED
+ Application.MainWindow.ACTION_TRASH_CONVERSATION
)
);
} else {
context_menu_model.append(
- _("_Star"),
- Action.Window.prefix(
- Application.MainWindow.ACTION_MARK_AS_STARRED
- )
- );
- }
- if ((conversation.base_folder.used_as != ARCHIVE) && (conversation.base_folder.used_as !=
ALL_MAIL)) {
- context_menu_model.append(
- _("Archive conversation"),
+ /// Translators: Context menu item
+ ngettext(
+ "_Delete conversation",
+ "_Delete conversations",
+ selection_size
+ ),
Action.Window.prefix(
- Application.MainWindow.ACTION_ARCHIVE_CONVERSATION
+ Application.MainWindow.ACTION_DELETE_CONVERSATION
)
);
}
+ }
- Menu actions_section = new Menu();
- actions_section.append(
- _("_Reply"),
+ if (row.conversation.is_unread()) {
+ context_menu_model.append(
+ _("Mark as _Read"),
Action.Window.prefix(
- Application.MainWindow.ACTION_REPLY_CONVERSATION
+ Application.MainWindow.ACTION_MARK_AS_READ
)
);
- actions_section.append(
- _("R_eply All"),
+ }
+
+ if (row.conversation.has_any_read_message()) {
+ context_menu_model.append(
+ _("Mark as _Unread"),
Action.Window.prefix(
- Application.MainWindow.ACTION_REPLY_ALL_CONVERSATION
+ Application.MainWindow.ACTION_MARK_AS_UNREAD
)
);
- actions_section.append(
- _("_Forward"),
+ }
+
+ if (row.conversation.is_flagged()) {
+ context_menu_model.append(
+ _("U_nstar"),
Action.Window.prefix(
- Application.MainWindow.ACTION_FORWARD_CONVERSATION
+ Application.MainWindow.ACTION_MARK_AS_UNSTARRED
)
);
- context_menu_model.append_section(null, actions_section);
-
- // Use a popover rather than a regular context menu since
- // the latter grabs the event queue, so the MainWindow
- // will not receive events if the user releases Shift,
- // making the trash/delete header bar state wrong.
- Gtk.Popover context_menu = new Gtk.Popover.from_model(
- this, context_menu_model
+ } else {
+ context_menu_model.append(
+ _("_Star"),
+ Action.Window.prefix(
+ Application.MainWindow.ACTION_MARK_AS_STARRED
+ )
);
- Gdk.Rectangle dest = Gdk.Rectangle();
- dest.x = (int) event.x;
- dest.y = (int) event.y;
- context_menu.set_pointing_to(dest);
- context_menu.popup();
-
- // When the conversation under the mouse is selected, stop event propagation
- return get_selection().path_is_selected(path);
}
- return false;
- }
+ if ((row.conversation.base_folder.used_as != ARCHIVE) &&
+ (row.conversation.base_folder.used_as != ALL_MAIL)) {
+ context_menu_model.append(
+ ngettext(
+ "_Archive conversation",
+ "_Archive conversations",
+ selection_size
+ ),
+ Action.Window.prefix(
+ Application.MainWindow.ACTION_ARCHIVE_CONVERSATION
+ )
+ );
+ }
- private void on_style_changed() {
- // Recalculate dimensions of child cells.
- ConversationListCellRenderer.style_changed(this);
+ Menu actions_section = new Menu();
+ actions_section.append(
+ _("_Reply"),
+ Action.Window.prefix(
+ Application.MainWindow.ACTION_REPLY_CONVERSATION
+ )
+ );
+ actions_section.append(
+ _("R_eply All"),
+ Action.Window.prefix(
+ Application.MainWindow.ACTION_REPLY_ALL_CONVERSATION
+ )
+ );
+ actions_section.append(
+ _("_Forward"),
+ Action.Window.prefix(
+ Application.MainWindow.ACTION_FORWARD_CONVERSATION
+ )
+ );
+ context_menu_model.append_section(null, actions_section);
+
+ // Use a popover rather than a regular context menu since
+ // the latter grabs the event queue, so the MainWindow
+ // will not receive events if the user releases Shift,
+ // making the trash/delete header bar state wrong.
+ Gtk.Popover context_menu = new Gtk.Popover.from_model(
+ row, context_menu_model
+ );
+
+ return context_menu;
+ }
+
+ // -------------------
+ // Selection
+ // -------------------
+
+ /**
+ * Emitted when one or more conversations are selected
+ */
+ public signal void conversations_selected(Gee.Set<Geary.App.Conversation> selected, bool
is_interactive=true);
+
+ /**
+ * Emitted when one or more conversations are activated
+ */
+ public signal void conversation_alt_action(Geary.App.Conversation activated);
+
+ /**
+ * Gets the conversations represented by the current selection in the ListBox
+ */
+ public Gee.Set<Geary.App.Conversation> get_selected() {
+ var selected = new Gee.HashSet<Geary.App.Conversation>();
- schedule_visible_conversations_changed();
+ foreach (var row in this.list.get_selected_rows()) {
+ selected.add(((Row) row).conversation);
+ }
+ return selected;
+ }
+
+ /**
+ * Selects the rows for a given collection of conversations
+ *
+ * If a conversation is not present in the ListBox, it is ignored.
+ */
+ public void select_conversations(Gee.Collection<Geary.App.Conversation> selection) {
+ this.list.foreach((child) => {
+ var row = (Row) child;
+ Geary.App.Conversation conversation = row.conversation;
+ if (selection.contains(conversation)) {
+ this.list.select_row(row);
+ }
+ });
}
- private void on_value_changed() {
- if (this.enable_load_more) {
- check_load_more();
- }
+ /**
+ * Unselects all conversations
+ */
+ public void unselect_all() {
+ this.list.unselect_all();
}
- private static Gtk.TreeViewColumn create_column(ConversationListStore.Column column,
- Gtk.CellRenderer renderer, string attr, int width = 0) {
- Gtk.TreeViewColumn view_column = new Gtk.TreeViewColumn.with_attributes(column.to_string(),
- renderer, attr, column);
- view_column.set_resizable(true);
+ // -----------------
+ // Button Actions
+ // ----------------
- if (width != 0) {
- view_column.set_sizing(Gtk.TreeViewColumnSizing.FIXED);
- view_column.set_fixed_width(width);
+ /**
+ * Emitted when the user expresses intent to update the flags on a set of conversations
+ */
+ public signal void mark_conversations(Gee.Collection<Geary.App.Conversation> conversations,
+ Geary.NamedFlag flag);
+
+
+ private void on_toggle_flags(ConversationList.Row row, Geary.NamedFlag flag) {
+ if (row.is_selected()) {
+ mark_conversations(get_selected(), flag);
+ } else {
+ mark_conversations(Geary.Collection.single(row.conversation), flag);
}
+ }
+
+ // ----------------
+ // Visibility
+ // ---------------
- return view_column;
+ /**
+ * If the number of pixels between the bottom of the viewport and the bottom of
+ * of the listbox is less than LOAD_MORE_THRESHOLD, request more from the
+ * monitor.
+ */
+ private double LOAD_MORE_THRESHOLD = 100;
+ private int LOAD_MORE_COUNT = 50;
+
+ /**
+ * Called on scroll to possibly load more conversations from the model
+ */
+ private void maybe_load_more(Gtk.Adjustment adjustment) {
+ double upper = adjustment.get_upper();
+ double threshold = upper - adjustment.page_size - LOAD_MORE_THRESHOLD;
+
+ if (this.is_visible() && adjustment.get_value() >= threshold) {
+ this.load_more(LOAD_MORE_COUNT);
+ }
}
- private List<Gtk.TreePath> get_all_selected_paths() {
- Gtk.TreeModel model;
- return get_selection().get_selected_rows(out model);
+ /**
+ * Time in milliseconds to delay updating the set of visible conversations.
+ * If another update is triggered during this delay, it will be discarded
+ * and the delay begins again.
+ */
+ private int VISIBILITY_UPDATE_DELAY_MS = 1000;
+
+ /**
+ * The set of all conversations currently displayed in the viewport
+ */
+ public Gee.Set<Geary.App.Conversation> visible_conversations {get; private set; default = new
Gee.HashSet<Geary.App.Conversation>(); }
+ private Geary.Scheduler.Scheduled? scheduled_visible_update;
+
+ /**
+ * Called on scroll to update the set of visible conversations
+ */
+ private void update_visible_conversations() {
+ if(scheduled_visible_update != null) {
+ scheduled_visible_update.cancel();
+ }
+
+ scheduled_visible_update = Geary.Scheduler.after_msec(VISIBILITY_UPDATE_DELAY_MS, () => {
+ var visible = new Gee.HashSet<Geary.App.Conversation>();
+ Gtk.ListBoxRow? first = this.list.get_row_at_y((int) this.vadjustment.value);
+
+ if (first == null) {
+ this.visible_conversations = visible;
+ return Source.REMOVE;
+ }
+
+ uint start_index = ((uint) first.get_index());
+ uint end_index = uint.min(
+ // Assume that all messages are the same height
+ start_index + (uint) (this.vadjustment.page_size / first.get_allocated_height()),
+ this.model.store.get_n_items()
+ );
+
+ for (uint i = start_index; i < end_index; i++) {
+ visible_conversations.add(
+ this.model.store.get_item(i) as Geary.App.Conversation
+ );
+ }
+
+ this.visible_conversations = visible;
+ return Source.REMOVE;
+ }, GLib.Priority.DEFAULT_IDLE);
}
- private void on_selection_changed() {
- // Schedule processing selection changes at low idle for
- // two reasons: (a) if a lot of changes come in
- // back-to-back, this allows for all that activity to
- // settle before updating state and firing signals (which
- // results in a lot of I/O), and (b) it means the
- // ConversationMonitor's signals may be processed in any
- // order by this class and the ConversationListView and
- // not result in a lot of screen flashing and (again)
- // unnecessary I/O as both classes update selection state.
- this.selection_update.schedule();
+ // ------------
+ // Model
+ // ------------
+ private bool should_inhibit_autoactivate = false;
+
+ /**
+ * Informs the listbox to suppress autoactivate behavior on the next update
+ */
+ public void inhibit_next_autoselect() {
+ should_inhibit_autoactivate = true;
}
- // Gtk.TreeSelection can fire its "changed" signal even when
- // nothing's changed, so look for that to avoid subscribers from
- // doing the same things (in particular, I/O) multiple times
- private void do_selection_changed() {
- Gee.HashSet<Geary.App.Conversation> new_selection =
- new Gee.HashSet<Geary.App.Conversation>();
- List<Gtk.TreePath> paths = get_all_selected_paths();
- if (paths.length() != 0) {
- // Conversations are selected, so collect them and
- // signal if different
- foreach (Gtk.TreePath path in paths) {
- Geary.App.Conversation? conversation =
- get_model().get_conversation_at_path(path);
- if (conversation != null)
- new_selection.add(conversation);
+ /**
+ * Find a selectable conversation near current selection
+ */
+ private Gtk.ListBoxRow? get_next_conversation(bool asc=true) {
+ int index = asc ? 0 : int.MAX;
+
+ foreach (Gtk.ListBoxRow row in this.list.get_selected_rows().copy()) {
+ if ((asc && row.get_index() > index) ||
+ (!asc && row.get_index() < index)) {
+ index = row.get_index();
}
}
-
- // only notify if different than what was previously reported
- if (this.selected.size != new_selection.size ||
- !this.selected.contains_all(new_selection)) {
- this.selected = new_selection;
- conversations_selected(this.selected.read_only_view);
+ if (asc) {
+ index += 1;
+ } else {
+ index -= 1;
}
+ Gtk.ListBoxRow? row = this.list.get_row_at_index(index);
+ return row != null || !asc ? row : get_next_conversation(false);
}
- public Gee.Set<Geary.App.Conversation> get_visible_conversations() {
- Gee.HashSet<Geary.App.Conversation> visible_conversations = new
Gee.HashSet<Geary.App.Conversation>();
-
- Gtk.TreePath start_path;
- Gtk.TreePath end_path;
- if (!get_visible_range(out start_path, out end_path))
- return visible_conversations;
-
- while (start_path.compare(end_path) <= 0) {
- Geary.App.Conversation? conversation = get_model().get_conversation_at_path(start_path);
- if (conversation != null)
- visible_conversations.add(conversation);
+ private void on_conversations_loaded() {
+ if (this.config.autoselect &&
+ !this.should_inhibit_autoactivate &&
+ this.list.get_selected_rows().length() == 0) {
- start_path.next();
+ Gtk.ListBoxRow first_row = this.list.get_row_at_index(0);
+ if (first_row != null) {
+ this.list.select_row(first_row);
+ conversations_selected(get_selected(), false);
+ }
}
- return visible_conversations;
+ this.should_inhibit_autoactivate = false;
}
- // Always returns false, so it can be used as a one-time SourceFunc
- private bool update_visible_conversations() {
- bool changed = false;
- Gee.Set<Geary.App.Conversation> visible_conversations = get_visible_conversations();
- if (this.current_visible_conversations == null ||
- this.current_visible_conversations.size != visible_conversations.size ||
- !this.current_visible_conversations.contains_all(visible_conversations)) {
- this.current_visible_conversations = visible_conversations;
- visible_conversations_changed(
- this.current_visible_conversations.read_only_view
- );
- changed = true;
+ /*
+ * Select next conversation
+ */
+ private void on_conversations_removed(bool start) {
+ // Before model update, just find a conversation
+ if (start) {
+ this.to_restore_row = get_next_conversation();
+ // If in selection mode, leaving will do the job
+ } else if (this.selection_mode_enabled) {
+ this.selection_mode_enabled = false;
+ // Set next conversation
+ } else if (this.to_restore_row != null) {
+ this.list.select_row(this.to_restore_row);
+ conversations_selected(get_selected(), false);
+ this.to_restore_row.grab_focus();
+ this.to_restore_row = null;
}
- return changed;
}
- private void schedule_visible_conversations_changed() {
- scheduled_update_visible_conversations = Geary.Scheduler.on_idle(update_visible_conversations);
- }
- public void select_conversations(Gee.Collection<Geary.App.Conversation> new_selection) {
- if (this.selected.size != new_selection.size ||
- !this.selected.contains_all(new_selection)) {
- var selection = get_selection();
- selection.unselect_all();
- var model = get_model();
- if (model != null) {
- foreach (var conversation in new_selection) {
- var path = model.get_path_for_conversation(conversation);
- if (path != null) {
- selection.select_path(path);
- }
+ // ----------
+ // Gestures
+ // ----------
+
+ private void on_press_gesture_released(int n_press, double x, double y) {
+ if (this.press_gesture.get_current_button() == 1) {
+ Gdk.EventSequence sequence = this.press_gesture.get_current_sequence();
+ Gdk.Event event = this.press_gesture.get_last_event(sequence);
+ Gdk.ModifierType modifier_type;
+ event.get_state(out modifier_type);
+ // Do shift range selection
+ if ((modifier_type & Gdk.ModifierType.SHIFT_MASK) ==
+ Gdk.ModifierType.SHIFT_MASK) {
+ this.selection_mode_enabled = true;
+ // Do control multiple selection
+ } else if ((modifier_type & Gdk.ModifierType.CONTROL_MASK) ==
+ Gdk.ModifierType.CONTROL_MASK) {
+ this.selection_mode_enabled = true;
+ }
+ } else {
+ Row? row = (Row) this.list.get_row_at_y((int) y);
+ if (row != null) {
+ if (this.press_gesture.get_current_button() == 2) {
+ conversation_alt_action(row.conversation);
+ } else if (this.press_gesture.get_current_button() == 3) {
+ var rect = Gdk.Rectangle();
+ row.translate_coordinates(this.list, 0, 0, out rect.x, out rect.y);
+ rect.x = (int) x;
+ rect.y = (int) y - rect.y;
+ rect.width = rect.height = 0;
+ context_menu(row, rect);
}
}
}
}
- private void on_rows_changed() {
- schedule_visible_conversations_changed();
+ private bool on_key_event_controller_key_pressed(uint keyval, uint keycode, Gdk.ModifierType
modifier_type) {
+ switch (keyval) {
+ case Gdk.Key.Up:
+ case Gdk.Key.Down:
+ if ((modifier_type & Gdk.ModifierType.SHIFT_MASK) ==
+ Gdk.ModifierType.SHIFT_MASK) {
+ this.selection_mode_enabled = true;
+ }
+ break;
+ case Gdk.Key.Escape:
+ if (this.selection_mode_enabled) {
+ this.selection_mode_enabled = false;
+ return true;
+ }
+ break;
+ }
+ return false;
}
- private void on_display_preview_changed() {
- style_updated();
- model.foreach(refresh_path);
- schedule_visible_conversations_changed();
- }
+ /**
+ * Widgets used as drag icons have to be explicitly destroyed after the drag
+ * so we track the widget as a private member
+ */
+ private Row? drag_widget = null;
- private bool refresh_path(Gtk.TreeModel model, Gtk.TreePath path, Gtk.TreeIter iter) {
- model.row_changed(path, iter);
- return false;
- }
+ private void on_drag_begin(Gdk.DragContext ctx) {
+ int screen_x, screen_y;
+ Gdk.ModifierType _modifier;
- // Enable/disable hover effect on all selected cells.
- private void set_hover_selected(bool hover) {
- ConversationListCellRenderer.set_hover_selected(hover);
- queue_draw();
- }
+ this.get_window().get_device_position(ctx.get_device(), out screen_x, out screen_y, out _modifier);
- private bool on_motion_notify_event(Gdk.EventMotion event) {
- if (get_selection().count_selected_rows() > 0) {
- Gtk.TreePath? path = null;
- int cell_x, cell_y;
- get_path_at_pos((int) event.x, (int) event.y, out path, null, out cell_x, out cell_y);
+ // If the user has a selection but drags starting from an unselected
+ // row, we need to set the selection to that row
+ Row? row = this.list.get_row_at_y(screen_y + (int) this.vadjustment.value) as Row?;
+ if (row != null && !row.is_selected()) {
+ this.list.unselect_all();
+ this.list.select_row(row);
+ }
- set_hover_selected(path != null && get_selection().path_is_selected(path));
+ this.drag_widget = new Row(this.config, row.conversation, false);
+ this.drag_widget.width_request = row.get_allocated_width();
+ this.drag_widget.get_style_context().add_class("drag-n-drop");
+ this.drag_widget.visible = true;
+
+ int hot_x, hot_y;
+ this.translate_coordinates(row, screen_x, screen_y, out hot_x, out hot_y);
+ Gtk.drag_set_icon_widget(ctx, this.drag_widget, hot_x, hot_y);
+ }
+
+ private void on_drag_end(Gdk.DragContext ctx) {
+ if (this.drag_widget != null) {
+ this.drag_widget.destroy();
+ this.drag_widget = null;
}
- return Gdk.EVENT_PROPAGATE;
}
- private bool on_leave_notify_event() {
- if (get_selection().count_selected_rows() > 0) {
- set_hover_selected(false);
+ private void on_selected_rows_changed() {
+ GLib.List<unowned Gtk.ListBoxRow> rows = this.list.get_selected_rows();
+
+ if (rows.length() > 1) {
+ this.selection_mode_enabled = true;
}
- return Gdk.EVENT_PROPAGATE;
+ if (this.selection_mode_enabled) {
+ conversations_selected(get_selected(), false);
+ }
}
- private void on_vadjustment_changed() {
- this.vadjustment.value_changed.connect(on_value_changed);
+ private void on_row_activated() {
+ if (!this.selection_mode_enabled) {
+ conversations_selected(get_selected());
+ }
}
+ private void on_selection_mode_changed() {
+ if (this.list.get_activate_on_single_click() != this.selection_mode_enabled) {
+ return;
+ }
+
+ this.list.foreach((child) => {
+ var row = (Row) child;
+ row.set_selection_enabled(this.selection_mode_enabled);
+ });
+
+ if (this.selection_mode_enabled) {
+ this.to_restore_row = this.list.get_selected_row();
+ this.list.set_selection_mode(Gtk.SelectionMode.MULTIPLE);
+ this.list.set_activate_on_single_click(false);
+ } else {
+ this.list.set_selection_mode(Gtk.SelectionMode.SINGLE);
+ this.list.set_activate_on_single_click(true);
+ this.list.unselect_all();
+ if (this.to_restore_row != null) {
+ this.list.select_row(this.to_restore_row);
+ this.to_restore_row.grab_focus();
+ conversations_selected(get_selected(), false);
+ this.to_restore_row = null;
+ }
+ }
+ }
}
diff --git a/src/client/conversation-viewer/conversation-viewer.vala
b/src/client/conversation-viewer/conversation-viewer.vala
index 0e507a021..90142537d 100644
--- a/src/client/conversation-viewer/conversation-viewer.vala
+++ b/src/client/conversation-viewer/conversation-viewer.vala
@@ -152,9 +152,9 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface {
// XXX move the ConversationListView management code into
// MainWindow or somewhere more appropriate
- ConversationListView conversation_list = main_window.conversation_list_view;
- this.selection_while_composing = conversation_list.copy_selected();
- conversation_list.get_selection().unselect_all();
+ ConversationList.View conversation_list = main_window.conversation_list_view;
+ this.selection_while_composing = conversation_list.get_selected();
+ conversation_list.unselect_all();
box.vanished.connect(on_composer_closed);
this.composer_page.add(box);
diff --git a/src/client/meson.build b/src/client/meson.build
index 08027b3d4..92aee2a62 100644
--- a/src/client/meson.build
+++ b/src/client/meson.build
@@ -88,10 +88,10 @@ client_vala_sources = files(
'composer/contact-entry-completion.vala',
'composer/spell-check-popover.vala',
- 'conversation-list/conversation-list-cell-renderer.vala',
- 'conversation-list/conversation-list-store.vala',
+ 'conversation-list/conversation-list-model.vala',
+ 'conversation-list/conversation-list-participant.vala',
+ 'conversation-list/conversation-list-row.vala',
'conversation-list/conversation-list-view.vala',
- 'conversation-list/formatted-conversation-data.vala',
'conversation-viewer/conversation-contact-popover.vala',
'conversation-viewer/conversation-email.vala',
diff --git a/src/client/sidebar/sidebar-count-cell-renderer.vala
b/src/client/sidebar/sidebar-count-cell-renderer.vala
index 615cb8641..c6bd1bfb5 100644
--- a/src/client/sidebar/sidebar-count-cell-renderer.vala
+++ b/src/client/sidebar/sidebar-count-cell-renderer.vala
@@ -23,7 +23,7 @@ public class SidebarCountCellRenderer : Gtk.CellRenderer {
public override void get_preferred_width(Gtk.Widget widget, out int minimum_size, out int natural_size) {
unread_count.count = counter;
- minimum_size = unread_count.get_width(widget) + FormattedConversationData.SPACING;
+ minimum_size = unread_count.get_width(widget) + CountBadge.SPACING;
natural_size = minimum_size;
}
diff --git a/src/engine/util/util-numeric.vala b/src/engine/util/util-numeric.vala
index 4c1ac9d79..646f2525e 100644
--- a/src/engine/util/util-numeric.vala
+++ b/src/engine/util/util-numeric.vala
@@ -56,5 +56,19 @@ public int int64_compare(void* a, void *b) {
return 0;
}
+public int max(int a, int b) {
+ if (a > b)
+ return a;
+ else
+ return b;
+}
+
+public int min(int a, int b) {
+ if (a < b)
+ return a;
+ else
+ return b;
+}
+
}
diff --git a/ui/application-main-window.ui b/ui/application-main-window.ui
index 36ae7a780..a10fcd53a 100644
--- a/ui/application-main-window.ui
+++ b/ui/application-main-window.ui
@@ -45,20 +45,13 @@
</packing>
</child>
<child>
- <object class="GtkFrame" id="folder_frame">
- <property name="visible">True</property>
- <property name="vexpand">True</property>
- <property name="label_xalign">0</property>
- <property name="shadow_type">none</property>
- <child>
- <object class="GtkScrolledWindow" id="folder_list_scrolled">
- <property name="visible">True</property>
- <property name="hscrollbar_policy">never</property>
- </object>
- </child>
- <style>
- <class name="geary-folder-frame"/>
- </style>
+ <object class="GtkScrolledWindow" id="folder_list_scrolled">
+ <property name="visible">True</property>
+ <property name="vexpand">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <style>
+ <class name="geary-folder"/>
+ </style>
</object>
<packing>
<property name="fill">True</property>
@@ -94,28 +87,6 @@
<property name="position">0</property>
</packing>
</child>
- <child>
- <object class="GtkFrame" id="conversation_frame">
- <property name="visible">True</property>
- <property name="label_xalign">0</property>
- <property name="shadow_type">none</property>
- <child>
- <object class="GtkScrolledWindow" id="conversation_list_scrolled">
- <property name="width_request">250</property>
- <property name="visible">True</property>
- </object>
- </child>
- <style>
- <class name="geary-conversation-frame"/>
- </style>
- </object>
- <packing>
- <property name="expand">True</property>
- <property name="fill">True</property>
- <property name="pack_type">end</property>
- <property name="position">1</property>
- </packing>
- </child>
<child>
<object class="GtkRevealer" id="conversation_list_actions_revealer">
<property name="visible">True</property>
diff --git a/ui/components-conversation-actions.ui b/ui/components-conversation-actions.ui
index f823dfcf4..744a464c1 100644
--- a/ui/components-conversation-actions.ui
+++ b/ui/components-conversation-actions.ui
@@ -82,15 +82,15 @@
<object class="GtkBox" id="mark_copy_move_buttons">
<property name="visible">True</property>
<child>
- <object class="GtkMenuButton" id="mark_message_button">
+ <object class="GtkMenuButton" id="copy_message_button">
<property name="visible">True</property>
<property name="focus_on_click">False</property>
<property name="receives_default">False</property>
<property name="always_show_image">True</property>
<child>
- <object class="GtkImage" id="mark_message_image">
+ <object class="GtkImage" id="copy_message_image">
<property name="visible">True</property>
- <property name="icon_name">checkbox-checked-symbolic</property>
+ <property name="icon_name">tag-symbolic</property>
</object>
</child>
</object>
@@ -101,15 +101,15 @@
</packing>
</child>
<child>
- <object class="GtkMenuButton" id="copy_message_button">
+ <object class="GtkMenuButton" id="move_message_button">
<property name="visible">True</property>
<property name="focus_on_click">False</property>
<property name="receives_default">False</property>
<property name="always_show_image">True</property>
<child>
- <object class="GtkImage" id="copy_message_image">
+ <object class="GtkImage" id="move_message_image">
<property name="visible">True</property>
- <property name="icon_name">tag-symbolic</property>
+ <property name="icon_name">folder-symbolic</property>
</object>
</child>
</object>
@@ -120,17 +120,20 @@
</packing>
</child>
<child>
- <object class="GtkMenuButton" id="move_message_button">
+ <object class="GtkMenuButton" id="mark_message_button">
<property name="visible">True</property>
<property name="focus_on_click">False</property>
<property name="receives_default">False</property>
<property name="always_show_image">True</property>
<child>
- <object class="GtkImage" id="move_message_image">
+ <object class="GtkImage" id="mark_message_image">
<property name="visible">True</property>
- <property name="icon_name">folder-symbolic</property>
+ <property name="icon_name">pan-down-symbolic</property>
</object>
</child>
+ <style>
+ <class name="thin-button"/>
+ </style>
</object>
<packing>
<property name="expand">False</property>
diff --git a/ui/components-headerbar-conversation-list.ui b/ui/components-headerbar-conversation-list.ui
index 9addaab67..0abf89fc4 100644
--- a/ui/components-headerbar-conversation-list.ui
+++ b/ui/components-headerbar-conversation-list.ui
@@ -60,7 +60,24 @@
</object>
<packing>
<property name="pack_type">end</property>
- <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="selection_button">
+ <property name="visible">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_text" translatable="yes">Selection conversations</property>
+ <property name="always_show_image">True</property>
+ <child>
+ <object class="GtkImage" id="selection_button_image">
+ <property name="visible">True</property>
+ <property name="icon_name">selection-mode-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
</packing>
</child>
</template>
diff --git a/ui/conversation-list-row.ui b/ui/conversation-list-row.ui
new file mode 100644
index 000000000..d3611cde2
--- /dev/null
+++ b/ui/conversation-list-row.ui
@@ -0,0 +1,214 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.38.2 -->
+<interface>
+ <requires lib="gtk+" version="3.24"/>
+ <object class="GtkImage" id="flagged_icon">
+ <property name="visible">True</property>
+ </object>
+ <object class="GtkImage" id="read_icon">
+ <property name="visible">True</property>
+ </object>
+ <template class="ConversationListRow" parent="GtkListBoxRow">
+ <property name="can-focus">True</property>
+ <child>
+ <object class="GtkEventBox" id="eventbox">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox" id="container">
+ <property name="visible">True</property>
+ <property name="has-tooltip">True</property>
+ <property name="spacing">5</property>
+ <property name="baseline-position">top</property>
+ <child>
+ <object class="GtkStack" id="stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox" id="buttons">
+ <property name="width-request">36</property>
+ <property name="visible">True</property>
+ <property name="vexpand">True</property>
+ <property name="orientation">vertical</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkButton" id="unread">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="valign">center</property>
+ <property name="relief">none</property>
+ <property name="image">read_icon</property>
+ <signal name="clicked" handler="on_unread_button_clicked"/>
+ <style>
+ <class name="conversation-ephemeral-button"/>
+ <class name="unread-button"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="flagged">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="valign">center</property>
+ <property name="relief">none</property>
+ <property name="image">flagged_icon</property>
+ <signal name="clicked" handler="on_flagged_button_clicked"/>
+ <style>
+ <class name="conversation-ephemeral-button"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="name">buttons</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="selected_button">
+ <property name="receives-default">True</property>
+ <property name="valign">center</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="name">selection-button</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="Details">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="baseline-position">top</property>
+ <child>
+ <object class="GtkBox" id="Header">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="participants">
+ <property name="visible">True</property>
+ <property name="use-markup">True</property>
+ <property name="ellipsize">end</property>
+ <property name="xalign">0</property>
+ <style>
+ <class name="participants"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="date">
+ <property name="visible">True</property>
+ <style>
+ <class name="date"/>
+ <class name="tertiary"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="pack-type">end</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="subject">
+ <property name="visible">True</property>
+ <property name="halign">start</property>
+ <property name="ellipsize">end</property>
+ <property name="single-line-mode">True</property>
+ <property name="xalign">0</property>
+ <style>
+ <class name="subject"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="preview_row">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="preview">
+ <property name="visible">True</property>
+ <property name="halign">start</property>
+ <property name="hexpand">True</property>
+ <property name="wrap">True</property>
+ <property name="wrap-mode">word-char</property>
+ <property name="ellipsize">end</property>
+ <property name="lines">1</property>
+ <property name="xalign">0</property>
+ <style>
+ <class name="preview"/>
+ <class name="tertiary"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="count_badge">
+ <property name="visible">True</property>
+ <property name="valign">center</property>
+ <style>
+ <class name="count-badge"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <style>
+ <class name="conversation-list"/>
+ </style>
+ </template>
+</interface>
diff --git a/ui/conversation-list-view.ui b/ui/conversation-list-view.ui
new file mode 100644
index 000000000..d34929576
--- /dev/null
+++ b/ui/conversation-list-view.ui
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.38.2 -->
+<interface>
+ <requires lib="gtk+" version="3.24"/>
+ <template class="ConversationListView" parent="GtkScrolledWindow">
+ <property name="width-request">250</property>
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkViewport">
+ <property name="visible">True</property>
+ <property name="shadow-type">none</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkListBox" id="list">
+ <property name="name">conversation-list</property>
+ <property name="visible">True</property>
+ <property name="selection-mode">single</property>
+ <property name="activate-on-single-click">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/ui/geary.css b/ui/geary.css
index 309b756bb..4c6e1aeff 100644
--- a/ui/geary.css
+++ b/ui/geary.css
@@ -8,13 +8,7 @@
/* MainWindow */
-.geary-folder-frame > border {
- border-left-width: 0;
- border-top-width: 0;
- border-right-width: 0;
-}
-
-.geary-folder-frame {
+.geary-folder {
min-width: 300px;
}
@@ -22,10 +16,7 @@ geary-conversation-list revealer {
margin: 6px;
}
-.geary-conversation-frame > border {
- border-left-width: 0;
- border-top-width: 0;
- border-right-width: 0;
+geary-conversation-list {
min-width: 360px;
}
@@ -33,10 +24,6 @@ geary-conversation-viewer {
min-width: 360px;
}
-.geary-sidebar-pane-separator.vertical .conversation-frame > border {
- border-bottom-width: 0;
-}
-
.geary-overlay {
background-color: @theme_base_color;
padding: 2px 6px;
@@ -57,7 +44,7 @@ geary-conversation-viewer {
}
infobar flowboxchild {
- padding: 0px;
+ padding: 0px;
}
revealer components-conversation-actions {
@@ -66,6 +53,93 @@ revealer components-conversation-actions {
padding: 6px;
}
+
+/* Conversation List */
+row.conversation-list {
+ padding-top: 0.5em;
+ padding-bottom: 0.5em;
+ padding-right: 0.5em;
+}
+
+row.conversation-list.drag-n-drop {
+ background: @theme_base_color;
+ opacity: 0.7;
+ box-shadow: none;
+}
+
+row.conversation-list label {
+ margin-bottom: .4em;
+}
+
+row.conversation-list .tertiary {
+ opacity: 0.7;
+ font-size: 0.8em;
+}
+
+row.conversation-list .subject {
+ font-size: 0.9em;
+}
+
+row.conversation-list .date {
+ margin-left: 1em;
+}
+
+/* Unread styling */
+row.conversation-list.unread .preview {
+ opacity: 1;
+}
+
+row.conversation-list.unread .subject {
+ font-weight: bold;
+}
+
+row.conversation-list.unread .participants {
+ font-weight: bold;
+}
+
+row.conversation-list.unread .unread-button {
+ opacity: 1;
+}
+
+/* Hover buttons */
+row.conversation-list .conversation-ephemeral-button {
+ opacity: 0;
+ margin: 2px;
+ border-radius: 50%;
+ border: none;
+}
+
+row.conversation-list:hover .conversation-ephemeral-button {
+ opacity: 1;
+}
+
+row.conversation-list:selected .conversation-ephemeral-button {
+ opacity: 1;
+}
+
+row.conversation-list .count-badge {
+ background: #888888;
+ color: white;
+ min-width: 1.5em;
+ border-radius: 1em;
+ font-size: .8em;
+ font-weight: bold;
+}
+
+row.conversation-list check {
+ border-radius: 50%;
+ padding: 2px;
+ margin: 6px;
+}
+
+row.selected.conversation-list {
+ background: alpha(@theme_selected_bg_color, 0.1);
+}
+
+row.selected.conversation-list:hover {
+ background: alpha(@theme_selected_bg_color, 0.2);
+}
+
/* FolderPopover */
.geary-folder-popover-list {
@@ -415,11 +489,11 @@ popover.geary-editor > grid > button.geary-setting-remove {
}
dialog.geary-remove-confirm .dialog-vbox {
- margin: 12px;
+ margin: 12px;
}
dialog.geary-remove-confirm .dialog-action-box {
- margin: 6px;
+ margin: 6px;
}
/* FolderList.Tree */
@@ -428,6 +502,11 @@ treeview.sidebar {
border: none;
}
+treeview:selected.sidebar {
+ background: alpha(@theme_text_color, 0.1);
+ color: @theme_text_color;
+}
+
treeview.sidebar .cell {
padding: 9px 6px;
}
@@ -464,3 +543,10 @@ dialog.geary-upgrade grid {
dialog.geary-upgrade label {
margin-top: 12px;
}
+
+/* Misc */
+
+.thin-button {
+ padding-left: 4px;
+ padding-right: 4px;
+}
diff --git a/ui/org.gnome.Geary.gresource.xml b/ui/org.gnome.Geary.gresource.xml
index a486d1fa8..3e314b92c 100644
--- a/ui/org.gnome.Geary.gresource.xml
+++ b/ui/org.gnome.Geary.gresource.xml
@@ -33,6 +33,8 @@
<file compressed="true">composer-web-view.css</file>
<file compressed="true">composer-web-view.js</file>
<file compressed="true" preprocess="xml-stripblanks">conversation-contact-popover.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks">conversation-list-row.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks">conversation-list-view.ui</file>
<file compressed="true" preprocess="xml-stripblanks">conversation-email.ui</file>
<file compressed="true" preprocess="xml-stripblanks">conversation-email-menus.ui</file>
<file compressed="true" preprocess="xml-stripblanks">conversation-message.ui</file>
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]