summaryrefslogtreecommitdiffstats
path: root/src/nautilus-list-base.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/nautilus-list-base.c')
-rw-r--r--src/nautilus-list-base.c1892
1 files changed, 1892 insertions, 0 deletions
diff --git a/src/nautilus-list-base.c b/src/nautilus-list-base.c
new file mode 100644
index 0000000..6b8d2f6
--- /dev/null
+++ b/src/nautilus-list-base.c
@@ -0,0 +1,1892 @@
+/*
+ * 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 <gdk/x11/gdkx.h>
+#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<GFile>. */
+ 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 <Tab> 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;
+}