summaryrefslogtreecommitdiffstats
path: root/src/nautilus-grid-view.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/nautilus-grid-view.c')
-rw-r--r--src/nautilus-grid-view.c604
1 files changed, 604 insertions, 0 deletions
diff --git a/src/nautilus-grid-view.c b/src/nautilus-grid-view.c
new file mode 100644
index 0000000..8e38d7c
--- /dev/null
+++ b/src/nautilus-grid-view.c
@@ -0,0 +1,604 @@
+/*
+ * Copyright (C) 2022 The GNOME project contributors
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "nautilus-list-base-private.h"
+#include "nautilus-grid-view.h"
+
+#include "nautilus-grid-cell.h"
+#include "nautilus-global-preferences.h"
+
+struct _NautilusGridView
+{
+ NautilusListBase parent_instance;
+
+ GtkGridView *view_ui;
+
+ GActionGroup *action_group;
+ gint zoom_level;
+
+ gboolean directories_first;
+
+ GQuark caption_attributes[NAUTILUS_GRID_CELL_N_CAPTIONS];
+
+ NautilusFileSortType sort_type;
+ gboolean reversed;
+};
+
+G_DEFINE_TYPE (NautilusGridView, nautilus_grid_view, NAUTILUS_TYPE_LIST_BASE)
+
+static guint get_icon_size_for_zoom_level (NautilusGridZoomLevel zoom_level);
+
+static gint
+nautilus_grid_view_sort (gconstpointer a,
+ gconstpointer b,
+ gpointer user_data)
+{
+ NautilusGridView *self = user_data;
+ NautilusFile *file_a;
+ NautilusFile *file_b;
+
+ file_a = nautilus_view_item_get_file (NAUTILUS_VIEW_ITEM ((gpointer) a));
+ file_b = nautilus_view_item_get_file (NAUTILUS_VIEW_ITEM ((gpointer) b));
+
+ return nautilus_file_compare_for_sort (file_a, file_b,
+ self->sort_type,
+ self->directories_first,
+ self->reversed);
+}
+
+static void
+real_bump_zoom_level (NautilusFilesView *files_view,
+ int zoom_increment)
+{
+ NautilusGridView *self = NAUTILUS_GRID_VIEW (files_view);
+ NautilusGridZoomLevel new_level;
+
+ new_level = self->zoom_level + zoom_increment;
+
+ if (new_level >= NAUTILUS_GRID_ZOOM_LEVEL_SMALL &&
+ new_level <= NAUTILUS_GRID_ZOOM_LEVEL_EXTRA_LARGE)
+ {
+ g_action_group_change_action_state (self->action_group,
+ "zoom-to-level",
+ g_variant_new_int32 (new_level));
+ }
+}
+
+static guint
+get_icon_size_for_zoom_level (NautilusGridZoomLevel zoom_level)
+{
+ switch (zoom_level)
+ {
+ case NAUTILUS_GRID_ZOOM_LEVEL_SMALL:
+ {
+ return NAUTILUS_GRID_ICON_SIZE_SMALL;
+ }
+ break;
+
+ case NAUTILUS_GRID_ZOOM_LEVEL_SMALL_PLUS:
+ {
+ return NAUTILUS_GRID_ICON_SIZE_SMALL_PLUS;
+ }
+ break;
+
+ case NAUTILUS_GRID_ZOOM_LEVEL_MEDIUM:
+ {
+ return NAUTILUS_GRID_ICON_SIZE_MEDIUM;
+ }
+ break;
+
+ case NAUTILUS_GRID_ZOOM_LEVEL_LARGE:
+ {
+ return NAUTILUS_GRID_ICON_SIZE_LARGE;
+ }
+ break;
+
+ case NAUTILUS_GRID_ZOOM_LEVEL_EXTRA_LARGE:
+ {
+ return NAUTILUS_GRID_ICON_SIZE_EXTRA_LARGE;
+ }
+ break;
+ }
+ g_return_val_if_reached (NAUTILUS_GRID_ICON_SIZE_MEDIUM);
+}
+
+static gint
+get_default_zoom_level (void)
+{
+ NautilusGridZoomLevel default_zoom_level;
+
+ default_zoom_level = g_settings_get_enum (nautilus_icon_view_preferences,
+ NAUTILUS_PREFERENCES_ICON_VIEW_DEFAULT_ZOOM_LEVEL);
+
+ /* Sanitize preference value */
+ return CLAMP (default_zoom_level,
+ NAUTILUS_GRID_ZOOM_LEVEL_SMALL,
+ NAUTILUS_GRID_ZOOM_LEVEL_EXTRA_LARGE);
+}
+
+static void
+set_captions_from_preferences (NautilusGridView *self)
+{
+ g_auto (GStrv) value = NULL;
+ gint n_captions_for_zoom_level;
+
+ value = g_settings_get_strv (nautilus_icon_view_preferences,
+ NAUTILUS_PREFERENCES_ICON_VIEW_CAPTIONS);
+
+ /* Set a celling on the number of captions depending on the zoom level. */
+ n_captions_for_zoom_level = MIN (1 + self->zoom_level,
+ G_N_ELEMENTS (self->caption_attributes));
+
+ /* Reset array to zeros beforehand, as we may not refill all elements. */
+ memset (&self->caption_attributes, 0, sizeof (self->caption_attributes));
+ for (gint i = 0, quark_i = 0;
+ value[i] != NULL && quark_i < n_captions_for_zoom_level;
+ i++)
+ {
+ if (g_strcmp0 (value[i], "none") == 0)
+ {
+ continue;
+ }
+
+ /* Convert to quarks in advance, otherwise each NautilusFile attribute
+ * getter would call g_quark_from_string() once for each file. */
+ self->caption_attributes[quark_i] = g_quark_from_string (value[i]);
+ quark_i++;
+ }
+}
+
+static void
+set_zoom_level (NautilusGridView *self,
+ guint new_level)
+{
+ self->zoom_level = new_level;
+
+ /* The zoom level may change how many captions are allowed. Update it before
+ * setting the icon size, under the assumption that NautilusGridCell
+ * updates captions whenever the icon size is set*/
+ set_captions_from_preferences (self);
+
+ nautilus_list_base_set_icon_size (NAUTILUS_LIST_BASE (self),
+ get_icon_size_for_zoom_level (new_level));
+
+ nautilus_files_view_update_toolbar_menus (NAUTILUS_FILES_VIEW (self));
+}
+
+static void
+real_restore_standard_zoom_level (NautilusFilesView *files_view)
+{
+ NautilusGridView *self;
+
+ self = NAUTILUS_GRID_VIEW (files_view);
+ g_action_group_change_action_state (self->action_group,
+ "zoom-to-level",
+ g_variant_new_int32 (NAUTILUS_GRID_ZOOM_LEVEL_MEDIUM));
+}
+
+static gboolean
+real_is_zoom_level_default (NautilusFilesView *files_view)
+{
+ NautilusGridView *self;
+ guint icon_size;
+
+ self = NAUTILUS_GRID_VIEW (files_view);
+ icon_size = get_icon_size_for_zoom_level (self->zoom_level);
+
+ return icon_size == NAUTILUS_GRID_ICON_SIZE_MEDIUM;
+}
+
+static gboolean
+real_can_zoom_in (NautilusFilesView *files_view)
+{
+ NautilusGridView *self = NAUTILUS_GRID_VIEW (files_view);
+
+ return self->zoom_level < NAUTILUS_GRID_ZOOM_LEVEL_EXTRA_LARGE;
+}
+
+static gboolean
+real_can_zoom_out (NautilusFilesView *files_view)
+{
+ NautilusGridView *self = NAUTILUS_GRID_VIEW (files_view);
+
+ return self->zoom_level > NAUTILUS_GRID_ZOOM_LEVEL_SMALL;
+}
+
+/* The generic implementation in src/nautilus-list-base.c doesn't allow the
+ * 2-dimensional movements expected from a grid. Let's hack GTK here. */
+static void
+real_preview_selection_event (NautilusFilesView *files_view,
+ GtkDirectionType direction)
+{
+ NautilusGridView *self = NAUTILUS_GRID_VIEW (files_view);
+ guint direction_keyval;
+ g_autoptr (GtkShortcutTrigger) direction_trigger = NULL;
+ g_autoptr (GListModel) controllers = NULL;
+ gboolean success = FALSE;
+
+ /* We want the same behavior as when the user presses the arrow keys while
+ * the focus is in the view. So, let's get the matching arrow key. */
+ switch (direction)
+ {
+ case GTK_DIR_UP:
+ {
+ direction_keyval = GDK_KEY_Up;
+ }
+ break;
+
+ case GTK_DIR_DOWN:
+ {
+ direction_keyval = GDK_KEY_Down;
+ }
+ break;
+
+ case GTK_DIR_LEFT:
+ {
+ direction_keyval = GDK_KEY_Left;
+ }
+ break;
+
+ case GTK_DIR_RIGHT:
+ {
+ direction_keyval = GDK_KEY_Right;
+ }
+ break;
+
+ default:
+ {
+ g_return_if_reached ();
+ }
+ }
+
+ /* We cannot simulate a click, but we can find the shortcut it triggers and
+ * activate its action programatically.
+ *
+ * First, we create out would-be trigger.*/
+ direction_trigger = gtk_keyval_trigger_new (direction_keyval, 0);
+
+ /* Then we iterate over the shortcut installed in GtkGridView until we find
+ * a matching trigger. There may be multiple shortcut controllers, and each
+ * shortcut controller may hold multiple shortcuts each. Let's loop. */
+ controllers = gtk_widget_observe_controllers (GTK_WIDGET (self->view_ui));
+ for (guint i = 0; i < g_list_model_get_n_items (controllers); i++)
+ {
+ g_autoptr (GtkEventController) controller = g_list_model_get_item (controllers, i);
+
+ if (!GTK_IS_SHORTCUT_CONTROLLER (controller))
+ {
+ continue;
+ }
+
+ for (guint j = 0; j < g_list_model_get_n_items (G_LIST_MODEL (controller)); j++)
+ {
+ g_autoptr (GtkShortcut) shortcut = g_list_model_get_item (G_LIST_MODEL (controller), j);
+ GtkShortcutTrigger *trigger = gtk_shortcut_get_trigger (shortcut);
+
+ if (gtk_shortcut_trigger_equal (trigger, direction_trigger))
+ {
+ /* Match found. Activate the action to move cursor. */
+ success = gtk_shortcut_action_activate (gtk_shortcut_get_action (shortcut),
+ 0,
+ GTK_WIDGET (self->view_ui),
+ gtk_shortcut_get_arguments (shortcut));
+ break;
+ }
+ }
+ }
+
+ /* If the hack fails (GTK may change it's internal behavior), fallback. */
+ if (!success)
+ {
+ NAUTILUS_FILES_VIEW_CLASS (nautilus_grid_view_parent_class)->preview_selection_event (files_view, direction);
+ }
+}
+
+/* We only care about the keyboard activation part that GtkGridView provides,
+ * but we don't need any special filtering here. Indeed, we ask GtkGridView
+ * to not activate on single click, and we get to handle double clicks before
+ * GtkGridView does (as one of widget subclassing's goal is to modify the parent
+ * class's behavior), while claiming the click gestures, so it means GtkGridView
+ * will never react to a click event to emit this signal. So we should be pretty
+ * safe here with regards to our custom item click handling.
+ */
+static void
+on_grid_view_item_activated (GtkGridView *grid_view,
+ guint position,
+ gpointer user_data)
+{
+ NautilusGridView *self = NAUTILUS_GRID_VIEW (user_data);
+
+ nautilus_files_view_activate_selection (NAUTILUS_FILES_VIEW (self));
+}
+
+static guint
+real_get_icon_size (NautilusListBase *list_base_view)
+{
+ NautilusGridView *self = NAUTILUS_GRID_VIEW (list_base_view);
+
+ return get_icon_size_for_zoom_level (self->zoom_level);
+}
+
+static GtkWidget *
+real_get_view_ui (NautilusListBase *list_base_view)
+{
+ NautilusGridView *self = NAUTILUS_GRID_VIEW (list_base_view);
+
+ return GTK_WIDGET (self->view_ui);
+}
+
+static void
+real_scroll_to_item (NautilusListBase *list_base_view,
+ guint position)
+{
+ NautilusGridView *self = NAUTILUS_GRID_VIEW (list_base_view);
+
+ gtk_widget_activate_action (GTK_WIDGET (self->view_ui),
+ "list.scroll-to-item",
+ "u",
+ position);
+}
+
+static void
+real_sort_directories_first_changed (NautilusFilesView *files_view)
+{
+ NautilusGridView *self;
+ NautilusViewModel *model;
+ g_autoptr (GtkCustomSorter) sorter = NULL;
+
+ self = NAUTILUS_GRID_VIEW (files_view);
+ self->directories_first = nautilus_files_view_should_sort_directories_first (NAUTILUS_FILES_VIEW (self));
+
+ model = nautilus_list_base_get_model (NAUTILUS_LIST_BASE (self));
+ sorter = gtk_custom_sorter_new (nautilus_grid_view_sort, self, NULL);
+ nautilus_view_model_set_sorter (model, GTK_SORTER (sorter));
+}
+
+static void
+action_sort_order_changed (GSimpleAction *action,
+ GVariant *value,
+ gpointer user_data)
+{
+ const gchar *target_name;
+ NautilusGridView *self = NAUTILUS_GRID_VIEW (user_data);
+ NautilusViewModel *model;
+ g_autoptr (GtkCustomSorter) sorter = NULL;
+
+ /* Don't resort if the action is in the same state as before */
+ if (g_variant_equal (value, g_action_get_state (G_ACTION (action))))
+ {
+ return;
+ }
+
+ g_variant_get (value, "(&sb)", &target_name, &self->reversed);
+ self->sort_type = get_sorts_type_from_metadata_text (target_name);
+
+ sorter = gtk_custom_sorter_new (nautilus_grid_view_sort, self, NULL);
+ model = nautilus_list_base_get_model (NAUTILUS_LIST_BASE (self));
+ nautilus_view_model_set_sorter (model, GTK_SORTER (sorter));
+ set_directory_sort_metadata (nautilus_files_view_get_directory_as_file (NAUTILUS_FILES_VIEW (self)),
+ target_name,
+ self->reversed);
+
+ g_simple_action_set_state (action, value);
+}
+
+static guint
+real_get_view_id (NautilusFilesView *files_view)
+{
+ return NAUTILUS_VIEW_GRID_ID;
+}
+
+static void
+action_zoom_to_level (GSimpleAction *action,
+ GVariant *state,
+ gpointer user_data)
+{
+ NautilusGridView *self = NAUTILUS_GRID_VIEW (user_data);
+ int zoom_level;
+
+ zoom_level = g_variant_get_int32 (state);
+ set_zoom_level (self, zoom_level);
+ g_simple_action_set_state (G_SIMPLE_ACTION (action), state);
+
+ if (g_settings_get_enum (nautilus_icon_view_preferences,
+ NAUTILUS_PREFERENCES_ICON_VIEW_DEFAULT_ZOOM_LEVEL) != zoom_level)
+ {
+ g_settings_set_enum (nautilus_icon_view_preferences,
+ NAUTILUS_PREFERENCES_ICON_VIEW_DEFAULT_ZOOM_LEVEL,
+ zoom_level);
+ }
+}
+
+static void
+on_captions_preferences_changed (NautilusGridView *self)
+{
+ set_captions_from_preferences (self);
+
+ /* Hack: this relies on the assumption that NautilusGridCell updates
+ * captions whenever the icon size is set (even if it's the same value). */
+ nautilus_list_base_set_icon_size (NAUTILUS_LIST_BASE (self),
+ get_icon_size_for_zoom_level (self->zoom_level));
+}
+
+static void
+dispose (GObject *object)
+{
+ G_OBJECT_CLASS (nautilus_grid_view_parent_class)->dispose (object);
+}
+
+static void
+finalize (GObject *object)
+{
+ G_OBJECT_CLASS (nautilus_grid_view_parent_class)->finalize (object);
+}
+
+static void
+bind_cell (GtkSignalListItemFactory *factory,
+ GtkListItem *listitem,
+ gpointer user_data)
+{
+ GtkWidget *cell;
+ NautilusViewItem *item;
+
+ cell = gtk_list_item_get_child (listitem);
+ item = NAUTILUS_VIEW_ITEM (gtk_list_item_get_item (listitem));
+
+ nautilus_view_item_set_item_ui (item, cell);
+
+ if (nautilus_view_cell_once (NAUTILUS_VIEW_CELL (cell)))
+ {
+ GtkWidget *parent;
+
+ /* At the time of ::setup emission, the item ui has got no parent yet,
+ * that's why we need to complete the widget setup process here, on the
+ * first time ::bind is emitted. */
+ parent = gtk_widget_get_parent (cell);
+ gtk_widget_set_halign (parent, GTK_ALIGN_CENTER);
+ gtk_widget_set_valign (parent, GTK_ALIGN_START);
+ gtk_widget_set_margin_top (parent, 3);
+ gtk_widget_set_margin_bottom (parent, 3);
+ gtk_widget_set_margin_start (parent, 3);
+ gtk_widget_set_margin_end (parent, 3);
+
+ gtk_accessible_update_relation (GTK_ACCESSIBLE (parent),
+ GTK_ACCESSIBLE_RELATION_LABELLED_BY, cell, NULL,
+ -1);
+ }
+}
+
+static void
+unbind_cell (GtkSignalListItemFactory *factory,
+ GtkListItem *listitem,
+ gpointer user_data)
+{
+ NautilusViewItem *item;
+
+ item = NAUTILUS_VIEW_ITEM (gtk_list_item_get_item (listitem));
+
+ nautilus_view_item_set_item_ui (item, NULL);
+}
+
+static void
+setup_cell (GtkSignalListItemFactory *factory,
+ GtkListItem *listitem,
+ gpointer user_data)
+{
+ NautilusGridView *self = NAUTILUS_GRID_VIEW (user_data);
+ NautilusGridCell *cell;
+
+ cell = nautilus_grid_cell_new (NAUTILUS_LIST_BASE (self));
+ setup_cell_common (listitem, NAUTILUS_VIEW_CELL (cell));
+
+ nautilus_grid_cell_set_caption_attributes (cell, self->caption_attributes);
+}
+
+static GtkGridView *
+create_view_ui (NautilusGridView *self)
+{
+ NautilusViewModel *model;
+ GtkListItemFactory *factory;
+ GtkWidget *widget;
+
+ model = nautilus_list_base_get_model (NAUTILUS_LIST_BASE (self));
+
+ factory = gtk_signal_list_item_factory_new ();
+ g_signal_connect (factory, "setup", G_CALLBACK (setup_cell), self);
+ g_signal_connect (factory, "bind", G_CALLBACK (bind_cell), self);
+ g_signal_connect (factory, "unbind", G_CALLBACK (unbind_cell), self);
+
+ widget = gtk_grid_view_new (GTK_SELECTION_MODEL (model), factory);
+
+ /* We don't use the built-in child activation feature for clicks because it
+ * doesn't fill all our needs nor does it match our expected behavior.
+ * Instead, we roll our own event handling and double/single click mode.
+ * However, GtkGridView:single-click-activate has other effects besides
+ * activation, as it affects the selection behavior as well (e.g. selects on
+ * hover). Setting it to FALSE gives us the expected behavior. */
+ gtk_grid_view_set_single_click_activate (GTK_GRID_VIEW (widget), FALSE);
+ gtk_grid_view_set_max_columns (GTK_GRID_VIEW (widget), 20);
+ gtk_grid_view_set_enable_rubberband (GTK_GRID_VIEW (widget), TRUE);
+
+ /* While we don't want to use GTK's click activation, we'll let it handle
+ * the key activation part (with Enter).
+ */
+ g_signal_connect (widget, "activate", G_CALLBACK (on_grid_view_item_activated), self);
+
+ return GTK_GRID_VIEW (widget);
+}
+
+const GActionEntry view_icon_actions[] =
+{
+ { "sort", NULL, "(sb)", "('invalid',false)", action_sort_order_changed },
+ { "zoom-to-level", NULL, NULL, "100", action_zoom_to_level }
+};
+
+static void
+nautilus_grid_view_class_init (NautilusGridViewClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ NautilusFilesViewClass *files_view_class = NAUTILUS_FILES_VIEW_CLASS (klass);
+ NautilusListBaseClass *list_base_view_class = NAUTILUS_LIST_BASE_CLASS (klass);
+
+ object_class->dispose = dispose;
+ object_class->finalize = finalize;
+
+ files_view_class->bump_zoom_level = real_bump_zoom_level;
+ files_view_class->can_zoom_in = real_can_zoom_in;
+ files_view_class->can_zoom_out = real_can_zoom_out;
+ files_view_class->sort_directories_first_changed = real_sort_directories_first_changed;
+ files_view_class->get_view_id = real_get_view_id;
+ files_view_class->restore_standard_zoom_level = real_restore_standard_zoom_level;
+ files_view_class->is_zoom_level_default = real_is_zoom_level_default;
+ files_view_class->preview_selection_event = real_preview_selection_event;
+
+ list_base_view_class->get_icon_size = real_get_icon_size;
+ list_base_view_class->get_view_ui = real_get_view_ui;
+ list_base_view_class->scroll_to_item = real_scroll_to_item;
+}
+
+static void
+nautilus_grid_view_init (NautilusGridView *self)
+{
+ GtkWidget *content_widget;
+
+ gtk_widget_add_css_class (GTK_WIDGET (self), "nautilus-grid-view");
+
+ set_captions_from_preferences (self);
+ g_signal_connect_object (nautilus_icon_view_preferences,
+ "changed::" NAUTILUS_PREFERENCES_ICON_VIEW_CAPTIONS,
+ G_CALLBACK (on_captions_preferences_changed),
+ self,
+ G_CONNECT_SWAPPED);
+
+ content_widget = nautilus_files_view_get_content_widget (NAUTILUS_FILES_VIEW (self));
+
+ self->view_ui = create_view_ui (self);
+ nautilus_list_base_setup_gestures (NAUTILUS_LIST_BASE (self));
+
+ gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (content_widget),
+ GTK_WIDGET (self->view_ui));
+
+ self->action_group = nautilus_files_view_get_action_group (NAUTILUS_FILES_VIEW (self));
+ g_action_map_add_action_entries (G_ACTION_MAP (self->action_group),
+ view_icon_actions,
+ G_N_ELEMENTS (view_icon_actions),
+ self);
+
+ self->directories_first = nautilus_files_view_should_sort_directories_first (NAUTILUS_FILES_VIEW (self));
+
+ self->zoom_level = get_default_zoom_level ();
+ /* Keep the action synced with the actual value, so the toolbar can poll it */
+ g_action_group_change_action_state (nautilus_files_view_get_action_group (NAUTILUS_FILES_VIEW (self)),
+ "zoom-to-level", g_variant_new_int32 (self->zoom_level));
+}
+
+NautilusGridView *
+nautilus_grid_view_new (NautilusWindowSlot *slot)
+{
+ return g_object_new (NAUTILUS_TYPE_GRID_VIEW,
+ "window-slot", slot,
+ NULL);
+}