Sort headers using clang-tidy
[geeqie.git] / src / ui-pathsel.cc
1 /*
2  * Copyright (C) 2006 John Ellis
3  * Copyright (C) 2008 - 2016 The Geeqie Team
4  *
5  * Author: John Ellis
6  *
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.
11  *
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.
16  *
17  * You should have received a copy of the GNU General Public License along
18  * with this program; if not, write to the Free Software Foundation, Inc.,
19  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20  */
21
22 #include "ui-pathsel.h"
23
24 #include <cstring>
25
26 #include <dirent.h>
27 #include <sys/stat.h>
28 #include <sys/types.h>
29
30 #include <config.h>
31
32 #include "compat.h"
33 #include "debug.h"
34 #include "intl.h"
35 #include "main-defines.h"
36 #include "misc.h"
37 #include "options.h"
38 #include "ui-bookmark.h"
39 #include "ui-fileops.h"
40 #include "ui-menu.h"
41 #include "ui-misc.h"
42 #include "ui-tabcomp.h"
43 #include "ui-tree-edit.h"
44 #include "uri-utils.h"
45 #include "utilops.h"
46
47
48 enum {
49         DEST_WIDTH = 250,
50         DEST_HEIGHT = 210
51 };
52
53 enum {
54         RENAME_PRESS_DELAY = 333        /* 1/3 second, to allow double clicks */
55 };
56
57 #define PATH_SEL_USE_HEADINGS FALSE
58
59 enum {
60         FILTER_COLUMN_NAME = 0,
61         FILTER_COLUMN_FILTER
62 };
63
64 struct Dest_Data
65 {
66         GtkWidget *d_view;
67         GtkWidget *f_view;
68         GtkWidget *entry;
69         gchar *filter;
70         gchar *path;
71
72         GList *filter_list;
73         GList *filter_text_list;
74         GtkWidget *filter_combo;
75
76         gboolean show_hidden;
77         GtkWidget *hidden_button;
78
79         GtkWidget *bookmark_list;
80
81         GtkTreePath *right_click_path;
82
83         void (*select_func)(const gchar *path, gpointer data);
84         gpointer select_data;
85
86         GenericDialog *gd;      /* any open confirm dialogs ? */
87 };
88
89 struct DestDel_Data
90 {
91         Dest_Data *dd;
92         gchar *path;
93 };
94
95
96 static void dest_view_delete_dlg_cancel(GenericDialog *gd, gpointer data);
97
98
99 /*
100  *-----------------------------------------------------------------------------
101  * (private)
102  *-----------------------------------------------------------------------------
103  */
104
105 static void dest_free_data(GtkWidget *, gpointer data)
106 {
107         auto dd = static_cast<Dest_Data *>(data);
108
109         if (dd->gd)
110                 {
111                 GenericDialog *gd = dd->gd;
112                 dest_view_delete_dlg_cancel(dd->gd, dd->gd->data);
113                 generic_dialog_close(gd);
114                 }
115         if (dd->right_click_path) gtk_tree_path_free(dd->right_click_path);
116
117         g_free(dd->filter);
118         g_free(dd->path);
119         g_free(dd);
120 }
121
122 static gboolean dest_check_filter(const gchar *filter, const gchar *file)
123 {
124         const gchar *f_ptr = filter;
125         const gchar *strt_ptr;
126         gint i;
127         gint l;
128
129         l = strlen(file);
130
131         if (filter[0] == '*') return TRUE;
132         while (f_ptr < filter + strlen(filter))
133                 {
134                 strt_ptr = f_ptr;
135                 i=0;
136                 while (*f_ptr != ';' && *f_ptr != '\0')
137                         {
138                         f_ptr++;
139                         i++;
140                         }
141                 if (*f_ptr != '\0' && f_ptr[1] == ' ') f_ptr++; /* skip space immediately after separator */
142                 f_ptr++;
143                 /**
144                  * @FIXME utf8 */
145                 if (l >= i && g_ascii_strncasecmp(file + l - i, strt_ptr, i) == 0) return TRUE;
146                 }
147         return FALSE;
148 }
149
150 #ifndef CASE_SORT
151 #define CASE_SORT strcmp
152 #endif
153
154 static gint dest_sort_cb(gpointer a, gpointer b)
155 {
156         return CASE_SORT((gchar *)a, (gchar *)b);
157 }
158
159 static gboolean is_hidden(const gchar *name)
160 {
161         if (name[0] != '.') return FALSE;
162         if (name[1] == '\0') return FALSE;
163         if (name[1] == '.' && name[2] == '\0') return FALSE;
164         return TRUE;
165 }
166
167 static void dest_populate(Dest_Data *dd, const gchar *path)
168 {
169         DIR *dp;
170         struct dirent *dir;
171         struct stat ent_sbuf;
172         GList *path_list = nullptr;
173         GList *file_list = nullptr;
174         GList *list;
175         GtkListStore *store;
176         gchar *pathl;
177
178         if (!path) return;
179
180         pathl = path_from_utf8(path);
181         dp = opendir(pathl);
182         if (!dp)
183                 {
184                 /* dir not found */
185                 g_free(pathl);
186                 return;
187                 }
188         while ((dir = readdir(dp)) != nullptr)
189                 {
190                 if (!options->file_filter.show_dot_directory
191                     && dir->d_name[0] == '.' && dir->d_name[1] == '\0')
192                         continue;
193                 if (dir->d_name[0] == '.' && dir->d_name[1] == '.' && dir->d_name[2] == '\0'
194                     && pathl[0] == G_DIR_SEPARATOR && pathl[1] == '\0')
195                         continue; /* no .. for root directory */
196                 if (dd->show_hidden || !is_hidden(dir->d_name))
197                         {
198                         gchar *name = dir->d_name;
199                         gchar *filepath = g_build_filename(pathl, name, NULL);
200                         if (stat(filepath, &ent_sbuf) >= 0 && S_ISDIR(ent_sbuf.st_mode))
201                                 {
202                                 path_list = g_list_prepend(path_list, path_to_utf8(name));
203                                 }
204                         else if (dd->f_view)
205                                 {
206                                 if (!dd->filter || (dd->filter && dest_check_filter(dd->filter, name)))
207                                         file_list = g_list_prepend(file_list, path_to_utf8(name));
208                                 }
209                         g_free(filepath);
210                         }
211                 }
212         closedir(dp);
213         g_free(pathl);
214
215         path_list = g_list_sort(path_list, reinterpret_cast<GCompareFunc>(dest_sort_cb));
216         file_list = g_list_sort(file_list, reinterpret_cast<GCompareFunc>(dest_sort_cb));
217
218         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dd->d_view)));
219         gtk_list_store_clear(store);
220
221         list = path_list;
222         while (list)
223                 {
224                 GtkTreeIter iter;
225                 gchar *filepath;
226
227                 if (strcmp(static_cast<const gchar *>(list->data), ".") == 0)
228                         {
229                         filepath = g_strdup(path);
230                         }
231                 else if (strcmp(static_cast<const gchar *>(list->data), "..") == 0)
232                         {
233                         gchar *p;
234                         filepath = g_strdup(path);
235                         p = const_cast<gchar *>(filename_from_path(filepath));
236                         if (p - 1 != filepath) p--;
237                         p[0] = '\0';
238                         }
239                 else
240                         {
241                         filepath = g_build_filename(path, list->data, NULL);
242                         }
243
244                 gtk_list_store_append(store, &iter);
245                 gtk_list_store_set(store, &iter, 0, list->data, 1, filepath, -1);
246
247                 g_free(filepath);
248                 list = list->next;
249                 }
250
251         g_list_free_full(path_list, g_free);
252
253
254         if (dd->f_view)
255                 {
256                 store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dd->f_view)));
257                 gtk_list_store_clear(store);
258
259                 list = file_list;
260                 while (list)
261                         {
262                         GtkTreeIter iter;
263                         gchar *filepath;
264                         auto name = static_cast<const gchar *>(list->data);
265
266                         filepath = g_build_filename(path, name, NULL);
267
268                         gtk_list_store_append(store, &iter);
269                         gtk_list_store_set(store, &iter, 0, name, 1, filepath, -1);
270
271                         g_free(filepath);
272                         list = list->next;
273                         }
274
275                 g_list_free_full(file_list, g_free);
276                 }
277
278         g_free(dd->path);
279         dd->path = g_strdup(path);
280 }
281
282 static void dest_change_dir(Dest_Data *dd, const gchar *path, gboolean retain_name)
283 {
284         const gchar *old_name = nullptr;
285         gchar *full_path;
286         gchar *new_directory;
287
288         if (retain_name)
289                 {
290                 const gchar *buf = gq_gtk_entry_get_text(GTK_ENTRY(dd->entry));
291
292                 if (!isdir(buf)) old_name = filename_from_path(buf);
293                 }
294
295         full_path = g_build_filename(path, old_name, NULL);
296         if (old_name)
297                 new_directory = g_path_get_dirname(full_path);
298         else
299                 new_directory = g_strdup(full_path);
300
301         gq_gtk_entry_set_text(GTK_ENTRY(dd->entry), full_path);
302
303         dest_populate(dd, new_directory);
304         g_free(new_directory);
305
306         if (old_name)
307                 {
308                 gchar *basename = g_path_get_basename(full_path);
309
310                 gtk_editable_select_region(GTK_EDITABLE(dd->entry), strlen(full_path) - strlen(basename), strlen(full_path));
311                 g_free(basename);
312                 }
313
314         g_free(full_path);
315 }
316
317 /*
318  *-----------------------------------------------------------------------------
319  * drag and drop
320  *-----------------------------------------------------------------------------
321  */
322
323 enum {
324         TARGET_URI_LIST,
325         TARGET_TEXT_PLAIN
326 };
327
328 static GtkTargetEntry dest_drag_types[] = {
329         { const_cast<gchar *>("text/uri-list"), 0, TARGET_URI_LIST },
330         { const_cast<gchar *>("text/plain"),    0, TARGET_TEXT_PLAIN }
331 };
332 enum {
333         dest_drag_types_n = 2
334 };
335
336
337 static void dest_dnd_set_data(GtkWidget *view, GdkDragContext *,
338                                   GtkSelectionData *selection_data,
339                                   guint, guint, gpointer)
340 {
341         gchar *path = nullptr;
342         GList *list = nullptr;
343         GtkTreeModel *model;
344         GtkTreeSelection *selection;
345         GtkTreeIter iter;
346
347         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
348         if (!gtk_tree_selection_get_selected(selection, &model, &iter)) return;
349
350         gtk_tree_model_get(model, &iter, 1, &path, -1);
351         if (!path) return;
352
353         list = g_list_append(list, path);
354
355         gchar **uris = uris_from_pathlist(list);
356         gboolean ret = gtk_selection_data_set_uris(selection_data, uris);
357         if (!ret)
358                 {
359                 char *str = g_strjoinv("\r\n", uris);
360                 ret = gtk_selection_data_set_text(selection_data, str, -1);
361                 g_free(str);
362                 }
363
364         g_list_free_full(list, g_free);
365 }
366
367 static void dest_dnd_init(Dest_Data *dd)
368 {
369         gtk_tree_view_enable_model_drag_source(GTK_TREE_VIEW(dd->d_view), GDK_BUTTON1_MASK,
370                                                dest_drag_types, dest_drag_types_n,
371                                                static_cast<GdkDragAction>(GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK | GDK_ACTION_ASK));
372         g_signal_connect(G_OBJECT(dd->d_view), "drag_data_get",
373                          G_CALLBACK(dest_dnd_set_data), dd);
374
375         if (dd->f_view)
376                 {
377                 gtk_tree_view_enable_model_drag_source(GTK_TREE_VIEW(dd->f_view), GDK_BUTTON1_MASK,
378                                                        dest_drag_types, dest_drag_types_n,
379                                                        static_cast<GdkDragAction>(GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK | GDK_ACTION_ASK));
380                 g_signal_connect(G_OBJECT(dd->f_view), "drag_data_get",
381                                  G_CALLBACK(dest_dnd_set_data), dd);
382                 }
383 }
384
385
386 /*
387  *-----------------------------------------------------------------------------
388  * destination widget file management utils
389  *-----------------------------------------------------------------------------
390  */
391
392 static void dest_view_store_selection(Dest_Data *dd, GtkTreeView *view)
393 {
394         GtkTreeModel *model;
395         GtkTreeSelection *selection;
396         GtkTreeIter iter;
397
398         if (dd->right_click_path) gtk_tree_path_free(dd->right_click_path);
399         dd->right_click_path = nullptr;
400
401         selection = gtk_tree_view_get_selection(view);
402         if (!gtk_tree_selection_get_selected(selection, &model, &iter))
403                 {
404                 return;
405                 }
406
407         dd->right_click_path = gtk_tree_model_get_path(model, &iter);
408 }
409
410 static gint dest_view_rename_cb(TreeEditData *ted, const gchar *old_name, const gchar *new_name, gpointer data)
411 {
412         auto dd = static_cast<Dest_Data *>(data);
413         GtkTreeModel *model;
414         GtkTreeIter iter;
415         gchar *buf;
416         gchar *old_path;
417         gchar *new_path;
418
419         model = gtk_tree_view_get_model(GTK_TREE_VIEW(ted->tree));
420         gtk_tree_model_get_iter(model, &iter, dd->right_click_path);
421
422         gtk_tree_model_get(model, &iter, 1, &old_path, -1);
423         if (!old_path) return FALSE;
424
425         buf = remove_level_from_path(old_path);
426         new_path = g_build_filename(buf, new_name, NULL);
427         g_free(buf);
428
429         if (isname(new_path))
430                 {
431                 buf = g_strdup_printf(_("A file with name %s already exists."), new_name);
432                 warning_dialog(_("Rename failed"), buf, GQ_ICON_DIALOG_INFO, dd->entry);
433                 g_free(buf);
434                 }
435         else if (!rename_file(old_path, new_path))
436                 {
437                 buf = g_strdup_printf(_("Failed to rename %s to %s."), old_name, new_name);
438                 warning_dialog(_("Rename failed"), buf, GQ_ICON_DIALOG_ERROR, dd->entry);
439                 g_free(buf);
440                 }
441         else
442                 {
443                 const gchar *text;
444
445                 gtk_list_store_set(GTK_LIST_STORE(model), &iter, 0, new_name, 1, new_path, -1);
446
447                 text = gq_gtk_entry_get_text(GTK_ENTRY(dd->entry));
448                 if (text && old_path && strcmp(text, old_path) == 0)
449                         {
450                         gq_gtk_entry_set_text(GTK_ENTRY(dd->entry), new_path);
451                         }
452                 }
453
454         g_free(old_path);
455         g_free(new_path);
456
457         return TRUE;
458 }
459
460 static void dest_view_rename(Dest_Data *dd, GtkTreeView *view)
461 {
462         GtkTreeModel *model;
463         GtkTreeIter iter;
464         gchar *text;
465
466         if (!dd->right_click_path) return;
467
468         model = gtk_tree_view_get_model(view);
469         gtk_tree_model_get_iter(model, &iter, dd->right_click_path);
470         gtk_tree_model_get(model, &iter, 0, &text, -1);
471
472         tree_edit_by_path(view, dd->right_click_path, 0, text,
473                           dest_view_rename_cb, dd);
474
475         g_free(text);
476 }
477
478 static void dest_view_delete_dlg_cancel(GenericDialog *, gpointer data)
479 {
480         auto dl = static_cast<DestDel_Data *>(data);
481
482         dl->dd->gd = nullptr;
483         g_free(dl->path);
484         g_free(dl);
485 }
486
487 static void dest_view_delete_dlg_ok_cb(GenericDialog *gd, gpointer data)
488 {
489         auto dl = static_cast<DestDel_Data *>(data);
490
491         if (!unlink_file(dl->path))
492                 {
493                 gchar *text = g_strdup_printf(_("Unable to delete file:\n%s"), dl->path);
494                 warning_dialog(_("File deletion failed"), text, GQ_ICON_DIALOG_WARNING, dl->dd->entry);
495                 g_free(text);
496                 }
497         else if (dl->dd->path)
498                 {
499                 /* refresh list */
500                 gchar *path = g_strdup(dl->dd->path);
501                 dest_populate(dl->dd, path);
502                 g_free(path);
503                 }
504
505         dest_view_delete_dlg_cancel(gd, data);
506 }
507
508 static void dest_view_delete(Dest_Data *dd, GtkTreeView *view)
509 {
510         gchar *path;
511         gchar *text;
512         DestDel_Data *dl;
513         GtkTreeModel *model;
514         GtkTreeIter iter;
515
516         if (view != GTK_TREE_VIEW(dd->f_view)) return;
517         if (!dd->right_click_path) return;
518
519         model = gtk_tree_view_get_model(view);
520         gtk_tree_model_get_iter(model, &iter, dd->right_click_path);
521         gtk_tree_model_get(model, &iter, 1, &path, -1);
522
523         if (!path) return;
524
525         dl = g_new(DestDel_Data, 1);
526         dl->dd = dd;
527         dl->path = path;
528
529         if (dd->gd)
530                 {
531                 GenericDialog *gd = dd->gd;
532                 dest_view_delete_dlg_cancel(dd->gd, dd->gd->data);
533                 generic_dialog_close(gd);
534                 }
535
536         dd->gd = generic_dialog_new(_("Delete file"), "dlg_confirm",
537                                     dd->entry, TRUE,
538                                     dest_view_delete_dlg_cancel, dl);
539
540         generic_dialog_add_button(dd->gd, GQ_ICON_DELETE, "Delete", dest_view_delete_dlg_ok_cb, TRUE);
541
542         text = g_strdup_printf(_("About to delete the file:\n %s"), path);
543         generic_dialog_add_message(dd->gd, GQ_ICON_DIALOG_QUESTION,
544                                    _("Delete file"), text, TRUE);
545         g_free(text);
546
547         gtk_widget_show(dd->gd->dialog);
548 }
549
550 static void dest_view_bookmark(Dest_Data *dd, GtkTreeView *view)
551 {
552         GtkTreeModel *model;
553         GtkTreeIter iter;
554         gchar *path;
555
556         if (!dd->right_click_path) return;
557
558         model = gtk_tree_view_get_model(view);
559         gtk_tree_model_get_iter(model, &iter, dd->right_click_path);
560         gtk_tree_model_get(model, &iter, 1, &path, -1);
561
562         bookmark_list_add(dd->bookmark_list, filename_from_path(path), path);
563         g_free(path);
564 }
565
566 static void dest_popup_dir_rename_cb(GtkWidget *, gpointer data)
567 {
568         auto dd = static_cast<Dest_Data *>(data);
569         dest_view_rename(dd, GTK_TREE_VIEW(dd->d_view));
570 }
571
572 static void dest_popup_dir_bookmark_cb(GtkWidget *, gpointer data)
573 {
574         auto dd = static_cast<Dest_Data *>(data);
575         dest_view_bookmark(dd, GTK_TREE_VIEW(dd->d_view));
576 }
577
578 static void dest_popup_file_rename_cb(GtkWidget *, gpointer data)
579 {
580         auto dd = static_cast<Dest_Data *>(data);
581         dest_view_rename(dd, GTK_TREE_VIEW(dd->f_view));
582 }
583
584 static void dest_popup_file_delete_cb(GtkWidget *, gpointer data)
585 {
586         auto dd = static_cast<Dest_Data *>(data);
587         dest_view_delete(dd, GTK_TREE_VIEW(dd->f_view));
588 }
589
590 static void dest_popup_file_bookmark_cb(GtkWidget *, gpointer data)
591 {
592         auto dd = static_cast<Dest_Data *>(data);
593         dest_view_bookmark(dd, GTK_TREE_VIEW(dd->f_view));
594 }
595
596 static gboolean dest_popup_menu(Dest_Data *dd, GtkTreeView *view, guint, guint32, gboolean local)
597 {
598         GtkWidget *menu;
599
600         if (!dd->right_click_path) return FALSE;
601
602         if (view == GTK_TREE_VIEW(dd->d_view))
603                 {
604                 GtkTreeModel *model;
605                 GtkTreeIter iter;
606                 gchar *text;
607                 gboolean normal_dir;
608
609                 model = gtk_tree_view_get_model(view);
610                 gtk_tree_model_get_iter(model, &iter, dd->right_click_path);
611                 gtk_tree_model_get(model, &iter, 0, &text, -1);
612
613                 if (!text) return FALSE;
614
615                 normal_dir = (strcmp(text, ".") == 0 || strcmp(text, "..") == 0);
616
617                 menu = popup_menu_short_lived();
618                 menu_item_add_sensitive(menu, _("_Rename"), !normal_dir,
619                               G_CALLBACK(dest_popup_dir_rename_cb), dd);
620                 menu_item_add_icon(menu, _("Add _Bookmark"), GQ_ICON_GO_JUMP,
621                               G_CALLBACK(dest_popup_dir_bookmark_cb), dd);
622                 }
623         else
624                 {
625                 menu = popup_menu_short_lived();
626                 menu_item_add(menu, _("_Rename"),
627                                 G_CALLBACK(dest_popup_file_rename_cb), dd);
628                 menu_item_add_icon(menu, _("_Delete"), GQ_ICON_DELETE,
629                                 G_CALLBACK(dest_popup_file_delete_cb), dd);
630                 menu_item_add_icon(menu, _("Add _Bookmark"), GQ_ICON_GO_JUMP,
631                                 G_CALLBACK(dest_popup_file_bookmark_cb), dd);
632                 }
633
634         if (local)
635                 {
636                 g_object_set_data(G_OBJECT(menu), "active_view", view);
637                 gtk_menu_popup_at_widget(GTK_MENU(menu), GTK_WIDGET(view), GDK_GRAVITY_CENTER, GDK_GRAVITY_CENTER, nullptr);
638                 }
639         else
640                 {
641                 gtk_menu_popup_at_pointer(GTK_MENU(menu), nullptr);
642
643                 }
644
645         return TRUE;
646 }
647
648 static gboolean dest_press_cb(GtkWidget *view, GdkEventButton *event, gpointer data)
649 {
650         auto dd = static_cast<Dest_Data *>(data);
651         GtkTreePath *tpath;
652         GtkTreeViewColumn *column;
653         gint cell_x;
654         gint cell_y;
655         GtkTreeModel *model;
656         GtkTreeIter iter;
657         GtkTreeSelection *selection;
658
659         if (event->button != MOUSE_BUTTON_RIGHT ||
660             !gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(view), event->x, event->y,
661                                            &tpath, &column, &cell_x, &cell_y))
662                 {
663                 return FALSE;
664                 }
665
666         model = gtk_tree_view_get_model(GTK_TREE_VIEW(view));
667         gtk_tree_model_get_iter(model, &iter, tpath);
668
669         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
670         gtk_tree_selection_select_iter(selection, &iter);
671
672         if (dd->right_click_path) gtk_tree_path_free(dd->right_click_path);
673         dd->right_click_path = tpath;
674
675         return dest_popup_menu(dd, GTK_TREE_VIEW(view), 0, event->time, FALSE);
676 }
677
678 static gboolean dest_keypress_cb(GtkWidget *view, GdkEventKey *event, gpointer data)
679 {
680         auto dd = static_cast<Dest_Data *>(data);
681
682         switch (event->keyval)
683                 {
684                 case GDK_KEY_F10:
685                         if (!(event->state & GDK_CONTROL_MASK)) return FALSE;
686                         /* fall through */
687                 case GDK_KEY_Menu:
688                         dest_view_store_selection(dd, GTK_TREE_VIEW(view));
689                         dest_popup_menu(dd, GTK_TREE_VIEW(view), 0, event->time, TRUE);
690                         return TRUE;
691                         break;
692                 case 'R': case 'r':
693                         if (event->state & GDK_CONTROL_MASK)
694                                 {
695                                 dest_view_store_selection(dd, GTK_TREE_VIEW(view));
696                                 dest_view_rename(dd, GTK_TREE_VIEW(view));
697                                 return TRUE;
698                                 }
699                         break;
700                 case GDK_KEY_Delete:
701                         dest_view_store_selection(dd, GTK_TREE_VIEW(view));
702                         dest_view_delete(dd, GTK_TREE_VIEW(view));
703                         return TRUE;
704                         break;
705                 case 'B' : case 'b':
706                         if (event->state & GDK_CONTROL_MASK)
707                                 {
708                                 dest_view_store_selection(dd, GTK_TREE_VIEW(view));
709                                 dest_view_bookmark(dd, GTK_TREE_VIEW(view));
710                                 return TRUE;
711                                 }
712                         break;
713                 }
714
715         return FALSE;
716 }
717
718 static void file_util_create_dir_cb(gboolean success, const gchar *new_path, gpointer data)
719 {
720         auto dd = static_cast<Dest_Data *>(data);
721         const gchar *text;
722         GtkListStore *store;
723         GtkTreeIter iter;
724
725         if (!success)
726                 {
727                 return;
728                 }
729
730         if (new_path == nullptr)
731                 {
732                 return;
733                 }
734
735         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dd->d_view)));
736
737         text = filename_from_path(new_path);
738
739         gtk_list_store_append(store, &iter);
740         gtk_list_store_set(store, &iter, 0, text, 1, new_path, -1);
741
742         if (dd->right_click_path)
743                 {
744                 gtk_tree_path_free(dd->right_click_path);
745                 }
746         dd->right_click_path = gtk_tree_model_get_path(GTK_TREE_MODEL(store), &iter);
747
748         gq_gtk_entry_set_text(GTK_ENTRY(dd->entry), new_path);
749
750         gtk_widget_grab_focus(GTK_WIDGET(dd->entry));
751 }
752
753 static void dest_new_dir_cb(GtkWidget *widget, gpointer data)
754 {
755         auto dd = static_cast<Dest_Data *>(data);
756
757 /**
758  * @FIXME on exit from the "new folder" modal dialog, focus returns to the main Geeqie
759  * window rather than the file dialog window. gtk_window_present() does not seem to
760  * function unless the window was previously minimized.
761  */
762         file_util_create_dir(gq_gtk_entry_get_text(GTK_ENTRY(dd->entry)), widget, file_util_create_dir_cb, data);
763 }
764
765 /*
766  *-----------------------------------------------------------------------------
767  * destination widget file selection, traversal, view options
768  *-----------------------------------------------------------------------------
769  */
770
771 static void dest_select_cb(GtkTreeSelection *selection, gpointer data)
772 {
773         auto dd = static_cast<Dest_Data *>(data);
774         GtkTreeView *view;
775         GtkTreeModel *store;
776         GtkTreeIter iter;
777         gchar *path;
778
779         if (!gtk_tree_selection_get_selected(selection, nullptr, &iter)) return;
780
781         view = gtk_tree_selection_get_tree_view(selection);
782         store = gtk_tree_view_get_model(view);
783         gtk_tree_model_get(store, &iter, 1, &path, -1);
784
785         if (view == GTK_TREE_VIEW(dd->d_view))
786                 {
787                 dest_change_dir(dd, path, (dd->f_view != nullptr));
788                 }
789         else
790                 {
791                 gq_gtk_entry_set_text(GTK_ENTRY(dd->entry), path);
792                 }
793
794         g_free(path);
795 }
796
797 static void dest_activate_cb(GtkWidget *view, GtkTreePath *tpath, GtkTreeViewColumn *, gpointer data)
798 {
799         auto dd = static_cast<Dest_Data *>(data);
800         GtkTreeModel *store;
801         GtkTreeIter iter;
802         gchar *path;
803
804         store = gtk_tree_view_get_model(GTK_TREE_VIEW(view));
805         gtk_tree_model_get_iter(store, &iter, tpath);
806         gtk_tree_model_get(store, &iter, 1, &path, -1);
807
808         if (view == dd->d_view)
809                 {
810                 dest_change_dir(dd, path, (dd->f_view != nullptr));
811                 }
812         else
813                 {
814                 if (dd->select_func)
815                         {
816                         dd->select_func(path, dd->select_data);
817                         }
818                 }
819
820         g_free(path);
821 }
822
823 static void dest_home_cb(GtkWidget *, gpointer data)
824 {
825         auto dd = static_cast<Dest_Data *>(data);
826
827         dest_change_dir(dd, homedir(), (dd->f_view != nullptr));
828 }
829
830 static void dest_show_hidden_cb(GtkWidget *, gpointer data)
831 {
832         auto dd = static_cast<Dest_Data *>(data);
833         gchar *buf;
834
835         dd->show_hidden = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(dd->hidden_button));
836
837         buf = g_strdup(dd->path);
838         dest_populate(dd, buf);
839         g_free(buf);
840 }
841
842 static void dest_entry_changed_cb(GtkEditable *, gpointer data)
843 {
844         auto dd = static_cast<Dest_Data *>(data);
845         const gchar *path;
846         gchar *buf;
847
848         path = gq_gtk_entry_get_text(GTK_ENTRY(dd->entry));
849         if (dd->path && strcmp(path, dd->path) == 0) return;
850
851         buf = remove_level_from_path(path);
852
853         if (buf && (!dd->path || strcmp(buf, dd->path) != 0))
854                 {
855                 gchar *tmp = remove_trailing_slash(path);
856                 if (isdir(tmp))
857                         {
858                         dest_populate(dd, tmp);
859                         }
860                 else if (isdir(buf))
861                         {
862                         dest_populate(dd, buf);
863                         }
864                 g_free(tmp);
865                 }
866         g_free(buf);
867 }
868
869 static void dest_filter_list_sync(Dest_Data *dd)
870 {
871         GtkWidget *entry;
872         GtkListStore *store;
873         gchar *old_text;
874         GList *fwork;
875         GList *twork;
876
877         if (!dd->filter_list || !dd->filter_combo) return;
878
879         entry = gtk_bin_get_child(GTK_BIN(dd->filter_combo));
880         old_text = g_strdup(gq_gtk_entry_get_text(GTK_ENTRY(entry)));
881
882         store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(dd->filter_combo)));
883         gtk_list_store_clear(store);
884
885         fwork = dd->filter_list;
886         twork = dd->filter_text_list;
887         while (fwork && twork)
888                 {
889                 GtkTreeIter iter;
890                 gchar *name;
891                 gchar *filter;
892
893                 name = static_cast<gchar *>(twork->data);
894                 filter = static_cast<gchar *>(fwork->data);
895
896                 gtk_list_store_append(store, &iter);
897                 gtk_list_store_set(store, &iter, FILTER_COLUMN_NAME, name,
898                                                  FILTER_COLUMN_FILTER, filter, -1);
899
900                 if (strcmp(old_text, filter) == 0)
901                         {
902                         gtk_combo_box_set_active_iter(GTK_COMBO_BOX(dd->filter_combo), &iter);
903                         }
904
905                 fwork = fwork->next;
906                 twork = twork->next;
907                 }
908
909         g_free(old_text);
910 }
911
912 static void dest_filter_add(Dest_Data *dd, const gchar *filter, const gchar *description, gboolean set)
913 {
914         GList *work;
915         gchar *buf;
916         gint c = 0;
917
918         if (!filter) return;
919
920         work = dd->filter_list;
921         while (work)
922                 {
923                 auto f = static_cast<gchar *>(work->data);
924
925                 if (strcmp(f, filter) == 0)
926                         {
927                         if (set) gtk_combo_box_set_active(GTK_COMBO_BOX(dd->filter_combo), c);
928                         return;
929                         }
930                 work = work->next;
931                 c++;
932                 }
933
934         dd->filter_list = uig_list_insert_link(dd->filter_list, g_list_last(dd->filter_list), g_strdup(filter));
935
936         if (description)
937                 {
938                 buf = g_strdup_printf("%s  ( %s )", description, filter);
939                 }
940         else
941                 {
942                 buf = g_strdup_printf("( %s )", filter);
943                 }
944         dd->filter_text_list = uig_list_insert_link(dd->filter_text_list, g_list_last(dd->filter_text_list), buf);
945
946         if (set) gq_gtk_entry_set_text(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(dd->filter_combo))), filter);
947         dest_filter_list_sync(dd);
948 }
949
950 static void dest_filter_clear(Dest_Data *dd)
951 {
952         g_list_free_full(dd->filter_list, g_free);
953         dd->filter_list = nullptr;
954
955         g_list_free_full(dd->filter_text_list, g_free);
956         dd->filter_text_list = nullptr;
957
958         dest_filter_add(dd, "*", _("All Files"), TRUE);
959 }
960
961 static void dest_filter_changed_cb(GtkEditable *, gpointer data)
962 {
963         auto dd = static_cast<Dest_Data *>(data);
964         GtkWidget *entry;
965         const gchar *buf;
966         gchar *path;
967
968         entry = gtk_bin_get_child(GTK_BIN(dd->filter_combo));
969         buf = gq_gtk_entry_get_text(GTK_ENTRY(entry));
970
971         g_free(dd->filter);
972         dd->filter = nullptr;
973         if (strlen(buf) > 0) dd->filter = g_strdup(buf);
974
975         path = g_strdup(dd->path);
976         dest_populate(dd, path);
977         g_free(path);
978 }
979
980 static void dest_bookmark_select_cb(const gchar *path, gpointer data)
981 {
982         auto dd = static_cast<Dest_Data *>(data);
983
984         if (isdir(path))
985                 {
986                 dest_change_dir(dd, path, (dd->f_view != nullptr));
987                 }
988         else if (isfile(path) && dd->f_view)
989                 {
990                 gq_gtk_entry_set_text(GTK_ENTRY(dd->entry), path);
991                 }
992 }
993
994 /*
995  *-----------------------------------------------------------------------------
996  * destination widget setup routines (public)
997  *-----------------------------------------------------------------------------
998  */
999
1000 GtkWidget *path_selection_new_with_files(GtkWidget *entry, const gchar *path,
1001                                          const gchar *filter, const gchar *filter_desc)
1002 {
1003         Dest_Data *dd;
1004         GtkCellRenderer *renderer;
1005         GtkListStore *store;
1006         GtkTreeSelection *selection;
1007         GtkTreeViewColumn *column;
1008         GtkWidget *hbox1; // home, new folder, hidden, filter
1009         GtkWidget *hbox2; // files paned
1010         GtkWidget *hbox3; // filter
1011         GtkWidget *paned;
1012         GtkWidget *scrolled;
1013         GtkWidget *table; // main box
1014
1015         dd = g_new0(Dest_Data, 1);
1016
1017         table = gtk_box_new(GTK_ORIENTATION_VERTICAL, PREF_PAD_GAP);
1018
1019         dd->entry = entry;
1020         g_object_set_data(G_OBJECT(dd->entry), "destination_data", dd);
1021
1022         hbox1 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_GAP);
1023         gtk_box_set_spacing(GTK_BOX(hbox1), PREF_PAD_BUTTON_GAP);
1024         pref_button_new(hbox1, nullptr, _("Home"),
1025                         G_CALLBACK(dest_home_cb), dd);
1026         pref_button_new(hbox1, nullptr, _("New folder"),
1027                         G_CALLBACK(dest_new_dir_cb), dd);
1028
1029         dd->hidden_button = gtk_check_button_new_with_label(_("Show hidden"));
1030         g_signal_connect(G_OBJECT(dd->hidden_button), "clicked",
1031                          G_CALLBACK(dest_show_hidden_cb), dd);
1032         gq_gtk_box_pack_end(GTK_BOX(hbox1), dd->hidden_button, FALSE, FALSE, 0);
1033         gtk_widget_show(dd->hidden_button);
1034
1035         gq_gtk_box_pack_start(GTK_BOX(table), hbox1, FALSE, FALSE, 0);
1036         gtk_widget_show_all(hbox1);
1037
1038         hbox2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_GAP);
1039         if (filter)
1040                 {
1041                 paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
1042                 DEBUG_NAME(paned);
1043                 gq_gtk_box_pack_end(GTK_BOX(table), paned, TRUE , TRUE, 0);
1044                 gtk_widget_show(paned);
1045                 gtk_paned_add1(GTK_PANED(paned), hbox2);
1046                 }
1047         else
1048                 {
1049                 paned = nullptr;
1050                 gq_gtk_box_pack_end(GTK_BOX(table), hbox2, TRUE, TRUE, 0);
1051                 }
1052         gtk_widget_show(hbox2);
1053
1054         /* bookmarks */
1055         scrolled = bookmark_list_new(nullptr, dest_bookmark_select_cb, dd);
1056         gq_gtk_box_pack_start(GTK_BOX(hbox2), scrolled, FALSE, FALSE, 0);
1057         gtk_widget_show(scrolled);
1058
1059         dd->bookmark_list = scrolled;
1060
1061         scrolled = gq_gtk_scrolled_window_new(nullptr, nullptr);
1062         gq_gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
1063         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
1064                                        GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS);
1065         gq_gtk_box_pack_start(GTK_BOX(hbox2), scrolled, TRUE, TRUE, 0);
1066         gtk_widget_show(scrolled);
1067
1068         store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING);
1069         dd->d_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
1070         g_object_unref(store);
1071
1072         gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(dd->d_view), PATH_SEL_USE_HEADINGS);
1073
1074         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dd->d_view));
1075         gtk_tree_selection_set_mode(GTK_TREE_SELECTION(selection), GTK_SELECTION_SINGLE);
1076
1077         column = gtk_tree_view_column_new();
1078         gtk_tree_view_column_set_title(column, _("Folders"));
1079         gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
1080
1081         renderer = gtk_cell_renderer_text_new();
1082         gtk_tree_view_column_pack_start(column, renderer, TRUE);
1083         gtk_tree_view_column_add_attribute(column, renderer, "text", 0);
1084
1085         gtk_tree_view_append_column(GTK_TREE_VIEW(dd->d_view), column);
1086
1087 #if 0
1088         /* only for debugging */
1089         column = gtk_tree_view_column_new();
1090         gtk_tree_view_column_set_title(column, _("Path"));
1091         renderer = gtk_cell_renderer_text_new();
1092         gtk_tree_view_column_pack_start(column, renderer, TRUE);
1093         gtk_tree_view_column_add_attribute(column, renderer, "text", 1);
1094         gtk_tree_view_append_column(GTK_TREE_VIEW(dd->d_view), column);
1095 #endif
1096
1097         gtk_widget_set_size_request(dd->d_view, DEST_WIDTH, DEST_HEIGHT);
1098         gq_gtk_container_add(GTK_WIDGET(scrolled), dd->d_view);
1099         gtk_widget_show(dd->d_view);
1100
1101         g_signal_connect(G_OBJECT(dd->d_view), "button_press_event",
1102                          G_CALLBACK(dest_press_cb), dd);
1103         g_signal_connect(G_OBJECT(dd->d_view), "key_press_event",
1104                          G_CALLBACK(dest_keypress_cb), dd);
1105         g_signal_connect(G_OBJECT(dd->d_view), "row_activated",
1106                          G_CALLBACK(dest_activate_cb), dd);
1107         g_signal_connect(G_OBJECT(dd->d_view), "destroy",
1108                          G_CALLBACK(dest_free_data), dd);
1109
1110         if (filter)
1111                 {
1112                 GtkListStore *store;
1113
1114                 hbox3 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
1115                 pref_label_new(hbox3, _("Filter:"));
1116
1117                 store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING);
1118
1119                 dd->filter_combo = gtk_combo_box_new_with_model_and_entry(GTK_TREE_MODEL(store));
1120                 gtk_combo_box_set_entry_text_column(GTK_COMBO_BOX(dd->filter_combo),
1121                                                                                                                 FILTER_COLUMN_FILTER);
1122                 gtk_widget_set_tooltip_text(dd->filter_combo, _("File extension.\nAll files: *\nOr, e.g. png;jpg\nOr, e.g. png; jpg"));
1123
1124                 g_object_unref(store);
1125                 gtk_cell_layout_clear(GTK_CELL_LAYOUT(dd->filter_combo));
1126                 renderer = gtk_cell_renderer_text_new();
1127                 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(dd->filter_combo), renderer, TRUE);
1128                 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(dd->filter_combo), renderer, "text", FILTER_COLUMN_NAME, NULL);
1129                 gq_gtk_box_pack_start(GTK_BOX(hbox3), dd->filter_combo, TRUE, TRUE, 0);
1130                 gtk_widget_show(dd->filter_combo);
1131
1132                 gq_gtk_box_pack_end(GTK_BOX(hbox1), hbox3, FALSE, FALSE, 0);
1133                 gtk_widget_show(hbox3);
1134
1135                 scrolled = gq_gtk_scrolled_window_new(nullptr, nullptr);
1136                 gq_gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
1137                 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
1138                                                GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS);
1139                 if (paned)
1140                         {
1141                         gtk_paned_add2(GTK_PANED(paned), scrolled);
1142                         }
1143                 else
1144                         {
1145                         gq_gtk_box_pack_end(GTK_BOX(table), paned, FALSE, FALSE, 0);
1146                         }
1147                 gtk_widget_show(scrolled);
1148
1149                 store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING);
1150                 dd->f_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
1151                 g_object_unref(store);
1152
1153                 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(dd->f_view), PATH_SEL_USE_HEADINGS);
1154
1155                 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dd->f_view));
1156                 gtk_tree_selection_set_mode(GTK_TREE_SELECTION(selection), GTK_SELECTION_SINGLE);
1157
1158                 column = gtk_tree_view_column_new();
1159                 gtk_tree_view_column_set_title(column, _("Files"));
1160                 gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
1161
1162                 renderer = gtk_cell_renderer_text_new();
1163                 gtk_tree_view_column_pack_start(column, renderer, TRUE);
1164                 gtk_tree_view_column_add_attribute(column, renderer, "text", 0);
1165
1166                 gtk_tree_view_append_column(GTK_TREE_VIEW(dd->f_view), column);
1167
1168                 gtk_widget_set_size_request(dd->f_view, DEST_WIDTH, DEST_HEIGHT);
1169                 gq_gtk_container_add(GTK_WIDGET(scrolled), dd->f_view);
1170                 gtk_widget_show(dd->f_view);
1171
1172                 g_signal_connect(G_OBJECT(dd->f_view), "button_press_event",
1173                                  G_CALLBACK(dest_press_cb), dd);
1174                 g_signal_connect(G_OBJECT(dd->f_view), "key_press_event",
1175                                  G_CALLBACK(dest_keypress_cb), dd);
1176                 g_signal_connect(G_OBJECT(dd->f_view), "row_activated",
1177                                  G_CALLBACK(dest_activate_cb), dd);
1178                 g_signal_connect(selection, "changed",
1179                                  G_CALLBACK(dest_select_cb), dd);
1180
1181                 dest_filter_clear(dd);
1182                 dest_filter_add(dd, filter, filter_desc, TRUE);
1183
1184                 dd->filter = g_strdup(gq_gtk_entry_get_text(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(dd->filter_combo)))));
1185                 }
1186
1187         if (path && path[0] == G_DIR_SEPARATOR && isdir(path))
1188                 {
1189                 dest_populate(dd, path);
1190                 }
1191         else
1192                 {
1193                 gchar *buf = remove_level_from_path(path);
1194                 if (buf && buf[0] == G_DIR_SEPARATOR && isdir(buf))
1195                         {
1196                         dest_populate(dd, buf);
1197                         }
1198                 else
1199                         {
1200                         gint pos = -1;
1201
1202                         dest_populate(dd, const_cast<gchar *>(homedir()));
1203                         if (path) gtk_editable_insert_text(GTK_EDITABLE(dd->entry), G_DIR_SEPARATOR_S, -1, &pos);
1204                         if (path) gtk_editable_insert_text(GTK_EDITABLE(dd->entry), path, -1, &pos);
1205                         }
1206                 g_free(buf);
1207                 }
1208
1209         if (dd->filter_combo)
1210                 {
1211                 g_signal_connect(G_OBJECT(gtk_bin_get_child(GTK_BIN(dd->filter_combo))), "changed",
1212                                  G_CALLBACK(dest_filter_changed_cb), dd);
1213                 }
1214         g_signal_connect(G_OBJECT(dd->entry), "changed",
1215                          G_CALLBACK(dest_entry_changed_cb), dd);
1216
1217         dest_dnd_init(dd);
1218
1219         return table;
1220 }
1221
1222 #pragma GCC diagnostic push
1223 #pragma GCC diagnostic ignored "-Wunused-function"
1224 GtkWidget *path_selection_new_unused(const gchar *path, GtkWidget *entry)
1225 {
1226         return path_selection_new_with_files(entry, path, nullptr, nullptr);
1227 }
1228
1229 void path_selection_sync_to_entry_unused(GtkWidget *entry)
1230 {
1231         auto *dd = static_cast<Dest_Data *>(g_object_get_data(G_OBJECT(entry), "destination_data"));
1232         const gchar *path;
1233
1234         if (!dd) return;
1235
1236         path = gq_gtk_entry_get_text(GTK_ENTRY(entry));
1237
1238         if (isdir(path) && (!dd->path || strcmp(path, dd->path) != 0))
1239                 {
1240                 dest_populate(dd, path);
1241                 }
1242         else
1243                 {
1244                 gchar *buf = remove_level_from_path(path);
1245                 if (isdir(buf) && (!dd->path || strcmp(buf, dd->path) != 0))
1246                         {
1247                         dest_populate(dd, buf);
1248                         }
1249                 g_free(buf);
1250                 }
1251 }
1252 #pragma GCC diagnostic pop
1253
1254 void path_selection_add_select_func(GtkWidget *entry,
1255                                     void (*func)(const gchar *, gpointer), gpointer data)
1256 {
1257         auto dd = static_cast<Dest_Data *>(g_object_get_data(G_OBJECT(entry), "destination_data"));
1258
1259         if (!dd) return;
1260
1261         dd->select_func = func;
1262         dd->select_data = data;
1263 }
1264
1265 void path_selection_add_filter(GtkWidget *entry, const gchar *filter, const gchar *description, gboolean set)
1266 {
1267         auto dd = static_cast<Dest_Data *>(g_object_get_data(G_OBJECT(entry), "destination_data"));
1268
1269         if (!dd) return;
1270         if (!filter) return;
1271
1272         dest_filter_add(dd, filter, description, set);
1273 }
1274
1275 void path_selection_clear_filter(GtkWidget *entry)
1276 {
1277         auto dd = static_cast<Dest_Data *>(g_object_get_data(G_OBJECT(entry), "destination_data"));
1278
1279         if (!dd) return;
1280
1281         dest_filter_clear(dd);
1282 }
1283 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */