Package: nautilus
Version: 3.38.2-1
Tags: patch
Severity: important
Justification: breaks common usage patterns and standards compliance

Dear maintainers,

I've found that Nautilus always opens each selected file in a separate
application instance even though the latter's desktop file Exec record
contains %F or %U. This leads to the following issues.

   1. This behavior breaks a common usage pattern when user selects
   multiple files and wants to open them in a single application.
   
   For example:
   
      * User wants to watch selected video files.
      
      With Nautilus 3.38, only one of them ends up in the play queue of
      GNOME's default video player (Totem) because opening each application
      instance overwrites the queue.
      
      It is even worse when using a player which allows simultanious
      instances leading to multiple videos playing at once.
      
      Same with audio files.
      
      * User wants to view selected images.
      
      With current behavior, each image opens in a separate Eye of GNOME
      window instead of opening the first one in a single window and cycling
      through the other ones.
      
   2. Current behavior doesn't meet the Freedesktop.org desktop files
   standard[1].
   
   3. As explained in the upstream bug report[2], this annoying behavior
   was introduced for development testing purposes only:
   
      "This was introduced to ensure compatibility with Flatpak. At the time
      there was no API to allow grouping files by their type or somesuch and
      opening them all at once."
      
      "It has nothing to do with how many users use "flatpaks". It's about
      using nautilus in a Flatpak. Which only developers do as nautilus is
      not meant to be used as a sandboxed application, but as a system
      application."

   So this change was introduced for development testing purposes only,
   and Nautilus is not and never meant to be used sandboxed by end user.
   At the same time, this change breaks common usage patterns for people
   who are not Nautilus developers i.e. an overwhelming majority of users.
   
   4. This problematic change was reverted in a subsequent MR[3] which
   unfortunately didn't make it into version 3.38.
   
Given all above, could you consider backporting the fix into Nautilus
in Debian 11 (stable), please? The patch in question, available for
download from the aforementioned MR, applies cleanly to the version in
stable and works flawlessly, providing both the correct behavior for
end users *and* Flatpak support.

The patch is also attached to this message for your convenience.

   [1]
   https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s07.html
   
   [2] https://gitlab.gnome.org/GNOME/nautilus/-/issues/117
   
   [3] https://gitlab.gnome.org/GNOME/nautilus/-/merge_requests/518

--
Regards,
Алексей Шилин
diff --git a/src/nautilus-mime-actions.c b/src/nautilus-mime-actions.c
index 1a3abff387b19c82e37d2c4179e3a62e033f3438..ecccd8efcddc93d7b1169fab0fee5d1d6a154446 100644
--- a/src/nautilus-mime-actions.c
+++ b/src/nautilus-mime-actions.c
@@ -60,6 +60,12 @@ typedef struct
     char *uri;
 } LaunchLocation;
 
+typedef struct
+{
+    GAppInfo *application;
+    GList *uris;
+} ApplicationLaunchParameters;
+
 typedef struct
 {
     NautilusWindowSlot *slot;
@@ -83,8 +89,7 @@ typedef struct
 {
     ActivateParameters *activation_params;
     GQueue *uris;
-    GQueue *unhandled_uris;
-} ApplicationLaunchParameters;
+} ApplicationLaunchAsyncParameters;
 
 /* Microsoft mime types at https://blogs.msdn.microsoft.com/vsofficedeveloper/2008/05/08/office-2007-file-format-mime-types-for-http-content-streaming-2/ */
 struct
@@ -239,6 +244,20 @@ static void activate_callback (GList   *files,
                                gpointer callback_data);
 static void activation_mount_not_mounted (ActivateParameters *parameters);
 
+static gboolean
+is_sandboxed (void)
+{
+    static gboolean ret;
+
+    static gsize init = 0;
+    if (g_once_init_enter (&init))
+    {
+        ret = g_file_test ("/.flatpak-info", G_FILE_TEST_EXISTS);
+        g_once_init_leave (&init, 1);
+    }
+
+    return ret;
+}
 
 static void
 launch_location_free (LaunchLocation *location)
@@ -340,19 +359,27 @@ launch_locations_from_file_list (GList *list)
 }
 
 static ApplicationLaunchParameters *
-application_launch_parameters_new (ActivateParameters *activation_params,
-                                   GQueue             *uris)
+application_launch_parameters_new (GAppInfo *application,
+                                   GList    *uris)
 {
     ApplicationLaunchParameters *result;
 
     result = g_new0 (ApplicationLaunchParameters, 1);
-    result->activation_params = activation_params;
-    result->uris = uris;
-    result->unhandled_uris = g_queue_new ();
+    result->application = g_object_ref (application);
+    result->uris = g_list_copy_deep (uris, (GCopyFunc) g_strdup, NULL);
 
     return result;
 }
 
+static void
+application_launch_parameters_free (ApplicationLaunchParameters *parameters)
+{
+    g_object_unref (parameters->application);
+    g_list_free_full (parameters->uris, g_free);
+
+    g_free (parameters);
+}
+
 static gboolean
 nautilus_mime_actions_check_if_required_attributes_ready (NautilusFile *file)
 {
@@ -684,6 +711,114 @@ nautilus_mime_file_opens_in_external_app (NautilusFile *file)
     return (activation_action == ACTIVATION_ACTION_OPEN_IN_APPLICATION);
 }
 
+
+static unsigned int
+mime_application_hash (GAppInfo *app)
+{
+    const char *id;
+
+    id = g_app_info_get_id (app);
+
+    if (id == NULL)
+    {
+        return GPOINTER_TO_UINT (app);
+    }
+
+    return g_str_hash (id);
+}
+
+static void
+list_to_parameters_foreach (GAppInfo  *application,
+                            GList     *uris,
+                            GList    **ret)
+{
+    ApplicationLaunchParameters *parameters;
+
+    uris = g_list_reverse (uris);
+
+    parameters = application_launch_parameters_new
+                     (application, uris);
+    *ret = g_list_prepend (*ret, parameters);
+}
+
+
+/**
+ * make_activation_parameters
+ *
+ * Construct a list of ApplicationLaunchParameters from a list of NautilusFiles,
+ * where files that have the same default application are put into the same
+ * launch parameter, and others are put into the unhandled_files list.
+ *
+ * @files: Files to use for construction.
+ * @unhandled_files: Files without any default application will be put here.
+ *
+ * Return value: Newly allocated list of ApplicationLaunchParameters.
+ **/
+static GList *
+make_activation_parameters (GList  *uris,
+                            GList **unhandled_uris)
+{
+    GList *ret, *l, *app_uris;
+    NautilusFile *file;
+    GAppInfo *app, *old_app;
+    GHashTable *app_table;
+    char *uri;
+
+    ret = NULL;
+    *unhandled_uris = NULL;
+
+    app_table = g_hash_table_new_full
+                    ((GHashFunc) mime_application_hash,
+                    (GEqualFunc) g_app_info_equal,
+                    (GDestroyNotify) g_object_unref,
+                    (GDestroyNotify) g_list_free);
+
+    for (l = uris; l != NULL; l = l->next)
+    {
+        uri = l->data;
+        file = nautilus_file_get_by_uri (uri);
+
+        app = nautilus_mime_get_default_application_for_file (file);
+        if (app != NULL)
+        {
+            app_uris = NULL;
+
+            if (g_hash_table_lookup_extended (app_table, app,
+                                              (gpointer *) &old_app,
+                                              (gpointer *) &app_uris))
+            {
+                g_hash_table_steal (app_table, old_app);
+
+                app_uris = g_list_prepend (app_uris, uri);
+
+                g_object_unref (app);
+                app = old_app;
+            }
+            else
+            {
+                app_uris = g_list_prepend (NULL, uri);
+            }
+
+            g_hash_table_insert (app_table, app, app_uris);
+        }
+        else
+        {
+            *unhandled_uris = g_list_prepend (*unhandled_uris, uri);
+        }
+        nautilus_file_unref (file);
+    }
+
+    g_hash_table_foreach (app_table,
+                          (GHFunc) list_to_parameters_foreach,
+                          &ret);
+
+    g_hash_table_destroy (app_table);
+
+    *unhandled_uris = g_list_reverse (*unhandled_uris);
+
+    return g_list_reverse (ret);
+}
+
 static gboolean
 file_was_cancelled (NautilusFile *file)
 {
@@ -736,9 +871,8 @@ activation_parameters_free (ActivateParameters *parameters)
 }
 
 static void
-application_launch_parameters_free (ApplicationLaunchParameters *parameters)
+application_launch_async_parameters_free (ApplicationLaunchAsyncParameters *parameters)
 {
-    g_queue_free (parameters->unhandled_uris);
     g_queue_free (parameters->uris);
     activation_parameters_free (parameters->activation_params);
 
@@ -1262,25 +1396,23 @@ out:
 }
 
 static void
-on_launch_default_for_uri (GObject      *source_object,
-                           GAsyncResult *res,
-                           gpointer      user_data)
+launch_default_for_uris_callback (GObject      *source_object,
+                                  GAsyncResult *res,
+                                  gpointer      user_data)
 {
-    ApplicationLaunchParameters *params;
+    ApplicationLaunchAsyncParameters *params;
     ActivateParameters *activation_params;
     char *uri;
-    gboolean sandboxed;
-    GError *error = NULL;
+    g_autoptr (GError) error = NULL;
 
     params = user_data;
     activation_params = params->activation_params;
     uri = g_queue_pop_head (params->uris);
-    sandboxed = g_file_test ("/.flatpak-info", G_FILE_TEST_EXISTS);
 
     nautilus_launch_default_for_uri_finish (res, &error);
-    if (!sandboxed && error != NULL && error->code != G_IO_ERROR_CANCELLED)
+    if (error == NULL)
     {
-        g_queue_push_tail (params->unhandled_uris, uri);
+        gtk_recent_manager_add_item (gtk_recent_manager_get_default (), uri);
     }
 
     if (!g_queue_is_empty (params->uris))
@@ -1288,17 +1420,12 @@ on_launch_default_for_uri (GObject      *source_object,
         nautilus_launch_default_for_uri_async (g_queue_peek_head (params->uris),
                                                activation_params->parent_window,
                                                activation_params->cancellable,
-                                               on_launch_default_for_uri,
+                                               launch_default_for_uris_callback,
                                                params);
     }
     else
     {
-        while ((uri = g_queue_pop_head (params->unhandled_uris)) != NULL)
-        {
-            application_unhandled_uri (activation_params, uri);
-        }
-
-        application_launch_parameters_free (params);
+        application_launch_async_parameters_free (params);
     }
 }
 
@@ -1307,9 +1434,16 @@ activate_files (ActivateParameters *parameters)
 {
     NautilusFile *file;
     NautilusWindowOpenFlags flags;
+    g_autoptr (GList) open_in_app_parameters = NULL;
+    g_autoptr (GList) unhandled_open_in_app_uris = NULL;
+    ApplicationLaunchParameters *one_parameters;
     int count;
     g_autofree char *old_working_dir = NULL;
     GdkScreen *screen;
+    gint num_apps;
+    gint num_unhandled;
+    gint num_files;
+    gboolean open_files;
     g_autoptr (GQueue) launch_files = NULL;
     g_autoptr (GQueue) launch_in_terminal_files = NULL;
     g_autoptr (GQueue) open_in_app_uris = NULL;
@@ -1489,26 +1623,87 @@ activate_files (ActivateParameters *parameters)
         }
     }
 
-    if (g_queue_is_empty (open_in_app_uris))
-    {
-        activation_parameters_free (parameters);
-    }
-    else
+    if (!g_queue_is_empty (open_in_app_uris) && is_sandboxed ())
     {
         const char *uri;
-        ApplicationLaunchParameters *params;
+        ApplicationLaunchAsyncParameters *async_params;
 
         uri = g_queue_peek_head (open_in_app_uris);
-        params = application_launch_parameters_new (parameters,
-                                                    g_queue_copy (open_in_app_uris));
 
-        gtk_recent_manager_add_item (gtk_recent_manager_get_default (), uri);
+        async_params = g_new0 (ApplicationLaunchAsyncParameters, 1);
+        async_params->activation_params = parameters;
+        async_params->uris = g_steal_pointer (&open_in_app_uris);
+
         nautilus_launch_default_for_uri_async (uri,
                                                parameters->parent_window,
                                                parameters->cancellable,
-                                               on_launch_default_for_uri,
-                                               params);
+                                               launch_default_for_uris_callback,
+                                               async_params);
+        return;
     }
+
+    if (open_in_app_uris != NULL)
+    {
+        open_in_app_parameters = make_activation_parameters (g_queue_peek_head_link (open_in_app_uris),
+                                                             &unhandled_open_in_app_uris);
+    }
+
+    num_apps = g_list_length (open_in_app_parameters);
+    num_unhandled = g_list_length (unhandled_open_in_app_uris);
+    num_files = g_queue_get_length (open_in_app_uris);
+    open_files = TRUE;
+
+    if (!g_queue_is_empty (open_in_app_uris) &&
+        (!parameters->user_confirmation ||
+         num_files + num_unhandled > SILENT_OPEN_LIMIT) &&
+        num_apps > 1)
+    {
+        GtkDialog *dialog;
+        char *prompt;
+        g_autofree char *detail = NULL;
+        int response;
+
+        pause_activation_timed_cancel (parameters);
+
+        prompt = _("Are you sure you want to open all files?");
+        detail = g_strdup_printf (ngettext ("This will open %d separate application.",
+                                            "This will open %d separate applications.", num_apps), num_apps);
+        dialog = eel_show_yes_no_dialog (prompt, detail,
+                                         _("_OK"), _("_Cancel"),
+                                         parameters->parent_window);
+        response = gtk_dialog_run (dialog);
+        gtk_widget_destroy (GTK_WIDGET (dialog));
+
+        unpause_activation_timed_cancel (parameters);
+
+        if (response != GTK_RESPONSE_YES)
+        {
+            open_files = FALSE;
+        }
+    }
+
+    if (open_files)
+    {
+        for (l = open_in_app_parameters; l != NULL; l = l->next)
+        {
+            one_parameters = l->data;
+
+            nautilus_launch_application_by_uri (one_parameters->application,
+                                                one_parameters->uris,
+                                                parameters->parent_window);
+            application_launch_parameters_free (one_parameters);
+        }
+
+        for (l = unhandled_open_in_app_uris; l != NULL; l = l->next)
+        {
+            char *uri = l->data;
+
+            /* this does not block */
+            application_unhandled_uri (parameters, uri);
+        }
+    }
+
+    activation_parameters_free (parameters);
 }
 
 static void

Reply via email to