[gtk/wip/otte/listview: 5/46] listview: Make widget actually do something



commit 8c8792823db0b1b1c87c8e07aef6d931d12573f3
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 | 361 +++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 585 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 176685fe50..a411608010 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..5905ed53f6
--- /dev/null
+++ b/tests/testlistview.c
@@ -0,0 +1,361 @@
+#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;
+}
+
+int
+main (int argc, char *argv[])
+{
+  GtkWidget *win, *vbox, *sw, *listview, *search_entry, *statusbar;
+  GListModel *dirmodel;
+  GtkTreeListModel *tree;
+  GtkFilterListModel *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);
+
+  filter = gtk_filter_list_model_new (G_LIST_MODEL (tree),
+                                      match_file,
+                                      search_entry,
+                                      NULL);
+  g_signal_connect_swapped (search_entry, "search-changed", G_CALLBACK (gtk_filter_list_model_refilter), 
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]