1 /*
2 * fm-path-entry.c
3 *
4 * Copyright 2009 PCMan <pcman.tw@gmail.com>
5 * Copyright 2009 Jürgen Hötzel <juergen@archlinux.org>
6 * Copyright 2012-2014 Andriy Grytsenko (LStranger) <andrej@rep.kiev.ua>
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
21 * MA 02110-1301, USA.
22 */
23
24 /**
25 * SECTION:fm-path-entry
26 * @short_description: An entry to enter path with completion.
27 * @title: FmPathEntry
28 *
29 * @include: libfm/fm-gtk.h
30 *
31 * The #FmPathEntry represents a widget to enter folder path for changing
32 * current directory. The path entry supports completion and can be used
33 * for both UNIX path or file URI entering. The path is represented in
34 * the entry unescaped therefore there is no way to enter escape sequence
35 * (such as \%23) into entry.
36 */
37
38 #ifdef HAVE_CONFIG_H
39 #include <config.h>
40 #endif
41
42 #include <glib/gi18n-lib.h>
43
44 #include "../gtk-compat.h"
45
46 #include "fm-path-entry.h"
47 /* for completion */
48 #include "fm-folder-model.h"
49 #include "fm-file.h"
50 #include "fm-utils.h"
51
52 #include <string.h>
53 #include <gio/gio.h>
54 #include <gdk/gdkkeysyms.h>
55
56 struct _FmPathEntry
57 {
58 GtkEntry parent_instance;
59 };
60
61 struct _FmPathEntryClass
62 {
63 GtkEntryClass parent_class;
64 };
65
66 /* properties */
67 enum
68 {
69 PROP_0,
70 PROP_HIGHLIGHT_COMPLETION_MATCH
71 };
72
73 typedef struct _FmPathEntryModel FmPathEntryModel;
74
75 #define FM_PATH_ENTRY_GET_PRIVATE(obj) ( G_TYPE_INSTANCE_GET_PRIVATE( (obj), FM_TYPE_PATH_ENTRY, FmPathEntryPrivate ) )
76
77 typedef struct _FmPathEntryPrivate FmPathEntryPrivate;
78
79 struct _FmPathEntryPrivate
80 {
81 FmPath* path;
82 /* model used for completion */
83 FmPathEntryModel* model;
84
85 /* name of parent dir */
86 char* parent_dir;
87 /* length of parent dir */
88 gint parent_len;
89
90 gboolean folder_loaded : 1;
91 gboolean highlight_completion_match : 1;
92 gboolean long_list : 1;
93 //gboolean complete_on_load :1;
94 GtkEntryCompletion* completion;
95 guint id_changed;
96
97 /* cancellable for dir listing */
98 GCancellable* cancellable;
99
100 /* length of basename typed by the user */
101 gint typed_basename_len;
102 };
103
104 typedef struct
105 {
106 FmPathEntry* entry;
107 GFile* dir;
108 GList* subdirs;
109 GCancellable* cancellable;
110 }ListSubDirNames;
111
112 //static gboolean fm_path_entry_grab_focus(GtkWidget *widget);
113 static gboolean fm_path_entry_focus_in_event(GtkWidget *widget, GdkEventFocus *event);
114 static gboolean fm_path_entry_focus_out_event(GtkWidget *widget, GdkEventFocus *event);
115 static void fm_path_entry_changed(GtkEditable *editable, gpointer user_data);
116 static void fm_path_entry_dispose(GObject *object);
117 static void fm_path_entry_finalize(GObject *object);
118 static gboolean fm_path_entry_match_func(GtkEntryCompletion *completion,
119 const gchar *key,
120 GtkTreeIter *iter,
121 gpointer user_data);
122 static void fm_path_entry_completion_render_func(GtkCellLayout *cell_layout,
123 GtkCellRenderer *cell,
124 GtkTreeModel *model,
125 GtkTreeIter *iter,
126 gpointer data);
127 static void fm_path_entry_set_property(GObject *object,
128 guint prop_id,
129 const GValue *value,
130 GParamSpec *pspec);
131 static void fm_path_entry_get_property(GObject *object,
132 guint prop_id,
133 GValue *value,
134 GParamSpec *pspec);
135
136 G_DEFINE_TYPE(FmPathEntry, fm_path_entry, GTK_TYPE_ENTRY)
137
138 /* customized model used for entry completion to save memory.
139 * GtkEntryCompletion requires that we store full paths in the model
140 * to work, but we only want to store basenames to save memory.
141 * So we created a custom model to do this. */
142
143 enum {
144 COL_BASENAME,
145 COL_FULL_PATH,
146 N_COLS
147 };
148
149 #define FM_TYPE_PATH_ENTRY_MODEL (fm_path_entry_model_get_type())
150
151 typedef struct _FmPathEntryModelClass FmPathEntryModelClass;
152
153 struct _FmPathEntryModel
154 {
155 GtkListStore parent_instance;
156 char* parent_dir;
157 };
158
159 struct _FmPathEntryModelClass
160 {
161 GtkListStoreClass parent_class;
162 };
163
164 static GType fm_path_entry_model_get_type(void);
165 static void fm_path_entry_model_iface_init(GtkTreeModelIface *iface);
166 static FmPathEntryModel* fm_path_entry_model_new(const char *dir);
167 static void fm_path_entry_model_set_parent_dir(FmPathEntryModel *model, const char *dir);
168
169 G_DEFINE_TYPE_EXTENDED( FmPathEntryModel, fm_path_entry_model, GTK_TYPE_LIST_STORE,
170 0, G_IMPLEMENT_INTERFACE(GTK_TYPE_TREE_MODEL, fm_path_entry_model_iface_init) );
171
172 /* end declaration of the customized model. */
173
174 static GtkTreeModelIface *parent_tree_model_interface = NULL;
175
176 #if 0
177 static void fm_path_entry_dispatch_properties_changed(GObject *object,
178 guint n_pspecs,
179 GParamSpec **pspecs)
180 {
181 FmPathEntry *entry = FM_PATH_ENTRY(object);
182 //FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
183 guint i;
184
185 G_OBJECT_CLASS(fm_path_entry_parent_class)->dispatch_properties_changed(object, n_pspecs, pspecs);
186
187 /* What we are after: The text in front of the cursor was modified.
188 * Unfortunately, there's no other way to catch this. */
189
190 for(i = 0; i < n_pspecs; i++)
191 {
192 if(pspecs[i]->name == g_intern_static_string("cursor-position") ||
193 pspecs[i]->name == g_intern_static_string("selection-bound") ||
194 pspecs[i]->name == g_intern_static_string("text"))
195 {
196 //priv->complete_on_load = FALSE;
197 break;
198 }
199 }
200 }
201 #endif
202
_path_entry_is_single_match(FmPathEntry * entry,FmPathEntryPrivate * priv)203 static gboolean _path_entry_is_single_match(FmPathEntry *entry, FmPathEntryPrivate *priv)
204 {
205 GtkTreeModel *model = GTK_TREE_MODEL(priv->model);
206 char *model_basename;
207 const char* typed_basename;
208 GtkTreeIter it;
209 gboolean partial = FALSE, match = FALSE;
210
211 typed_basename = gtk_entry_get_text(GTK_ENTRY(entry)) + priv->parent_len;
212 if (gtk_tree_model_get_iter_first(model, &it)) do
213 {
214 gtk_tree_model_get(model, &it, COL_BASENAME, &model_basename, -1);
215 if (!match && strcmp(model_basename, typed_basename) == 0)
216 match = TRUE; /* exact match */
217 else if (g_str_has_prefix(model_basename, typed_basename))
218 partial = TRUE;
219 g_free(model_basename);
220 }
221 while (!partial && gtk_tree_model_iter_next(model, &it));
222
223 return (match && !partial);
224 }
225
fm_path_entry_key_press(GtkWidget * widget,GdkEventKey * event,gpointer user_data)226 static gboolean fm_path_entry_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
227 {
228 FmPathEntry *entry = FM_PATH_ENTRY(widget);
229 FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
230 GdkModifierType state;
231
232 if(gtk_get_current_event_state(&state) &&
233 (state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK)
234 return FALSE;
235
236 switch( event->keyval )
237 {
238 case GDK_KEY_Tab:
239 {
240 #if 0
241 gtk_editable_get_selection_bounds(editable, &start, &end);
242 if(start != end)
243 gtk_editable_set_position(editable, MAX(start, end));
244 else
245 start_explicit_completion(entry);
246 #endif
247 gtk_entry_completion_insert_prefix(priv->completion);
248 gtk_editable_set_position(GTK_EDITABLE(entry), -1);
249 if (_path_entry_is_single_match(entry, priv))
250 {
251 int pos = gtk_editable_get_position(GTK_EDITABLE(entry));
252 gtk_editable_insert_text(GTK_EDITABLE(entry), "/", 1, &pos);
253 gtk_editable_set_position(GTK_EDITABLE(entry), pos);
254 }
255 return TRUE;
256 }
257 }
258 return FALSE;
259 }
260
_set_entry_text_from_path(GtkEntry * entry,FmPathEntryPrivate * priv)261 static void _set_entry_text_from_path(GtkEntry *entry, FmPathEntryPrivate *priv)
262 {
263 char *disp_name;
264 FmPath *path = priv->path;
265
266 disp_name = fm_path_display_name(path, FALSE);
267 /* block our handler for "changed" signal, we'll update it below */
268 if(priv->id_changed > 0)
269 g_signal_handler_block(entry, priv->id_changed);
270 gtk_entry_set_text(entry, disp_name);
271 if(priv->id_changed > 0)
272 g_signal_handler_unblock(entry, priv->id_changed);
273 /* update list of items now */
274 fm_path_entry_changed(GTK_EDITABLE(entry), NULL);
275 g_free(disp_name);
276 }
277
fm_path_entry_activate(GtkEntry * entry,gpointer user_data)278 static void fm_path_entry_activate(GtkEntry *entry, gpointer user_data)
279 {
280 FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
281 const char* full_path;
282 /* convert current path string to FmPath here */
283
284 full_path = gtk_entry_get_text(entry);
285 if(priv->path)
286 fm_path_unref(priv->path);
287
288 /* special handling for home dir */
289 if(full_path[0] == '~' && full_path[1] == G_DIR_SEPARATOR)
290 priv->path = fm_path_new_relative(fm_path_get_home(), full_path + 2);
291 else if(full_path[0] == '~' && full_path[1] == 0)
292 priv->path = fm_path_ref(fm_path_get_home());
293 else
294 priv->path = fm_path_new_for_display_name(full_path);
295
296 #if G_ENABLE_DEBUG
297 {
298 char *real_path = fm_path_to_str(priv->path);
299 g_debug("FmPathEntry activated: '%s' => '%s'", full_path, real_path);
300 g_free(real_path);
301 }
302 #endif
303
304 _set_entry_text_from_path(entry, priv);
305
306 gtk_editable_set_position(GTK_EDITABLE(entry), -1);
307 }
308
fm_path_entry_class_init(FmPathEntryClass * klass)309 static void fm_path_entry_class_init(FmPathEntryClass *klass)
310 {
311 GtkWidgetClass* widget_class = GTK_WIDGET_CLASS(klass);
312 GObjectClass* object_class = G_OBJECT_CLASS(klass);
313
314 object_class->get_property = fm_path_entry_get_property;
315 object_class->set_property = fm_path_entry_set_property;
316 /**
317 * FmPathEntry:highlight-completion-match:
318 *
319 * The #FmPathEntry:highlight-completion-match property is the flag
320 * whether the completion match should be highlighted or not.
321 *
322 * Since: 0.1.0
323 */
324 g_object_class_install_property( object_class,
325 PROP_HIGHLIGHT_COMPLETION_MATCH,
326 g_param_spec_boolean("highlight-completion-match",
327 "Highlight completion match",
328 "Whether to highlight the completion match",
329 TRUE, G_PARAM_READWRITE) );
330 object_class->dispose = fm_path_entry_dispose;
331 object_class->finalize = fm_path_entry_finalize;
332 /* object_class->dispatch_properties_changed = fm_path_entry_dispatch_properties_changed; */
333
334 widget_class->focus_in_event = fm_path_entry_focus_in_event;
335 /* widget_class->grab_focus = fm_path_entry_grab_focus; */
336 widget_class->focus_out_event = fm_path_entry_focus_out_event;
337
338 g_type_class_add_private( klass, sizeof (FmPathEntryPrivate) );
339 }
340
update_inline_completion(FmPathEntryPrivate * priv)341 static inline void update_inline_completion(FmPathEntryPrivate* priv)
342 {
343 gtk_entry_completion_set_inline_completion(priv->completion,
344 priv->folder_loaded);
345 }
346
clear_completion(FmPathEntryPrivate * priv)347 static void clear_completion(FmPathEntryPrivate* priv)
348 {
349 if(priv->model)
350 {
351 priv->parent_len = 0;
352 fm_path_entry_model_set_parent_dir(priv->model, NULL);
353 g_free(priv->parent_dir);
354 priv->parent_dir = NULL;
355 /* cancel running dir-listing jobs */
356 if(priv->cancellable)
357 {
358 g_cancellable_cancel(priv->cancellable);
359 g_object_unref(priv->cancellable);
360 priv->cancellable = NULL;
361 }
362 /* clear current model */
363 gtk_list_store_clear(GTK_LIST_STORE(priv->model));
364 update_inline_completion(priv);
365 }
366 priv->typed_basename_len = 0;
367 }
368
fm_path_entry_focus_in_event(GtkWidget * widget,GdkEventFocus * event)369 static gboolean fm_path_entry_focus_in_event(GtkWidget *widget, GdkEventFocus *event)
370 {
371 FmPathEntry *entry = FM_PATH_ENTRY(widget);
372 FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
373
374 /* listen to 'changed' signal for auto-completion */
375 priv->id_changed = g_signal_connect(entry, "changed",
376 G_CALLBACK(fm_path_entry_changed), NULL);
377
378 return GTK_WIDGET_CLASS(fm_path_entry_parent_class)->focus_in_event(widget, event);
379 }
380
381 #if 0
382 static void gtk_file_chooser_entry_grab_focus(GtkWidget *widget)
383 {
384 GTK_WIDGET_CLASS(fm_path_entry_parent_class)->grab_focus(widget);
385 gtk_editable_select_region(GTK_EDITABLE(widget), 0, (gint)-1);
386 }
387 #endif
388
fm_path_entry_focus_out_event(GtkWidget * widget,GdkEventFocus * event)389 static gboolean fm_path_entry_focus_out_event(GtkWidget *widget, GdkEventFocus *event)
390 {
391 FmPathEntry *entry = FM_PATH_ENTRY(widget);
392 FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
393
394 /* disconnect from 'changed' signal since we don't do auto-completion
395 * when we have no keyboard focus. */
396 priv->id_changed = 0;
397 g_signal_handlers_disconnect_by_func(entry, fm_path_entry_changed, NULL);
398
399 return GTK_WIDGET_CLASS(fm_path_entry_parent_class)->focus_out_event(widget, event);
400 }
401
402 #if GLIB_CHECK_VERSION(2, 36, 0)
on_dir_list_finished(GObject * source_object,GAsyncResult * res,gpointer user_data)403 static void on_dir_list_finished(GObject *source_object, GAsyncResult *res,
404 gpointer user_data)
405 #else
406 static gboolean on_dir_list_finished(gpointer user_data)
407 #endif
408 {
409 ListSubDirNames* data = (ListSubDirNames*)user_data;
410 FmPathEntry* entry = data->entry;
411 FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
412 GList* l;
413 FmPathEntryModel* new_model;
414
415 /* final chance to check cancellable */
416 if(g_cancellable_is_cancelled(data->cancellable))
417 #if GLIB_CHECK_VERSION(2, 36, 0)
418 return;
419 #else
420 return TRUE;
421 #endif
422 /* FIXME: check errors! */
423
424 new_model = fm_path_entry_model_new(priv->parent_dir);
425 /* g_debug("dir list is finished!"); */
426
427 /* update the model */
428 for(l = data->subdirs; l; l=l->next)
429 {
430 char* name = l->data;
431 gtk_list_store_insert_with_values((GtkListStore*)new_model, NULL, -1, COL_BASENAME, name, -1);
432 }
433 priv->folder_loaded = TRUE;
434 priv->long_list = (g_list_length(data->subdirs) > 40);
435
436 gtk_entry_completion_set_model(priv->completion, GTK_TREE_MODEL(new_model));
437 if(priv->model)
438 g_object_unref(priv->model);
439 priv->model = new_model;
440 //if(entry->complete_on_load)
441 //explicitly_complete(entry);
442 update_inline_completion(priv);
443 gtk_entry_completion_insert_prefix(priv->completion);
444 gtk_entry_completion_complete(priv->completion);
445
446 /* NOTE: after the content of entry gets changed, by default gtk+ installs
447 * an timeout handler with timeout 300 ms to popup completion list.
448 * If the dir listing takes more than 300 ms and finished after the
449 * timeout callback is called, then the completion list is empty at
450 * that time. So completion doesn't work. So, we trigger a 'changed'
451 * signal here to let GtkEntry do the completion with the new model again. */
452
453 /* trigger completion popup. FIXME: this is a little bit dirty.
454 * A even more dirty thing to do is to check if we finished after
455 * 300 ms timeout happens. */
456 g_signal_emit_by_name(entry, "changed", 0);
457 #if !GLIB_CHECK_VERSION(2, 36, 0)
458 return TRUE;
459 #endif
460 }
461
462 #if GLIB_CHECK_VERSION(2, 36, 0)
list_sub_dirs(GTask * task,gpointer source_object,gpointer user_data,GCancellable * cancellable)463 static void list_sub_dirs(GTask *task, gpointer source_object, gpointer user_data,
464 GCancellable *cancellable)
465 #else
466 static gboolean list_sub_dirs(GIOSchedulerJob *job, GCancellable *cancellable, gpointer user_data)
467 #endif
468 {
469 ListSubDirNames* data = (ListSubDirNames*)user_data;
470 GError *err = NULL;
471 /* g_debug("new dir listing job!"); */
472 GFileEnumerator* enu = g_file_enumerate_children(data->dir,
473 G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME","
474 G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME","
475 G_FILE_ATTRIBUTE_STANDARD_TYPE,
476 G_FILE_QUERY_INFO_NONE, cancellable,
477 NULL);
478 if(enu)
479 {
480 while(!g_cancellable_is_cancelled(cancellable))
481 {
482 GFileInfo* inf = g_file_enumerator_next_file(enu, cancellable, &err);
483 if(inf)
484 {
485 GFileType type = g_file_info_get_file_type(inf);
486 if(type == G_FILE_TYPE_DIRECTORY)
487 {
488 const char* name = g_file_info_get_edit_name(inf);
489 if (!name)
490 name = g_file_info_get_display_name(inf);
491 data->subdirs = g_list_prepend(data->subdirs, g_strdup(name));
492 }
493 g_object_unref(inf);
494 }
495 else
496 {
497 if(err) /* error happens */
498 g_clear_error(&err);
499 else /* EOF */
500 break;
501 }
502 }
503 g_object_unref(enu);
504 }
505
506 if(!g_cancellable_is_cancelled(cancellable))
507 #if GLIB_CHECK_VERSION(2, 36, 0)
508 g_task_return_pointer(task, NULL, NULL);
509 #else
510 {
511 /* finished! */
512 g_io_scheduler_job_send_to_mainloop(job, on_dir_list_finished, data, NULL);
513 }
514 return FALSE;
515 #endif
516 }
517
list_sub_dir_names_free(gpointer user_data)518 static void list_sub_dir_names_free(gpointer user_data)
519 {
520 ListSubDirNames* data = (ListSubDirNames*)user_data;
521 g_object_unref(data->dir);
522 g_object_unref(data->cancellable);
523 g_list_foreach(data->subdirs, (GFunc)g_free, NULL);
524 g_list_free(data->subdirs);
525 g_slice_free(ListSubDirNames, data);
526 }
527
fm_path_entry_changed(GtkEditable * editable,gpointer user_data)528 static void fm_path_entry_changed(GtkEditable *editable, gpointer user_data)
529 {
530 FmPathEntry *entry = FM_PATH_ENTRY(editable);
531 FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
532 const gchar *path_str, *sep;
533 #if GLIB_CHECK_VERSION(2, 36, 0)
534 GTask *task;
535 #endif
536
537 if(priv->model == NULL)
538 return;
539 /* find parent dir of current path */
540 path_str = gtk_entry_get_text( GTK_ENTRY(entry) );
541 sep = g_utf8_strrchr(path_str, -1, G_DIR_SEPARATOR);
542 if(sep) /* we found a parent dir */
543 {
544 int parent_len = (sep - path_str) + 1; /* includes the dir separator / */
545 if(!priv->parent_dir
546 || priv->parent_len != parent_len
547 || strncmp(priv->parent_dir, path_str, parent_len ))
548 {
549 /* parent dir has been changed, reload dir list */
550 ListSubDirNames* data = g_slice_new0(ListSubDirNames);
551 priv->folder_loaded = FALSE;
552 clear_completion(priv);
553 priv->parent_dir = g_strndup(path_str, parent_len);
554 priv->parent_len = parent_len;
555 fm_path_entry_model_set_parent_dir(priv->model, priv->parent_dir);
556 /* g_debug("parent dir is changed to %s", priv->parent_dir); */
557
558 /* FIXME: convert utf-8 encoded path to on-disk encoding. */
559 data->entry = entry;
560 if(priv->parent_dir[0] == '~') /* special case for home dir */
561 {
562 char* expand = g_strconcat(fm_get_home_dir(), priv->parent_dir + 1, NULL);
563 data->dir = fm_file_new_for_commandline_arg(expand);
564 g_free(expand);
565 }
566 else
567 {
568 FmPath *p = fm_path_new_for_display_name(priv->parent_dir);
569 data->dir = fm_path_to_gfile(p);
570 fm_path_unref(p);
571 }
572
573 /* launch a new job to do dir listing */
574 if (G_LIKELY(priv->cancellable == NULL))
575 priv->cancellable = g_cancellable_new();
576 data->cancellable = (GCancellable*)g_object_ref(priv->cancellable);
577 #if GLIB_CHECK_VERSION(2, 36, 0)
578 task = g_task_new(editable, data->cancellable, on_dir_list_finished, data);
579 g_task_set_task_data(task, data, list_sub_dir_names_free);
580 g_task_set_priority(task, G_PRIORITY_LOW);
581 g_task_run_in_thread(task, list_sub_dirs);
582 g_object_unref(task);
583 #else
584 g_io_scheduler_push_job(list_sub_dirs,
585 data, list_sub_dir_names_free,
586 G_PRIORITY_LOW, data->cancellable);
587 #endif
588 }
589 /* calculate the length of remaining part after / */
590 priv->typed_basename_len = strlen(sep + 1);
591 }
592 else /* clear all autocompletion thing. */
593 clear_completion(priv);
594 }
595
fm_path_entry_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)596 static void fm_path_entry_set_property(GObject *object,
597 guint prop_id,
598 const GValue *value,
599 GParamSpec *pspec)
600 {
601 FmPathEntry *entry = FM_PATH_ENTRY(object);
602 FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
603
604 switch( prop_id )
605 {
606 case PROP_HIGHLIGHT_COMPLETION_MATCH:
607 priv->highlight_completion_match = g_value_get_boolean(value);
608 break;
609 default:
610 G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
611 break;
612 }
613 }
614
fm_path_entry_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)615 static void fm_path_entry_get_property(GObject *object,
616 guint prop_id,
617 GValue *value,
618 GParamSpec *pspec)
619 {
620 FmPathEntry *entry = FM_PATH_ENTRY(object);
621 FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
622
623 switch( prop_id ) {
624 case PROP_HIGHLIGHT_COMPLETION_MATCH:
625 g_value_set_boolean(value, priv->highlight_completion_match);
626 break;
627 default:
628 G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
629 break;
630 }
631 }
632
fm_path_entry_paste_and_go(GtkMenuItem * menuitem,GtkEntry * entry)633 static void fm_path_entry_paste_and_go(GtkMenuItem *menuitem, GtkEntry *entry)
634 {
635 GtkClipboard* clipboard = gtk_clipboard_get_for_display(
636 gtk_widget_get_display(GTK_WIDGET(menuitem)), GDK_SELECTION_CLIPBOARD);
637
638 gchar* full_path = gtk_clipboard_wait_for_text(clipboard);
639
640 if (full_path)
641 {
642 FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
643
644 if(priv->path)
645 fm_path_unref(priv->path);
646
647 /* special handling for home dir */
648 if(full_path[0] == '~' && full_path[1] == G_DIR_SEPARATOR)
649 priv->path = fm_path_new_relative(fm_path_get_home(), full_path + 2);
650 else if(full_path[0] == '~' && full_path[1] == 0)
651 priv->path = fm_path_ref(fm_path_get_home());
652 else
653 /* FIXME: use fm_path_new_for_display_name ? */
654 priv->path = fm_path_new_for_str(full_path);
655
656 gchar * disp_name = fm_path_display_name(priv->path, FALSE);
657 gtk_entry_set_text(entry, disp_name);
658 g_free(disp_name);
659
660 gtk_editable_set_position(GTK_EDITABLE(entry), -1);
661
662 g_free(full_path);
663
664 g_signal_emit_by_name(entry, "activate", 0);
665 }
666 }
667
fm_path_entry_populate_popup(GtkEntry * entry,GtkMenu * menu,gpointer user_data)668 static void fm_path_entry_populate_popup(GtkEntry *entry, GtkMenu *menu, gpointer user_data)
669 {
670 GtkWidget* menuitem;
671
672 GtkClipboard* clipboard = gtk_clipboard_get_for_display(
673 gtk_widget_get_display(GTK_WIDGET(entry)), GDK_SELECTION_CLIPBOARD);
674
675 menuitem = gtk_menu_item_new_with_mnemonic(_("Pa_ste and Go"));
676 gtk_widget_show(menuitem);
677
678 /* Insert menu item after default Paste menu item */
679 gtk_menu_shell_insert(GTK_MENU_SHELL(menu), menuitem, 3);
680
681 g_signal_connect(menuitem, "activate",
682 G_CALLBACK(fm_path_entry_paste_and_go), entry);
683
684 if (!gtk_clipboard_wait_is_text_available(clipboard))
685 gtk_widget_set_sensitive(menuitem, FALSE);
686 }
687
688 static void
fm_path_entry_init(FmPathEntry * entry)689 fm_path_entry_init(FmPathEntry *entry)
690 {
691 FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
692 GtkEntryCompletion* completion = gtk_entry_completion_new();
693 GtkCellRenderer* render;
694 AtkObject *obj;
695
696 priv->model = fm_path_entry_model_new(NULL);
697 priv->completion = completion;
698 priv->cancellable = g_cancellable_new();
699 priv->highlight_completion_match = TRUE;
700 gtk_entry_completion_set_minimum_key_length(completion, 1);
701
702 gtk_entry_completion_set_match_func(completion, fm_path_entry_match_func, NULL, NULL);
703 g_object_set(completion, "text_column", COL_FULL_PATH, NULL);
704 gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(priv->model));
705 gtk_entry_set_completion(GTK_ENTRY(entry), completion);
706
707 render = gtk_cell_renderer_text_new();
708 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(completion), render, TRUE);
709 gtk_cell_layout_add_attribute(GTK_CELL_LAYOUT(completion), render, "text", COL_BASENAME);
710 gtk_cell_layout_set_cell_data_func(GTK_CELL_LAYOUT(completion), render, fm_path_entry_completion_render_func, entry, NULL);
711
712 /* NOTE: this is to avoid a bug of gtk+.
713 * The inline selection provided by GtkEntry is buggy.
714 * If we change the content of the entry, it still stores
715 * the old prefix sometimes so things don't work as expected.
716 * So, unfortunately, we're not able to use this nice feature.
717 *
718 * Please see gtk_entry_completion_key_press() of gtk/gtkentry.c
719 * and look for completion->priv->completion_prefix.
720 */
721 /* gtk_entry_completion_set_inline_selection(completion, FALSE); */
722
723 /* gtk_entry_completion_set_inline_completion(completion, TRUE); */
724 gtk_entry_completion_set_popup_set_width(completion, TRUE);
725 gtk_entry_completion_set_popup_single_match(completion, FALSE);
726
727 /* connect to these signals rather than overriding default handlers since
728 * we want to invoke our handlers before the default ones provided by Gtk. */
729 g_signal_connect(entry, "key-press-event", G_CALLBACK(fm_path_entry_key_press), NULL);
730 g_signal_connect(entry, "activate", G_CALLBACK(fm_path_entry_activate), NULL);
731 g_signal_connect(entry, "populate-popup", G_CALLBACK(fm_path_entry_populate_popup), NULL);
732
733 obj = gtk_widget_get_accessible(GTK_WIDGET(entry));
734 atk_object_set_description(obj, _("Folder location bar"));
735 }
736
fm_path_entry_completion_render_func(GtkCellLayout * cell_layout,GtkCellRenderer * cell,GtkTreeModel * model,GtkTreeIter * iter,gpointer data)737 static void fm_path_entry_completion_render_func(GtkCellLayout *cell_layout,
738 GtkCellRenderer *cell,
739 GtkTreeModel *model,
740 GtkTreeIter *iter,
741 gpointer data)
742 {
743 gchar *model_file_name;
744 int model_file_name_len;
745 FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE( FM_PATH_ENTRY(data) );
746 gtk_tree_model_get(model, iter, COL_BASENAME, &model_file_name, -1);
747 model_file_name_len = strlen(model_file_name);
748
749 if( priv->highlight_completion_match && (model_file_name_len >= priv->typed_basename_len) )
750 {
751 int buf_len = model_file_name_len + 14 + 1;
752 gchar* markup = g_malloc(buf_len);
753 gchar *trail = g_stpcpy(markup, "<b><u>");
754 strncpy(trail, model_file_name, priv->typed_basename_len);
755 trail += priv->typed_basename_len;
756 trail = g_stpcpy(trail, "</u></b>");
757 trail = g_stpcpy(trail, model_file_name + priv->typed_basename_len);
758 g_object_set(cell, "markup", markup, NULL);
759 g_free(markup);
760 }
761 /* FIXME: We don't need a custom render func if we don't hightlight */
762 else
763 g_object_set(cell, "text", model_file_name, NULL);
764 g_free(model_file_name);
765 }
766
fm_path_entry_dispose(GObject * object)767 static void fm_path_entry_dispose(GObject *object)
768 {
769 FmPathEntryPrivate* priv = FM_PATH_ENTRY_GET_PRIVATE(object);
770
771 g_signal_handlers_disconnect_by_func(object, fm_path_entry_key_press, NULL);
772 g_signal_handlers_disconnect_by_func(object, fm_path_entry_activate, NULL);
773
774 gtk_entry_set_completion(GTK_ENTRY(object), NULL);
775 clear_completion(priv);
776
777 if(priv->completion)
778 {
779 gtk_entry_completion_set_model(priv->completion, NULL);
780 g_object_unref(priv->completion);
781 priv->completion = NULL;
782 }
783
784 if(priv->path)
785 {
786 fm_path_unref(priv->path);
787 priv->path = NULL;
788 }
789
790 if(priv->model)
791 {
792 g_object_unref(priv->model);
793 priv->model = NULL;
794 }
795
796 if(priv->cancellable)
797 {
798 g_cancellable_cancel(priv->cancellable);
799 g_object_unref(priv->cancellable);
800 priv->cancellable = NULL;
801 }
802
803 G_OBJECT_CLASS(fm_path_entry_parent_class)->dispose(object);
804 }
805
806 static void
fm_path_entry_finalize(GObject * object)807 fm_path_entry_finalize(GObject *object)
808 {
809 FmPathEntryPrivate* priv = FM_PATH_ENTRY_GET_PRIVATE(object);
810
811 g_free(priv->parent_dir);
812
813 (*G_OBJECT_CLASS(fm_path_entry_parent_class)->finalize)(object);
814 }
815
816 /**
817 * fm_path_entry_new
818 *
819 * Creates new path entry widget.
820 *
821 * Returns: (transfer full): a new #FmPathEntry object.
822 *
823 * Since: 0.1.0
824 */
fm_path_entry_new(void)825 FmPathEntry* fm_path_entry_new(void)
826 {
827 return g_object_new(FM_TYPE_PATH_ENTRY, NULL);
828 }
829
830 /**
831 * fm_path_entry_set_path
832 * @entry: a widget to apply
833 * @path: new path to set
834 *
835 * Sets new path into enter field.
836 *
837 * Since: 0.1.10
838 */
fm_path_entry_set_path(FmPathEntry * entry,FmPath * path)839 void fm_path_entry_set_path(FmPathEntry *entry, FmPath* path)
840 {
841 FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
842
843 if(priv->path)
844 fm_path_unref(priv->path);
845
846 if(path)
847 {
848 priv->path = fm_path_ref(path);
849 _set_entry_text_from_path(GTK_ENTRY(entry), priv);
850 }
851 else
852 {
853 priv->path = NULL;
854 gtk_entry_set_text(GTK_ENTRY(entry), "");
855 }
856 }
857
fm_path_entry_match_func(GtkEntryCompletion * completion,const gchar * key,GtkTreeIter * iter,gpointer user_data)858 static gboolean fm_path_entry_match_func(GtkEntryCompletion *completion,
859 const gchar *key,
860 GtkTreeIter *iter,
861 gpointer user_data)
862 {
863 gboolean ret;
864 GtkTreeModel *model = gtk_entry_completion_get_model(completion);
865 FmPathEntry *entry = FM_PATH_ENTRY( gtk_entry_completion_get_entry(completion) );
866 FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
867 char *model_basename;
868 const char* typed_basename;
869 /* we don't use the case-insensitive key provided by entry completion here */
870 typed_basename = gtk_entry_get_text(GTK_ENTRY(entry)) + priv->parent_len;
871 gtk_tree_model_get(model, iter, COL_BASENAME, &model_basename, -1);
872
873 if (G_UNLIKELY(model_basename == NULL))
874 ret = FALSE; /* it is invalid if file name is empty but it's possible! */
875 else if(model_basename[0] == '.' && typed_basename[0] != '.')
876 ret = FALSE; /* ignore hidden files when not requested. */
877 else if(priv->long_list && /* don't create too long lists */
878 (typed_basename[0] == '\0' /* "/xxx/" - no names here yet */
879 || (typed_basename[0] == '.' && typed_basename[1] == '\0'))) /* "/xxx/." */
880 ret = FALSE;
881 else
882 ret = g_str_has_prefix(model_basename, typed_basename); /* FIXME: should we be case insensitive here? */
883 g_free(model_basename);
884 return ret;
885 }
886
887
888 /**
889 * fm_path_entry_get_path
890 * @entry: the widget to inspect
891 *
892 * Retrieves the current path in the @entry. Returned data are owned by
893 * @entry and should be not freed by caller.
894 *
895 * Returns: (transfer none): the current path.
896 *
897 * Since: 0.1.10
898 */
fm_path_entry_get_path(FmPathEntry * entry)899 FmPath* fm_path_entry_get_path(FmPathEntry *entry)
900 {
901 FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
902 return priv->path;
903 }
904
905
906 /* ------------------------------------------------------------------------
907 * custom tree model implementation. */
908
fm_path_entry_model_init(FmPathEntryModel * model)909 static void fm_path_entry_model_init(FmPathEntryModel *model)
910 {
911 GType cols[] = {G_TYPE_STRING, G_TYPE_STRING};
912 gtk_list_store_set_column_types(GTK_LIST_STORE(model), G_N_ELEMENTS(cols), cols);
913 }
914
fm_path_entry_model_finalize(GObject * object)915 static void fm_path_entry_model_finalize(GObject *object)
916 {
917 g_free(((FmPathEntryModel*)object)->parent_dir);
918
919 (*G_OBJECT_CLASS(fm_path_entry_model_parent_class)->finalize)(object);
920 }
921
fm_path_entry_model_class_init(FmPathEntryModelClass * klass)922 static void fm_path_entry_model_class_init(FmPathEntryModelClass *klass)
923 {
924 GObjectClass* object_class = G_OBJECT_CLASS(klass);
925
926 object_class->finalize = fm_path_entry_model_finalize;
927 }
928
fm_path_entry_model_get_value(GtkTreeModel * tree_model,GtkTreeIter * iter,gint column,GValue * value)929 static void fm_path_entry_model_get_value(GtkTreeModel *tree_model,
930 GtkTreeIter *iter,
931 gint column,
932 GValue *value)
933 {
934 FmPathEntryModel *model = (FmPathEntryModel*)tree_model;
935 if(column == COL_FULL_PATH)
936 {
937 char* full_path;
938 parent_tree_model_interface->get_value(tree_model, iter, COL_BASENAME, value);
939 full_path = g_strconcat(model->parent_dir, g_value_get_string(value), NULL);
940 g_value_take_string(value, full_path);
941 }
942 else
943 parent_tree_model_interface->get_value(tree_model, iter, column, value);
944 }
945
fm_path_entry_model_iface_init(GtkTreeModelIface * iface)946 static void fm_path_entry_model_iface_init(GtkTreeModelIface *iface)
947 {
948 parent_tree_model_interface = g_type_interface_peek_parent(iface);
949 iface->get_value = fm_path_entry_model_get_value;
950 }
951
fm_path_entry_model_set_parent_dir(FmPathEntryModel * model,const char * dir)952 static void fm_path_entry_model_set_parent_dir(FmPathEntryModel* model, const char *dir)
953 {
954 g_free(model->parent_dir);
955 model->parent_dir = dir ? g_strdup(dir) : NULL;
956 }
957
fm_path_entry_model_new(const char * parent_dir)958 static FmPathEntryModel* fm_path_entry_model_new(const char *parent_dir)
959 {
960 FmPathEntryModel* model = g_object_new(FM_TYPE_PATH_ENTRY_MODEL, NULL);
961 fm_path_entry_model_set_parent_dir(model, parent_dir);
962 return model;
963 }
964