[seahorse/wip/nielsdg/src-port: 1/4] WIP



commit 354a0ba81274fa0cc58867d014c5bd800508d93e
Author: Niels De Graef <nielsdegraef gmail com>
Date:   Sun Jan 28 14:40:37 2018 +0100

    WIP

 common/config.vapi                     |    7 +-
 libseahorse/seahorse-search-provider.c |    2 -
 src/generate-select.vala               |  140 +++
 src/import-dialog.vala                 |   78 ++
 src/key-manager.vala                   |  499 ++++++++++
 src/main.vala                          |   54 ++
 src/meson.build                        |   13 +-
 src/seahorse-generate-select.c         |  298 ------
 src/seahorse-generate-select.h         |   33 -
 src/seahorse-import-dialog.c           |  188 ----
 src/seahorse-import-dialog.h           |   50 -
 src/seahorse-key-manager.c             |  855 -----------------
 src/seahorse-key-manager.h             |   61 --
 src/seahorse-main.c                    |  100 --
 src/seahorse-sidebar.c                 | 1598 --------------------------------
 src/seahorse-sidebar.h                 |   61 --
 src/sidebar.vala                       |  926 ++++++++++++++++++
 ssh/meson.build                        |    1 +
 18 files changed, 1711 insertions(+), 3253 deletions(-)
---
diff --git a/common/config.vapi b/common/config.vapi
index 02ccb37..aa0b584 100644
--- a/common/config.vapi
+++ b/common/config.vapi
@@ -4,6 +4,7 @@ namespace Config
        public const string PKGDATADIR;
 
        public const string EXECDIR;
+       public const string LOCALEDIR;
 
        public const string VERSION;
        public const string PACKAGE;
@@ -21,9 +22,11 @@ namespace Config
 
 namespace Seahorse {
 
+public void register_resource();
+
 [CCode (cheader_filename = "libseahorse/seahorse-application.h")]
-namespace Application {
-       public unowned Gtk.Application @get();
+public class Application : Gtk.Application {
+  public Application();
 }
 
 [CCode (cheader_filename = "libseahorse/seahorse-util.h")]
diff --git a/libseahorse/seahorse-search-provider.c b/libseahorse/seahorse-search-provider.c
index dddaae1..976ff18 100644
--- a/libseahorse/seahorse-search-provider.c
+++ b/libseahorse/seahorse-search-provider.c
@@ -27,8 +27,6 @@
 
 #include "seahorse-common.h"
 
-#include "src/seahorse-key-manager.h"
-
 #include <glib/gi18n.h>
 
 #include <gcr/gcr.h>
diff --git a/src/generate-select.vala b/src/generate-select.vala
new file mode 100644
index 0000000..1652c4f
--- /dev/null
+++ b/src/generate-select.vala
@@ -0,0 +1,140 @@
+/*
+ * Seahorse
+ *
+ * Copyright (C) 2008 Stefan Walter
+ *
+ * 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/>.
+ */
+
+public class Seahorse.GenerateSelect : Gtk.Dialog {
+    private Gtk.ListStore store;
+    private Gtk.TreeView view;
+    private List<Gtk.ActionGroup>? action_groups;
+
+    private enum Column {
+        ICON,
+        TEXT,
+        ACTION,
+        N_COLUMNS
+    }
+
+    public GenerateSelect(Gtk.Window? parent) {
+        GLib.Object(
+            transient_for: parent,
+            modal: true
+        );
+        this.store = new Gtk.ListStore(Column.N_COLUMNS, typeof(Icon), typeof(string), typeof(Gtk.Action));
+        this.store.set_default_sort_func(on_list_sort);
+        this.store.set_sort_column_id(Gtk.TREE_SORTABLE_DEFAULT_SORT_COLUMN_ID, Gtk.SortType.ASCENDING);
+
+        this.action_groups = (List<Gtk.ActionGroup>) Registry.object_instances("generator");
+        foreach (Gtk.ActionGroup action_group in this.action_groups) {
+            foreach (weak Gtk.Action action in action_group.list_actions()) {
+                string text = "<span size=\"larger\" weight=\"bold\">%s</span>\n%s"
+                                  .printf(action.label, action.tooltip);
+
+                Icon? icon = action.gicon;
+                if (icon == null) {
+                    if (action.icon_name != null)
+                        icon = new ThemedIcon(action.icon_name);
+                }
+
+                Gtk.TreeIter iter;
+                this.store.append(out iter);
+                this.store.set(iter, Column.TEXT, text,
+                                     Column.ICON, icon,
+                                     Column.ACTION, action,
+                                     -1);
+            }
+        }
+
+        Gtk.Builder builder = new Gtk.Builder();
+        try {
+            string path = "/org/gnome/Seahorse/seahorse-ssh-generate.ui";
+            builder.add_from_resource(path);
+        } catch (GLib.Error err) {
+            critical("%s", err.message);
+        }
+
+        // Setup the dialog
+        set_default_size(-1, 410);
+        get_content_area().pack_start((Gtk.Widget) builder.get_object("generate-select"),
+                                        true, true, 0);
+        add_buttons(Gtk.Stock.CANCEL, Gtk.ResponseType.CANCEL,
+                    _("Continue"), Gtk.ResponseType.OK,
+                    null);
+
+        // Hook it into the view
+        this.view = (Gtk.TreeView) builder.get_object("keytype-tree");
+
+        Gtk.CellRendererPixbuf pixcell = new Gtk.CellRendererPixbuf();
+        pixcell.stock_size = Gtk.IconSize.DND;
+        this.view.insert_column_with_attributes(-1, "", pixcell, "gicon", Column.ICON, null);
+        this.view.insert_column_with_attributes(-1, "", new Gtk.CellRendererText(), "markup", Column.TEXT, 
null);
+        this.view.set_model(this.store);
+
+        // Setup selection, select first item
+        Gtk.TreeSelection selection = this.view.get_selection();
+        selection.set_mode(Gtk.SelectionMode.BROWSE);
+
+        Gtk.TreeIter iter;
+        this.store.get_iter_first(out iter);
+        selection.select_iter(iter);
+
+        this.view.row_activated.connect(on_row_activated);
+        this.view.height_request = 410;
+    }
+
+    private Gtk.Action? get_selected_action() {
+        Gtk.TreeSelection selection = this.view.get_selection();
+
+        Gtk.TreeIter iter;
+        Gtk.TreeModel? model;
+        if (!selection.get_selected(out model, out iter))
+            return null;
+
+        Gtk.Action? action;
+        this.store.get(iter, Column.ACTION, out action, -1);
+        assert (action != null);
+
+        return action;
+    }
+
+    private void on_row_activated(Gtk.TreeView view, Gtk.TreePath path, Gtk.TreeViewColumn col) {
+        Gtk.Action? action = get_selected_action();
+        if (action != null) {
+            Action.activate_with_window(action, null, this.transient_for);
+            destroy();
+        }
+    }
+
+    public override void response(int response)  {
+        Gtk.Action? action = (response == Gtk.ResponseType.OK)? get_selected_action() : null;
+        Gtk.Window? parent = (action != null)? this.transient_for : null;
+
+        if (action != null)
+            Action.activate_with_window(action, null, parent);
+
+        destroy();
+    }
+
+    private int on_list_sort (Gtk.TreeModel? model, Gtk.TreeIter a, Gtk.TreeIter b) {
+        string? a_text = null, b_text = null;
+        model.get(a, Column.TEXT, out a_text, -1);
+        model.get(b, Column.TEXT, out b_text, -1);
+
+        return a_text.collate(b_text);
+    }
+}
diff --git a/src/import-dialog.vala b/src/import-dialog.vala
new file mode 100644
index 0000000..9d228a5
--- /dev/null
+++ b/src/import-dialog.vala
@@ -0,0 +1,78 @@
+/*
+ * Seahorse
+ *
+ * Copyright (C) 2011 Collabora Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Author: Stef Walter <stefw collabora co uk>
+ */
+
+public class Seahorse.ImportDialog : Gtk.Dialog {
+
+    private Gcr.ViewerWidget viewer;
+    private Gcr.ImportButton import;
+
+    public ImportDialog(Gtk.Window? parent) {
+        GLib.Object(transient_for: parent);
+
+        Gtk.Widget button = new Gtk.Button.from_stock(Gtk.Stock.CANCEL);
+        button.show();
+        add_action_widget(button, Gtk.ResponseType.CANCEL);
+
+        this.import = new Gcr.ImportButton(_("Import"));
+        this.import.importing.connect(() => this.viewer.clear_error());
+        this.import.imported.connect(on_import_button_imported);
+        this.import.show();
+        ((Gtk.Box) get_action_area()).pack_start(this.import, false, true, 0);
+
+        Gtk.Frame frame = new Gtk.Frame(_("<b>Data to be imported:</b>"));
+        ((Gtk.Label) frame.label_widget).use_markup = true;
+        ((Gtk.Box) get_content_area()).pack_start(frame, true, true, 0);
+        frame.set_border_width(6);
+        frame.show();
+
+        this.viewer = new Gcr.ViewerWidget();
+        this.viewer.added.connect((v, r, parsed) => this.import.add_parsed(parsed));
+        this.viewer.show();
+
+        frame.add(this.viewer);
+    }
+
+    public void add_uris(string[] uris) {
+        foreach (string uri in uris)
+            this.viewer.load_file(File.new_for_uri(uri));
+    }
+
+    public void add_text(string? display_name, string text) {
+        this.viewer.load_data(display_name, (uint8[]) text);
+    }
+
+    private void on_import_button_imported(GLib.Object importer, Error? error) {
+        if (error == null) {
+            response(Gtk.ResponseType.OK);
+
+            string uri = ((Gcr.Importer) importer).uri;
+            foreach (Backend backend in Backend.get_registered()) {
+                Place? place = backend.lookup_place(uri);
+                if (place != null)
+                    place.load.begin(null);
+            }
+
+        } else {
+            if (!(error is GLib.IOError.CANCELLED))
+                this.viewer.show_error(_("Import failed"), error);
+        }
+    }
+}
diff --git a/src/key-manager.vala b/src/key-manager.vala
new file mode 100644
index 0000000..39340f9
--- /dev/null
+++ b/src/key-manager.vala
@@ -0,0 +1,499 @@
+/*
+ * 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/>.
+ */
+
+public class Seahorse.KeyManager : Catalog {
+
+    private Gtk.ActionGroup view_actions;
+    private Gtk.RadioAction show_action;
+    private Gtk.Entry filter_entry;
+    private Predicate pred;
+    private Sidebar sidebar;
+
+    private Gtk.TreeView view;
+    private Gcr.Collection collection; // owned by the sidebar
+    private KeyManagerStore store;
+
+    private GLib.Settings settings;
+    private int sidebar_width;
+    private uint sidebar_width_sig;
+
+    private enum ShowFilter {
+        ANY,
+        PERSONAL,
+        TRUSTED,
+    }
+
+    private enum DndTarget { // Drag 'n Drop target type
+        PLAIN,
+        URIS
+    }
+
+    private const Gtk.ActionEntry[] GENERAL_ACTIONS = {
+        // TRANSLATORS: The "Remote" menu contains key operations on remote systems.
+        { "remote-menu", null, N_("_Remote") },
+        { "new-menu", null, N_("_New") },
+        { "app-quit", Gtk.Stock.QUIT, null, "<control>Q", N_("Close this program"), on_app_quit },
+        { "file-new", Gtk.Stock.NEW, N_("_New…"), "<control>N", N_("Create a new key or item"), on_file_new 
},
+        { "new-object", Gtk.Stock.ADD, N_("_New…"), null, N_("Add a new key or item"), on_file_new },
+        { "file-import", Gtk.Stock.OPEN, N_("_Import…"), "<control>I", N_("Import from a file"), 
on_key_import_file },
+        { "edit-import-clipboard", Gtk.Stock.PASTE, null, "<control>V", N_("Import from the clipboard"), 
on_key_import_clipboard }
+    };
+
+    private const Gtk.ToggleActionEntry[] SIDEBAR_ACTIONS = {
+        { "view-sidebar", null, N_("By _Keyring"), null, N_("Show sidebar listing keyrings"), null, false },
+    };
+
+    private const Gtk.RadioActionEntry[] VIEW_RADIO_ACTIONS = {
+        { "view-personal", null, N_("Show _Personal"), null, N_("Only show personal keys, certificates and 
passwords"), ShowFilter.PERSONAL },
+        { "view-trusted", null, N_("Show _Trusted"), null, N_("Only show trusted keys, certificates and 
passwords"), ShowFilter.TRUSTED },
+        { "view-any", null, N_("Show _Any"), null, N_("Show all keys, certificates and passwords"), 
ShowFilter.ANY },
+    };
+
+    public KeyManager() {
+        GLib.Object(ui_name: "key-manager");
+        this.settings = new GLib.Settings("org.gnome.seahorse.manager");
+
+        Gtk.Window window = this.window;
+        window.set_default_geometry(640, 476);
+        window.set_events(Gdk.EventMask.POINTER_MOTION_MASK
+                          | Gdk.EventMask.POINTER_MOTION_HINT_MASK
+                          | Gdk.EventMask.BUTTON_PRESS_MASK
+                          | Gdk.EventMask.BUTTON_RELEASE_MASK);
+        window.set_title(_("Passwords and Keys"));
+
+        this.collection = setup_sidebar();
+
+        // Init key list & selection settings
+        Gtk.Builder builder = get_builder();
+        this.view = (Gtk.TreeView) builder.get_object("key-list");
+        assert (this.view != null);
+
+        Gtk.TreeSelection selection = this.view.get_selection();
+        selection.set_mode(Gtk.SelectionMode.MULTIPLE);
+        selection.changed.connect(on_view_selection_changed);
+        this.view.realize();
+
+        // Add new key store and associate it
+        this.store = new KeyManagerStore(this.collection, this.view, this.pred, this.settings);
+
+        Gtk.ActionGroup actions = new Gtk.ActionGroup("general");
+        actions.set_translation_domain(Config.GETTEXT_PACKAGE);
+        actions.add_actions(GENERAL_ACTIONS, null);
+        include_actions(actions);
+
+        this.view_actions = new Gtk.ActionGroup("view");
+        this.view_actions.set_translation_domain(Config.GETTEXT_PACKAGE);
+        this.view_actions.add_radio_actions(VIEW_RADIO_ACTIONS, -1, () => {
+            this.settings.set_string("item-filter", update_view_filter());
+        });
+        Gtk.RadioAction action = (Gtk.RadioAction) this.view_actions.get_action("view-personal");
+        include_actions(this.view_actions);
+        this.show_action = action;
+
+        // Notify us when settings change
+        this.settings.changed["item-filter"].connect(on_item_filter_changed);
+        on_item_filter_changed(this.settings, "item-filter");
+
+        // first time signals
+        ((Gtk.Button) builder.get_object("import-button")).clicked.connect(on_keymanager_import_button);
+        ((Gtk.Button) builder.get_object("new-button")).clicked.connect(on_keymanager_new_button);
+
+        // Make sure import is only available with clipboard content
+        Gtk.Action import_clipboard_action = actions.get_action("edit-import-clipboard");
+        Gtk.Clipboard clipboard = Gtk.Clipboard.get(Gdk.SELECTION_PRIMARY);
+        clipboard.owner_change.connect((c, e) => update_clipboard_state(c, e, actions));
+        update_clipboard_state(clipboard, null, actions);
+
+        // Flush all updates
+        ensure_updated();
+
+        // Find the toolbar
+        Gtk.Container placeholder = (Gtk.Container) builder.get_object("toolbar-placeholder");
+        if (placeholder != null) {
+            List<weak Gtk.Widget> children = placeholder.get_children();
+            if (children != null && children.data != null) {
+                // The toolbar is the first (and only) element
+                Gtk.Toolbar toolbar = (Gtk.Toolbar) children.data;
+                if (toolbar != null) {
+                    toolbar.get_style_context().add_class("sidebar");
+                    toolbar.reset_style();
+
+                    // Insert a separator to right align the filter
+                    Gtk.SeparatorToolItem sep = new Gtk.SeparatorToolItem();
+                    sep.set_draw(false);
+                    sep.set_expand(true);
+                    sep.show_all();
+                    toolbar.insert(sep, -1);
+
+                    // Insert a filter bar
+                    Gtk.Box box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0);
+
+                    this.filter_entry = new Gtk.Entry();
+                    this.filter_entry.set_placeholder_text(_("Filter"));
+                    box.pack_start(this.filter_entry, false, true, 0);
+
+                    box.pack_start(new Gtk.Label(null), false, false, 0);
+                    box.show_all();
+
+                    Gtk.ToolItem item = new Gtk.ToolItem();
+                    item.add(box);
+                    item.show_all();
+                    toolbar.insert(item, -1);
+                }
+            }
+        }
+
+        on_filter_changed(this.filter_entry);
+        this.filter_entry.set_width_chars(30);
+        this.filter_entry.icon_release.connect(() => this.filter_entry.set_text(""));
+
+        // For the filtering
+        this.filter_entry.changed.connect(on_filter_changed);
+        this.view.start_interactive_search.connect(() => {
+            this.filter_entry.grab_focus();
+            return false;
+        });
+
+        // Set focus to the current key list
+        this.view.grab_focus();
+        selection_changed();
+
+        // To avoid flicker
+        show();
+
+        // Setup drops
+        Gtk.drag_dest_set(window, Gtk.DestDefaults.ALL, {}, Gdk.DragAction.COPY);
+        Gtk.TargetList targets = new Gtk.TargetList(null);
+        targets.add_uri_targets(DndTarget.URIS);
+        targets.add_text_targets(DndTarget.PLAIN);
+        Gtk.drag_dest_set_target_list(window, targets);
+
+        window.drag_data_received.connect(on_target_drag_data_received);
+
+        this.view.button_press_event.connect(on_keymanager_key_list_button_pressed);
+        this.view.row_activated.connect(on_keymanager_row_activated);
+        this.view.popup_menu.connect(on_keymanager_key_list_popup_menu);
+
+#if REFACTOR_FIRST
+        // To show first time dialog
+        Timeout.add_seconds(1, on_first_timer);
+#endif
+    }
+
+    ~KeyManager() {
+        if (this.sidebar_width_sig != 0) {
+            Source.remove(this.sidebar_width_sig);
+            this.sidebar_width_sig = 0;
+        }
+    }
+
+    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_keymanager_row_activated(Gtk.TreeView view, Gtk.TreePath? path, Gtk.TreeViewColumn 
column) {
+        if (path == null)
+            return;
+
+        GLib.Object obj = KeyManagerStore.get_object_from_path(view, path);
+        if (obj != null)
+            show_properties(obj);
+    }
+
+    private bool on_keymanager_key_list_button_pressed(Gdk.EventButton event) {
+        if (event.button == 3)
+            show_context_menu(Catalog.MENU_OBJECT, event.button, event.time);
+
+        return false;
+    }
+
+    private bool on_keymanager_key_list_popup_menu() {
+        GLib.List<weak GLib.Object> objects = get_selected_objects();
+        if (objects != null)
+            show_context_menu(Catalog.MENU_OBJECT, 0, Gtk.get_current_event_time());
+        return false;
+    }
+
+    private void on_file_new(Gtk.Action action) {
+        GenerateSelect dialog = new GenerateSelect(this.window);
+        dialog.run();
+        dialog.destroy();
+    }
+
+    private void on_keymanager_new_button(Gtk.Button button) {
+        GenerateSelect dialog = new GenerateSelect(this.window);
+        dialog.run();
+        dialog.destroy();
+    }
+
+#if REFACTOR_FIRST
+    private bool on_first_timer() {
+        // Although not all the keys have completed we'll know whether we have
+        // any or not at this point
+        if (this.collection.get_length() == 0) {
+            Gtk.Widget widget = xxwidget_get_widget (XX_WIDGET (self), "first-time-box");
+            widget.show();
+        }
+
+        return false;
+    }
+#endif
+
+    private void on_filter_changed(Gtk.Editable entry) {
+        string? text = this.filter_entry.get_text();
+        this.store.filter = text;
+
+        if (text == null || text == "") {
+            this.filter_entry.secondary_icon_name = "edit-find-symbolic";
+            this.filter_entry.secondary_icon_activatable = false;
+            this.filter_entry.secondary_icon_sensitive = false;
+        } else {
+            this.filter_entry.secondary_icon_name = "edit-clear-symbolic";
+            this.filter_entry.secondary_icon_activatable = true;
+            this.filter_entry.secondary_icon_sensitive = true;
+        }
+    }
+
+    private void import_files(string[]? uris) {
+        ImportDialog dialog = new ImportDialog(this.window);
+        dialog.add_uris(uris);
+        dialog.run();
+        dialog.destroy();
+    }
+
+    private void import_prompt() {
+        Gtk.FileChooserDialog dialog =
+            new Gtk.FileChooserDialog(_("Import Key"), this.window,
+                                      Gtk.FileChooserAction.OPEN,
+                                      Gtk.Stock.CANCEL, Gtk.ResponseType.CANCEL,
+                                      Gtk.Stock.OPEN, Gtk.ResponseType.ACCEPT,
+                                      null);
+
+        dialog.set_default_response(Gtk.ResponseType.ACCEPT);
+        dialog.set_local_only(false);
+
+        // TODO: This should come from libgcr somehow
+        Gtk.FileFilter filter = new Gtk.FileFilter();
+        filter.set_name(_("All key files"));
+        filter.add_mime_type("application/pgp-keys");
+        filter.add_mime_type("application/x-ssh-key");
+        filter.add_mime_type("application/pkcs12");
+        filter.add_mime_type("application/pkcs12+pem");
+        filter.add_mime_type("application/pkcs7-mime");
+        filter.add_mime_type("application/pkcs7-mime+pem");
+        filter.add_mime_type("application/pkcs8");
+        filter.add_mime_type("application/pkcs8+pem");
+        filter.add_mime_type("application/pkix-cert");
+        filter.add_mime_type("application/pkix-cert+pem");
+        filter.add_mime_type("application/pkix-crl");
+        filter.add_mime_type("application/pkix-crl+pem");
+        filter.add_mime_type("application/x-pem-file");
+        filter.add_mime_type("application/x-pem-key");
+        filter.add_mime_type("application/x-pkcs12");
+        filter.add_mime_type("application/x-pkcs7-certificates");
+        filter.add_mime_type("application/x-x509-ca-cert");
+        filter.add_mime_type("application/x-x509-user-cert");
+        filter.add_mime_type("application/pkcs10");
+        filter.add_mime_type("application/pkcs10+pem");
+        filter.add_mime_type("application/x-spkac");
+        filter.add_mime_type("application/x-spkac+base64");
+        filter.add_pattern("*.asc");
+        filter.add_pattern("*.gpg");
+        filter.add_pattern("*.pub");
+        filter.add_pattern("*.pfx");
+        filter.add_pattern("*.cer");
+        filter.add_pattern("*.crt");
+        filter.add_pattern("*.pem");
+        dialog.add_filter(filter);
+        dialog.set_filter(filter);
+
+        filter = new Gtk.FileFilter();
+        filter.set_name(_("All files"));
+        filter.add_pattern("*");
+        dialog.add_filter(filter);
+
+        string? uri = null;
+        if (dialog.run() == Gtk.ResponseType.ACCEPT) {
+            uri = dialog.get_uri();
+        }
+
+        dialog.destroy();
+
+        if (uri != null) {
+            string? uris[2];
+            uris[0] = uri;
+            uris[1] = null;
+            import_files(uris);
+        }
+    }
+
+    private void on_key_import_file(Gtk.Action action) {
+        import_prompt();
+    }
+
+    private void on_keymanager_import_button(Gtk.Button button) {
+        import_prompt();
+    }
+
+    private void import_text(string? display_name, string? text) {
+        ImportDialog dialog = new ImportDialog(this.window);
+        dialog.add_text(display_name, text);
+        dialog.run();
+        dialog.destroy();
+    }
+
+    private void on_target_drag_data_received(Gdk.DragContext context, int x, int y,
+                                              Gtk.SelectionData? selection_data, uint info, uint time) {
+        if (selection_data == null)
+            return;
+
+        if (info == DndTarget.PLAIN) {
+            string? text = selection_data.get_text();
+            import_text(_("Dropped text"), text);
+        } else if (info == DndTarget.URIS) {
+            string[]? uris = selection_data.get_uris();
+            foreach (string uri in uris)
+                uri._strip();
+            import_files(uris);
+        }
+    }
+
+    private void update_clipboard_state(Gtk.Clipboard clipboard, Gdk.Event? event, Gtk.ActionGroup group) {
+        Gtk.Action action = group.get_action("edit-import-clipboard");
+        action.set_sensitive(clipboard.wait_is_text_available());
+    }
+
+    private void on_clipboard_received(Gtk.Clipboard board, string? text) {
+        if (text == null)
+            return;
+
+        assert(this.filter_entry != null);
+        if (this.filter_entry.is_focus)
+            this.filter_entry.paste_clipboard();
+        else
+            if (text != null && text.char_count() > 0)
+                import_text(_("Clipboard text"), text);
+    }
+
+    private void on_key_import_clipboard(Gtk.Action action) {
+        Gdk.Atom atom = Gdk.Atom.intern("CLIPBOARD", false);
+        Gtk.Clipboard clipboard = Gtk.Clipboard.get(atom);
+        clipboard.request_text(on_clipboard_received);
+    }
+
+    private void on_app_quit(Gtk.Action action) {
+        Application.get().quit();
+    }
+
+    private string update_view_filter() {
+        string val = "";
+
+        switch (this.show_action.get_current_value()) {
+            case ShowFilter.PERSONAL:
+                this.pred.flags = Seahorse.Flags.PERSONAL;
+                val = "personal";
+                break;
+            case ShowFilter.TRUSTED:
+                this.pred.flags = Seahorse.Flags.TRUSTED;
+                val = "trusted";
+                break;
+            case ShowFilter.ANY:
+                this.pred.flags = 0;
+                val = "";
+                break;
+        }
+
+        this.store.refilter();
+        return val;
+    }
+
+    private void on_item_filter_changed(GLib.Settings settings, string? key) {
+        int radio;
+
+        string? value = settings.get_string(key);
+        if (value == null || value == "")
+            radio = ShowFilter.ANY;
+        else if (value == "personal")
+            radio = ShowFilter.PERSONAL;
+        else if (value == "trusted")
+            radio = ShowFilter.TRUSTED;
+        else
+            radio = -1;
+
+        this.show_action.set_current_value(radio);
+        update_view_filter();
+    }
+
+    public override GLib.List<weak GLib.Object> get_selected_objects() {
+        return KeyManagerStore.get_selected_objects(this.view);
+    }
+
+    public override Place? get_focused_place() {
+        return this.sidebar.get_focused_place();
+    }
+
+    private Gcr.Collection setup_sidebar() {
+        this.sidebar = new Sidebar();
+
+        this.sidebar_width = this.settings.get_int("sidebar-width");
+        Gtk.Builder builder = get_builder();
+
+        Gtk.Paned panes = (Gtk.Paned) builder.get_object("sidebar-panes");
+        panes.position = this.sidebar_width;
+        panes.realize.connect(() =>   { panes.position = this.settings.get_int("sidebar-width"); });
+        panes.unrealize.connect(() => { this.settings.set_int("sidebar-width", panes.position);  });
+
+        panes.get_child1().set_size_request(50, -1);
+        panes.get_child2().set_size_request(150, -1);
+
+        foreach (weak Backend backend in get_backends()) {
+            if (backend.actions != null)
+                include_actions(backend.actions);
+        }
+
+        Gtk.Container area = (Gtk.Container) builder.get_object("sidebar-area");
+        area.add(this.sidebar);
+        this.sidebar.show();
+
+        Gtk.ActionGroup actions = new Gtk.ActionGroup("sidebar");
+        actions.set_translation_domain(Config.GETTEXT_PACKAGE);
+        actions.add_toggle_actions(SIDEBAR_ACTIONS, null);
+        Gtk.Action action = actions.get_action("view-sidebar");
+        this.settings.bind("sidebar-visible", action, "active", SettingsBindFlags.DEFAULT);
+        action.bind_property("active", area, "visible", BindingFlags.SYNC_CREATE);
+        action.bind_property("active", this.sidebar, "combined", BindingFlags.INVERT_BOOLEAN | 
BindingFlags.SYNC_CREATE);
+        include_actions(actions);
+
+        this.settings.bind("keyrings-selected", this.sidebar, "selected-uris", SettingsBindFlags.DEFAULT);
+
+        return this.sidebar.collection;
+    }
+
+    public override List<weak Backend> get_backends() {
+        return this.sidebar.get_backends();
+    }
+}
diff --git a/src/main.vala b/src/main.vala
new file mode 100644
index 0000000..f2ed405
--- /dev/null
+++ b/src/main.vala
@@ -0,0 +1,54 @@
+/*
+ * Seahorse
+ *
+ * Copyright (C) 2003 Jacob Perkins
+ * Copyright (C) 2004,2005 Stefan Walter
+ * 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 General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+public int main (string[] args) {
+    Intl.bindtextdomain(Config.GETTEXT_PACKAGE, Config.LOCALEDIR);
+    Intl.bind_textdomain_codeset(Config.GETTEXT_PACKAGE, "UTF-8");
+    Intl.textdomain(Config.GETTEXT_PACKAGE);
+
+    Seahorse.register_resource();
+
+    Seahorse.Application app = new Seahorse.Application();
+
+    app.activate.connect(() => {
+        Seahorse.KeyManager key_mgr = new Seahorse.KeyManager();
+        key_mgr.show();
+    });
+
+    // Initialize the backends
+    app.startup.connect(() => {
+        Seahorse.Gkr.Backend.initialize();
+        Seahorse.Ssh.Backend.initialize();
+#if WITH_PGP
+        Seahorse.Pgp.Backend.initialize();
+#endif
+#if WITH_PKCS11
+        Seahorse.Pkcs11.Backend.initialize();
+#endif
+    });
+
+    int status = app.run(args);
+
+    Seahorse.Registry.cleanup();
+    Seahorse.Servers.cleanup();
+
+    return status;
+}
diff --git a/src/meson.build b/src/meson.build
index a4c73a6..8c04772 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -1,9 +1,10 @@
 seahorse_sources = [
-  'seahorse-generate-select.c',
-  'seahorse-import-dialog.c',
-  'seahorse-key-manager.c',
-  'seahorse-main.c',
-  'seahorse-sidebar.c',
+  'generate-select.vala',
+  'import-dialog.vala',
+  'key-manager.vala',
+  'main.vala',
+  'sidebar.vala',
+
   resources_src,
 ]
 
@@ -11,6 +12,8 @@ seahorse_dependencies = [
   glib_deps,
   gtk,
   gcr,
+  gcr_ui,
+  libsecret,
   config,
   common_dep,
   libseahorse_dep,
diff --git a/src/sidebar.vala b/src/sidebar.vala
new file mode 100644
index 0000000..cd11199
--- /dev/null
+++ b/src/sidebar.vala
@@ -0,0 +1,926 @@
+/*
+ * Seahorse
+ *
+ * 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 General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+public class Seahorse.Sidebar : Gtk.ScrolledWindow {
+
+    private const int ACTION_BUTTON_XPAD = 6;
+
+    private Gtk.TreeView? tree_view;
+
+    private Gtk.ListStore? store;
+    private List<Backend> backends;
+    private Gcr.UnionCollection? objects;
+
+    // The selection
+    private HashTable<Gcr.Collection, Gcr.Collection>? selection;
+    private bool updating;
+
+    // A set of chosen uris, used with settings
+    private HashTable<string, string>? chosen;
+
+    // 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 Gtk.AccelGroup? accel_group;
+
+    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 get_selected_uris(); }
+        set { set_selected_uris(value); }
+    }
+
+    /**
+     * Collection shows all objects combined
+     */
+    [Notify]
+    public bool combined {
+        get { return this._combined; }
+        set {
+            if (this._combined != value) {
+                this._combined = value;
+                update_objects_in_collection(false);
+            }
+        }
+    }
+    private bool _combined;
+
+    private enum RowType {
+        BACKEND,
+        PLACE,
+    }
+
+    private enum Columns {
+        ROW_TYPE,
+        ICON,
+        LABEL,
+        TOOLTIP,
+        CATEGORY,
+        COLLECTION,
+        URI,
+        N_COLUMNS;
+    }
+
+    private Type[] column_types = {
+        typeof(uint),
+        typeof(Icon),
+        typeof(string),
+        typeof(string),
+        typeof(bool),
+        typeof(string),
+        typeof(Gcr.Collection),
+        typeof(string)
+    };
+
+    public Sidebar() {
+        this.store = new Gtk.ListStore(Columns.N_COLUMNS, column_types);
+
+        this.backends = new List<Backend>();
+        this.selection = new HashTable<Gcr.Collection, Gcr.Collection>(direct_hash, direct_equal);
+        this.objects = new Gcr.UnionCollection();
+        this.chosen = new HashTable<string, string>(str_hash, str_equal);
+
+        this.accel_group = new Gtk.AccelGroup();
+
+        get_style_context().add_class("sidebar");
+
+        set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
+        set_hadjustment(null);
+        set_vadjustment(null);
+        set_shadow_type(Gtk.ShadowType.OUT);
+        get_style_context().set_junction_sides(Gtk.JunctionSides.RIGHT | Gtk.JunctionSides.LEFT);
+
+        /* tree view */
+        this.tree_view = new Gtk.TreeView();
+        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", Columns.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", Columns.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", Columns.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);
+        this.tree_view.append_column(col);
+
+        this.tree_view.set_headers_visible(false);
+        this.tree_view.set_tooltip_column(Columns.TOOLTIP);
+        this.tree_view.set_search_column(Columns.LABEL);
+        this.tree_view.set_model(this.store);
+        this.tree_view.popup_menu.connect(on_tree_view_popup_menu);
+        this.tree_view.button_press_event.connect(on_tree_view_button_press_event);
+        this.tree_view.motion_notify_event.connect(on_tree_view_motion_notify_event);
+        this.tree_view.button_release_event.connect(on_tree_view_button_release_event);
+        add(this.tree_view);
+        this.tree_view.show();
+
+        Gtk.TreeSelection selection = this.tree_view.get_selection();
+        selection.set_mode(Gtk.SelectionMode.MULTIPLE);
+        selection.set_select_function(on_tree_selection_validate);
+        selection.changed.connect(() => update_objects_for_selection(selection));
+
+        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 (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() {
+        foreach (Backend backend in Backend.get_registered()) {
+            this.backends.append(backend);
+            backend.added.connect(on_place_added);
+            backend.removed.connect(on_place_removed);
+            backend.notify.connect(on_backend_changed);
+
+            foreach (weak GLib.Object obj in backend.get_objects())
+                on_place_added(backend, obj);
+        }
+
+        this.backends.sort((a, b) => {
+            int ordera = order_from_backend((Backend) a);
+            int orderb = order_from_backend((Backend) b);
+            return ordera - orderb;
+        });
+    }
+
+    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_backend_changed(GLib.Object obj, ParamSpec spec) {
+        update_places_later();
+    }
+
+    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 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.GENERIC_FALLBACK);
+        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.GENERIC_FALLBACK);
+        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 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++;
+                }
+            }
+        }
+        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 invalidate_sidebar_pixbufs() {
+        this.pixbuf_lock = null;
+        this.pixbuf_unlock = null;
+        this.pixbuf_lock_l = null;
+        this.pixbuf_unlock_l = null;
+    }
+
+    private void next_or_append_row(Gtk.ListStore? store, 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((void*) iter) && iter.user_data3 == ((void*) iter) &&
+            iter.user_data2 == ((void*) iter) && iter.user_data == ((void*) iter)) {
+            if (!store.get_iter_first(out iter))
+                store.append(out iter);
+            return;
+        }
+
+        if (!store.iter_next(ref iter)) {
+            store.append(out iter);
+            return;
+        }
+
+        for (;;) {
+            string? row_category;
+            Gcr.Collection? row_collection;
+            store.get(iter, Columns.CATEGORY, out row_category,
+                            Columns.COLLECTION, out row_collection, -1);
+
+            if (row_category == category && row_collection == collection)
+                return;
+
+            if (!store.remove(ref iter)) {
+                store.append(out iter);
+                return;
+            }
+        }
+    }
+
+    private void update_backend(Backend? backend, Gtk.TreeIter? iter) {
+        if (backend.get_objects() == null) // Ignore categories that have nothing
+            return;
+
+        next_or_append_row(this.store, iter, backend.name, backend);
+        this.store.set(iter, Columns.ROW_TYPE, RowType.BACKEND,
+                             Columns.CATEGORY, backend.name,
+                             Columns.LABEL, backend.label,
+                             Columns.TOOLTIP, backend.description,
+                             Columns.COLLECTION, backend,
+                             -1);
+
+        foreach (weak GLib.Object obj in backend.get_objects()) {
+            Place place = obj as Place;
+            if (place == null)
+                continue;
+
+            next_or_append_row(this.store, iter, backend.name, place);
+            this.store.set(iter, Columns.ROW_TYPE, RowType.PLACE,
+                                 Columns.CATEGORY, backend.name,
+                                 Columns.LABEL, place.label,
+                                 Columns.TOOLTIP, place.description,
+                                 Columns.ICON, place.icon,
+                                 Columns.COLLECTION, place,
+                                 Columns.URI, place.uri,
+                                 -1);
+        }
+    }
+
+    private void update_objects_in_collection(bool update_chosen) {
+        if (this.updating) // Updating collection is blocked
+            return;
+
+        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 = (this.chosen.lookup(uri) != null);
+                    if (include && !have) {
+                        this.chosen.insert(uri, "");
+                        changed = true;
+                    } else if (!include && have) {
+                        this.chosen.remove(uri);
+                        changed = true;
+                    }
+                }
+
+                // Combined overrides and shows all objects
+                if (this.combined)
+                    include = true;
+
+                bool have = this.objects.have(place);
+                if (include && !have)
+                    this.objects.add(place);
+                else if (!include && have)
+                    this.objects.remove(place);
+            }
+        }
+    }
+
+    private void update_objects_for_selection(Gtk.TreeSelection selection) {
+        if (this.updating)
+            return;
+
+        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, Columns.COLLECTION, out collection, -1);
+            if (collection != null)
+                selected.insert(collection, collection);
+        });
+
+        this.selection = selected;
+
+        if (!this.combined)
+            update_objects_in_collection(true);
+    }
+
+    private void update_objects_for_chosen(HashTable<string, string>? chosen) {
+        this.updating = true;
+
+        Gtk.TreeSelection selection = this.tree_view.get_selection();
+
+        // 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, Columns.COLLECTION, out collection,
+                                     Columns.URI, out uri, -1);
+
+                if (collection != null && uri != null) {
+                    if (chosen.lookup(uri) != null)
+                        selection.select_iter(iter);
+                    else
+                        selection.unselect_iter(iter);
+                }
+            } while (this.store.iter_next(ref iter));
+        }
+
+        this.updating = false;
+        update_objects_for_selection(selection);
+    }
+
+    private void update_places() {
+        Gtk.TreeIter iter = Gtk.TreeIter();
+        iter.stamp = (int) &iter; // A marker that tells us the iter is not yet valid
+        iter.user_data3 = iter.user_data2 = iter.user_data = &iter;
+
+        foreach (Backend backend in this.backends)
+            update_backend(backend, iter);
+
+        // Update selection
+        update_objects_for_chosen(this.chosen);
+
+        if (this.combined)
+            update_objects_in_collection(false);
+    }
+
+    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
+            });
+        }
+    }
+
+    private Lockable? lookup_lockable_for_iter(Gtk.TreeModel? model, Gtk.TreeIter? iter) {
+        Gcr.Collection? collection = null;
+        model.get(iter, Columns.COLLECTION, out collection, -1);
+
+        return collection as Lockable;
+    }
+
+    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;
+
+        Lockable? lockable = lookup_lockable_for_iter(model, iter);
+        if (lockable != null) {
+            can_lock = lockable.lockable;
+            can_unlock = lockable.unlockable;
+        }
+
+        if (can_lock || can_unlock) {
+            ensure_sidebar_pixbufs();
+
+            bool highlight = false;
+            if (this.action_highlight_path!= null) {
+                Gtk.TreePath? path = model.get_path(iter);
+                highlight = path.compare(this.action_highlight_path) == 0;
+            }
+
+            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;
+
+            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;
+        }
+    }
+
+    private void on_cell_renderer_heading_visible(Gtk.CellLayout layout, Gtk.CellRenderer? cell,
+                                                  Gtk.TreeModel? model, Gtk.TreeIter? iter) {
+        RowType type;
+        model.get(iter, Columns.ROW_TYPE, out type, -1);
+        cell.visible = (type == RowType.BACKEND);
+    }
+
+    private void on_padding_cell_renderer(Gtk.CellLayout layout, Gtk.CellRenderer? cell,
+                                          Gtk.TreeModel? model, Gtk.TreeIter? iter) {
+        RowType type;
+        model.get(iter, Columns.ROW_TYPE, out type, -1);
+
+        if (type == RowType.BACKEND) {
+            cell.visible = false;
+            cell.xpad = 0;
+            cell.ypad = 0;
+        } else {
+            cell.visible = true;
+            cell.xpad = 3;
+            cell.ypad = 3;
+        }
+    }
+
+    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, Columns.ROW_TYPE, out type, -1);
+        cell.visible = (type != RowType.BACKEND);
+    }
+
+    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);
+
+        RowType row_type;
+        model.get(iter, Columns.ROW_TYPE, out row_type, -1);
+        if (row_type == RowType.BACKEND)
+            return false;
+
+        return true;
+    }
+
+    private void place_lock(Lockable lockable, Gtk.Window? window) {
+        Cancellable cancellable = new Cancellable();
+        TlsInteraction interaction = new Interaction(window);
+
+        lockable.lock.begin(interaction, cancellable, (obj, res) => {
+            try {
+                lockable.lock.end(res);
+            } catch (Error e) {
+                Util.show_error(window, _("Couldn’t lock"), e.message);
+            }
+        });
+    }
+
+    private void on_place_lock(Gtk.MenuItem item, Lockable lockable) {
+        place_lock(lockable, (Gtk.Window) item.get_toplevel());
+    }
+
+    private void place_unlock(Lockable lockable, Gtk.Window? window) {
+        Cancellable cancellable = new Cancellable();
+        TlsInteraction interaction = new Interaction(window);
+
+        lockable.unlock.begin(interaction, cancellable, (obj, res) => {
+            try {
+                lockable.unlock.end(res);
+            } catch (Error e) {
+                Util.show_error(window, _("Couldn’t unlock"), e.message);
+            }
+        });
+    }
+
+    private void on_place_unlock(Gtk.MenuItem item, Lockable lockable) {
+        place_unlock(lockable, (Gtk.Window) item.get_toplevel());
+    }
+
+    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);
+                }
+            });
+        }
+    }
+
+    private void popup_menu_for_place(Place place, uint button, uint32 activate_time) {
+        Gtk.Menu menu = new Gtk.Menu();
+
+        // First add all the actions from the collection
+        Gtk.ActionGroup actions = place.actions;
+        foreach (weak Gtk.Action action in actions.list_actions()) {
+            action.set_accel_group(this.accel_group);
+            menu.append((Gtk.MenuItem) action.create_menu_item());
+        }
+
+        // Lock and unlock items
+        if (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);
+            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);
+            menu.append(item);
+        }
+
+        // Delete item
+        if (place is Deletable) {
+            Gtk.ImageMenuItem item = new Gtk.ImageMenuItem.with_mnemonic(_("_Delete"));
+            item.set_image(new Gtk.Image.from_stock(Gtk.Stock.DELETE, Gtk.IconSize.MENU));
+            item.activate.connect(() => on_place_delete(item, (Deletable) place));
+            place.bind_property("deletable", item, "sensitive", BindingFlags.SYNC_CREATE);
+            menu.append(item);
+            item.show();
+        }
+
+        // Properties item
+        if (place is Viewable) {
+            Gtk.ImageMenuItem item = new Gtk.ImageMenuItem.with_mnemonic(_("_Properties"));
+            item.set_image(new Gtk.Image.from_stock(Gtk.Stock.PROPERTIES, Gtk.IconSize.MENU));
+            item.activate.connect(() => Viewable.view(place, (Gtk.Window) item.get_toplevel()));
+            menu.append(item);
+            item.show();
+        }
+
+        bool visible = false;
+        menu.foreach((widget) => {
+            if (widget.visible)
+                visible = true;
+        });
+
+        if (visible) {
+            menu.popup(null, null, null, button, activate_time);
+            menu.attach_to_widget(this, null);
+            menu.show();
+        } else {
+            menu.destroy();
+        }
+    }
+
+    private bool on_tree_view_popup_menu(Gtk.Widget? widget) {
+        Gtk.TreePath? path;
+        this.tree_view.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, Columns.COLLECTION, out collection, -1);
+
+        if (collection is Place) {
+            popup_menu_for_place((Place) collection, 0, Gtk.get_current_event_time());
+            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 (this.tree_view.get_path_at_pos(x, y, out path, out column, null, null)) {
+            Gtk.TreeIter iter;
+            this.store.get_iter(out iter, path);
+
+            int hseparator;
+            this.tree_view.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_tree_view_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_tree_view_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 (!this.tree_view.get_path_at_pos((int) event.x, (int) event.y, out path, null, null, null))
+            return false;
+
+        this.tree_view.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, Columns.COLLECTION, out collection, -1);
+
+        if (collection is Place)
+            popup_menu_for_place((Place) collection, event.button, event.time);
+
+        return true;
+    }
+
+    private bool on_tree_view_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[] get_selected_uris() {
+        string[] results = {};
+        this.chosen.foreach((key, v) => {
+            results += key;
+        });
+        results += null;
+
+        return results;
+    }
+
+    public void set_selected_uris(string[] uris) {
+        // For quick lookups
+        HashTable<string, string>? chosen = new HashTable<string, string>(str_hash, str_equal);
+        foreach (string uri in uris)
+            chosen.insert(uri, "");
+
+        update_objects_for_chosen(chosen);
+        this.chosen = chosen;
+    }
+
+    public List<weak Gcr.Collection>? get_selected_places() {
+        List<weak Gcr.Collection> places = this.objects.elements();
+
+        Gtk.TreePath? path = null;
+        this.tree_view.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, Columns.ROW_TYPE, out row_type,
+                                 Columns.COLLECTION, out collection, -1);
+
+            if (collection != null) {
+                if (row_type == RowType.PLACE) {
+                    places.remove(collection);
+                    places.prepend(collection);
+                }
+            }
+        }
+
+        return places;
+    }
+
+    public Place? get_focused_place() {
+        Gtk.TreeIter iter;
+
+        Gtk.TreePath? path = null;
+        this.tree_view.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, Columns.ROW_TYPE, out row_type,
+                                 Columns.COLLECTION, out collection, -1);
+
+            if (row_type == RowType.PLACE)
+                return (Place) collection;
+        }
+
+        return null;
+    }
+
+    public List<weak Backend>? get_backends() {
+        Gtk.TreeIter iter;
+
+        List<weak Backend> backends = this.backends.copy();
+        backends.reverse();
+
+        Gtk.TreePath? path = null;
+        this.tree_view.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, Columns.ROW_TYPE, out row_type,
+                                 Columns.COLLECTION, out collection, -1);
+
+            if (collection != null) {
+                if (row_type == RowType.BACKEND) {
+                    backends.remove((Backend) collection);
+                    backends.prepend((Backend) collection);
+                }
+            }
+        }
+
+        return backends;
+    }
+}
diff --git a/ssh/meson.build b/ssh/meson.build
index efb0719..f122d23 100644
--- a/ssh/meson.build
+++ b/ssh/meson.build
@@ -18,6 +18,7 @@ ssh_sources = [
 ssh_dependencies = [
   glib_deps,
   gcr,
+  gcr_ui,
   posix,
   gtk,
   common_dep,


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