[gnome-boxes] Add ListView widget



commit 45206f58001e74bfde5291bbfdee2e78248247ee
Author: Adrien Plazas <kekun plazas laposte net>
Date:   Mon Jul 20 14:18:15 2015 +0200

    Add ListView widget
    
    This will be used in a subsequent commit by the app window to offer a
    list view.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=733252

 data/gnome-boxes.gresource.xml |    1 +
 data/ui/list-view.ui           |   38 ++++
 src/Makefile.am                |    1 +
 src/list-view.vala             |  415 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 455 insertions(+), 0 deletions(-)
---
diff --git a/data/gnome-boxes.gresource.xml b/data/gnome-boxes.gresource.xml
index 6385c4f..c935191 100644
--- a/data/gnome-boxes.gresource.xml
+++ b/data/gnome-boxes.gresource.xml
@@ -14,6 +14,7 @@
     <file preprocess="xml-stripblanks">ui/empty-boxes.ui</file>
     <file preprocess="xml-stripblanks">ui/editable-entry.ui</file>
     <file preprocess="xml-stripblanks">ui/resource-graph.ui</file>
+    <file preprocess="xml-stripblanks">ui/list-view.ui</file>
     <file preprocess="xml-stripblanks">ui/list-view-row.ui</file>
     <file preprocess="xml-stripblanks">ui/notification.ui</file>
     <file preprocess="xml-stripblanks">ui/properties-toolbar.ui</file>
diff --git a/data/ui/list-view.ui b/data/ui/list-view.ui
new file mode 100644
index 0000000..47eb4a0
--- /dev/null
+++ b/data/ui/list-view.ui
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.9 -->
+  <template class="BoxesListView" parent="GtkScrolledWindow">
+    <property name="visible">True</property>
+    <property name="hscrollbar-policy">never</property>
+    <property name="vscrollbar-policy">automatic</property>
+
+    <child>
+      <object class="GtkViewport" id="viewport">
+        <property name="visible">True</property>
+        <style>
+          <class name="view"/>
+          <class name="content-view"/>
+        </style>
+
+        <child>
+          <object class="GtkListBox" id="list_box">
+            <property name="visible">True</property>
+            <property name="margin-start">24</property>
+            <property name="margin-end">24</property>
+            <property name="margin-top">12</property>
+            <property name="margin-bottom">12</property>
+            <property name="halign">center</property>
+            <property name="valign">start</property>
+            <signal name="button-release-event" handler="on_button_press_event"/>
+            <signal name="key-press-event" handler="on_key_press_event"/>
+            <style>
+              <class name="boxes-list-box"/>
+              <class name="view"/>
+              <class name="content-view"/>
+            </style>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/Makefile.am b/src/Makefile.am
index c86a64a..00a6286 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -119,6 +119,7 @@ gnome_boxes_SOURCES =                               \
        libvirt-broker.vala                     \
        libvirt-machine.vala                    \
        libvirt-machine-properties.vala         \
+       list-view.vala                          \
        list-view-row.vala                      \
        machine.vala                            \
        machine-thumbnailer.vala                \
diff --git a/src/list-view.vala b/src/list-view.vala
new file mode 100644
index 0000000..aa03ba4
--- /dev/null
+++ b/src/list-view.vala
@@ -0,0 +1,415 @@
+// This file is part of GNOME Boxes. License: LGPLv2+
+
+[GtkTemplate (ui = "/org/gnome/Boxes/ui/list-view.ui")]
+private class Boxes.ListView: Gtk.ScrolledWindow, Boxes.ICollectionView, Boxes.UI {
+    public UIState previous_ui_state { get; protected set; }
+    public UIState ui_state { get; protected set; }
+
+    public CollectionFilter filter { get; protected set; }
+
+    [GtkChild]
+    private Gtk.ListBox list_box;
+
+    private ListStore store;
+    private GLib.List<CollectionItem> hidden_items;
+    private HashTable<CollectionItem, ItemConnections> items_connections;
+
+    private AppWindow window;
+    private Boxes.ActionsPopover context_popover;
+
+    private class ItemConnections: Object {
+        private ulong categories_id;
+        private ulong name_id;
+        private ulong info_id;
+
+        public weak ListView view { get; private set; }
+        public Machine machine { get; private set; }
+
+        public ItemConnections (ListView view, Machine machine) {
+            this.view = view;
+            this.machine = machine;
+
+            categories_id = machine.config.notify["categories"].connect (() => {
+                view.list_box.invalidate_sort ();
+                view.list_box.invalidate_filter ();
+            });
+            name_id = machine.notify["name"].connect (() => {
+                view.list_box.invalidate_sort ();
+                view.list_box.invalidate_filter ();
+            });
+            info_id = machine.notify["info"].connect (() => {
+                view.list_box.invalidate_sort ();
+                view.list_box.invalidate_filter ();
+            });
+        }
+
+        public override void dispose () {
+            machine.config.disconnect (categories_id);
+            machine.disconnect (name_id);
+            machine.disconnect (info_id);
+            base.dispose ();
+        }
+    }
+
+    construct {
+        hidden_items = new GLib.List<CollectionItem> ();
+        items_connections = new HashTable<CollectionItem, ItemConnections> (direct_hash, direct_equal);
+
+        setup_store ();
+        setup_list_box ();
+
+        filter = new CollectionFilter ();
+        filter.notify["text"].connect (() => {
+            list_box.invalidate_filter ();
+        });
+        filter.filter_func_changed.connect (() => {
+            list_box.invalidate_filter ();
+        });
+
+        notify["ui-state"].connect (ui_state_changed);
+    }
+
+    public void setup_ui (AppWindow window) {
+        this.window = window;
+
+        window.notify["selection-mode"].connect (() => {
+            list_box.selection_mode = window.selection_mode ? Gtk.SelectionMode.MULTIPLE :
+                                                              Gtk.SelectionMode.NONE;
+
+            update_selection_mode ();
+        });
+
+        context_popover = new Boxes.ActionsPopover (window);
+    }
+
+    public void add_item (CollectionItem item) {
+        var machine = item as Machine;
+
+        if (machine == null) {
+            warning ("Cannot add item %p".printf (&item));
+
+            return;
+        }
+
+        var window = machine.window;
+        if (window.ui_state == UIState.WIZARD) {
+            // Don't show newly created items until user is out of wizard
+            hidden_items.append (item);
+
+            ulong ui_state_id = 0;
+
+            ui_state_id = window.notify["ui-state"].connect (() => {
+                if (window.ui_state == UIState.WIZARD)
+                    return;
+
+                if (hidden_items.find (item) != null) {
+                    add_item (item);
+                    hidden_items.remove (item);
+                }
+                window.disconnect (ui_state_id);
+            });
+
+            return;
+        }
+
+        store.append (item);
+        items_connections[item] = new ItemConnections (this, machine);
+
+        item.set_state (window.ui_state);
+    }
+
+    public void remove_item (CollectionItem item) {
+        hidden_items.remove (item);
+        items_connections.remove (item);
+
+        uint index = 0;
+        while (index < store.get_n_items () && store.get_item (index) != item)
+            index++;
+
+        if (index >= store.get_n_items ()) {
+            debug ("item not in view or already removed");
+
+            return;
+        }
+
+        // FIXME: Dirty hack to workaround this bug in GTK+: 
https://bugzilla.gnome.org/show_bug.cgi?id=752615
+        // Set the default sorting and filtering functions to make the model's and the view's orders match 
before
+        // removing the element, then set the regular sorting and filtering functions back.
+        list_box.set_sort_func (default_sort);
+        list_box.set_filter_func (default_filter);
+
+        store.remove (index);
+
+        // FIXME: Workaround for bug #752615 (see above).
+        list_box.set_sort_func (model_sort);
+        list_box.set_filter_func (model_filter);
+    }
+
+    public void select_by_criteria (SelectionCriteria criteria) {
+        window.selection_mode = true;
+
+        switch (criteria) {
+        default:
+        case SelectionCriteria.ALL:
+            foreach_row ((box_row) => { select_row (box_row); });
+
+            break;
+        case SelectionCriteria.NONE:
+            foreach_row ((box_row) => { unselect_row (box_row); });
+
+            break;
+        case SelectionCriteria.RUNNING:
+            foreach_row ((box_row) => {
+                var item = get_item_for_row (box_row);
+                if (item != null && item is Machine && (item as Machine).is_running)
+                    select_row (box_row);
+                else
+                    unselect_row (box_row);
+            });
+
+            break;
+        }
+
+        App.app.notify_property ("selected-items");
+    }
+
+    public List<CollectionItem> get_selected_items () {
+        var selected = new List<CollectionItem> ();
+
+        foreach (var box_row in list_box.get_selected_rows ()) {
+            var item = get_item_for_row (box_row);
+            selected.append (item);
+        }
+
+        return (owned) selected;
+    }
+
+    public void activate_first_item () {
+        if (store.get_n_items () >= 1) {
+            var box_row = list_box.get_row_at_index (0);
+            list_box.row_activated (box_row);
+        }
+    }
+
+    private void setup_store () {
+        store = new ListStore (typeof (CollectionItem));
+
+        store.items_changed.connect (() => {
+            App.app.notify_property ("selected-items");
+        });
+
+        store.items_changed.connect (() => {
+            App.app.notify_property ("selected-items");
+        });
+    }
+
+    private void setup_list_box () {
+        list_box.selection_mode = Gtk.SelectionMode.NONE;
+        list_box.set_sort_func (model_sort);
+        list_box.set_filter_func (model_filter);
+
+        list_box.bind_model (store, (object) => {
+            var view_row = new ListViewRow (object as CollectionItem);
+            view_row.notify["selected"].connect (() => {
+                propagate_view_row_selection (view_row);
+            });
+
+            return view_row;
+        });
+
+        list_box.row_activated.connect ((box_row) => {
+            if (window.selection_mode)
+                return;
+
+            var item = get_item_for_row (box_row);
+            if (item is LibvirtMachine && (item as LibvirtMachine).importing)
+                return;
+
+            window.select_item (item);
+        });
+
+        update_selection_mode ();
+    }
+
+    private CollectionItem? get_item_for_row (Gtk.ListBoxRow box_row) {
+        var view = box_row.get_child () as ListViewRow;
+        if (view == null)
+            return null;
+
+        return view.item;
+    }
+
+    private void foreach_row (Func<Gtk.ListBoxRow> func) {
+        list_box.forall ((child) => {
+            var box_row = child as Gtk.ListBoxRow;
+            if (box_row == null)
+                return;
+
+            func (box_row);
+        });
+    }
+
+    // FIXME: Part of the workaround for this bug in GTK+: https://bugzilla.gnome.org/show_bug.cgi?id=752615
+    // Sort the items in the same order than in the model.
+    private int default_sort (Gtk.ListBoxRow box_row1, Gtk.ListBoxRow box_row2) {
+            var item1 = get_item_for_row (box_row1);
+            uint pos1 = 0;
+            while (pos1 < store.get_n_items () && store.get_item (pos1) != item1)
+                pos1++;
+
+            var item2 = get_item_for_row (box_row2);
+            uint pos2 = 0;
+            while (pos2 < store.get_n_items () && store.get_item (pos2) != item2)
+                pos2++;
+
+            return ((pos1 > pos2) ? 1 : 0) - ((pos1 < pos2) ? 1 : 0);
+    }
+
+    // FIXME: Part of the workaround for this bug in GTK+: https://bugzilla.gnome.org/show_bug.cgi?id=752615
+    // Show all items.
+    private bool default_filter (Gtk.ListBoxRow box_row) {
+        return true;
+    }
+
+    private int model_sort (Gtk.ListBoxRow box_row1, Gtk.ListBoxRow box_row2) {
+        var view_row1 = box_row1.get_child () as ListViewRow;
+        var view_row2 = box_row2.get_child () as ListViewRow;
+        var item1 = view_row1.item;
+        var item2 = view_row2.item;
+
+        if (item1 == null || item2 == null)
+            return 0;
+
+        return item1.compare (item2);
+    }
+
+    private bool model_filter (Gtk.ListBoxRow box_row) {
+        var view = box_row.get_child () as ListViewRow;
+        if (view  == null)
+            return false;
+
+        var item = view.item;
+        if (item  == null)
+            return false;
+
+        return filter.filter (item as CollectionItem);
+    }
+
+    private void ui_state_changed () {
+        if (ui_state == UIState.COLLECTION)
+            list_box.unselect_all ();
+    }
+
+    [GtkCallback]
+    private bool on_button_press_event (Gdk.EventButton event) {
+        if (event.type != Gdk.EventType.BUTTON_RELEASE)
+            return false;
+
+        switch (event.button) {
+        case 1:
+            return on_button_1_press_event (event);
+        case 3:
+            return on_button_3_press_event (event);
+        default:
+            return false;
+        }
+    }
+
+    private bool on_button_1_press_event (Gdk.EventButton event) {
+        if (!window.selection_mode)
+            return false;
+
+        // Necessary to avoid treating an event from a child widget which would mess with getting the 
correct row.
+        if (event.window != list_box.get_window ())
+            return false;
+
+        var box_row = list_box.get_row_at_y ((int) event.y);
+        if (box_row == null)
+            return false;
+
+        toggle_row_selected (box_row);
+
+        return true;
+    }
+
+    private bool on_button_3_press_event (Gdk.EventButton event) {
+        var box_row = list_box.get_row_at_y ((int) event.y);
+        if (box_row == null)
+            return false;
+
+        return launch_context_popover_for_row (box_row);
+    }
+
+    [GtkCallback]
+    private bool on_key_press_event (Gdk.EventKey event) {
+        if (event.keyval != Gdk.Key.Menu)
+            return false;
+
+        var box_row = list_box.get_selected_row ();
+        if (box_row == null)
+            return false;
+
+        return launch_context_popover_for_row (box_row);
+    }
+
+    private bool launch_context_popover_for_row (Gtk.ListBoxRow box_row) {
+        var item = get_item_for_row (box_row);
+        if (item == null)
+            return false;
+
+        context_popover.update_for_item (item);
+        context_popover.relative_to = box_row;
+        context_popover.show ();
+
+        return true;
+    }
+
+    private void update_selection_mode () {
+        foreach_row ((box_row) => {
+            var view_row = box_row.get_child () as ListViewRow;
+
+            if (view_row.selection_mode != window.selection_mode)
+                view_row.selection_mode = window.selection_mode;
+
+            unselect_row (box_row);
+        });
+    }
+
+    private void propagate_view_row_selection (ListViewRow view_row) {
+        var box_row = view_row.parent as Gtk.ListBoxRow;
+
+        if (view_row.selected)
+            select_row (box_row);
+        else
+            unselect_row (box_row);
+    }
+
+    private void toggle_row_selected (Gtk.ListBoxRow box_row) {
+        var view_row = box_row.get_child () as ListViewRow;
+
+        if (view_row.selected)
+            unselect_row (box_row);
+        else
+            select_row (box_row);
+    }
+
+    private void select_row (Gtk.ListBoxRow box_row) {
+        var view_row = box_row.get_child () as ListViewRow;
+
+        list_box.select_row (box_row);
+        if (!view_row.selected)
+            view_row.selected = true;
+
+        App.app.notify_property ("selected-items");
+    }
+
+    private void unselect_row (Gtk.ListBoxRow box_row) {
+        var view_row = box_row.get_child () as ListViewRow;
+
+        list_box.unselect_row (box_row);
+        if (view_row.selected)
+            view_row.selected = false;
+
+        App.app.notify_property ("selected-items");
+    }
+}


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