/* * Copyright (C) 2000 Eazel, Inc. * Copyright (C) 2001, 2002 Anders Carlsson * Copyright (C) 2022 GNOME project contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ #include /* Needed for NautilusColumn. */ #include #include "nautilus-list-base-private.h" #include "nautilus-list-view.h" #include "nautilus-column-chooser.h" #include "nautilus-column-utilities.h" #include "nautilus-directory.h" #include "nautilus-file.h" #include "nautilus-file-utilities.h" #include "nautilus-global-preferences.h" #include "nautilus-label-cell.h" #include "nautilus-metadata.h" #include "nautilus-name-cell.h" #include "nautilus-scheme.h" #include "nautilus-search-directory.h" #include "nautilus-star-cell.h" #include "nautilus-tag-manager.h" #include "nautilus-view-item.h" #include "nautilus-view-model.h" /* We wait two seconds after row is collapsed to unload the subdirectory */ #define COLLAPSE_TO_UNLOAD_DELAY 2 struct _NautilusListView { NautilusListBase parent_instance; GtkColumnView *view_ui; NautilusSearchDirectory *search_directory; gint zoom_level; gboolean directories_first; gboolean expand_as_a_tree; GQuark path_attribute_q; GFile *file_path_base_location; GtkColumnViewColumn *star_column; GtkWidget *column_editor; GHashTable *factory_to_column_map; GtkSorter *view_model_sorter; }; G_DEFINE_FINAL_TYPE (NautilusListView, nautilus_list_view, NAUTILUS_TYPE_LIST_BASE) enum { LOAD_SUBDIRECTORY, UNLOAD_SUBDIRECTORY, LAST_SIGNAL }; static guint signals[LAST_SIGNAL]; #define get_view_item(cell) \ (NAUTILUS_VIEW_ITEM (gtk_tree_list_row_get_item (GTK_TREE_LIST_ROW (gtk_column_view_cell_get_item (cell))))) static const NautilusViewInfo list_view_info = { .view_id = NAUTILUS_VIEW_LIST_ID, .zoom_level_min = NAUTILUS_LIST_ZOOM_LEVEL_SMALL, .zoom_level_max = NAUTILUS_LIST_ZOOM_LEVEL_LARGE, .zoom_level_standard = NAUTILUS_LIST_ZOOM_LEVEL_MEDIUM, }; static NautilusViewInfo real_get_view_info (NautilusListBase *list_base) { return list_view_info; } static guint get_icon_size_for_zoom_level (NautilusListZoomLevel zoom_level) { switch (zoom_level) { case NAUTILUS_LIST_ZOOM_LEVEL_SMALL: { return NAUTILUS_LIST_ICON_SIZE_SMALL; } break; case NAUTILUS_LIST_ZOOM_LEVEL_MEDIUM: { return NAUTILUS_LIST_ICON_SIZE_MEDIUM; } break; case NAUTILUS_LIST_ZOOM_LEVEL_LARGE: { return NAUTILUS_LIST_ICON_SIZE_LARGE; } break; } g_return_val_if_reached (NAUTILUS_LIST_ICON_SIZE_MEDIUM); } static guint real_get_icon_size (NautilusListBase *list_base_view) { NautilusListView *self = NAUTILUS_LIST_VIEW (list_base_view); return get_icon_size_for_zoom_level (self->zoom_level); } static int real_get_zoom_level (NautilusListBase *list_base_view) { NautilusListView *self = NAUTILUS_LIST_VIEW (list_base_view); return self->zoom_level; } static void apply_columns_settings (NautilusListView *self, char **column_order, char **visible_columns) { g_autolist (NautilusColumn) all_columns = NULL; NautilusFile *file = nautilus_list_base_get_directory_as_file (NAUTILUS_LIST_BASE (self)); g_autoptr (GFile) location = NULL; g_autoptr (GList) view_columns = NULL; GListModel *old_view_columns; g_autoptr (GHashTable) visible_columns_hash = NULL; g_autoptr (GHashTable) old_view_columns_hash = NULL; int column_i = 0; if (self->search_directory != NULL) { location = nautilus_search_directory_get_search_location (self->search_directory); } if (location == NULL) { location = nautilus_file_get_location (file); } all_columns = nautilus_get_columns_for_file (file); all_columns = nautilus_sort_columns (all_columns, column_order); /* hash table to lookup if a given column should be visible */ visible_columns_hash = g_hash_table_new_full (g_str_hash, g_str_equal, (GDestroyNotify) g_free, (GDestroyNotify) g_free); /* always show name column */ g_hash_table_insert (visible_columns_hash, g_strdup ("name"), g_strdup ("name")); /* always show star column if supported */ if (nautilus_tag_manager_can_star_contents (nautilus_tag_manager_get (), location) || g_file_has_uri_scheme (location, SCHEME_STARRED)) { g_hash_table_insert (visible_columns_hash, g_strdup ("starred"), g_strdup ("starred")); } if (visible_columns != NULL) { for (int i = 0; visible_columns[i] != NULL; ++i) { g_hash_table_insert (visible_columns_hash, g_ascii_strdown (visible_columns[i], -1), g_ascii_strdown (visible_columns[i], -1)); } } old_view_columns_hash = g_hash_table_new_full (g_str_hash, g_str_equal, (GDestroyNotify) g_free, NULL); old_view_columns = gtk_column_view_get_columns (self->view_ui); for (guint i = 0; i < g_list_model_get_n_items (old_view_columns); i++) { g_autoptr (GtkColumnViewColumn) view_column = NULL; GtkListItemFactory *factory; NautilusColumn *nautilus_column; gchar *name; view_column = g_list_model_get_item (old_view_columns, i); factory = gtk_column_view_column_get_factory (view_column); nautilus_column = g_hash_table_lookup (self->factory_to_column_map, factory); g_object_get (nautilus_column, "name", &name, NULL); g_hash_table_insert (old_view_columns_hash, name, view_column); } for (GList *l = all_columns; l != NULL; l = l->next) { g_autofree char *name = NULL; g_autofree char *lowercase = NULL; g_object_get (G_OBJECT (l->data), "name", &name, NULL); lowercase = g_ascii_strdown (name, -1); if (g_hash_table_lookup (visible_columns_hash, lowercase) != NULL) { GtkColumnViewColumn *view_column; view_column = g_hash_table_lookup (old_view_columns_hash, name); if (view_column != NULL) { view_columns = g_list_prepend (view_columns, view_column); } } } view_columns = g_list_reverse (view_columns); /* hide columns that are not present in the configuration */ for (guint i = 0; i < g_list_model_get_n_items (old_view_columns); i++) { g_autoptr (GtkColumnViewColumn) view_column = NULL; view_column = g_list_model_get_item (old_view_columns, i); if (g_list_find (view_columns, view_column) == NULL) { gtk_column_view_column_set_visible (view_column, FALSE); } else { gtk_column_view_column_set_visible (view_column, TRUE); } } /* place columns in the correct order */ for (GList *l = view_columns; l != NULL; l = l->next, column_i++) { gtk_column_view_insert_column (self->view_ui, column_i, l->data); } } static void real_scroll_to (NautilusListBase *list_base_view, guint position, GtkListScrollFlags flags, GtkScrollInfo *scroll) { NautilusListView *self = NAUTILUS_LIST_VIEW (list_base_view); gtk_column_view_scroll_to (self->view_ui, position, NULL, flags, scroll); } static gint nautilus_list_view_sort (gconstpointer a, gconstpointer b, gpointer user_data) { GQuark attribute_q = GPOINTER_TO_UINT (user_data); NautilusFile *file_a = nautilus_view_item_get_file ((NautilusViewItem *) a); NautilusFile *file_b = nautilus_view_item_get_file ((NautilusViewItem *) b); /* The reversed argument is FALSE because the columnview sorter handles that * itself and if we don't want to reverse the reverse. The directories_first * argument is also FALSE for the same reason: we don't want the columnview * sorter to reverse it (it would display directories last!); instead we * handle directories_first in a separate sorter. */ return nautilus_file_compare_for_sort_by_attribute_q (file_a, file_b, attribute_q, FALSE /* directories_first */, FALSE /* reversed */); } static gint sort_directories_func (gconstpointer a, gconstpointer b, gpointer user_data) { gboolean *directories_first = user_data; if (*directories_first) { NautilusFile *file_a = nautilus_view_item_get_file ((NautilusViewItem *) a); NautilusFile *file_b = nautilus_view_item_get_file ((NautilusViewItem *) b); gboolean a_is_directory = nautilus_file_is_directory (file_a); gboolean b_is_directory = nautilus_file_is_directory (file_b); if (a_is_directory && !b_is_directory) { return GTK_ORDERING_SMALLER; } if (b_is_directory && !a_is_directory) { return GTK_ORDERING_LARGER; } } return GTK_ORDERING_EQUAL; } static void update_columns_settings_from_metadata_and_preferences (NautilusListView *self) { NautilusFile *file = nautilus_list_base_get_directory_as_file (NAUTILUS_LIST_BASE (self)); g_auto (GStrv) column_order = nautilus_column_get_column_order (file); g_auto (GStrv) visible_columns = nautilus_column_get_visible_columns (file); apply_columns_settings (self, column_order, visible_columns); } static GFile * get_base_location (NautilusListView *self) { GFile *base_location = NULL; if (self->search_directory != NULL) { g_autoptr (GFile) location = NULL; location = nautilus_search_directory_get_search_location (self->search_directory); if (location != NULL && !g_file_has_uri_scheme (location, SCHEME_RECENT) && !g_file_has_uri_scheme (location, SCHEME_STARRED) && !g_file_has_uri_scheme (location, SCHEME_TRASH)) { base_location = g_steal_pointer (&location); } } return base_location; } static void on_column_view_item_activated (GtkGridView *grid_view, guint position, gpointer user_data) { NautilusListView *self = NAUTILUS_LIST_VIEW (user_data); nautilus_list_base_activate_selection (NAUTILUS_LIST_BASE (self), FALSE); } static void setup_row (GtkSignalListItemFactory *factory, GtkColumnViewRow *columnviewrow, gpointer user_data) { GtkExpression *expression; /* Use file display name as accessible label. Explaining in pseudo-code: * columnviewrow:accessible-name :- columnviewrow:item:item:file:a11y-name */ expression = gtk_property_expression_new (GTK_TYPE_LIST_ITEM, NULL, "item"); expression = gtk_property_expression_new (GTK_TYPE_TREE_LIST_ROW, expression, "item"); expression = gtk_property_expression_new (NAUTILUS_TYPE_VIEW_ITEM, expression, "file"); expression = gtk_property_expression_new (NAUTILUS_TYPE_FILE, expression, "a11y-name"); gtk_expression_bind (expression, columnviewrow, "accessible-label", columnviewrow); } static GtkColumnView * create_view_ui (NautilusListView *self) { GtkWidget *widget; g_autoptr (GtkListItemFactory) row_factory = gtk_signal_list_item_factory_new (); widget = gtk_column_view_new (NULL); gtk_widget_set_hexpand (widget, TRUE); g_signal_connect (row_factory, "setup", G_CALLBACK (setup_row), self); /* We don't use the built-in child activation feature for click because it * doesn't fill all our needs nor does it match our expected behavior. * Instead, we roll our own event handling and double/single click mode. * However, GtkColumnView:single-click-activate has other effects besides * activation, as it affects the selection behavior as well (e.g. selects on * hover). Setting it to FALSE gives us the expected behavior. */ gtk_column_view_set_single_click_activate (GTK_COLUMN_VIEW (widget), FALSE); gtk_column_view_set_tab_behavior (GTK_COLUMN_VIEW (widget), GTK_LIST_TAB_ITEM); gtk_column_view_set_row_factory (GTK_COLUMN_VIEW (widget), row_factory); gtk_accessible_update_property (GTK_ACCESSIBLE (widget), GTK_ACCESSIBLE_PROPERTY_LABEL, _("Content View"), GTK_ACCESSIBLE_PROPERTY_ROLE_DESCRIPTION, _("View of the current location"), -1); /* While we don't want to use GTK's click activation, we'll let it handle * the key activation part (with Enter). */ g_signal_connect (widget, "activate", G_CALLBACK (on_column_view_item_activated), self); return GTK_COLUMN_VIEW (widget); } static GtkWidget * create_column_editor (NautilusListView *view) { GtkWidget *column_chooser; NautilusFile *file; file = nautilus_list_base_get_directory_as_file (NAUTILUS_LIST_BASE (view)); column_chooser = nautilus_column_chooser_new (file); g_signal_connect_swapped (column_chooser, "changed", G_CALLBACK (apply_columns_settings), view); return column_chooser; } void nautilus_list_view_present_column_editor (NautilusListView *self) { if (self->column_editor == NULL) { self->column_editor = create_column_editor (self); g_object_add_weak_pointer (G_OBJECT (self->column_editor), (gpointer *) &self->column_editor); } adw_dialog_present (ADW_DIALOG (self->column_editor), GTK_WIDGET (self)); } static void on_sorter_changed (GtkSorter *sorter, GtkSorterChange change, gpointer user_data) { NautilusListView *self = NAUTILUS_LIST_VIEW (user_data); /* Notify about changes not effected by nautilus_list_base_set_sort_state(), * such as when the user clicks the column headers. This is important to * update the selected item in the sort menu, assuming this property is * bound to the state of the "view.sort" action. */ g_object_notify (G_OBJECT (self), "sort-state"); } static GVariant * real_get_sort_state (NautilusListBase *list_base) { NautilusListView *self = NAUTILUS_LIST_VIEW (list_base); GtkColumnViewSorter *column_view_sorter = GTK_COLUMN_VIEW_SORTER (gtk_column_view_get_sorter (self->view_ui)); GtkColumnViewColumn *primary; const char *sort_text; gboolean reversed; primary = gtk_column_view_sorter_get_primary_sort_column (column_view_sorter); if (primary == NULL) { return NULL; } sort_text = gtk_column_view_column_get_id (primary); reversed = gtk_column_view_sorter_get_primary_sort_order (column_view_sorter); return g_variant_take_ref (g_variant_new ("(sb)", sort_text, reversed)); } static void real_set_enable_rubberband (NautilusListBase *list_base, gboolean enabled) { NautilusListView *self = NAUTILUS_LIST_VIEW (list_base); gtk_column_view_set_enable_rubberband (self->view_ui, enabled); } static void real_set_sort_state (NautilusListBase *list_base, GVariant *value) { NautilusListView *self = NAUTILUS_LIST_VIEW (list_base); GtkSorter *column_view_sorter = gtk_column_view_get_sorter (self->view_ui); const gchar *target_name; gboolean reversed; GListModel *view_columns; g_autoptr (GtkColumnViewColumn) sort_column = NULL; g_variant_get (value, "(&sb)", &target_name, &reversed); view_columns = gtk_column_view_get_columns (self->view_ui); for (guint i = 0; i < g_list_model_get_n_items (view_columns); i++) { g_autoptr (GtkColumnViewColumn) view_column = NULL; const char *attribute; view_column = g_list_model_get_item (view_columns, i); attribute = gtk_column_view_column_get_id (view_column); if (g_strcmp0 (target_name, attribute) == 0) { sort_column = g_steal_pointer (&view_column); break; } } g_signal_handlers_block_by_func (column_view_sorter, on_sorter_changed, self); /* Clear sorting before setting new sort column, to avoid double triangle, * as per https://gitlab.gnome.org/GNOME/gtk/-/issues/4696#note_1578945 */ gtk_column_view_sort_by_column (self->view_ui, NULL, FALSE); gtk_column_view_sort_by_column (self->view_ui, sort_column, reversed); g_signal_handlers_unblock_by_func (column_view_sorter, on_sorter_changed, self); } static void real_set_zoom_level (NautilusListBase *list_base, int new_level) { NautilusListView *self = NAUTILUS_LIST_VIEW (list_base); g_return_if_fail (new_level >= list_view_info.zoom_level_min && new_level <= list_view_info.zoom_level_max); self->zoom_level = new_level; if (g_settings_get_enum (nautilus_list_view_preferences, NAUTILUS_PREFERENCES_LIST_VIEW_DEFAULT_ZOOM_LEVEL) != new_level) { g_settings_set_enum (nautilus_list_view_preferences, NAUTILUS_PREFERENCES_LIST_VIEW_DEFAULT_ZOOM_LEVEL, new_level); } g_object_notify (G_OBJECT (self), "icon-size"); if (self->zoom_level == NAUTILUS_LIST_ZOOM_LEVEL_SMALL) { gtk_widget_add_css_class (GTK_WIDGET (self), "compact"); } else { gtk_widget_remove_css_class (GTK_WIDGET (self), "compact"); } } static void update_sort_directories_first (NautilusListView *self) { NautilusFile *directory_as_file = nautilus_list_base_get_directory_as_file (NAUTILUS_LIST_BASE (self)); NautilusViewModel *model = nautilus_list_base_get_model (NAUTILUS_LIST_BASE (self)); /* Always treat directories as normal items in search and recent. Recent * can accidentally contain directories when they were picked via file chooser. */ if (nautilus_file_is_in_search (directory_as_file) || nautilus_file_is_in_recent (directory_as_file)) { self->directories_first = FALSE; } else { self->directories_first = g_settings_get_boolean (gtk_filechooser_preferences, NAUTILUS_PREFERENCES_SORT_DIRECTORIES_FIRST); } if (model != NULL) { nautilus_view_model_sort (model); } } static void nautilus_list_view_setup_directory (NautilusListBase *list_base, NautilusDirectory *new_directory) { NautilusListView *self = NAUTILUS_LIST_VIEW (list_base); NautilusViewModel *model; NautilusFile *file; NAUTILUS_LIST_BASE_CLASS (nautilus_list_view_parent_class)->setup_directory (list_base, new_directory); g_clear_object (&self->search_directory); if (NAUTILUS_IS_SEARCH_DIRECTORY (new_directory)) { self->search_directory = g_object_ref (NAUTILUS_SEARCH_DIRECTORY (new_directory)); } update_columns_settings_from_metadata_and_preferences (self); self->expand_as_a_tree = g_settings_get_boolean (nautilus_list_view_preferences, NAUTILUS_PREFERENCES_LIST_VIEW_USE_TREE); self->path_attribute_q = 0; g_clear_object (&self->file_path_base_location); file = nautilus_list_base_get_directory_as_file (list_base); if (nautilus_file_is_in_trash (file)) { self->path_attribute_q = g_quark_from_string ("trash_orig_path"); self->file_path_base_location = get_base_location (self); } else if (nautilus_file_is_in_search (file) || nautilus_file_is_in_recent (file) || nautilus_file_is_in_starred (file)) { self->path_attribute_q = g_quark_from_string ("where"); self->file_path_base_location = get_base_location (self); /* Forcefully disabling tree in these special locations because this * view and its model currently don't expect the same file appearing * more than once. * * NautilusFilesView still has support for the same file being present * in multiple directories (struct FileAndDirectory), so, if someone * cares enough about expanding folders in these special locations: * TODO: Making the model items aware of their current model instead of * relying on `nautilus_file_get_parent()`. */ self->expand_as_a_tree = FALSE; } update_sort_directories_first (self); model = nautilus_list_base_get_model (NAUTILUS_LIST_BASE (self)); if (model != NULL) { nautilus_view_model_expand_as_a_tree (model, self->expand_as_a_tree); } } static gint get_default_zoom_level (void) { gint default_zoom_level = g_settings_get_enum (nautilus_list_view_preferences, NAUTILUS_PREFERENCES_LIST_VIEW_DEFAULT_ZOOM_LEVEL); /* Sanitize preference value. */ return CLAMP (default_zoom_level, list_view_info.zoom_level_min, list_view_info.zoom_level_max); } static NautilusViewItem * real_get_backing_item (NautilusListBase *list_base) { NautilusListView *self = NAUTILUS_LIST_VIEW (list_base); NautilusViewModel *model = nautilus_list_base_get_model (list_base); if (model == NULL || !self->expand_as_a_tree) { return NULL; } /* If we are using tree expanders use the items parent, unless it * is an expanded folder, in which case we should use that folder directly. * When dealing with multiple selections, use the same rules, but only * if a common parent exists. */ g_autoptr (GtkBitset) selection = gtk_selection_model_get_selection (GTK_SELECTION_MODEL (model)); g_autoptr (GtkTreeListRow) common_parent = NULL; GtkBitsetIter iter; guint i; for (gtk_bitset_iter_init_first (&iter, selection, &i); gtk_bitset_iter_is_valid (&iter); gtk_bitset_iter_next (&iter, &i)) { g_autoptr (GtkTreeListRow) row = g_list_model_get_item (G_LIST_MODEL (model), i); g_autoptr (GtkTreeListRow) parent = gtk_tree_list_row_get_parent (row); GtkTreeListRow *current_parent = gtk_tree_list_row_get_expanded (row) ? row : parent; if (current_parent == NULL) { return NULL; } if (common_parent == NULL) { common_parent = g_object_ref (current_parent); } else if (current_parent != common_parent) { g_clear_object (&common_parent); break; } } if (common_parent != NULL) { return gtk_tree_list_row_get_item (common_parent); } return NULL; } typedef struct { NautilusListView *self; GtkTreeListRow *row; } UnloadDelayData; static void unload_delay_data_free (UnloadDelayData *unload_data) { g_clear_weak_pointer (&unload_data->self); g_clear_object (&unload_data->row); g_free (unload_data); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (UnloadDelayData, unload_delay_data_free) static UnloadDelayData * unload_delay_data_new (NautilusListView *self, GtkTreeListRow *row) { UnloadDelayData *unload_data; unload_data = g_new0 (UnloadDelayData, 1); g_set_weak_pointer (&unload_data->self, self); g_set_object (&unload_data->row, row); return unload_data; } static void unload_file_timeout (gpointer data) { g_autoptr (UnloadDelayData) unload_data = data; NautilusListView *self = unload_data->self; if (unload_data->self == NULL) { return; } if (gtk_tree_list_row_get_expanded (unload_data->row)) { /* It has been expanded again before the timeout. Do nothing. */ return; } g_autoptr (NautilusViewItem) item = gtk_tree_list_row_get_item (unload_data->row); if (nautilus_file_is_gone (nautilus_view_item_get_file (item))) { /* It's been removed in the meantime. */ return; } g_signal_emit (self, signals[UNLOAD_SUBDIRECTORY], 0, item); } static void on_row_expanded_changed (GObject *gobject, GParamSpec *pspec, gpointer user_data) { GtkTreeListRow *row = GTK_TREE_LIST_ROW (gobject); NautilusListView *self = NAUTILUS_LIST_VIEW (user_data); g_autoptr (NautilusViewItem) item = NAUTILUS_VIEW_ITEM (gtk_tree_list_row_get_item (row)); if (gtk_tree_list_row_get_expanded (row)) { g_signal_emit (self, signals[LOAD_SUBDIRECTORY], 0, item); } else { nautilus_view_item_set_loading (item, FALSE); g_timeout_add_seconds_once (COLLAPSE_TO_UNLOAD_DELAY, unload_file_timeout, unload_delay_data_new (self, row)); } } static gboolean tree_expander_shortcut_cb (GtkWidget *widget, GVariant *args, gpointer user_data) { NautilusListView *self = NAUTILUS_LIST_VIEW (widget); GtkWidget *child; g_autoptr (NautilusViewItem) item = NULL; GtkTreeExpander *expander; GtkTreeListRow *row; GtkTextDirection direction = gtk_widget_get_direction (widget); char *action; guint keyval = GPOINTER_TO_INT (user_data); if (!self->expand_as_a_tree) { return FALSE; } /* Hack to find the focus item. */ child = gtk_root_get_focus (gtk_widget_get_root (widget)); while (child != NULL && !NAUTILUS_IS_VIEW_CELL (child)) { child = gtk_widget_get_first_child (child); } if (!NAUTILUS_IS_VIEW_CELL (child)) { return FALSE; } /* The name cell might not be the first column (the user may have changed * column order), but the expander is always on the name cell. */ item = nautilus_view_cell_get_item (NAUTILUS_VIEW_CELL (child)); expander = nautilus_name_cell_get_expander (NAUTILUS_NAME_CELL (nautilus_view_item_get_item_ui (item))); row = gtk_tree_expander_get_list_row (expander); /* End of hack. */ if ((keyval == GDK_KEY_Right && direction == GTK_TEXT_DIR_LTR) || (keyval == GDK_KEY_Left && direction == GTK_TEXT_DIR_RTL)) { action = "listitem.expand"; } else { action = "listitem.collapse"; } if (!gtk_tree_list_row_get_expanded (row) && g_strcmp0 (action, "listitem.collapse") == 0) { g_autoptr (GtkTreeListRow) parent = gtk_tree_list_row_get_parent (row); if (parent != NULL) { guint parent_position = gtk_tree_list_row_get_position (parent); g_return_val_if_fail (parent_position != GTK_INVALID_LIST_POSITION, FALSE); gtk_column_view_scroll_to (self->view_ui, parent_position, NULL, GTK_LIST_SCROLL_FOCUS | GTK_LIST_SCROLL_SELECT, NULL); } } else { gtk_widget_activate_action (GTK_WIDGET (expander), action, NULL); } return TRUE; } static void setup_name_cell (GtkSignalListItemFactory *factory, GtkColumnViewCell *listitem, gpointer user_data) { NautilusListView *self = NAUTILUS_LIST_VIEW (user_data); NautilusViewCell *cell = nautilus_name_cell_new (NAUTILUS_LIST_BASE (self)); gtk_column_view_cell_set_child (listitem, GTK_WIDGET (cell)); setup_cell_common (G_OBJECT (listitem), cell, nautilus_name_cell_get_content (NAUTILUS_NAME_CELL (cell))); g_object_bind_property (self, "icon-size", cell, "icon-size", G_BINDING_SYNC_CREATE); } static void on_n_items_notify (GObject *object, GParamSpec *pspec, gpointer user_data) { GListModel *model = G_LIST_MODEL (object); GtkTreeExpander *expander = GTK_TREE_EXPANDER (user_data); guint n_items = g_list_model_get_n_items (model); gtk_tree_expander_set_hide_expander (expander, n_items == 0); } static void on_row_children_changed (GObject *gobject, GParamSpec *pspec, gpointer user_data) { GtkTreeExpander *expander = user_data; GListModel *model = gtk_tree_list_row_get_children (GTK_TREE_LIST_ROW (gobject)); if (model == NULL) { return; } g_signal_connect_object (model, "notify::n-items", G_CALLBACK (on_n_items_notify), expander, G_CONNECT_DEFAULT); } static void bind_name_cell (GtkSignalListItemFactory *factory, GtkColumnViewCell *listitem, gpointer user_data) { GtkWidget *cell = gtk_column_view_cell_get_child (listitem); NautilusListView *self = user_data; g_autoptr (NautilusViewItem) item = get_view_item (listitem); GtkTreeExpander *expander = nautilus_name_cell_get_expander (NAUTILUS_NAME_CELL (cell)); nautilus_view_item_set_item_ui (item, gtk_column_view_cell_get_child (listitem)); nautilus_name_cell_set_path (NAUTILUS_NAME_CELL (cell), self->path_attribute_q, self->file_path_base_location); nautilus_name_cell_set_show_snippet (NAUTILUS_NAME_CELL (cell), self->search_directory != NULL); gtk_tree_expander_set_indent_for_icon (expander, self->expand_as_a_tree); if (self->expand_as_a_tree) { GtkTreeListRow *row = GTK_TREE_LIST_ROW (gtk_column_view_cell_get_item (listitem)); gtk_tree_expander_set_list_row (expander, row); g_signal_connect_object (row, "notify::expanded", G_CALLBACK (on_row_expanded_changed), self, G_CONNECT_DEFAULT); g_signal_connect_object (row, "notify::children", G_CALLBACK (on_row_children_changed), expander, G_CONNECT_DEFAULT); } else { gtk_tree_expander_set_list_row (expander, NULL); } } static void unbind_name_cell (GtkSignalListItemFactory *factory, GtkColumnViewCell *listitem, gpointer user_data) { NautilusListView *self = user_data; g_autoptr (NautilusViewItem) item = NULL; item = get_view_item (listitem); if (item == NULL) { /* The row is gone */ return; } g_return_if_fail (NAUTILUS_IS_VIEW_ITEM (item)); nautilus_view_item_set_item_ui (item, NULL); if (self->expand_as_a_tree) { g_signal_handlers_disconnect_by_func (gtk_column_view_cell_get_item (listitem), on_row_expanded_changed, self); g_signal_handlers_disconnect_by_func (gtk_column_view_cell_get_item (listitem), on_row_children_changed, self); } } static void setup_star_cell (GtkSignalListItemFactory *factory, GtkColumnViewCell *listitem, gpointer user_data) { NautilusViewCell *cell; cell = nautilus_star_cell_new (NAUTILUS_LIST_BASE (user_data)); gtk_column_view_cell_set_child (listitem, GTK_WIDGET (cell)); setup_cell_common (G_OBJECT (listitem), cell, GTK_WIDGET (cell)); } static void setup_label_cell (GtkSignalListItemFactory *factory, GtkColumnViewCell *listitem, gpointer user_data) { NautilusListView *self = user_data; NautilusColumn *nautilus_column; NautilusViewCell *cell; nautilus_column = g_hash_table_lookup (self->factory_to_column_map, factory); cell = nautilus_label_cell_new (NAUTILUS_LIST_BASE (user_data), nautilus_column); gtk_column_view_cell_set_child (listitem, GTK_WIDGET (cell)); setup_cell_common (G_OBJECT (listitem), cell, GTK_WIDGET (cell)); } static void setup_view_columns (NautilusListView *self) { GtkListItemFactory *factory; g_autolist (NautilusColumn) nautilus_columns = NULL; nautilus_columns = nautilus_get_all_columns (); self->factory_to_column_map = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_object_unref); for (GList *l = nautilus_columns; l != NULL; l = l->next) { NautilusColumn *nautilus_column = NAUTILUS_COLUMN (l->data); g_autofree gchar *name = NULL; g_autofree gchar *label = NULL; GQuark attribute_q = 0; GtkSortType sort_order; g_autoptr (GtkCustomSorter) sorter = NULL; g_autoptr (GtkColumnViewColumn) view_column = NULL; g_object_get (nautilus_column, "name", &name, "label", &label, "attribute_q", &attribute_q, "default-sort-order", &sort_order, NULL); sorter = gtk_custom_sorter_new (nautilus_list_view_sort, GUINT_TO_POINTER (attribute_q), NULL); factory = gtk_signal_list_item_factory_new (); view_column = gtk_column_view_column_new (NULL, factory); gtk_column_view_column_set_expand (view_column, FALSE); gtk_column_view_column_set_resizable (view_column, TRUE); gtk_column_view_column_set_title (view_column, label); gtk_column_view_column_set_sorter (view_column, GTK_SORTER (sorter)); if (strcmp (name, "name") == 0) { g_signal_connect (factory, "setup", G_CALLBACK (setup_name_cell), self); g_signal_connect (factory, "bind", G_CALLBACK (bind_name_cell), self); g_signal_connect (factory, "unbind", G_CALLBACK (unbind_name_cell), self); gtk_column_view_column_set_expand (view_column, TRUE); } else if (g_strcmp0 (name, "starred") == 0) { g_signal_connect (factory, "setup", G_CALLBACK (setup_star_cell), self); gtk_column_view_column_set_title (view_column, ""); gtk_column_view_column_set_resizable (view_column, FALSE); self->star_column = view_column; } else { g_signal_connect (factory, "setup", G_CALLBACK (setup_label_cell), self); } gtk_column_view_append_column (self->view_ui, view_column); gtk_column_view_column_set_id (view_column, name); g_hash_table_insert (self->factory_to_column_map, factory, g_object_ref (nautilus_column)); } } static void nautilus_list_view_reload (NautilusListView *self) { gtk_widget_activate_action (GTK_WIDGET (self), "slot.reload", NULL); } static void on_model_changed (NautilusListView *self) { NautilusViewModel *model = nautilus_list_base_get_model (NAUTILUS_LIST_BASE (self)); if (model != NULL) { g_autoptr (GtkMultiSorter) sorter = gtk_multi_sorter_new (); GtkSorter *column_view_sorter = gtk_column_view_get_sorter (self->view_ui); g_autoptr (GtkCustomSorter) directories_sorter = NULL; directories_sorter = gtk_custom_sorter_new (sort_directories_func, &self->directories_first, NULL); gtk_multi_sorter_append (sorter, g_object_ref (GTK_SORTER (directories_sorter))); gtk_multi_sorter_append (sorter, g_object_ref (column_view_sorter)); g_set_object (&self->view_model_sorter, GTK_SORTER (sorter)); nautilus_view_model_set_sorter (model, self->view_model_sorter); nautilus_view_model_expand_as_a_tree (model, self->expand_as_a_tree); gtk_column_view_set_enable_rubberband (GTK_COLUMN_VIEW (self->view_ui), !nautilus_view_model_get_single_selection (model)); } gtk_column_view_set_model (self->view_ui, GTK_SELECTION_MODEL (model)); } static void nautilus_list_view_init (NautilusListView *self) { GtkWidget *scrolled_window = nautilus_list_base_get_scrolled_window (NAUTILUS_LIST_BASE (self)); GtkSorter *column_view_sorter; GtkEventController *controller; GtkShortcut *shortcut; gtk_widget_add_css_class (GTK_WIDGET (self), "nautilus-list-view"); g_signal_connect_object (nautilus_list_view_preferences, "changed::" NAUTILUS_PREFERENCES_LIST_VIEW_DEFAULT_VISIBLE_COLUMNS, G_CALLBACK (update_columns_settings_from_metadata_and_preferences), self, G_CONNECT_SWAPPED); g_signal_connect_object (nautilus_list_view_preferences, "changed::" NAUTILUS_PREFERENCES_LIST_VIEW_DEFAULT_COLUMN_ORDER, G_CALLBACK (update_columns_settings_from_metadata_and_preferences), self, G_CONNECT_SWAPPED); g_signal_connect_object (nautilus_list_view_preferences, "changed::" NAUTILUS_PREFERENCES_LIST_VIEW_USE_TREE, G_CALLBACK (nautilus_list_view_reload), self, G_CONNECT_SWAPPED); self->view_ui = create_view_ui (self); nautilus_list_base_setup_gestures (NAUTILUS_LIST_BASE (self)); setup_view_columns (self); column_view_sorter = gtk_column_view_get_sorter (self->view_ui); g_signal_connect (column_view_sorter, "changed", G_CALLBACK (on_sorter_changed), self); g_signal_connect_swapped (self, "notify::model", G_CALLBACK (on_model_changed), self); gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scrolled_window), GTK_WIDGET (self->view_ui)); g_signal_connect_object (gtk_filechooser_preferences, "changed::" NAUTILUS_PREFERENCES_SORT_DIRECTORIES_FIRST, G_CALLBACK (update_sort_directories_first), self, G_CONNECT_SWAPPED); nautilus_list_base_set_zoom_level (NAUTILUS_LIST_BASE (self), get_default_zoom_level ()); /* Set up tree expand/collapse shortcuts in capture phase otherwise they * would be handled by GtkListBase's cursor movement shortcuts. */ controller = gtk_shortcut_controller_new (); gtk_widget_add_controller (GTK_WIDGET (self), controller); gtk_event_controller_set_propagation_phase (controller, GTK_PHASE_CAPTURE); shortcut = gtk_shortcut_new (gtk_keyval_trigger_new (GDK_KEY_Left, 0), gtk_callback_action_new (tree_expander_shortcut_cb, GINT_TO_POINTER (GDK_KEY_Left), NULL)); gtk_shortcut_controller_add_shortcut (GTK_SHORTCUT_CONTROLLER (controller), shortcut); shortcut = gtk_shortcut_new (gtk_keyval_trigger_new (GDK_KEY_Right, 0), gtk_callback_action_new (tree_expander_shortcut_cb, GINT_TO_POINTER (GDK_KEY_Right), NULL)); gtk_shortcut_controller_add_shortcut (GTK_SHORTCUT_CONTROLLER (controller), shortcut); } static void nautilus_list_view_dispose (GObject *object) { NautilusListView *self = NAUTILUS_LIST_VIEW (object); NautilusViewModel *model = nautilus_list_base_get_model (NAUTILUS_LIST_BASE (self)); if (model != NULL) { GtkSorter *sorter = nautilus_view_model_get_sorter (model); /* We should unset the sorter if it references the view. */ if (sorter == self->view_model_sorter) { nautilus_view_model_set_sorter (model, NULL); } } g_clear_object (&self->view_model_sorter); g_clear_object (&self->search_directory); g_signal_handlers_disconnect_by_func (nautilus_list_view_preferences, update_columns_settings_from_metadata_and_preferences, self); g_signal_handlers_disconnect_by_func (nautilus_list_view_preferences, nautilus_list_view_reload, self); g_clear_object (&self->file_path_base_location); g_clear_pointer (&self->factory_to_column_map, g_hash_table_destroy); g_signal_handlers_disconnect_by_func (gtk_column_view_get_sorter (self->view_ui), on_sorter_changed, self); G_OBJECT_CLASS (nautilus_list_view_parent_class)->dispose (object); } static void nautilus_list_view_finalize (GObject *object) { G_OBJECT_CLASS (nautilus_list_view_parent_class)->finalize (object); } static void nautilus_list_view_class_init (NautilusListViewClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); NautilusListBaseClass *list_base_view_class = NAUTILUS_LIST_BASE_CLASS (klass); object_class->dispose = nautilus_list_view_dispose; object_class->finalize = nautilus_list_view_finalize; list_base_view_class->get_backing_item = real_get_backing_item; list_base_view_class->get_icon_size = real_get_icon_size; list_base_view_class->get_sort_state = real_get_sort_state; list_base_view_class->get_view_info = real_get_view_info; list_base_view_class->get_zoom_level = real_get_zoom_level; list_base_view_class->scroll_to = real_scroll_to; list_base_view_class->set_enable_rubberband = real_set_enable_rubberband; list_base_view_class->set_sort_state = real_set_sort_state; list_base_view_class->set_zoom_level = real_set_zoom_level; list_base_view_class->setup_directory = nautilus_list_view_setup_directory; signals[LOAD_SUBDIRECTORY] = g_signal_new ("load-subdirectory", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, NAUTILUS_TYPE_VIEW_ITEM); signals[UNLOAD_SUBDIRECTORY] = g_signal_new ("unload-subdirectory", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, NAUTILUS_TYPE_VIEW_ITEM); } NautilusListView * nautilus_list_view_new (void) { return g_object_new (NAUTILUS_TYPE_LIST_VIEW, NULL); }