4
* Copyright 2009 PCMan <pcman.tw@gmail.com>
5
* Copyright 2009 Jürgen Hötzel <juergen@archlinux.org>
7
* This program is free software; you can redistribute it and/or modify
8
* it under the terms of the GNU General Public License as published by
9
* the Free Software Foundation; either version 2 of the License, or
10
* (at your option) any later version.
12
* This program is distributed in the hope that it will be useful,
13
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
* GNU General Public License for more details.
17
* You should have received a copy of the GNU General Public License
18
* along with this program; if not, write to the Free Software
19
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
23
#include "fm-path-entry.h"
25
#include "fm-folder-model.h"
28
#include <gdk/gdkkeysyms.h>
33
PROP_HIGHLIGHT_COMPLETION_MATCH
36
#define FM_PATH_ENTRY_GET_PRIVATE(obj) ( G_TYPE_INSTANCE_GET_PRIVATE( (obj), FM_TYPE_PATH_ENTRY, FmPathEntryPrivate ) )
38
typedef struct _FmPathEntryPrivate FmPathEntryPrivate;
40
struct _FmPathEntryPrivate
43
/* associated with a folder model */
45
/* current model used for completion */
46
FmFolderModel* completion_model;
47
/* Current len of the completion string */
49
/* prevent recurs. if text is changed by insert. compl. suffix */
51
gboolean highlight_completion_match;
52
GtkEntryCompletion* completion;
53
/* automatic common suffix completion */
54
gint common_suffix_append_idle_id;
55
gchar common_suffix[PATH_MAX];
58
static void fm_path_entry_activate(GtkEntry *entry);
59
static gboolean fm_path_entry_key_press(GtkWidget *widget, GdkEventKey *event);
60
static void fm_path_entry_class_init(FmPathEntryClass *klass);
61
static void fm_path_entry_editable_init(GtkEditableClass *iface);
62
static void fm_path_entry_changed(GtkEditable *editable);
63
static void fm_path_entry_do_insert_text(GtkEditable *editable,
64
const gchar *new_text,
67
static gboolean fm_path_entry_suffix_append_idle(gpointer user_data);
68
static void fm_path_entry_suffix_append_idle_destroy(gpointer user_data);
69
static gboolean fm_path_entry_update_expand_path(FmPathEntry *entry);
70
static void fm_path_entry_init(FmPathEntry *entry);
71
static void fm_path_entry_finalize(GObject *object);
72
static gboolean fm_path_entry_match_func(GtkEntryCompletion *completion,
76
static gboolean fm_path_entry_match_selected(GtkEntryCompletion *widget,
80
static void fm_path_entry_completion_render_func(GtkCellLayout *cell_layout,
81
GtkCellRenderer *cell,
85
static void fm_path_entry_set_property(GObject *object,
89
static void fm_path_entry_get_property(GObject *object,
94
G_DEFINE_TYPE_EXTENDED( FmPathEntry, fm_path_entry, GTK_TYPE_ENTRY,
95
0, G_IMPLEMENT_INTERFACE(GTK_TYPE_EDITABLE, fm_path_entry_editable_init) );
97
static GtkEditableClass *parent_editable_interface = NULL;
99
static gboolean fm_path_entry_key_press(GtkWidget *widget, GdkEventKey *event) {
100
FmPathEntry *entry = FM_PATH_ENTRY(widget);
102
switch( event->keyval )
105
/* place the cursor at the end */
106
gtk_editable_set_position(GTK_EDITABLE(entry), -1);
112
static void fm_path_entry_activate(GtkEntry *entry)
114
/* Chain up so that entry->activates_default is honored */
115
GTK_ENTRY_CLASS(fm_path_entry_parent_class)->activate(entry);
118
static void fm_path_entry_class_init(FmPathEntryClass *klass)
120
GtkWidgetClass* widget_class = GTK_WIDGET_CLASS(klass);
121
GObjectClass* object_class = G_OBJECT_CLASS(klass);
122
GtkEntryClass* entry_class = GTK_ENTRY_CLASS(klass);
124
object_class->get_property = fm_path_entry_get_property;
125
object_class->set_property = fm_path_entry_set_property;
126
g_object_class_install_property( object_class,
127
PROP_HIGHLIGHT_COMPLETION_MATCH,
128
g_param_spec_boolean("highlight-completion-match",
129
"Highlight completion match",
130
"Wheather to highlight the completion match",
131
TRUE, G_PARAM_READWRITE) );
132
object_class->finalize = fm_path_entry_finalize;
133
entry_class->activate = fm_path_entry_activate;
135
g_type_class_add_private( klass, sizeof (FmPathEntryPrivate) );
138
static void fm_path_entry_editable_init(GtkEditableClass *iface)
140
parent_editable_interface = g_type_interface_peek_parent(iface);
141
iface->changed = fm_path_entry_changed;
142
iface->do_insert_text = fm_path_entry_do_insert_text;
145
static void fm_path_entry_changed(GtkEditable *editable)
147
FmPathEntry *entry = FM_PATH_ENTRY(editable);
148
FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
149
const gchar *original_key = gtk_entry_get_text( GTK_ENTRY(entry) );
150
/* len of directory part */
152
gchar *last_slash = strrchr(original_key, G_DIR_SEPARATOR);
154
if( priv->in_change || !priv->path )
157
/* not path -> keep current completion model */
158
if( last_slash == NULL )
161
/* Check if path entry is not part of current completion folder model */
162
key_dir_len = last_slash - original_key;
163
if( !fm_path_equal_str(priv->path, original_key, key_dir_len) )
165
gchar* new_path = g_path_get_dirname(original_key);
166
FmPath *new_fm_path = fm_path_new(new_path);
168
if( new_fm_path != NULL )
170
/* set hidden parameter based on prev. model */
171
/* FIXME: this is not very good */
172
gboolean show_hidden = priv->completion_model ? fm_folder_model_get_show_hidden(priv->completion_model) : FALSE;
173
if(priv->completion_model)
174
g_object_unref(priv->completion_model);
175
if(priv->model && fm_path_equal(priv->model->dir->dir_path, new_fm_path))
178
fm_path_unref(priv->path);
179
priv->path = fm_path_ref(priv->model->dir->dir_path);
180
fm_path_unref(new_fm_path);
181
priv->completion_model = g_object_ref(priv->model);
185
FmFolder *new_fm_folder = fm_folder_get_for_path(new_fm_path);
186
FmFolderModel *new_fm = fm_folder_model_new(new_fm_folder, show_hidden);
187
g_object_unref(new_fm_folder);
188
priv->completion_model = new_fm;
190
fm_path_unref(priv->path);
191
priv->path = new_fm_path;
194
gtk_entry_completion_set_model( priv->completion, GTK_TREE_MODEL(priv->completion_model) );
198
/* FIXME: Handle invalid Paths */
199
g_warning("Invalid Path: %s", new_path);
204
static void fm_path_entry_do_insert_text(GtkEditable *editable, const gchar *new_text,
205
gint new_text_length, gint *position)
207
FmPathEntry *entry = FM_PATH_ENTRY(editable);
208
FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
209
/* let the GtkEntry class handle the insert */
210
(parent_editable_interface->do_insert_text)(editable, new_text, new_text_length, position);
212
if( GTK_WIDGET_HAS_FOCUS(editable) && priv->completion_model )
214
/* we have a common suffix -> add idle function */
215
if( (priv->common_suffix_append_idle_id < 0) && ( fm_path_entry_update_expand_path(entry) ) )
216
priv->common_suffix_append_idle_id = g_idle_add_full(G_PRIORITY_HIGH,
217
fm_path_entry_suffix_append_idle,
218
entry, fm_path_entry_suffix_append_idle_destroy);
222
static gboolean fm_path_entry_suffix_append_idle(gpointer user_data)
224
FmPathEntry *entry = FM_PATH_ENTRY(user_data);
225
FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
226
const gchar *original_key = gtk_entry_get_text( GTK_ENTRY(entry) );
228
/* we have a common suffix -> insert/select it */
229
fm_path_entry_update_expand_path(entry);
230
if( priv->common_suffix[0] )
232
gint suffix_offset = g_utf8_strlen(original_key, -1);
233
gint suffix_offset_save = suffix_offset;
235
priv->in_change = TRUE;
236
gtk_editable_insert_text(GTK_EDITABLE(entry), priv->common_suffix, -1, &suffix_offset);
237
gtk_editable_select_region(GTK_EDITABLE(entry), suffix_offset_save, -1);
238
priv->in_change = FALSE;
240
/* don't call again */
244
static void fm_path_entry_suffix_append_idle_destroy(gpointer user_data)
246
FmPathEntry *entry = FM_PATH_ENTRY(user_data);
247
FM_PATH_ENTRY_GET_PRIVATE(entry)->common_suffix_append_idle_id = -1;
250
static gboolean fm_path_entry_update_expand_path(FmPathEntry *entry)
252
FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
253
const gchar *original_key = gtk_entry_get_text( GTK_ENTRY(entry) );
254
/* len of directory part */
256
gchar *last_slash = strrchr(original_key, G_DIR_SEPARATOR);
258
priv->common_suffix[0] = 0;
260
/* get completion suffix */
261
if( last_slash && priv->completion_model )
262
fm_folder_model_get_common_suffix_for_prefix(priv->completion_model,
265
priv->common_suffix);
266
return (priv->common_suffix[0] != 0);
269
static void fm_path_entry_set_property(GObject *object,
274
FmPathEntry *entry = FM_PATH_ENTRY(object);
275
FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
279
case PROP_HIGHLIGHT_COMPLETION_MATCH:
280
priv->highlight_completion_match = g_value_get_boolean(value);
283
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
288
static void fm_path_entry_get_property(GObject *object,
293
FmPathEntry *entry = FM_PATH_ENTRY(object);
294
FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
297
case PROP_HIGHLIGHT_COMPLETION_MATCH:
298
g_value_set_boolean(value, priv->highlight_completion_match);
301
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
307
fm_path_entry_init(FmPathEntry *entry)
309
FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
310
GtkEntryCompletion* completion = gtk_entry_completion_new();
311
GtkCellRenderer* render;
314
priv->completion_model = NULL;
315
priv->completion_len = 0;
316
priv->in_change = FALSE;
317
priv->completion = completion;
318
priv->highlight_completion_match = TRUE;
319
priv->common_suffix_append_idle_id = -1;
320
priv->common_suffix[0] = 0;
321
gtk_entry_completion_set_minimum_key_length(completion, 1);
322
gtk_entry_completion_set_match_func(completion, fm_path_entry_match_func, NULL, NULL);
323
g_signal_connect(G_OBJECT(completion), "match-selected", G_CALLBACK(fm_path_entry_match_selected), (gpointer)NULL);
324
g_object_set(completion, "text-column", COL_FILE_NAME, NULL);
325
render = gtk_cell_renderer_text_new();
326
gtk_cell_layout_pack_start( (GtkCellLayout*)completion, render, TRUE );
327
gtk_cell_layout_set_cell_data_func(GTK_CELL_LAYOUT(completion), render, fm_path_entry_completion_render_func, entry, NULL);
328
gtk_entry_completion_set_inline_completion(completion, TRUE);
329
gtk_entry_completion_set_popup_set_width(completion, TRUE);
330
g_signal_connect(G_OBJECT(entry), "key-press-event", G_CALLBACK(fm_path_entry_key_press), NULL);
331
gtk_entry_set_completion(GTK_ENTRY(entry), completion);
334
static void fm_path_entry_completion_render_func(GtkCellLayout *cell_layout,
335
GtkCellRenderer *cell,
341
gchar *model_file_name;
342
FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE( FM_PATH_ENTRY(data) );
343
gtk_tree_model_get(GTK_TREE_MODEL(model), iter,
344
COL_FILE_NAME, &model_file_name, -1);
345
if( priv->highlight_completion_match )
347
gchar markup[PATH_MAX];
348
gchar *trail = g_stpcpy(markup, "<b><u>");
349
trail = strncpy(trail, model_file_name, priv->completion_len) + priv->completion_len;
350
trail = g_stpcpy(trail, "</u></b>");
351
trail = g_stpcpy(trail, model_file_name + priv->completion_len);
352
g_object_set(cell, "markup", markup, NULL);
354
/* FIXME: We don't need a custom render func if we don't hightlight */
356
g_object_set(cell, "text", model_file_name, NULL);
360
fm_path_entry_finalize(GObject *object)
362
FmPathEntryPrivate* priv = FM_PATH_ENTRY_GET_PRIVATE(object);
365
g_object_unref(priv->completion);
368
fm_path_unref(priv->path);
370
/* release the folder model reference */
372
g_object_unref(priv->model);
374
if(priv->completion_model)
375
g_object_unref(priv->completion_model);
377
/* drop idle func for suffix completion */
378
if( priv->common_suffix_append_idle_id > 0 )
379
g_source_remove(priv->common_suffix_append_idle_id);
381
(*G_OBJECT_CLASS(fm_path_entry_parent_class)->finalize)(object);
385
GtkWidget* fm_path_entry_new()
387
return g_object_new(FM_TYPE_PATH_ENTRY, NULL);
390
void fm_path_entry_set_model(FmPathEntry *entry, FmPath* path, FmFolderModel* model)
392
FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(entry);
393
/* FIXME: should we use UTF-8 encoded display name here? */
394
gchar *path_str = fm_path_display_name(path, FALSE);
396
fm_path_unref(priv->path);
397
priv->path = fm_path_ref(path);
400
g_object_unref( priv->model );
401
if( priv->completion_model )
402
g_object_unref(priv->completion_model);
405
priv->model = g_object_ref(model);
406
priv->completion_model = g_object_ref(model);
407
gtk_entry_set_completion(GTK_ENTRY(entry), priv->completion);
412
priv->completion_model = NULL;
415
g_object_unref(priv->completion);
416
priv->completion = NULL;
418
gtk_entry_set_completion(GTK_ENTRY(entry), NULL);
421
gtk_entry_completion_set_model( priv->completion, (GtkTreeModel*)priv->completion_model );
422
priv->in_change = TRUE;
423
gtk_entry_set_text(GTK_ENTRY(entry), path_str);
424
priv->in_change = FALSE;
425
gtk_editable_set_position(GTK_EDITABLE(entry), -1);
429
static gboolean fm_path_entry_match_func(GtkEntryCompletion *completion,
435
GtkTreeModel *model = gtk_entry_completion_get_model(completion);
436
FmPathEntry *pe = FM_PATH_ENTRY( gtk_entry_completion_get_entry(completion) );
437
FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE(pe);
438
FmFileInfo *model_file_info;
439
gchar *model_file_name;
440
/* get original key (case sensitive) */
441
const gchar *original_key = gtk_entry_get_text( GTK_ENTRY(pe) );
443
/* find sep in key */
444
gchar *key_last_slash = strrchr(original_key, G_DIR_SEPARATOR);
446
/* no model based completion possible */
447
if( (!model) || (key_last_slash == NULL) )
450
priv->completion_len = strlen(key_last_slash + 1);
452
/* get filename, info from model */
453
gtk_tree_model_get(GTK_TREE_MODEL(model), iter,
454
COL_FILE_NAME, &model_file_name,
455
COL_FILE_INFO, &model_file_info,
458
ret = fm_file_info_is_dir(model_file_info) && g_str_has_prefix(model_file_name, key_last_slash + 1);
459
g_free(model_file_name);
463
static gboolean fm_path_entry_match_selected(GtkEntryCompletion *widget,
468
GtkWidget *entry = gtk_entry_completion_get_entry(widget);
469
gchar new_text[PATH_MAX];
470
FmPathEntryPrivate *priv = FM_PATH_ENTRY_GET_PRIVATE( FM_PATH_ENTRY(entry) );
471
gchar *model_file_name;
473
gtk_tree_model_get(GTK_TREE_MODEL(model), iter,
474
COL_FILE_NAME, &model_file_name,
476
/* FIXME: should we use UTF-8 encoded display name here? */
477
new_path = fm_path_to_str(priv->completion_model->dir->dir_path);
478
g_sprintf(new_text, "%s/%s",
479
/* prevent leading double slash */
480
g_str_equal(new_path, "/") ? "" : new_path,
483
priv->completion_len = 0;
484
gtk_entry_set_text(GTK_ENTRY(entry), new_text);
485
/* move the cursor to the end of entry */
486
gtk_editable_set_position(GTK_EDITABLE(entry), -1);