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