1 | /* GIO - GLib Input, Output and Streaming Library |
2 | * |
3 | * Copyright (C) 2006-2007 Red Hat, Inc. |
4 | * |
5 | * This library is free software; you can redistribute it and/or |
6 | * modify it under the terms of the GNU Lesser General Public |
7 | * License as published by the Free Software Foundation; either |
8 | * version 2.1 of the License, or (at your option) any later version. |
9 | * |
10 | * This library is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
13 | * Lesser General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU Lesser General |
16 | * Public License along with this library; if not, see <http://www.gnu.org/licenses/>. |
17 | * |
18 | * Author: Alexander Larsson <alexl@redhat.com> |
19 | */ |
20 | |
21 | #include "config.h" |
22 | #include "gfilenamecompleter.h" |
23 | #include "gfileenumerator.h" |
24 | #include "gfileattribute.h" |
25 | #include "gfile.h" |
26 | #include "gfileinfo.h" |
27 | #include "gcancellable.h" |
28 | #include <string.h> |
29 | #include "glibintl.h" |
30 | |
31 | |
32 | /** |
33 | * SECTION:gfilenamecompleter |
34 | * @short_description: Filename Completer |
35 | * @include: gio/gio.h |
36 | * |
37 | * Completes partial file and directory names given a partial string by |
38 | * looking in the file system for clues. Can return a list of possible |
39 | * completion strings for widget implementations. |
40 | * |
41 | **/ |
42 | |
43 | enum { |
44 | GOT_COMPLETION_DATA, |
45 | LAST_SIGNAL |
46 | }; |
47 | |
48 | static guint signals[LAST_SIGNAL] = { 0 }; |
49 | |
50 | typedef struct { |
51 | GFilenameCompleter *completer; |
52 | GFileEnumerator *enumerator; |
53 | GCancellable *cancellable; |
54 | gboolean should_escape; |
55 | GFile *dir; |
56 | GList *basenames; |
57 | gboolean dirs_only; |
58 | } LoadBasenamesData; |
59 | |
60 | struct _GFilenameCompleter { |
61 | GObject parent; |
62 | |
63 | GFile *basenames_dir; |
64 | gboolean basenames_are_escaped; |
65 | gboolean dirs_only; |
66 | GList *basenames; |
67 | |
68 | LoadBasenamesData *basename_loader; |
69 | }; |
70 | |
71 | G_DEFINE_TYPE (GFilenameCompleter, g_filename_completer, G_TYPE_OBJECT) |
72 | |
73 | static void cancel_load_basenames (GFilenameCompleter *completer); |
74 | |
75 | static void |
76 | g_filename_completer_finalize (GObject *object) |
77 | { |
78 | GFilenameCompleter *completer; |
79 | |
80 | completer = G_FILENAME_COMPLETER (object); |
81 | |
82 | cancel_load_basenames (completer); |
83 | |
84 | if (completer->basenames_dir) |
85 | g_object_unref (object: completer->basenames_dir); |
86 | |
87 | g_list_free_full (list: completer->basenames, free_func: g_free); |
88 | |
89 | G_OBJECT_CLASS (g_filename_completer_parent_class)->finalize (object); |
90 | } |
91 | |
92 | static void |
93 | g_filename_completer_class_init (GFilenameCompleterClass *klass) |
94 | { |
95 | GObjectClass *gobject_class = G_OBJECT_CLASS (klass); |
96 | |
97 | gobject_class->finalize = g_filename_completer_finalize; |
98 | /** |
99 | * GFilenameCompleter::got-completion-data: |
100 | * |
101 | * Emitted when the file name completion information comes available. |
102 | **/ |
103 | signals[GOT_COMPLETION_DATA] = g_signal_new (I_("got-completion-data" ), |
104 | G_TYPE_FILENAME_COMPLETER, |
105 | signal_flags: G_SIGNAL_RUN_LAST, |
106 | G_STRUCT_OFFSET (GFilenameCompleterClass, got_completion_data), |
107 | NULL, NULL, |
108 | NULL, |
109 | G_TYPE_NONE, n_params: 0); |
110 | } |
111 | |
112 | static void |
113 | g_filename_completer_init (GFilenameCompleter *completer) |
114 | { |
115 | } |
116 | |
117 | /** |
118 | * g_filename_completer_new: |
119 | * |
120 | * Creates a new filename completer. |
121 | * |
122 | * Returns: a #GFilenameCompleter. |
123 | **/ |
124 | GFilenameCompleter * |
125 | g_filename_completer_new (void) |
126 | { |
127 | return g_object_new (G_TYPE_FILENAME_COMPLETER, NULL); |
128 | } |
129 | |
130 | static char * |
131 | longest_common_prefix (char *a, char *b) |
132 | { |
133 | char *start; |
134 | |
135 | start = a; |
136 | |
137 | while (g_utf8_get_char (p: a) == g_utf8_get_char (p: b)) |
138 | { |
139 | a = g_utf8_next_char (a); |
140 | b = g_utf8_next_char (b); |
141 | } |
142 | |
143 | return g_strndup (str: start, n: a - start); |
144 | } |
145 | |
146 | static void |
147 | load_basenames_data_free (LoadBasenamesData *data) |
148 | { |
149 | if (data->enumerator) |
150 | g_object_unref (object: data->enumerator); |
151 | |
152 | g_object_unref (object: data->cancellable); |
153 | g_object_unref (object: data->dir); |
154 | |
155 | g_list_free_full (list: data->basenames, free_func: g_free); |
156 | |
157 | g_free (mem: data); |
158 | } |
159 | |
160 | static void |
161 | got_more_files (GObject *source_object, |
162 | GAsyncResult *res, |
163 | gpointer user_data) |
164 | { |
165 | LoadBasenamesData *data = user_data; |
166 | GList *infos, *l; |
167 | GFileInfo *info; |
168 | const char *name; |
169 | gboolean append_slash; |
170 | char *t; |
171 | char *basename; |
172 | |
173 | if (data->completer == NULL) |
174 | { |
175 | /* Was cancelled */ |
176 | load_basenames_data_free (data); |
177 | return; |
178 | } |
179 | |
180 | infos = g_file_enumerator_next_files_finish (enumerator: data->enumerator, result: res, NULL); |
181 | |
182 | for (l = infos; l != NULL; l = l->next) |
183 | { |
184 | info = l->data; |
185 | |
186 | if (data->dirs_only && |
187 | g_file_info_get_file_type (info) != G_FILE_TYPE_DIRECTORY) |
188 | { |
189 | g_object_unref (object: info); |
190 | continue; |
191 | } |
192 | |
193 | append_slash = g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY; |
194 | name = g_file_info_get_name (info); |
195 | if (name == NULL) |
196 | { |
197 | g_object_unref (object: info); |
198 | continue; |
199 | } |
200 | |
201 | |
202 | if (data->should_escape) |
203 | basename = g_uri_escape_string (unescaped: name, |
204 | G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, |
205 | TRUE); |
206 | else |
207 | /* If not should_escape, must be a local filename, convert to utf8 */ |
208 | basename = g_filename_to_utf8 (opsysstring: name, len: -1, NULL, NULL, NULL); |
209 | |
210 | if (basename) |
211 | { |
212 | if (append_slash) |
213 | { |
214 | t = basename; |
215 | basename = g_strconcat (string1: basename, "/" , NULL); |
216 | g_free (mem: t); |
217 | } |
218 | |
219 | data->basenames = g_list_prepend (list: data->basenames, data: basename); |
220 | } |
221 | |
222 | g_object_unref (object: info); |
223 | } |
224 | |
225 | g_list_free (list: infos); |
226 | |
227 | if (infos) |
228 | { |
229 | /* Not last, get more files */ |
230 | g_file_enumerator_next_files_async (enumerator: data->enumerator, |
231 | num_files: 100, |
232 | io_priority: 0, |
233 | cancellable: data->cancellable, |
234 | callback: got_more_files, user_data: data); |
235 | } |
236 | else |
237 | { |
238 | data->completer->basename_loader = NULL; |
239 | |
240 | if (data->completer->basenames_dir) |
241 | g_object_unref (object: data->completer->basenames_dir); |
242 | g_list_free_full (list: data->completer->basenames, free_func: g_free); |
243 | |
244 | data->completer->basenames_dir = g_object_ref (data->dir); |
245 | data->completer->basenames = data->basenames; |
246 | data->completer->basenames_are_escaped = data->should_escape; |
247 | data->basenames = NULL; |
248 | |
249 | g_file_enumerator_close_async (enumerator: data->enumerator, io_priority: 0, NULL, NULL, NULL); |
250 | |
251 | g_signal_emit (instance: data->completer, signal_id: signals[GOT_COMPLETION_DATA], detail: 0); |
252 | load_basenames_data_free (data); |
253 | } |
254 | } |
255 | |
256 | |
257 | static void |
258 | got_enum (GObject *source_object, |
259 | GAsyncResult *res, |
260 | gpointer user_data) |
261 | { |
262 | LoadBasenamesData *data = user_data; |
263 | |
264 | if (data->completer == NULL) |
265 | { |
266 | /* Was cancelled */ |
267 | load_basenames_data_free (data); |
268 | return; |
269 | } |
270 | |
271 | data->enumerator = g_file_enumerate_children_finish (G_FILE (source_object), res, NULL); |
272 | |
273 | if (data->enumerator == NULL) |
274 | { |
275 | data->completer->basename_loader = NULL; |
276 | |
277 | if (data->completer->basenames_dir) |
278 | g_object_unref (object: data->completer->basenames_dir); |
279 | g_list_free_full (list: data->completer->basenames, free_func: g_free); |
280 | |
281 | /* Mark up-to-date with no basenames */ |
282 | data->completer->basenames_dir = g_object_ref (data->dir); |
283 | data->completer->basenames = NULL; |
284 | data->completer->basenames_are_escaped = data->should_escape; |
285 | |
286 | load_basenames_data_free (data); |
287 | return; |
288 | } |
289 | |
290 | g_file_enumerator_next_files_async (enumerator: data->enumerator, |
291 | num_files: 100, |
292 | io_priority: 0, |
293 | cancellable: data->cancellable, |
294 | callback: got_more_files, user_data: data); |
295 | } |
296 | |
297 | static void |
298 | schedule_load_basenames (GFilenameCompleter *completer, |
299 | GFile *dir, |
300 | gboolean should_escape) |
301 | { |
302 | LoadBasenamesData *data; |
303 | |
304 | cancel_load_basenames (completer); |
305 | |
306 | data = g_new0 (LoadBasenamesData, 1); |
307 | data->completer = completer; |
308 | data->cancellable = g_cancellable_new (); |
309 | data->dir = g_object_ref (dir); |
310 | data->should_escape = should_escape; |
311 | data->dirs_only = completer->dirs_only; |
312 | |
313 | completer->basename_loader = data; |
314 | |
315 | g_file_enumerate_children_async (file: dir, |
316 | G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE, |
317 | flags: 0, io_priority: 0, |
318 | cancellable: data->cancellable, |
319 | callback: got_enum, user_data: data); |
320 | } |
321 | |
322 | static void |
323 | cancel_load_basenames (GFilenameCompleter *completer) |
324 | { |
325 | LoadBasenamesData *loader; |
326 | |
327 | if (completer->basename_loader) |
328 | { |
329 | loader = completer->basename_loader; |
330 | loader->completer = NULL; |
331 | |
332 | g_cancellable_cancel (cancellable: loader->cancellable); |
333 | |
334 | completer->basename_loader = NULL; |
335 | } |
336 | } |
337 | |
338 | |
339 | /* Returns a list of possible matches and the basename to use for it */ |
340 | static GList * |
341 | init_completion (GFilenameCompleter *completer, |
342 | const char *initial_text, |
343 | char **basename_out) |
344 | { |
345 | gboolean should_escape; |
346 | GFile *file, *parent; |
347 | char *basename; |
348 | char *t; |
349 | int len; |
350 | |
351 | *basename_out = NULL; |
352 | |
353 | should_escape = ! (g_path_is_absolute (file_name: initial_text) || *initial_text == '~'); |
354 | |
355 | len = strlen (s: initial_text); |
356 | |
357 | if (len > 0 && |
358 | initial_text[len - 1] == '/') |
359 | return NULL; |
360 | |
361 | file = g_file_parse_name (parse_name: initial_text); |
362 | parent = g_file_get_parent (file); |
363 | if (parent == NULL) |
364 | { |
365 | g_object_unref (object: file); |
366 | return NULL; |
367 | } |
368 | |
369 | if (completer->basenames_dir == NULL || |
370 | completer->basenames_are_escaped != should_escape || |
371 | !g_file_equal (file1: parent, file2: completer->basenames_dir)) |
372 | { |
373 | schedule_load_basenames (completer, dir: parent, should_escape); |
374 | g_object_unref (object: file); |
375 | return NULL; |
376 | } |
377 | |
378 | basename = g_file_get_basename (file); |
379 | if (should_escape) |
380 | { |
381 | t = basename; |
382 | basename = g_uri_escape_string (unescaped: basename, G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, TRUE); |
383 | g_free (mem: t); |
384 | } |
385 | else |
386 | { |
387 | t = basename; |
388 | basename = g_filename_to_utf8 (opsysstring: basename, len: -1, NULL, NULL, NULL); |
389 | g_free (mem: t); |
390 | |
391 | if (basename == NULL) |
392 | return NULL; |
393 | } |
394 | |
395 | *basename_out = basename; |
396 | |
397 | return completer->basenames; |
398 | } |
399 | |
400 | /** |
401 | * g_filename_completer_get_completion_suffix: |
402 | * @completer: the filename completer. |
403 | * @initial_text: text to be completed. |
404 | * |
405 | * Obtains a completion for @initial_text from @completer. |
406 | * |
407 | * Returns: (nullable) (transfer full): a completed string, or %NULL if no |
408 | * completion exists. This string is not owned by GIO, so remember to g_free() |
409 | * it when finished. |
410 | **/ |
411 | char * |
412 | g_filename_completer_get_completion_suffix (GFilenameCompleter *completer, |
413 | const char *initial_text) |
414 | { |
415 | GList *possible_matches, *l; |
416 | char *prefix; |
417 | char *suffix; |
418 | char *possible_match; |
419 | char *lcp; |
420 | |
421 | g_return_val_if_fail (G_IS_FILENAME_COMPLETER (completer), NULL); |
422 | g_return_val_if_fail (initial_text != NULL, NULL); |
423 | |
424 | possible_matches = init_completion (completer, initial_text, basename_out: &prefix); |
425 | |
426 | suffix = NULL; |
427 | |
428 | for (l = possible_matches; l != NULL; l = l->next) |
429 | { |
430 | possible_match = l->data; |
431 | |
432 | if (g_str_has_prefix (str: possible_match, prefix)) |
433 | { |
434 | if (suffix == NULL) |
435 | suffix = g_strdup (str: possible_match + strlen (s: prefix)); |
436 | else |
437 | { |
438 | lcp = longest_common_prefix (a: suffix, |
439 | b: possible_match + strlen (s: prefix)); |
440 | g_free (mem: suffix); |
441 | suffix = lcp; |
442 | |
443 | if (*suffix == 0) |
444 | break; |
445 | } |
446 | } |
447 | } |
448 | |
449 | g_free (mem: prefix); |
450 | |
451 | return suffix; |
452 | } |
453 | |
454 | /** |
455 | * g_filename_completer_get_completions: |
456 | * @completer: the filename completer. |
457 | * @initial_text: text to be completed. |
458 | * |
459 | * Gets an array of completion strings for a given initial text. |
460 | * |
461 | * Returns: (array zero-terminated=1) (transfer full): array of strings with possible completions for @initial_text. |
462 | * This array must be freed by g_strfreev() when finished. |
463 | **/ |
464 | char ** |
465 | g_filename_completer_get_completions (GFilenameCompleter *completer, |
466 | const char *initial_text) |
467 | { |
468 | GList *possible_matches, *l; |
469 | char *prefix; |
470 | char *possible_match; |
471 | GPtrArray *res; |
472 | |
473 | g_return_val_if_fail (G_IS_FILENAME_COMPLETER (completer), NULL); |
474 | g_return_val_if_fail (initial_text != NULL, NULL); |
475 | |
476 | possible_matches = init_completion (completer, initial_text, basename_out: &prefix); |
477 | |
478 | res = g_ptr_array_new (); |
479 | for (l = possible_matches; l != NULL; l = l->next) |
480 | { |
481 | possible_match = l->data; |
482 | |
483 | if (g_str_has_prefix (str: possible_match, prefix)) |
484 | g_ptr_array_add (array: res, |
485 | data: g_strconcat (string1: initial_text, possible_match + strlen (s: prefix), NULL)); |
486 | } |
487 | |
488 | g_free (mem: prefix); |
489 | |
490 | g_ptr_array_add (array: res, NULL); |
491 | |
492 | return (char**)g_ptr_array_free (array: res, FALSE); |
493 | } |
494 | |
495 | /** |
496 | * g_filename_completer_set_dirs_only: |
497 | * @completer: the filename completer. |
498 | * @dirs_only: a #gboolean. |
499 | * |
500 | * If @dirs_only is %TRUE, @completer will only |
501 | * complete directory names, and not file names. |
502 | **/ |
503 | void |
504 | g_filename_completer_set_dirs_only (GFilenameCompleter *completer, |
505 | gboolean dirs_only) |
506 | { |
507 | g_return_if_fail (G_IS_FILENAME_COMPLETER (completer)); |
508 | |
509 | completer->dirs_only = dirs_only; |
510 | } |
511 | |