1 | /* GTK - The GIMP Toolkit |
2 | * gtkfilechooserentry.c: Entry with filename completion |
3 | * Copyright (C) 2003, 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 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 Public |
16 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
17 | */ |
18 | |
19 | #include "config.h" |
20 | |
21 | #include "gtkfilechooserentry.h" |
22 | |
23 | #include <string.h> |
24 | |
25 | #include "gtkcelllayout.h" |
26 | #include "gtkcellrenderertext.h" |
27 | #include "gtkentryprivate.h" |
28 | #include "gtkfilechooserutils.h" |
29 | #include "gtklabel.h" |
30 | #include "gtkmain.h" |
31 | #include "gtksizerequest.h" |
32 | #include "gtkwindow.h" |
33 | #include "gtkintl.h" |
34 | #include "gtkmarshalers.h" |
35 | #include "gtkfilefilterprivate.h" |
36 | #include "gtkfilter.h" |
37 | #include "gtkeventcontrollerfocus.h" |
38 | |
39 | typedef struct _GtkFileChooserEntryClass GtkFileChooserEntryClass; |
40 | |
41 | #define GTK_FILE_CHOOSER_ENTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_FILE_CHOOSER_ENTRY, GtkFileChooserEntryClass)) |
42 | #define GTK_IS_FILE_CHOOSER_ENTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_FILE_CHOOSER_ENTRY)) |
43 | #define GTK_FILE_CHOOSER_ENTRY_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_FILE_CHOOSER_ENTRY, GtkFileChooserEntryClass)) |
44 | |
45 | struct _GtkFileChooserEntryClass |
46 | { |
47 | GtkEntryClass parent_class; |
48 | }; |
49 | |
50 | struct _GtkFileChooserEntry |
51 | { |
52 | GtkEntry parent_instance; |
53 | |
54 | GtkFileChooserAction action; |
55 | |
56 | GFile *base_folder; |
57 | GFile *current_folder_file; |
58 | char *dir_part; |
59 | char *file_part; |
60 | |
61 | GtkTreeModel *completion_store; |
62 | GtkFileFilter *current_filter; |
63 | |
64 | guint current_folder_loaded : 1; |
65 | guint complete_on_load : 1; |
66 | guint eat_tabs : 1; |
67 | guint eat_escape : 1; |
68 | }; |
69 | |
70 | enum |
71 | { |
72 | DISPLAY_NAME_COLUMN, |
73 | FULL_PATH_COLUMN, |
74 | N_COLUMNS |
75 | }; |
76 | |
77 | enum |
78 | { |
79 | HIDE_ENTRY, |
80 | LAST_SIGNAL |
81 | }; |
82 | |
83 | static guint signals[LAST_SIGNAL] = { 0 }; |
84 | |
85 | static void gtk_file_chooser_entry_finalize (GObject *object); |
86 | static void gtk_file_chooser_entry_dispose (GObject *object); |
87 | static gboolean gtk_file_chooser_entry_grab_focus (GtkWidget *widget); |
88 | static gboolean gtk_file_chooser_entry_tab_handler (GtkEventControllerKey *key, |
89 | guint keyval, |
90 | guint keycode, |
91 | GdkModifierType state, |
92 | GtkFileChooserEntry *chooser_entry); |
93 | |
94 | #ifdef G_OS_WIN32 |
95 | static int insert_text_callback (GtkFileChooserEntry *widget, |
96 | const char *new_text, |
97 | int new_text_length, |
98 | int *position, |
99 | gpointer user_data); |
100 | static void delete_text_callback (GtkFileChooserEntry *widget, |
101 | int start_pos, |
102 | int end_pos, |
103 | gpointer user_data); |
104 | #endif |
105 | |
106 | static gboolean match_selected_callback (GtkEntryCompletion *completion, |
107 | GtkTreeModel *model, |
108 | GtkTreeIter *iter, |
109 | GtkFileChooserEntry *chooser_entry); |
110 | |
111 | static void set_complete_on_load (GtkFileChooserEntry *chooser_entry, |
112 | gboolean complete_on_load); |
113 | static void refresh_current_folder_and_file_part (GtkFileChooserEntry *chooser_entry); |
114 | static void set_completion_folder (GtkFileChooserEntry *chooser_entry, |
115 | GFile *folder, |
116 | char *dir_part); |
117 | static void finished_loading_cb (GtkFileSystemModel *model, |
118 | GError *error, |
119 | GtkFileChooserEntry *chooser_entry); |
120 | |
121 | G_DEFINE_TYPE (GtkFileChooserEntry, _gtk_file_chooser_entry, GTK_TYPE_ENTRY) |
122 | |
123 | static char * |
124 | gtk_file_chooser_entry_get_completion_text (GtkFileChooserEntry *chooser_entry) |
125 | { |
126 | GtkEditable *editable = GTK_EDITABLE (chooser_entry); |
127 | int start, end; |
128 | |
129 | gtk_editable_get_selection_bounds (editable, start_pos: &start, end_pos: &end); |
130 | return gtk_editable_get_chars (editable, start_pos: 0, MIN (start, end)); |
131 | } |
132 | |
133 | static void |
134 | gtk_file_chooser_entry_dispatch_properties_changed (GObject *object, |
135 | guint n_pspecs, |
136 | GParamSpec **pspecs) |
137 | { |
138 | GtkFileChooserEntry *chooser_entry = GTK_FILE_CHOOSER_ENTRY (object); |
139 | guint i; |
140 | |
141 | G_OBJECT_CLASS (_gtk_file_chooser_entry_parent_class)->dispatch_properties_changed (object, n_pspecs, pspecs); |
142 | |
143 | /* Don't do this during or after disposal */ |
144 | if (gtk_widget_get_parent (GTK_WIDGET (object)) != NULL) |
145 | { |
146 | /* What we are after: The text in front of the cursor was modified. |
147 | * Unfortunately, there's no other way to catch this. |
148 | */ |
149 | for (i = 0; i < n_pspecs; i++) |
150 | { |
151 | if (pspecs[i]->name == I_("cursor-position" ) || |
152 | pspecs[i]->name == I_("selection-bound" ) || |
153 | pspecs[i]->name == I_("text" )) |
154 | { |
155 | set_complete_on_load (chooser_entry, FALSE); |
156 | refresh_current_folder_and_file_part (chooser_entry); |
157 | break; |
158 | } |
159 | } |
160 | } |
161 | } |
162 | |
163 | static void |
164 | _gtk_file_chooser_entry_class_init (GtkFileChooserEntryClass *class) |
165 | { |
166 | GObjectClass *gobject_class = G_OBJECT_CLASS (class); |
167 | GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); |
168 | |
169 | gobject_class->finalize = gtk_file_chooser_entry_finalize; |
170 | gobject_class->dispose = gtk_file_chooser_entry_dispose; |
171 | gobject_class->dispatch_properties_changed = gtk_file_chooser_entry_dispatch_properties_changed; |
172 | |
173 | widget_class->grab_focus = gtk_file_chooser_entry_grab_focus; |
174 | |
175 | signals[HIDE_ENTRY] = |
176 | g_signal_new (I_("hide-entry" ), |
177 | G_OBJECT_CLASS_TYPE (gobject_class), |
178 | signal_flags: G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, |
179 | class_offset: 0, |
180 | NULL, NULL, |
181 | NULL, |
182 | G_TYPE_NONE, n_params: 0); |
183 | } |
184 | |
185 | static gboolean |
186 | match_func (GtkEntryCompletion *compl, |
187 | const char *key, |
188 | GtkTreeIter *iter, |
189 | gpointer user_data) |
190 | { |
191 | GtkFileChooserEntry *chooser_entry = user_data; |
192 | |
193 | /* If we arrive here, the GtkFileSystemModel's GtkFileFilter already filtered out all |
194 | * files that don't start with the current prefix, so we manually apply the GtkFileChooser's |
195 | * current file filter (e.g. just jpg files) here. */ |
196 | if (chooser_entry->current_filter != NULL) |
197 | { |
198 | GFile *file; |
199 | GFileInfo *info; |
200 | |
201 | file = _gtk_file_system_model_get_file (GTK_FILE_SYSTEM_MODEL (chooser_entry->completion_store), |
202 | iter); |
203 | info = _gtk_file_system_model_get_info (GTK_FILE_SYSTEM_MODEL (chooser_entry->completion_store), |
204 | iter); |
205 | |
206 | /* We always allow navigating into subfolders, so don't ever filter directories */ |
207 | if (g_file_info_get_file_type (info) != G_FILE_TYPE_REGULAR) |
208 | return TRUE; |
209 | |
210 | if (!g_file_info_has_attribute (info, attribute: "standard::file" )) |
211 | g_file_info_set_attribute_object (info, attribute: "standard::file" , G_OBJECT (file)); |
212 | |
213 | return gtk_filter_match (self: GTK_FILTER (ptr: chooser_entry->current_filter), item: info); |
214 | } |
215 | |
216 | return TRUE; |
217 | } |
218 | |
219 | static void |
220 | chooser_entry_focus_out (GtkEventController *controller, |
221 | GtkFileChooserEntry *chooser_entry) |
222 | { |
223 | set_complete_on_load (chooser_entry, FALSE); |
224 | } |
225 | |
226 | static void |
227 | _gtk_file_chooser_entry_init (GtkFileChooserEntry *chooser_entry) |
228 | { |
229 | GtkEventController *controller; |
230 | GtkEntryCompletion *comp; |
231 | GtkCellRenderer *cell; |
232 | |
233 | g_object_set (object: chooser_entry, first_property_name: "truncate-multiline" , TRUE, NULL); |
234 | |
235 | comp = gtk_entry_completion_new (); |
236 | gtk_entry_completion_set_popup_single_match (completion: comp, FALSE); |
237 | gtk_entry_completion_set_minimum_key_length (completion: comp, length: 0); |
238 | /* see docs for gtk_entry_completion_set_text_column() */ |
239 | g_object_set (object: comp, first_property_name: "text-column" , FULL_PATH_COLUMN, NULL); |
240 | |
241 | /* Need a match func here or entry completion uses a wrong one. |
242 | * We do our own filtering after all. */ |
243 | gtk_entry_completion_set_match_func (completion: comp, |
244 | func: match_func, |
245 | func_data: chooser_entry, |
246 | NULL); |
247 | |
248 | cell = gtk_cell_renderer_text_new (); |
249 | gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (comp), |
250 | cell, TRUE); |
251 | gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (comp), |
252 | cell, |
253 | attribute: "text" , column: DISPLAY_NAME_COLUMN); |
254 | |
255 | g_signal_connect (comp, "match-selected" , |
256 | G_CALLBACK (match_selected_callback), chooser_entry); |
257 | |
258 | gtk_entry_set_completion (GTK_ENTRY (chooser_entry), completion: comp); |
259 | g_object_unref (object: comp); |
260 | |
261 | /* NB: This needs to happen after the completion is set, so this controller |
262 | * runs before the one installed by entrycompletion */ |
263 | controller = gtk_event_controller_key_new (); |
264 | g_signal_connect (controller, |
265 | "key-pressed" , |
266 | G_CALLBACK (gtk_file_chooser_entry_tab_handler), |
267 | chooser_entry); |
268 | gtk_widget_add_controller (GTK_WIDGET (chooser_entry), controller); |
269 | controller = gtk_event_controller_focus_new (); |
270 | g_signal_connect (controller, |
271 | "leave" , G_CALLBACK (chooser_entry_focus_out), |
272 | chooser_entry); |
273 | gtk_widget_add_controller (GTK_WIDGET (chooser_entry), controller); |
274 | |
275 | #ifdef G_OS_WIN32 |
276 | g_signal_connect (chooser_entry, "insert-text" , |
277 | G_CALLBACK (insert_text_callback), NULL); |
278 | g_signal_connect (chooser_entry, "delete-text" , |
279 | G_CALLBACK (delete_text_callback), NULL); |
280 | #endif |
281 | } |
282 | |
283 | static void |
284 | gtk_file_chooser_entry_finalize (GObject *object) |
285 | { |
286 | GtkFileChooserEntry *chooser_entry = GTK_FILE_CHOOSER_ENTRY (object); |
287 | |
288 | if (chooser_entry->base_folder) |
289 | g_object_unref (object: chooser_entry->base_folder); |
290 | |
291 | if (chooser_entry->current_folder_file) |
292 | g_object_unref (object: chooser_entry->current_folder_file); |
293 | |
294 | g_free (mem: chooser_entry->dir_part); |
295 | g_free (mem: chooser_entry->file_part); |
296 | |
297 | G_OBJECT_CLASS (_gtk_file_chooser_entry_parent_class)->finalize (object); |
298 | } |
299 | |
300 | static void |
301 | gtk_file_chooser_entry_dispose (GObject *object) |
302 | { |
303 | GtkFileChooserEntry *chooser_entry = GTK_FILE_CHOOSER_ENTRY (object); |
304 | |
305 | set_completion_folder (chooser_entry, NULL, NULL); |
306 | |
307 | G_OBJECT_CLASS (_gtk_file_chooser_entry_parent_class)->dispose (object); |
308 | } |
309 | |
310 | /* Match functions for the GtkEntryCompletion */ |
311 | static gboolean |
312 | match_selected_callback (GtkEntryCompletion *completion, |
313 | GtkTreeModel *model, |
314 | GtkTreeIter *iter, |
315 | GtkFileChooserEntry *chooser_entry) |
316 | { |
317 | char *path; |
318 | int pos; |
319 | |
320 | gtk_tree_model_get (tree_model: model, iter, |
321 | FULL_PATH_COLUMN, &path, |
322 | -1); |
323 | |
324 | gtk_editable_delete_text (GTK_EDITABLE (chooser_entry), |
325 | start_pos: 0, |
326 | end_pos: gtk_editable_get_position (GTK_EDITABLE (chooser_entry))); |
327 | pos = 0; |
328 | gtk_editable_insert_text (GTK_EDITABLE (chooser_entry), |
329 | text: path, |
330 | length: -1, |
331 | position: &pos); |
332 | |
333 | gtk_editable_set_position (GTK_EDITABLE (chooser_entry), position: pos); |
334 | |
335 | g_free (mem: path); |
336 | |
337 | return TRUE; |
338 | } |
339 | |
340 | static void |
341 | set_complete_on_load (GtkFileChooserEntry *chooser_entry, |
342 | gboolean complete_on_load) |
343 | { |
344 | /* a completion was triggered, but we couldn't do it. |
345 | * So no text was inserted when pressing tab, so we beep |
346 | */ |
347 | if (chooser_entry->complete_on_load && !complete_on_load) |
348 | gtk_widget_error_bell (GTK_WIDGET (chooser_entry)); |
349 | |
350 | chooser_entry->complete_on_load = complete_on_load; |
351 | } |
352 | |
353 | static gboolean |
354 | is_valid_scheme_character (char c) |
355 | { |
356 | return g_ascii_isalnum (c) || c == '+' || c == '-' || c == '.'; |
357 | } |
358 | |
359 | static gboolean |
360 | has_uri_scheme (const char *str) |
361 | { |
362 | const char *p; |
363 | |
364 | p = str; |
365 | |
366 | if (!is_valid_scheme_character (c: *p)) |
367 | return FALSE; |
368 | |
369 | do |
370 | p++; |
371 | while (is_valid_scheme_character (c: *p)); |
372 | |
373 | return (strncmp (s1: p, s2: "://" , n: 3) == 0); |
374 | } |
375 | |
376 | static GFile * |
377 | gtk_file_chooser_get_file_for_text (GtkFileChooserEntry *chooser_entry, |
378 | const char *str) |
379 | { |
380 | GFile *file; |
381 | |
382 | if (str[0] == '~' || g_path_is_absolute (file_name: str) || has_uri_scheme (str)) |
383 | file = g_file_parse_name (parse_name: str); |
384 | else if (chooser_entry->base_folder != NULL) |
385 | file = g_file_resolve_relative_path (file: chooser_entry->base_folder, relative_path: str); |
386 | else |
387 | file = NULL; |
388 | |
389 | return file; |
390 | } |
391 | |
392 | static gboolean |
393 | is_directory_shortcut (const char *text) |
394 | { |
395 | return strcmp (s1: text, s2: "." ) == 0 || |
396 | strcmp (s1: text, s2: ".." ) == 0 || |
397 | strcmp (s1: text, s2: "~" ) == 0; |
398 | } |
399 | |
400 | static GFile * |
401 | gtk_file_chooser_entry_get_directory_for_text (GtkFileChooserEntry *chooser_entry, |
402 | const char * text) |
403 | { |
404 | GFile *file, *parent; |
405 | |
406 | file = gtk_file_chooser_get_file_for_text (chooser_entry, str: text); |
407 | |
408 | if (file == NULL) |
409 | return NULL; |
410 | |
411 | if (text[0] == 0 || text[strlen (s: text) - 1] == G_DIR_SEPARATOR || |
412 | is_directory_shortcut (text)) |
413 | return file; |
414 | |
415 | parent = g_file_get_parent (file); |
416 | g_object_unref (object: file); |
417 | |
418 | return parent; |
419 | } |
420 | |
421 | /* Finds a common prefix based on the contents of the entry |
422 | * and mandatorily appends it |
423 | */ |
424 | static void |
425 | explicitly_complete (GtkFileChooserEntry *chooser_entry) |
426 | { |
427 | chooser_entry->complete_on_load = FALSE; |
428 | |
429 | if (chooser_entry->completion_store) |
430 | { |
431 | char *completion, *text; |
432 | gsize completion_len, text_len; |
433 | |
434 | text = gtk_file_chooser_entry_get_completion_text (chooser_entry); |
435 | text_len = strlen (s: text); |
436 | completion = gtk_entry_completion_compute_prefix (completion: gtk_entry_get_completion (GTK_ENTRY (chooser_entry)), key: text); |
437 | completion_len = completion ? strlen (s: completion) : 0; |
438 | |
439 | if (completion_len > text_len) |
440 | { |
441 | GtkEditable *editable = GTK_EDITABLE (chooser_entry); |
442 | int pos = gtk_editable_get_position (editable); |
443 | |
444 | gtk_editable_insert_text (editable, |
445 | text: completion + text_len, |
446 | length: completion_len - text_len, |
447 | position: &pos); |
448 | gtk_editable_set_position (editable, position: pos); |
449 | return; |
450 | } |
451 | } |
452 | |
453 | gtk_widget_error_bell (GTK_WIDGET (chooser_entry)); |
454 | } |
455 | |
456 | static gboolean |
457 | gtk_file_chooser_entry_grab_focus (GtkWidget *widget) |
458 | { |
459 | if (!GTK_WIDGET_CLASS (_gtk_file_chooser_entry_parent_class)->grab_focus (widget)) |
460 | return FALSE; |
461 | |
462 | _gtk_file_chooser_entry_select_filename (GTK_FILE_CHOOSER_ENTRY (widget)); |
463 | return TRUE; |
464 | } |
465 | |
466 | static void |
467 | start_explicit_completion (GtkFileChooserEntry *chooser_entry) |
468 | { |
469 | if (chooser_entry->current_folder_loaded) |
470 | explicitly_complete (chooser_entry); |
471 | else |
472 | set_complete_on_load (chooser_entry, TRUE); |
473 | } |
474 | |
475 | static gboolean |
476 | gtk_file_chooser_entry_tab_handler (GtkEventControllerKey *key, |
477 | guint keyval, |
478 | guint keycode, |
479 | GdkModifierType state, |
480 | GtkFileChooserEntry *chooser_entry) |
481 | { |
482 | GtkEditable *editable = GTK_EDITABLE (chooser_entry); |
483 | int start, end; |
484 | |
485 | if (keyval == GDK_KEY_Escape && |
486 | chooser_entry->eat_escape) |
487 | { |
488 | g_signal_emit (instance: chooser_entry, signal_id: signals[HIDE_ENTRY], detail: 0); |
489 | return GDK_EVENT_STOP; |
490 | } |
491 | |
492 | if (!chooser_entry->eat_tabs) |
493 | return GDK_EVENT_PROPAGATE; |
494 | |
495 | if (keyval != GDK_KEY_Tab) |
496 | return GDK_EVENT_PROPAGATE; |
497 | |
498 | if ((state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK) |
499 | return GDK_EVENT_PROPAGATE; |
500 | |
501 | /* This is a bit evil -- it makes Tab never leave the entry. It basically |
502 | * makes it 'safe' for people to hit. */ |
503 | gtk_editable_get_selection_bounds (editable, start_pos: &start, end_pos: &end); |
504 | |
505 | if (start != end) |
506 | gtk_editable_set_position (editable, MAX (start, end)); |
507 | else |
508 | start_explicit_completion (chooser_entry); |
509 | |
510 | return GDK_EVENT_STOP; |
511 | } |
512 | |
513 | static void |
514 | update_inline_completion (GtkFileChooserEntry *chooser_entry) |
515 | { |
516 | GtkEntryCompletion *completion = gtk_entry_get_completion (GTK_ENTRY (chooser_entry)); |
517 | |
518 | if (!chooser_entry->current_folder_loaded) |
519 | { |
520 | gtk_entry_completion_set_inline_completion (completion, FALSE); |
521 | return; |
522 | } |
523 | |
524 | switch (chooser_entry->action) |
525 | { |
526 | case GTK_FILE_CHOOSER_ACTION_OPEN: |
527 | case GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER: |
528 | gtk_entry_completion_set_inline_completion (completion, TRUE); |
529 | break; |
530 | case GTK_FILE_CHOOSER_ACTION_SAVE: |
531 | default: |
532 | gtk_entry_completion_set_inline_completion (completion, FALSE); |
533 | break; |
534 | } |
535 | } |
536 | |
537 | static void |
538 | discard_completion_store (GtkFileChooserEntry *chooser_entry) |
539 | { |
540 | if (!chooser_entry->completion_store) |
541 | return; |
542 | |
543 | gtk_entry_completion_set_model (completion: gtk_entry_get_completion (GTK_ENTRY (chooser_entry)), NULL); |
544 | update_inline_completion (chooser_entry); |
545 | g_object_unref (object: chooser_entry->completion_store); |
546 | chooser_entry->completion_store = NULL; |
547 | } |
548 | |
549 | static gboolean |
550 | completion_store_set (GtkFileSystemModel *model, |
551 | GFile *file, |
552 | GFileInfo *info, |
553 | int column, |
554 | GValue *value, |
555 | gpointer data) |
556 | { |
557 | GtkFileChooserEntry *chooser_entry = data; |
558 | |
559 | const char *prefix = "" ; |
560 | const char *suffix = "" ; |
561 | |
562 | switch (column) |
563 | { |
564 | case FULL_PATH_COLUMN: |
565 | prefix = chooser_entry->dir_part; |
566 | G_GNUC_FALLTHROUGH; |
567 | case DISPLAY_NAME_COLUMN: |
568 | if (_gtk_file_info_consider_as_directory (info)) |
569 | suffix = G_DIR_SEPARATOR_S; |
570 | |
571 | g_value_take_string (value, |
572 | v_string: g_strconcat (string1: prefix, |
573 | g_file_info_get_display_name (info), |
574 | suffix, |
575 | NULL)); |
576 | break; |
577 | default: |
578 | g_assert_not_reached (); |
579 | break; |
580 | } |
581 | |
582 | return TRUE; |
583 | } |
584 | |
585 | /* Fills the completion store from the contents of the current folder */ |
586 | static void |
587 | populate_completion_store (GtkFileChooserEntry *chooser_entry) |
588 | { |
589 | chooser_entry->completion_store = GTK_TREE_MODEL ( |
590 | _gtk_file_system_model_new_for_directory (chooser_entry->current_folder_file, |
591 | "standard::name,standard::display-name,standard::type," |
592 | "standard::content-type" , |
593 | completion_store_set, |
594 | chooser_entry, |
595 | N_COLUMNS, |
596 | G_TYPE_STRING, |
597 | G_TYPE_STRING)); |
598 | g_signal_connect (chooser_entry->completion_store, "finished-loading" , |
599 | G_CALLBACK (finished_loading_cb), chooser_entry); |
600 | |
601 | _gtk_file_system_model_set_filter_folders (GTK_FILE_SYSTEM_MODEL (chooser_entry->completion_store), |
602 | TRUE); |
603 | _gtk_file_system_model_set_show_files (GTK_FILE_SYSTEM_MODEL (chooser_entry->completion_store), |
604 | show_files: chooser_entry->action == GTK_FILE_CHOOSER_ACTION_OPEN || |
605 | chooser_entry->action == GTK_FILE_CHOOSER_ACTION_SAVE); |
606 | gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (chooser_entry->completion_store), |
607 | sort_column_id: DISPLAY_NAME_COLUMN, order: GTK_SORT_ASCENDING); |
608 | |
609 | gtk_entry_completion_set_model (completion: gtk_entry_get_completion (GTK_ENTRY (chooser_entry)), |
610 | model: chooser_entry->completion_store); |
611 | } |
612 | |
613 | /* Callback when the current folder finishes loading */ |
614 | static void |
615 | finished_loading_cb (GtkFileSystemModel *model, |
616 | GError *error, |
617 | GtkFileChooserEntry *chooser_entry) |
618 | { |
619 | GtkEntryCompletion *completion; |
620 | |
621 | chooser_entry->current_folder_loaded = TRUE; |
622 | |
623 | if (error) |
624 | { |
625 | discard_completion_store (chooser_entry); |
626 | set_complete_on_load (chooser_entry, FALSE); |
627 | return; |
628 | } |
629 | |
630 | if (chooser_entry->complete_on_load) |
631 | explicitly_complete (chooser_entry); |
632 | |
633 | gtk_widget_set_tooltip_text (GTK_WIDGET (chooser_entry), NULL); |
634 | |
635 | completion = gtk_entry_get_completion (GTK_ENTRY (chooser_entry)); |
636 | update_inline_completion (chooser_entry); |
637 | |
638 | if (gtk_widget_has_focus (GTK_WIDGET (chooser_entry))) |
639 | { |
640 | gtk_entry_completion_complete (completion); |
641 | gtk_entry_completion_insert_prefix (completion); |
642 | } |
643 | } |
644 | |
645 | static void |
646 | set_completion_folder (GtkFileChooserEntry *chooser_entry, |
647 | GFile *folder_file, |
648 | char *dir_part) |
649 | { |
650 | if (((chooser_entry->current_folder_file |
651 | && folder_file |
652 | && g_file_equal (file1: folder_file, file2: chooser_entry->current_folder_file)) |
653 | || chooser_entry->current_folder_file == folder_file) |
654 | && g_strcmp0 (str1: dir_part, str2: chooser_entry->dir_part) == 0) |
655 | { |
656 | return; |
657 | } |
658 | |
659 | if (chooser_entry->current_folder_file) |
660 | { |
661 | g_object_unref (object: chooser_entry->current_folder_file); |
662 | chooser_entry->current_folder_file = NULL; |
663 | } |
664 | |
665 | g_free (mem: chooser_entry->dir_part); |
666 | chooser_entry->dir_part = g_strdup (str: dir_part); |
667 | |
668 | chooser_entry->current_folder_loaded = FALSE; |
669 | |
670 | discard_completion_store (chooser_entry); |
671 | |
672 | if (folder_file) |
673 | { |
674 | chooser_entry->current_folder_file = g_object_ref (folder_file); |
675 | populate_completion_store (chooser_entry); |
676 | } |
677 | } |
678 | |
679 | static void |
680 | refresh_current_folder_and_file_part (GtkFileChooserEntry *chooser_entry) |
681 | { |
682 | GFile *folder_file; |
683 | char *text, *last_slash, *old_file_part; |
684 | char *dir_part; |
685 | |
686 | old_file_part = chooser_entry->file_part; |
687 | |
688 | text = gtk_file_chooser_entry_get_completion_text (chooser_entry); |
689 | |
690 | last_slash = strrchr (s: text, G_DIR_SEPARATOR); |
691 | if (last_slash) |
692 | { |
693 | dir_part = g_strndup (str: text, n: last_slash - text + 1); |
694 | chooser_entry->file_part = g_strdup (str: last_slash + 1); |
695 | } |
696 | else |
697 | { |
698 | dir_part = g_strdup (str: "" ); |
699 | chooser_entry->file_part = g_strdup (str: text); |
700 | } |
701 | |
702 | folder_file = gtk_file_chooser_entry_get_directory_for_text (chooser_entry, text); |
703 | |
704 | set_completion_folder (chooser_entry, folder_file, dir_part); |
705 | |
706 | if (folder_file) |
707 | g_object_unref (object: folder_file); |
708 | |
709 | g_free (mem: dir_part); |
710 | |
711 | if (chooser_entry->completion_store && |
712 | (g_strcmp0 (str1: old_file_part, str2: chooser_entry->file_part) != 0)) |
713 | { |
714 | GtkFileFilter *filter; |
715 | char *pattern; |
716 | |
717 | filter = gtk_file_filter_new (); |
718 | pattern = g_strconcat (string1: chooser_entry->file_part, "*" , NULL); |
719 | gtk_file_filter_add_pattern (filter, pattern); |
720 | |
721 | _gtk_file_system_model_set_filter (GTK_FILE_SYSTEM_MODEL (chooser_entry->completion_store), |
722 | filter); |
723 | |
724 | g_free (mem: pattern); |
725 | g_object_unref (object: filter); |
726 | } |
727 | |
728 | g_free (mem: text); |
729 | g_free (mem: old_file_part); |
730 | } |
731 | |
732 | #ifdef G_OS_WIN32 |
733 | static int |
734 | insert_text_callback (GtkFileChooserEntry *chooser_entry, |
735 | const char *new_text, |
736 | int new_text_length, |
737 | int *position, |
738 | gpointer user_data) |
739 | { |
740 | const char *colon = memchr (new_text, ':', new_text_length); |
741 | int i; |
742 | |
743 | /* Disallow these characters altogether */ |
744 | for (i = 0; i < new_text_length; i++) |
745 | { |
746 | if (new_text[i] == '<' || |
747 | new_text[i] == '>' || |
748 | new_text[i] == '"' || |
749 | new_text[i] == '|' || |
750 | new_text[i] == '*' || |
751 | new_text[i] == '?') |
752 | break; |
753 | } |
754 | |
755 | if (i < new_text_length || |
756 | /* Disallow entering text that would cause a colon to be anywhere except |
757 | * after a drive letter. |
758 | */ |
759 | (colon != NULL && |
760 | *position + (colon - new_text) != 1) || |
761 | (new_text_length > 0 && |
762 | *position <= 1 && |
763 | gtk_entry_get_text_length (GTK_ENTRY (chooser_entry)) >= 2 && |
764 | gtk_editable_get_text (GTK_EDITABLE (chooser_entry))[1] == ':')) |
765 | { |
766 | gtk_widget_error_bell (GTK_WIDGET (chooser_entry)); |
767 | g_signal_stop_emission_by_name (chooser_entry, "insert_text" ); |
768 | return FALSE; |
769 | } |
770 | |
771 | return TRUE; |
772 | } |
773 | |
774 | static void |
775 | delete_text_callback (GtkFileChooserEntry *chooser_entry, |
776 | int start_pos, |
777 | int end_pos, |
778 | gpointer user_data) |
779 | { |
780 | /* If deleting a drive letter, delete the colon, too */ |
781 | if (start_pos == 0 && end_pos == 1 && |
782 | gtk_entry_get_text_length (GTK_ENTRY (chooser_entry)) >= 2 && |
783 | gtk_editable_get_text (GTK_EDITABLE (chooser_entry))[1] == ':') |
784 | { |
785 | g_signal_handlers_block_by_func (chooser_entry, |
786 | G_CALLBACK (delete_text_callback), |
787 | user_data); |
788 | gtk_editable_delete_text (GTK_EDITABLE (chooser_entry), 0, 1); |
789 | g_signal_handlers_unblock_by_func (chooser_entry, |
790 | G_CALLBACK (delete_text_callback), |
791 | user_data); |
792 | } |
793 | } |
794 | #endif |
795 | |
796 | /** |
797 | * _gtk_file_chooser_entry_new: |
798 | * @eat_tabs: If %FALSE, allow focus navigation with the tab key. |
799 | * @eat_escape: If %TRUE, capture Escape key presses and emit ::hide-entry |
800 | * |
801 | * Creates a new `GtkFileChooserEntry` object. `GtkFileChooserEntry` |
802 | * is an internal implementation widget for the GTK file chooser |
803 | * which is an entry with completion with respect to a |
804 | * `GtkFileSystem` object. |
805 | * |
806 | * Returns: the newly created `GtkFileChooserEntry` |
807 | **/ |
808 | GtkWidget * |
809 | _gtk_file_chooser_entry_new (gboolean eat_tabs, |
810 | gboolean eat_escape) |
811 | { |
812 | GtkFileChooserEntry *chooser_entry; |
813 | |
814 | chooser_entry = g_object_new (GTK_TYPE_FILE_CHOOSER_ENTRY, NULL); |
815 | chooser_entry->eat_tabs = (eat_tabs != FALSE); |
816 | chooser_entry->eat_escape = (eat_escape != FALSE); |
817 | |
818 | return GTK_WIDGET (chooser_entry); |
819 | } |
820 | |
821 | /** |
822 | * _gtk_file_chooser_entry_set_base_folder: |
823 | * @chooser_entry: a `GtkFileChooserEntry` |
824 | * @file: file for a folder in the chooser entries current file system. |
825 | * |
826 | * Sets the folder with respect to which completions occur. |
827 | **/ |
828 | void |
829 | _gtk_file_chooser_entry_set_base_folder (GtkFileChooserEntry *chooser_entry, |
830 | GFile *file) |
831 | { |
832 | g_return_if_fail (GTK_IS_FILE_CHOOSER_ENTRY (chooser_entry)); |
833 | g_return_if_fail (file == NULL || G_IS_FILE (file)); |
834 | |
835 | if (chooser_entry->base_folder == file || |
836 | (file != NULL && chooser_entry->base_folder != NULL |
837 | && g_file_equal (file1: chooser_entry->base_folder, file2: file))) |
838 | return; |
839 | |
840 | if (file) |
841 | g_object_ref (file); |
842 | |
843 | if (chooser_entry->base_folder) |
844 | g_object_unref (object: chooser_entry->base_folder); |
845 | |
846 | chooser_entry->base_folder = file; |
847 | |
848 | refresh_current_folder_and_file_part (chooser_entry); |
849 | } |
850 | |
851 | /** |
852 | * _gtk_file_chooser_entry_get_current_folder: |
853 | * @chooser_entry: a `GtkFileChooserEntry` |
854 | * |
855 | * Gets the current folder for the `GtkFileChooserEntry`. |
856 | * |
857 | * If the user has only entered a filename, this will be in the base |
858 | * folder (see _gtk_file_chooser_entry_set_base_folder()), but if the |
859 | * user has entered a relative or absolute path, then it will be |
860 | * different. If the user has entered unparsable text, or text which |
861 | * the entry cannot handle, this will return %NULL. |
862 | * |
863 | * Returns: (nullable) (transfer full): the file for the current folder |
864 | * or %NULL if the current folder can not be determined |
865 | */ |
866 | GFile * |
867 | _gtk_file_chooser_entry_get_current_folder (GtkFileChooserEntry *chooser_entry) |
868 | { |
869 | g_return_val_if_fail (GTK_IS_FILE_CHOOSER_ENTRY (chooser_entry), NULL); |
870 | |
871 | return gtk_file_chooser_entry_get_directory_for_text (chooser_entry, |
872 | text: gtk_editable_get_text (GTK_EDITABLE (chooser_entry))); |
873 | } |
874 | |
875 | /** |
876 | * _gtk_file_chooser_entry_get_file_part: |
877 | * @chooser_entry: a `GtkFileChooserEntry` |
878 | * |
879 | * Gets the non-folder portion of whatever the user has entered |
880 | * into the file selector. What is returned is a UTF-8 string, |
881 | * and if a filename path is needed, g_file_get_child_for_display_name() |
882 | * must be used |
883 | * |
884 | * Returns: the entered filename - this value is owned by the |
885 | * chooser entry and must not be modified or freed. |
886 | **/ |
887 | const char * |
888 | _gtk_file_chooser_entry_get_file_part (GtkFileChooserEntry *chooser_entry) |
889 | { |
890 | const char *last_slash, *text; |
891 | |
892 | g_return_val_if_fail (GTK_IS_FILE_CHOOSER_ENTRY (chooser_entry), NULL); |
893 | |
894 | text = gtk_editable_get_text (GTK_EDITABLE (chooser_entry)); |
895 | last_slash = strrchr (s: text, G_DIR_SEPARATOR); |
896 | if (last_slash) |
897 | return last_slash + 1; |
898 | else if (is_directory_shortcut (text)) |
899 | return "" ; |
900 | else |
901 | return text; |
902 | } |
903 | |
904 | /** |
905 | * _gtk_file_chooser_entry_set_action: |
906 | * @chooser_entry: a `GtkFileChooserEntry` |
907 | * @action: the action which is performed by the file selector using this entry |
908 | * |
909 | * Sets action which is performed by the file selector using this entry. |
910 | * The `GtkFileChooserEntry` will use different completion strategies for |
911 | * different actions. |
912 | **/ |
913 | void |
914 | _gtk_file_chooser_entry_set_action (GtkFileChooserEntry *chooser_entry, |
915 | GtkFileChooserAction action) |
916 | { |
917 | g_return_if_fail (GTK_IS_FILE_CHOOSER_ENTRY (chooser_entry)); |
918 | |
919 | if (chooser_entry->action != action) |
920 | { |
921 | GtkEntryCompletion *comp; |
922 | |
923 | chooser_entry->action = action; |
924 | |
925 | comp = gtk_entry_get_completion (GTK_ENTRY (chooser_entry)); |
926 | |
927 | /* FIXME: do we need to actually set the following? */ |
928 | |
929 | switch (action) |
930 | { |
931 | case GTK_FILE_CHOOSER_ACTION_OPEN: |
932 | case GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER: |
933 | default: |
934 | gtk_entry_completion_set_popup_single_match (completion: comp, FALSE); |
935 | break; |
936 | case GTK_FILE_CHOOSER_ACTION_SAVE: |
937 | gtk_entry_completion_set_popup_single_match (completion: comp, TRUE); |
938 | break; |
939 | } |
940 | |
941 | if (chooser_entry->completion_store) |
942 | _gtk_file_system_model_set_show_files (GTK_FILE_SYSTEM_MODEL (chooser_entry->completion_store), |
943 | show_files: action == GTK_FILE_CHOOSER_ACTION_OPEN || |
944 | action == GTK_FILE_CHOOSER_ACTION_SAVE); |
945 | |
946 | update_inline_completion (chooser_entry); |
947 | } |
948 | } |
949 | |
950 | |
951 | /** |
952 | * _gtk_file_chooser_entry_get_action: |
953 | * @chooser_entry: a `GtkFileChooserEntry` |
954 | * |
955 | * Gets the action for this entry. |
956 | * |
957 | * Returns: the action |
958 | **/ |
959 | GtkFileChooserAction |
960 | _gtk_file_chooser_entry_get_action (GtkFileChooserEntry *chooser_entry) |
961 | { |
962 | g_return_val_if_fail (GTK_IS_FILE_CHOOSER_ENTRY (chooser_entry), |
963 | GTK_FILE_CHOOSER_ACTION_OPEN); |
964 | |
965 | return chooser_entry->action; |
966 | } |
967 | |
968 | gboolean |
969 | _gtk_file_chooser_entry_get_is_folder (GtkFileChooserEntry *chooser_entry, |
970 | GFile *file) |
971 | { |
972 | GtkTreeIter iter; |
973 | GFileInfo *info; |
974 | |
975 | if (chooser_entry->completion_store == NULL || |
976 | !_gtk_file_system_model_get_iter_for_file (GTK_FILE_SYSTEM_MODEL (chooser_entry->completion_store), |
977 | iter: &iter, |
978 | file)) |
979 | return FALSE; |
980 | |
981 | info = _gtk_file_system_model_get_info (GTK_FILE_SYSTEM_MODEL (chooser_entry->completion_store), |
982 | iter: &iter); |
983 | |
984 | return _gtk_file_info_consider_as_directory (info); |
985 | } |
986 | |
987 | |
988 | /* |
989 | * _gtk_file_chooser_entry_select_filename: |
990 | * @chooser_entry: a `GtkFileChooserEntry` |
991 | * |
992 | * Selects the filename (without the extension) for user edition. |
993 | */ |
994 | void |
995 | _gtk_file_chooser_entry_select_filename (GtkFileChooserEntry *chooser_entry) |
996 | { |
997 | const char *str, *ext; |
998 | glong len = -1; |
999 | |
1000 | if (chooser_entry->action == GTK_FILE_CHOOSER_ACTION_SAVE) |
1001 | { |
1002 | str = gtk_editable_get_text (GTK_EDITABLE (chooser_entry)); |
1003 | ext = g_strrstr (haystack: str, needle: "." ); |
1004 | |
1005 | if (ext) |
1006 | len = g_utf8_pointer_to_offset (str, pos: ext); |
1007 | } |
1008 | |
1009 | gtk_editable_select_region (GTK_EDITABLE (chooser_entry), start_pos: 0, end_pos: (int) len); |
1010 | } |
1011 | |
1012 | void |
1013 | _gtk_file_chooser_entry_set_file_filter (GtkFileChooserEntry *chooser_entry, |
1014 | GtkFileFilter *filter) |
1015 | { |
1016 | chooser_entry->current_filter = filter; |
1017 | } |
1018 | |