From a37ce0a844e2c807c5bef7deae8f8cbefc2ee0c0 Mon Sep 17 00:00:00 2001 From: Benjamin Otte Date: Tue, 18 Sep 2018 04:56:19 +0200 Subject: [PATCH] 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 | 345 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 569 insertions(+), 13 deletions(-) create mode 100644 tests/testlistview.c 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; - if (gtk_list_view_is_empty (self)) + /* XXX: Figure out how to split a given height into per-row heights. + * Good luck! */ + if (orientation == GTK_ORIENTATION_HORIZONTAL) + for_size = -1; + + 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..dce92f2e66 --- /dev/null +++ b/tests/testlistview.c @@ -0,0 +1,345 @@ +#include + +#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 char * +get_file_path (GFileInfo *info) +{ + GFile *file; + + file = g_object_get_data (G_OBJECT (info), "file"); + return g_file_get_path (file); +} + +static GListModel * +create_list_model_for_directory (gpointer file) +{ + GtkSortListModel *sort; + GListStore *store; + GtkSorter *sorter; + + 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; + + sorter = gtk_string_sorter_new (gtk_cclosure_expression_new (G_TYPE_STRING, NULL, 0, NULL, (GCallback) get_file_path, NULL, NULL)); + sort = gtk_sort_list_model_new (G_LIST_MODEL (store), sorter); + g_object_unref (sorter); + + 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; +}