Files
gtk/tests/testlistview.c
Benjamin Otte 4ef2b241eb Add GtkTreeExpander
This is a container widget that takes over all the duties of tree
expanding and collapsing.
It has to be a container so it can capture keybindings while focus is
inside the listitem.

So far, this widget does not allow interacting with it, but it shows the
expander arrow in its correct state.

Also, testlistview uses this widget now instead of implementing
expanding itself.
2020-05-29 19:46:31 -04:00

665 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);
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);
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_box_append (GTK_BOX (box), data->name);
}
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_list_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_list_view_set_model (GTK_LIST_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;
}