diff options
Diffstat (limited to '')
-rw-r--r-- | src/nautilus-grid-view.c | 604 |
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); +} |