[gtk/wip/otte/listview: 24/139] listview: Make widget actually do something
- From: Benjamin Otte <otte src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gtk/wip/otte/listview: 24/139] listview: Make widget actually do something
- Date: Tue, 26 Nov 2019 16:42:07 +0000 (UTC)
commit e8ad7688701df083cc0f1bd95a7ce090fa079db8
Author: Benjamin Otte <otte redhat com>
Date: Tue Sep 18 04:56:19 2018 +0200
listview: Make widget actually do something
The thing we're actually doing is create and maintain a widget for every
row. That's it.
Also add a testcase using this. The testcase quickly allocates too many
rows though and then becomes unresponsive though. You have been warned.
gtk/gtklistview.c | 236 +++++++++++++++++++++++++++++++--
tests/meson.build | 1 +
tests/testlistview.c | 368 +++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 592 insertions(+), 13 deletions(-)
---
diff --git a/gtk/gtklistview.c b/gtk/gtklistview.c
index b8a5337d8a..17e4925d0d 100644
--- a/gtk/gtklistview.c
+++ b/gtk/gtklistview.c
@@ -22,6 +22,7 @@
#include "gtklistview.h"
#include "gtkintl.h"
+#include "gtkrbtreeprivate.h"
#include "gtklistitemfactoryprivate.h"
/**
@@ -33,12 +34,30 @@
* GtkListView is a widget to present a view into a large dynamic list of items.
*/
+typedef struct _ListRow ListRow;
+typedef struct _ListRowAugment ListRowAugment;
+
struct _GtkListView
{
GtkWidget parent_instance;
GListModel *model;
GtkListItemFactory *item_factory;
+
+ GtkRbTree *rows;
+};
+
+struct _ListRow
+{
+ guint n_rows;
+ guint height;
+ GtkWidget *widget;
+};
+
+struct _ListRowAugment
+{
+ guint n_rows;
+ guint height;
};
enum
@@ -53,10 +72,78 @@ G_DEFINE_TYPE (GtkListView, gtk_list_view, GTK_TYPE_WIDGET)
static GParamSpec *properties[N_PROPS] = { NULL, };
-static gboolean
-gtk_list_view_is_empty (GtkListView *self)
+static void
+list_row_augment (GtkRbTree *tree,
+ gpointer node_augment,
+ gpointer node,
+ gpointer left,
+ gpointer right)
{
- return self->model == NULL;
+ ListRow *row = node;
+ ListRowAugment *aug = node_augment;
+
+ aug->height = row->height;
+ aug->n_rows = row->n_rows;
+
+ if (left)
+ {
+ ListRowAugment *left_aug = gtk_rb_tree_get_augment (tree, left);
+
+ aug->height += left_aug->height;
+ aug->n_rows += left_aug->n_rows;
+ }
+
+ if (right)
+ {
+ ListRowAugment *right_aug = gtk_rb_tree_get_augment (tree, right);
+
+ aug->height += right_aug->height;
+ aug->n_rows += right_aug->n_rows;
+ }
+}
+
+static void
+list_row_clear (gpointer _row)
+{
+ ListRow *row = _row;
+
+ g_clear_pointer (&row->widget, gtk_widget_unparent);
+}
+
+static ListRow *
+gtk_list_view_get_row (GtkListView *self,
+ guint position,
+ guint *offset)
+{
+ ListRow *row, *tmp;
+
+ row = gtk_rb_tree_get_root (self->rows);
+
+ while (row)
+ {
+ tmp = gtk_rb_tree_node_get_left (row);
+ if (tmp)
+ {
+ ListRowAugment *aug = gtk_rb_tree_get_augment (self->rows, tmp);
+ if (position < aug->n_rows)
+ {
+ row = tmp;
+ continue;
+ }
+ position -= aug->n_rows;
+ }
+
+ if (position < row->n_rows)
+ break;
+ position -= row->n_rows;
+
+ row = gtk_rb_tree_node_get_right (row);
+ }
+
+ if (offset)
+ *offset = row ? position : 0;
+
+ return row;
}
static void
@@ -69,17 +156,38 @@ gtk_list_view_measure (GtkWidget *widget,
int *natural_baseline)
{
GtkListView *self = GTK_LIST_VIEW (widget);
+ ListRow *row;
+ int min, nat, child_min, child_nat;
+
+ /* XXX: Figure out how to split a given height into per-row heights.
+ * Good luck! */
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ for_size = -1;
- if (gtk_list_view_is_empty (self))
+ min = 0;
+ nat = 0;
+
+ for (row = gtk_rb_tree_get_first (self->rows);
+ row != NULL;
+ row = gtk_rb_tree_node_get_next (row))
{
- *minimum = 0;
- *natural = 0;
- return;
+ gtk_widget_measure (row->widget,
+ orientation, for_size,
+ &child_min, &child_nat, NULL, NULL);
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ {
+ min = MAX (min, child_min);
+ nat = MAX (nat, child_nat);
+ }
+ else
+ {
+ min += child_nat;
+ nat = min;
+ }
}
- *minimum = 0;
- *natural = 0;
- return;
+ *minimum = min;
+ *natural = nat;
}
static void
@@ -88,7 +196,81 @@ gtk_list_view_size_allocate (GtkWidget *widget,
int height,
int baseline)
{
- //GtkListView *self = GTK_LIST_VIEW (widget);
+ GtkListView *self = GTK_LIST_VIEW (widget);
+ GtkAllocation child_allocation = { 0, 0, 0, 0 };
+ ListRow *row;
+ int nat;
+
+ child_allocation.width = width;
+
+ for (row = gtk_rb_tree_get_first (self->rows);
+ row != NULL;
+ row = gtk_rb_tree_node_get_next (row))
+ {
+ gtk_widget_measure (row->widget, GTK_ORIENTATION_VERTICAL,
+ width,
+ NULL, &nat, NULL, NULL);
+ if (row->height != nat)
+ {
+ row->height = nat;
+ gtk_rb_tree_node_mark_dirty (row);
+ }
+ child_allocation.height = row->height;
+ gtk_widget_size_allocate (row->widget, &child_allocation, -1);
+ child_allocation.y += child_allocation.height;
+ }
+}
+
+static void
+gtk_list_view_remove_rows (GtkListView *self,
+ guint position,
+ guint n_rows)
+{
+ ListRow *row;
+ guint i;
+
+ if (n_rows == 0)
+ return;
+
+ row = gtk_list_view_get_row (self, position, NULL);
+
+ for (i = 0; i < n_rows; i++)
+ {
+ ListRow *next = gtk_rb_tree_node_get_next (row);
+ gtk_rb_tree_remove (self->rows, row);
+ row = next;
+ }
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+gtk_list_view_add_rows (GtkListView *self,
+ guint position,
+ guint n_rows)
+{
+ ListRow *row;
+ guint i;
+
+ if (n_rows == 0)
+ return;
+
+ row = gtk_list_view_get_row (self, position, NULL);
+
+ for (i = 0; i < n_rows; i++)
+ {
+ ListRow *new_row;
+ gpointer item;
+
+ new_row = gtk_rb_tree_insert_before (self->rows, row);
+ new_row->n_rows = 1;
+ new_row->widget = gtk_list_item_factory_create (self->item_factory);
+ gtk_widget_insert_before (new_row->widget, GTK_WIDGET (self), row ? row->widget : NULL);
+ item = g_list_model_get_item (self->model, position + i);
+ gtk_list_item_factory_bind (self->item_factory, new_row->widget, item);
+ }
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
}
static void
@@ -98,6 +280,8 @@ gtk_list_view_model_items_changed_cb (GListModel *model,
guint added,
GtkListView *self)
{
+ gtk_list_view_remove_rows (self, position, removed);
+ gtk_list_view_add_rows (self, position, added);
}
static void
@@ -106,6 +290,8 @@ gtk_list_view_clear_model (GtkListView *self)
if (self->model == NULL)
return;
+ gtk_list_view_remove_rows (self, 0, g_list_model_get_n_items (self->model));
+
g_signal_handlers_disconnect_by_func (self->model, gtk_list_view_model_items_changed_cb, self);
g_clear_object (&self->model);
}
@@ -122,6 +308,16 @@ gtk_list_view_dispose (GObject *object)
G_OBJECT_CLASS (gtk_list_view_parent_class)->dispose (object);
}
+static void
+gtk_list_view_finalize (GObject *object)
+{
+ GtkListView *self = GTK_LIST_VIEW (object);
+
+ gtk_rb_tree_unref (self->rows);
+
+ G_OBJECT_CLASS (gtk_list_view_parent_class)->finalize (object);
+}
+
static void
gtk_list_view_get_property (GObject *object,
guint property_id,
@@ -172,6 +368,7 @@ gtk_list_view_class_init (GtkListViewClass *klass)
widget_class->size_allocate = gtk_list_view_size_allocate;
gobject_class->dispose = gtk_list_view_dispose;
+ gobject_class->finalize = gtk_list_view_finalize;
gobject_class->get_property = gtk_list_view_get_property;
gobject_class->set_property = gtk_list_view_set_property;
@@ -195,6 +392,11 @@ gtk_list_view_class_init (GtkListViewClass *klass)
static void
gtk_list_view_init (GtkListView *self)
{
+ self->rows = gtk_rb_tree_new (ListRow,
+ ListRowAugment,
+ list_row_augment,
+ list_row_clear,
+ NULL);
}
/**
@@ -253,9 +455,11 @@ gtk_list_view_set_model (GtkListView *self,
self->model = g_object_ref (model);
g_signal_connect (model,
- "items-changed",
+ "items-changed",
G_CALLBACK (gtk_list_view_model_items_changed_cb),
self);
+
+ gtk_list_view_add_rows (self, 0, g_list_model_get_n_items (model));
}
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MODEL]);
@@ -268,13 +472,19 @@ gtk_list_view_set_functions (GtkListView *self,
gpointer user_data,
GDestroyNotify user_destroy)
{
+ guint n_items;
+
g_return_if_fail (GTK_IS_LIST_VIEW (self));
g_return_if_fail (create_func);
g_return_if_fail (bind_func);
g_return_if_fail (user_data != NULL || user_destroy == NULL);
- g_clear_object (&self->item_factory);
+ n_items = self->model ? g_list_model_get_n_items (self->model) : 0;
+ gtk_list_view_remove_rows (self, 0, n_items);
+ g_clear_object (&self->item_factory);
self->item_factory = gtk_list_item_factory_new (create_func, bind_func, user_data, user_destroy);
+
+ gtk_list_view_add_rows (self, 0, n_items);
}
diff --git a/tests/meson.build b/tests/meson.build
index 0d01766571..1abe8ee147 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -59,6 +59,7 @@ gtk_tests = [
['testlist2'],
['testlist3'],
['testlist4'],
+ ['testlistview'],
['testlevelbar'],
['testlockbutton'],
['testmenubutton'],
diff --git a/tests/testlistview.c b/tests/testlistview.c
new file mode 100644
index 0000000000..8da354ab0b
--- /dev/null
+++ b/tests/testlistview.c
@@ -0,0 +1,368 @@
+#include <gtk/gtk.h>
+
+#define ROWS 30
+
+GSList *pending = NULL;
+guint active = 0;
+
+static void
+got_files (GObject *enumerate,
+ GAsyncResult *res,
+ gpointer store);
+
+static gboolean
+start_enumerate (GListStore *store)
+{
+ GFileEnumerator *enumerate;
+ GFile *file = g_object_get_data (G_OBJECT (store), "file");
+ GError *error = NULL;
+
+ enumerate = g_file_enumerate_children (file,
+ G_FILE_ATTRIBUTE_STANDARD_TYPE
+ "," G_FILE_ATTRIBUTE_STANDARD_ICON
+ "," G_FILE_ATTRIBUTE_STANDARD_NAME,
+ 0,
+ NULL,
+ &error);
+
+ if (enumerate == NULL)
+ {
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TOO_MANY_OPEN_FILES) && active)
+ {
+ g_clear_error (&error);
+ pending = g_slist_prepend (pending, g_object_ref (store));
+ return TRUE;
+ }
+
+ g_clear_error (&error);
+ g_object_unref (store);
+ return FALSE;
+ }
+
+ if (active > 20)
+ {
+ g_object_unref (enumerate);
+ pending = g_slist_prepend (pending, g_object_ref (store));
+ return TRUE;
+ }
+
+ active++;
+ g_file_enumerator_next_files_async (enumerate,
+ g_file_is_native (file) ? 5000 : 100,
+ G_PRIORITY_DEFAULT_IDLE,
+ NULL,
+ got_files,
+ g_object_ref (store));
+
+ g_object_unref (enumerate);
+ return TRUE;
+}
+
+static void
+got_files (GObject *enumerate,
+ GAsyncResult *res,
+ gpointer store)
+{
+ GList *l, *files;
+ GFile *file = g_object_get_data (store, "file");
+ GPtrArray *array;
+
+ files = g_file_enumerator_next_files_finish (G_FILE_ENUMERATOR (enumerate), res, NULL);
+ if (files == NULL)
+ {
+ g_object_unref (store);
+ if (pending)
+ {
+ GListStore *store = pending->data;
+ pending = g_slist_remove (pending, store);
+ start_enumerate (store);
+ }
+ active--;
+ return;
+ }
+
+ array = g_ptr_array_new ();
+ g_ptr_array_new_with_free_func (g_object_unref);
+ for (l = files; l; l = l->next)
+ {
+ GFileInfo *info = l->data;
+ GFile *child;
+
+ child = g_file_get_child (file, g_file_info_get_name (info));
+ g_object_set_data_full (G_OBJECT (info), "file", child, g_object_unref);
+ g_ptr_array_add (array, info);
+ }
+ g_list_free (files);
+
+ g_list_store_splice (store, g_list_model_get_n_items (store), 0, array->pdata, array->len);
+ g_ptr_array_unref (array);
+
+ g_file_enumerator_next_files_async (G_FILE_ENUMERATOR (enumerate),
+ g_file_is_native (file) ? 5000 : 100,
+ G_PRIORITY_DEFAULT_IDLE,
+ NULL,
+ got_files,
+ store);
+}
+
+static int
+compare_files (gconstpointer first,
+ gconstpointer second,
+ gpointer unused)
+{
+ GFile *first_file, *second_file;
+ char *first_path, *second_path;
+ int result;
+#if 0
+ GFileType first_type, second_type;
+
+ /* This is a bit slow, because each g_file_query_file_type() does a stat() */
+ first_type = g_file_query_file_type (G_FILE (first), G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL);
+ second_type = g_file_query_file_type (G_FILE (second), G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL);
+
+ if (first_type == G_FILE_TYPE_DIRECTORY && second_type != G_FILE_TYPE_DIRECTORY)
+ return -1;
+ if (first_type != G_FILE_TYPE_DIRECTORY && second_type == G_FILE_TYPE_DIRECTORY)
+ return 1;
+#endif
+
+ first_file = g_object_get_data (G_OBJECT (first), "file");
+ second_file = g_object_get_data (G_OBJECT (second), "file");
+ first_path = g_file_get_path (first_file);
+ second_path = g_file_get_path (second_file);
+
+ result = strcasecmp (first_path, second_path);
+
+ g_free (first_path);
+ g_free (second_path);
+
+ return result;
+}
+
+static GListModel *
+create_list_model_for_directory (gpointer file)
+{
+ GtkSortListModel *sort;
+ GListStore *store;
+
+ if (g_file_query_file_type (file, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL) != G_FILE_TYPE_DIRECTORY)
+ return NULL;
+
+ store = g_list_store_new (G_TYPE_FILE_INFO);
+ g_object_set_data_full (G_OBJECT (store), "file", g_object_ref (file), g_object_unref);
+
+ if (!start_enumerate (store))
+ return NULL;
+
+ sort = gtk_sort_list_model_new (G_LIST_MODEL (store),
+ compare_files,
+ NULL, NULL);
+ g_object_unref (store);
+ return G_LIST_MODEL (sort);
+}
+
+static GtkWidget *
+create_widget (gpointer unused)
+{
+ return gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4);
+}
+
+static void
+bind_widget (GtkWidget *box,
+ gpointer item,
+ gpointer unused)
+{
+ GtkWidget *child;
+ GFileInfo *info;
+ GFile *file;
+ guint depth;
+ GIcon *icon;
+
+ while (gtk_widget_get_first_child (box))
+ gtk_container_remove (GTK_CONTAINER (box), gtk_widget_get_first_child (box));
+
+ depth = gtk_tree_list_row_get_depth (item);
+ if (depth > 0)
+ {
+ child = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_set_size_request (child, 16 * depth, 0);
+ gtk_container_add (GTK_CONTAINER (box), child);
+ }
+
+ if (gtk_tree_list_row_is_expandable (item))
+ {
+ GtkWidget *title, *arrow;
+
+ child = g_object_new (GTK_TYPE_BOX, "css-name", "expander", NULL);
+
+ title = g_object_new (GTK_TYPE_TOGGLE_BUTTON, "css-name", "title", NULL);
+ gtk_button_set_relief (GTK_BUTTON (title), GTK_RELIEF_NONE);
+ g_object_bind_property (item, "expanded", title, "active", G_BINDING_BIDIRECTIONAL |
G_BINDING_SYNC_CREATE);
+ g_object_set_data_full (G_OBJECT (title), "make-sure-its-not-unreffed", g_object_ref (item),
g_object_unref);
+ gtk_container_add (GTK_CONTAINER (child), title);
+
+ arrow = g_object_new (GTK_TYPE_SPINNER, "css-name", "arrow", NULL);
+ gtk_container_add (GTK_CONTAINER (title), arrow);
+ }
+ else
+ {
+ child = gtk_image_new (); /* empty whatever */
+ }
+ gtk_container_add (GTK_CONTAINER (box), child);
+
+ info = gtk_tree_list_row_get_item (item);
+
+ icon = g_file_info_get_icon (info);
+ if (icon)
+ {
+ child = gtk_image_new_from_gicon (icon);
+ gtk_container_add (GTK_CONTAINER (box), child);
+ }
+
+ file = g_object_get_data (G_OBJECT (info), "file");
+ child = gtk_label_new (g_file_get_basename (file));
+ g_object_unref (info);
+
+ gtk_container_add (GTK_CONTAINER (box), child);
+}
+
+static GListModel *
+create_list_model_for_file_info (gpointer file_info,
+ gpointer unused)
+{
+ GFile *file = g_object_get_data (file_info, "file");
+
+ if (file == NULL)
+ return NULL;
+
+ return create_list_model_for_directory (file);
+}
+
+static gboolean
+update_statusbar (GtkStatusbar *statusbar)
+{
+ GListModel *model = g_object_get_data (G_OBJECT (statusbar), "model");
+ GString *string = g_string_new (NULL);
+ guint n;
+ gboolean result = G_SOURCE_REMOVE;
+
+ gtk_statusbar_remove_all (statusbar, 0);
+
+ n = g_list_model_get_n_items (model);
+ g_string_append_printf (string, "%u", n);
+ if (GTK_IS_FILTER_LIST_MODEL (model))
+ {
+ guint n_unfiltered = g_list_model_get_n_items (gtk_filter_list_model_get_model (GTK_FILTER_LIST_MODEL
(model)));
+ if (n != n_unfiltered)
+ g_string_append_printf (string, "/%u", n_unfiltered);
+ }
+ g_string_append (string, " items");
+
+ if (pending || active)
+ {
+ g_string_append_printf (string, " (%u directories remaining)", active + g_slist_length (pending));
+ result = G_SOURCE_CONTINUE;
+ }
+
+ gtk_statusbar_push (statusbar, 0, string->str);
+ g_free (string->str);
+
+ return result;
+}
+
+static gboolean
+match_file (gpointer item, gpointer data)
+{
+ GtkWidget *search_entry = data;
+ GFileInfo *info = gtk_tree_list_row_get_item (item);
+ GFile *file = g_object_get_data (G_OBJECT (info), "file");
+ char *path;
+ gboolean result;
+
+ path = g_file_get_path (file);
+
+ result = strstr (path, gtk_editable_get_text (GTK_EDITABLE (search_entry))) != NULL;
+
+ g_object_unref (info);
+ g_free (path);
+
+ return result;
+}
+
+static void
+search_changed_cb (GtkSearchEntry *entry,
+ GtkFilter *custom_filter)
+{
+ gtk_filter_changed (custom_filter, GTK_FILTER_CHANGE_DIFFERENT);
+}
+
+int
+main (int argc, char *argv[])
+{
+ GtkWidget *win, *vbox, *sw, *listview, *search_entry, *statusbar;
+ GListModel *dirmodel;
+ GtkTreeListModel *tree;
+ GtkFilterListModel *filter;
+ GtkFilter *custom_filter;
+ GFile *root;
+
+ gtk_init ();
+
+ win = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+ gtk_window_set_default_size (GTK_WINDOW (win), 400, 600);
+ g_signal_connect (win, "destroy", G_CALLBACK (gtk_main_quit), win);
+
+ vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
+ gtk_container_add (GTK_CONTAINER (win), vbox);
+
+ search_entry = gtk_search_entry_new ();
+ gtk_container_add (GTK_CONTAINER (vbox), search_entry);
+
+ sw = gtk_scrolled_window_new (NULL, NULL);
+ gtk_widget_set_vexpand (sw, TRUE);
+ gtk_search_entry_set_key_capture_widget (GTK_SEARCH_ENTRY (search_entry), sw);
+ gtk_container_add (GTK_CONTAINER (vbox), sw);
+
+ listview = gtk_list_view_new ();
+ gtk_list_view_set_functions (GTK_LIST_VIEW (listview),
+ create_widget,
+ bind_widget,
+ NULL, NULL);
+ gtk_container_add (GTK_CONTAINER (sw), listview);
+
+ if (argc > 1)
+ root = g_file_new_for_commandline_arg (argv[1]);
+ else
+ root = g_file_new_for_path (g_get_current_dir ());
+ dirmodel = create_list_model_for_directory (root);
+ tree = gtk_tree_list_model_new (FALSE,
+ dirmodel,
+ TRUE,
+ create_list_model_for_file_info,
+ NULL, NULL);
+ g_object_unref (dirmodel);
+ g_object_unref (root);
+
+ custom_filter = gtk_custom_filter_new (match_file, search_entry, NULL);
+ filter = gtk_filter_list_model_new (G_LIST_MODEL (tree), custom_filter);
+ g_signal_connect (search_entry, "search-changed", G_CALLBACK (search_changed_cb), custom_filter);
+ g_object_unref (custom_filter);
+
+ gtk_list_view_set_model (GTK_LIST_VIEW (listview), G_LIST_MODEL (filter));
+
+ statusbar = gtk_statusbar_new ();
+ gtk_widget_add_tick_callback (statusbar, (GtkTickCallback) update_statusbar, NULL, NULL);
+ g_object_set_data (G_OBJECT (statusbar), "model", filter);
+ g_signal_connect_swapped (filter, "items-changed", G_CALLBACK (update_statusbar), statusbar);
+ update_statusbar (GTK_STATUSBAR (statusbar));
+ gtk_container_add (GTK_CONTAINER (vbox), statusbar);
+
+ g_object_unref (tree);
+ g_object_unref (filter);
+
+ gtk_widget_show (win);
+
+ gtk_main ();
+
+ return 0;
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]