gtk/tests/testlistview.c
Benjamin Otte 30f09ea10b listitem: Make this a GObject
This splits GtkListItem into 2 parts:

1. GtkListItem
   This is purely a GObject with public API for developers who want to
   populate lists. There is no chance to cause conflict with GtkWidget
   properties that the list implementation assumed control over and
   defines a clear boundary.
2. GtkListItemWidget
   The widget part of the listitem. This is not only fully in control of
   the list machinery, the machinery can also use different widget
   implementations for different list widgets like I inted to for
   GtkColumnView.
2020-05-30 19:26:46 -04:00

668 lines
18 KiB
C

#include <gtk/gtk.h>
#define FILE_INFO_TYPE_SELECTION (file_info_selection_get_type ())
G_DECLARE_FINAL_TYPE (FileInfoSelection, file_info_selection, FILE_INFO, SELECTION, GObject)
struct _FileInfoSelection
{
GObject parent_instance;
GListModel *model;
};
struct _FileInfoSelectionClass
{
GObjectClass parent_class;
};
static GType
file_info_selection_get_item_type (GListModel *list)
{
FileInfoSelection *self = FILE_INFO_SELECTION (list);
return g_list_model_get_item_type (self->model);
}
static guint
file_info_selection_get_n_items (GListModel *list)
{
FileInfoSelection *self = FILE_INFO_SELECTION (list);
return g_list_model_get_n_items (self->model);
}
static gpointer
file_info_selection_get_item (GListModel *list,
guint position)
{
FileInfoSelection *self = FILE_INFO_SELECTION (list);
return g_list_model_get_item (self->model, position);
}
static void
file_info_selection_list_model_init (GListModelInterface *iface)
{
iface->get_item_type = file_info_selection_get_item_type;
iface->get_n_items = file_info_selection_get_n_items;
iface->get_item = file_info_selection_get_item;
}
static gboolean
file_info_selection_is_selected (GtkSelectionModel *model,
guint position)
{
FileInfoSelection *self = FILE_INFO_SELECTION (model);
gpointer item;
item = g_list_model_get_item (self->model, position);
if (item == NULL)
return FALSE;
if (GTK_IS_TREE_LIST_ROW (item))
{
GtkTreeListRow *row = item;
item = gtk_tree_list_row_get_item (row);
g_object_unref (row);
}
return g_file_info_get_attribute_boolean (item, "filechooser::selected");
}
static void
file_info_selection_set_selected (FileInfoSelection *self,
guint position,
gboolean selected)
{
gpointer item;
item = g_list_model_get_item (self->model, position);
if (item == NULL)
return;
if (GTK_IS_TREE_LIST_ROW (item))
{
GtkTreeListRow *row = item;
item = gtk_tree_list_row_get_item (row);
g_object_unref (row);
}
g_file_info_set_attribute_boolean (item, "filechooser::selected", selected);
}
static gboolean
file_info_selection_select_item (GtkSelectionModel *model,
guint position,
gboolean exclusive)
{
FileInfoSelection *self = FILE_INFO_SELECTION (model);
if (exclusive)
{
guint i;
for (i = 0; i < g_list_model_get_n_items (self->model); i++)
file_info_selection_set_selected (self, i, i == position);
gtk_selection_model_selection_changed (model, 0, g_list_model_get_n_items (self->model));
}
else
{
file_info_selection_set_selected (self, position, TRUE);
gtk_selection_model_selection_changed (model, position, 1);
}
return TRUE;
}
static gboolean
file_info_selection_unselect_item (GtkSelectionModel *model,
guint position)
{
FileInfoSelection *self = FILE_INFO_SELECTION (model);
file_info_selection_set_selected (self, position, FALSE);
gtk_selection_model_selection_changed (model, position, 1);
return TRUE;
}
static gboolean
file_info_selection_select_range (GtkSelectionModel *model,
guint position,
guint n_items,
gboolean exclusive)
{
FileInfoSelection *self = FILE_INFO_SELECTION (model);
guint i;
if (exclusive)
for (i = 0; i < position; i++)
file_info_selection_set_selected (self, i, FALSE);
for (i = position; i < position + n_items; i++)
file_info_selection_set_selected (self, i, TRUE);
if (exclusive)
for (i = position + n_items; i < g_list_model_get_n_items (self->model); i++)
file_info_selection_set_selected (self, i, FALSE);
if (exclusive)
gtk_selection_model_selection_changed (model, 0, g_list_model_get_n_items (self->model));
else
gtk_selection_model_selection_changed (model, position, n_items);
return TRUE;
}
static gboolean
file_info_selection_unselect_range (GtkSelectionModel *model,
guint position,
guint n_items)
{
FileInfoSelection *self = FILE_INFO_SELECTION (model);
guint i;
for (i = position; i < position + n_items; i++)
file_info_selection_set_selected (self, i, FALSE);
gtk_selection_model_selection_changed (model, position, n_items);
return TRUE;
}
static void
file_info_selection_selection_model_init (GtkSelectionModelInterface *iface)
{
iface->is_selected = file_info_selection_is_selected;
iface->select_item = file_info_selection_select_item;
iface->unselect_item = file_info_selection_unselect_item;
iface->select_range = file_info_selection_select_range;
iface->unselect_range = file_info_selection_unselect_range;
}
G_DEFINE_TYPE_EXTENDED (FileInfoSelection, file_info_selection, G_TYPE_OBJECT, 0,
G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL,
file_info_selection_list_model_init)
G_IMPLEMENT_INTERFACE (GTK_TYPE_SELECTION_MODEL,
file_info_selection_selection_model_init))
static void
file_info_selection_items_changed_cb (GListModel *model,
guint position,
guint removed,
guint added,
FileInfoSelection *self)
{
g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added);
}
static void
file_info_selection_clear_model (FileInfoSelection *self)
{
if (self->model == NULL)
return;
g_signal_handlers_disconnect_by_func (self->model,
file_info_selection_items_changed_cb,
self);
g_clear_object (&self->model);
}
static void
file_info_selection_dispose (GObject *object)
{
FileInfoSelection *self = FILE_INFO_SELECTION (object);
file_info_selection_clear_model (self);
G_OBJECT_CLASS (file_info_selection_parent_class)->dispose (object);
}
static void
file_info_selection_class_init (FileInfoSelectionClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
gobject_class->dispose = file_info_selection_dispose;
}
static void
file_info_selection_init (FileInfoSelection *self)
{
}
static FileInfoSelection *
file_info_selection_new (GListModel *model)
{
FileInfoSelection *result;
result = g_object_new (FILE_INFO_TYPE_SELECTION, NULL);
result->model = g_object_ref (model);
g_signal_connect (result->model, "items-changed",
G_CALLBACK (file_info_selection_items_changed_cb), result);
return result;
}
/*** ---------------------- ***/
GSList *pending = NULL;
guint active = 0;
static void
loading_cb (GtkDirectoryList *dir,
GParamSpec *pspec,
gpointer unused)
{
if (gtk_directory_list_is_loading (dir))
{
active++;
/* HACK: ensure loading finishes and the dir doesn't get destroyed */
g_object_ref (dir);
}
else
{
active--;
g_object_unref (dir);
while (active < 20 && pending)
{
GtkDirectoryList *dir2 = pending->data;
pending = g_slist_remove (pending, dir2);
gtk_directory_list_set_file (dir2, g_object_get_data (G_OBJECT (dir2), "file"));
g_object_unref (dir2);
}
}
}
static GtkDirectoryList *
create_directory_list (GFile *file)
{
GtkDirectoryList *dir;
dir = gtk_directory_list_new (G_FILE_ATTRIBUTE_STANDARD_TYPE
"," G_FILE_ATTRIBUTE_STANDARD_NAME
"," G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
NULL);
gtk_directory_list_set_io_priority (dir, G_PRIORITY_DEFAULT_IDLE);
g_signal_connect (dir, "notify::loading", G_CALLBACK (loading_cb), NULL);
g_assert (!gtk_directory_list_is_loading (dir));
if (active > 20)
{
g_object_set_data_full (G_OBJECT (dir), "file", g_object_ref (file), g_object_unref);
pending = g_slist_prepend (pending, g_object_ref (dir));
}
else
{
gtk_directory_list_set_file (dir, file);
}
return dir;
}
static char *
get_file_path (GFileInfo *info)
{
GFile *file;
file = G_FILE (g_file_info_get_attribute_object (info, "standard::file"));
return g_file_get_path (file);
}
static GListModel *
create_list_model_for_directory (gpointer file)
{
GtkSortListModel *sort;
GtkDirectoryList *dir;
GtkSorter *sorter;
if (g_file_query_file_type (file, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL) != G_FILE_TYPE_DIRECTORY)
return NULL;
dir = create_directory_list (file);
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 (dir), sorter);
g_object_unref (sorter);
g_object_unref (dir);
return G_LIST_MODEL (sort);
}
typedef struct _RowData RowData;
struct _RowData
{
GtkWidget *expander;
GtkWidget *icon;
GtkWidget *name;
GCancellable *cancellable;
GtkTreeListRow *current_item;
};
static void row_data_notify_item (GtkListItem *item,
GParamSpec *pspec,
RowData *data);
static void
row_data_unbind (RowData *data)
{
if (data->current_item == NULL)
return;
if (data->cancellable)
{
g_cancellable_cancel (data->cancellable);
g_clear_object (&data->cancellable);
}
g_clear_object (&data->current_item);
}
static void
row_data_update_info (RowData *data,
GFileInfo *info)
{
GIcon *icon;
const char *thumbnail_path;
thumbnail_path = g_file_info_get_attribute_byte_string (info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH);
if (thumbnail_path)
{
/* XXX: not async */
GFile *thumbnail_file = g_file_new_for_path (thumbnail_path);
icon = g_file_icon_new (thumbnail_file);
g_object_unref (thumbnail_file);
}
else
{
icon = g_file_info_get_icon (info);
}
gtk_widget_set_visible (data->icon, icon != NULL);
gtk_image_set_from_gicon (GTK_IMAGE (data->icon), icon);
}
static void
copy_attribute (GFileInfo *to,
GFileInfo *from,
const gchar *attribute)
{
GFileAttributeType type;
gpointer value;
if (g_file_info_get_attribute_data (from, attribute, &type, &value, NULL))
g_file_info_set_attribute (to, attribute, type, value);
}
static void
row_data_got_thumbnail_info_cb (GObject *source,
GAsyncResult *res,
gpointer _data)
{
RowData *data = _data; /* invalid if operation was cancelled */
GFile *file = G_FILE (source);
GFileInfo *queried, *info;
queried = g_file_query_info_finish (file, res, NULL);
if (queried == NULL)
return;
/* now we know row is valid */
info = gtk_tree_list_row_get_item (data->current_item);
copy_attribute (info, queried, G_FILE_ATTRIBUTE_THUMBNAIL_PATH);
copy_attribute (info, queried, G_FILE_ATTRIBUTE_THUMBNAILING_FAILED);
copy_attribute (info, queried, G_FILE_ATTRIBUTE_STANDARD_ICON);
g_object_unref (queried);
row_data_update_info (data, info);
g_clear_object (&data->cancellable);
}
static void
row_data_bind (RowData *data,
GtkTreeListRow *item)
{
GFileInfo *info;
row_data_unbind (data);
if (item == NULL)
return;
data->current_item = g_object_ref (item);
gtk_tree_expander_set_list_row (GTK_TREE_EXPANDER (data->expander), item);
info = gtk_tree_list_row_get_item (item);
if (!g_file_info_has_attribute (info, "filechooser::queried"))
{
data->cancellable = g_cancellable_new ();
g_file_info_set_attribute_boolean (info, "filechooser::queried", TRUE);
g_file_query_info_async (G_FILE (g_file_info_get_attribute_object (info, "standard::file")),
G_FILE_ATTRIBUTE_THUMBNAIL_PATH ","
G_FILE_ATTRIBUTE_THUMBNAILING_FAILED ","
G_FILE_ATTRIBUTE_STANDARD_ICON,
G_FILE_QUERY_INFO_NONE,
G_PRIORITY_DEFAULT,
data->cancellable,
row_data_got_thumbnail_info_cb,
data);
}
row_data_update_info (data, info);
gtk_label_set_label (GTK_LABEL (data->name), g_file_info_get_display_name (info));
g_object_unref (info);
}
static void
row_data_notify_item (GtkListItem *item,
GParamSpec *pspec,
RowData *data)
{
row_data_bind (data, gtk_list_item_get_item (item));
}
static void
row_data_free (gpointer _data)
{
RowData *data = _data;
row_data_unbind (data);
g_slice_free (RowData, data);
}
static void
setup_widget (GtkListItem *list_item,
gpointer unused)
{
GtkWidget *box, *child;
RowData *data;
data = g_slice_new0 (RowData);
box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4);
gtk_list_item_set_child (list_item, box);
child = gtk_label_new (NULL);
gtk_label_set_width_chars (GTK_LABEL (child), 5);
gtk_box_append (GTK_BOX (box), child);
data->expander = gtk_tree_expander_new ();
gtk_box_append (GTK_BOX (box), data->expander);
box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4);
gtk_tree_expander_set_child (GTK_TREE_EXPANDER (data->expander), box);
data->icon = gtk_image_new ();
gtk_box_append (GTK_BOX (box), data->icon);
data->name = gtk_label_new (NULL);
gtk_label_set_max_width_chars (GTK_LABEL (data->name), 25);
gtk_label_set_ellipsize (GTK_LABEL (data->name), PANGO_ELLIPSIZE_END);
gtk_box_append (GTK_BOX (box), data->name);
g_signal_connect (list_item, "notify::item", G_CALLBACK (row_data_notify_item), data);
g_object_set_data_full (G_OBJECT (list_item), "row-data", data, row_data_free);
}
static GListModel *
create_list_model_for_file_info (gpointer file_info,
gpointer unused)
{
GFile *file = G_FILE (g_file_info_get_attribute_object (file_info, "standard::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;
}
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_FILE (g_file_info_get_attribute_object (info, "standard::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;
FileInfoSelection *selectionmodel;
GFile *root;
GListModel *toplevels;
gtk_init ();
win = gtk_window_new ();
gtk_window_set_default_size (GTK_WINDOW (win), 400, 600);
vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
gtk_window_set_child (GTK_WINDOW (win), vbox);
search_entry = gtk_search_entry_new ();
gtk_box_append (GTK_BOX (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_box_append (GTK_BOX (vbox), sw);
listview = gtk_grid_view_new_with_factory (
gtk_functions_list_item_factory_new (setup_widget,
NULL,
NULL, NULL));
gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (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);
selectionmodel = file_info_selection_new (G_LIST_MODEL (filter));
g_object_unref (filter);
gtk_grid_view_set_model (GTK_GRID_VIEW (listview), G_LIST_MODEL (selectionmodel));
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_box_append (GTK_BOX (vbox), statusbar);
g_object_unref (tree);
g_object_unref (selectionmodel);
gtk_widget_show (win);
toplevels = gtk_window_get_toplevels ();
while (g_list_model_get_n_items (toplevels))
g_main_context_iteration (NULL, TRUE);
return 0;
}