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
This commit is contained in:
parent
0675662f86
commit
533a32e67b
23 changed files with 1724 additions and 1872 deletions
|
|
@ -69,10 +69,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
|
||||
|
|
@ -466,6 +466,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
|
||||
|
|
|
|||
|
|
@ -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.selected;
|
||||
}
|
||||
this.new_window.begin(folder, conversations);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ public class Application.MainWindow :
|
|||
{ ACTION_FIND_IN_CONVERSATION, on_find_in_conversation_action },
|
||||
{ ACTION_SEARCH, on_search_activated },
|
||||
{ ACTION_SELECT_INBOX, on_select_inbox, "i" },
|
||||
{ ACTION_NAVIGATION_BACK, focus_previous_pane},
|
||||
{ ACTION_NAVIGATION_BACK, go_to_previous_pane},
|
||||
|
||||
// Message actions
|
||||
{ ACTION_REPLY_CONVERSATION, on_reply_conversation },
|
||||
|
|
@ -237,6 +237,16 @@ public class Application.MainWindow :
|
|||
"navigate", 1,
|
||||
typeof(Gtk.ScrollType), Gtk.ScrollType.STEP_DOWN
|
||||
);
|
||||
Gtk.BindingEntry.add_signal(
|
||||
bindings,
|
||||
Gdk.Key.Escape, 0,
|
||||
"escape", 0
|
||||
);
|
||||
Gtk.BindingEntry.add_signal(
|
||||
bindings,
|
||||
Gdk.Key.a, CONTROL_MASK,
|
||||
"select_all", 0
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -355,7 +365,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 +412,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;
|
||||
|
||||
|
|
@ -517,22 +526,34 @@ public class Application.MainWindow :
|
|||
activate_action(get_window_action(ACTION_FIND_IN_CONVERSATION));
|
||||
}
|
||||
|
||||
/** Keybinding signal for escaping current view. */
|
||||
[Signal (action=true)]
|
||||
public virtual signal void escape() {
|
||||
navigate_previous_pane();
|
||||
}
|
||||
|
||||
/** Keybinding signal for selecting all elements in current view. */
|
||||
[Signal (action=true)]
|
||||
public virtual signal void select_all() {
|
||||
this.conversation_list_view.select_all();
|
||||
}
|
||||
|
||||
/** Keybinding signal for shifting the keyboard focus. */
|
||||
[Signal (action=true)]
|
||||
public virtual signal void navigate(Gtk.ScrollType type) {
|
||||
switch (type) {
|
||||
case Gtk.ScrollType.PAGE_LEFT:
|
||||
if (get_direction() != RTL) {
|
||||
focus_previous_pane();
|
||||
go_to_previous_pane();
|
||||
} else {
|
||||
focus_next_pane();
|
||||
go_to_next_pane();
|
||||
}
|
||||
break;
|
||||
case Gtk.ScrollType.PAGE_RIGHT:
|
||||
if (get_direction() != RTL) {
|
||||
focus_next_pane();
|
||||
go_to_next_pane();
|
||||
} else {
|
||||
focus_previous_pane();
|
||||
go_to_previous_pane();
|
||||
}
|
||||
break;
|
||||
case Gtk.ScrollType.STEP_UP:
|
||||
|
|
@ -659,7 +680,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 +783,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 +801,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 +850,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 +946,6 @@ public class Application.MainWindow :
|
|||
Gee.Collection.empty<Geary.EmailIdentifier>(),
|
||||
is_interactive
|
||||
);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1335,15 +1350,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.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 +1377,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 +1413,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 +1584,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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1750,15 +1778,49 @@ 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) {
|
||||
bool folded = this.outer_leaflet.folded;
|
||||
// Else selection handled by activated
|
||||
if (selected.size > 1 || !folded) {
|
||||
select_conversations.begin(selected, Gee.Collection.empty(), true);
|
||||
}
|
||||
if (this.conversation_list_view.selection_mode_enabled) {
|
||||
if (selected.size > 0) {
|
||||
this.conversation_list_actions_revealer.reveal_child = folded;
|
||||
} else {
|
||||
this.conversation_list_actions_revealer.reveal_child = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void on_conversations_selected(Gee.Set<Geary.App.Conversation> selected) {
|
||||
this.select_conversations.begin(selected, Gee.Collection.empty(), true);
|
||||
private void on_conversation_activated(Geary.App.Conversation activated, uint button) {
|
||||
if (button == 1) {
|
||||
bool folded = this.outer_leaflet.folded;
|
||||
go_to_next_pane(true);
|
||||
if (folded) {
|
||||
Gee.Collection<Geary.App.Conversation> selected =
|
||||
new Gee.ArrayList<Geary.App.Conversation>();
|
||||
selected.add(activated);
|
||||
select_conversations.begin(selected, Gee.Collection.empty(), true);
|
||||
}
|
||||
} else if (this.selected_folder != null) {
|
||||
if (this.selected_folder.used_as != DRAFTS) {
|
||||
this.application.new_window.begin(
|
||||
this.selected_folder,
|
||||
this.conversation_list_view.selected
|
||||
);
|
||||
} else {
|
||||
// TODO: Determine how to map between conversations
|
||||
// and drafts correctly.
|
||||
Geary.Email draft = activated.get_latest_recv_email(IN_FOLDER);
|
||||
this.create_composer.begin(
|
||||
this.selected_folder.account,
|
||||
EDIT,
|
||||
draft,
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void on_conversation_count_changed() {
|
||||
|
|
@ -1778,7 +1840,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.selected.size == 0) {
|
||||
this.conversation_viewer.show_none_selected();
|
||||
update_conversation_actions(NONE);
|
||||
}
|
||||
|
|
@ -1865,20 +1927,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);
|
||||
}
|
||||
|
||||
|
|
@ -1907,7 +1955,7 @@ public class Application.MainWindow :
|
|||
Gee.Collection<Geary.EmailIdentifier> ids =
|
||||
new Gee.LinkedList<Geary.EmailIdentifier>();
|
||||
foreach (Geary.App.Conversation convo in
|
||||
this.conversation_list_view.get_selected()) {
|
||||
this.conversation_list_view.selected) {
|
||||
ids.add_all(convo.get_email_ids());
|
||||
}
|
||||
try {
|
||||
|
|
@ -1956,65 +2004,83 @@ public class Application.MainWindow :
|
|||
}
|
||||
}
|
||||
|
||||
private void focus_next_pane() {
|
||||
var focus = get_focus();
|
||||
|
||||
if (this.outer_leaflet.folded) {
|
||||
if (this.outer_leaflet.visible_child_name == INNER_LEAFLET) {
|
||||
if (this.inner_leaflet.folded &&
|
||||
this.inner_leaflet.visible_child_name == FOLDER_LIST ||
|
||||
focus == this.folder_list) {
|
||||
this.inner_leaflet.navigate(Hdy.NavigationDirection.FORWARD);
|
||||
focus = this.conversation_list_view;
|
||||
} else {
|
||||
if (this.conversation_list_view.get_selected().size == 1 &&
|
||||
this.selected_folder.properties.email_total > 0) {
|
||||
this.outer_leaflet.navigate(Hdy.NavigationDirection.FORWARD);
|
||||
focus = this.conversation_viewer.visible_child;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (focus != null) {
|
||||
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)) {
|
||||
focus = this.conversation_viewer.visible_child;
|
||||
} else if (focus == this.conversation_viewer ||
|
||||
focus.is_ancestor(this.conversation_viewer)) {
|
||||
focus = this.folder_list;
|
||||
}
|
||||
}
|
||||
|
||||
if (focus != null) {
|
||||
focus.focus(TAB_FORWARD);
|
||||
private void focus_widget(Gtk.Widget? widget) {
|
||||
if (widget != null) {
|
||||
widget.focus(TAB_FORWARD);
|
||||
} else {
|
||||
error_bell();
|
||||
}
|
||||
}
|
||||
|
||||
private void focus_previous_pane() {
|
||||
private void navigate_next_pane() {
|
||||
var focus = get_focus();
|
||||
if (this.outer_leaflet.visible_child_name == INNER_LEAFLET) {
|
||||
if (this.inner_leaflet.folded &&
|
||||
this.inner_leaflet.visible_child_name == FOLDER_LIST ||
|
||||
focus == this.folder_list) {
|
||||
this.inner_leaflet.navigate(Hdy.NavigationDirection.FORWARD);
|
||||
focus = this.conversation_list_view;
|
||||
} else {
|
||||
if (this.conversation_list_view.selected.size == 1 &&
|
||||
this.selected_folder.properties.email_total > 0) {
|
||||
this.outer_leaflet.navigate(Hdy.NavigationDirection.FORWARD);
|
||||
focus = this.conversation_viewer.visible_child;
|
||||
}
|
||||
}
|
||||
}
|
||||
focus_widget(focus);
|
||||
}
|
||||
|
||||
private void focus_next_pane() {
|
||||
var focus = get_focus();
|
||||
if (focus != null) {
|
||||
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)) {
|
||||
focus = this.conversation_viewer.visible_child;
|
||||
} else if (focus == this.conversation_viewer ||
|
||||
focus.is_ancestor(this.conversation_viewer)) {
|
||||
focus = this.folder_list;
|
||||
}
|
||||
}
|
||||
focus_widget(focus);
|
||||
}
|
||||
|
||||
private void go_to_next_pane(bool only_if_folded=false) {
|
||||
if (this.outer_leaflet.folded) {
|
||||
if (this.outer_leaflet.visible_child_name == INNER_LEAFLET) {
|
||||
if (this.inner_leaflet.folded) {
|
||||
if (this.inner_leaflet.visible_child_name == CONVERSATION_LIST) {
|
||||
this.inner_leaflet.navigate(Hdy.NavigationDirection.BACK);
|
||||
focus = this.folder_list;
|
||||
}
|
||||
} else {
|
||||
if (focus == this.conversation_list_view)
|
||||
focus = this.folder_list;
|
||||
else
|
||||
focus = this.conversation_list_view;
|
||||
navigate_next_pane();
|
||||
} else if (!only_if_folded) {
|
||||
focus_next_pane();
|
||||
}
|
||||
}
|
||||
|
||||
private void navigate_previous_pane() {
|
||||
var focus = get_focus();
|
||||
if (this.outer_leaflet.visible_child_name == INNER_LEAFLET) {
|
||||
if (this.inner_leaflet.folded) {
|
||||
if (this.inner_leaflet.visible_child_name == CONVERSATION_LIST) {
|
||||
this.inner_leaflet.navigate(Hdy.NavigationDirection.BACK);
|
||||
focus = this.folder_list;
|
||||
}
|
||||
} else {
|
||||
this.outer_leaflet.navigate(Hdy.NavigationDirection.BACK);
|
||||
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;
|
||||
}
|
||||
} else if (focus != null) {
|
||||
} else {
|
||||
this.outer_leaflet.navigate(Hdy.NavigationDirection.BACK);
|
||||
focus = this.conversation_list_view;
|
||||
}
|
||||
focus_widget(focus);
|
||||
}
|
||||
|
||||
private void focus_previous_pane() {
|
||||
var focus = get_focus();
|
||||
if (focus != null) {
|
||||
if (focus == this.folder_list ||
|
||||
focus.is_ancestor(this.folder_list)) {
|
||||
focus = this.conversation_viewer.visible_child;
|
||||
|
|
@ -2026,13 +2092,15 @@ public class Application.MainWindow :
|
|||
focus = this.conversation_list_view;
|
||||
}
|
||||
}
|
||||
focus_widget(focus);
|
||||
}
|
||||
|
||||
if (focus != null) {
|
||||
focus.focus(TAB_FORWARD);
|
||||
private void go_to_previous_pane() {
|
||||
if (this.outer_leaflet.folded) {
|
||||
navigate_previous_pane();
|
||||
} else {
|
||||
error_bell();
|
||||
focus_previous_pane();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private SimpleAction get_window_action(string name) {
|
||||
|
|
@ -2055,7 +2123,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()) &&
|
||||
|
|
@ -2063,7 +2131,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2076,10 +2144,6 @@ public class Application.MainWindow :
|
|||
);
|
||||
}
|
||||
|
||||
private void on_load_more() {
|
||||
load_more();
|
||||
}
|
||||
|
||||
[GtkCallback]
|
||||
private void on_map() {
|
||||
this.update_ui_timeout.start();
|
||||
|
|
@ -2116,7 +2180,7 @@ public class Application.MainWindow :
|
|||
|
||||
[GtkCallback]
|
||||
private void on_outer_leaflet_changed() {
|
||||
int selected = this.conversation_list_view.get_selected().size;
|
||||
int selected = this.conversation_list_view.selected.size;
|
||||
update_conversation_actions(
|
||||
ConversationCount.for_size(selected)
|
||||
);
|
||||
|
|
@ -2140,6 +2204,13 @@ public class Application.MainWindow :
|
|||
} else {
|
||||
this.conversation_list_headerbar.show_close_button = false;
|
||||
this.conversation_headerbar.back_button.visible = false;
|
||||
if (selected > 0) {
|
||||
select_conversations.begin(
|
||||
this.conversation_list_view.selected,
|
||||
Gee.Collection.empty<Geary.EmailIdentifier>(),
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2321,7 +2392,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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2355,39 +2426,15 @@ 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)
|
||||
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) {
|
||||
if (this.selected_folder.used_as != DRAFTS) {
|
||||
this.application.new_window.begin(
|
||||
this.selected_folder,
|
||||
this.conversation_list_view.copy_selected()
|
||||
);
|
||||
} else {
|
||||
// TODO: Determine how to map between conversations
|
||||
// and drafts correctly.
|
||||
Geary.Email draft = activated.get_latest_recv_email(IN_FOLDER);
|
||||
this.create_composer.begin(
|
||||
this.selected_folder.account,
|
||||
EDIT,
|
||||
draft,
|
||||
null
|
||||
);
|
||||
}
|
||||
if (folder != null) {
|
||||
go_to_next_pane();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2470,7 +2517,7 @@ public class Application.MainWindow :
|
|||
bool starred_selected = false;
|
||||
bool unstarred_selected = false;
|
||||
foreach (Geary.App.Conversation conversation in
|
||||
this.conversation_list_view.get_selected()) {
|
||||
this.conversation_list_view.selected) {
|
||||
if (conversation.is_unread())
|
||||
unread_selected = true;
|
||||
|
||||
|
|
@ -2529,7 +2576,7 @@ public class Application.MainWindow :
|
|||
if (location != null) {
|
||||
this.controller.mark_conversations.begin(
|
||||
location,
|
||||
this.conversation_list_view.copy_selected(),
|
||||
this.conversation_list_view.selected,
|
||||
Geary.EmailFlags.UNREAD,
|
||||
false,
|
||||
(obj, res) => {
|
||||
|
|
@ -2541,6 +2588,7 @@ public class Application.MainWindow :
|
|||
}
|
||||
);
|
||||
}
|
||||
this.conversation_list_view.selection_mode_enabled = false;
|
||||
}
|
||||
|
||||
private void on_mark_as_unread() {
|
||||
|
|
@ -2548,7 +2596,7 @@ public class Application.MainWindow :
|
|||
if (location != null) {
|
||||
this.controller.mark_conversations.begin(
|
||||
location,
|
||||
this.conversation_list_view.copy_selected(),
|
||||
this.conversation_list_view.selected,
|
||||
Geary.EmailFlags.UNREAD,
|
||||
true,
|
||||
(obj, res) => {
|
||||
|
|
@ -2560,6 +2608,7 @@ public class Application.MainWindow :
|
|||
}
|
||||
);
|
||||
}
|
||||
this.conversation_list_view.selection_mode_enabled = false;
|
||||
}
|
||||
|
||||
private void on_mark_as_starred() {
|
||||
|
|
@ -2567,7 +2616,7 @@ public class Application.MainWindow :
|
|||
if (location != null) {
|
||||
this.controller.mark_conversations.begin(
|
||||
location,
|
||||
this.conversation_list_view.copy_selected(),
|
||||
this.conversation_list_view.selected,
|
||||
Geary.EmailFlags.FLAGGED,
|
||||
true,
|
||||
(obj, res) => {
|
||||
|
|
@ -2579,6 +2628,7 @@ public class Application.MainWindow :
|
|||
}
|
||||
);
|
||||
}
|
||||
this.conversation_list_view.selection_mode_enabled = false;
|
||||
}
|
||||
|
||||
private void on_mark_as_unstarred() {
|
||||
|
|
@ -2586,7 +2636,7 @@ public class Application.MainWindow :
|
|||
if (location != null) {
|
||||
this.controller.mark_conversations.begin(
|
||||
location,
|
||||
this.conversation_list_view.copy_selected(),
|
||||
this.conversation_list_view.selected,
|
||||
Geary.EmailFlags.FLAGGED,
|
||||
false,
|
||||
(obj, res) => {
|
||||
|
|
@ -2598,6 +2648,7 @@ public class Application.MainWindow :
|
|||
}
|
||||
);
|
||||
}
|
||||
this.conversation_list_view.selection_mode_enabled = false;
|
||||
}
|
||||
|
||||
private void on_mark_as_junk_toggle() {
|
||||
|
|
@ -2610,7 +2661,7 @@ public class Application.MainWindow :
|
|||
this.controller.move_conversations_special.begin(
|
||||
source,
|
||||
destination,
|
||||
this.conversation_list_view.copy_selected(),
|
||||
this.conversation_list_view.selected,
|
||||
(obj, res) => {
|
||||
try {
|
||||
this.controller.move_conversations_special.end(res);
|
||||
|
|
@ -2620,6 +2671,7 @@ public class Application.MainWindow :
|
|||
}
|
||||
);
|
||||
}
|
||||
this.conversation_list_view.selection_mode_enabled = false;
|
||||
}
|
||||
|
||||
private void on_move_conversation(Geary.Folder destination) {
|
||||
|
|
@ -2629,7 +2681,7 @@ public class Application.MainWindow :
|
|||
this.controller.move_conversations.begin(
|
||||
source,
|
||||
destination,
|
||||
this.conversation_list_view.copy_selected(),
|
||||
this.conversation_list_view.selected,
|
||||
(obj, res) => {
|
||||
try {
|
||||
this.controller.move_conversations.end(res);
|
||||
|
|
@ -2640,6 +2692,7 @@ public class Application.MainWindow :
|
|||
);
|
||||
|
||||
}
|
||||
this.conversation_list_view.selection_mode_enabled = false;
|
||||
}
|
||||
|
||||
private void on_copy_conversation(Geary.Folder destination) {
|
||||
|
|
@ -2649,7 +2702,7 @@ public class Application.MainWindow :
|
|||
this.controller.copy_conversations.begin(
|
||||
source,
|
||||
destination,
|
||||
this.conversation_list_view.copy_selected(),
|
||||
this.conversation_list_view.selected,
|
||||
(obj, res) => {
|
||||
try {
|
||||
this.controller.copy_conversations.end(res);
|
||||
|
|
@ -2660,6 +2713,7 @@ public class Application.MainWindow :
|
|||
);
|
||||
|
||||
}
|
||||
this.conversation_list_view.selection_mode_enabled = false;
|
||||
}
|
||||
|
||||
private void on_archive_conversation() {
|
||||
|
|
@ -2668,7 +2722,7 @@ public class Application.MainWindow :
|
|||
this.controller.move_conversations_special.begin(
|
||||
source,
|
||||
ARCHIVE,
|
||||
this.conversation_list_view.copy_selected(),
|
||||
this.conversation_list_view.selected,
|
||||
(obj, res) => {
|
||||
try {
|
||||
this.controller.move_conversations_special.end(res);
|
||||
|
|
@ -2678,6 +2732,7 @@ public class Application.MainWindow :
|
|||
}
|
||||
);
|
||||
}
|
||||
this.conversation_list_view.selection_mode_enabled = false;
|
||||
}
|
||||
|
||||
private void on_trash_conversation() {
|
||||
|
|
@ -2686,7 +2741,7 @@ public class Application.MainWindow :
|
|||
this.controller.move_conversations_special.begin(
|
||||
source,
|
||||
TRASH,
|
||||
this.conversation_list_view.copy_selected(),
|
||||
this.conversation_list_view.selected,
|
||||
(obj, res) => {
|
||||
try {
|
||||
this.controller.move_conversations_special.end(res);
|
||||
|
|
@ -2696,13 +2751,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.selected;
|
||||
if (target != null && this.prompt_delete_conversations(conversations.size)) {
|
||||
this.controller.delete_conversations.begin(
|
||||
target,
|
||||
|
|
@ -2716,6 +2772,7 @@ public class Application.MainWindow :
|
|||
}
|
||||
);
|
||||
}
|
||||
// No need to disable selection mode, handled by model change
|
||||
}
|
||||
|
||||
private void on_email_loaded(ConversationListBox view,
|
||||
|
|
@ -2757,6 +2814,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) {
|
||||
|
|
@ -2765,6 +2823,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) {
|
||||
|
|
@ -2773,6 +2832,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) {
|
||||
|
|
@ -2781,6 +2841,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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,73 +0,0 @@
|
|||
/* Copyright 2016 Software Freedom Conservancy Inc.
|
||||
*
|
||||
* This software is licensed under the GNU Lesser General Public License
|
||||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
public class ConversationListCellRenderer : Gtk.CellRenderer {
|
||||
private static FormattedConversationData? example_data = null;
|
||||
private static bool hover_selected = false;
|
||||
|
||||
// Mail message data.
|
||||
public FormattedConversationData data { get; set; }
|
||||
|
||||
public ConversationListCellRenderer() {
|
||||
}
|
||||
|
||||
~ConversationListCellRenderer() {
|
||||
example_data = null;
|
||||
}
|
||||
|
||||
public override void get_preferred_height(Gtk.Widget widget,
|
||||
out int minimum_size,
|
||||
out int natural_size) {
|
||||
if (example_data == null)
|
||||
style_changed(widget);
|
||||
|
||||
minimum_size = natural_size = example_data.get_height();
|
||||
}
|
||||
|
||||
public override void get_preferred_width(Gtk.Widget widget,
|
||||
out int minimum_size,
|
||||
out int natural_size) {
|
||||
// Set width to 1 (rather than 0) to work around certain
|
||||
// themes that cause the conversation list to be shown as
|
||||
// "squished":
|
||||
// https://bugzilla.gnome.org/show_bug.cgi?id=713954
|
||||
minimum_size = natural_size = 1;
|
||||
}
|
||||
|
||||
public override void render(Cairo.Context ctx, Gtk.Widget widget, Gdk.Rectangle background_area,
|
||||
Gdk.Rectangle cell_area, Gtk.CellRendererState flags) {
|
||||
if (data != null)
|
||||
data.render(ctx, widget, background_area, cell_area, flags, hover_selected);
|
||||
}
|
||||
|
||||
// Recalculates size when the style changed.
|
||||
// Note: this must be called by the parent TreeView.
|
||||
public static void style_changed(Gtk.Widget widget) {
|
||||
var window = widget.get_toplevel() as Application.MainWindow;
|
||||
if (window != null && example_data == null) {
|
||||
example_data = new FormattedConversationData.create_example(
|
||||
window.application.config
|
||||
);
|
||||
}
|
||||
|
||||
example_data.calculate_sizes(widget);
|
||||
}
|
||||
|
||||
// Shows hover effect on all selected cells.
|
||||
public static void set_hover_selected(bool hover) {
|
||||
hover_selected = hover;
|
||||
}
|
||||
|
||||
// This is implemented because it's required; ignore it and look at get_preferred_height() instead.
|
||||
public override void get_size(Gtk.Widget widget, Gdk.Rectangle? cell_area, out int x_offset,
|
||||
out int y_offset, out int width, out int height) {
|
||||
// Set values to avoid compiler warning.
|
||||
x_offset = 0;
|
||||
y_offset = 0;
|
||||
width = 0;
|
||||
height = 0;
|
||||
}
|
||||
}
|
||||
217
src/client/conversation-list/conversation-list-model.vala
Normal file
217
src/client/conversation-list/conversation-list-model.vala
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// The whole goal of this class to wrap the ConversationMonitor with a view that presents a sorted list
|
||||
public class ConversationList.Model : Object, ListModel {
|
||||
internal GLib.GenericArray<Geary.App.Conversation> items = new GLib.GenericArray<Geary.App.Conversation>();
|
||||
internal Geary.App.ConversationMonitor monitor { get; set; }
|
||||
|
||||
private bool scanning = false;
|
||||
|
||||
internal Model(Geary.App.ConversationMonitor monitor) {
|
||||
this.monitor = monitor;
|
||||
|
||||
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();
|
||||
public signal void conversation_updated(Geary.App.Conversation convo);
|
||||
|
||||
private static int compare(Object a, Object b) {
|
||||
return Util.Email.compare_conversation_descending(a as Geary.App.Conversation, b as Geary.App.Conversation);
|
||||
}
|
||||
|
||||
// ------------------------
|
||||
// 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;
|
||||
GLib.Timeout.add(100, () => {
|
||||
if (!this.scanning) {
|
||||
conversations_loaded();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public bool load_more(int amount) {
|
||||
if (this.scanning) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.monitor.min_window_count += amount;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ------------------------
|
||||
// Model
|
||||
// ------------------------
|
||||
|
||||
public Object? get_item(uint position) {
|
||||
return this.items.get(position);
|
||||
}
|
||||
|
||||
public Type get_item_type() {
|
||||
return typeof(Geary.App.Conversation);
|
||||
}
|
||||
|
||||
public uint get_n_items() {
|
||||
return this.items.length;
|
||||
}
|
||||
|
||||
private bool insert_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;
|
||||
}
|
||||
|
||||
this.items.add(convo);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private GenericArray<uint> conversations_indexes(Gee.Collection<Geary.App.Conversation> conversations) {
|
||||
GenericArray<uint> indexes = new GenericArray<uint>();
|
||||
uint index;
|
||||
|
||||
foreach (Geary.App.Conversation convo in conversations) {
|
||||
if (this.items.find(convo, out index)) {
|
||||
indexes.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
return indexes;
|
||||
}
|
||||
|
||||
private void update_added(GenericArray<uint> indexes) {
|
||||
indexes.sort((a, b) => {
|
||||
return (int) (a > b) - (int) (a < b);
|
||||
});
|
||||
|
||||
while (indexes.length > 0) {
|
||||
uint? last_index = null;
|
||||
uint count = 0;
|
||||
foreach (unowned uint index in indexes) {
|
||||
if (last_index != null && index > last_index + 1) {
|
||||
break;
|
||||
}
|
||||
last_index = (int) index;
|
||||
count++;
|
||||
}
|
||||
this.items_changed(indexes[0], 0, count);
|
||||
indexes.remove_range(0, count);
|
||||
}
|
||||
}
|
||||
|
||||
private void update_removed(GenericArray<uint> indexes) {
|
||||
indexes.sort((a, b) => {
|
||||
return (int) (a < b) - (int) (a > b);
|
||||
});
|
||||
|
||||
while (indexes.length > 0) {
|
||||
uint? last_index = null;
|
||||
uint count = 0;
|
||||
foreach (unowned uint index in indexes) {
|
||||
if (last_index != null && index < last_index - 1) {
|
||||
break;
|
||||
}
|
||||
last_index = index;
|
||||
count++;
|
||||
}
|
||||
this.items_changed(last_index, count, 0);
|
||||
indexes.remove_range(0, count);
|
||||
}
|
||||
}
|
||||
|
||||
private void on_conversations_added(Gee.Collection<Geary.App.Conversation> conversations) {
|
||||
debug("Adding %d conversations.", conversations.size);
|
||||
|
||||
conversations_added(true);
|
||||
|
||||
var added = 0;
|
||||
foreach (Geary.App.Conversation convo in conversations) {
|
||||
if (insert_conversation(convo)) {
|
||||
added++;
|
||||
}
|
||||
}
|
||||
this.items.sort(compare);
|
||||
|
||||
GenericArray<uint> indexes = conversations_indexes(conversations);
|
||||
update_added(indexes);
|
||||
|
||||
conversations_added(false);
|
||||
|
||||
debug("Added %d/%d conversations.", added, conversations.size);
|
||||
}
|
||||
|
||||
private void on_conversations_removed(Gee.Collection<Geary.App.Conversation> conversations) {
|
||||
GenericArray<uint> indexes = conversations_indexes(conversations);
|
||||
|
||||
debug("Removing %d conversations.", conversations.size);
|
||||
|
||||
conversations_removed(true);
|
||||
|
||||
var removed = 0;
|
||||
foreach (Geary.App.Conversation convo in conversations) {
|
||||
this.items.remove(convo);
|
||||
removed++;
|
||||
}
|
||||
|
||||
update_removed(indexes);
|
||||
|
||||
conversations_removed(false);
|
||||
|
||||
debug("Removed %ld/%d conversations.", removed, conversations.size);
|
||||
}
|
||||
|
||||
private void on_conversation_updated(Geary.App.ConversationMonitor sender, Geary.App.Conversation convo, Gee.Collection<Geary.Email> emails) {
|
||||
conversation_updated(convo);
|
||||
|
||||
uint initial_index;
|
||||
if (!this.items.find(convo, out initial_index)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.items.sort(compare);
|
||||
|
||||
uint final_index;
|
||||
if (!this.items.find(convo, out final_index) || initial_index == final_index) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint count = initial_index > final_index ?
|
||||
initial_index + 1 - final_index :
|
||||
final_index + 1 - initial_index;
|
||||
this.items_changed(uint.min(initial_index, final_index), count, count);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
212
src/client/conversation-list/conversation-list-row.vala
Normal file
212
src/client/conversation-list/conversation-list-row.vala
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* 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 signal void toggle_selection(ConversationList.Row row,
|
||||
bool active);
|
||||
|
||||
internal Row(Application.Configuration config,
|
||||
Geary.App.Conversation conversation,
|
||||
bool selection_mode_enabled) {
|
||||
this.config = config;
|
||||
this.conversation = conversation;
|
||||
|
||||
conversation.email_flags_changed.connect(update_flags);
|
||||
|
||||
config.bind(Application.Configuration.DISPLAY_PREVIEW_KEY,
|
||||
this.preview_row, "visible");
|
||||
|
||||
if (selection_mode_enabled) {
|
||||
set_selection_enabled(true);
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
internal void update() {
|
||||
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();
|
||||
}
|
||||
|
||||
update_flags(null);
|
||||
|
||||
}
|
||||
|
||||
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 = (Gtk.StateFlags.SELECTED in this.get_state_flags());
|
||||
|
||||
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);
|
||||
toggle_selection(this, this.selected_button.get_active());
|
||||
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()) {
|
||||
get_style_context().add_class("starred");
|
||||
flagged_icon.set_from_icon_name("starred-symbolic", Gtk.IconSize.BUTTON);
|
||||
} else {
|
||||
get_style_context().remove_class("starred");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,494 +0,0 @@
|
|||
/* Copyright 2016 Software Freedom Conservancy Inc.
|
||||
*
|
||||
* This software is licensed under the GNU Lesser General Public License
|
||||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A Gtk.ListStore of sorted {@link Geary.App.Conversation}s.
|
||||
*
|
||||
* Conversations are sorted by {@link Geary.EmailProperties.date_received} (IMAP's INTERNALDATE)
|
||||
* rather than the Date: header, as that ensures newly received email sort to the top where the
|
||||
* user expects to see them. The ConversationViewer sorts by the Date: header, as that presents
|
||||
* better to the user.
|
||||
*/
|
||||
|
||||
public class ConversationListStore : Gtk.ListStore {
|
||||
|
||||
public const Geary.Email.Field REQUIRED_FIELDS = (
|
||||
Geary.Email.Field.ENVELOPE |
|
||||
Geary.Email.Field.FLAGS |
|
||||
Geary.Email.Field.PROPERTIES
|
||||
);
|
||||
|
||||
// XXX Remove REQUIRED_FOR_BODY when PREVIEW has been fixed. See Bug 714317.
|
||||
public const Geary.Email.Field WITH_PREVIEW_FIELDS = (
|
||||
REQUIRED_FIELDS |
|
||||
Geary.Email.Field.PREVIEW |
|
||||
Geary.Email.REQUIRED_FOR_MESSAGE
|
||||
);
|
||||
|
||||
public enum Column {
|
||||
CONVERSATION_DATA,
|
||||
CONVERSATION_OBJECT,
|
||||
ROW_WRAPPER;
|
||||
|
||||
public static Type[] get_types() {
|
||||
return {
|
||||
typeof (FormattedConversationData), // CONVERSATION_DATA
|
||||
typeof (Geary.App.Conversation), // CONVERSATION_OBJECT
|
||||
typeof (RowWrapper) // ROW_WRAPPER
|
||||
};
|
||||
}
|
||||
|
||||
public string to_string() {
|
||||
switch (this) {
|
||||
case CONVERSATION_DATA:
|
||||
return "data";
|
||||
|
||||
case CONVERSATION_OBJECT:
|
||||
return "envelope";
|
||||
|
||||
case ROW_WRAPPER:
|
||||
return "wrapper";
|
||||
|
||||
default:
|
||||
assert_not_reached();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class RowWrapper : Geary.BaseObject {
|
||||
public Geary.App.Conversation conversation;
|
||||
public Gtk.TreeRowReference row;
|
||||
|
||||
public RowWrapper(Gtk.TreeModel model, Geary.App.Conversation conversation, Gtk.TreePath path) {
|
||||
this.conversation = conversation;
|
||||
this.row = new Gtk.TreeRowReference(model, path);
|
||||
}
|
||||
|
||||
public Gtk.TreePath get_path() {
|
||||
return row.get_path();
|
||||
}
|
||||
|
||||
public bool get_iter(out Gtk.TreeIter iter) {
|
||||
return row.get_model().get_iter(out iter, get_path());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static int sort_by_date(Gtk.TreeModel model,
|
||||
Gtk.TreeIter aiter,
|
||||
Gtk.TreeIter biter) {
|
||||
Geary.App.Conversation a, b;
|
||||
model.get(aiter, Column.CONVERSATION_OBJECT, out a);
|
||||
model.get(biter, Column.CONVERSATION_OBJECT, out b);
|
||||
return Util.Email.compare_conversation_ascending(a, b);
|
||||
}
|
||||
|
||||
|
||||
public Geary.App.ConversationMonitor conversations { get; set; }
|
||||
public Geary.ProgressMonitor preview_monitor { get; private set; default =
|
||||
new Geary.SimpleProgressMonitor(Geary.ProgressType.ACTIVITY); }
|
||||
|
||||
private Application.Configuration config;
|
||||
|
||||
private Gee.HashMap<Geary.App.Conversation, RowWrapper> row_map = new Gee.HashMap<
|
||||
Geary.App.Conversation, RowWrapper>();
|
||||
private Geary.App.EmailStore? email_store = null;
|
||||
private Cancellable cancellable = new Cancellable();
|
||||
private bool loading_local_only = true;
|
||||
private Geary.Nonblocking.Mutex refresh_mutex = new Geary.Nonblocking.Mutex();
|
||||
|
||||
public signal void conversations_added(bool start);
|
||||
public signal void conversations_removed(bool start);
|
||||
|
||||
public ConversationListStore(Geary.App.ConversationMonitor conversations,
|
||||
Application.Configuration config) {
|
||||
set_column_types(Column.get_types());
|
||||
set_default_sort_func(ConversationListStore.sort_by_date);
|
||||
set_sort_column_id(Gtk.SortColumn.DEFAULT, Gtk.SortType.DESCENDING);
|
||||
|
||||
this.conversations = conversations;
|
||||
this.email_store = new Geary.App.EmailStore(
|
||||
conversations.base_folder.account
|
||||
);
|
||||
this.config = config;
|
||||
this.config.settings.changed[
|
||||
Application.Configuration.DISPLAY_PREVIEW_KEY
|
||||
].connect(on_display_preview_changed);
|
||||
|
||||
conversations.scan_completed.connect(on_scan_completed);
|
||||
conversations.conversations_added.connect(on_conversations_added);
|
||||
conversations.conversations_removed.connect(on_conversations_removed);
|
||||
conversations.conversation_appended.connect(on_conversation_appended);
|
||||
conversations.conversation_trimmed.connect(on_conversation_trimmed);
|
||||
conversations.email_flags_changed.connect(on_email_flags_changed);
|
||||
|
||||
// add all existing conversations
|
||||
on_conversations_added(conversations.read_only_view);
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
this.cancellable.cancel();
|
||||
this.email_store = null;
|
||||
clear();
|
||||
|
||||
// Release circular refs.
|
||||
this.row_map.clear();
|
||||
}
|
||||
|
||||
public void update_display() {
|
||||
this.foreach(update_date_string);
|
||||
}
|
||||
|
||||
public Geary.App.Conversation? get_conversation_at_path(Gtk.TreePath path) {
|
||||
Gtk.TreeIter iter;
|
||||
if (!get_iter(out iter, path))
|
||||
return null;
|
||||
|
||||
return get_conversation_at_iter(iter);
|
||||
}
|
||||
|
||||
private async void refresh_previews_async(Geary.App.ConversationMonitor conversation_monitor) {
|
||||
// Use a mutex because it's possible for the conversation monitor to fire multiple
|
||||
// "scan-started" signals as messages come in fast and furious, but only want to process
|
||||
// previews one at a time, otherwise it's possible to issue multiple requests for the
|
||||
// same set
|
||||
int token;
|
||||
try {
|
||||
token = yield refresh_mutex.claim_async(this.cancellable);
|
||||
} catch (Error err) {
|
||||
debug("Unable to claim refresh mutex: %s", err.message);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
preview_monitor.notify_start();
|
||||
|
||||
yield do_refresh_previews_async(conversation_monitor);
|
||||
|
||||
preview_monitor.notify_finish();
|
||||
|
||||
try {
|
||||
refresh_mutex.release(ref token);
|
||||
} catch (Error err) {
|
||||
debug("Unable to release refresh mutex: %s", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// should only be called by refresh_previews_async()
|
||||
private async void do_refresh_previews_async(Geary.App.ConversationMonitor conversation_monitor) {
|
||||
if (conversation_monitor == null || !this.config.display_preview)
|
||||
return;
|
||||
|
||||
Gee.Set<Geary.EmailIdentifier> needing_previews = get_emails_needing_previews();
|
||||
|
||||
Gee.ArrayList<Geary.Email> emails = new Gee.ArrayList<Geary.Email>();
|
||||
if (needing_previews.size > 0)
|
||||
emails.add_all(yield do_get_previews_async(needing_previews));
|
||||
if (emails.size < 1)
|
||||
return;
|
||||
|
||||
foreach (Geary.Email email in emails) {
|
||||
Geary.App.Conversation? conversation = conversation_monitor.get_by_email_identifier(email.id);
|
||||
// The conversation can be null if e.g. a search is
|
||||
// changing quickly and the original has evaporated
|
||||
// already.
|
||||
if (conversation != null) {
|
||||
set_preview_for_conversation(conversation, email);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Gee.Collection<Geary.Email> do_get_previews_async(
|
||||
Gee.Collection<Geary.EmailIdentifier> emails_needing_previews) {
|
||||
Geary.Folder.ListFlags flags = (loading_local_only) ? Geary.Folder.ListFlags.LOCAL_ONLY
|
||||
: Geary.Folder.ListFlags.NONE;
|
||||
Gee.Collection<Geary.Email>? emails = null;
|
||||
try {
|
||||
emails = yield email_store.list_email_by_sparse_id_async(emails_needing_previews,
|
||||
ConversationListStore.WITH_PREVIEW_FIELDS, flags, cancellable);
|
||||
} catch (GLib.IOError.CANCELLED err) {
|
||||
// All good
|
||||
} catch (Geary.EngineError.NOT_FOUND err) {
|
||||
// All good also, as that's entirely possible when waiting
|
||||
// for the remote to open
|
||||
} catch (GLib.Error err) {
|
||||
warning("Unable to fetch preview: %s", err.message);
|
||||
}
|
||||
|
||||
return emails ?? new Gee.ArrayList<Geary.Email>();
|
||||
}
|
||||
|
||||
private Gee.Set<Geary.EmailIdentifier> get_emails_needing_previews() {
|
||||
Gee.Set<Geary.EmailIdentifier> needing = new Gee.HashSet<Geary.EmailIdentifier>();
|
||||
|
||||
// sort the conversations so the previews are fetched from the newest to the oldest, matching
|
||||
// the user experience
|
||||
var sorted_conversations = Geary.traverse(
|
||||
this.conversations.read_only_view
|
||||
).to_sorted_list(
|
||||
Util.Email.compare_conversation_descending
|
||||
);
|
||||
foreach (Geary.App.Conversation conversation in sorted_conversations) {
|
||||
// find oldest unread message for the preview
|
||||
Geary.Email? need_preview = null;
|
||||
foreach (Geary.Email email in conversation.get_emails(Geary.App.Conversation.Ordering.RECV_DATE_ASCENDING)) {
|
||||
if (email.email_flags.is_unread()) {
|
||||
need_preview = email;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if all are read, use newest in-folder message, then newest out-of-folder if not
|
||||
// present
|
||||
if (need_preview == null) {
|
||||
need_preview = conversation.get_latest_recv_email(Geary.App.Conversation.Location.IN_FOLDER_OUT_OF_FOLDER);
|
||||
if (need_preview == null)
|
||||
continue;
|
||||
}
|
||||
|
||||
Geary.Email? current_preview = get_preview_for_conversation(conversation);
|
||||
|
||||
// if all preview fields present and it's the same email, don't need to refresh
|
||||
if (current_preview != null
|
||||
&& need_preview.id.equal_to(current_preview.id)
|
||||
&& current_preview.fields.is_all_set(ConversationListStore.WITH_PREVIEW_FIELDS)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
needing.add(need_preview.id);
|
||||
}
|
||||
|
||||
return needing;
|
||||
}
|
||||
|
||||
private Geary.Email? get_preview_for_conversation(Geary.App.Conversation conversation) {
|
||||
Gtk.TreeIter iter;
|
||||
if (!get_iter_for_conversation(conversation, out iter)) {
|
||||
debug("Unable to find preview for conversation");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
FormattedConversationData? message_data = get_message_data_at_iter(iter);
|
||||
return message_data == null ? null : message_data.preview;
|
||||
}
|
||||
|
||||
private void set_preview_for_conversation(Geary.App.Conversation conversation, Geary.Email preview) {
|
||||
Gtk.TreeIter iter;
|
||||
if (get_iter_for_conversation(conversation, out iter))
|
||||
set_row(iter, conversation, preview);
|
||||
else
|
||||
debug("Unable to find preview for conversation");
|
||||
}
|
||||
|
||||
private void set_row(Gtk.TreeIter iter, Geary.App.Conversation conversation, Geary.Email preview) {
|
||||
FormattedConversationData conversation_data = new FormattedConversationData(
|
||||
this.config,
|
||||
conversation,
|
||||
preview,
|
||||
this.conversations.base_folder.account.information.sender_mailboxes
|
||||
);
|
||||
|
||||
Gtk.TreePath? path = get_path(iter);
|
||||
assert(path != null);
|
||||
RowWrapper wrapper = new RowWrapper(this, conversation, path);
|
||||
|
||||
set(iter,
|
||||
Column.CONVERSATION_DATA, conversation_data,
|
||||
Column.CONVERSATION_OBJECT, conversation,
|
||||
Column.ROW_WRAPPER, wrapper
|
||||
);
|
||||
|
||||
row_map.set(conversation, wrapper);
|
||||
}
|
||||
|
||||
private void refresh_conversation(Geary.App.Conversation conversation) {
|
||||
Gtk.TreeIter iter;
|
||||
if (!get_iter_for_conversation(conversation, out iter)) {
|
||||
// Unknown conversation, attempt to append it.
|
||||
add_conversation(conversation);
|
||||
return;
|
||||
}
|
||||
|
||||
Geary.Email? last_email = conversation.get_latest_recv_email(Geary.App.Conversation.Location.ANYWHERE);
|
||||
if (last_email == null) {
|
||||
debug("Cannot refresh conversation: last email is null");
|
||||
|
||||
#if VALA_0_36
|
||||
remove(ref iter);
|
||||
#else
|
||||
remove(iter);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
set_row(iter, conversation, last_email);
|
||||
|
||||
Gtk.TreePath? path = get_path(iter);
|
||||
if (path != null)
|
||||
row_changed(path, iter);
|
||||
else
|
||||
debug("Cannot refresh conversation: no path for iterator");
|
||||
}
|
||||
|
||||
private void refresh_flags(Geary.App.Conversation conversation) {
|
||||
Gtk.TreeIter iter;
|
||||
if (!get_iter_for_conversation(conversation, out iter)) {
|
||||
// Unknown conversation, attempt to append it.
|
||||
add_conversation(conversation);
|
||||
return;
|
||||
}
|
||||
|
||||
FormattedConversationData? existing_message_data = get_message_data_at_iter(iter);
|
||||
if (existing_message_data == null)
|
||||
return;
|
||||
|
||||
existing_message_data.is_unread = conversation.is_unread();
|
||||
existing_message_data.is_flagged = conversation.is_flagged();
|
||||
|
||||
Gtk.TreePath? path = get_path(iter);
|
||||
if (path != null)
|
||||
row_changed(path, iter);
|
||||
}
|
||||
|
||||
public Gtk.TreePath? get_path_for_conversation(Geary.App.Conversation conversation) {
|
||||
RowWrapper? wrapper = row_map.get(conversation);
|
||||
|
||||
return (wrapper != null) ? wrapper.get_path() : null;
|
||||
}
|
||||
|
||||
private bool get_iter_for_conversation(Geary.App.Conversation conversation, out Gtk.TreeIter iter) {
|
||||
RowWrapper? wrapper = row_map.get(conversation);
|
||||
if (wrapper != null)
|
||||
return wrapper.get_iter(out iter);
|
||||
|
||||
// use get_iter_first() because boxing Gtk.TreeIter with a nullable is problematic with
|
||||
// current bindings
|
||||
get_iter_first(out iter);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool has_conversation(Geary.App.Conversation conversation) {
|
||||
return row_map.has_key(conversation);
|
||||
}
|
||||
|
||||
private Geary.App.Conversation? get_conversation_at_iter(Gtk.TreeIter iter) {
|
||||
Geary.App.Conversation? conversation;
|
||||
get(iter, Column.CONVERSATION_OBJECT, out conversation);
|
||||
|
||||
return conversation;
|
||||
}
|
||||
|
||||
private FormattedConversationData? get_message_data_at_iter(Gtk.TreeIter iter) {
|
||||
FormattedConversationData? message_data;
|
||||
get(iter, Column.CONVERSATION_DATA, out message_data);
|
||||
|
||||
return message_data;
|
||||
}
|
||||
|
||||
private void remove_conversation(Geary.App.Conversation conversation) {
|
||||
Gtk.TreeIter iter;
|
||||
if (get_iter_for_conversation(conversation, out iter))
|
||||
#if VALA_0_36
|
||||
remove(ref iter);
|
||||
#else
|
||||
remove(iter);
|
||||
#endif
|
||||
|
||||
row_map.unset(conversation);
|
||||
}
|
||||
|
||||
private bool add_conversation(Geary.App.Conversation conversation) {
|
||||
Geary.Email? last_email = conversation.get_latest_recv_email(Geary.App.Conversation.Location.ANYWHERE);
|
||||
if (last_email == null) {
|
||||
debug("Cannot add conversation: last email is null");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (has_conversation(conversation)) {
|
||||
debug("Conversation already present; not adding");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Gtk.TreeIter iter;
|
||||
append(out iter);
|
||||
set_row(iter, conversation, last_email);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void on_scan_completed(Geary.App.ConversationMonitor sender) {
|
||||
refresh_previews_async.begin(sender);
|
||||
loading_local_only = false;
|
||||
}
|
||||
|
||||
private void on_conversations_added(Gee.Collection<Geary.App.Conversation> conversations) {
|
||||
// this handler is used to initialize the display, so it's possible for an empty list to
|
||||
// be passed in (the ConversationMonitor signal should never do this)
|
||||
if (conversations.size == 0)
|
||||
return;
|
||||
|
||||
conversations_added(true);
|
||||
|
||||
debug("Adding %d conversations.", conversations.size);
|
||||
int added = 0;
|
||||
foreach (Geary.App.Conversation conversation in conversations) {
|
||||
if (add_conversation(conversation))
|
||||
added++;
|
||||
}
|
||||
debug("Added %d/%d conversations.", added, conversations.size);
|
||||
|
||||
conversations_added(false);
|
||||
}
|
||||
|
||||
private void on_conversations_removed(Gee.Collection<Geary.App.Conversation> conversations) {
|
||||
conversations_removed(true);
|
||||
foreach (Geary.App.Conversation removed in conversations)
|
||||
remove_conversation(removed);
|
||||
conversations_removed(false);
|
||||
}
|
||||
|
||||
private void on_conversation_appended(Geary.App.Conversation conversation) {
|
||||
if (has_conversation(conversation)) {
|
||||
refresh_conversation(conversation);
|
||||
} else {
|
||||
add_conversation(conversation);
|
||||
}
|
||||
}
|
||||
|
||||
private void on_conversation_trimmed(Geary.App.Conversation conversation) {
|
||||
refresh_conversation(conversation);
|
||||
}
|
||||
|
||||
private void on_display_preview_changed() {
|
||||
refresh_previews_async.begin(this.conversations);
|
||||
}
|
||||
|
||||
private void on_email_flags_changed(Geary.App.Conversation conversation) {
|
||||
refresh_flags(conversation);
|
||||
|
||||
// refresh previews because the oldest unread message is displayed as the preview, and if
|
||||
// that's changed, need to change the preview
|
||||
// TODO: need support code to load preview for single conversation, not scan all
|
||||
refresh_previews_async.begin(this.conversations);
|
||||
}
|
||||
|
||||
private bool update_date_string(Gtk.TreeModel model, Gtk.TreePath path, Gtk.TreeIter iter) {
|
||||
FormattedConversationData? message_data;
|
||||
model.get(iter, Column.CONVERSATION_DATA, out message_data);
|
||||
|
||||
if (message_data != null && message_data.update_date_string())
|
||||
row_changed(path, iter);
|
||||
|
||||
// Continue iterating, don't stop
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,476 +0,0 @@
|
|||
/* Copyright 2016 Software Freedom Conservancy Inc.
|
||||
*
|
||||
* This software is licensed under the GNU Lesser General Public License
|
||||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
// Stores formatted data for a message.
|
||||
public class FormattedConversationData : Geary.BaseObject {
|
||||
struct Participants {
|
||||
string? markup;
|
||||
|
||||
// markup may look different depending on whether widget is selected
|
||||
bool was_widget_selected;
|
||||
}
|
||||
|
||||
public const int SPACING = 6;
|
||||
|
||||
private const string ME = _("Me");
|
||||
private const string STYLE_EXAMPLE = "Gg"; // Use both upper and lower case to get max height.
|
||||
private const int TEXT_LEFT = SPACING * 2 + IconFactory.UNREAD_ICON_SIZE;
|
||||
private const double DIM_TEXT_AMOUNT = 0.05;
|
||||
private const double DIM_PREVIEW_TEXT_AMOUNT = 0.25;
|
||||
|
||||
|
||||
private class ParticipantDisplay : Geary.BaseObject, Gee.Hashable<ParticipantDisplay> {
|
||||
public Geary.RFC822.MailboxAddress address;
|
||||
public bool is_unread;
|
||||
|
||||
public ParticipantDisplay(Geary.RFC822.MailboxAddress address, bool is_unread) {
|
||||
this.address = address;
|
||||
this.is_unread = is_unread;
|
||||
}
|
||||
|
||||
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 (is_unread) {
|
||||
markup = "<b>%s</b>".printf(markup);
|
||||
}
|
||||
|
||||
if (this.address.is_spoofed()) {
|
||||
markup = "<s>%s</s>".printf(markup);
|
||||
}
|
||||
|
||||
return markup;
|
||||
}
|
||||
|
||||
public bool equal_to(ParticipantDisplay other) {
|
||||
return address.equal_to(other.address)
|
||||
&& address.name == other.address.name;
|
||||
}
|
||||
|
||||
public uint hash() {
|
||||
return address.hash();
|
||||
}
|
||||
}
|
||||
|
||||
private static int cell_height = -1;
|
||||
private static int preview_height = -1;
|
||||
|
||||
public bool is_unread { get; set; }
|
||||
public bool is_flagged { get; set; }
|
||||
public string date { get; private set; }
|
||||
public string? body { get; private set; default = null; } // optional
|
||||
public int num_emails { get; set; }
|
||||
public Geary.Email? preview { get; private set; default = null; }
|
||||
|
||||
private Application.Configuration config;
|
||||
|
||||
private Gtk.Settings? gtk;
|
||||
private Pango.FontDescription font;
|
||||
|
||||
private Geary.App.Conversation? conversation = null;
|
||||
private Gee.List<Geary.RFC822.MailboxAddress>? account_owner_emails = null;
|
||||
private bool use_to = true;
|
||||
private CountBadge count_badge = new CountBadge(2);
|
||||
private string subject_html_escaped;
|
||||
private Participants participants = Participants(){markup = null};
|
||||
|
||||
// Creates a formatted message data from an e-mail.
|
||||
public FormattedConversationData(Application.Configuration config,
|
||||
Geary.App.Conversation conversation,
|
||||
Geary.Email preview,
|
||||
Gee.List<Geary.RFC822.MailboxAddress> account_owner_emails) {
|
||||
this.config = config;
|
||||
this.gtk = Gtk.Settings.get_default();
|
||||
this.conversation = conversation;
|
||||
this.account_owner_emails = account_owner_emails;
|
||||
this.use_to = conversation.base_folder.used_as.is_outgoing();
|
||||
|
||||
this.gtk.notify["gtk-font-name"].connect(this.update_font);
|
||||
update_font();
|
||||
|
||||
// Load preview-related data.
|
||||
update_date_string();
|
||||
this.subject_html_escaped
|
||||
= Geary.HTML.escape_markup(Util.Email.strip_subject_prefixes(preview));
|
||||
this.body = Geary.String.reduce_whitespace(preview.get_preview_as_string());
|
||||
this.preview = preview;
|
||||
|
||||
// Load conversation-related data.
|
||||
this.is_unread = conversation.is_unread();
|
||||
this.is_flagged = conversation.is_flagged();
|
||||
this.num_emails = conversation.get_count();
|
||||
|
||||
// todo: instead of clearing the cache update it
|
||||
this.conversation.appended.connect(clear_participants_cache);
|
||||
this.conversation.trimmed.connect(clear_participants_cache);
|
||||
this.conversation.email_flags_changed.connect(clear_participants_cache);
|
||||
}
|
||||
|
||||
// Creates an example message (used internally for styling calculations.)
|
||||
public FormattedConversationData.create_example(Application.Configuration config) {
|
||||
this.config = config;
|
||||
this.is_unread = false;
|
||||
this.is_flagged = false;
|
||||
this.date = STYLE_EXAMPLE;
|
||||
this.subject_html_escaped = STYLE_EXAMPLE;
|
||||
this.body = STYLE_EXAMPLE + "\n" + STYLE_EXAMPLE;
|
||||
this.num_emails = 1;
|
||||
|
||||
this.font = Pango.FontDescription.from_string(
|
||||
this.config.gnome_interface.get_string("font-name")
|
||||
);
|
||||
}
|
||||
|
||||
private void clear_participants_cache(Geary.Email email) {
|
||||
participants.markup = null;
|
||||
}
|
||||
|
||||
public bool update_date_string() {
|
||||
// get latest email *in folder* for the conversation's date, fall back on out-of-folder
|
||||
Geary.Email? latest = conversation.get_latest_recv_email(Geary.App.Conversation.Location.IN_FOLDER_OUT_OF_FOLDER);
|
||||
if (latest == null || latest.properties == null)
|
||||
return false;
|
||||
|
||||
// conversation list store sorts by date-received, so display that instead of sender's
|
||||
// Date:
|
||||
string new_date = Util.Date.pretty_print(
|
||||
latest.properties.date_received.to_local(),
|
||||
this.config.clock_format
|
||||
);
|
||||
if (new_date == date)
|
||||
return false;
|
||||
|
||||
date = new_date;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private uint8 gdk_to_rgb(double gdk) {
|
||||
return (uint8) (gdk.clamp(0.0, 1.0) * 255.0);
|
||||
}
|
||||
|
||||
private Gdk.RGBA dim_rgba(Gdk.RGBA rgba, double amount) {
|
||||
amount = amount.clamp(0.0, 1.0);
|
||||
|
||||
// can't use ternary in struct initializer due to this bug:
|
||||
// https://bugzilla.gnome.org/show_bug.cgi?id=684742
|
||||
double dim_red = (rgba.red >= 0.5) ? -amount : amount;
|
||||
double dim_green = (rgba.green >= 0.5) ? -amount : amount;
|
||||
double dim_blue = (rgba.blue >= 0.5) ? -amount : amount;
|
||||
|
||||
return Gdk.RGBA() {
|
||||
red = (rgba.red + dim_red).clamp(0.0, 1.0),
|
||||
green = (rgba.green + dim_green).clamp(0.0, 1.0),
|
||||
blue = (rgba.blue + dim_blue).clamp(0.0, 1.0),
|
||||
alpha = rgba.alpha
|
||||
};
|
||||
}
|
||||
|
||||
private string rgba_to_markup(Gdk.RGBA rgba) {
|
||||
return "#%02x%02x%02x".printf(
|
||||
gdk_to_rgb(rgba.red), gdk_to_rgb(rgba.green), gdk_to_rgb(rgba.blue));
|
||||
}
|
||||
|
||||
private Gdk.RGBA get_foreground_rgba(Gtk.Widget widget, bool selected) {
|
||||
// Do the https://bugzilla.gnome.org/show_bug.cgi?id=763796 dance
|
||||
Gtk.StyleContext context = widget.get_style_context();
|
||||
context.save();
|
||||
context.set_state(
|
||||
selected ? Gtk.StateFlags.SELECTED : Gtk.StateFlags.NORMAL
|
||||
);
|
||||
Gdk.RGBA colour = context.get_color(context.get_state());
|
||||
context.restore();
|
||||
return colour;
|
||||
}
|
||||
|
||||
private string get_participants_markup(Gtk.Widget widget, bool selected) {
|
||||
if (participants.markup != null && participants.was_widget_selected == selected)
|
||||
return participants.markup;
|
||||
|
||||
if (conversation == null || account_owner_emails == null || account_owner_emails.size == 0)
|
||||
return "";
|
||||
|
||||
// Build chronological list of unique AuthorDisplay records, setting to
|
||||
// unread if any message by that author is unread
|
||||
Gee.ArrayList<ParticipantDisplay> list = new Gee.ArrayList<ParticipantDisplay>();
|
||||
foreach (Geary.Email message in conversation.get_emails(Geary.App.Conversation.Ordering.RECV_DATE_ASCENDING)) {
|
||||
// only display if something to display
|
||||
Geary.RFC822.MailboxAddresses? addresses = use_to
|
||||
? new Geary.RFC822.MailboxAddresses.single(Util.Email.get_primary_originator(message))
|
||||
: message.from;
|
||||
if (addresses == null || addresses.size < 1)
|
||||
continue;
|
||||
|
||||
foreach (Geary.RFC822.MailboxAddress address in addresses) {
|
||||
ParticipantDisplay participant_display = new ParticipantDisplay(address,
|
||||
message.email_flags.is_unread());
|
||||
|
||||
int existing_index = list.index_of(participant_display);
|
||||
if (existing_index < 0) {
|
||||
list.add(participant_display);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// if present and this message is unread but the prior were read,
|
||||
// this author is now unread
|
||||
if (message.email_flags.is_unread())
|
||||
list[existing_index].is_unread = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (list.size == 1) {
|
||||
// if only one participant, use full name
|
||||
participants.markup = "<span foreground='%s'>%s</span>"
|
||||
.printf(rgba_to_markup(get_foreground_rgba(widget, selected)),
|
||||
list[0].get_full_markup(account_owner_emails));
|
||||
} else {
|
||||
StringBuilder builder = new StringBuilder("<span foreground='%s'>".printf(
|
||||
rgba_to_markup(get_foreground_rgba(widget, selected))));
|
||||
bool first = true;
|
||||
foreach (ParticipantDisplay participant in list) {
|
||||
if (!first)
|
||||
builder.append(", ");
|
||||
|
||||
builder.append(participant.get_short_markup(account_owner_emails));
|
||||
first = false;
|
||||
}
|
||||
builder.append("</span>");
|
||||
participants.markup = builder.str;
|
||||
}
|
||||
participants.was_widget_selected = selected;
|
||||
return participants.markup;
|
||||
}
|
||||
|
||||
public void render(Cairo.Context ctx, Gtk.Widget widget, Gdk.Rectangle background_area,
|
||||
Gdk.Rectangle cell_area, Gtk.CellRendererState flags, bool hover_select) {
|
||||
render_internal(widget, cell_area, ctx, flags, false, hover_select);
|
||||
}
|
||||
|
||||
// Call this on style changes.
|
||||
public void calculate_sizes(Gtk.Widget widget) {
|
||||
render_internal(widget, null, null, 0, true, false);
|
||||
}
|
||||
|
||||
// Must call calculate_sizes() first.
|
||||
public int get_height() {
|
||||
assert(cell_height != -1); // ensures calculate_sizes() was called.
|
||||
return cell_height;
|
||||
}
|
||||
|
||||
// Can be used for rendering or calculating height.
|
||||
private void render_internal(Gtk.Widget widget, Gdk.Rectangle? cell_area,
|
||||
Cairo.Context? ctx, Gtk.CellRendererState flags, bool recalc_dims,
|
||||
bool hover_select) {
|
||||
bool display_preview = this.config.display_preview;
|
||||
int y = SPACING + (cell_area != null ? cell_area.y : 0);
|
||||
|
||||
bool selected = (flags & Gtk.CellRendererState.SELECTED) != 0;
|
||||
bool hover = (flags & Gtk.CellRendererState.PRELIT) != 0 || (selected && hover_select);
|
||||
|
||||
// Date field.
|
||||
Pango.Rectangle ink_rect = render_date(widget, cell_area, ctx, y, selected);
|
||||
|
||||
// From field.
|
||||
ink_rect = render_from(widget, cell_area, ctx, y, selected, ink_rect);
|
||||
y += ink_rect.height + ink_rect.y + SPACING;
|
||||
|
||||
// If we are displaying a preview then the message counter goes on the same line as the
|
||||
// preview, otherwise it is with the subject.
|
||||
int preview_height = 0;
|
||||
|
||||
// Setup counter badge.
|
||||
count_badge.count = num_emails;
|
||||
int counter_width = count_badge.get_width(widget) + SPACING;
|
||||
int counter_x = cell_area != null ? cell_area.width - cell_area.x - counter_width +
|
||||
(SPACING / 2) : 0;
|
||||
|
||||
if (display_preview) {
|
||||
// Subject field.
|
||||
render_subject(widget, cell_area, ctx, y, selected);
|
||||
y += ink_rect.height + ink_rect.y + (SPACING / 2);
|
||||
|
||||
// Number of e-mails field.
|
||||
count_badge.render(widget, ctx, counter_x, y + (SPACING / 2), selected);
|
||||
|
||||
// Body preview.
|
||||
ink_rect = render_preview(widget, cell_area, ctx, y, selected, counter_width);
|
||||
preview_height = ink_rect.height + ink_rect.y + (int) (SPACING * 1.2);
|
||||
} else {
|
||||
// Number of e-mails field.
|
||||
count_badge.render(widget, ctx, counter_x, y, selected);
|
||||
|
||||
// Subject field.
|
||||
render_subject(widget, cell_area, ctx, y, selected, counter_width);
|
||||
y += ink_rect.height + ink_rect.y + (int) (SPACING * 1.2);
|
||||
}
|
||||
|
||||
if (recalc_dims) {
|
||||
FormattedConversationData.preview_height = preview_height;
|
||||
FormattedConversationData.cell_height = y + preview_height;
|
||||
} else {
|
||||
int unread_y = display_preview ? cell_area.y + SPACING * 2 : cell_area.y +
|
||||
SPACING;
|
||||
|
||||
// Unread indicator.
|
||||
if (is_unread || hover) {
|
||||
Gdk.Pixbuf read_icon = IconFactory.instance.load_symbolic(
|
||||
is_unread ? "mail-unread-symbolic" : "mail-read-symbolic",
|
||||
IconFactory.UNREAD_ICON_SIZE, widget.get_style_context());
|
||||
Gdk.cairo_set_source_pixbuf(ctx, read_icon, cell_area.x + SPACING, unread_y);
|
||||
ctx.paint();
|
||||
}
|
||||
|
||||
// Starred indicator.
|
||||
if (is_flagged || hover) {
|
||||
int star_y = cell_area.y + (cell_area.height / 2) + (display_preview ? SPACING : 0);
|
||||
Gdk.Pixbuf starred_icon = IconFactory.instance.load_symbolic(
|
||||
is_flagged ? "starred-symbolic" : "non-starred-symbolic",
|
||||
IconFactory.STAR_ICON_SIZE, widget.get_style_context());
|
||||
Gdk.cairo_set_source_pixbuf(ctx, starred_icon, cell_area.x + SPACING, star_y);
|
||||
ctx.paint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Pango.Rectangle render_date(Gtk.Widget widget, Gdk.Rectangle? cell_area,
|
||||
Cairo.Context? ctx, int y, bool selected) {
|
||||
string date_markup = "<span size='smaller' foreground='%s'>%s</span>".printf(
|
||||
rgba_to_markup(dim_rgba(get_foreground_rgba(widget, selected), DIM_TEXT_AMOUNT)),
|
||||
Geary.HTML.escape_markup(date));
|
||||
|
||||
Pango.Rectangle? ink_rect;
|
||||
Pango.Rectangle? logical_rect;
|
||||
Pango.Layout layout_date = widget.create_pango_layout(null);
|
||||
layout_date.set_font_description(this.font);
|
||||
layout_date.set_markup(date_markup, -1);
|
||||
layout_date.set_alignment(Pango.Alignment.RIGHT);
|
||||
layout_date.get_pixel_extents(out ink_rect, out logical_rect);
|
||||
if (ctx != null && cell_area != null) {
|
||||
ctx.move_to(cell_area.width - cell_area.x - ink_rect.width - ink_rect.x - SPACING, y);
|
||||
Pango.cairo_show_layout(ctx, layout_date);
|
||||
}
|
||||
return ink_rect;
|
||||
}
|
||||
|
||||
private Pango.Rectangle render_from(Gtk.Widget widget, Gdk.Rectangle? cell_area,
|
||||
Cairo.Context? ctx, int y, bool selected, Pango.Rectangle ink_rect) {
|
||||
string from_markup = (conversation != null) ? get_participants_markup(widget, selected) : STYLE_EXAMPLE;
|
||||
|
||||
Pango.FontDescription font = this.font;
|
||||
if (is_unread) {
|
||||
font = font.copy();
|
||||
font.set_weight(Pango.Weight.BOLD);
|
||||
}
|
||||
Pango.Layout layout_from = widget.create_pango_layout(null);
|
||||
layout_from.set_font_description(font);
|
||||
layout_from.set_markup(from_markup, -1);
|
||||
layout_from.set_ellipsize(Pango.EllipsizeMode.END);
|
||||
if (ctx != null && cell_area != null) {
|
||||
layout_from.set_width((cell_area.width - ink_rect.width - ink_rect.x - (SPACING * 3) -
|
||||
TEXT_LEFT)
|
||||
* Pango.SCALE);
|
||||
ctx.move_to(cell_area.x + TEXT_LEFT, y);
|
||||
Pango.cairo_show_layout(ctx, layout_from);
|
||||
}
|
||||
return ink_rect;
|
||||
}
|
||||
|
||||
private void render_subject(Gtk.Widget widget, Gdk.Rectangle? cell_area, Cairo.Context? ctx,
|
||||
int y, bool selected, int counter_width = 0) {
|
||||
string subject_markup = "<span size='smaller' foreground='%s'>%s</span>".printf(
|
||||
rgba_to_markup(dim_rgba(get_foreground_rgba(widget, selected), DIM_TEXT_AMOUNT)),
|
||||
subject_html_escaped);
|
||||
|
||||
Pango.FontDescription font = this.font;
|
||||
if (is_unread) {
|
||||
font = font.copy();
|
||||
font.set_weight(Pango.Weight.BOLD);
|
||||
}
|
||||
Pango.Layout layout_subject = widget.create_pango_layout(null);
|
||||
layout_subject.set_font_description(font);
|
||||
layout_subject.set_markup(subject_markup, -1);
|
||||
if (cell_area != null)
|
||||
layout_subject.set_width((cell_area.width - TEXT_LEFT - counter_width) * Pango.SCALE);
|
||||
layout_subject.set_ellipsize(Pango.EllipsizeMode.END);
|
||||
if (ctx != null && cell_area != null) {
|
||||
ctx.move_to(cell_area.x + TEXT_LEFT, y);
|
||||
Pango.cairo_show_layout(ctx, layout_subject);
|
||||
}
|
||||
}
|
||||
|
||||
private Pango.Rectangle render_preview(Gtk.Widget widget, Gdk.Rectangle? cell_area,
|
||||
Cairo.Context? ctx, int y, bool selected, int counter_width = 0) {
|
||||
double dim = selected ? DIM_TEXT_AMOUNT : DIM_PREVIEW_TEXT_AMOUNT;
|
||||
string preview_markup = "<span size='smaller' foreground='%s'>%s</span>".printf(
|
||||
rgba_to_markup(dim_rgba(get_foreground_rgba(widget, selected), dim)),
|
||||
Geary.String.is_empty(body) ? "" : Geary.HTML.escape_markup(body));
|
||||
|
||||
Pango.Layout layout_preview = widget.create_pango_layout(null);
|
||||
layout_preview.set_font_description(this.font);
|
||||
layout_preview.set_markup(preview_markup, -1);
|
||||
layout_preview.set_wrap(Pango.WrapMode.WORD);
|
||||
layout_preview.set_ellipsize(Pango.EllipsizeMode.END);
|
||||
if (ctx != null && cell_area != null) {
|
||||
layout_preview.set_width((cell_area.width - TEXT_LEFT - counter_width - SPACING) * Pango.SCALE);
|
||||
layout_preview.set_height(preview_height * Pango.SCALE);
|
||||
|
||||
ctx.move_to(cell_area.x + TEXT_LEFT, y);
|
||||
Pango.cairo_show_layout(ctx, layout_preview);
|
||||
} else {
|
||||
layout_preview.set_width(int.MAX);
|
||||
layout_preview.set_height(int.MAX);
|
||||
}
|
||||
|
||||
Pango.Rectangle? ink_rect;
|
||||
Pango.Rectangle? logical_rect;
|
||||
layout_preview.get_pixel_extents(out ink_rect, out logical_rect);
|
||||
return ink_rect;
|
||||
}
|
||||
|
||||
private void update_font() {
|
||||
var name = "Cantarell 11";
|
||||
if (this.gtk != null) {
|
||||
name = this.gtk.gtk_font_name;
|
||||
}
|
||||
this.font = Pango.FontDescription.from_string(name);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -156,9 +156,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.selected;
|
||||
conversation_list.unselect_all();
|
||||
|
||||
box.vanished.connect(on_composer_closed);
|
||||
this.composer_page.add(box);
|
||||
|
|
|
|||
|
|
@ -89,10 +89,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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -81,25 +81,6 @@
|
|||
<child>
|
||||
<object class="GtkBox" id="mark_copy_move_buttons">
|
||||
<property name="visible">True</property>
|
||||
<child>
|
||||
<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="mark_message_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="icon_name">checkbox-checked-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="copy_message_button">
|
||||
<property name="visible">True</property>
|
||||
|
|
@ -116,7 +97,7 @@
|
|||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
|
|
@ -132,6 +113,28 @@
|
|||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<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="mark_message_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="icon_name">pan-down-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="thin-button"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
|
|
|
|||
|
|
@ -61,7 +61,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>
|
||||
|
|
|
|||
203
ui/conversation-list-row.ui
Normal file
203
ui/conversation-list-row.ui
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
<?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="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>
|
||||
</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"/>
|
||||
<class name="flagged-button"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</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="vexpand">True</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>
|
||||
</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>
|
||||
</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>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</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>
|
||||
</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>
|
||||
</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>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="conversation-list"/>
|
||||
</style>
|
||||
</template>
|
||||
</interface>
|
||||
25
ui/conversation-list-view.ui
Normal file
25
ui/conversation-list-view.ui
Normal file
|
|
@ -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">false</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
113
ui/geary.css
113
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,89 @@ 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.starred .flagged-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;
|
||||
}
|
||||
|
||||
/* FolderPopover */
|
||||
|
||||
.geary-folder-popover-list {
|
||||
|
|
@ -440,11 +510,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 */
|
||||
|
|
@ -489,3 +559,10 @@ dialog.geary-upgrade grid {
|
|||
dialog.geary-upgrade label {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* Misc */
|
||||
|
||||
.thin-button {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue