[seahorse/wip/nielsdg/seahorse-listview: 1/3] common: Add SeahorseItemList
- From: Niels De Graef <nielsdg src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [seahorse/wip/nielsdg/seahorse-listview: 1/3] common: Add SeahorseItemList
- Date: Wed, 11 Dec 2019 07:06:15 +0000 (UTC)
commit 742d83742933993cd1cf12ee8ff348eb8733b42a
Author: Niels De Graef <nielsdegraef gmail com>
Date: Sun Mar 10 09:41:26 2019 +0100
common: Add SeahorseItemList
SeahorseItemList is a wrapper to easily combine a GtkListBox and a
GListModel. It's mostly a stepping point to get rid of some of our
GtkTreeViews (which are supported by GcrCollectionModel), as they're
leading to bugs when scrolling.
common/item-list.vala | 234 ++++++++++++++++++++++++++++++++++++++++++
common/meson.build | 1 +
libseahorse/seahorse.css | 21 ++++
src/key-manager-item-row.vala | 67 ++++++++++++
src/key-manager.vala | 140 ++++++++++++-------------
src/meson.build | 1 +
src/seahorse-key-manager.ui | 30 ++----
7 files changed, 401 insertions(+), 93 deletions(-)
---
diff --git a/common/item-list.vala b/common/item-list.vala
new file mode 100644
index 00000000..696779f8
--- /dev/null
+++ b/common/item-list.vala
@@ -0,0 +1,234 @@
+/*
+ * Seahorse
+ *
+ * Copyright (C) 2019 Niels De Graef
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * A ItemList is a {@GLib.ListModel} which knows how to handle
+ * {@link Gcr.Collection}s. These basically provide a compatibility wrapper
+ * {@link GLib.ListModel}, lists that propagate changes to their listener (such
+ * as a {@link Gtk.ListBox}).
+ *
+ * This list makes an assumption that each iten is a Seahorse.Object, or provides
+ * the necessary properties like "label" and possibly "description".
+ */
+public class Seahorse.ItemList : GLib.Object, GLib.ListModel {
+
+ /** The basic collection */
+ private Gcr.Collection base_collection { get; private set; }
+
+ /** The filtered and sorted list store */
+ private GLib.GenericArray<GLib.Object> items = new GLib.GenericArray<GLib.Object>();
+
+ public enum ShowFilter {
+ ANY,
+ PERSONAL,
+ TRUSTED;
+
+ public unowned string? to_string() {
+ switch (this) {
+ case ShowFilter.ANY:
+ return "";
+ case ShowFilter.PERSONAL:
+ return "personal";
+ case ShowFilter.TRUSTED:
+ return "trusted";
+ default:
+ assert_not_reached();
+ }
+ }
+
+ public static ShowFilter from_string(string? str) {
+ switch (str) {
+ case null:
+ case "":
+ case "any":
+ return ShowFilter.ANY;
+ case "personal":
+ return ShowFilter.PERSONAL;
+ case "trusted":
+ return ShowFilter.TRUSTED;
+ default:
+ critical ("Got unknown ShowFilter string: %s", str);
+ assert_not_reached();
+ }
+ }
+ }
+
+ public ShowFilter showfilter { get; set; default = ShowFilter.ANY; }
+
+ private string _filter_text = "";
+ public string filter_text {
+ set {
+ if (this._filter_text == value)
+ return;
+ this._filter_text = value.casefold();
+ refilter();
+ }
+ }
+
+ public ItemList(Gcr.Collection collection) {
+ this.base_collection = collection;
+
+ // Make sure our model and the GcrCollection stay in sync
+ collection.added.connect(on_collection_item_added);
+ collection.removed.connect(on_collection_item_removed);
+
+ // Add the existing elements
+ foreach (var obj in collection.get_objects())
+ this.items.add(obj);
+
+ // Sort afterwards
+ this.items.sort(compare_items);
+
+ // Notify listeners
+ items_changed(0, 0, this.items.length);
+ }
+
+ private void on_collection_item_added(GLib.Object object) {
+ // First check if the current filter wants this
+ if (!item_matches_filters(object))
+ return;
+
+ int index = this.items.length;
+ for (int i = 0; i < this.items.length; i++) {
+ if (compare_items(object, this.items[i]) < 0) {
+ index = i;
+ break;
+ }
+ }
+ this.items.insert(index, object);
+ items_changed(index, 0, 1);
+ }
+
+ private void on_collection_item_removed(GLib.Object object) {
+ uint index;
+ if (this.items.find(object, out index)) {
+ this.items.remove_index(index);
+ items_changed(index, 1, 0);
+ }
+ }
+
+ private bool item_matches_filters(GLib.Object object) {
+ return matches_showfilter(object)
+ && object_contains_filtered_text(object, this._filter_text);
+ }
+
+ private bool matches_showfilter(GLib.Object? obj) {
+ Flags obj_flags = Flags.NONE;
+ obj.get("object-flags", out obj_flags, null);
+
+ switch (this.showfilter) {
+ case ShowFilter.PERSONAL:
+ return Seahorse.Flags.PERSONAL in obj_flags;
+ case ShowFilter.TRUSTED:
+ return Seahorse.Flags.TRUSTED in obj_flags;
+ case ShowFilter.ANY:
+ return true;
+ }
+
+ return false;
+ }
+
+ // Search through row for text
+ private bool object_contains_filtered_text (GLib.Object? object, string? text) {
+ // Empty search text results in a match
+ if (text == null || text == "")
+ return true;
+
+ string? name = null;
+ object.get("label", out name, null);
+ if (name != null && (text in name.down()))
+ return true;
+
+ if (object.get_class().find_property("description") != null) {
+ string? description = null;
+ object.get("description", out description, null);
+ if (description != null && (text in description.down()))
+ return true;
+ }
+
+ return false;
+ }
+
+ private static int compare_items(GLib.Object gobj_a, GLib.Object gobj_b) {
+ string? a_label = null, b_label = null;
+
+ gobj_a.get("label", out a_label, null);
+ gobj_b.get("label", out b_label, null);
+
+ // Put (null) labels at the bottom
+ if (a_label == null || b_label == null)
+ return (a_label == null)? 1 : -1;
+
+ return compare_labels(a_label, b_label);
+ }
+
+ public GLib.Object? get_item(uint position) {
+ return this.items[position];
+ }
+
+ public GLib.Type get_item_type() {
+ return typeof(GLib.Object);
+ }
+
+ public uint get_n_items () {
+ return this.items.length;
+ }
+
+ /**
+ * Updates the collection.
+ * Automatically called when you change filter_text to another value
+ */
+ public void refilter() {
+ // First remove all items
+ this.items.remove_range(0, this.items.length);
+ items_changed(0, this.items.length, 0);
+
+ // Add only the ones that match the filter
+ foreach (var obj in this.base_collection.get_objects()) {
+ if (item_matches_filters(obj))
+ this.items.add(obj);
+ }
+
+ // Sort afterwards
+ this.items.sort(compare_items);
+
+ // Notify listeners
+ items_changed(0, 0, this.items.length);
+
+ debug("%u/%u elements visible after refilter",
+ this.items.length, this.base_collection.get_length());
+ }
+
+ // Compares 2 labels in an intuitive way
+ // (case-insensitive; with respect to the user's locale)
+ private static int compare_labels(string a_label, string b_label) {
+ return a_label.casefold().collate(b_label.casefold());
+ }
+
+ private static bool matches_show_filter(GLib.Object obj, string substr) {
+ string? obj_label = null;
+ obj.get("label", out obj_label, null);
+
+ if (obj_label == null)
+ return substr == "";
+
+ return substr in obj_label.casefold();
+ }
+}
diff --git a/common/meson.build b/common/meson.build
index 36788790..71b80cc3 100644
--- a/common/meson.build
+++ b/common/meson.build
@@ -12,6 +12,7 @@ common_sources = [
'icons.vala',
'key-manager-store.vala',
'interaction.vala',
+ 'item-list.vala',
'lockable.vala',
'object.vala',
'passphrase-prompt.vala',
diff --git a/libseahorse/seahorse.css b/libseahorse/seahorse.css
index 448eb001..b77eaaad 100644
--- a/libseahorse/seahorse.css
+++ b/libseahorse/seahorse.css
@@ -5,6 +5,27 @@
border-radius: 0;
}
+.seahorse-item-listbox {
+ border: 1px solid @borders;
+ min-width: 400px;
+}
+
+.seahorse-item-listbox > row {
+ border-bottom: 1px groove @borders;
+}
+
+.seahorse-item-listbox > row:last-child {
+ border-bottom: 0;
+}
+
+.seahorse-item-listbox-row {
+ margin: 12;
+}
+
+.seahorse-item-listbox-row > .seahorse-item-listbox-row-description {
+ font-size: small;
+}
+
.new-item-list {
background-color: transparent;
}
diff --git a/src/key-manager-item-row.vala b/src/key-manager-item-row.vala
new file mode 100644
index 00000000..df51825b
--- /dev/null
+++ b/src/key-manager-item-row.vala
@@ -0,0 +1,67 @@
+/*
+ * Seahorse
+ *
+ * Copyright (C) 2008 Stefan Walter
+ * Copyright (C) 2011 Collabora Ltd.
+ * Copyright (C) 2017 Niels De Graef
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Represents an item in the KeyManager's (i.e. the main window) list of items.
+ */
+public class Seahorse.KeyManagerItemRow : Gtk.ListBoxRow {
+
+ public GLib.Object object { get; construct set; }
+
+ construct {
+ var grid = new Gtk.Grid();
+ grid.get_style_context().add_class("seahorse-item-listbox-row");
+ add(grid);
+
+ GLib.Icon? icon = null;
+ object.get("icon", out icon);
+ if (icon != null) {
+ var img = new Gtk.Image.from_gicon(icon, Gtk.IconSize.DND);
+ img.margin_end = 12;
+ img.pixel_size = 32;
+ grid.attach(img, 0, 0, 1, 2);
+ }
+
+ string markup;
+ object.get("markup", out markup);
+ var markup_label = new Gtk.Label(markup);
+ markup_label.use_markup = true;
+ markup_label.halign = Gtk.Align.START;
+ markup_label.xalign = 0.0f;
+ markup_label.hexpand = true;
+ markup_label.ellipsize = Pango.EllipsizeMode.END;
+ grid.attach(markup_label, 1, 0);
+
+ string description = "";
+ object.get("description", out description);
+ var description_label = new Gtk.Label(description);
+ description_label.xalign = 1.0f;
+ description_label.get_style_context().add_class("seahorse-item-listbox-row-description");
+ grid.attach(description_label, 2, 0);
+
+ show_all();
+ }
+
+ public KeyManagerItemRow(GLib.Object object) {
+ GLib.Object(object: object);
+ }
+}
diff --git a/src/key-manager.vala b/src/key-manager.vala
index 8e616b74..5fb1c937 100644
--- a/src/key-manager.vala
+++ b/src/key-manager.vala
@@ -36,15 +36,15 @@ public class Seahorse.KeyManager : Catalog {
[GtkChild]
private Gtk.Stack content_stack;
[GtkChild]
- private Gtk.TreeView key_list;
+ private Gtk.ListBox item_listbox;
[GtkChild]
private Gtk.MenuButton new_item_button;
[GtkChild]
private Gtk.ToggleButton show_search_button;
+ private Seahorse.ItemList item_list;
private Gcr.Collection collection;
- private KeyManagerStore store;
private GLib.Settings settings;
@@ -70,19 +70,18 @@ public class Seahorse.KeyManager : Catalog {
);
this.settings = new GLib.Settings("org.gnome.seahorse.manager");
- set_events(Gdk.EventMask.POINTER_MOTION_MASK
- | Gdk.EventMask.POINTER_MOTION_HINT_MASK
- | Gdk.EventMask.BUTTON_PRESS_MASK
- | Gdk.EventMask.BUTTON_RELEASE_MASK);
-
this.collection = setup_sidebar();
load_css();
- // Add new key store and associate it
- this.store = new KeyManagerStore(this.collection, this.key_list, this.settings);
- this.store.row_inserted.connect(on_store_row_inserted);
- this.store.row_deleted.connect(on_store_row_deleted);
+ // Add new item list and bind our listbox to it
+ this.item_list = new Seahorse.ItemList(this.collection);
+ this.item_list.items_changed.connect((idx, removed, added) => check_empty_state());
+ this.item_listbox.bind_model(this.item_list, (obj) => new KeyManagerItemRow(obj));
+ this.item_listbox.row_activated.connect(on_item_listbox_row_activated);
+ this.item_listbox.selected_rows_changed.connect(on_item_listbox_selected_rows_changed);
+ this.item_listbox.popup_menu.connect(on_item_listbox_popup_menu);
+ this.item_listbox.button_press_event.connect(on_item_listbox_button_press_event);
init_actions();
@@ -92,14 +91,6 @@ public class Seahorse.KeyManager : Catalog {
// For the filtering
on_filter_changed(this.filter_entry);
- this.key_list.start_interactive_search.connect(() => {
- this.filter_entry.grab_focus();
- return false;
- });
-
- // Set focus to the current key list
- this.key_list.grab_focus();
- selection_changed();
// Setup drops
Gtk.drag_dest_set(this, Gtk.DestDefaults.ALL, {}, Gdk.DragAction.COPY);
@@ -134,48 +125,68 @@ public class Seahorse.KeyManager : Catalog {
provider.load_from_resource("/org/gnome/Seahorse/seahorse.css");
}
- [GtkCallback]
- private void on_view_selection_changed(Gtk.TreeSelection selection) {
- Idle.add(() => {
- // Fire the signal
- selection_changed();
- // Return false so we don't get run again
- return false;
- });
+ private void on_item_listbox_row_activated(Gtk.ListBox item_listbox, Gtk.ListBoxRow row) {
+ unowned GLib.Object obj = ((KeyManagerItemRow) row).object;
+ assert(obj != null);
+ show_properties(obj);
}
- public override void selection_changed() {
- base.selection_changed();
-
- var objects = get_selected_objects();
- foreach (weak Backend backend in get_backends())
- backend.actions.set_actions_for_selected_objects(objects);
+ private void on_item_listbox_selected_rows_changed(Gtk.ListBox item_listbox) {
+ selection_changed();
}
- [GtkCallback]
- private void on_key_list_row_activated(Gtk.TreeView key_list, Gtk.TreePath? path, Gtk.TreeViewColumn
column) {
- if (path == null)
- return;
+ private bool on_item_listbox_button_press_event (Gdk.EventButton event) {
+ // Check for right click
+ if ((event.type == Gdk.EventType.BUTTON_PRESS) && (event.button == 3)) {
+ // Make sure that the right-clicked row is also selected
+ var row = this.item_listbox.get_row_at_y((int) event.y);
+ if (row != null) {
+ this.item_listbox.unselect_all();
+ this.item_listbox.select_row(row);
+ }
- GLib.Object obj = KeyManagerStore.get_object_from_path(key_list, path);
- if (obj != null)
- show_properties(obj);
+ // Show context menu (unless no row was right clicked or nothing was selected)
+ var objects = get_selected_item_listbox_items();
+ warning("We have %u selected objects", objects.length());
+ if (objects != null)
+ show_context_menu(null);
+ return true;
+ }
+
+ return false;
}
- private void on_store_row_inserted(Gtk.TreeModel store, Gtk.TreePath path, Gtk.TreeIter iter) {
- check_empty_state();
+ private bool on_item_listbox_popup_menu(Gtk.Widget? listview) {
+ var objects = get_selected_item_listbox_items();
+ if (objects != null)
+ show_context_menu(null);
+ return false;
}
- private void on_store_row_deleted(Gtk.TreeModel store, Gtk.TreePath path) {
- check_empty_state();
+ private GLib.List<weak GLib.Object> get_selected_item_listbox_items() {
+ var rows = this.item_listbox.get_selected_rows();
+ var objects = new GLib.List<weak GLib.Object>();
+
+ foreach (var row in rows)
+ objects.prepend(((KeyManagerItemRow) row).object);
+
+ return objects;
+ }
+
+ public override void selection_changed() {
+ base.selection_changed();
+
+ var objects = get_selected_objects();
+ foreach (weak Backend backend in get_backends())
+ backend.actions.set_actions_for_selected_objects(objects);
}
private void check_empty_state() {
- bool empty = (store.iter_n_children(null) == 0);
+ bool empty = (this.item_list.get_n_items() == 0);
this.show_search_button.sensitive = !empty;
if (!empty) {
- this.content_stack.visible_child_name = "key_list_page";
+ this.content_stack.visible_child_name = "item_listbox_page";
return;
}
@@ -190,27 +201,6 @@ public class Seahorse.KeyManager : Catalog {
this.content_stack.visible_child_name = "empty_state_page";
}
- [GtkCallback]
- private bool on_key_list_button_pressed(Gdk.EventButton event) {
- if (event.button == 3) {
- show_context_menu(event);
- GLib.List<GLib.Object> objects = get_selected_objects();
- if (objects.length() > 1) {
- return true;
- }
- }
-
- return false;
- }
-
- [GtkCallback]
- private bool on_key_list_popup_menu() {
- GLib.List<GLib.Object> objects = get_selected_objects();
- if (objects != null)
- show_context_menu(null);
- return false;
- }
-
private void on_new_item(SimpleAction action, GLib.Variant? param) {
this.new_item_button.activate();
}
@@ -232,7 +222,8 @@ public class Seahorse.KeyManager : Catalog {
[GtkCallback]
private void on_filter_changed(Gtk.Editable entry) {
- this.store.filter = this.filter_entry.text;
+ //XXX
+ this.item_list.filter_text = this.filter_entry.text;
}
public void import_files(string[]? uris) {
@@ -360,9 +351,9 @@ public class Seahorse.KeyManager : Catalog {
SimpleAction action = lookup_action("filter-items") as SimpleAction;
action.set_state(filter_str);
- // Update the store
- this.store.showfilter = KeyManagerStore.ShowFilter.from_string(filter_str);
- this.store.refilter();
+ // Update the item list
+ this.item_list.showfilter = ItemList.ShowFilter.from_string(filter_str);
+ this.item_list.refilter();
}
private void on_show_search(SimpleAction action, Variant? param) {
@@ -378,7 +369,12 @@ public class Seahorse.KeyManager : Catalog {
}
public override GLib.List<GLib.Object> get_selected_objects() {
- return KeyManagerStore.get_selected_objects(this.key_list);
+ var objects = new GLib.List<GLib.Object>();
+
+ foreach (var row in this.item_listbox.get_selected_rows()) {
+ objects.append(((KeyManagerItemRow) row).object);
+ }
+ return objects;
}
private void on_focus_place(SimpleAction action, Variant? param) {
diff --git a/src/meson.build b/src/meson.build
index c5b11df5..6d07c18c 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -8,6 +8,7 @@ seahorse_sources = [
'application.vala',
'import-dialog.vala',
'key-manager.vala',
+ 'key-manager-item-row.vala',
'main.vala',
'search-provider.vala',
'sidebar.vala',
diff --git a/src/seahorse-key-manager.ui b/src/seahorse-key-manager.ui
index 000c2ba9..664f2b73 100644
--- a/src/seahorse-key-manager.ui
+++ b/src/seahorse-key-manager.ui
@@ -398,37 +398,25 @@
</packing>
</child>
<child>
- <object class="GtkFrame">
+ <object class="GtkListBox" id="item_listbox">
<property name="visible">True</property>
<property name="hexpand">True</property>
+ <property name="can-focus">True</property>
+ <property name="has-focus">True</property>
<property name="margin-start">18</property>
<property name="margin-end">18</property>
- <child>
- <object class="GtkTreeView" id="key_list">
- <property name="visible">True</property>
- <property name="enable-search">False</property>
- <property name="show-expanders">False</property>
- <property name="headers-visible">False</property>
- <property name="can_focus">True</property>
- <property name="enable-grid-lines">horizontal</property>
- <signal name="row-activated" handler="on_key_list_row_activated"/>
- <signal name="button-press-event" handler="on_key_list_button_pressed" />
- <signal name="popup-menu" handler="on_key_list_popup_menu" />
- <child internal-child="selection">
- <object class="GtkTreeSelection" id="treeview-selection">
- <property name="mode">multiple</property>
- <signal name="changed" handler="on_view_selection_changed" />
- </object>
- </child>
- </object>
- </child>
+ <property name="activate-on-single-click">False</property>
+ <property name="selection-mode">multiple</property>
+ <style>
+ <class name="seahorse-item-listbox"/>
+ </style>
</object>
</child>
</object>
</child>
</object>
<packing>
- <property name="name">key_list_page</property>
+ <property name="name">item_listbox_page</property>
</packing>
</child>
<child>
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]