/* * Copyright (C) 2024 The GNOME project contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ #include "nautilus-network-view.h" #include #include "nautilus-file.h" #include "nautilus-global-preferences.h" #include "nautilus-list-base-private.h" #include "nautilus-network-cell.h" #include "nautilus-scheme.h" #include "nautilus-view-item.h" #include "nautilus-view-model.h" struct _NautilusNetworkView { NautilusListBase parent_instance; GtkListView *view_ui; }; G_DEFINE_FINAL_TYPE (NautilusNetworkView, nautilus_network_view, NAUTILUS_TYPE_LIST_BASE) #define get_view_item(li) \ (NAUTILUS_VIEW_ITEM (gtk_tree_list_row_get_item (GTK_TREE_LIST_ROW (gtk_list_item_get_item (li))))) static const NautilusViewInfo network_view_info = { .view_id = NAUTILUS_VIEW_NETWORK_ID, .zoom_level_min = NAUTILUS_LIST_ZOOM_LEVEL_SMALL, .zoom_level_max = NAUTILUS_LIST_ZOOM_LEVEL_SMALL, .zoom_level_standard = NAUTILUS_LIST_ZOOM_LEVEL_SMALL, }; static NautilusViewInfo real_get_view_info (NautilusListBase *list_base) { return network_view_info; } enum { SECTION_CONNECTED, SECTION_PREVIOUS, SECTION_AVAILABLE, }; static inline gint get_section (NautilusViewItem *item) { NautilusFile *file = nautilus_view_item_get_file (item); if (nautilus_file_can_unmount (file)) { return SECTION_CONNECTED; } g_autoptr (GFile) location = nautilus_file_get_location (file); if (g_file_has_uri_scheme (location, SCHEME_NETWORK)) { return SECTION_AVAILABLE; } else { return SECTION_PREVIOUS; } } static gint sort_network_sections (gconstpointer a, gconstpointer b, gpointer user_data) { GtkTreeListRow *row_a = GTK_TREE_LIST_ROW ((gpointer) a); GtkTreeListRow *row_b = GTK_TREE_LIST_ROW ((gpointer) b); g_autoptr (NautilusViewItem) item_a = NAUTILUS_VIEW_ITEM (gtk_tree_list_row_get_item (row_a)); g_autoptr (NautilusViewItem) item_b = NAUTILUS_VIEW_ITEM (gtk_tree_list_row_get_item (row_b)); return get_section (item_a) - get_section (item_b); } static gint sort_network_items (gconstpointer a, gconstpointer b, gpointer user_data) { NautilusViewItem *item_a = NAUTILUS_VIEW_ITEM ((gpointer) a); NautilusViewItem *item_b = NAUTILUS_VIEW_ITEM ((gpointer) b); NautilusFile *file_a = nautilus_view_item_get_file (item_a); NautilusFile *file_b = nautilus_view_item_get_file (item_b); if (get_section (item_a) == SECTION_PREVIOUS && get_section (item_a) == SECTION_PREVIOUS) { return nautilus_file_compare_for_sort (file_a, file_b, NAUTILUS_FILE_SORT_BY_ATIME, FALSE, TRUE /* reversed */); } return nautilus_file_compare_for_sort (file_a, file_b, NAUTILUS_FILE_SORT_BY_DISPLAY_NAME, FALSE, FALSE); } static void real_set_zoom_level (NautilusListBase *list_base, int new_level) { g_warn_if_fail (new_level == network_view_info.zoom_level_standard); } /* We only care about the keyboard activation part that GtkListView provides, * but we don't need any special filtering here. Indeed, we ask GtkListView * to not activate on single click, and we get to handle double clicks before * GtkListView does (as one of widget subclassing's goal is to modify the parent * class's behavior), while claiming the click gestures, so it means GtkListView * 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_list_view_item_activated (GtkListView *list_view, guint position, gpointer user_data) { NautilusNetworkView *self = NAUTILUS_NETWORK_VIEW (user_data); nautilus_list_base_activate_selection (NAUTILUS_LIST_BASE (self), FALSE); } static void real_popup_background_context_menu (NautilusListBase *self, double x, double y) { g_signal_stop_emission_by_name (G_OBJECT (self), "popup-background-context-menu"); } static guint real_get_icon_size (NautilusListBase *list_base_view) { return NAUTILUS_LIST_ICON_SIZE_SMALL; } static int real_get_zoom_level (NautilusListBase *list_base_view) { return network_view_info.zoom_level_standard; } static void real_scroll_to (NautilusListBase *list_base_view, guint position, GtkListScrollFlags flags, GtkScrollInfo *scroll) { NautilusNetworkView *self = NAUTILUS_NETWORK_VIEW (list_base_view); gtk_list_view_scroll_to (self->view_ui, position, flags, scroll); } static GVariant * real_get_sort_state (NautilusListBase *list_base) { return g_variant_take_ref (g_variant_new ("(sb)", "invalid", FALSE)); } static void real_set_enable_rubberband (NautilusListBase *list_base, gboolean enabled) { /* We ignore setting this value as it should always stay disabled. */ } static void real_set_sort_state (NautilusListBase *list_base, GVariant *value) { /* No op */ } static void nautilus_network_view_dispose (GObject *object) { G_OBJECT_CLASS (nautilus_network_view_parent_class)->dispose (object); } static void nautilus_network_view_finalize (GObject *object) { G_OBJECT_CLASS (nautilus_network_view_parent_class)->finalize (object); } static void bind_cell (GtkSignalListItemFactory *factory, GtkListItem *listitem, gpointer user_data) { GtkWidget *cell = gtk_list_item_get_child (listitem); g_autoptr (NautilusViewItem) item = get_view_item (listitem); nautilus_view_item_set_item_ui (item, cell); } static void unbind_cell (GtkSignalListItemFactory *factory, GtkListItem *listitem, gpointer user_data) { g_autoptr (NautilusViewItem) item = get_view_item (listitem); /* item may be NULL when row has just been destroyed. */ if (item != NULL) { nautilus_view_item_set_item_ui (item, NULL); } } static void setup_cell (GtkSignalListItemFactory *factory, GtkListItem *listitem, gpointer user_data) { NautilusNetworkView *self = NAUTILUS_NETWORK_VIEW (user_data); NautilusViewCell *cell; GtkExpression *expression; cell = nautilus_network_cell_new (NAUTILUS_LIST_BASE (self)); gtk_list_item_set_child (listitem, GTK_WIDGET (cell)); setup_cell_common (G_OBJECT (listitem), cell, GTK_WIDGET (cell)); g_object_bind_property (self, "icon-size", cell, "icon-size", G_BINDING_SYNC_CREATE); /* Use file display name as accessible label. Explaining in pseudo-code: * listitem:accessible-name :- listitem:item:item:file:display-name */ expression = gtk_property_expression_new (GTK_TYPE_LIST_ITEM, NULL, "item"); expression = gtk_property_expression_new (GTK_TYPE_TREE_LIST_ROW, expression, "item"); expression = gtk_property_expression_new (NAUTILUS_TYPE_VIEW_ITEM, expression, "file"); expression = gtk_property_expression_new (NAUTILUS_TYPE_FILE, expression, "display-name"); gtk_expression_bind (expression, listitem, "accessible-label", listitem); } static void bind_header (GtkSignalListItemFactory *factory, GtkListHeader *listheader, gpointer user_data) { GtkWidget *label = gtk_list_header_get_child (listheader); GtkTreeListRow *row = GTK_TREE_LIST_ROW (gtk_list_header_get_item (listheader)); g_autoptr (NautilusViewItem) item = NAUTILUS_VIEW_ITEM (gtk_tree_list_row_get_item (row)); switch (get_section (item)) { case SECTION_CONNECTED: { /* Translators: This refers to network places which are currently mounted */ gtk_label_set_label (GTK_LABEL (label), _("Connected")); } break; case SECTION_PREVIOUS: { /* Translators: This refers to network servers the user has previously connected to */ gtk_label_set_label (GTK_LABEL (label), _("Previous")); } break; case SECTION_AVAILABLE: { gtk_label_set_label (GTK_LABEL (label), _("Available on Current Network")); /* TODO: Use network name from NMClient:primary-connection:id to * match design mockup: "Available on Network1234" */ } break; default: { g_assert_not_reached (); } } } static void setup_header (GtkSignalListItemFactory *factory, GtkListHeader *listheader, gpointer user_data) { GtkWidget *label = gtk_label_new (NULL); gtk_widget_add_css_class (label, "heading"); gtk_label_set_xalign (GTK_LABEL (label), 0.0); gtk_list_header_set_child (listheader, label); } static void on_model_changed (NautilusNetworkView *self) { NautilusViewModel *model = nautilus_list_base_get_model (NAUTILUS_LIST_BASE (self)); if (model != NULL) { g_autoptr (GtkCustomSorter) sorter = gtk_custom_sorter_new (sort_network_items, self, NULL); g_autoptr (GtkCustomSorter) sections_sorter = gtk_custom_sorter_new (sort_network_sections, self, NULL); nautilus_view_model_set_sorter (model, GTK_SORTER (sorter)); nautilus_view_model_set_section_sorter (model, GTK_SORTER (sections_sorter)); } gtk_list_view_set_model (self->view_ui, GTK_SELECTION_MODEL (model)); } static GtkListView * create_view_ui (NautilusNetworkView *self) { g_autoptr (GtkListItemFactory) factory = gtk_signal_list_item_factory_new (); g_autoptr (GtkListItemFactory) header_factory = gtk_signal_list_item_factory_new (); GtkListView *list_view; 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); list_view = GTK_LIST_VIEW (gtk_list_view_new (NULL, g_steal_pointer (&factory))); g_signal_connect (header_factory, "setup", G_CALLBACK (setup_header), self); g_signal_connect (header_factory, "bind", G_CALLBACK (bind_header), self); gtk_list_view_set_header_factory (list_view, header_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, GtkListView: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_list_view_set_single_click_activate (list_view, FALSE); gtk_list_view_set_enable_rubberband (list_view, FALSE); gtk_list_view_set_tab_behavior (list_view, GTK_LIST_TAB_ITEM); gtk_accessible_update_property (GTK_ACCESSIBLE (list_view), GTK_ACCESSIBLE_PROPERTY_LABEL, _("Content View"), GTK_ACCESSIBLE_PROPERTY_ROLE_DESCRIPTION, _("View of the current location"), -1); /* While we don't want to use GTK's click activation, we'll let it handle * the key activation part (with Enter). */ g_signal_connect (list_view, "activate", G_CALLBACK (on_list_view_item_activated), self); return list_view; } static void nautilus_network_view_class_init (NautilusNetworkViewClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); NautilusListBaseClass *list_base_view_class = NAUTILUS_LIST_BASE_CLASS (klass); object_class->dispose = nautilus_network_view_dispose; object_class->finalize = nautilus_network_view_finalize; list_base_view_class->get_icon_size = real_get_icon_size; list_base_view_class->get_sort_state = real_get_sort_state; list_base_view_class->get_view_info = real_get_view_info; list_base_view_class->get_zoom_level = real_get_zoom_level; list_base_view_class->popup_background_context_menu = real_popup_background_context_menu; list_base_view_class->scroll_to = real_scroll_to; list_base_view_class->set_enable_rubberband = real_set_enable_rubberband; list_base_view_class->set_sort_state = real_set_sort_state; list_base_view_class->set_zoom_level = real_set_zoom_level; } static void nautilus_network_view_init (NautilusNetworkView *self) { GtkWidget *scrolled_window = nautilus_list_base_get_scrolled_window (NAUTILUS_LIST_BASE (self)); gtk_widget_add_css_class (GTK_WIDGET (self), "nautilus-network-view"); self->view_ui = create_view_ui (self); nautilus_list_base_setup_gestures (NAUTILUS_LIST_BASE (self)); g_signal_connect_swapped (self, "notify::model", G_CALLBACK (on_model_changed), self); gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scrolled_window), GTK_WIDGET (self->view_ui)); nautilus_list_base_set_zoom_level (NAUTILUS_LIST_BASE (self), network_view_info.zoom_level_standard); } NautilusNetworkView * nautilus_network_view_new (void) { return g_object_new (NAUTILUS_TYPE_NETWORK_VIEW, NULL); }