0d861339466df06b2297d364665dd830c1671189
[geeqie.git] / src / tabcomp.c
1 /*
2  * GQview image viewer
3  * (C)1999 John Ellis
4  *
5  * Author: John Ellis
6  *
7  */
8
9 #include "gqview.h"
10 #include <gdk/gdkkeysyms.h> /* for key values */
11
12 #include "tabcomp.xpm"
13
14 /* ----------------------------------------------------------------
15    Tab completion routines, can be connected to any gtkentry widget
16    using the tab_completion_add_to_entry() function.
17    Use remove_trailing_slash() to strip the trailing '/'.
18    ----------------------------------------------------------------*/
19
20 typedef struct _TabCompData TabCompData;
21 struct _TabCompData
22 {
23         GtkWidget *entry;
24         gchar *dir_path;
25         GList *file_list;
26         void (*enter_func)(gchar *, gpointer);
27         void (*tab_func)(gchar *, gpointer);
28         gpointer enter_data;
29         gpointer tab_data;
30
31         GtkWidget *combo;
32         gint has_history;
33         gchar *history_key;
34         gint history_levels;
35 };
36
37 typedef struct _HistoryData HistoryData;
38 struct _HistoryData
39 {
40         gchar *key;
41         GList *list;
42 };
43
44 static GList *history_list = NULL;
45
46 static void tab_completion_free_list(TabCompData *td);
47 static void tab_completion_read_dir(TabCompData *td, gchar *path);
48 static void tab_completion_destroy(GtkWidget *widget, gpointer data);
49 static void tab_completion_emit_enter_signal(TabCompData *td);
50 static void tab_completion_emit_tab_signal(TabCompData *td);
51 static gint tab_completion_do(TabCompData *td);
52 static gint tab_completion_key_pressed(GtkWidget *widget, GdkEventKey *event, gpointer data);
53 static void tab_completion_button_pressed(GtkWidget *widget, gpointer data);
54 static GtkWidget *tab_completion_create_complete_button(GtkWidget *window, GtkWidget *entry);
55
56 static void history_list_free(HistoryData *hd);
57 static HistoryData *history_list_find_by_key(const gchar* key);
58 static gchar *history_list_find_last_path_by_key(const gchar* key);
59 static void history_list_free_key(const gchar *key);
60 static void history_list_add_to_key(const gchar *key, const gchar *path, gint max);
61
62 static void history_list_free(HistoryData *hd)
63 {
64         GList *work;
65
66         if (!hd) return;
67
68         work = hd->list;
69         while(work)
70                 {
71                 g_free(work->data);
72                 work = work->next;
73                 }
74
75         g_free(hd->key);
76         g_free(hd);
77 }
78
79 static HistoryData *history_list_find_by_key(const gchar* key)
80 {
81         GList *work = history_list;
82         while(work)
83                 {
84                 HistoryData *hd = work->data;
85                 if (strcmp(hd->key, key) == 0) return hd;
86                 work = work->next;
87                 }
88         return NULL;
89 }
90
91 static gchar *history_list_find_last_path_by_key(const gchar* key)
92 {
93         HistoryData *hd;
94         hd = history_list_find_by_key(key);
95         if (!hd || !hd->list) return NULL;
96
97         return hd->list->data;
98 }
99
100 static void history_list_free_key(const gchar *key)
101 {
102         HistoryData *hd;
103         hd = history_list_find_by_key(key);
104         if (!hd) return;
105
106         history_list = g_list_remove(history_list, hd);
107         history_list_free(hd);
108 }
109
110 static void history_list_add_to_key(const gchar *key, const gchar *path, gint max)
111 {
112         HistoryData *hd;
113         GList *work;
114
115         hd = history_list_find_by_key(key);
116         if (!hd)
117                 {
118                 hd = g_new(HistoryData, 1);
119                 hd->key = g_strdup(key);
120                 hd->list = NULL;
121                 history_list = g_list_prepend(history_list, hd);
122                 }
123
124         /* if already in the list, simply move it to the top */
125         work = hd->list;
126         while(work)
127                 {
128                 gchar *buf = work->data;
129                 work = work->next;
130                 if (strcmp(buf, path) == 0)
131                         {
132                         hd->list = g_list_remove(hd->list, buf);
133                         hd->list = g_list_prepend(hd->list, buf);
134                         return;
135                         }
136                 }
137
138         hd->list = g_list_prepend(hd->list, g_strdup(path));
139
140         if (max > 0)
141                 {
142                 while(hd->list && g_list_length(hd->list) > max)
143                         {
144                         GList *work = g_list_last(hd->list);
145                         gchar *buf = work->data;
146                         hd->list = g_list_remove(hd->list, buf);
147                         g_free(buf);
148                         }
149                 }
150 }
151
152 static void tab_completion_free_list(TabCompData *td)
153 {
154         GList *list;
155
156         g_free(td->dir_path);
157         td->dir_path = NULL;
158
159         list = td->file_list;
160
161         while(list)
162                 {
163                 g_free(list->data);
164                 list = list->next;
165                 }
166
167         g_list_free(td->file_list);
168         td->file_list = NULL;
169 }
170
171 static void tab_completion_read_dir(TabCompData *td, gchar *path)
172 {
173         DIR *dp;
174         struct dirent *dir;
175         GList *list = NULL;
176
177         tab_completion_free_list(td);
178
179         if((dp = opendir(path))==NULL)
180                 {
181                 /* dir not found */
182                 return;
183                 }
184         while ((dir = readdir(dp)) != NULL)
185                 {
186                 /* skips removed files */
187                 if (dir->d_ino > 0)
188                         {
189                         gchar *name = dir->d_name;
190                         if (strcmp(name, ".") != 0 && strcmp(name, "..") != 0)
191                                 {
192                                 list = g_list_prepend(list, g_strdup(name));
193                                 }
194                         }
195                 }
196         closedir(dp);
197
198         td->dir_path = g_strdup(path);
199         td->file_list = list;
200 }
201
202 static void tab_completion_destroy(GtkWidget *widget, gpointer data)
203 {
204         TabCompData *td = data;
205         tab_completion_free_list(td);
206         g_free(td->history_key);
207         g_free(td);
208 }
209
210 static void tab_completion_emit_enter_signal(TabCompData *td)
211 {
212         gchar *text;
213         if (!td->enter_func) return;
214
215         text = g_strdup(gtk_entry_get_text(GTK_ENTRY(td->entry)));
216
217         if (text[0] == '~')
218                 {
219                 gchar *t = text;
220                 text = g_strconcat(homedir(), t + 1, NULL);
221                 g_free(t);
222                 }
223
224         td->enter_func(text, td->enter_data);
225         g_free(text);
226 }
227
228 static void tab_completion_emit_tab_signal(TabCompData *td)
229 {
230         gchar *text;
231         if (!td->tab_func) return;
232
233         text = g_strdup(gtk_entry_get_text(GTK_ENTRY(td->entry)));
234
235         if (text[0] == '~')
236                 {
237                 gchar *t = text;
238                 text = g_strconcat(homedir(), t + 1, NULL);
239                 g_free(t);
240                 }
241
242         td->tab_func(text, td->tab_data);
243         g_free(text);
244 }
245
246 static gint tab_completion_do(TabCompData *td)
247 {
248         gchar *entry_text = gtk_entry_get_text(GTK_ENTRY(td->entry));
249         gchar *entry_file;
250         gchar *entry_dir;
251         gchar *ptr;
252         gint home_exp = FALSE;
253
254         /* home dir expansion */
255         if (entry_text[0] == '~')
256                 {
257                 entry_dir = g_strconcat(homedir(), entry_text + 1, NULL);
258                 home_exp = TRUE;
259                 }
260         else
261                 {
262                 entry_dir = g_strdup(entry_text);
263                 }
264
265         entry_file = filename_from_path(entry_text);
266
267         if (isfile(entry_dir))
268                 {
269                 if (home_exp)
270                         {
271                         gtk_entry_set_text(GTK_ENTRY(td->entry), entry_dir);
272                         gtk_entry_set_position(GTK_ENTRY(td->entry), strlen(entry_dir));
273                         }
274                 g_free(entry_dir);
275                 return home_exp;
276                 }
277         if (isdir(entry_dir) && strcmp(entry_file, ".") != 0 && strcmp(entry_file, "..") != 0)
278                 {
279                 ptr = entry_dir + strlen(entry_dir) - 1;
280                 if (ptr[0] == '/')
281                         {
282                         if (home_exp)
283                                 {
284                                 gtk_entry_set_text(GTK_ENTRY(td->entry), entry_dir);
285                                 gtk_entry_set_position(GTK_ENTRY(td->entry), strlen(entry_dir));
286                                 }
287                         g_free(entry_dir);
288                         return home_exp;
289                         }
290                 else
291                         {
292                         gchar *buf = g_strconcat(entry_dir, "/", NULL);
293                         gtk_entry_set_text(GTK_ENTRY(td->entry), buf);
294                         gtk_entry_set_position(GTK_ENTRY(td->entry), strlen(buf));
295                         g_free(buf);
296                         g_free(entry_dir);
297                         return TRUE;
298                         }
299                 }
300
301         ptr = filename_from_path(entry_dir);
302         if (ptr > entry_dir) ptr--;
303         ptr[0] = '\0';
304
305         if (strlen(entry_dir) == 0)
306                 {
307                 g_free(entry_dir);
308                 entry_dir = g_strdup("/");
309                 }
310
311         if (isdir(entry_dir))
312                 {
313                 GList *list;
314                 GList *poss = NULL;
315                 gint l = strlen(entry_file);
316
317                 if (!td->dir_path || !td->file_list || strcmp(td->dir_path, entry_dir) != 0)
318                         {
319                         tab_completion_read_dir(td, entry_dir);
320                         }
321
322                 if (strcmp(entry_dir, "/") == 0) entry_dir[0] = '\0';
323
324                 list = td->file_list;
325                 while(list)
326                         {
327                         gchar *file = list->data;
328                         if (strncmp(entry_file, file, l) == 0)
329                                 {
330                                 poss = g_list_prepend(poss, file);
331                                 }
332                         list = list->next;
333                         }
334
335                 if (poss)
336                         {
337                         if (!poss->next)
338                                 {
339                                 gchar *file = poss->data;
340                                 gchar *buf;
341
342                                 buf = g_strconcat(entry_dir, "/", file, NULL);
343
344                                 if (isdir(buf))
345                                         {
346                                         g_free(buf);
347                                         buf = g_strconcat(entry_dir, "/", file, "/", NULL);
348                                         }
349                                 gtk_entry_set_text(GTK_ENTRY(td->entry), buf);
350                                 gtk_entry_set_position(GTK_ENTRY(td->entry), strlen(buf));
351                                 g_free(buf);
352                                 g_list_free(poss);
353                                 g_free(entry_dir);
354                                 return TRUE;
355                                 }
356                         else
357                                 {
358                                 gint c = strlen(entry_file);
359                                 gint done = FALSE;
360                                 gchar *test_file = poss->data;
361
362                                 while (!done)
363                                         {
364                                         list = poss;
365                                         if (!list) done = TRUE;
366                                         while(list && !done)
367                                                 {
368                                                 gchar *file = list->data;
369                                                 if (strlen(file) < c || strncmp(test_file, file, c) != 0)
370                                                         {
371                                                         done = TRUE;
372                                                         }
373                                                 list = list->next;
374                                                 }
375                                         c++;
376                                         }
377                                 c -= 2;
378                                 if (c > 0)
379                                         {
380                                         gchar *file;
381                                         gchar *buf;
382                                         file = g_strdup(test_file);
383                                         file[c] = '\0';
384                                         buf = g_strconcat(entry_dir, "/", file, NULL);
385                                         gtk_entry_set_text(GTK_ENTRY(td->entry), buf);
386                                         gtk_entry_set_position(GTK_ENTRY(td->entry), strlen(buf));
387                                         g_free(file);
388                                         g_free(buf);
389                                         g_list_free(poss);
390                                         g_free(entry_dir);
391                                         return TRUE;
392                                         }
393                                 }
394                         g_list_free(poss);
395                         }
396                 }
397
398         g_free(entry_dir);
399
400         return FALSE;
401 }
402
403 static gint tab_completion_key_pressed(GtkWidget *widget, GdkEventKey *event, gpointer data)
404 {
405         TabCompData *td = data;
406         gint stop_signal = FALSE;
407
408         switch (event->keyval)
409                 {
410                 case GDK_Tab:
411                         if (tab_completion_do(td))
412                                 {
413                                 tab_completion_emit_tab_signal(td);
414                                 }
415                         stop_signal = TRUE;
416                         break;
417                 case GDK_Return:
418                         tab_completion_emit_enter_signal(td);
419                         stop_signal = TRUE;
420                         break;
421                 default:
422                         break;
423                 }
424
425         if (stop_signal)
426                 {
427                 if (stop_signal) gtk_signal_emit_stop_by_name(GTK_OBJECT(widget), "key_press_event");
428                 return TRUE;
429                 }
430
431         return FALSE;
432 }
433
434 static void tab_completion_button_pressed(GtkWidget *widget, gpointer data)
435 {
436         TabCompData *td;
437         GtkWidget *entry = data;
438
439         td = gtk_object_get_data(GTK_OBJECT(entry), "tab_completion_data");
440
441         if (!td) return;
442
443         if (!GTK_WIDGET_HAS_FOCUS(entry))
444                 {
445                 gtk_widget_grab_focus(entry);
446                 }
447
448         if (tab_completion_do(td))
449                 {
450                 tab_completion_emit_tab_signal(td);
451                 }
452 }
453
454 static GtkWidget *tab_completion_create_complete_button(GtkWidget *window, GtkWidget *entry)
455 {
456         GtkWidget *button;
457         GtkWidget *icon;
458         GdkPixmap *pixmap = NULL;
459         GdkBitmap *mask = NULL;
460         GtkStyle *style;
461
462         style = gtk_widget_get_style(window);
463         pixmap = gdk_pixmap_create_from_xpm_d(window->window, &mask,
464                 &style->bg[GTK_STATE_NORMAL], (gchar **)tabcomp_xpm);
465
466         button = gtk_button_new();
467         GTK_WIDGET_UNSET_FLAGS(button, GTK_CAN_FOCUS);
468         gtk_signal_connect(GTK_OBJECT(button), "clicked", (GtkSignalFunc) tab_completion_button_pressed, entry);
469         
470         icon = gtk_pixmap_new(pixmap, mask);
471         gtk_container_add(GTK_CONTAINER(button), icon);
472         gtk_widget_show(icon);
473
474         return button;
475 }
476
477 /*
478  *----------------------------------------------------------------------------
479  * public interface
480  *----------------------------------------------------------------------------
481  */
482
483 GtkWidget *tab_completion_new_with_history(GtkWidget **entry, GtkWidget *window, gchar *text,
484                                            const gchar *history_key, gint max_levels,
485                                            void (*enter_func)(gchar *, gpointer), gpointer data)
486 {
487         GtkWidget *combo;
488         GtkWidget *button;
489         HistoryData *hd;
490         TabCompData *td;
491
492         combo = gtk_combo_new();
493         gtk_combo_set_use_arrows(GTK_COMBO(combo), FALSE);
494
495         button = tab_completion_create_complete_button(window, GTK_COMBO(combo)->entry);
496         gtk_box_pack_start(GTK_BOX(combo), button, FALSE, FALSE, 0);
497         gtk_box_reorder_child(GTK_BOX(combo), button, 1);
498         gtk_widget_show(button);
499         
500         tab_completion_add_to_entry(GTK_COMBO(combo)->entry, enter_func, data);
501
502         td = gtk_object_get_data(GTK_OBJECT(GTK_COMBO(combo)->entry), "tab_completion_data");
503         if (!td) return; /* this should never happen! */
504
505         td->combo = combo;
506         td->has_history = TRUE;
507         td->history_key = g_strdup(history_key);
508         td->history_levels = max_levels;
509
510         hd = history_list_find_by_key(td->history_key);
511         if (hd && hd->list)
512                 {
513                 gtk_combo_set_popdown_strings(GTK_COMBO(combo), hd->list);
514                 }
515
516         if (text) gtk_entry_set_text(GTK_ENTRY(GTK_COMBO(combo)->entry), text);
517
518         if (entry) *entry = GTK_COMBO(combo)->entry;
519         return combo;
520 }
521
522 gchar *tab_completion_set_to_last_history(GtkWidget *entry)
523 {
524         TabCompData *td = gtk_object_get_data(GTK_OBJECT(entry), "tab_completion_data");
525         gchar *buf;
526
527         if (!td || !td->has_history) return NULL;
528
529         buf = history_list_find_last_path_by_key(td->history_key);
530         if (buf)
531                 {
532                 gtk_entry_set_text(GTK_ENTRY(td->entry), buf);
533                 }
534
535         return buf;
536 }
537
538 void tab_completion_append_to_history(GtkWidget *entry, gchar *path)
539 {
540         TabCompData *td = gtk_object_get_data(GTK_OBJECT(entry), "tab_completion_data");
541         HistoryData *hd;
542
543         if (!td || !td->has_history) return;
544
545         history_list_add_to_key(td->history_key, path, td->history_levels);
546         hd = history_list_find_by_key(td->history_key);
547         if (hd && hd->list)
548                 {
549                 gtk_combo_set_popdown_strings(GTK_COMBO(td->combo), hd->list);
550                 }
551 }
552
553 GtkWidget *tab_completion_new(GtkWidget **entry, GtkWidget *window, gchar *text,
554                               void (*enter_func)(gchar *, gpointer), gpointer data)
555 {
556         GtkWidget *hbox;
557         GtkWidget *button;
558         GtkWidget *newentry;
559
560         hbox = gtk_hbox_new(FALSE, 0);
561
562         newentry = gtk_entry_new();
563         if (text) gtk_entry_set_text(GTK_ENTRY(newentry), text);
564         gtk_box_pack_start(GTK_BOX(hbox), newentry, TRUE, TRUE, 0);
565         gtk_widget_show(newentry);
566
567         button = tab_completion_create_complete_button(window, newentry);
568         gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 0);
569         gtk_widget_show(button);
570
571         tab_completion_add_to_entry(newentry, enter_func, data);
572
573         if (entry) *entry = newentry;
574         return hbox;
575 }
576
577 void tab_completion_add_to_entry(GtkWidget *entry, void (*enter_func)(gchar *, gpointer), gpointer data)
578 {
579         TabCompData *td;
580         if (!entry)
581                 {
582                 printf("Tab completion error: entry != NULL\n");
583                 return;
584                 }
585
586         td = g_new0(TabCompData, 1);
587         td->entry = entry;
588         td->dir_path = NULL;
589         td->file_list = NULL;
590         td->enter_func = enter_func;
591         td->enter_data = data;
592         td->tab_func = NULL;
593         td->tab_data = NULL;
594
595         td->has_history = FALSE;
596         td->history_key = NULL;
597         td->history_levels = 0;
598
599         gtk_object_set_data(GTK_OBJECT(td->entry), "tab_completion_data", td);
600
601         gtk_signal_connect(GTK_OBJECT(entry), "key_press_event",
602                         (GtkSignalFunc) tab_completion_key_pressed, td);
603         gtk_signal_connect(GTK_OBJECT(entry), "destroy",
604                         (GtkSignalFunc) tab_completion_destroy, td);
605 }
606
607 void tab_completion_add_tab_func(GtkWidget *entry, void (*tab_func)(gchar *, gpointer), gpointer data)
608 {
609         TabCompData *td = gtk_object_get_data(GTK_OBJECT(entry), "tab_completion_data");
610
611         if (!td) return;
612
613         td->tab_func = tab_func;
614         td->tab_data = data;
615 }
616
617 gchar *remove_trailing_slash(gchar *path)
618 {
619         gchar *ret;
620         gint l;
621         if (!path) return NULL;
622
623         ret = g_strdup(path);
624         l = strlen(ret);
625         if (l > 1 && ret[l - 1] == '/') ret[l - 1] = '\0';
626
627         return ret;
628 }
629