/*
* Copyright © 2023 Benjamin Otte
*
* This library 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 of the License, or (at your option) any later version.
*
* This library 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 library. If not, see .
*/
#include
#include
#include "gtk/gtklistitemmanagerprivate.h"
#include "gtk/gtklistbaseprivate.h"
static GListModel *
create_source_model (guint min_size, guint max_size)
{
GtkStringList *list;
guint i, size;
size = g_test_rand_int_range (min_size, max_size + 1);
list = gtk_string_list_new (NULL);
for (i = 0; i < size; i++)
gtk_string_list_append (list, g_test_rand_bit () ? "A" : "B");
return G_LIST_MODEL (list);
}
void
print_list_item_manager_tiles (GtkListItemManager *items)
{
GString *string;
GtkListTile *tile;
string = g_string_new ("");
for (tile = gtk_list_item_manager_get_first (items);
tile != NULL;
tile = gtk_rb_tree_node_get_next (tile))
{
switch (tile->type)
{
case GTK_LIST_TILE_ITEM:
if (tile->widget)
g_string_append_c (string, 'W');
else if (tile->n_items == 1)
g_string_append_c (string, 'x');
else
g_string_append_printf (string, "%u,", tile->n_items);
break;
case GTK_LIST_TILE_HEADER:
g_string_append_c (string, '[');
break;
case GTK_LIST_TILE_UNMATCHED_HEADER:
g_string_append_c (string, '(');
break;
case GTK_LIST_TILE_FOOTER:
g_string_append_c (string, ']');
break;
case GTK_LIST_TILE_UNMATCHED_FOOTER:
g_string_append_c (string, ')');
break;
case GTK_LIST_TILE_REMOVED:
g_string_append_c (string, '.');
break;
default:
g_assert_not_reached ();
break;
}
}
g_print ("%s\n", string->str);
g_string_free (string, TRUE);
}
static gsize
widget_count_children (GtkWidget *widget)
{
GtkWidget *child;
gsize n_children = 0;
for (child = gtk_widget_get_first_child (widget);
child;
child = gtk_widget_get_next_sibling (child))
n_children++;
return n_children;
}
static void
check_list_item_manager (GtkListItemManager *items,
GtkWidget *widget,
GtkListItemTracker **trackers,
gsize n_trackers)
{
GListModel *model = G_LIST_MODEL (gtk_list_item_manager_get_model (items));
GtkListTile *tile;
guint n_items, n_tile_widgets;
guint i;
gboolean has_sections;
enum {
NO_SECTION,
MATCHED_SECTION,
UNMATCHED_SECTION
} section_state = NO_SECTION;
gboolean after_items = FALSE;
has_sections = gtk_list_item_manager_get_has_sections (items);
n_items = 0;
n_tile_widgets = 0;
for (tile = gtk_list_item_manager_get_first (items);
tile != NULL;
tile = gtk_rb_tree_node_get_next (tile))
{
switch (tile->type)
{
case GTK_LIST_TILE_HEADER:
g_assert_cmpint (section_state, ==, NO_SECTION);
g_assert_cmpint (tile->n_items, ==, 0);
g_assert_true (has_sections);
g_assert_true (tile->widget);
section_state = MATCHED_SECTION;
after_items = FALSE;
break;
case GTK_LIST_TILE_UNMATCHED_HEADER:
g_assert_cmpint (section_state, ==, NO_SECTION);
g_assert_cmpint (tile->n_items, ==, 0);
g_assert_null (tile->widget);
section_state = UNMATCHED_SECTION;
after_items = FALSE;
break;
case GTK_LIST_TILE_FOOTER:
g_assert_cmpint (section_state, ==, MATCHED_SECTION);
g_assert_cmpint (tile->n_items, ==, 0);
g_assert_true (has_sections);
g_assert_null (tile->widget);
section_state = NO_SECTION;
after_items = FALSE;
break;
case GTK_LIST_TILE_UNMATCHED_FOOTER:
g_assert_cmpint (section_state, ==, UNMATCHED_SECTION);
g_assert_cmpint (tile->n_items, ==, 0);
g_assert_null (tile->widget);
section_state = NO_SECTION;
after_items = FALSE;
break;
case GTK_LIST_TILE_ITEM:
g_assert_cmpint (section_state, !=, NO_SECTION);
if (tile->widget)
{
GObject *item = g_list_model_get_item (model, n_items);
if (has_sections)
g_assert_cmpint (section_state, ==, MATCHED_SECTION);
else
g_assert_cmpint (section_state, ==, UNMATCHED_SECTION);
g_assert_cmphex (GPOINTER_TO_SIZE (item), ==, GPOINTER_TO_SIZE (gtk_list_item_base_get_item (GTK_LIST_ITEM_BASE (tile->widget))));
g_object_unref (item);
g_assert_cmpint (n_items, ==, gtk_list_item_base_get_position (GTK_LIST_ITEM_BASE (tile->widget)));
g_assert_cmpint (tile->n_items, ==, 1);
after_items = FALSE;
}
else
{
after_items = TRUE;
}
if (tile->n_items)
n_items += tile->n_items;
break;
case GTK_LIST_TILE_REMOVED:
g_assert_cmpint (tile->n_items, ==, 0);
g_assert_null (tile->widget);
break;
default:
g_assert_not_reached ();
break;
}
if (tile->widget)
n_tile_widgets++;
}
g_assert_cmpint (section_state, ==, NO_SECTION);
g_assert_cmpint (n_items, ==, g_list_model_get_n_items (model));
g_assert_cmpint (n_tile_widgets, ==, widget_count_children (widget));
for (i = 0; i < n_trackers; i++)
{
guint pos, offset;
pos = gtk_list_item_tracker_get_position (items, trackers[i]);
if (pos == GTK_INVALID_LIST_POSITION)
continue;
tile = gtk_list_item_manager_get_nth (items, pos, &offset);
g_assert_cmpint (tile->n_items, ==, 1);
g_assert_cmpint (offset, ==, 0);
g_assert_true (tile->widget);
}
gtk_list_item_manager_gc_tiles (items);
n_items = 0;
n_tile_widgets = 0;
for (tile = gtk_list_item_manager_get_first (items);
tile != NULL;
tile = gtk_rb_tree_node_get_next (tile))
{
switch (tile->type)
{
case GTK_LIST_TILE_HEADER:
g_assert_cmpint (section_state, ==, NO_SECTION);
g_assert_cmpint (tile->n_items, ==, 0);
g_assert_true (has_sections);
g_assert_true (tile->widget);
section_state = MATCHED_SECTION;
after_items = FALSE;
break;
case GTK_LIST_TILE_UNMATCHED_HEADER:
g_assert_cmpint (section_state, ==, NO_SECTION);
g_assert_cmpint (tile->n_items, ==, 0);
g_assert_false (tile->widget);
section_state = UNMATCHED_SECTION;
after_items = FALSE;
break;
case GTK_LIST_TILE_FOOTER:
g_assert_cmpint (section_state, ==, MATCHED_SECTION);
g_assert_cmpint (tile->n_items, ==, 0);
g_assert_true (has_sections);
g_assert_false (tile->widget);
section_state = NO_SECTION;
after_items = FALSE;
break;
case GTK_LIST_TILE_UNMATCHED_FOOTER:
g_assert_cmpint (section_state, ==, UNMATCHED_SECTION);
g_assert_cmpint (tile->n_items, ==, 0);
g_assert_false (tile->widget);
section_state = NO_SECTION;
after_items = FALSE;
break;
case GTK_LIST_TILE_ITEM:
g_assert_cmpint (section_state, !=, NO_SECTION);
if (tile->widget)
{
g_assert_cmpint (tile->n_items, ==, 1);
after_items = FALSE;
}
else
{
g_assert_false (after_items);
after_items = TRUE;
}
if (tile->n_items)
n_items += tile->n_items;
break;
case GTK_LIST_TILE_REMOVED:
default:
g_assert_not_reached ();
break;
}
if (tile->widget)
n_tile_widgets++;
}
g_assert_cmpint (section_state, ==, NO_SECTION);
g_assert_cmpint (n_items, ==, g_list_model_get_n_items (model));
g_assert_cmpint (n_tile_widgets, ==, widget_count_children (widget));
for (i = 0; i < n_trackers; i++)
{
guint pos, offset;
pos = gtk_list_item_tracker_get_position (items, trackers[i]);
if (pos == GTK_INVALID_LIST_POSITION)
continue;
tile = gtk_list_item_manager_get_nth (items, pos, &offset);
g_assert_cmpint (tile->n_items, ==, 1);
g_assert_cmpint (offset, ==, 0);
g_assert_true (tile->widget);
}
}
static GtkListTile *
split_simple (GtkWidget *widget,
GtkListTile *tile,
guint n_items)
{
GtkListItemManager *items = g_object_get_data (G_OBJECT (widget), "the-items");
return gtk_list_tile_split (items, tile, n_items);
}
static void
prepare_simple (GtkWidget *widget,
GtkListTile *tile,
guint n_items)
{
}
static GtkListItemBase *
create_simple_item (GtkWidget *widget)
{
return g_object_new (GTK_TYPE_LIST_ITEM_BASE, NULL);
}
static GtkListHeaderBase *
create_simple_header (GtkWidget *widget)
{
return g_object_new (GTK_TYPE_LIST_HEADER_BASE, NULL);
}
static void
test_create (void)
{
GtkListItemManager *items;
GtkWidget *widget;
widget = gtk_window_new ();
items = gtk_list_item_manager_new (widget,
split_simple,
create_simple_item,
prepare_simple,
create_simple_header);
g_object_set_data_full (G_OBJECT (widget), "the-items", items, g_object_unref);
gtk_window_destroy (GTK_WINDOW (widget));
}
static void
test_create_with_items (void)
{
GListModel *source;
GtkNoSelection *selection;
GtkListItemManager *items;
GtkWidget *widget;
widget = gtk_window_new ();
items = gtk_list_item_manager_new (widget,
split_simple,
create_simple_item,
prepare_simple,
create_simple_header);
g_object_set_data_full (G_OBJECT (widget), "the-items", items, g_object_unref);
source = create_source_model (1, 50);
selection = gtk_no_selection_new (G_LIST_MODEL (source));
gtk_list_item_manager_set_model (items, GTK_SELECTION_MODEL (selection));
check_list_item_manager (items, widget, NULL, 0);
gtk_list_item_manager_set_model (items, GTK_SELECTION_MODEL (selection));
check_list_item_manager (items, widget, NULL, 0);
g_object_unref (selection);
gtk_window_destroy (GTK_WINDOW (widget));
}
#define N_TRACKERS 3
#define N_WIDGETS_PER_TRACKER 10
#define N_RUNS 500
static void
print_changes_cb (GListModel *model,
guint position,
guint removed,
guint added,
gpointer unused)
{
if (!g_test_verbose ())
return;
if (removed == 0)
g_test_message ("%u/%u: adding %u items", position, g_list_model_get_n_items (model), added);
else if (added == 0)
g_test_message ("%u/%u: removing %u items", position, g_list_model_get_n_items (model), removed);
else
g_test_message ("%u/%u: removing %u and adding %u items", position, g_list_model_get_n_items (model), removed, added);
}
static void
test_exhaustive (void)
{
GtkListItemTracker *trackers[N_TRACKERS];
GListStore *store;
GtkFlattenListModel *flatten;
GtkNoSelection *selection;
GtkListItemManager *items;
GtkWidget *widget;
gsize i;
widget = gtk_window_new ();
items = gtk_list_item_manager_new (widget,
split_simple,
create_simple_item,
prepare_simple,
create_simple_header);
for (i = 0; i < N_TRACKERS; i++)
trackers[i] = gtk_list_item_tracker_new (items);
g_object_set_data_full (G_OBJECT (widget), "the-items", items, g_object_unref);
store = g_list_store_new (G_TYPE_OBJECT);
flatten = gtk_flatten_list_model_new (G_LIST_MODEL (store));
selection = gtk_no_selection_new (G_LIST_MODEL (flatten));
g_signal_connect (selection, "items-changed", G_CALLBACK (print_changes_cb), NULL);
gtk_list_item_manager_set_model (items, GTK_SELECTION_MODEL (selection));
for (i = 0; i < N_RUNS; i++)
{
gboolean add = FALSE, remove = FALSE;
guint position, n_items;
switch (g_test_rand_int_range (0, 6))
{
case 0:
if (g_test_verbose ())
g_test_message ("GC and checking");
check_list_item_manager (items, widget, trackers, N_TRACKERS);
break;
case 1:
/* remove a model */
remove = TRUE;
break;
case 2:
/* add a model */
add = TRUE;
break;
case 3:
/* replace a model */
remove = TRUE;
add = TRUE;
break;
case 4:
n_items = g_list_model_get_n_items (G_LIST_MODEL (selection));
if (n_items > 0)
{
guint tracker_id = g_test_rand_int_range (0, N_TRACKERS);
guint pos = g_test_rand_int_range (0, n_items);
guint n_before = g_test_rand_int_range (0, N_WIDGETS_PER_TRACKER / 2);
guint n_after = g_test_rand_int_range (0, N_WIDGETS_PER_TRACKER / 2);
if (g_test_verbose ())
g_test_message ("setting tracker %u to %u -%u + %u", tracker_id, pos, n_before, n_after);
gtk_list_item_tracker_set_position (items,
trackers [tracker_id],
pos,
n_before, n_after);
}
break;
case 5:
{
gboolean has_sections = g_test_rand_bit ();
if (g_test_verbose ())
g_test_message ("Setting has_sections to %s", has_sections ? "true" : "false");
gtk_list_item_manager_set_has_sections (items, has_sections);
}
break;
default:
g_assert_not_reached ();
break;
}
position = g_test_rand_int_range (0, g_list_model_get_n_items (G_LIST_MODEL (store)) + 1);
if (g_list_model_get_n_items (G_LIST_MODEL (store)) == position)
remove = FALSE;
if (add)
{
/* We want at least one element, otherwise the filters will see no changes */
GListModel *source = create_source_model (1, 50);
g_list_store_splice (store,
position,
remove ? 1 : 0,
(gpointer *) &source, 1);
g_object_unref (source);
}
else if (remove)
{
g_list_store_remove (store, position);
}
if (g_test_verbose ())
print_list_item_manager_tiles (items);
}
check_list_item_manager (items, widget, trackers, N_TRACKERS);
if (g_test_verbose ())
g_test_message ("removing trackers");
for (i = 0; i < N_TRACKERS; i++)
gtk_list_item_tracker_free (items, trackers[i]);
g_object_unref (selection);
if (g_test_verbose ())
print_list_item_manager_tiles (items);
check_list_item_manager (items, widget, NULL, 0);
gtk_window_destroy (GTK_WINDOW (widget));
}
int
main (int argc, char *argv[])
{
gtk_test_init (&argc, &argv);
g_test_add_func ("/listitemmanager/create", test_create);
g_test_add_func ("/listitemmanager/create_with_items", test_create_with_items);
g_test_add_func ("/listitemmanager/exhaustive", test_exhaustive);
return g_test_run ();
}