/* * Copyright (C) 2022 The GNOME project contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ #include "nautilus-list-base-private.h" #include "nautilus-clipboard.h" #include "nautilus-dnd.h" #include "nautilus-view-cell.h" #include "nautilus-view-item.h" #include "nautilus-view-model.h" #include "nautilus-files-view.h" #include "nautilus-files-view-dnd.h" #include "nautilus-file.h" #include "nautilus-file-operations.h" #include "nautilus-metadata.h" #include "nautilus-global-preferences.h" #include "nautilus-thumbnails.h" #ifdef GDK_WINDOWING_X11 #include #endif /** * NautilusListBase: * * Abstract class containing shared code for #NautilusFilesView implementations * using a #GtkListBase-derived widget (e.g. GtkGridView, GtkColumnView) which * takes a #NautilusViewModel instance as its model and and a #NautilusViewCell * instance as #GtkListItem:child. * * It has been has been created to avoid code duplication in implementations, * while keeping #NautilusFilesView implementation-agnostic (should the need for * non-#GtkListBase views arise). */ typedef struct _NautilusListBasePrivate NautilusListBasePrivate; struct _NautilusListBasePrivate { NautilusViewModel *model; GList *cut_files; guint scroll_to_file_handle_id; guint prioritize_thumbnailing_handle_id; GtkAdjustment *vadjustment; gboolean single_click_mode; gboolean activate_on_release; gboolean deny_background_click; GdkDragAction drag_item_action; GdkDragAction drag_view_action; graphene_point_t hover_start_point; guint hover_timer_id; GtkDropTarget *view_drop_target; }; G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (NautilusListBase, nautilus_list_base, NAUTILUS_TYPE_FILES_VIEW) typedef struct { const NautilusFileSortType sort_type; const gchar *metadata_name; } SortConstants; static const SortConstants sorts_constants[] = { { NAUTILUS_FILE_SORT_BY_DISPLAY_NAME, "name", }, { NAUTILUS_FILE_SORT_BY_SIZE, "size", }, { NAUTILUS_FILE_SORT_BY_TYPE, "type", }, { NAUTILUS_FILE_SORT_BY_MTIME, "date_modified", }, { NAUTILUS_FILE_SORT_BY_ATIME, "date_accessed", }, { NAUTILUS_FILE_SORT_BY_BTIME, "date_created", }, { NAUTILUS_FILE_SORT_BY_TRASHED_TIME, "trashed_on", }, { NAUTILUS_FILE_SORT_BY_SEARCH_RELEVANCE, "search_relevance", }, { NAUTILUS_FILE_SORT_BY_RECENCY, "recency", }, }; static inline NautilusViewItem * get_view_item (GListModel *model, guint position) { return NAUTILUS_VIEW_ITEM (g_list_model_get_item (model, position)); } static const SortConstants * get_sorts_constants_from_sort_type (NautilusFileSortType sort_type) { guint i; for (i = 0; i < G_N_ELEMENTS (sorts_constants); i++) { if (sort_type == sorts_constants[i].sort_type) { return &sorts_constants[i]; } } return &sorts_constants[0]; } static const SortConstants * get_sorts_constants_from_metadata_text (const char *metadata_name) { guint i; for (i = 0; i < G_N_ELEMENTS (sorts_constants); i++) { if (g_strcmp0 (sorts_constants[i].metadata_name, metadata_name) == 0) { return &sorts_constants[i]; } } return &sorts_constants[0]; } const NautilusFileSortType get_sorts_type_from_metadata_text (const char *metadata_name) { return get_sorts_constants_from_metadata_text (metadata_name)->sort_type; } static const SortConstants * get_default_sort_order (NautilusFile *file, gboolean *reversed) { NautilusFileSortType sort_type; sort_type = nautilus_file_get_default_sort_type (file, reversed); return get_sorts_constants_from_sort_type (sort_type); } static const SortConstants * get_directory_sort_by (NautilusFile *file, gboolean *reversed) { const SortConstants *default_sort; g_autofree char *sort_by = NULL; default_sort = get_default_sort_order (file, reversed); g_return_val_if_fail (default_sort != NULL, NULL); if (default_sort->sort_type == NAUTILUS_FILE_SORT_BY_RECENCY || default_sort->sort_type == NAUTILUS_FILE_SORT_BY_TRASHED_TIME || default_sort->sort_type == NAUTILUS_FILE_SORT_BY_SEARCH_RELEVANCE) { /* These defaults are important. Ignore metadata. */ return default_sort; } sort_by = nautilus_file_get_metadata (file, NAUTILUS_METADATA_KEY_ICON_VIEW_SORT_BY, default_sort->metadata_name); *reversed = nautilus_file_get_boolean_metadata (file, NAUTILUS_METADATA_KEY_ICON_VIEW_SORT_REVERSED, *reversed); return get_sorts_constants_from_metadata_text (sort_by); } void set_directory_sort_metadata (NautilusFile *file, const gchar *metadata_name, gboolean reversed) { const SortConstants *default_sort; gboolean default_reversed; default_sort = get_default_sort_order (file, &default_reversed); nautilus_file_set_metadata (file, NAUTILUS_METADATA_KEY_ICON_VIEW_SORT_BY, default_sort->metadata_name, metadata_name); nautilus_file_set_boolean_metadata (file, NAUTILUS_METADATA_KEY_ICON_VIEW_SORT_REVERSED, default_reversed, reversed); } static void update_sort_order_from_metadata_and_preferences (NautilusListBase *self) { const SortConstants *default_directory_sort; GActionGroup *view_action_group; gboolean reversed; default_directory_sort = get_directory_sort_by (nautilus_files_view_get_directory_as_file (NAUTILUS_FILES_VIEW (self)), &reversed); view_action_group = nautilus_files_view_get_action_group (NAUTILUS_FILES_VIEW (self)); g_action_group_change_action_state (view_action_group, "sort", g_variant_new ("(sb)", default_directory_sort->metadata_name, reversed)); } void nautilus_list_base_set_icon_size (NautilusListBase *self, gint icon_size) { GListModel *model; guint n_items; model = G_LIST_MODEL (nautilus_list_base_get_model (self)); n_items = g_list_model_get_n_items (model); for (guint i = 0; i < n_items; i++) { g_autoptr (NautilusViewItem) current_item = NULL; current_item = get_view_item (model, i); nautilus_view_item_set_icon_size (current_item, icon_size); } } static void set_focus_item (NautilusListBase *self, NautilusViewItem *item) { NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); GtkWidget *item_widget = nautilus_view_item_get_item_ui (item); GtkWidget *parent; if (item_widget == NULL) { /* We can't set the focus if the item isn't created yet. Return early to prevent a crash */ return; } parent = gtk_widget_get_parent (item_widget); if (!gtk_widget_grab_focus (parent)) { /* In GtkColumnView, the parent is a cell; its parent is the row. */ gtk_widget_grab_focus (gtk_widget_get_parent (parent)); } /* HACK: Grabbing focus is not enough for the listbase item tracker to * acknowledge it. So, poke the internal actions to fix the bug reported * in https://gitlab.gnome.org/GNOME/nautilus/-/issues/2294 */ gtk_widget_activate_action (item_widget, "list.select-item", "(ubb)", nautilus_view_model_get_index (priv->model, item), FALSE, FALSE); } static guint nautilus_list_base_get_icon_size (NautilusListBase *self) { return NAUTILUS_LIST_BASE_CLASS (G_OBJECT_GET_CLASS (self))->get_icon_size (self); } /* GtkListBase changes selection only with the primary button, and only after * release. But we need to anticipate selection earlier if we are to activate it * or open its context menu. This helper should be used in these situations if * it's desirable to act on a multi-item selection, because it preserves it. */ static void select_single_item_if_not_selected (NautilusListBase *self, NautilusViewItem *item) { NautilusViewModel *model; guint position; model = nautilus_list_base_get_model (self); position = nautilus_view_model_get_index (model, item); if (!gtk_selection_model_is_selected (GTK_SELECTION_MODEL (model), position)) { gtk_selection_model_select_item (GTK_SELECTION_MODEL (model), position, TRUE); set_focus_item (self, item); } } static void activate_selection_on_click (NautilusListBase *self, gboolean open_in_new_tab) { g_autolist (NautilusFile) selection = NULL; NautilusOpenFlags flags = 0; NautilusFilesView *files_view = NAUTILUS_FILES_VIEW (self); selection = nautilus_view_get_selection (NAUTILUS_VIEW (self)); if (open_in_new_tab) { flags |= NAUTILUS_OPEN_FLAG_NEW_TAB; flags |= NAUTILUS_OPEN_FLAG_DONT_MAKE_ACTIVE; } nautilus_files_view_activate_files (files_view, selection, flags, TRUE); } static void open_context_menu_on_press (NautilusListBase *self, NautilusViewCell *cell, gdouble x, gdouble y) { g_autoptr (NautilusViewItem) item = NULL; gdouble view_x, view_y; item = nautilus_view_cell_get_item (cell); g_return_if_fail (item != NULL); /* Antecipate selection, if necessary. */ select_single_item_if_not_selected (self, item); gtk_widget_translate_coordinates (GTK_WIDGET (cell), GTK_WIDGET (self), x, y, &view_x, &view_y); nautilus_files_view_pop_up_selection_context_menu (NAUTILUS_FILES_VIEW (self), view_x, view_y); } static void rubberband_set_state (NautilusListBase *self, gboolean enabled) { /* This is a temporary workaround to deal with the rubberbanding issues * during a drag and drop. Disable rubberband on item press and enable * rubberbnad on item release/stop. The permanent solution for this is * https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/4831 */ GtkWidget *view; view = NAUTILUS_LIST_BASE_CLASS (G_OBJECT_GET_CLASS (self))->get_view_ui (self); if (GTK_IS_GRID_VIEW (view)) { gtk_grid_view_set_enable_rubberband (GTK_GRID_VIEW (view), enabled); } else if (GTK_IS_COLUMN_VIEW (view)) { gtk_column_view_set_enable_rubberband (GTK_COLUMN_VIEW (view), enabled); } } static void on_item_click_pressed (GtkGestureClick *gesture, gint n_press, gdouble x, gdouble y, gpointer user_data) { NautilusViewCell *cell = user_data; g_autoptr (NautilusListBase) self = nautilus_view_cell_get_view (cell); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); guint button; GdkModifierType modifiers; gboolean selection_mode; button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture)); modifiers = gtk_event_controller_get_current_event_state (GTK_EVENT_CONTROLLER (gesture)); selection_mode = (modifiers & (GDK_CONTROL_MASK | GDK_SHIFT_MASK)); /* Before anything else, store event state to be read by other handlers. */ priv->deny_background_click = TRUE; priv->activate_on_release = (priv->single_click_mode && button == GDK_BUTTON_PRIMARY && n_press == 1 && !selection_mode); rubberband_set_state (self, FALSE); /* It's safe to claim event sequence on press in the following cases because * they don't interfere with touch scrolling. */ if (button == GDK_BUTTON_PRIMARY && n_press == 2 && !priv->single_click_mode) { /* If Ctrl + Shift are held, we don't want to activate selection. But * we still need to claim the event, otherwise GtkListBase's default * gesture is going to trigger activation. */ if (!selection_mode) { activate_selection_on_click (self, FALSE); } gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED); } else if (button == GDK_BUTTON_MIDDLE && n_press == 1) { g_autoptr (NautilusViewItem) item = nautilus_view_cell_get_item (cell); g_return_if_fail (item != NULL); /* Anticipate selection, if necessary, to activate it. */ select_single_item_if_not_selected (self, item); activate_selection_on_click (self, TRUE); gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED); } else if (button == GDK_BUTTON_SECONDARY && n_press == 1) { open_context_menu_on_press (self, cell, x, y); gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED); } } static void on_item_click_released (GtkGestureClick *gesture, gint n_press, gdouble x, gdouble y, gpointer user_data) { NautilusViewCell *cell = user_data; g_autoptr (NautilusListBase) self = nautilus_view_cell_get_view (cell); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); if (priv->activate_on_release) { NautilusViewModel *model; g_autoptr (NautilusViewItem) item = NULL; guint i; model = nautilus_list_base_get_model (self); item = nautilus_view_cell_get_item (cell); g_return_if_fail (item != NULL); i = nautilus_view_model_get_index (model, item); /* Anticipate selection, enforcing single selection of target item. */ gtk_selection_model_select_item (GTK_SELECTION_MODEL (model), i, TRUE); activate_selection_on_click (self, FALSE); } rubberband_set_state (self, TRUE); priv->activate_on_release = FALSE; priv->deny_background_click = FALSE; } static void on_item_click_stopped (GtkGestureClick *gesture, gpointer user_data) { NautilusViewCell *cell = user_data; g_autoptr (NautilusListBase) self = nautilus_view_cell_get_view (cell); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); rubberband_set_state (self, TRUE); priv->activate_on_release = FALSE; priv->deny_background_click = FALSE; } static void on_view_click_pressed (GtkGestureClick *gesture, gint n_press, gdouble x, gdouble y, gpointer user_data) { NautilusListBase *self = user_data; NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); guint button; GdkModifierType modifiers; gboolean selection_mode; if (priv->deny_background_click) { /* Item was clicked. */ gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED); return; } /* We are overriding many of the gestures for the views so let's make sure to * grab the focus in order to make rubberbanding and background click work */ gtk_widget_grab_focus (GTK_WIDGET (self)); /* Don't interfere with GtkListBase default selection handling when * holding Ctrl and Shift. */ modifiers = gtk_event_controller_get_current_event_state (GTK_EVENT_CONTROLLER (gesture)); selection_mode = (modifiers & (GDK_CONTROL_MASK | GDK_SHIFT_MASK)); if (!selection_mode) { nautilus_view_set_selection (NAUTILUS_VIEW (self), NULL); } button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture)); if (button == GDK_BUTTON_SECONDARY) { GtkWidget *event_widget; gdouble view_x; gdouble view_y; event_widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture)); gtk_widget_translate_coordinates (event_widget, GTK_WIDGET (self), x, y, &view_x, &view_y); nautilus_files_view_pop_up_background_context_menu (NAUTILUS_FILES_VIEW (self), view_x, view_y); } } static void on_item_longpress_pressed (GtkGestureLongPress *gesture, gdouble x, gdouble y, gpointer user_data) { NautilusViewCell *cell = user_data; g_autoptr (NautilusListBase) self = nautilus_view_cell_get_view (cell); open_context_menu_on_press (self, cell, x, y); gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED); } static void on_view_longpress_pressed (GtkGestureLongPress *gesture, gdouble x, gdouble y, gpointer user_data) { NautilusListBase *self = NAUTILUS_LIST_BASE (user_data); GtkWidget *event_widget; gdouble view_x; gdouble view_y; event_widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture)); gtk_widget_translate_coordinates (event_widget, GTK_WIDGET (self), x, y, &view_x, &view_y); nautilus_view_set_selection (NAUTILUS_VIEW (self), NULL); nautilus_files_view_pop_up_background_context_menu (NAUTILUS_FILES_VIEW (self), view_x, view_y); } static GdkContentProvider * on_item_drag_prepare (GtkDragSource *source, double x, double y, gpointer user_data) { NautilusViewCell *cell = user_data; g_autoptr (NautilusListBase) self = nautilus_view_cell_get_view (cell); GtkWidget *view_ui; g_autolist (NautilusFile) selection = NULL; g_autoslist (GFile) file_list = NULL; g_autoptr (GdkPaintable) paintable = NULL; g_autoptr (NautilusViewItem) item = nautilus_view_cell_get_item (cell); GdkDragAction actions; gint scale_factor; /* Anticipate selection, if necessary, for dragging the clicked item. */ select_single_item_if_not_selected (self, item); selection = nautilus_view_get_selection (NAUTILUS_VIEW (self)); g_return_val_if_fail (selection != NULL, NULL); gtk_gesture_set_state (GTK_GESTURE (source), GTK_EVENT_SEQUENCE_CLAIMED); actions = GDK_ACTION_ALL | GDK_ACTION_ASK; for (GList *l = selection; l != NULL; l = l->next) { /* Convert to GTK_TYPE_FILE_LIST, which is assumed to be a GSList. */ file_list = g_slist_prepend (file_list, nautilus_file_get_activation_location (l->data)); if (!nautilus_file_can_delete (l->data)) { actions &= ~GDK_ACTION_MOVE; } } file_list = g_slist_reverse (file_list); gtk_drag_source_set_actions (source, actions); scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (self)); paintable = get_paintable_for_drag_selection (selection, scale_factor); view_ui = NAUTILUS_LIST_BASE_CLASS (G_OBJECT_GET_CLASS (self))->get_view_ui (self); if (GTK_IS_GRID_VIEW (view_ui)) { x = x * NAUTILUS_DRAG_SURFACE_ICON_SIZE / nautilus_list_base_get_icon_size (self); y = y * NAUTILUS_DRAG_SURFACE_ICON_SIZE / nautilus_list_base_get_icon_size (self); } else { x = 0; y = 0; } gtk_drag_source_set_icon (source, paintable, x, y); return gdk_content_provider_new_typed (GDK_TYPE_FILE_LIST, file_list); } static gboolean hover_timer (gpointer user_data) { NautilusViewCell *cell = user_data; g_autoptr (NautilusListBase) self = nautilus_view_cell_get_view (cell); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); g_autoptr (NautilusViewItem) item = nautilus_view_cell_get_item (cell); g_autofree gchar *uri = NULL; priv->hover_timer_id = 0; if (priv->drag_item_action == 0) { /* If we aren't able to dropped don't change the location. This stops * drops onto themselves, and another unnecessary drops. */ return G_SOURCE_REMOVE; } uri = nautilus_file_get_uri (nautilus_view_item_get_file (item)); nautilus_files_view_handle_hover (NAUTILUS_FILES_VIEW (self), uri); return G_SOURCE_REMOVE; } static void on_item_drag_hover_enter (GtkDropControllerMotion *controller, gdouble x, gdouble y, gpointer user_data) { NautilusViewCell *cell = user_data; g_autoptr (NautilusListBase) self = nautilus_view_cell_get_view (cell); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); priv->hover_start_point.x = x; priv->hover_start_point.y = y; } static void on_item_drag_hover_leave (GtkDropControllerMotion *controller, gpointer user_data) { NautilusViewCell *cell = user_data; g_autoptr (NautilusListBase) self = nautilus_view_cell_get_view (cell); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); g_clear_handle_id (&priv->hover_timer_id, g_source_remove); } static void on_item_drag_hover_motion (GtkDropControllerMotion *controller, gdouble x, gdouble y, gpointer user_data) { NautilusViewCell *cell = user_data; g_autoptr (NautilusListBase) self = nautilus_view_cell_get_view (cell); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); graphene_point_t start = priv->hover_start_point; /* This condition doubles in two roles: * - If the timeout hasn't started yet, to ensure the pointer has entered * deep enough into the cell before starting the timeout to switch; * - If the timeout has already started, to reset it if the pointer is * moving a lot. * Both serve to prevent accidental triggering of switch-on-hover. */ if (gtk_drag_check_threshold (GTK_WIDGET (cell), start.x, start.y, x, y)) { g_clear_handle_id (&priv->hover_timer_id, g_source_remove); priv->hover_timer_id = g_timeout_add (HOVER_TIMEOUT, hover_timer, cell); priv->hover_start_point.x = x; priv->hover_start_point.y = y; } } static GdkDragAction get_preferred_action (NautilusFile *target_file, const GValue *value) { GdkDragAction action = 0; if (value == NULL) { action = nautilus_dnd_get_preferred_action (target_file, NULL); } else if (G_VALUE_HOLDS (value, GDK_TYPE_FILE_LIST)) { GSList *source_file_list = g_value_get_boxed (value); if (source_file_list != NULL) { action = nautilus_dnd_get_preferred_action (target_file, source_file_list->data); } else { action = nautilus_dnd_get_preferred_action (target_file, NULL); } } else if (G_VALUE_HOLDS (value, G_TYPE_STRING)) { action = GDK_ACTION_COPY; } return action; } static void real_perform_drop (NautilusListBase *self, const GValue *value, GdkDragAction action, GFile *target_location) { g_autofree gchar *target_uri = g_file_get_uri (target_location); if (!gdk_drag_action_is_unique (action)) { /* TODO: Implement */ } else if (G_VALUE_HOLDS (value, G_TYPE_STRING)) { nautilus_files_view_handle_text_drop (NAUTILUS_FILES_VIEW (self), g_value_get_string (value), target_uri, action); } else if (G_VALUE_HOLDS (value, GDK_TYPE_FILE_LIST)) { GSList *source_file_list = g_value_get_boxed (value); GList *source_uri_list = NULL; for (GSList *l = source_file_list; l != NULL; l = l->next) { source_uri_list = g_list_prepend (source_uri_list, g_file_get_uri (l->data)); } source_uri_list = g_list_reverse (source_uri_list); nautilus_files_view_drop_proxy_received_uris (NAUTILUS_FILES_VIEW (self), source_uri_list, target_uri, action); g_list_free_full (source_uri_list, g_free); } } static GdkDragAction on_item_drag_enter (GtkDropTarget *target, double x, double y, gpointer user_data) { NautilusViewCell *cell = user_data; g_autoptr (NautilusListBase) self = nautilus_view_cell_get_view (cell); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); g_autoptr (NautilusViewItem) item = NULL; const GValue *value; g_autoptr (NautilusFile) dest_file = NULL; /* Reset action cache. */ priv->drag_item_action = 0; item = nautilus_view_cell_get_item (cell); if (item == NULL) { gtk_drop_target_reject (target); return 0; } dest_file = nautilus_file_ref (nautilus_view_item_get_file (item)); if (!nautilus_file_is_archive (dest_file) && !nautilus_file_is_directory (dest_file)) { gtk_drop_target_reject (target); return 0; } value = gtk_drop_target_get_value (target); priv->drag_item_action = get_preferred_action (dest_file, value); if (priv->drag_item_action == 0) { gtk_drop_target_reject (target); return 0; } nautilus_view_item_set_drag_accept (item, TRUE); return priv->drag_item_action; } static void on_item_drag_value_notify (GObject *object, GParamSpec *pspec, gpointer user_data) { GtkDropTarget *target = GTK_DROP_TARGET (object); NautilusViewCell *cell = user_data; g_autoptr (NautilusListBase) self = nautilus_view_cell_get_view (cell); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); const GValue *value; g_autoptr (NautilusViewItem) item = NULL; value = gtk_drop_target_get_value (target); if (value == NULL) { return; } item = nautilus_view_cell_get_item (cell); g_return_if_fail (NAUTILUS_IS_VIEW_ITEM (item)); priv->drag_item_action = get_preferred_action (nautilus_view_item_get_file (item), value); } static GdkDragAction on_item_drag_motion (GtkDropTarget *target, double x, double y, gpointer user_data) { NautilusViewCell *cell = user_data; g_autoptr (NautilusListBase) self = nautilus_view_cell_get_view (cell); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); /* There's a bug in GtkDropTarget where motion overrides enter * so until we fix that let's just return the action that we already * received from enter*/ return priv->drag_item_action; } static void on_item_drag_leave (GtkDropTarget *dest, gpointer user_data) { NautilusViewCell *cell = user_data; g_autoptr (NautilusViewItem) item = nautilus_view_cell_get_item (cell); nautilus_view_item_set_drag_accept (item, FALSE); } static gboolean on_item_drop (GtkDropTarget *target, const GValue *value, double x, double y, gpointer user_data) { NautilusViewCell *cell = user_data; g_autoptr (NautilusListBase) self = nautilus_view_cell_get_view (cell); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); g_autoptr (NautilusViewItem) item = nautilus_view_cell_get_item (cell); GdkDragAction actions; GFile *target_location; actions = gdk_drop_get_actions (gtk_drop_target_get_current_drop (target)); target_location = nautilus_file_get_location (nautilus_view_item_get_file (item)); #ifdef GDK_WINDOWING_X11 if (GDK_IS_X11_DISPLAY (gtk_widget_get_display (GTK_WIDGET (self)))) { /* Temporary workaround until the below GTK MR (or equivalend fix) * is merged. Without this fix, the preferred action isn't set correctly. * https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/4982 */ GdkDrag *drag = gdk_drop_get_drag (gtk_drop_target_get_current_drop (target)); actions = drag != NULL ? gdk_drag_get_selected_action (drag) : GDK_ACTION_COPY; } #endif /* In x11 the leave signal isn't emitted on a drop so we need to clear the timeout */ g_clear_handle_id (&priv->hover_timer_id, g_source_remove); real_perform_drop (self, value, actions, target_location); return TRUE; } static GdkDragAction on_view_drag_enter (GtkDropTarget *target, double x, double y, gpointer user_data) { NautilusListBase *self = user_data; NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); NautilusFile *dest_file; const GValue *value; dest_file = nautilus_files_view_get_directory_as_file (NAUTILUS_FILES_VIEW (self)); value = gtk_drop_target_get_value (target); priv->drag_view_action = get_preferred_action (dest_file, value); if (priv->drag_view_action == 0) { /* Don't summarily reject because the view's location might change on * hover, so a DND action may become available. */ return 0; } return priv->drag_view_action; } static void on_view_drag_value_notify (GObject *object, GParamSpec *pspec, gpointer user_data) { GtkDropTarget *target = GTK_DROP_TARGET (object); NautilusListBase *self = user_data; NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); const GValue *value; NautilusFile *dest_file; value = gtk_drop_target_get_value (target); if (value == NULL) { return; } dest_file = nautilus_files_view_get_directory_as_file (NAUTILUS_FILES_VIEW (self)); priv->drag_view_action = get_preferred_action (dest_file, value); } static GdkDragAction on_view_drag_motion (GtkDropTarget *target, double x, double y, gpointer user_data) { NautilusListBase *self = user_data; NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); return priv->drag_view_action; } static gboolean on_view_drop (GtkDropTarget *target, const GValue *value, double x, double y, gpointer user_data) { NautilusListBase *self = user_data; NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); GdkDragAction actions; GFile *target_location; if (priv->drag_view_action == 0) { /* We didn't reject earlier because the view's location may change and, * as a result, a drop action might become available. */ return FALSE; } actions = gdk_drop_get_actions (gtk_drop_target_get_current_drop (target)); target_location = nautilus_view_get_location (NAUTILUS_VIEW (self)); #ifdef GDK_WINDOWING_X11 if (GDK_IS_X11_DISPLAY (gtk_widget_get_display (GTK_WIDGET (self)))) { /* Temporary workaround until the below GTK MR (or equivalend fix) * is merged. Without this fix, the preferred action isn't set correctly. * https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/4982 */ GdkDrag *drag = gdk_drop_get_drag (gtk_drop_target_get_current_drop (target)); actions = drag != NULL ? gdk_drag_get_selected_action (drag) : GDK_ACTION_COPY; } #endif real_perform_drop (self, value, actions, target_location); return TRUE; } void setup_cell_common (GtkListItem *listitem, NautilusViewCell *cell) { GtkEventController *controller; GtkDropTarget *drop_target; g_object_bind_property (listitem, "item", cell, "item", G_BINDING_SYNC_CREATE); gtk_list_item_set_child (listitem, GTK_WIDGET (cell)); controller = GTK_EVENT_CONTROLLER (gtk_gesture_click_new ()); gtk_widget_add_controller (GTK_WIDGET (cell), controller); gtk_event_controller_set_propagation_phase (controller, GTK_PHASE_BUBBLE); gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (controller), 0); g_signal_connect (controller, "pressed", G_CALLBACK (on_item_click_pressed), cell); g_signal_connect (controller, "stopped", G_CALLBACK (on_item_click_stopped), cell); g_signal_connect (controller, "released", G_CALLBACK (on_item_click_released), cell); controller = GTK_EVENT_CONTROLLER (gtk_gesture_long_press_new ()); gtk_widget_add_controller (GTK_WIDGET (cell), controller); gtk_event_controller_set_propagation_phase (controller, GTK_PHASE_BUBBLE); gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (controller), TRUE); g_signal_connect (controller, "pressed", G_CALLBACK (on_item_longpress_pressed), cell); controller = GTK_EVENT_CONTROLLER (gtk_drag_source_new ()); gtk_widget_add_controller (GTK_WIDGET (cell), controller); gtk_event_controller_set_propagation_phase (controller, GTK_PHASE_CAPTURE); g_signal_connect (controller, "prepare", G_CALLBACK (on_item_drag_prepare), cell); /* TODO: Implement GDK_ACTION_ASK */ drop_target = gtk_drop_target_new (G_TYPE_INVALID, GDK_ACTION_ALL); gtk_drop_target_set_preload (drop_target, TRUE); /* TODO: Implement GDK_TYPE_STRING */ gtk_drop_target_set_gtypes (drop_target, (GType[2]) { GDK_TYPE_FILE_LIST, G_TYPE_STRING }, 2); g_signal_connect (drop_target, "enter", G_CALLBACK (on_item_drag_enter), cell); g_signal_connect (drop_target, "notify::value", G_CALLBACK (on_item_drag_value_notify), cell); g_signal_connect (drop_target, "leave", G_CALLBACK (on_item_drag_leave), cell); g_signal_connect (drop_target, "motion", G_CALLBACK (on_item_drag_motion), cell); g_signal_connect (drop_target, "drop", G_CALLBACK (on_item_drop), cell); gtk_widget_add_controller (GTK_WIDGET (cell), GTK_EVENT_CONTROLLER (drop_target)); controller = gtk_drop_controller_motion_new (); gtk_widget_add_controller (GTK_WIDGET (cell), controller); g_signal_connect (controller, "enter", G_CALLBACK (on_item_drag_hover_enter), cell); g_signal_connect (controller, "leave", G_CALLBACK (on_item_drag_hover_leave), cell); g_signal_connect (controller, "motion", G_CALLBACK (on_item_drag_hover_motion), cell); } static void nautilus_list_base_scroll_to_item (NautilusListBase *self, guint position) { NAUTILUS_LIST_BASE_CLASS (G_OBJECT_GET_CLASS (self))->scroll_to_item (self, position); } static GtkWidget * nautilus_list_base_get_view_ui (NautilusListBase *self) { return NAUTILUS_LIST_BASE_CLASS (G_OBJECT_GET_CLASS (self))->get_view_ui (self); } typedef struct { NautilusListBase *self; GQuark attribute_q; } NautilusListBaseSortData; static void real_begin_loading (NautilusFilesView *files_view) { NautilusListBase *self = NAUTILUS_LIST_BASE (files_view); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); /* Temporary workaround */ rubberband_set_state (self, TRUE); /*TODO move this to the files view class begin_loading and hook up? */ /* TODO: This calls sort once, and update_context_menus calls update_actions * which calls the action again */ update_sort_order_from_metadata_and_preferences (NAUTILUS_LIST_BASE (files_view)); /* We could have changed to the trash directory or to searching, and then * we need to update the menus */ nautilus_files_view_update_context_menus (files_view); nautilus_files_view_update_toolbar_menus (files_view); /* When double clicking on an item this deny_background_click can persist * because the new view interrupts the gesture sequence, so lets reset it.*/ priv->deny_background_click = FALSE; /* When DnD is used to navigate between directories, the normal callbacks * are ignored. Update DnD variables here upon navigating to a directory*/ if (gtk_drop_target_get_current_drop (priv->view_drop_target) != NULL) { priv->drag_view_action = get_preferred_action (nautilus_files_view_get_directory_as_file (files_view), gtk_drop_target_get_value (priv->view_drop_target)); priv->drag_item_action = 0; } } static void real_clear (NautilusFilesView *files_view) { NautilusListBase *self = NAUTILUS_LIST_BASE (files_view); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); g_clear_handle_id (&priv->scroll_to_file_handle_id, g_source_remove); nautilus_view_model_remove_all_items (priv->model); } static void set_click_mode_from_settings (NautilusListBase *self) { NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); int click_policy; click_policy = g_settings_get_enum (nautilus_preferences, NAUTILUS_PREFERENCES_CLICK_POLICY); priv->single_click_mode = (click_policy == NAUTILUS_CLICK_POLICY_SINGLE); } static void real_click_policy_changed (NautilusFilesView *files_view) { set_click_mode_from_settings (NAUTILUS_LIST_BASE (files_view)); } static void real_file_changed (NautilusFilesView *files_view, NautilusFile *file, NautilusDirectory *directory) { NautilusListBase *self = NAUTILUS_LIST_BASE (files_view); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); NautilusViewItem *item; item = nautilus_view_model_get_item_from_file (priv->model, file); nautilus_view_item_file_changed (item); } static GList * real_get_selection (NautilusFilesView *files_view) { NautilusListBase *self = NAUTILUS_LIST_BASE (files_view); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); g_autoptr (GtkSelectionFilterModel) selection = NULL; guint n_selected; GList *selected_files = NULL; selection = gtk_selection_filter_model_new (GTK_SELECTION_MODEL (priv->model)); n_selected = g_list_model_get_n_items (G_LIST_MODEL (selection)); for (guint i = 0; i < n_selected; i++) { g_autoptr (NautilusViewItem) item = NULL; item = get_view_item (G_LIST_MODEL (selection), i); selected_files = g_list_prepend (selected_files, g_object_ref (nautilus_view_item_get_file (item))); } selected_files = g_list_reverse (selected_files); return selected_files; } static gboolean real_is_empty (NautilusFilesView *files_view) { NautilusListBase *self = NAUTILUS_LIST_BASE (files_view); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); return g_list_model_get_n_items (G_LIST_MODEL (priv->model)) == 0; } static void real_end_file_changes (NautilusFilesView *files_view) { } static void real_remove_file (NautilusFilesView *files_view, NautilusFile *file, NautilusDirectory *directory) { NautilusListBase *self = NAUTILUS_LIST_BASE (files_view); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); NautilusViewItem *item; item = nautilus_view_model_get_item_from_file (priv->model, file); if (item != NULL) { nautilus_view_model_remove_item (priv->model, item); nautilus_files_view_notify_selection_changed (files_view); } } static GQueue * convert_glist_to_queue (GList *list) { GList *l; GQueue *queue; queue = g_queue_new (); for (l = list; l != NULL; l = l->next) { g_queue_push_tail (queue, l->data); } return queue; } static GQueue * convert_files_to_items (NautilusListBase *self, GQueue *files) { GList *l; GQueue *models; models = g_queue_new (); for (l = g_queue_peek_head_link (files); l != NULL; l = l->next) { NautilusViewItem *item; item = nautilus_view_item_new (NAUTILUS_FILE (l->data), nautilus_list_base_get_icon_size (self)); g_queue_push_tail (models, item); } return models; } static void real_set_selection (NautilusFilesView *files_view, GList *selection) { NautilusListBase *self = NAUTILUS_LIST_BASE (files_view); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); g_autoptr (GQueue) selection_files = NULL; g_autoptr (GQueue) selection_items = NULL; g_autoptr (GtkBitset) update_set = NULL; g_autoptr (GtkBitset) new_selection_set = NULL; g_autoptr (GtkBitset) old_selection_set = NULL; old_selection_set = gtk_selection_model_get_selection (GTK_SELECTION_MODEL (priv->model)); /* We aren't allowed to modify the actual selection bitset */ update_set = gtk_bitset_copy (old_selection_set); new_selection_set = gtk_bitset_new_empty (); /* Convert file list into set of model indices */ selection_files = convert_glist_to_queue (selection); selection_items = nautilus_view_model_get_items_from_files (priv->model, selection_files); for (GList *l = g_queue_peek_head_link (selection_items); l != NULL; l = l->next) { gtk_bitset_add (new_selection_set, nautilus_view_model_get_index (priv->model, l->data)); } /* Set focus on the first selected row. */ if (!g_queue_is_empty (selection_items)) { NautilusViewItem *item = g_queue_peek_head (selection_items); set_focus_item (self, item); } gtk_bitset_union (update_set, new_selection_set); gtk_selection_model_set_selection (GTK_SELECTION_MODEL (priv->model), new_selection_set, update_set); } static void real_select_all (NautilusFilesView *files_view) { NautilusListBase *self = NAUTILUS_LIST_BASE (files_view); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); gtk_selection_model_select_all (GTK_SELECTION_MODEL (priv->model)); } static void real_invert_selection (NautilusFilesView *files_view) { NautilusListBase *self = NAUTILUS_LIST_BASE (files_view); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); GtkSelectionModel *selection_model = GTK_SELECTION_MODEL (priv->model); g_autoptr (GtkBitset) selected = NULL; g_autoptr (GtkBitset) all = NULL; g_autoptr (GtkBitset) new_selected = NULL; selected = gtk_selection_model_get_selection (selection_model); /* We are going to flip the selection state of every item in the model. */ all = gtk_bitset_new_range (0, g_list_model_get_n_items (G_LIST_MODEL (priv->model))); /* The new selection is all items minus the ones currently selected. */ new_selected = gtk_bitset_copy (all); gtk_bitset_subtract (new_selected, selected); gtk_selection_model_set_selection (selection_model, new_selected, all); } static guint get_first_selected_item (NautilusListBase *self) { NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); g_autolist (NautilusFile) selection = NULL; NautilusFile *file; NautilusViewItem *item; selection = nautilus_view_get_selection (NAUTILUS_VIEW (self)); if (selection == NULL) { return G_MAXUINT; } file = NAUTILUS_FILE (selection->data); item = nautilus_view_model_get_item_from_file (priv->model, file); return nautilus_view_model_get_index (priv->model, item); } static void real_reveal_selection (NautilusFilesView *files_view) { NautilusListBase *self = NAUTILUS_LIST_BASE (files_view); nautilus_list_base_scroll_to_item (self, get_first_selected_item (self)); } static void on_clipboard_contents_received (GObject *source_object, GAsyncResult *res, gpointer user_data) { NautilusFilesView *files_view = NAUTILUS_FILES_VIEW (source_object); NautilusListBase *self = NAUTILUS_LIST_BASE (files_view); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); NautilusClipboard *clip; NautilusViewItem *item; for (GList *l = priv->cut_files; l != NULL; l = l->next) { item = nautilus_view_model_get_item_from_file (priv->model, l->data); if (item != NULL) { nautilus_view_item_set_cut (item, FALSE); } } g_clear_list (&priv->cut_files, g_object_unref); clip = nautilus_files_view_get_clipboard_finish (files_view, res, NULL); if (clip != NULL && nautilus_clipboard_is_cut (clip)) { priv->cut_files = g_list_copy_deep (nautilus_clipboard_peek_files (clip), (GCopyFunc) g_object_ref, NULL); } for (GList *l = priv->cut_files; l != NULL; l = l->next) { item = nautilus_view_model_get_item_from_file (priv->model, l->data); if (item != NULL) { nautilus_view_item_set_cut (item, TRUE); } } } static void update_clipboard_status (NautilusFilesView *view) { nautilus_files_view_get_clipboard_async (view, on_clipboard_contents_received, NULL); } static void on_clipboard_owner_changed (GdkClipboard *clipboard, gpointer user_data) { update_clipboard_status (NAUTILUS_FILES_VIEW (user_data)); } static void real_end_loading (NautilusFilesView *files_view, gboolean all_files_seen) { update_clipboard_status (files_view); } static guint get_first_visible_item (NautilusListBase *self) { NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); guint n_items; GtkWidget *view_ui; GtkBorder border = {0}; n_items = g_list_model_get_n_items (G_LIST_MODEL (priv->model)); view_ui = nautilus_list_base_get_view_ui (self); gtk_scrollable_get_border (GTK_SCROLLABLE (view_ui), &border); for (guint i = 0; i < n_items; i++) { g_autoptr (NautilusViewItem) item = NULL; GtkWidget *item_ui; item = get_view_item (G_LIST_MODEL (priv->model), i); item_ui = nautilus_view_item_get_item_ui (item); if (item_ui != NULL && gtk_widget_get_mapped (item_ui)) { GtkWidget *list_item_widget = gtk_widget_get_parent (item_ui); gdouble h = gtk_widget_get_allocated_height (list_item_widget); gdouble y; gtk_widget_translate_coordinates (list_item_widget, GTK_WIDGET (self), 0, h, NULL, &y); if (y >= border.top) { return i; } } } return G_MAXUINT; } static char * real_get_first_visible_file (NautilusFilesView *files_view) { NautilusListBase *self = NAUTILUS_LIST_BASE (files_view); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); guint i; g_autoptr (NautilusViewItem) item = NULL; gchar *uri = NULL; i = get_first_visible_item (self); if (i < G_MAXUINT) { item = get_view_item (G_LIST_MODEL (priv->model), i); uri = nautilus_file_get_uri (nautilus_view_item_get_file (item)); } return uri; } typedef struct { NautilusListBase *view; char *uri; } ScrollToFileData; static void scroll_to_file_data_free (ScrollToFileData *data) { g_free (data->uri); g_free (data); } static gboolean scroll_to_file_on_idle (ScrollToFileData *data) { NautilusListBase *self = data->view; NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); g_autoptr (NautilusFile) file = NULL; NautilusViewItem *item; guint i; priv->scroll_to_file_handle_id = 0; file = nautilus_file_get_existing_by_uri (data->uri); item = nautilus_view_model_get_item_from_file (priv->model, file); g_return_val_if_fail (item != NULL, G_SOURCE_REMOVE); i = nautilus_view_model_get_index (priv->model, item); nautilus_list_base_scroll_to_item (self, i); return G_SOURCE_REMOVE; } static void real_scroll_to_file (NautilusFilesView *files_view, const char *uri) { NautilusListBase *self = NAUTILUS_LIST_BASE (files_view); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); ScrollToFileData *data; guint handle_id; data = g_new (ScrollToFileData, 1); data->view = self; data->uri = g_strdup (uri); handle_id = g_idle_add_full (G_PRIORITY_LOW, (GSourceFunc) scroll_to_file_on_idle, data, (GDestroyNotify) scroll_to_file_data_free); priv->scroll_to_file_handle_id = handle_id; } static void real_add_files (NautilusFilesView *files_view, GList *files) { NautilusListBase *self = NAUTILUS_LIST_BASE (files_view); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); g_autoptr (GQueue) files_queue = NULL; g_autoptr (GQueue) items = NULL; files_queue = convert_glist_to_queue (files); items = convert_files_to_items (self, files_queue); nautilus_view_model_add_items (priv->model, items); } static void real_select_first (NautilusFilesView *files_view) { NautilusListBase *self = NAUTILUS_LIST_BASE (files_view); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); gtk_selection_model_select_item (GTK_SELECTION_MODEL (priv->model), 0, TRUE); } static GdkRectangle * get_rectangle_for_item_ui (NautilusListBase *self, GtkWidget *item_ui) { GdkRectangle *rectangle; GtkWidget *content_widget; gdouble view_x; gdouble view_y; rectangle = g_new0 (GdkRectangle, 1); gtk_widget_get_allocation (item_ui, rectangle); content_widget = nautilus_files_view_get_content_widget (NAUTILUS_FILES_VIEW (self)); gtk_widget_translate_coordinates (item_ui, content_widget, rectangle->x, rectangle->y, &view_x, &view_y); rectangle->x = view_x; rectangle->y = view_y; return rectangle; } static GdkRectangle * real_compute_rename_popover_pointing_to (NautilusFilesView *files_view) { NautilusListBase *self = NAUTILUS_LIST_BASE (files_view); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); g_autoptr (NautilusViewItem) item = NULL; GtkWidget *item_ui; /* We only allow one item to be renamed with a popover */ item = get_view_item (G_LIST_MODEL (priv->model), get_first_selected_item (self)); item_ui = nautilus_view_item_get_item_ui (item); g_return_val_if_fail (item_ui != NULL, NULL); return get_rectangle_for_item_ui (self, item_ui); } static GdkRectangle * real_reveal_for_selection_context_menu (NautilusFilesView *files_view) { NautilusListBase *self = NAUTILUS_LIST_BASE (files_view); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); g_autoptr (GtkSelectionFilterModel) selection = NULL; guint n_selected; GtkWidget *focus_child; guint i; GtkWidget *item_ui; selection = gtk_selection_filter_model_new (GTK_SELECTION_MODEL (priv->model)); n_selected = g_list_model_get_n_items (G_LIST_MODEL (selection)); g_return_val_if_fail (n_selected > 0, NULL); /* Get the focused item_ui, if selected. * Otherwise, get the selected item_ui which is sorted the lowest.*/ focus_child = gtk_widget_get_focus_child (nautilus_list_base_get_view_ui (self)); for (i = 0; i < n_selected; i++) { g_autoptr (NautilusViewItem) item = NULL; item = get_view_item (G_LIST_MODEL (selection), i); item_ui = nautilus_view_item_get_item_ui (item); if (item_ui != NULL && gtk_widget_get_parent (item_ui) == focus_child) { break; } } nautilus_list_base_scroll_to_item (self, i); return get_rectangle_for_item_ui (self, item_ui); } static void real_preview_selection_event (NautilusFilesView *files_view, GtkDirectionType direction) { NautilusListBase *self = NAUTILUS_LIST_BASE (files_view); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); guint i; gboolean rtl = (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL); i = get_first_selected_item (self); if (direction == GTK_DIR_UP || direction == (rtl ? GTK_DIR_RIGHT : GTK_DIR_LEFT)) { if (i == 0) { return; } i--; } else { i++; if (i >= g_list_model_get_n_items (G_LIST_MODEL (priv->model))) { return; } } gtk_selection_model_select_item (GTK_SELECTION_MODEL (priv->model), i, TRUE); set_focus_item (self, g_list_model_get_item (G_LIST_MODEL (priv->model), i)); } static void default_sort_order_changed_callback (NautilusListBase *self) { update_sort_order_from_metadata_and_preferences (self); } static void nautilus_list_base_dispose (GObject *object) { NautilusListBase *self = NAUTILUS_LIST_BASE (object); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); g_clear_handle_id (&priv->scroll_to_file_handle_id, g_source_remove); g_clear_handle_id (&priv->prioritize_thumbnailing_handle_id, g_source_remove); g_clear_handle_id (&priv->hover_timer_id, g_source_remove); G_OBJECT_CLASS (nautilus_list_base_parent_class)->dispose (object); } static void nautilus_list_base_finalize (GObject *object) { NautilusListBase *self = NAUTILUS_LIST_BASE (object); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); g_clear_list (&priv->cut_files, g_object_unref); G_OBJECT_CLASS (nautilus_list_base_parent_class)->finalize (object); } static gboolean prioritize_thumbnailing_on_idle (NautilusListBase *self) { NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); gdouble page_size; GtkWidget *first_visible_child; GtkWidget *next_child; guint first_index; guint next_index; gdouble y; guint last_index; g_autoptr (NautilusViewItem) first_item = NULL; NautilusFile *file; priv->prioritize_thumbnailing_handle_id = 0; page_size = gtk_adjustment_get_page_size (priv->vadjustment); first_index = get_first_visible_item (self); if (first_index == G_MAXUINT) { return G_SOURCE_REMOVE; } first_item = get_view_item (G_LIST_MODEL (priv->model), first_index); first_visible_child = nautilus_view_item_get_item_ui (first_item); for (next_index = first_index + 1; next_index < g_list_model_get_n_items (G_LIST_MODEL (priv->model)); next_index++) { g_autoptr (NautilusViewItem) next_item = NULL; next_item = get_view_item (G_LIST_MODEL (priv->model), next_index); next_child = nautilus_view_item_get_item_ui (next_item); if (next_child == NULL) { break; } if (gtk_widget_translate_coordinates (next_child, first_visible_child, 0, 0, NULL, &y)) { if (y > page_size) { break; } } } last_index = next_index - 1; /* Do the iteration in reverse to give higher priority to the top */ for (gint i = 0; i <= last_index - first_index; i++) { g_autoptr (NautilusViewItem) item = NULL; item = get_view_item (G_LIST_MODEL (priv->model), last_index - i); g_return_val_if_fail (item != NULL, G_SOURCE_REMOVE); file = nautilus_view_item_get_file (NAUTILUS_VIEW_ITEM (item)); if (file != NULL && nautilus_file_is_thumbnailing (file)) { g_autofree gchar *uri = nautilus_file_get_uri (file); nautilus_thumbnail_prioritize (uri); } } return G_SOURCE_REMOVE; } static void on_vadjustment_changed (GtkAdjustment *adjustment, gpointer user_data) { NautilusListBase *self = NAUTILUS_LIST_BASE (user_data); NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); guint handle_id; /* Schedule on idle to rate limit and to avoid delaying scrolling. */ if (priv->prioritize_thumbnailing_handle_id == 0) { handle_id = g_idle_add ((GSourceFunc) prioritize_thumbnailing_on_idle, self); priv->prioritize_thumbnailing_handle_id = handle_id; } } static gboolean nautilus_list_base_focus (GtkWidget *widget, GtkDirectionType direction) { NautilusListBase *self = NAUTILUS_LIST_BASE (widget); g_autolist (NautilusFile) selection = NULL; gboolean no_selection; gboolean handled; /* If focus is already inside the view, allow to immediately tab out of it, * instead of having to cycle through every item (potentially many). */ if (direction == GTK_DIR_TAB_FORWARD || direction == GTK_DIR_TAB_BACKWARD) { GtkWidget *focus_widget = gtk_root_get_focus (gtk_widget_get_root (widget)); if (focus_widget != NULL && gtk_widget_is_ancestor (focus_widget, widget)) { return FALSE; } } selection = nautilus_view_get_selection (NAUTILUS_VIEW (self)); no_selection = (selection == NULL); handled = GTK_WIDGET_CLASS (nautilus_list_base_parent_class)->focus (widget, direction); if (handled && no_selection) { GtkWidget *focus_widget = gtk_root_get_focus (gtk_widget_get_root (widget)); /* Workaround for https://gitlab.gnome.org/GNOME/nautilus/-/issues/2489 * Also ensures an item gets selected when using to focus the view. * Ideally to be fixed in GtkListBase instead. */ if (focus_widget != NULL) { gtk_widget_activate_action (focus_widget, "listitem.select", "(bb)", FALSE, FALSE); } } return handled; } static void nautilus_list_base_class_init (NautilusListBaseClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); NautilusFilesViewClass *files_view_class = NAUTILUS_FILES_VIEW_CLASS (klass); object_class->dispose = nautilus_list_base_dispose; object_class->finalize = nautilus_list_base_finalize; widget_class->focus = nautilus_list_base_focus; files_view_class->add_files = real_add_files; files_view_class->begin_loading = real_begin_loading; files_view_class->clear = real_clear; files_view_class->click_policy_changed = real_click_policy_changed; files_view_class->file_changed = real_file_changed; files_view_class->get_selection = real_get_selection; /* TODO: remove this get_selection_for_file_transfer, this doesn't even * take into account we could us the view for recursive search :/ * CanvasView has the same issue. */ files_view_class->get_selection_for_file_transfer = real_get_selection; files_view_class->is_empty = real_is_empty; files_view_class->remove_file = real_remove_file; files_view_class->select_all = real_select_all; files_view_class->set_selection = real_set_selection; files_view_class->invert_selection = real_invert_selection; files_view_class->end_file_changes = real_end_file_changes; files_view_class->end_loading = real_end_loading; files_view_class->get_first_visible_file = real_get_first_visible_file; files_view_class->reveal_selection = real_reveal_selection; files_view_class->scroll_to_file = real_scroll_to_file; files_view_class->select_first = real_select_first; files_view_class->compute_rename_popover_pointing_to = real_compute_rename_popover_pointing_to; files_view_class->reveal_for_selection_context_menu = real_reveal_for_selection_context_menu; files_view_class->preview_selection_event = real_preview_selection_event; } static void nautilus_list_base_init (NautilusListBase *self) { NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); GtkWidget *content_widget; GtkAdjustment *vadjustment; gtk_widget_add_css_class (GTK_WIDGET (self), "view"); g_signal_connect_object (nautilus_preferences, "changed::" NAUTILUS_PREFERENCES_DEFAULT_SORT_ORDER, G_CALLBACK (default_sort_order_changed_callback), self, G_CONNECT_SWAPPED); g_signal_connect_object (nautilus_preferences, "changed::" NAUTILUS_PREFERENCES_DEFAULT_SORT_IN_REVERSE_ORDER, G_CALLBACK (default_sort_order_changed_callback), self, G_CONNECT_SWAPPED); /* React to clipboard changes */ g_signal_connect_object (gdk_display_get_clipboard (gdk_display_get_default ()), "changed", G_CALLBACK (on_clipboard_owner_changed), self, 0); content_widget = nautilus_files_view_get_content_widget (NAUTILUS_FILES_VIEW (self)); vadjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (content_widget)); priv->vadjustment = vadjustment; g_signal_connect (vadjustment, "changed", (GCallback) on_vadjustment_changed, self); g_signal_connect (vadjustment, "value-changed", (GCallback) on_vadjustment_changed, self); priv->model = nautilus_view_model_new (); g_signal_connect_object (GTK_SELECTION_MODEL (priv->model), "selection-changed", G_CALLBACK (nautilus_files_view_notify_selection_changed), NAUTILUS_FILES_VIEW (self), G_CONNECT_SWAPPED); set_click_mode_from_settings (self); } NautilusViewModel * nautilus_list_base_get_model (NautilusListBase *self) { NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); return priv->model; } void nautilus_list_base_setup_gestures (NautilusListBase *self) { NautilusListBasePrivate *priv = nautilus_list_base_get_instance_private (self); GtkEventController *controller; GtkDropTarget *drop_target; controller = GTK_EVENT_CONTROLLER (gtk_gesture_click_new ()); gtk_widget_add_controller (GTK_WIDGET (self), controller); gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (controller), 0); g_signal_connect (controller, "pressed", G_CALLBACK (on_view_click_pressed), self); controller = GTK_EVENT_CONTROLLER (gtk_gesture_long_press_new ()); gtk_widget_add_controller (GTK_WIDGET (self), controller); gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (controller), TRUE); g_signal_connect (controller, "pressed", G_CALLBACK (on_view_longpress_pressed), self); /* TODO: Implement GDK_ACTION_ASK */ drop_target = gtk_drop_target_new (G_TYPE_INVALID, GDK_ACTION_ALL); gtk_drop_target_set_preload (drop_target, TRUE); /* TODO: Implement GDK_TYPE_STRING */ gtk_drop_target_set_gtypes (drop_target, (GType[2]) { GDK_TYPE_FILE_LIST, G_TYPE_STRING }, 2); g_signal_connect (drop_target, "enter", G_CALLBACK (on_view_drag_enter), self); g_signal_connect (drop_target, "notify::value", G_CALLBACK (on_view_drag_value_notify), self); g_signal_connect (drop_target, "motion", G_CALLBACK (on_view_drag_motion), self); g_signal_connect (drop_target, "drop", G_CALLBACK (on_view_drop), self); gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (drop_target)); priv->view_drop_target = drop_target; }