[seahorse/wip/nielsdg/seahorse-listview: 35/35] Use a GtkListBox instead of a GtkTreeView
- From: Niels De Graef <nielsdg src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [seahorse/wip/nielsdg/seahorse-listview: 35/35] Use a GtkListBox instead of a GtkTreeView
- Date: Tue, 14 Jan 2020 22:51:28 +0000 (UTC)
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]