[seahorse/wip/nielsdg/seahorse-listview: 35/35] Use a GtkListBox instead of a GtkTreeView



commit 334b7d5383071081c0b51434e2e20a68c36667c6
Author: Niels De Graef <nielsdegraef gmail com>
Date:   Sun Mar 10 09:41:26 2019 +0100

    Use a GtkListBox instead of a GtkTreeView
    
    The biggest reason to get rid of some of our `GtkTreeView`s (which are
    supported by `GcrCollectionModel`), as they're leading to bugs when
    scrolling or selecting items.
    
    The first element to accomplish this is a new object: `SeahorseItemList`
    `SeahorseItemList` is a wrapper to easily combine a `GtkListBox` and a
    `GcrCollectionModel`. In the long term, we probably want to get rid of
    the latter in favor of `GListModel`s everywhere, but this will also need
    changes (and the appropriate deprecations) in libgcr.
    
    Second, we create a `KeyManagerItemRow`, which represents a widget in
    the main window (the `KeyManager`), rather than using custom
    `GtkCellRenderer`s.
    
    Third, we rewrite the `SideBar` to also make use of an internal
    `GListStore`, combined with a `GcrUnionCollection` to keep the
    transition smaller.
    
    Note that this is one big commit, which I normally really dislike, but
    it was the only practical way, since a lot of the things were linked
    together in some unfathomable way, which meant that changing something
    could break the code somewhere completely else.

 common/catalog.vala           |   1 -
 common/item-list.vala         | 226 ++++++++++
 common/key-manager-store.vala | 496 ---------------------
 common/meson.build            |   2 +-
 gkr/gkr-backend.vala          |  30 +-
 libseahorse/seahorse.css      |  39 ++
 src/key-manager-item-row.vala |  68 +++
 src/key-manager.vala          | 154 ++++---
 src/meson.build               |   1 +
 src/seahorse-key-manager.ui   |  30 +-
 src/sidebar.vala              | 994 ++++++++++++------------------------------
 11 files changed, 720 insertions(+), 1321 deletions(-)
---
diff --git a/common/catalog.vala b/common/catalog.vala
index f8f9a911..2cff02e5 100644
--- a/common/catalog.vala
+++ b/common/catalog.vala
@@ -30,7 +30,6 @@ public abstract class Catalog : Gtk.ApplicationWindow {
     private GLib.Settings _settings;
 
     public abstract GLib.List<weak Backend> get_backends();
-    public abstract Place? get_focused_place();
     public abstract GLib.List<GLib.Object> get_selected_objects();
 
     private const ActionEntry[] ACTION_ENTRIES = {
diff --git a/common/item-list.vala b/common/item-list.vala
new file mode 100644
index 00000000..55d4cd3e
--- /dev/null
+++ b/common/item-list.vala
@@ -0,0 +1,226 @@
+/*
+ * 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 */
+    public Gcr.Collection base_collection { get; private set; }
+    // XXX put back on private
+
+    /** 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
+        var len = this.items.length;
+        this.items.remove_range(0, len);
+        items_changed(0, len, 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());
+    }
+}
diff --git a/common/meson.build b/common/meson.build
index 36788790..9a2b622c 100644
--- a/common/meson.build
+++ b/common/meson.build
@@ -10,8 +10,8 @@ common_sources = [
   'exportable.vala',
   'exporter.vala',
   'icons.vala',
-  'key-manager-store.vala',
   'interaction.vala',
+  'item-list.vala',
   'lockable.vala',
   'object.vala',
   'passphrase-prompt.vala',
diff --git a/gkr/gkr-backend.vala b/gkr/gkr-backend.vala
index 44bf065e..ad70bf6c 100644
--- a/gkr/gkr-backend.vala
+++ b/gkr/gkr-backend.vala
@@ -115,18 +115,20 @@ public class Backend: GLib.Object , Gcr.Collection, Seahorse.Backend {
                        if (this._aliases.lookup("session") == object_path)
                                continue;
 
-                       seen.add(object_path);
-                       if (this._keyrings.lookup(object_path) == null) {
-                               this._keyrings.insert(object_path, (Keyring)keyring);
+                       var uri = "secret-service://%s".printf(object_path);
+                       seen.add(uri);
+                       if (this._keyrings.lookup(uri) == null) {
+                               this._keyrings.insert(uri, (Keyring)keyring);
                                emit_added(keyring);
                        }
                }
 
                /* Remove any that we didn't find */
                var iter = GLib.HashTableIter<string, Keyring>(this._keyrings);
-               while (iter.next(out object_path, null)) {
-                       if (!seen.contains(object_path)) {
-                               var keyring = this._keyrings.lookup(object_path);
+               string uri;
+               while (iter.next(out uri, null)) {
+                       if (!seen.contains(uri)) {
+                               var keyring = this._keyrings.lookup(uri);
                                iter.remove();
                                emit_removed(keyring);
                        }
@@ -146,13 +148,13 @@ public class Backend: GLib.Object , Gcr.Collection, Seahorse.Backend {
                return get_keyrings();
        }
 
-       public bool contains(GLib.Object object) {
-               if (object is Keyring) {
-                       var keyring = (Keyring)object;
-                       return this._keyrings.lookup(keyring.uri) == keyring;
-               }
-               return false;
-       }
+    public bool contains(GLib.Object object) {
+        var keyring = object as Gkr.Keyring;
+        if (keyring == null)
+            return false;
+
+        return this._keyrings.lookup(keyring.uri) == keyring;
+    }
 
        public Place? lookup_place(string uri) {
                return this._keyrings.lookup(uri);
@@ -169,7 +171,7 @@ public class Backend: GLib.Object , Gcr.Collection, Seahorse.Backend {
                return Backend._instance;
        }
 
-       public GLib.List<weak Keyring> get_keyrings() {
+       public GLib.List<unowned Keyring> get_keyrings() {
                return this._keyrings.get_values();
        }
 
diff --git a/libseahorse/seahorse.css b/libseahorse/seahorse.css
index 448eb001..e0dfcbc7 100644
--- a/libseahorse/seahorse.css
+++ b/libseahorse/seahorse.css
@@ -5,6 +5,45 @@
     border-radius: 0;
 }
 
+.seahorse-sidebar-item-header {
+  margin-top: 6px;
+  font-weight: bold;
+}
+
+.seahorse-sidebar-item {
+  margin: 0 3px 0 12px;
+}
+
+.seahorse-sidebar-item > label {
+  margin: 3px 0 3px 0;
+}
+
+.seahorse-sidebar-item > button {
+  padding: 0;
+  margin: 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..0fdf09ac
--- /dev/null
+++ b/src/key-manager-item-row.vala
@@ -0,0 +1,68 @@
+/*
+ * 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.valign = Gtk.Align.START;
+        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 a900a67d..6d302ce1 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) => { return 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);
@@ -138,55 +129,76 @@ public class Seahorse.KeyManager : Catalog {
             get_style_context().add_class("devel");
     }
 
-    [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();
+            debug("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);
+        debug("Checking empty state: %s", empty.to_string());
 
         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;
         }
 
         // We have an empty page, that might still have 2 reasons:
         // - we really have no items in our collections
         // - we're dealing with a locked keyring
-        Place? place = get_focused_place();
+        Place? place = this.sidebar.get_focused_place();
         if (place != null && place is Lockable && ((Lockable) place).unlockable) {
             this.content_stack.visible_child_name = "locked_keyring_page";
             return;
@@ -194,27 +206,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();
     }
@@ -236,7 +227,7 @@ public class Seahorse.KeyManager : Catalog {
 
     [GtkCallback]
     private void on_filter_changed(Gtk.Editable entry) {
-        this.store.filter = this.filter_entry.text;
+        this.item_list.filter_text = this.filter_entry.text;
     }
 
     public void import_files(string[]? uris) {
@@ -361,9 +352,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) {
@@ -379,7 +370,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) {
@@ -389,17 +385,13 @@ public class Seahorse.KeyManager : Catalog {
         }
     }
 
-    public override Place? get_focused_place() {
-        return this.sidebar.get_focused_place();
-    }
-
     private Gcr.Collection setup_sidebar() {
         this.sidebar = new Sidebar();
         sidebar.hexpand = true;
 
         /* Make sure we update the empty state on any change */
-        this.sidebar.get_selection().changed.connect((sel) => check_empty_state());
-        this.sidebar.current_collection_changed.connect (() => check_empty_state ());
+        this.sidebar.selected_rows_changed.connect((sidebar) => { check_empty_state(); });
+        this.sidebar.current_collection_changed.connect((sidebar) => { check_empty_state (); });
 
         this.sidebar_panes.position = this.settings.get_int("sidebar-width");
         this.sidebar_panes.realize.connect(() =>   { this.sidebar_panes.position = 
this.settings.get_int("sidebar-width"); });
@@ -416,7 +408,7 @@ public class Seahorse.KeyManager : Catalog {
 
         this.settings.bind("keyrings-selected", this.sidebar, "selected-uris", SettingsBindFlags.DEFAULT);
 
-        return this.sidebar.collection;
+        return this.sidebar.objects;
     }
 
     public override List<weak Backend> get_backends() {
@@ -431,7 +423,7 @@ public class Seahorse.KeyManager : Catalog {
 
     [GtkCallback]
     private void on_locked_keyring_unlock_button_clicked(Gtk.Button unlock_button) {
-        Lockable? place = get_focused_place() as Lockable;
+        Lockable? place = this.sidebar.get_focused_place() as Lockable;
         return_if_fail(place != null && place.unlockable);
 
         unlock_button.sensitive = false;
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>
diff --git a/src/sidebar.vala b/src/sidebar.vala
index f7a0b2f9..76dc73cb 100644
--- a/src/sidebar.vala
+++ b/src/sidebar.vala
@@ -18,47 +18,15 @@
  * <http://www.gnu.org/licenses/>.
  */
 
-public class Seahorse.Sidebar : Gtk.TreeView {
+public class Seahorse.Sidebar : Gtk.ListBox {
 
-    private const int ACTION_BUTTON_XPAD = 6;
-
-    private Gtk.ListStore store = new Gtk.ListStore.newv(Column.types());
+    private GLib.ListStore store = new GLib.ListStore(typeof(Seahorse.Place));
     private List<Backend> backends = new List<Backend>();
-    private Gcr.UnionCollection objects = new Gcr.UnionCollection();
-
-    // The selection
-    private HashTable<Gcr.Collection, Gcr.Collection> selection
-         = new HashTable<Gcr.Collection, Gcr.Collection>(direct_hash, direct_equal);
-    private bool updating;
-
-    // A set of chosen uris, used with settings
-    private GenericSet<string?> chosen = new GenericSet<string?>(str_hash, str_equal);
-
-    // Action icons
-    private Gdk.Pixbuf? pixbuf_lock;
-    private Gdk.Pixbuf? pixbuf_unlock;
-    private Gdk.Pixbuf? pixbuf_lock_l;
-    private Gdk.Pixbuf? pixbuf_unlock_l;
-    private Gtk.TreePath? action_highlight_path;
-    private Gtk.CellRendererPixbuf action_cell_renderer;
-    private int action_button_size;
-
-    private uint update_places_sig;
 
     /**
      * Collection of objects sidebar represents
      */
-    public Gcr.Collection collection {
-        get { return this.objects; }
-    }
-
-    /**
-     * The URIs selected by the user
-     */
-    public string[] selected_uris {
-        owned get { return chosen_uris_to_array(); }
-        set { replace_chosen_uris(value); }
-    }
+    public Gcr.UnionCollection objects { get; private set; default = new Gcr.UnionCollection(); }
 
     /**
      * Collection shows all objects combined
@@ -68,7 +36,7 @@ public class Seahorse.Sidebar : Gtk.TreeView {
         set {
             if (this._combined != value) {
                 this._combined = value;
-                update_objects_in_collection(false);
+                on_row_selected (get_selected_row());
             }
         }
     }
@@ -80,116 +48,20 @@ public class Seahorse.Sidebar : Gtk.TreeView {
      */
     public signal void current_collection_changed();
 
-    private enum RowType {
-        BACKEND,
-        PLACE,
-    }
-
-    private enum Column {
-        ROW_TYPE,
-        ICON,
-        LABEL,
-        TOOLTIP,
-        CATEGORY,
-        COLLECTION,
-        URI,
-        N_COLUMNS;
-
-        public static Type[] types() {
-            return {
-                typeof(uint),
-                typeof(Icon),
-                typeof(string),
-                typeof(string),
-                typeof(string),
-                typeof(Gcr.Collection),
-                typeof(string)
-            };
-        }
-    }
+    construct {
+        this.selection_mode = Gtk.SelectionMode.BROWSE;
 
-    public Sidebar() {
-        /* get_style_context().set_junction_sides(Gtk.JunctionSides.RIGHT | Gtk.JunctionSides.LEFT); */
-
-        // tree view
-        Gtk.TreeViewColumn col = new Gtk.TreeViewColumn();
-
-        // initial padding
-        Gtk.CellRenderer cell = new Gtk.CellRendererText();
-        col.pack_start(cell, false);
-        cell.xpad = 6;
-
-        // headings
-        Gtk.CellRendererText headings_cell = new Gtk.CellRendererText();
-        col.pack_start(headings_cell, false);
-        col.set_attributes(headings_cell, "text", Column.LABEL, null);
-        headings_cell.weight = Pango.Weight.BOLD;
-        headings_cell.weight_set = true;
-        headings_cell.ypad = 6;
-        headings_cell.xpad = 0;
-        col.set_cell_data_func(headings_cell, on_cell_renderer_heading_visible);
-
-        // icon padding
-        cell = new Gtk.CellRendererText();
-        col.pack_start(cell, false);
-        col.set_cell_data_func(cell, on_padding_cell_renderer);
-
-        // icon renderer
-        cell = new Gtk.CellRendererPixbuf();
-        col.pack_start(cell, false);
-        col.set_attributes(cell, "gicon", Column.ICON, null);
-        col.set_cell_data_func(cell, on_cell_renderer_heading_not_visible);
-
-        // normal text renderer
-        Gtk.CellRendererText text_cell = new Gtk.CellRendererText();
-        col.pack_start(text_cell, true);
-        text_cell.editable = false;
-        col.set_attributes(text_cell, "text", Column.LABEL, null);
-        col.set_cell_data_func(text_cell, on_cell_renderer_heading_not_visible);
-        text_cell.ellipsize = Pango.EllipsizeMode.END;
-        text_cell.ellipsize_set = true;
-
-        // lock/unlock icon renderer
-        this.action_cell_renderer = new Gtk.CellRendererPixbuf();
-        this.action_cell_renderer.mode = Gtk.CellRendererMode.ACTIVATABLE;
-        this.action_cell_renderer.stock_size = Gtk.IconSize.MENU;
-        this.action_cell_renderer.xpad = ACTION_BUTTON_XPAD;
-        this.action_cell_renderer.xalign = 1.0f;
-        col.pack_start(this.action_cell_renderer, false);
-        col.set_cell_data_func(this.action_cell_renderer, on_cell_renderer_action_icon);
-        col.set_max_width(24);
-        append_column(col);
-
-        set_headers_visible(false);
-        set_tooltip_column(Column.TOOLTIP);
-        set_model(this.store);
-        this.popup_menu.connect(on_popup_menu);
-        this.button_press_event.connect(on_button_press_event);
-        this.motion_notify_event.connect(on_motion_notify_event);
-        this.button_release_event.connect(on_button_release_event);
-
-        Gtk.TreeSelection selection = get_selection();
-        selection.set_mode(Gtk.SelectionMode.MULTIPLE);
-        selection.set_select_function(on_tree_selection_validate);
-        selection.changed.connect(() => update_objects_for_selection(selection));
+        bind_model(this.store, place_widget_create_cb);
+        set_header_func(place_header_cb);
+        this.row_selected.connect(on_row_selected);
 
         load_backends();
     }
 
     ~Sidebar() {
-        foreach (Backend backend in this.backends) {
-            SignalHandler.disconnect_by_func((void*) backend, (void*) on_place_added, this);
-            SignalHandler.disconnect_by_func((void*) backend, (void*) on_place_removed, this);
-            SignalHandler.disconnect_by_func((void*) backend, (void*) on_backend_changed, this);
-
+        foreach (Backend backend in this.backends)
             foreach (weak GLib.Object obj in backend.get_objects())
                 on_place_removed (backend, (Place) obj);
-        }
-
-        invalidate_sidebar_pixbufs();
-
-        if (this.update_places_sig != 0)
-            Source.remove(this.update_places_sig);
     }
 
     private void load_backends() {
@@ -210,462 +82,362 @@ public class Seahorse.Sidebar : Gtk.TreeView {
         });
     }
 
-    private void on_place_added(Gcr.Collection? places, GLib.Object obj) {
-        ((Place) obj).notify.connect(() => update_places_later());
-        update_places_later();
-    }
-
-    private void on_place_removed(Gcr.Collection? places, GLib.Object obj) {
-        SignalHandler.disconnect_by_func((void*) obj, (void*) update_places_later, this);
-        update_places_later();
-    }
+    private void on_place_added(Gcr.Collection? backend, GLib.Object place_obj) {
+        var place = place_obj as Place;
+        return_if_fail (place != null);
 
-    private void on_backend_changed(GLib.Object obj, ParamSpec spec) {
-        update_places_later();
+        debug("New place '%s' added", place.label);
+        this.store.insert_sorted(place, compare_places);
+        place.notify.connect(on_place_changed);
     }
 
-    private static int order_from_backend (Backend backend) {
-        switch (backend.name) {
-            case "gkr":
-                return 0;
-            case "pgp":
-                return 1;
-            case "pkcs11":
-                return 2;
-            case "ssh":
-                return 3;
-            default:
-                return 10;
-        }
+    private void on_place_changed(GLib.Object obj, ParamSpec pspec) {
+        update_places();
     }
 
-    private void ensure_sidebar_pixbufs() {
-        if (this.pixbuf_lock != null && this.pixbuf_lock_l != null
-            && this.pixbuf_unlock_l != null && this.pixbuf_unlock != null)
-            return;
-
-        Gtk.IconTheme icon_theme = Gtk.IconTheme.get_default();
-        Gtk.StyleContext style = get_style_context();
-
-        int height;
-        if (!Gtk.icon_size_lookup(Gtk.IconSize.MENU, out this.action_button_size, out height))
-            this.action_button_size = 16;
-
-        // Lock icon
-        Icon icon = new ThemedIcon.with_default_fallbacks("changes-prevent-symbolic");
-        Gtk.IconInfo? icon_info = icon_theme.lookup_by_gicon(icon, this.action_button_size, 
Gtk.IconLookupFlags.FORCE_SYMBOLIC);
-        if (icon_info == null)
-            return;
-        try {
-            if (this.pixbuf_lock == null)
-                this.pixbuf_lock = icon_info.load_symbolic_for_context(style, null);
-            if (this.pixbuf_lock_l == null)
-                this.pixbuf_lock_l = create_spotlight_pixbuf(this.pixbuf_lock);
-        } catch (Error e) {
-            debug("Error while looking up lock icon: %s", e.message);
-        }
-
-        // Unlock icon
-        icon = new ThemedIcon.with_default_fallbacks("changes-allow-symbolic");
-        icon_info = icon_theme.lookup_by_gicon(icon, this.action_button_size, 
Gtk.IconLookupFlags.FORCE_SYMBOLIC);
-        if (icon_info == null)
-            return;
-        try {
-            if (this.pixbuf_unlock == null)
-                this.pixbuf_unlock = icon_info.load_symbolic_for_context(style, null);
-            if (this.pixbuf_unlock_l == null)
-                this.pixbuf_unlock_l = create_spotlight_pixbuf(this.pixbuf_unlock);
-        } catch (Error e) {
-            debug("Error while looking up unlock icon: %s", e.message);
-        }
-    }
+    private void on_place_removed(Gcr.Collection? backend, GLib.Object place_obj) {
+        var place = place_obj as Place;
+        return_if_fail (place != null);
 
-    private Gdk.Pixbuf? create_spotlight_pixbuf (Gdk.Pixbuf? src) {
-        Gdk.Pixbuf? dest = new Gdk.Pixbuf(src.colorspace, src.has_alpha, src.bits_per_sample,
-                                          src.width, src.height);
-
-        bool has_alpha = src.has_alpha;
-        int width = src.width;
-        int height = src.height;
-        int dst_row_stride = dest.rowstride;
-        int src_row_stride = src.rowstride;
-        uint8* target_pixels = dest.pixels;
-        uint8* original_pixels = src.pixels;
-
-        for (int i = 0; i < height; i++) {
-            uint8* pixdest = target_pixels + i * dst_row_stride;
-            uint8* pixsrc = original_pixels + i * src_row_stride;
-            for (int j = 0; j < width; j++) {
-                *pixdest++ = lighten_component (*pixsrc++);
-                *pixdest++ = lighten_component (*pixsrc++);
-                *pixdest++ = lighten_component (*pixsrc++);
-                if (has_alpha) {
-                    *pixdest++ = *pixsrc++;
-                }
+        debug("Place '%s' removed", place.label);
+        for (uint i = 0; i < this.store.get_n_items(); i++) {
+            if (this.store.get_item(i) == place) {
+                this.store.remove(i);
+                break;
             }
         }
-        return dest;
     }
 
-    private uint8 lighten_component(uint8 cur_value) {
-        int new_value = cur_value + 24 + (cur_value >> 3);
-        return (new_value > 255)? (uint8)255 : (uint8)new_value;
+    private void on_backend_changed(GLib.Object obj, ParamSpec spec) {
+        debug("Backend changed");
+        update_places();
     }
 
-    private void invalidate_sidebar_pixbufs() {
-        this.pixbuf_lock = null;
-        this.pixbuf_unlock = null;
-        this.pixbuf_lock_l = null;
-        this.pixbuf_unlock_l = null;
+    private Gtk.Widget place_widget_create_cb(GLib.Object object) {
+        var item = new SidebarItem(object as Seahorse.Place);
+        item.place_changed.connect(on_sidebar_item_changed);
+        return item;
     }
 
-    private void next_or_append_row(Gtk.ListStore? store, ref Gtk.TreeIter iter, string? category,
-                                    Gcr.Collection? collection) {
-        // We try to keep the same row in order to preserve checked state
-        // and selections. So if the next row matches the values we want to
-        // set on it, then just keep that row.
-        //
-        // This is complicated by the fact that the first row being inserted
-        // doesn't have a valid iter, and we don't have a standard way to
-        // detect that an iter isn't valid.
-
-        // A marker that tells us the iter is not yet valid
-        if (iter.stamp == int.from_pointer(&iter) && iter.user_data3 == (&iter) &&
-            iter.user_data2 == (&iter) && iter.user_data == (&iter)) {
-            if (!store.get_iter_first(out iter))
-                store.append(out iter);
-            return;
-        }
-
-        if (!store.iter_next(ref iter)) {
-            store.append(out iter);
-            return;
-        }
+    private void on_sidebar_item_changed(SidebarItem item) {
+        select_row(item);
+        current_collection_changed();
+    }
 
-        for (;;) {
-            string? row_category;
-            Gcr.Collection? row_collection;
-            store.get(iter, Column.CATEGORY, out row_category,
-                            Column.COLLECTION, out row_collection);
+    private void place_header_cb(Gtk.ListBoxRow row, Gtk.ListBoxRow? before) {
+        Seahorse.Place place = ((SidebarItem) row).place;
+        string scheme = Uri.parse_scheme(place.uri);
 
-            if (row_category == category && row_collection == collection)
+        // We don't need a title iff
+        // * there is no previous row
+        // * the previous row is from another backend
+        if (before != null) {
+            Seahorse.Place before_place = ((SidebarItem) before).place;
+            if (Uri.parse_scheme(before_place.uri) == scheme)
                 return;
+        }
 
-            if (!store.remove(ref iter)) {
-                store.append(out iter);
+        // Find the backend that has the given scheme
+        foreach (var b in this.backends) {
+            if (place in b) {
+                var label = new Gtk.Label(b.label);
+                label.tooltip_text = b.description;
+                label.get_style_context().add_class("seahorse-sidebar-item-header");
+                label.xalign = 0f;
+                label.margin_start = 6;
+                label.margin_top = 6;
+                label.show();
+                row.set_header(label);
                 return;
             }
         }
+
+        warning("Couldn't find backend for place %s", place.label);
     }
 
-    private void update_objects_in_collection(bool update_chosen) {
-        if (this.updating) // Updating collection is blocked
-            return;
+    private int compare_places(GLib.Object obj_a, GLib.Object obj_b) {
+        Seahorse.Place a = (Seahorse.Place) obj_a;
+        Seahorse.Place b = (Seahorse.Place) obj_b;
 
-        bool changed = false;
-        foreach (Backend backend in this.backends) {
-            foreach (weak GLib.Object obj in backend.get_objects()) {
-                Place place = (Place) obj;
-                bool include = this.selection.lookup(place) != null;
-
-                if (update_chosen) {
-                    string? uri = place.uri;
-                    bool have = (uri in this.chosen);
-                    if (include && !have) {
-                        this.chosen.add(uri);
-                        changed = true;
-                    } else if (!include && have) {
-                        this.chosen.remove(uri);
-                        changed = true;
-                    }
-                }
+        // First of all, order the backends (SSH vs GPG)
+        // Since there is no easy way to map a place to its original backend,
+        // we can use the URI scheme
+        var a_scheme = GLib.Uri.parse_scheme(a.uri);
+        var b_scheme = GLib.Uri.parse_scheme(b.uri);
+        if (a_scheme != b_scheme)
+            return order_from_scheme(b_scheme) - order_from_scheme(a_scheme);
 
-                // Combined overrides and shows all objects
-                if (this.combined)
-                    include = true;
+        // In the same backend, order alphabetically
+        return a.label.casefold().collate(b.label.casefold());
+    }
 
-                bool have = this.objects.have(place);
-                if (include && !have)
-                    this.objects.add(place);
-                else if (!include && have)
-                    this.objects.remove(place);
-            }
-        }
+    private struct BackendEntry {
+        unowned string name;
+        unowned string scheme;
     }
+    // Note that this is really the reverse order
+    const BackendEntry[] BACKEND_ORDER = {
+        { "pkcs11", "pkcs11" },
+        { "pgp", "gnupg" },
+        { "ssh", "openssh" },
+        { "gkr", "secret-service" },
+    };
 
-    private void update_objects_for_selection(Gtk.TreeSelection selection) {
-        if (this.updating)
-            return;
+    private static int order_from_backend (Backend backend) {
+        for (int i = 0; i < BACKEND_ORDER.length; i++)
+            if (backend.name == BACKEND_ORDER[i].name)
+                return i;
 
-        HashTable<Gcr.Collection, Gcr.Collection> selected = new HashTable<Gcr.Collection, 
Gcr.Collection>(direct_hash, direct_equal);
-        selection.selected_foreach((model, path, iter) => {
-            Gcr.Collection? collection = null;
-            model.get(iter, Column.COLLECTION, out collection, -1);
-            if (collection != null)
-                selected.insert(collection, collection);
-        });
+        return BACKEND_ORDER.length + 1;
+    }
 
-        this.selection = selected;
+    private static int order_from_scheme(string scheme) {
+        for (int i = 0; i < BACKEND_ORDER.length; i++)
+            if (scheme == BACKEND_ORDER[i].scheme)
+                return i;
 
-        if (!this.combined)
-            update_objects_in_collection(true);
+        return BACKEND_ORDER.length + 1;
     }
 
-    private void update_objects_for_chosen(GenericSet<string?> chosen) {
-        this.updating = true;
+    private void on_row_selected(Gtk.ListBoxRow? row) {
+        debug("Updating objects (combined: %s)", this.combined.to_string());
 
-        Gtk.TreeSelection selection = get_selection();
+        // First clear the list
+        foreach (var place in this.objects.elements())
+            this.objects.remove(place);
 
-        // Update the display
-        Gtk.TreeIter iter;
-        if (this.store.get_iter_first(out iter)) {
-            do {
-                Gcr.Collection? collection = null;
-                string? uri = null;
-                this.store.get(iter, Column.COLLECTION, out collection,
-                                     Column.URI, out uri, -1);
-
-                if (collection != null && uri != null) {
-                    if (uri in chosen)
-                        selection.select_iter(iter);
-                    else
-                        selection.unselect_iter(iter);
+        // Combined overrides and shows all objects
+        if (this.combined) {
+            foreach (Backend backend in this.backends) {
+                foreach (var obj in backend.get_objects()) {
+                    var place = (Place) obj;
+                    if (!this.objects.have(place))
+                        this.objects.add(place);
                 }
-            } while (this.store.iter_next(ref iter));
+            }
+            return;
         }
 
-        this.updating = false;
-        update_objects_for_selection(selection);
+        // Only selected ones should be in this.objects
+        var selected = row as SidebarItem;
+        if (selected == null)
+            return;
+
+        foreach (var place in this.objects.elements()) {
+            if (selected.place != place)
+                this.objects.remove(place);
+        }
+        if (!this.objects.have(selected.place))
+            this.objects.add(selected.place);
     }
 
     private void update_places() {
-        Gtk.TreeIter iter = Gtk.TreeIter();
-        iter.stamp = int.from_pointer(&iter); // A marker that tells us the iter is not yet valid
-        iter.user_data3 = iter.user_data2 = iter.user_data = &iter;
+        // Save current selection
+        var old_selected = get_selected_row() as SidebarItem;
+        Place? place = null;
+        if (old_selected != null)
+            place = old_selected.place;
 
         foreach (Backend backend in this.backends)
-            update_backend(backend, ref iter);
+            update_backend(backend);
 
-        // Update selection
-        update_objects_for_chosen(this.chosen);
+        this.store.sort(compare_places);
 
-        if (this.combined)
-            update_objects_in_collection(false);
+        // Restore selection -- this got cleared by the call to sort()
+        Gtk.ListBoxRow? new_row = null;
+        foreach (var row in this.get_children()) {
+            if (((SidebarItem)row).place == place) {
+                new_row = (Gtk.ListBoxRow) row;
+                break;
+            }
+        }
+        select_row(new_row ?? get_row_at_index(0));
     }
 
-    private void update_backend(Backend? backend, ref Gtk.TreeIter iter) {
+    private void update_backend(Backend? backend) {
         if (backend.get_objects() == null) // Ignore categories that have nothing
             return;
 
-        next_or_append_row(this.store, ref iter, backend.name, backend);
-        this.store.set(iter, Column.ROW_TYPE, RowType.BACKEND,
-                             Column.CATEGORY, backend.name,
-                             Column.LABEL, backend.label,
-                             Column.TOOLTIP, backend.description,
-                             Column.COLLECTION, backend);
-
         foreach (weak GLib.Object obj in backend.get_objects()) {
-            Place place = obj as Place;
+            unowned Place? place = obj as Place;
             if (place == null)
                 continue;
 
-            next_or_append_row(this.store, ref iter, backend.name, place);
-            this.store.set(iter, Column.ROW_TYPE, RowType.PLACE,
-                                 Column.CATEGORY, backend.name,
-                                 Column.LABEL, place.label,
-                                 Column.TOOLTIP, place.description,
-                                 Column.ICON, place.icon,
-                                 Column.COLLECTION, place,
-                                 Column.URI, place.uri);
-        }
-    }
+            bool already_in = false;
+            for (int i = 0; i < this.store.get_n_items(); i++) {
+                if (this.store.get_object(i) == place) {
+                    already_in = true;
+                    break;
+                }
+            }
 
-    private void update_places_later() {
-        if (this.update_places_sig == 0) {
-            this.update_places_sig = Idle.add(() => {
-                this.update_places_sig = 0;
-                update_places();
-                return false; // don't call again
-            });
+            if (!already_in)
+                this.store.insert_sorted(place, compare_places);
         }
     }
 
-    private Lockable? lookup_lockable_for_iter(Gtk.TreeModel? model, Gtk.TreeIter? iter) {
-        Gcr.Collection? collection = null;
-        model.get(iter, Column.COLLECTION, out collection, -1);
+    public override bool popup_menu() {
+        if (base.popup_menu())
+            return true;
 
-        return collection as Lockable;
+        var row = get_selected_row() as SidebarItem;
+        if (row == null)
+            return false;
+
+        row.show_popup_menu();
+        return true;
     }
 
-    private void on_cell_renderer_action_icon(Gtk.CellLayout layout, Gtk.CellRenderer? cell,
-                                              Gtk.TreeModel? model, Gtk.TreeIter? iter) {
-        bool can_lock = false;
-        bool can_unlock = false;
+    public override bool button_press_event(Gdk.EventButton event) {
+        if (base.button_press_event(event))
+            return true;
 
-        Lockable? lockable = lookup_lockable_for_iter(model, iter);
-        if (lockable != null) {
-            can_lock = lockable.lockable;
-            can_unlock = lockable.unlockable;
-        }
+        if (event.button != 3 || event.type != Gdk.EventType.BUTTON_PRESS)
+            return false;
 
-        if (can_lock || can_unlock) {
-            ensure_sidebar_pixbufs();
+        var row = get_row_at_y((int) event.y) as SidebarItem;
+        if (row != null)
+            row.show_popup_menu();
 
-            bool highlight = false;
-            if (this.action_highlight_path != null) {
-                Gtk.TreePath? path = model.get_path(iter);
-                highlight = path.compare(this.action_highlight_path) == 0;
-            }
+        return true;
+    }
 
-            Gdk.Pixbuf? pixbuf;
-            if (can_lock)
-                pixbuf = highlight ? this.pixbuf_unlock : this.pixbuf_unlock_l;
-            else
-                pixbuf = highlight ? this.pixbuf_lock : this.pixbuf_lock_l;
+    public List<weak Gcr.Collection>? get_selected_places() {
+        List<weak Gcr.Collection>? places = null;
 
-            this.action_cell_renderer.visible = true;
-            this.action_cell_renderer.pixbuf = pixbuf;
-        } else {
-            this.action_cell_renderer.visible = false;
-            this.action_cell_renderer.pixbuf = null;
+        foreach (var row in get_selected_rows()) {
+            var item = row as SidebarItem;
+            if (item != null)
+                places.append(item.place);
         }
-    }
 
-    private void on_cell_renderer_heading_visible(Gtk.CellLayout layout, Gtk.CellRenderer? cell,
-                                                  Gtk.TreeModel? model, Gtk.TreeIter? iter) {
-        RowType type;
-        model.get(iter, Column.ROW_TYPE, out type, -1);
-        cell.visible = (type == RowType.BACKEND);
+        return places;
     }
 
-    private void on_padding_cell_renderer(Gtk.CellLayout layout, Gtk.CellRenderer? cell,
-                                          Gtk.TreeModel? model, Gtk.TreeIter? iter) {
-        RowType type;
-        model.get(iter, Column.ROW_TYPE, out type, -1);
+    public Place? get_focused_place() {
+        var row = get_selected_row() as SidebarItem;
+        return (row != null)? row.place : null;
+    }
 
-        if (type == RowType.BACKEND) {
-            cell.visible = false;
-            cell.xpad = 0;
-            cell.ypad = 0;
-        } else {
-            cell.visible = true;
-            cell.xpad = 3;
-            cell.ypad = 3;
+    public void set_focused_place(string uri_prefix) {
+        foreach (var row in get_children()) {
+            var item = (SidebarItem) row;
+            if (item.place.uri.has_prefix(uri_prefix)) {
+                select_row(item);
+                break;
+            }
         }
     }
 
-    private void on_cell_renderer_heading_not_visible(Gtk.CellLayout layout, Gtk.CellRenderer? cell,
-                                                      Gtk.TreeModel? model, Gtk.TreeIter? iter) {
-        RowType type;
-        model.get(iter, Column.ROW_TYPE, out type, -1);
-        cell.visible = (type != RowType.BACKEND);
+    public List<weak Backend>? get_backends() {
+        return this.backends.copy();
     }
+}
 
-    private bool on_tree_selection_validate(Gtk.TreeSelection selection, Gtk.TreeModel? model,
-                                            Gtk.TreePath? path, bool path_currently_selected) {
-        Gtk.TreeIter iter;
-        model.get_iter(out iter, path);
+internal class Seahorse.SidebarItem : Gtk.ListBoxRow {
 
-        RowType row_type;
-        model.get(iter, Column.ROW_TYPE, out row_type, -1);
-        if (row_type == RowType.BACKEND)
-            return false;
+    private Gtk.Button? lock_button = null;
 
-        return true;
-    }
+    public weak Seahorse.Place place { get; construct set; }
 
-    private void place_lock(Lockable lockable, Gtk.Window? window) {
-        Cancellable cancellable = new Cancellable();
-        TlsInteraction interaction = new Interaction(window);
+    public signal void place_changed();
 
-        lockable.lock.begin(interaction, cancellable, (obj, res) => {
-            try {
-                lockable.lock.end(res);
-                current_collection_changed();
-            } catch (Error e) {
-                Util.show_error(window, _("Couldn’t lock"), e.message);
-            }
-        });
-    }
+    construct {
+      var grid = new Gtk.Grid();
+      grid.get_style_context().add_class("seahorse-sidebar-item");
+      grid.valign = Gtk.Align.CENTER;
+      grid.row_spacing = 6;
+      grid.column_spacing = 6;
+      add(grid);
+
+      var icon = new Gtk.Image.from_gicon(place.icon, Gtk.IconSize.BUTTON);
+      grid.attach(icon, 0, 0);
+
+      var label = new Gtk.Label(place.label);
+      label.hexpand = true;
+      label.ellipsize = Pango.EllipsizeMode.END;
+      label.xalign = 0f;
+      grid.attach(label, 1, 0);
 
-    private void on_place_lock(Gtk.MenuItem item, Lockable lockable) {
-        place_lock(lockable, (Gtk.Window) item.get_toplevel());
+      var lockable = place as Lockable;
+      if (lockable != null && (lockable.lockable || lockable.unlockable)) {
+          this.lock_button = new Gtk.Button.from_icon_name(get_lock_icon_name(lockable),
+                                                          Gtk.IconSize.BUTTON);
+          this.lock_button.get_style_context().add_class("flat");
+          this.lock_button.clicked.connect((b) => {
+              if (lockable.unlockable)
+                  place_unlock(lockable, (Gtk.Window) get_toplevel());
+              else if (lockable.lockable)
+                  place_lock(lockable, (Gtk.Window) get_toplevel());
+
+              update_lock_icon(lockable);
+          });
+          grid.attach(this.lock_button, 2, 0);
+      }
+
+      show_all();
     }
 
-    private void place_unlock(Lockable lockable, Gtk.Window? window) {
-        Cancellable cancellable = new Cancellable();
-        TlsInteraction interaction = new Interaction(window);
+    private static unowned string? get_lock_icon_name(Lockable lockable) {
+          if (lockable.unlockable)
+              return "changes-prevent-symbolic";
 
-        lockable.unlock.begin(interaction, cancellable, (obj, res) => {
-            try {
-                lockable.unlock.end(res);
-                current_collection_changed();
-            } catch (Error e) {
-                Util.show_error(window, _("Couldn’t unlock"), e.message);
-            }
-        });
+          if (lockable.lockable)
+              return "changes-allow-symbolic";
+
+          return null;
     }
 
-    private void on_place_unlock(Gtk.MenuItem item, Lockable lockable) {
-        place_unlock(lockable, (Gtk.Window) item.get_toplevel());
+    private void update_lock_icon(Lockable lockable) {
+        ((Gtk.Image) this.lock_button.get_image()).icon_name = get_lock_icon_name(lockable);
     }
 
-    private void on_place_delete(Gtk.MenuItem item, Deletable deletable) {
-        Deleter deleter = deletable.create_deleter();
-        if (deleter.prompt((Gtk.Window) item.get_toplevel())) {
-            deleter.delete.begin(null, (obj, res) => {
-                try {
-                    deleter.delete.end(res);
-                } catch (Error e) {
-                    Util.show_error(parent, _("Couldn’t delete"), e.message);
-                }
-            });
-        }
+    public SidebarItem(Seahorse.Place place) {
+        GLib.Object(place: place);
     }
 
-    private void popup_menu_for_place(Place place) {
-        // Start from the menu model provided by the place (if any)
-        var menu = (place.menu_model != null)? new Gtk.Menu.from_model(place.menu_model)
-                                             : new Gtk.Menu();
+    public void show_popup_menu() {
+        // Start from the menu model provided by the this.place (if any)
+        var menu = (this.place.menu_model != null)? new Gtk.Menu.from_model(this.place.menu_model)
+                                                  : new Gtk.Menu();
 
         // Make sure the actions from the collection
-        if (place.actions != null)
-            menu.insert_action_group(place.action_prefix, place.actions);
+        if (this.place.actions != null)
+            menu.insert_action_group(this.place.action_prefix, this.place.actions);
 
         // Lock and unlock items
-        if (place is Lockable) {
+        if (this.place is Lockable) {
             Gtk.MenuItem item = new Gtk.MenuItem.with_mnemonic(_("_Lock"));
-            item.activate.connect(() => on_place_lock(item, (Lockable) place));
-            place.bind_property("lockable", item, "visible", BindingFlags.SYNC_CREATE);
+            item.activate.connect(() => on_place_lock(item, (Lockable) this.place));
+            this.place.bind_property("lockable", item, "visible", BindingFlags.SYNC_CREATE);
             menu.append(item);
 
             item = new Gtk.MenuItem.with_mnemonic(_("_Unlock"));
-            item.activate.connect(() => on_place_unlock(item, (Lockable) place));
-            place.bind_property("unlockable", item, "visible", BindingFlags.SYNC_CREATE);
+            item.activate.connect(() => on_place_unlock(item, (Lockable) this.place));
+            this.place.bind_property("unlockable", item, "visible", BindingFlags.SYNC_CREATE);
             menu.append(item);
         }
 
         // Delete item
-        if (place is Deletable) {
+        if (this.place is Deletable) {
             Gtk.MenuItem item = new Gtk.MenuItem.with_mnemonic(_("_Delete"));
-            item.activate.connect(() => on_place_delete(item, (Deletable) place));
-            place.bind_property("deletable", item, "sensitive", BindingFlags.SYNC_CREATE);
+            item.activate.connect(() => on_place_delete(item, (Deletable) this.place));
+            this.place.bind_property("deletable", item, "sensitive", BindingFlags.SYNC_CREATE);
             menu.append(item);
             item.show();
         }
 
         // Properties item
-        if (place is Viewable) {
+        if (this.place is Viewable) {
             Gtk.MenuItem item = new Gtk.MenuItem.with_mnemonic(_("_Properties"));
-            item.activate.connect(() => Viewable.view(place, (Gtk.Window) item.get_toplevel()));
+            item.activate.connect(() => Viewable.view(this.place, (Gtk.Window) item.get_toplevel()));
             menu.append(item);
             item.show();
         }
 
         bool visible = false;
         menu.foreach((widget) => {
-            if (widget.visible)
-                visible = true;
+            visible |= widget.visible;
         });
 
         if (visible) {
@@ -677,246 +449,54 @@ public class Seahorse.Sidebar : Gtk.TreeView {
         }
     }
 
-    private bool on_popup_menu(Gtk.Widget? widget) {
-        Gtk.TreePath? path;
-        get_cursor(out path, null);
-        if (path == null)
-            return false;
-
-        Gtk.TreeIter iter;
-        if (!this.store.get_iter(out iter, path))
-            return false;
-
-        Gcr.Collection? collection;
-        this.store.get(iter, Column.COLLECTION, out collection, -1);
-
-        if (collection is Place) {
-            popup_menu_for_place((Place) collection);
-            return true;
-        }
-
-        return false;
-    }
-
-    private void update_action_buttons_take_path(Gtk.TreePath? path) {
-        if (path == this.action_highlight_path)
-            return;
-
-        if (path != null && this.action_highlight_path != null &&
-            this.action_highlight_path.compare(path) == 0) {
-            return;
-        }
-
-        Gtk.TreePath? old_path = this.action_highlight_path;
-        this.action_highlight_path = path;
-
-        Gtk.TreeIter? iter = null;
-        if (this.action_highlight_path != null
-              && this.store.get_iter(out iter, this.action_highlight_path))
-            this.store.row_changed(this.action_highlight_path, iter);
-
-        if (old_path != null && this.store.get_iter(out iter, old_path))
-            this.store.row_changed(old_path, iter);
-    }
-
-    private bool over_action_button(int x, int y, out Gtk.TreePath? path) {
-
-        path = null;
-        Gtk.TreeViewColumn column;
-        if (get_path_at_pos(x, y, out path, out column, null, null)) {
-            Gtk.TreeIter iter;
-            this.store.get_iter(out iter, path);
-
-            int hseparator;
-            style_get("horizontal-separator", out hseparator, null);
-
-            // Reload cell attributes for this particular row
-            column.cell_set_cell_data(this.store, iter, false, false);
-            int width, x_offset;
-            column.cell_get_position(this.action_cell_renderer, out x_offset, out width);
-
-            // This is kinda weird, but we have to do it to workaround gtk+ expanding
-            // the eject cell renderer (even thought we told it not to) and we then
-            // had to set it right-aligned
-            x_offset += width - hseparator - ACTION_BUTTON_XPAD - this.action_button_size;
-
-            if (x - x_offset >= 0 && x - x_offset <= this.action_button_size)
-                return true;
-        }
-
-        if (path != null)
-            path = null;
-
-        return false;
-    }
-
-    private bool on_motion_notify_event(Gtk.Widget? widget, Gdk.EventMotion event) {
-        Gtk.TreePath? path = null;
-        if (over_action_button((int) event.x, (int) event.y, out path)) {
-            update_action_buttons_take_path(path);
-            return true;
-        }
-
-        update_action_buttons_take_path(null);
-        return false;
-    }
-
-    private bool on_button_press_event (Gtk.Widget? widget, Gdk.EventButton event) {
-        if (event.button != 3 || event.type != Gdk.EventType.BUTTON_PRESS)
-            return false;
-
-        Gtk.TreePath? path;
-        if (!get_path_at_pos((int) event.x, (int) event.y, out path, null, null, null))
-            return false;
-
-        set_cursor(path, null, false);
-        Gtk.TreeIter iter;
-        if (!this.store.get_iter(out iter, path))
-            return false;
-
-        Gcr.Collection? collection;
-        this.store.get(iter, Column.COLLECTION, out collection, -1);
-
-        if (collection is Place)
-            popup_menu_for_place((Place) collection);
-
-        return true;
-    }
-
-    private bool on_button_release_event (Gtk.Widget? widget, Gdk.EventButton event) {
-        if (event.type != Gdk.EventType.BUTTON_RELEASE)
-            return true;
-
-        Gtk.TreePath? path;
-        if (!over_action_button((int) event.x, (int) event.y, out path))
-            return false;
-
-        Gtk.TreeIter iter;
-        if (!this.store.get_iter(out iter, path))
-            return false;
-
-        Gtk.Window? window = (Gtk.Window) widget.get_toplevel();
-
-        Lockable? lockable = lookup_lockable_for_iter(this.store, iter);
-        if (lockable != null) {
-            if (lockable.lockable)
-                place_lock(lockable, window);
-            else if (lockable.unlockable)
-                place_unlock(lockable, window);
-        }
-
-        return true;
-    }
-
-    public string[] chosen_uris_to_array() {
-        string[] results = {};
-        foreach (string? uri in this.chosen)
-            results += uri;
-
-        results += null;
+    private void place_lock(Lockable lockable, Gtk.Window? window) {
+        Cancellable cancellable = new Cancellable();
+        TlsInteraction interaction = new Interaction(window);
 
-        return results;
+        lockable.lock.begin(interaction, cancellable, (obj, res) => {
+            try {
+                lockable.lock.end(res);
+                update_lock_icon(lockable);
+                place_changed();
+            } catch (Error e) {
+                Util.show_error(window, _("Couldn’t lock"), e.message);
+            }
+        });
     }
 
-    public void replace_chosen_uris(string[] uris) {
-        // For quick lookups
-        GenericSet<string?> chosen = new GenericSet<string?>(str_hash, str_equal);
-        foreach (string uri in uris)
-            chosen.add(uri);
-
-        update_objects_for_chosen(chosen);
-        this.chosen = chosen;
+    private void on_place_lock(Gtk.Widget widget, Lockable lockable) {
+        place_lock(lockable, (Gtk.Window) widget.get_toplevel());
     }
 
-    public List<weak Gcr.Collection>? get_selected_places() {
-        List<weak Gcr.Collection> places = this.objects.elements();
-
-        Gtk.TreePath? path = null;
-        get_cursor(out path, null);
-        if (path != null) {
-
-            Gtk.TreeIter iter;
-            if (!this.store.get_iter(out iter, path))
-                return null;
-
-            Gcr.Collection? collection;
-            RowType row_type;
-            this.store.get(iter, Column.ROW_TYPE, out row_type,
-                                 Column.COLLECTION, out collection, -1);
+    private void place_unlock(Lockable lockable, Gtk.Window? window) {
+        Cancellable cancellable = new Cancellable();
+        TlsInteraction interaction = new Interaction(window);
 
-            if (collection != null) {
-                if (row_type == RowType.PLACE) {
-                    places.remove(collection);
-                    places.prepend(collection);
-                }
+        lockable.unlock.begin(interaction, cancellable, (obj, res) => {
+            try {
+                lockable.unlock.end(res);
+                update_lock_icon(lockable);
+                place_changed();
+            } catch (Error e) {
+                Util.show_error(window, _("Couldn’t unlock"), e.message);
             }
-        }
-
-        return places;
-    }
-
-    public Place? get_focused_place() {
-        Gtk.TreeIter iter;
-
-        Gtk.TreePath? path = null;
-        get_cursor(out path, null);
-        if (path != null) {
-            if (!this.store.get_iter(out iter, path))
-                return null;
-
-            Gcr.Collection? collection;
-            RowType row_type;
-            this.store.get(iter, Column.ROW_TYPE, out row_type,
-                                 Column.COLLECTION, out collection, -1);
-
-            if (row_type == RowType.PLACE)
-                return (Place) collection;
-        }
-
-        return null;
+        });
     }
 
-    public void set_focused_place(string uri_prefix) {
-        foreach (Backend backend in this.backends) {
-            foreach (weak GLib.Object obj in backend.get_objects()) {
-                Place place = obj as Place;
-                if (place == null)
-                    continue;
-                else if (place.uri.has_prefix(uri_prefix)) {
-                    var chosen = new GenericSet<string?>(str_hash, str_equal);
-                    chosen.add(place.uri);
-                    this.update_objects_for_chosen(chosen);
-                    return;
-                }
-            }
-        }
+    private void on_place_unlock(Gtk.MenuItem widget, Lockable lockable) {
+        place_unlock(lockable, (Gtk.Window) widget.get_toplevel());
     }
 
-    public List<weak Backend>? get_backends() {
-        Gtk.TreeIter iter;
-
-        List<weak Backend> backends = this.backends.copy();
-        backends.reverse();
-
-        Gtk.TreePath? path = null;
-        get_cursor(out path, null);
-        if (path != null) {
-            if (!this.store.get_iter(out iter, path))
-                return null;
-
-            Gcr.Collection? collection;
-            RowType row_type;
-            this.store.get(iter, Column.ROW_TYPE, out row_type,
-                                 Column.COLLECTION, out collection, -1);
-
-            if (collection != null) {
-                if (row_type == RowType.BACKEND) {
-                    backends.remove((Backend) collection);
-                    backends.prepend((Backend) collection);
+    private void on_place_delete(Gtk.MenuItem item, Deletable deletable) {
+        Deleter deleter = deletable.create_deleter();
+        if (deleter.prompt((Gtk.Window) item.get_toplevel())) {
+            deleter.delete.begin(null, (obj, res) => {
+                try {
+                    deleter.delete.end(res);
+                } catch (Error e) {
+                    Util.show_error(parent, _("Couldn’t delete"), e.message);
                 }
-            }
+            });
         }
-
-        return backends;
     }
 }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]