mirror of
				https://gitlab.gnome.org/GNOME/glib.git
				synced 2025-10-31 16:32:18 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			511 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			511 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| /* GIO - GLib Input, Output and Streaming Library
 | |
|  * 
 | |
|  * Copyright (C) 2006-2007 Red Hat, Inc.
 | |
|  *
 | |
|  * This library is free software; you can redistribute it and/or
 | |
|  * modify it under the terms of the GNU Lesser General Public
 | |
|  * License as published by the Free Software Foundation; either
 | |
|  * version 2.1 of the License, or (at your option) any later version.
 | |
|  *
 | |
|  * This library is distributed in the hope that it will be useful,
 | |
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 | |
|  * Lesser General Public License for more details.
 | |
|  *
 | |
|  * You should have received a copy of the GNU Lesser General
 | |
|  * Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
 | |
|  *
 | |
|  * Author: Alexander Larsson <alexl@redhat.com>
 | |
|  */
 | |
| 
 | |
| #include "config.h"
 | |
| #include "gfilenamecompleter.h"
 | |
| #include "gfileenumerator.h"
 | |
| #include "gfileattribute.h"
 | |
| #include "gfile.h"
 | |
| #include "gfileinfo.h"
 | |
| #include "gcancellable.h"
 | |
| #include <string.h>
 | |
| #include "glibintl.h"
 | |
| 
 | |
| 
 | |
| /**
 | |
|  * SECTION:gfilenamecompleter
 | |
|  * @short_description: Filename Completer
 | |
|  * @include: gio/gio.h
 | |
|  * 
 | |
|  * Completes partial file and directory names given a partial string by
 | |
|  * looking in the file system for clues. Can return a list of possible 
 | |
|  * completion strings for widget implementations.
 | |
|  * 
 | |
|  **/
 | |
| 
 | |
| enum {
 | |
|   GOT_COMPLETION_DATA,
 | |
|   LAST_SIGNAL
 | |
| };
 | |
| 
 | |
| static guint signals[LAST_SIGNAL] = { 0 };
 | |
| 
 | |
| typedef struct {
 | |
|   GFilenameCompleter *completer;
 | |
|   GFileEnumerator *enumerator;
 | |
|   GCancellable *cancellable;
 | |
|   gboolean should_escape;
 | |
|   GFile *dir;
 | |
|   GList *basenames;
 | |
|   gboolean dirs_only;
 | |
| } LoadBasenamesData;
 | |
| 
 | |
| struct _GFilenameCompleter {
 | |
|   GObject parent;
 | |
| 
 | |
|   GFile *basenames_dir;
 | |
|   gboolean basenames_are_escaped;
 | |
|   gboolean dirs_only;
 | |
|   GList *basenames;
 | |
| 
 | |
|   LoadBasenamesData *basename_loader;
 | |
| };
 | |
| 
 | |
| G_DEFINE_TYPE (GFilenameCompleter, g_filename_completer, G_TYPE_OBJECT)
 | |
| 
 | |
| static void cancel_load_basenames (GFilenameCompleter *completer);
 | |
| 
 | |
| static void
 | |
| g_filename_completer_finalize (GObject *object)
 | |
| {
 | |
|   GFilenameCompleter *completer;
 | |
| 
 | |
|   completer = G_FILENAME_COMPLETER (object);
 | |
| 
 | |
|   cancel_load_basenames (completer);
 | |
| 
 | |
|   if (completer->basenames_dir)
 | |
|     g_object_unref (completer->basenames_dir);
 | |
| 
 | |
|   g_list_free_full (completer->basenames, g_free);
 | |
| 
 | |
|   G_OBJECT_CLASS (g_filename_completer_parent_class)->finalize (object);
 | |
| }
 | |
| 
 | |
| static void
 | |
| g_filename_completer_class_init (GFilenameCompleterClass *klass)
 | |
| {
 | |
|   GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
 | |
|   
 | |
|   gobject_class->finalize = g_filename_completer_finalize;
 | |
|   /**
 | |
|    * GFilenameCompleter::got-completion-data:
 | |
|    * 
 | |
|    * Emitted when the file name completion information comes available.
 | |
|    **/
 | |
|   signals[GOT_COMPLETION_DATA] = g_signal_new (I_("got-completion-data"),
 | |
| 					  G_TYPE_FILENAME_COMPLETER,
 | |
| 					  G_SIGNAL_RUN_LAST,
 | |
| 					  G_STRUCT_OFFSET (GFilenameCompleterClass, got_completion_data),
 | |
| 					  NULL, NULL,
 | |
| 					  NULL,
 | |
| 					  G_TYPE_NONE, 0);
 | |
| }
 | |
| 
 | |
| static void
 | |
| g_filename_completer_init (GFilenameCompleter *completer)
 | |
| {
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * g_filename_completer_new:
 | |
|  * 
 | |
|  * Creates a new filename completer.
 | |
|  * 
 | |
|  * Returns: a #GFilenameCompleter.
 | |
|  **/
 | |
| GFilenameCompleter *
 | |
| g_filename_completer_new (void)
 | |
| {
 | |
|   return g_object_new (G_TYPE_FILENAME_COMPLETER, NULL);
 | |
| }
 | |
| 
 | |
| static char *
 | |
| longest_common_prefix (char *a, char *b)
 | |
| {
 | |
|   char *start;
 | |
| 
 | |
|   start = a;
 | |
| 
 | |
|   while (g_utf8_get_char (a) == g_utf8_get_char (b))
 | |
|     {
 | |
|       a = g_utf8_next_char (a);
 | |
|       b = g_utf8_next_char (b);
 | |
|     }
 | |
| 
 | |
|   return g_strndup (start, a - start);
 | |
| }
 | |
| 
 | |
| static void
 | |
| load_basenames_data_free (LoadBasenamesData *data)
 | |
| {
 | |
|   if (data->enumerator)
 | |
|     g_object_unref (data->enumerator);
 | |
|   
 | |
|   g_object_unref (data->cancellable);
 | |
|   g_object_unref (data->dir);
 | |
|   
 | |
|   g_list_free_full (data->basenames, g_free);
 | |
|   
 | |
|   g_free (data);
 | |
| }
 | |
| 
 | |
| static void
 | |
| got_more_files (GObject *source_object,
 | |
| 		GAsyncResult *res,
 | |
| 		gpointer user_data)
 | |
| {
 | |
|   LoadBasenamesData *data = user_data;
 | |
|   GList *infos, *l;
 | |
|   GFileInfo *info;
 | |
|   const char *name;
 | |
|   gboolean append_slash;
 | |
|   char *t;
 | |
|   char *basename;
 | |
| 
 | |
|   if (data->completer == NULL)
 | |
|     {
 | |
|       /* Was cancelled */
 | |
|       load_basenames_data_free (data);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|   infos = g_file_enumerator_next_files_finish (data->enumerator, res, NULL);
 | |
| 
 | |
|   for (l = infos; l != NULL; l = l->next)
 | |
|     {
 | |
|       info = l->data;
 | |
| 
 | |
|       if (data->dirs_only &&
 | |
| 	  g_file_info_get_file_type (info) != G_FILE_TYPE_DIRECTORY)
 | |
| 	{
 | |
| 	  g_object_unref (info);
 | |
| 	  continue;
 | |
| 	}
 | |
|       
 | |
|       append_slash = g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY;
 | |
|       name = g_file_info_get_name (info);
 | |
|       if (name == NULL)
 | |
| 	{
 | |
| 	  g_object_unref (info);
 | |
| 	  continue;
 | |
| 	}
 | |
| 
 | |
|       
 | |
|       if (data->should_escape)
 | |
| 	basename = g_uri_escape_string (name,
 | |
| 					G_URI_RESERVED_CHARS_ALLOWED_IN_PATH,
 | |
| 					TRUE);
 | |
|       else
 | |
| 	/* If not should_escape, must be a local filename, convert to utf8 */
 | |
| 	basename = g_filename_to_utf8 (name, -1, NULL, NULL, NULL);
 | |
|       
 | |
|       if (basename)
 | |
| 	{
 | |
| 	  if (append_slash)
 | |
| 	    {
 | |
| 	      t = basename;
 | |
| 	      basename = g_strconcat (basename, "/", NULL);
 | |
| 	      g_free (t);
 | |
| 	    }
 | |
| 	  
 | |
| 	  data->basenames = g_list_prepend (data->basenames, basename);
 | |
| 	}
 | |
|       
 | |
|       g_object_unref (info);
 | |
|     }
 | |
|   
 | |
|   g_list_free (infos);
 | |
|   
 | |
|   if (infos)
 | |
|     {
 | |
|       /* Not last, get more files */
 | |
|       g_file_enumerator_next_files_async (data->enumerator,
 | |
| 					  100,
 | |
| 					  0,
 | |
| 					  data->cancellable,
 | |
| 					  got_more_files, data);
 | |
|     }
 | |
|   else
 | |
|     {
 | |
|       data->completer->basename_loader = NULL;
 | |
|       
 | |
|       if (data->completer->basenames_dir)
 | |
| 	g_object_unref (data->completer->basenames_dir);
 | |
|       g_list_free_full (data->completer->basenames, g_free);
 | |
|       
 | |
|       data->completer->basenames_dir = g_object_ref (data->dir);
 | |
|       data->completer->basenames = data->basenames;
 | |
|       data->completer->basenames_are_escaped = data->should_escape;
 | |
|       data->basenames = NULL;
 | |
|       
 | |
|       g_file_enumerator_close_async (data->enumerator, 0, NULL, NULL, NULL);
 | |
| 
 | |
|       g_signal_emit (data->completer, signals[GOT_COMPLETION_DATA], 0);
 | |
|       load_basenames_data_free (data);
 | |
|     }
 | |
| }
 | |
| 
 | |
| 
 | |
| static void
 | |
| got_enum (GObject *source_object,
 | |
| 	  GAsyncResult *res,
 | |
| 	  gpointer user_data)
 | |
| {
 | |
|   LoadBasenamesData *data = user_data;
 | |
| 
 | |
|   if (data->completer == NULL)
 | |
|     {
 | |
|       /* Was cancelled */
 | |
|       load_basenames_data_free (data);
 | |
|       return;
 | |
|     }
 | |
|   
 | |
|   data->enumerator = g_file_enumerate_children_finish (G_FILE (source_object), res, NULL);
 | |
|   
 | |
|   if (data->enumerator == NULL)
 | |
|     {
 | |
|       data->completer->basename_loader = NULL;
 | |
| 
 | |
|       if (data->completer->basenames_dir)
 | |
| 	g_object_unref (data->completer->basenames_dir);
 | |
|       g_list_free_full (data->completer->basenames, g_free);
 | |
| 
 | |
|       /* Mark up-to-date with no basenames */
 | |
|       data->completer->basenames_dir = g_object_ref (data->dir);
 | |
|       data->completer->basenames = NULL;
 | |
|       data->completer->basenames_are_escaped = data->should_escape;
 | |
|       
 | |
|       load_basenames_data_free (data);
 | |
|       return;
 | |
|     }
 | |
|   
 | |
|   g_file_enumerator_next_files_async (data->enumerator,
 | |
| 				      100,
 | |
| 				      0,
 | |
| 				      data->cancellable,
 | |
| 				      got_more_files, data);
 | |
| }
 | |
| 
 | |
| static void
 | |
| schedule_load_basenames (GFilenameCompleter *completer,
 | |
| 			 GFile *dir,
 | |
| 			 gboolean should_escape)
 | |
| {
 | |
|   LoadBasenamesData *data;
 | |
| 
 | |
|   cancel_load_basenames (completer);
 | |
| 
 | |
|   data = g_new0 (LoadBasenamesData, 1);
 | |
|   data->completer = completer;
 | |
|   data->cancellable = g_cancellable_new ();
 | |
|   data->dir = g_object_ref (dir);
 | |
|   data->should_escape = should_escape;
 | |
|   data->dirs_only = completer->dirs_only;
 | |
| 
 | |
|   completer->basename_loader = data;
 | |
|   
 | |
|   g_file_enumerate_children_async (dir,
 | |
| 				   G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE,
 | |
| 				   0, 0,
 | |
| 				   data->cancellable,
 | |
| 				   got_enum, data);
 | |
| }
 | |
| 
 | |
| static void
 | |
| cancel_load_basenames (GFilenameCompleter *completer)
 | |
| {
 | |
|   LoadBasenamesData *loader;
 | |
|   
 | |
|   if (completer->basename_loader)
 | |
|     {
 | |
|       loader = completer->basename_loader; 
 | |
|       loader->completer = NULL;
 | |
|       
 | |
|       g_cancellable_cancel (loader->cancellable);
 | |
|       
 | |
|       completer->basename_loader = NULL;
 | |
|     }
 | |
| }
 | |
| 
 | |
| 
 | |
| /* Returns a list of possible matches and the basename to use for it */
 | |
| static GList *
 | |
| init_completion (GFilenameCompleter *completer,
 | |
| 		 const char *initial_text,
 | |
| 		 char **basename_out)
 | |
| {
 | |
|   gboolean should_escape;
 | |
|   GFile *file, *parent;
 | |
|   char *basename;
 | |
|   char *t;
 | |
|   int len;
 | |
| 
 | |
|   *basename_out = NULL;
 | |
|   
 | |
|   should_escape = ! (g_path_is_absolute (initial_text) || *initial_text == '~');
 | |
| 
 | |
|   len = strlen (initial_text);
 | |
|   
 | |
|   if (len > 0 &&
 | |
|       initial_text[len - 1] == '/')
 | |
|     return NULL;
 | |
|   
 | |
|   file = g_file_parse_name (initial_text);
 | |
|   parent = g_file_get_parent (file);
 | |
|   if (parent == NULL)
 | |
|     {
 | |
|       g_object_unref (file);
 | |
|       return NULL;
 | |
|     }
 | |
| 
 | |
|   if (completer->basenames_dir == NULL ||
 | |
|       completer->basenames_are_escaped != should_escape ||
 | |
|       !g_file_equal (parent, completer->basenames_dir))
 | |
|     {
 | |
|       schedule_load_basenames (completer, parent, should_escape);
 | |
|       g_object_unref (file);
 | |
|       return NULL;
 | |
|     }
 | |
|   
 | |
|   basename = g_file_get_basename (file);
 | |
|   if (should_escape)
 | |
|     {
 | |
|       t = basename;
 | |
|       basename = g_uri_escape_string (basename, G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, TRUE);
 | |
|       g_free (t);
 | |
|     }
 | |
|   else
 | |
|     {
 | |
|       t = basename;
 | |
|       basename = g_filename_to_utf8 (basename, -1, NULL, NULL, NULL);
 | |
|       g_free (t);
 | |
|       
 | |
|       if (basename == NULL)
 | |
| 	return NULL;
 | |
|     }
 | |
| 
 | |
|   *basename_out = basename;
 | |
| 
 | |
|   return completer->basenames;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * g_filename_completer_get_completion_suffix:
 | |
|  * @completer: the filename completer.
 | |
|  * @initial_text: text to be completed.
 | |
|  *
 | |
|  * Obtains a completion for @initial_text from @completer.
 | |
|  *  
 | |
|  * Returns: (nullable) (transfer full): a completed string, or %NULL if no
 | |
|  *     completion exists. This string is not owned by GIO, so remember to g_free()
 | |
|  *     it when finished.
 | |
|  **/
 | |
| char *
 | |
| g_filename_completer_get_completion_suffix (GFilenameCompleter *completer,
 | |
| 					    const char *initial_text)
 | |
| {
 | |
|   GList *possible_matches, *l;
 | |
|   char *prefix;
 | |
|   char *suffix;
 | |
|   char *possible_match;
 | |
|   char *lcp;
 | |
| 
 | |
|   g_return_val_if_fail (G_IS_FILENAME_COMPLETER (completer), NULL);
 | |
|   g_return_val_if_fail (initial_text != NULL, NULL);
 | |
| 
 | |
|   possible_matches = init_completion (completer, initial_text, &prefix);
 | |
| 
 | |
|   suffix = NULL;
 | |
|   
 | |
|   for (l = possible_matches; l != NULL; l = l->next)
 | |
|     {
 | |
|       possible_match = l->data;
 | |
|       
 | |
|       if (g_str_has_prefix (possible_match, prefix))
 | |
| 	{
 | |
| 	  if (suffix == NULL)
 | |
| 	    suffix = g_strdup (possible_match + strlen (prefix));
 | |
| 	  else
 | |
| 	    {
 | |
| 	      lcp = longest_common_prefix (suffix,
 | |
| 					   possible_match + strlen (prefix));
 | |
| 	      g_free (suffix);
 | |
| 	      suffix = lcp;
 | |
| 	      
 | |
| 	      if (*suffix == 0)
 | |
| 		break;
 | |
| 	    }
 | |
| 	}
 | |
|     }
 | |
| 
 | |
|   g_free (prefix);
 | |
|   
 | |
|   return suffix;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * g_filename_completer_get_completions:
 | |
|  * @completer: the filename completer.
 | |
|  * @initial_text: text to be completed.
 | |
|  * 
 | |
|  * Gets an array of completion strings for a given initial text.
 | |
|  * 
 | |
|  * Returns: (array zero-terminated=1) (transfer full): array of strings with possible completions for @initial_text.
 | |
|  * This array must be freed by g_strfreev() when finished. 
 | |
|  **/
 | |
| char **
 | |
| g_filename_completer_get_completions (GFilenameCompleter *completer,
 | |
| 				      const char         *initial_text)
 | |
| {
 | |
|   GList *possible_matches, *l;
 | |
|   char *prefix;
 | |
|   char *possible_match;
 | |
|   GPtrArray *res;
 | |
| 
 | |
|   g_return_val_if_fail (G_IS_FILENAME_COMPLETER (completer), NULL);
 | |
|   g_return_val_if_fail (initial_text != NULL, NULL);
 | |
| 
 | |
|   possible_matches = init_completion (completer, initial_text, &prefix);
 | |
| 
 | |
|   res = g_ptr_array_new ();
 | |
|   for (l = possible_matches; l != NULL; l = l->next)
 | |
|     {
 | |
|       possible_match = l->data;
 | |
| 
 | |
|       if (g_str_has_prefix (possible_match, prefix))
 | |
| 	g_ptr_array_add (res,
 | |
| 			 g_strconcat (initial_text, possible_match + strlen (prefix), NULL));
 | |
|     }
 | |
| 
 | |
|   g_free (prefix);
 | |
| 
 | |
|   g_ptr_array_add (res, NULL);
 | |
| 
 | |
|   return (char**)g_ptr_array_free (res, FALSE);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * g_filename_completer_set_dirs_only:
 | |
|  * @completer: the filename completer.
 | |
|  * @dirs_only: a #gboolean.
 | |
|  * 
 | |
|  * If @dirs_only is %TRUE, @completer will only 
 | |
|  * complete directory names, and not file names.
 | |
|  **/
 | |
| void
 | |
| g_filename_completer_set_dirs_only (GFilenameCompleter *completer,
 | |
| 				    gboolean dirs_only)
 | |
| {
 | |
|   g_return_if_fail (G_IS_FILENAME_COMPLETER (completer));
 | |
| 
 | |
|   completer->dirs_only = dirs_only;
 | |
| }
 |