[geary/wip/730682-refine-convo-list] Add basic HIG selection mode pattern support to ConversationList.
- From: Michael Gratton <mjog src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/wip/730682-refine-convo-list] Add basic HIG selection mode pattern support to ConversationList.
- Date: Sun, 24 Dec 2017 08:11:32 +0000 (UTC)
commit cf4372ceadcb624788fd38a77d588ff9825ea1e5
Author: Michael James Gratton <mike vee net>
Date: Sun Dec 24 18:37:08 2017 +1030
Add basic HIG selection mode pattern support to ConversationList.
* src/client/conversation-list/conversation-list.vala (ConversationListBox):
Add support for selection mode.
* src/client/conversation-list/conversation-list-item.vala
(ConversationListItem): Allow items to be marked, and display a
selected icon when they are.
.../conversation-list/conversation-list-item.vala | 21 +++
.../conversation-list/conversation-list.vala | 162 ++++++++++++++++++--
ui/conversation-list-item.ui | 55 ++++++--
ui/geary.css | 30 +++--
4 files changed, 237 insertions(+), 31 deletions(-)
---
diff --git a/src/client/conversation-list/conversation-list-item.vala
b/src/client/conversation-list/conversation-list-item.vala
index 91f545a..49f4c0f 100644
--- a/src/client/conversation-list/conversation-list-item.vala
+++ b/src/client/conversation-list/conversation-list-item.vala
@@ -76,6 +76,9 @@ public class ConversationListItem : Gtk.ListBoxRow {
/** The conversation displayed by this item */
public Geary.App.Conversation conversation { get; private set; }
+ /** Determines if this row is marked for selection mode */
+ public bool is_marked { get; private set; default = false; }
+
[GtkChild]
private Gtk.Button star_button;
@@ -98,12 +101,20 @@ public class ConversationListItem : Gtk.ListBoxRow {
[GtkChild]
private Gtk.Label count;
+ [GtkChild]
+ private Gtk.Revealer mark_revealer;
+
private Gee.List<Geary.RFC822.MailboxAddress> account_addresses;
private bool use_to;
private PreviewLoader previews;
private Cancellable preview_cancellable = new Cancellable();
private Configuration config;
+
+ /** Fired when this row is marked for selection mode. */
+ public signal void item_marked(bool marked);
+
+
public ConversationListItem(Geary.App.Conversation conversation,
Gee.List<Geary.RFC822.MailboxAddress> account_addresses,
bool use_to,
@@ -137,6 +148,16 @@ public class ConversationListItem : Gtk.ListBoxRow {
base.destroy();
}
+ internal void toggle_marked() {
+ set_marked(!this.is_marked);
+ }
+
+ internal void set_marked(bool marked) {
+ this.is_marked = marked;
+ this.mark_revealer.set_reveal_child(marked);
+ item_marked(marked);
+ }
+
private void update() {
Gtk.StyleContext style = get_style_context();
diff --git a/src/client/conversation-list/conversation-list.vala
b/src/client/conversation-list/conversation-list.vala
index caf8d16..dd4b4b2 100644
--- a/src/client/conversation-list/conversation-list.vala
+++ b/src/client/conversation-list/conversation-list.vala
@@ -7,7 +7,12 @@
*/
/**
- * A Gtk.ListBox that displays a list of conversations.
+ * A GtkListBox that displays a list of conversations.
+ *
+ * This class uses the GtkListBox's selection system for selecting and
+ * displaying individual conversations, and supports the GNOME3 HIG
+ * selection mode pattern for to allow multiple conversations to be
+ * marked, independent of the list's selection.
*/
public class ConversationList : Gtk.ListBox {
@@ -21,15 +26,19 @@ public class ConversationList : Gtk.ListBox {
/**
* The conversation highlighted as selected, if any.
*
- * This is distinct to the conversations chosen via selection
+ * This is distinct to the conversations marked via selection
* mode, which are checked and might not be highlighted.
*/
public Geary.App.Conversation? selected { get; private set; default = null; }
+ /** Determines if selection mode is enabled for the list. */
+ public bool is_selection_mode_enabled { get; private set; default = false; }
private Configuration config;
private int selected_index = -1;
private bool selection_frozen = false;
+ private Gee.Map<Geary.App.Conversation,ConversationListItem> marked =
+ new Gee.HashMap<Geary.App.Conversation,ConversationListItem>();
private Gee.Set<Geary.App.Conversation>? visible_conversations = null;
private Geary.Scheduler.Scheduled? update_visible_scheduled = null;
private bool enable_load_more = true;
@@ -51,6 +60,20 @@ public class ConversationList : Gtk.ListBox {
this.enable_load_more = false;
}
+ /**
+ * Fired when a list item was targeted with a selection gesture.
+ *
+ * Selection gestures include Ctrl-click or Shift-click on the
+ * list row.
+ */
+ public signal void selection_mode_enabled();
+
+ /**
+ * Fired when a list item was marked as selected in selection mode.
+ */
+ public signal void item_marked(ConversationListItem item, bool marked);
+
+
public ConversationList(Configuration config) {
this.config = config;
get_style_context().add_class(LIST_CLASS);
@@ -67,6 +90,16 @@ public class ConversationList : Gtk.ListBox {
this.show.connect(on_show);
}
+ /**
+ * Returns a read-only collection of currently marked items.
+ *
+ * This is distinct to the conversations marked via the list's
+ * selection, which are highlighted as selected.
+ */
+ public Gee.Collection<Geary.App.Conversation> get_marked_items() {
+ return this.marked.keys.read_only_view;
+ }
+
public new void bind_model(Geary.App.ConversationMonitor monitor) {
Geary.Folder displayed = monitor.base_folder;
Geary.App.EmailStore store = new Geary.App.EmailStore(displayed.account);
@@ -77,6 +110,7 @@ public class ConversationList : Gtk.ListBox {
monitor.scan_completed.connect(() => {
loader.load_remote();
});
+ monitor.conversations_removed.connect(on_conversations_removed);
this.model = new ConversationListModel(monitor, loader);
this.model.items_changed.connect(on_model_items_changed);
@@ -84,15 +118,21 @@ public class ConversationList : Gtk.ListBox {
// Clear these since they will belong to the old model
this.selected = null;
this.selected_index = -1;
+ this.marked.clear();
+ this.visible_conversations = null;
Gee.List<Geary.RFC822.MailboxAddress> account_addresses =
displayed.account.information.get_all_mailboxes();
bool use_to = displayed.special_folder_type.is_outgoing();
base.bind_model(this.model, (convo) => {
- return new ConversationListItem(convo as Geary.App.Conversation,
- account_addresses,
- use_to,
- loader,
- this.config);
+ ConversationListItem item = new ConversationListItem(
+ convo as Geary.App.Conversation,
+ account_addresses,
+ use_to,
+ loader,
+ this.config
+ );
+ item.item_marked.connect(on_item_marked);
+ return item;
}
);
}
@@ -119,12 +159,97 @@ public class ConversationList : Gtk.ListBox {
}
}
+ public override bool button_press_event(Gdk.EventButton event) {
+ if (event.button == 1) {
+ if ((event.state & Gdk.ModifierType.SHIFT_MASK) == 0) {
+ // Shift isn't down
+ if ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0 &&
+ !this.is_selection_mode_enabled) {
+ // Not currently in selection mode, but Ctrl is
+ // down, so enable it
+ set_selection_mode_enabled(true);
+ selection_mode_enabled();
+ }
+ if (this.is_selection_mode_enabled) {
+ // Are (now) currently in selection mode, so
+ // toggle the row
+ ConversationListItem? row =
+ get_row_at_y((int) event.y) as ConversationListItem;
+ if (row != null) {
+ row.toggle_marked();
+ }
+ }
+ } else if ((event.state & Gdk.ModifierType.SHIFT_MASK) != 0) {
+ // Shift is down, so emulate Gtk.TreeView-like
+ // contiguous selection behaviour
+ if (!this.is_selection_mode_enabled) {
+ set_selection_mode_enabled(true);
+ selection_mode_enabled();
+ }
+ ConversationListItem? clicked =
+ get_row_at_y((int) event.y) as ConversationListItem;
+ if (clicked != null) {
+ ConversationListItem? selected =
+ get_selected_row() as ConversationListItem;
+
+ if (selected == null) {
+ selected = get_item_at_index(0);
+ }
+
+ int index = int.min(clicked.get_index(), selected.get_index());
+ int end = index + (clicked.get_index() - selected.get_index()).abs();
+ while (index <= end) {
+ ConversationListItem? row = get_item_at_index(index++);
+ if (row != null) {
+ row.set_marked(true);
+ }
+ }
+ }
+ }
+ }
+ return base.button_press_event(event);
+ }
+
+ public override bool key_press_event(Gdk.EventKey event) {
+ if (event.keyval == Gdk.Key.Return ||
+ event.keyval == Gdk.Key.KP_Enter) {
+ if ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0 &&
+ !this.is_selection_mode_enabled) {
+ set_selection_mode_enabled(true);
+ selection_mode_enabled();
+ }
+ if (this.is_selection_mode_enabled) {
+ // Are (now) currently in selection mode, so
+ // toggle the row
+ ConversationListItem? row =
+ get_selected_row() as ConversationListItem;
+ if (row != null) {
+ row.toggle_marked();
+ }
+ }
+ }
+ return base.key_press_event(event);
+ }
+
internal Gee.Set<Geary.App.Conversation> get_visible_conversations() {
Gee.HashSet<Geary.App.Conversation> visible = new Gee.HashSet<Geary.App.Conversation>();
// XXX Implement me
return visible;
}
+ internal void set_selection_mode_enabled(bool enabled) {
+ if (!enabled) {
+ // Call to_array here to get a copy of the value
+ // collection, since unmarking the items will cause the
+ // underlying map to be modified
+ foreach (ConversationListItem item in this.marked.values.to_array()) {
+ item.set_marked(false);
+ }
+ this.marked.clear();
+ }
+ this.is_selection_mode_enabled = enabled;
+ }
+
private inline ConversationListItem? get_item_at_index(int index) {
return get_row_at_index(index) as ConversationListItem;
}
@@ -183,10 +308,10 @@ public class ConversationList : Gtk.ListBox {
this.selected_index = -1;
}
- debug("Selection changed to: %s",
- selected != null ? selected.to_string() : null
- );
if (this.selected != selected) {
+ debug("Selection changed to: %s",
+ selected != null ? selected.to_string() : null
+ );
this.selected = selected;
this.conversation_selection_changed(selected);
}
@@ -276,4 +401,21 @@ public class ConversationList : Gtk.ListBox {
}
}
+ private void on_conversations_removed(Gee.Collection<Geary.App.Conversation> removed) {
+ if (this.is_selection_mode_enabled) {
+ foreach (Geary.App.Conversation convo in removed) {
+ this.marked.remove(convo);
+ }
+ }
+ }
+
+ private void on_item_marked(ConversationListItem item, bool marked) {
+ if (marked) {
+ this.marked.set(item.conversation, item);
+ } else {
+ this.marked.remove(item.conversation);
+ }
+ item_marked(item, marked);
+ }
+
}
diff --git a/ui/conversation-list-item.ui b/ui/conversation-list-item.ui
index c6b2e87..c8182ee 100644
--- a/ui/conversation-list-item.ui
+++ b/ui/conversation-list-item.ui
@@ -5,6 +5,7 @@
<template class="ConversationListItem" parent="GtkListBoxRow">
<property name="visible">True</property>
<property name="can_focus">True</property>
+ <property name="events">GDK_BUTTON_PRESS_MASK | GDK_KEY_PRESS_MASK | GDK_STRUCTURE_MASK</property>
<child>
<object class="GtkGrid">
<property name="visible">True</property>
@@ -15,7 +16,6 @@
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="valign">start</property>
- <property name="margin_bottom">2</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Participants</property>
<property name="ellipsize">end</property>
@@ -23,7 +23,7 @@
<property name="xalign">0</property>
</object>
<packing>
- <property name="left_attach">1</property>
+ <property name="left_attach">2</property>
<property name="top_attach">0</property>
</packing>
</child>
@@ -39,7 +39,7 @@
<property name="xalign">0</property>
</object>
<packing>
- <property name="left_attach">1</property>
+ <property name="left_attach">2</property>
<property name="top_attach">1</property>
<property name="width">2</property>
</packing>
@@ -100,7 +100,7 @@
</child>
</object>
<packing>
- <property name="left_attach">0</property>
+ <property name="left_attach">1</property>
<property name="top_attach">0</property>
<property name="height">3</property>
</packing>
@@ -120,7 +120,7 @@
</style>
</object>
<packing>
- <property name="left_attach">1</property>
+ <property name="left_attach">2</property>
<property name="top_attach">2</property>
<property name="width">2</property>
</packing>
@@ -142,7 +142,7 @@
</style>
</object>
<packing>
- <property name="left_attach">3</property>
+ <property name="left_attach">4</property>
<property name="top_attach">1</property>
<property name="height">2</property>
</packing>
@@ -159,15 +159,50 @@
</style>
</object>
<packing>
- <property name="left_attach">2</property>
+ <property name="left_attach">3</property>
<property name="top_attach">0</property>
<property name="width">2</property>
</packing>
</child>
- <style>
- <class name="geary-conversation-list-item"/>
- </style>
+ <child>
+ <object class="GtkGrid" id="mark_container">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="valign">center</property>
+ <property name="vexpand">True</property>
+ <child>
+ <object class="GtkRevealer" id="mark_revealer">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="transition_type">slide-right</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">object-select-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <style>
+ <class name="geary-mark-container"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="height">3</property>
+ </packing>
+ </child>
</object>
</child>
+ <style>
+ <class name="geary-conversation-list-item"/>
+ </style>
</template>
</interface>
diff --git a/ui/geary.css b/ui/geary.css
index f87e091..408c6f5 100644
--- a/ui/geary.css
+++ b/ui/geary.css
@@ -1,6 +1,6 @@
/*
* Copyright 2016 Software Freedom Conservancy Inc.
- * Copyright 2016, 2017 Michael Gratton <mike vee net>
+ * Copyright 2016-2017 Michael Gratton <mike vee net>
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
@@ -61,17 +61,17 @@ row.geary-folder-popover-list-row > label {
color: @theme_text_color;
}
+/* ConversationListItem */
+
/* ConversationList */
-list.geary-conversation-list > row {
+.geary-conversation-list-item {
margin: 0;
border: 0;
padding: 0;
}
-/* ConversationListItem */
-
-grid.geary-conversation-list-item {
+.geary-conversation-list-item > grid {
border: 4px solid transparent;
border-top-width: 0px;
border-bottom-width: 0px;
@@ -79,29 +79,29 @@ grid.geary-conversation-list-item {
padding: 12px;
transition: border 4s;
}
-grid.geary-conversation-list-item:dir(ltr) {
+.geary-conversation-list-item > grid:dir(ltr) {
border-right-width: 0px;
padding-left: 0;
}
-grid.geary-conversation-list-item:dir(rtl) {
+.geary-conversation-list-item > grid:dir(rtl) {
border-left-width: 0px;
padding-right: 0;
}
-grid.geary-conversation-list-item.geary-unread {
+.geary-conversation-list-item.geary-unread > grid {
border-color: @theme_selected_bg_color;
transition: border 0.25s;
}
-grid.geary-conversation-list-item label.geary-date:dir(ltr) {
+.geary-conversation-list-item label.geary-date:dir(ltr) {
margin-right: 6px;
}
-grid.geary-conversation-list-item label.geary-date:dir(rtl) {
+.geary-conversation-list-item label.geary-date:dir(rtl) {
margin-left: 6px;
}
-grid.geary-conversation-list-item label.geary-count {
+.geary-conversation-list-item label.geary-count {
padding: 2px 6px;
border-radius: 2px;
font-size: small;
@@ -109,6 +109,14 @@ grid.geary-conversation-list-item label.geary-count {
background: @theme_selected_bg_color;
}
+.geary-conversation-list-item .geary-mark-container image:dir(ltr) {
+ padding-left: 6px;
+}
+
+.geary-conversation-list-item .geary-mark-container image:dir(rtl) {
+ padding-right: 6px;
+}
+
/* ConversationListBox */
.conversation-listbox {
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]