[nautilus/wip/antoniof/new-list-view-continuation: 138/141] list-view: Support expanding as a tree




commit f34f5f71faa8039c0108fe4672a64124c6e7fb08
Author: António Fernandes <antoniof gnome org>
Date:   Wed Apr 6 00:40:00 2022 +0100

    list-view: Support expanding as a tree
    
    Starred, Recent and Search still not supported due to bugs

 src/nautilus-files-view.c                       |  10 ++
 src/nautilus-files-view.h                       |   2 +
 src/nautilus-list-view.c                        | 162 +++++++++++++++++++++++-
 src/nautilus-name-cell.c                        |   8 ++
 src/nautilus-name-cell.h                        |   1 +
 src/resources/ui/nautilus-name-cell.ui          | 143 +++++++++++----------
 src/resources/ui/nautilus-preferences-window.ui |   1 -
 7 files changed, 256 insertions(+), 71 deletions(-)
---
diff --git a/src/nautilus-files-view.c b/src/nautilus-files-view.c
index 52e9e6ac4..a2c5a042c 100644
--- a/src/nautilus-files-view.c
+++ b/src/nautilus-files-view.c
@@ -4728,6 +4728,16 @@ load_error_callback (NautilusDirectory *directory,
         nautilus_files_view_get_containing_window (view));
 }
 
+gboolean
+nautilus_files_view_has_subdirectory (NautilusFilesView *view,
+                                      NautilusDirectory *directory)
+{
+    NautilusFilesViewPrivate *priv;
+    priv = nautilus_files_view_get_instance_private (view);
+
+    return g_list_find (priv->subdirectory_list, directory) != NULL;
+}
+
 void
 nautilus_files_view_add_subdirectory (NautilusFilesView *view,
                                       NautilusDirectory *directory)
diff --git a/src/nautilus-files-view.h b/src/nautilus-files-view.h
index e1882b7ba..ae2031920 100644
--- a/src/nautilus-files-view.h
+++ b/src/nautilus-files-view.h
@@ -268,6 +268,8 @@ gboolean            nautilus_files_view_should_show_file                 (Nautil
 gboolean            nautilus_files_view_should_sort_directories_first    (NautilusFilesView *view);
 void                nautilus_files_view_ignore_hidden_file_preferences   (NautilusFilesView *view);
 
+gboolean            nautilus_files_view_has_subdirectory                (NautilusFilesView *view,
+                                                                         NautilusDirectory *directory);
 void                nautilus_files_view_add_subdirectory                (NautilusFilesView *view,
                                                                          NautilusDirectory *directory);
 void                nautilus_files_view_remove_subdirectory             (NautilusFilesView *view,
diff --git a/src/nautilus-list-view.c b/src/nautilus-list-view.c
index dea71fea9..3db5b9a9f 100644
--- a/src/nautilus-list-view.c
+++ b/src/nautilus-list-view.c
@@ -27,6 +27,9 @@
 #include "nautilus-star-cell.h"
 #include "nautilus-tag-manager.h"
 
+/* We wait two seconds after row is collapsed to unload the subdirectory */
+#define COLLAPSE_TO_UNLOAD_DELAY 2
+
 struct _NautilusListView
 {
     NautilusListBase parent_instance;
@@ -37,6 +40,7 @@ struct _NautilusListView
     gint zoom_level;
 
     gboolean directories_first;
+    gboolean expand_as_a_tree;
 
     GQuark path_attribute_q;
     GFile *file_path_base_location;
@@ -738,12 +742,19 @@ static void
 real_begin_loading (NautilusFilesView *files_view)
 {
     NautilusListView *self = NAUTILUS_LIST_VIEW (files_view);
+    NautilusViewModel *model;
     NautilusFile *file;
 
     NAUTILUS_FILES_VIEW_CLASS (nautilus_list_view_parent_class)->begin_loading (files_view);
 
     update_columns_settings_from_metadata_and_preferences (self);
 
+    /* TODO Reload the view if this setting changes. We can't easily switch
+     * tree mode on/off on an already loaded view and the preference is not
+     * expected to be changed frequently. */
+    self->expand_as_a_tree = g_settings_get_boolean (nautilus_list_view_preferences,
+                                                     NAUTILUS_PREFERENCES_LIST_VIEW_USE_TREE);
+
     self->path_attribute_q = 0;
     g_clear_object (&self->file_path_base_location);
     file = nautilus_files_view_get_directory_as_file (files_view);
@@ -758,7 +769,21 @@ real_begin_loading (NautilusFilesView *files_view)
     {
         self->path_attribute_q = g_quark_from_string ("where");
         self->file_path_base_location = get_base_location (self);
+
+        /* Forcefully disabling tree in these special locations because this
+         * view and its model currently don't expect the same file appearing
+         * more than once.
+         *
+         * NautilusFilesView still has support for the same file being present
+         * in multiple directories (struct FileAndDirectory), so, if someone
+         * cares enough about expanding folders in these special locations:
+         * TODO: Making the model items aware of their current model instead of
+         * relying on `nautilus_file_get_parent()`. */
+        self->expand_as_a_tree = FALSE;
     }
+
+    model = nautilus_list_base_get_model (NAUTILUS_LIST_BASE (self));
+    nautilus_view_model_expand_as_a_tree (model, self->expand_as_a_tree);
 }
 
 static void
@@ -836,7 +861,6 @@ static void
 real_sort_directories_first_changed (NautilusFilesView *files_view)
 {
     NautilusListView *self = NAUTILUS_LIST_VIEW (files_view);
-    NautilusViewModel *model;
 
     self->directories_first = nautilus_files_view_should_sort_directories_first (NAUTILUS_FILES_VIEW (self));
 
@@ -863,6 +887,114 @@ real_get_view_id (NautilusFilesView *files_view)
     return NAUTILUS_VIEW_LIST_ID;
 }
 
+typedef struct
+{
+    NautilusListView *self;
+    NautilusViewItem *item;
+    NautilusDirectory *directory;
+} UnloadDelayData;
+
+static void
+unload_delay_data_free (UnloadDelayData *unload_data)
+{
+    g_clear_weak_pointer (&unload_data->self);
+    g_clear_object (&unload_data->item);
+    g_clear_object (&unload_data->directory);
+
+    g_free (unload_data);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (UnloadDelayData, unload_delay_data_free)
+
+static UnloadDelayData *
+unload_delay_data_new (NautilusListView  *self,
+                       NautilusViewItem  *item,
+                       NautilusDirectory *directory)
+{
+    UnloadDelayData *unload_data;
+
+    unload_data = g_new0 (UnloadDelayData, 1);
+    g_set_weak_pointer (&unload_data->self, self);
+    g_set_object (&unload_data->item, item);
+    g_set_object (&unload_data->directory, directory);
+
+    return unload_data;
+}
+
+static gboolean
+unload_file_timeout (gpointer data)
+{
+    g_autoptr (UnloadDelayData) unload_data = data;
+    NautilusListView *self = unload_data->self;
+    NautilusViewModel *model = nautilus_list_base_get_model (NAUTILUS_LIST_BASE (self));
+    if (unload_data->self == NULL)
+    {
+        return G_SOURCE_REMOVE;
+    }
+
+    for (guint i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (model)); i++)
+    {
+        g_autoptr (GtkTreeListRow) row = g_list_model_get_item (G_LIST_MODEL (model), i);
+        g_autoptr (NautilusViewItem) item = gtk_tree_list_row_get_item (row);
+        if (item != NULL && item == unload_data->item)
+        {
+            if (gtk_tree_list_row_get_expanded (row))
+            {
+                /* It has been expanded again before the timeout. Do nothing. */
+                return G_SOURCE_REMOVE;
+            }
+            break;
+        }
+    }
+
+    if (nautilus_files_view_has_subdirectory (NAUTILUS_FILES_VIEW (self),
+                                              unload_data->directory))
+    {
+        nautilus_files_view_remove_subdirectory (NAUTILUS_FILES_VIEW (self),
+                                                 unload_data->directory);
+    }
+
+    /* The model holds a GListStore for every subdirectory. Empty it. */
+    nautilus_view_model_clear_subdirectory (model, unload_data->item);
+
+    return G_SOURCE_REMOVE;
+}
+
+static void
+on_row_expanded_changed (GObject    *gobject,
+                         GParamSpec *pspec,
+                         gpointer    user_data)
+{
+    GtkTreeListRow *row = GTK_TREE_LIST_ROW (gobject);
+    NautilusListView *self = NAUTILUS_LIST_VIEW (user_data);
+    NautilusViewItem *item;
+    g_autoptr (NautilusDirectory) directory = NULL;
+    gboolean expanded;
+
+    item = NAUTILUS_VIEW_ITEM (gtk_tree_list_row_get_item (row));
+    if (item == NULL)
+    {
+        /* Row has been destroyed. */
+        return;
+    }
+
+    directory = nautilus_directory_get_for_file (nautilus_view_item_get_file (item));
+    expanded = gtk_tree_list_row_get_expanded (row);
+    if (expanded)
+    {
+        if (!nautilus_files_view_has_subdirectory (NAUTILUS_FILES_VIEW (self), directory))
+        {
+            nautilus_files_view_add_subdirectory (NAUTILUS_FILES_VIEW (self), directory);
+        }
+    }
+    else
+    {
+        g_timeout_add_seconds (COLLAPSE_TO_UNLOAD_DELAY,
+                               unload_file_timeout,
+                               unload_delay_data_new (self, item, directory));
+    }
+}
+
 static void
 on_item_click_released_workaround (GtkGestureClick *gesture,
                                    gint             n_press,
@@ -937,6 +1069,17 @@ setup_name_cell (GtkSignalListItemFactory *factory,
     gtk_list_item_set_child (listitem, GTK_WIDGET (cell));
 
     setup_selection_click_workaround (cell);
+
+    if (self->expand_as_a_tree)
+    {
+        GtkTreeExpander *expander;
+
+        expander = nautilus_name_cell_get_expander (NAUTILUS_NAME_CELL (cell));
+        gtk_tree_expander_set_indent_for_icon (expander, TRUE);
+        g_object_bind_property (listitem, "item",
+                                expander, "list-row",
+                                G_BINDING_SYNC_CREATE);
+    }
 }
 
 static void
@@ -944,11 +1087,20 @@ bind_name_cell (GtkSignalListItemFactory *factory,
                 GtkListItem              *listitem,
                 gpointer                  user_data)
 {
+    NautilusListView *self = user_data;
     NautilusViewItem *item;
 
     item = listitem_get_view_item (listitem);
 
     nautilus_view_item_set_item_ui (item, gtk_list_item_get_child (listitem));
+
+    if (self->expand_as_a_tree)
+    {
+        g_signal_connect_object (GTK_TREE_LIST_ROW (gtk_list_item_get_item (listitem)),
+                                 "notify::expanded",
+                                 G_CALLBACK (on_row_expanded_changed),
+                                 self, 0);
+    }
 }
 
 static void
@@ -956,12 +1108,20 @@ unbind_name_cell (GtkSignalListItemFactory *factory,
                   GtkListItem              *listitem,
                   gpointer                  user_data)
 {
+    NautilusListView *self = user_data;
     NautilusViewItem *item;
 
     item = listitem_get_view_item (listitem);
     g_return_if_fail (NAUTILUS_IS_VIEW_ITEM (item));
 
     nautilus_view_item_set_item_ui (item, NULL);
+
+    if (self->expand_as_a_tree)
+    {
+        g_signal_handlers_disconnect_by_func (gtk_list_item_get_item (listitem),
+                                              on_row_expanded_changed,
+                                              self);
+    }
 }
 
 static void
diff --git a/src/nautilus-name-cell.c b/src/nautilus-name-cell.c
index 1a0720ea6..54f04a2ab 100644
--- a/src/nautilus-name-cell.c
+++ b/src/nautilus-name-cell.c
@@ -16,6 +16,7 @@ struct _NautilusNameCell
     GQuark path_attribute_q;
     GFile *file_path_base_location;
 
+    GtkWidget *expander;
     GtkWidget *fixed_height_box;
     GtkWidget *icon;
     GtkWidget *label;
@@ -314,6 +315,7 @@ nautilus_name_cell_class_init (NautilusNameCellClass *klass)
 
     gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/nautilus/ui/nautilus-name-cell.ui");
 
+    gtk_widget_class_bind_template_child (widget_class, NautilusNameCell, expander);
     gtk_widget_class_bind_template_child (widget_class, NautilusNameCell, fixed_height_box);
     gtk_widget_class_bind_template_child (widget_class, NautilusNameCell, icon);
     gtk_widget_class_bind_template_child (widget_class, NautilusNameCell, label);
@@ -344,3 +346,9 @@ nautilus_name_cell_show_snippet (NautilusNameCell *self)
 {
     self->show_snippet = TRUE;
 }
+
+GtkTreeExpander *
+nautilus_name_cell_get_expander (NautilusNameCell *self)
+{
+    return GTK_TREE_EXPANDER (self->expander);
+}
diff --git a/src/nautilus-name-cell.h b/src/nautilus-name-cell.h
index 62862cdf5..11b84931e 100644
--- a/src/nautilus-name-cell.h
+++ b/src/nautilus-name-cell.h
@@ -19,5 +19,6 @@ void nautilus_name_cell_set_path (NautilusNameCell *self,
                                   GQuark            path_attribute_q,
                                   GFile            *base_location);
 void nautilus_name_cell_show_snippet (NautilusNameCell *self);
+GtkTreeExpander * nautilus_name_cell_get_expander (NautilusNameCell *self);
 
 G_END_DECLS
diff --git a/src/resources/ui/nautilus-name-cell.ui b/src/resources/ui/nautilus-name-cell.ui
index 4313b92d6..01b895eb7 100644
--- a/src/resources/ui/nautilus-name-cell.ui
+++ b/src/resources/ui/nautilus-name-cell.ui
@@ -3,100 +3,105 @@
   <requires lib="gtk" version="4.0"/>
   <template class="NautilusNameCell" parent="NautilusViewCell">
     <child>
-      <object class="GtkBox">
-        <property name="spacing">6</property>
-        <property name="orientation">horizontal</property>
-        <property name="halign">fill</property>
-        <property name="valign">center</property>
-        <child>
-          <object class="GtkBox" id="fixed_height_box">
-            <property name="orientation">vertical</property>
-            <property name="halign">center</property>
-            <property name="height-request">16</property>
-            <property name="valign">center</property>
-            <child>
-              <object class="GtkPicture" id="icon">
-                <property name="halign">center</property>
-                <property name="valign">center</property>
-                <property name="can-shrink">False</property>
-              </object>
-            </child>
-          </object>
-        </child>
-        <child>
+      <object class="GtkTreeExpander" id="expander">
+        <property name="indent-for-icon">False</property>
+        <property name="child">
           <object class="GtkBox">
-            <property name="orientation">vertical</property>
+            <property name="spacing">6</property>
+            <property name="orientation">horizontal</property>
             <property name="halign">fill</property>
-            <property name="hexpand">True</property>
             <property name="valign">center</property>
-            <style>
-              <class name="column-name-labels-box"/>
-            </style>
             <child>
-              <object class="GtkLabel" id="path">
-                <property name="visible">False</property>
-                <property name="ellipsize">start</property>
-                <property name="justify">left</property>
-                <property name="lines">2</property>
-                <property name="wrap">True</property>
-                <property name="wrap-mode">word-char</property>
-                <property name="halign">fill</property>
-                <property name="xalign">0.0</property>
-                <attributes>
-                  <attribute name="insert-hyphens" value="false"></attribute>
-                </attributes>
-                <style>
-                  <class name="caption"/>
-                  <class name="dim-label"/>
-                </style>
+              <object class="GtkBox" id="fixed_height_box">
+                <property name="orientation">vertical</property>
+                <property name="halign">center</property>
+                <property name="height-request">16</property>
+                <property name="valign">center</property>
+                <child>
+                  <object class="GtkPicture" id="icon">
+                    <property name="halign">center</property>
+                    <property name="valign">center</property>
+                    <property name="can-shrink">False</property>
+                  </object>
+                </child>
               </object>
             </child>
             <child>
               <object class="GtkBox">
-                <property name="orientation">horizontal</property>
+                <property name="orientation">vertical</property>
                 <property name="halign">fill</property>
                 <property name="hexpand">True</property>
-                <property name="spacing">6</property>
+                <property name="valign">center</property>
+                <style>
+                  <class name="column-name-labels-box"/>
+                </style>
                 <child>
-                  <object class="GtkLabel" id="label">
-                    <property name="ellipsize">middle</property>
-                    <property name="lines">1</property>
-                    <property name="max-width-chars">-1</property>
-                    <property name="wrap">False</property>
+                  <object class="GtkLabel" id="path">
+                    <property name="visible">False</property>
+                    <property name="ellipsize">start</property>
+                    <property name="justify">left</property>
+                    <property name="lines">2</property>
+                    <property name="wrap">True</property>
                     <property name="wrap-mode">word-char</property>
-                    <property name="halign">start</property>
+                    <property name="halign">fill</property>
+                    <property name="xalign">0.0</property>
                     <attributes>
                       <attribute name="insert-hyphens" value="false"></attribute>
                     </attributes>
+                    <style>
+                      <class name="caption"/>
+                      <class name="dim-label"/>
+                    </style>
                   </object>
                 </child>
                 <child>
-                  <object class="GtkBox" id="emblems_box">
+                  <object class="GtkBox">
                     <property name="orientation">horizontal</property>
-                    <property name="halign">start</property>
+                    <property name="halign">fill</property>
+                    <property name="hexpand">True</property>
                     <property name="spacing">6</property>
+                    <child>
+                      <object class="GtkLabel" id="label">
+                        <property name="ellipsize">middle</property>
+                        <property name="lines">1</property>
+                        <property name="max-width-chars">-1</property>
+                        <property name="wrap">False</property>
+                        <property name="wrap-mode">word-char</property>
+                        <property name="halign">start</property>
+                        <attributes>
+                          <attribute name="insert-hyphens" value="false"></attribute>
+                        </attributes>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkBox" id="emblems_box">
+                        <property name="orientation">horizontal</property>
+                        <property name="halign">start</property>
+                        <property name="spacing">6</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="snippet">
+                    <property name="visible">False</property>
+                    <property name="ellipsize">end</property>
+                    <property name="justify">left</property>
+                    <property name="lines">2</property>
+                    <property name="wrap">True</property>
+                    <property name="wrap-mode">word</property>
+                    <property name="halign">fill</property>
+                    <property name="xalign">0.0</property>
+                    <style>
+                      <class name="caption-heading"/>
+                      <class name="accent"/>
+                    </style>
                   </object>
                 </child>
-              </object>
-            </child>
-            <child>
-              <object class="GtkLabel" id="snippet">
-                <property name="visible">False</property>
-                <property name="ellipsize">end</property>
-                <property name="justify">left</property>
-                <property name="lines">2</property>
-                <property name="wrap">True</property>
-                <property name="wrap-mode">word</property>
-                <property name="halign">fill</property>
-                <property name="xalign">0.0</property>
-                <style>
-                  <class name="caption-heading"/>
-                  <class name="accent"/>
-                </style>
               </object>
             </child>
           </object>
-        </child>
+        </property>
       </object>
     </child>
   </template>
diff --git a/src/resources/ui/nautilus-preferences-window.ui b/src/resources/ui/nautilus-preferences-window.ui
index abee4f2af..9b2cd2edc 100644
--- a/src/resources/ui/nautilus-preferences-window.ui
+++ b/src/resources/ui/nautilus-preferences-window.ui
@@ -34,7 +34,6 @@
                 <property name="title" translatable="yes">_Expandable Folders in List View</property>
                 <property name="title_lines">0</property>
                 <property name="use_underline">True</property>
-                <property name="visible">False</property>
                 <child>
                   <object class="GtkSwitch" id="use_tree_view_switch">
                     <property name="valign">center</property>


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]