[seahorse/wip/nielsdg/seahorse-listview: 1/3] common: Add SeahorseItemList



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]