Fix missing translation
[geeqie.git] / src / ui-utildlg.cc
1 /*
2  * Copyright (C) 2004 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-utildlg.h"
23
24 #include <cstring>
25
26 #include <config.h>
27
28 #include "compat.h"
29 #include "debug.h"
30 #include "filedata.h"
31 #include "intl.h"
32 #include "main-defines.h"
33 #include "misc.h"
34 #include "rcfile.h"
35 #include "ui-fileops.h"
36 #include "ui-misc.h"
37 #include "ui-pathsel.h"
38 #include "ui-tabcomp.h"
39 #include "window.h"
40
41 /*
42  *-----------------------------------------------------------------------------
43  * generic dialog
44  *-----------------------------------------------------------------------------
45  */
46
47 struct DialogWindow
48 {
49         gint x;
50         gint y;
51         gint w;
52         gint h;
53         gchar *title;
54         gchar *role;
55 };
56
57 static GList *dialog_windows = nullptr;
58
59 static void generic_dialog_save_window(const gchar *title, const gchar *role, gint x, gint y, gint h, gint w)
60 {
61         GList *work;
62
63         work = g_list_first(dialog_windows);
64         while (work)
65                 {
66                 auto dw = static_cast<DialogWindow *>(work->data);
67                 if (g_strcmp0(dw->title ,title) == 0 && g_strcmp0(dw->role, role) == 0)
68                         {
69                         dw->x = x;
70                         dw->y = y;
71                         dw->w = w;
72                         dw->h = h;
73                         return;
74                         }
75                 work = work->next;
76                 }
77
78         auto dw = g_new0(DialogWindow, 1);
79         dw->title = g_strdup(title);
80         dw->role = g_strdup(role);
81         dw->x = x;
82         dw->y = y;
83         dw->w = w;
84         dw->h = h;
85
86         dialog_windows = g_list_append(dialog_windows, dw);
87 }
88
89 static gboolean generic_dialog_find_window(const gchar *title, const gchar *role, gint *x, gint *y, gint *h, gint *w)
90 {
91         GList *work;
92
93         work = g_list_first(dialog_windows);
94         while (work)
95                 {
96                 auto dw = static_cast<DialogWindow *>(work->data);
97
98                 if (g_strcmp0(dw->title,title) == 0 && g_strcmp0(dw->role, role) == 0)
99                         {
100                         *x = dw->x;
101                         *y = dw->y;
102                         *w = dw->w;
103                         *h = dw->h;
104                         return TRUE;
105                         }
106                 work = work->next;
107                 }
108         return FALSE;
109 }
110
111 void generic_dialog_close(GenericDialog *gd)
112 {
113         gchar *ident_string;
114         gchar *full_title;
115         gchar *actual_title;
116         gint x;
117         gint y;
118         gint h;
119         gint w;
120
121         gdk_window_get_root_origin(gtk_widget_get_window (gd->dialog), &x, &y);
122         w = gdk_window_get_width(gtk_widget_get_window (gd->dialog));
123         h = gdk_window_get_height(gtk_widget_get_window (gd->dialog));
124
125         /* The window title is modified in window.cc: window_new()
126          * by appending the string " - Geeqie"
127          */
128         ident_string = g_strconcat(" - ", GQ_APPNAME, NULL);
129         full_title = g_strdup(gtk_window_get_title(GTK_WINDOW(gd->dialog)));
130         actual_title = strndup(full_title, g_strrstr(full_title, ident_string) - full_title);
131
132         generic_dialog_save_window(actual_title, gtk_window_get_role(GTK_WINDOW(gd->dialog)), x, y, w, h);
133
134         gq_gtk_widget_destroy(gd->dialog);
135         g_free(gd);
136         g_free(ident_string);
137         g_free(full_title);
138         g_free(actual_title);
139 }
140
141 static void generic_dialog_click_cb(GtkWidget *widget, gpointer data)
142 {
143         auto gd = static_cast<GenericDialog *>(data);
144         void (*func)(GenericDialog *, gpointer);
145         gboolean auto_close;
146
147         func = reinterpret_cast<void(*)(GenericDialog *, gpointer)>(g_object_get_data(G_OBJECT(widget), "dialog_function"));
148         auto_close = gd->auto_close;
149
150         if (func) func(gd, gd->data);
151         if (auto_close) generic_dialog_close(gd);
152 }
153
154 static gboolean generic_dialog_default_key_press_cb(GtkWidget *widget, GdkEventKey *event, gpointer data)
155 {
156         auto gd = static_cast<GenericDialog *>(data);
157
158         if (event->keyval == GDK_KEY_Return && gtk_widget_has_focus(widget)
159             && gd->default_cb)
160                 {
161                 gboolean auto_close;
162
163                 auto_close = gd->auto_close;
164                 gd->default_cb(gd, gd->data);
165                 if (auto_close) generic_dialog_close(gd);
166
167                 return TRUE;
168                 }
169         return FALSE;
170 }
171
172 void generic_dialog_attach_default(GenericDialog *gd, GtkWidget *widget)
173 {
174         if (!gd || !widget) return;
175         g_signal_connect(G_OBJECT(widget), "key_press_event",
176                          G_CALLBACK(generic_dialog_default_key_press_cb), gd);
177 }
178
179 static gboolean generic_dialog_key_press_cb(GtkWidget *widget, GdkEventKey *event, gpointer data)
180 {
181         auto gd = static_cast<GenericDialog *>(data);
182         gboolean auto_close = gd->auto_close;
183
184         if (event->keyval == GDK_KEY_Escape)
185                 {
186                 if (gd->cancel_cb)
187                         {
188                         gd->cancel_cb(gd, gd->data);
189                         if (auto_close) generic_dialog_close(gd);
190                         }
191                 else
192                         {
193                         if (auto_close) generic_dialog_click_cb(widget, data);
194                         }
195                 return TRUE;
196                 }
197         return FALSE;
198 }
199
200 static gboolean generic_dialog_delete_cb(GtkWidget *, GdkEventAny *, gpointer data)
201 {
202         auto gd = static_cast<GenericDialog *>(data);
203         gboolean auto_close;
204
205         auto_close = gd->auto_close;
206
207         if (gd->cancel_cb) gd->cancel_cb(gd, gd->data);
208         if (auto_close) generic_dialog_close(gd);
209
210         return TRUE;
211 }
212
213 static void generic_dialog_show_cb(GtkWidget *widget, gpointer data)
214 {
215         auto gd = static_cast<GenericDialog *>(data);
216         if (gd->cancel_button)
217                 {
218                 gtk_box_reorder_child(GTK_BOX(gd->hbox), gd->cancel_button, -1);
219                 }
220
221         g_signal_handlers_disconnect_by_func(G_OBJECT(widget), (gpointer)(generic_dialog_show_cb), gd);
222 }
223
224 gboolean generic_dialog_get_alternative_button_order(GtkWidget *widget)
225 {
226         GtkSettings *settings;
227         GObjectClass *klass;
228         gboolean alternative_order = FALSE;
229
230         settings = gtk_settings_get_for_screen(gtk_widget_get_screen(widget));
231         klass = G_OBJECT_CLASS(GTK_SETTINGS_GET_CLASS(settings));
232         if (g_object_class_find_property(klass, "gtk-alternative-button-order"))
233                 {
234                 g_object_get(settings, "gtk-alternative-button-order", &alternative_order, NULL);
235                 }
236
237         return alternative_order;
238 }
239
240 GtkWidget *generic_dialog_add_button(GenericDialog *gd, const gchar *icon_name, const gchar *text,
241                                      void (*func_cb)(GenericDialog *, gpointer), gboolean is_default)
242 {
243         GtkWidget *button;
244         gboolean alternative_order;
245
246         button = pref_button_new(nullptr, icon_name, text,
247                                  G_CALLBACK(generic_dialog_click_cb), gd);
248
249         gtk_widget_set_can_default(button, TRUE);
250         g_object_set_data(G_OBJECT(button), "dialog_function", reinterpret_cast<void *>(func_cb));
251
252         gq_gtk_container_add(GTK_WIDGET(gd->hbox), button);
253
254         alternative_order = generic_dialog_get_alternative_button_order(gd->hbox);
255
256         if (is_default)
257                 {
258                 gtk_widget_grab_default(button);
259                 gtk_widget_grab_focus(button);
260                 gd->default_cb = func_cb;
261
262                 if (!alternative_order) gtk_box_reorder_child(GTK_BOX(gd->hbox), button, -1);
263                 }
264         else
265                 {
266                 if (!alternative_order) gtk_box_reorder_child(GTK_BOX(gd->hbox), button, 0);
267                 }
268
269         gtk_widget_show(button);
270
271         return button;
272 }
273
274 /**
275  * @brief
276  * @param gd
277  * @param icon_stock_id
278  * @param heading
279  * @param text
280  * @param expand Used as the "expand" and "fill" parameters in the eventual call to gq_gtk_box_pack_start()
281  * @returns
282  *
283  *
284  */
285 GtkWidget *generic_dialog_add_message(GenericDialog *gd, const gchar *icon_name,
286                                       const gchar *heading, const gchar *text, gboolean expand)
287 {
288         GtkWidget *hbox;
289         GtkWidget *vbox;
290         GtkWidget *label;
291
292         hbox = pref_box_new(gd->vbox, expand, GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
293         if (icon_name)
294                 {
295                 GtkWidget *image;
296
297                 image = gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_DIALOG);
298                 gtk_widget_set_halign(GTK_WIDGET(image), GTK_ALIGN_CENTER);
299                 gtk_widget_set_valign(GTK_WIDGET(image), GTK_ALIGN_START);
300                 gq_gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 0);
301                 gtk_widget_show(image);
302                 }
303
304         vbox = pref_box_new(hbox, TRUE, GTK_ORIENTATION_VERTICAL, PREF_PAD_SPACE);
305         if (heading)
306                 {
307                 label = pref_label_new(vbox, heading);
308                 pref_label_bold(label, TRUE, TRUE);
309                 gtk_label_set_xalign(GTK_LABEL(label), 0.0);
310                 gtk_label_set_yalign(GTK_LABEL(label), 0.5);
311                 }
312         if (text)
313                 {
314                 label = pref_label_new(vbox, text);
315                 gtk_label_set_xalign(GTK_LABEL(label), 0.0);
316                 gtk_label_set_yalign(GTK_LABEL(label), 0.5);
317                 gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
318                 }
319
320         return vbox;
321 }
322
323 void generic_dialog_windows_load_config(const gchar **attribute_names, const gchar **attribute_values)
324 {
325         auto dw =  g_new0(DialogWindow, 1);
326         gchar *title = nullptr;
327         gchar *role = nullptr;
328         gint x = 0;
329         gint y = 0;
330         gint w = 0;
331         gint h = 0;
332
333         while (*attribute_names)
334                 {
335                 const gchar *option = *attribute_names++;
336                 const gchar *value = *attribute_values++;
337                 if (READ_CHAR_FULL("title", title)) continue;
338                 if (READ_CHAR_FULL("role", role)) continue;
339                 if (READ_INT_FULL("x", x)) continue;
340                 if (READ_INT_FULL("y", y)) continue;
341                 if (READ_INT_FULL("w", w)) continue;
342                 if (READ_INT_FULL("h", h)) continue;
343
344                 log_printf("unknown attribute %s = %s\n", option, value);
345                 }
346
347         if (title && title[0] != 0)
348                 {
349                 dw->title = g_strdup(title);
350                 dw->role = g_strdup(role);
351                 dw->x = x;
352                 dw->y = y;
353                 dw->w = w;
354                 dw->h = h;
355
356                 dialog_windows = g_list_append(dialog_windows, dw);
357                 }
358 }
359
360 void generic_dialog_windows_write_config(GString *outstr, gint indent)
361 {
362         GList *work;
363
364         if (options->save_dialog_window_positions && dialog_windows)
365                 {
366                 WRITE_NL(); WRITE_STRING("<%s>", "dialogs");
367                 indent++;
368
369                 work = g_list_first(dialog_windows);
370                 while (work)
371                         {
372                         auto dw = static_cast<DialogWindow *>(work->data);
373                         WRITE_NL(); WRITE_STRING("<window ");
374                         write_char_option(outstr, indent + 1, "title", dw->title);
375                         write_char_option(outstr, indent + 1, "role", dw->role);
376                         WRITE_INT(*dw, x);
377                         WRITE_INT(*dw, y);
378                         WRITE_INT(*dw, w);
379                         WRITE_INT(*dw, h);
380                         WRITE_STRING("/>");
381                         work = work->next;
382                         }
383                 indent--;
384                 WRITE_NL(); WRITE_STRING("</%s>", "dialogs");
385                 }
386 }
387
388 static void generic_dialog_setup(GenericDialog *gd,
389                                  const gchar *title,
390                                  const gchar *role,
391                                  GtkWidget *parent, gboolean auto_close,
392                                  void (*cancel_cb)(GenericDialog *, gpointer), gpointer data)
393 {
394         GtkWidget *vbox;
395         gint x;
396         gint y;
397         gint w;
398         gint h;
399         GtkWidget *scrolled;
400
401         gd->auto_close = auto_close;
402         gd->data = data;
403         gd->cancel_cb = cancel_cb;
404
405         gd->dialog = window_new(role, nullptr, nullptr, title);
406         DEBUG_NAME(gd->dialog);
407         gtk_window_set_type_hint(GTK_WINDOW(gd->dialog), GDK_WINDOW_TYPE_HINT_DIALOG);
408
409         if (options->save_dialog_window_positions)
410                 {
411                 if (generic_dialog_find_window(title, role, &x, &y, &w, &h))
412                         {
413                         gtk_window_set_default_size(GTK_WINDOW(gd->dialog), w, h);
414                         gq_gtk_window_move(GTK_WINDOW(gd->dialog), x, y);
415                         }
416                 }
417
418         if (parent)
419                 {
420                 GtkWindow *window = nullptr;
421
422                 if (GTK_IS_WINDOW(parent))
423                         {
424                         window = GTK_WINDOW(parent);
425                         }
426                 else
427                         {
428                         GtkWidget *top;
429
430                         top = gtk_widget_get_toplevel(parent);
431                         if (GTK_IS_WINDOW(top) && gtk_widget_is_toplevel(top)) window = GTK_WINDOW(top);
432                         }
433
434                 if (window) gtk_window_set_transient_for(GTK_WINDOW(gd->dialog), window);
435                 }
436
437         g_signal_connect(G_OBJECT(gd->dialog), "delete_event",
438                          G_CALLBACK(generic_dialog_delete_cb), gd);
439         g_signal_connect(G_OBJECT(gd->dialog), "key_press_event",
440                          G_CALLBACK(generic_dialog_key_press_cb), gd);
441
442         gtk_window_set_resizable(GTK_WINDOW(gd->dialog), TRUE);
443         gtk_container_set_border_width(GTK_CONTAINER(gd->dialog), PREF_PAD_BORDER);
444
445         scrolled = gq_gtk_scrolled_window_new(nullptr, nullptr);
446         gtk_scrolled_window_set_propagate_natural_height(GTK_SCROLLED_WINDOW(scrolled), TRUE);
447         gtk_scrolled_window_set_propagate_natural_width(GTK_SCROLLED_WINDOW(scrolled), TRUE);
448         vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, PREF_PAD_BUTTON_SPACE);
449         gq_gtk_container_add(GTK_WIDGET(scrolled), vbox);
450         gq_gtk_container_add(GTK_WIDGET(gd->dialog), scrolled);
451         gtk_widget_show(scrolled);
452
453         gtk_widget_show(vbox);
454
455         gd->vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, PREF_PAD_GAP);
456         gq_gtk_box_pack_start(GTK_BOX(vbox), gd->vbox, TRUE, TRUE, 0);
457         gtk_widget_show(gd->vbox);
458
459         gd->hbox = gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL);
460         gtk_button_box_set_layout(GTK_BUTTON_BOX(gd->hbox), GTK_BUTTONBOX_END);
461         gtk_box_set_spacing(GTK_BOX(gd->hbox), PREF_PAD_BUTTON_GAP);
462         gq_gtk_box_pack_start(GTK_BOX(vbox), gd->hbox, FALSE, FALSE, 0);
463         gtk_widget_show(gd->hbox);
464
465         if (gd->cancel_cb)
466                 {
467                 gd->cancel_button = generic_dialog_add_button(gd, GQ_ICON_CANCEL, _("Cancel"), gd->cancel_cb, TRUE);
468                 }
469         else
470                 {
471                 gd->cancel_button = nullptr;
472                 }
473
474         if (generic_dialog_get_alternative_button_order(gd->hbox))
475                 {
476                 g_signal_connect(G_OBJECT(gd->dialog), "show",
477                                  G_CALLBACK(generic_dialog_show_cb), gd);
478                 }
479
480         gd->default_cb = nullptr;
481 }
482
483 GenericDialog *generic_dialog_new(const gchar *title,
484                                   const gchar *role,
485                                   GtkWidget *parent, gboolean auto_close,
486                                   void (*cancel_cb)(GenericDialog *, gpointer), gpointer data)
487 {
488         GenericDialog *gd;
489
490         gd = g_new0(GenericDialog, 1);
491         generic_dialog_setup(gd, title, role,
492                              parent, auto_close, cancel_cb, data);
493         return gd;
494 }
495 /*
496  *-----------------------------------------------------------------------------
497  * simple warning dialog
498  *-----------------------------------------------------------------------------
499  */
500
501 static void warning_dialog_ok_cb(GenericDialog *, gpointer)
502 {
503         /* no op */
504 }
505
506 GenericDialog *warning_dialog(const gchar *heading, const gchar *text,
507                               const gchar *icon_name, GtkWidget *parent)
508 {
509         GenericDialog *gd;
510
511         gd = generic_dialog_new(heading, "warning", parent, TRUE, nullptr, nullptr);
512         generic_dialog_add_button(gd, GQ_ICON_OK, "OK", warning_dialog_ok_cb, TRUE);
513
514         generic_dialog_add_message(gd, icon_name, heading, text, TRUE);
515
516         gtk_widget_show(gd->dialog);
517
518         return gd;
519 }
520
521 /*
522  *-----------------------------------------------------------------------------
523  * AppImage version update notification message with fade-out
524  *-----------------------------------------------------------------------------
525  *
526  * If the current version is not on GitHub, assume a newer one is available
527  * and show a fade-out message.
528  */
529
530 struct AppImageData
531 {
532         GThreadPool *thread_pool;
533         GtkWidget *window;
534         guint id;
535 };
536
537 static gboolean appimage_notification_close_cb(gpointer data)
538 {
539         auto appimage_data = static_cast<AppImageData *>(data);
540
541         if (appimage_data->window && gtk_widget_get_opacity(appimage_data->window) != 0)
542                 {
543                 g_source_remove(appimage_data->id);
544                 }
545
546         if (appimage_data->window)
547                 {
548                 g_object_unref(appimage_data->window);
549                 }
550
551         g_thread_pool_free(appimage_data->thread_pool, TRUE, TRUE);
552         g_free(appimage_data);
553
554         return G_SOURCE_REMOVE;
555 }
556
557 static gboolean appimage_notification_fade_cb(gpointer data)
558 {
559         auto appimage_data = static_cast<AppImageData *>(data);
560
561         gtk_widget_set_opacity(appimage_data->window, (gtk_widget_get_opacity(appimage_data->window) - 0.02));
562
563         if (gtk_widget_get_opacity(appimage_data->window) == 0)
564                 {
565                 g_idle_add(appimage_notification_close_cb, data);
566
567                 return FALSE;
568                 }
569
570         return TRUE;
571 }
572
573 static gboolean user_close_cb(GtkWidget *, GdkEvent *, gpointer data)
574 {
575         auto appimage_data = static_cast<AppImageData *>(data);
576
577         g_idle_add(appimage_notification_close_cb, appimage_data);
578
579         return FALSE;
580 }
581
582 static void show_notification_message(AppImageData *appimage_data)
583 {
584         GtkBuilder *builder;
585
586         builder = gtk_builder_new_from_resource(GQ_RESOURCE_PATH_UI "/appimage-notification.ui");
587
588         appimage_data->window = GTK_WIDGET(gtk_builder_get_object(builder, "appimage_notification"));
589
590         GdkRectangle workarea;
591         gdk_monitor_get_workarea(gdk_display_get_primary_monitor(gdk_display_get_default()),
592                              &workarea);
593         gq_gtk_window_move(GTK_WINDOW(appimage_data->window), workarea.width * 0.8, workarea.height / 20);
594         g_signal_connect(appimage_data->window, "focus-in-event", G_CALLBACK(user_close_cb), appimage_data);
595         appimage_data->id = g_timeout_add(100, appimage_notification_fade_cb, appimage_data);
596
597         g_object_unref(builder);
598         gtk_widget_show(appimage_data->window);
599 }
600
601 void appimage_notification_func(gpointer data, gpointer)
602 {
603         auto appimage_data = static_cast<AppImageData *>(data);
604         gboolean internet_available = FALSE;
605         gchar **version_split;
606         GFile *file_github;
607         GFileInfo *file_info;
608         GNetworkMonitor *net_mon;
609         GSocketConnectable *geeqie_github;
610         struct tm current_version_date;
611         int64_t file_github_date;
612         unsigned int year;
613         unsigned int month;
614         unsigned int day;
615
616         /* If this is a release version, do not check for updates */
617         if (g_strrstr(VERSION, "git"))
618                 {
619                 net_mon = g_network_monitor_get_default();
620                 geeqie_github = g_network_address_parse_uri("https://github.com/", 80, nullptr);
621
622                 if (geeqie_github)
623                         {
624                         internet_available = g_network_monitor_can_reach(net_mon, geeqie_github, nullptr, nullptr);
625                         g_object_unref(geeqie_github);
626                         }
627
628                 if (internet_available)
629                         {
630                         /* VERSION looks like: 2.0.1+git20220116-c791cbee */
631                         version_split = g_strsplit_set(VERSION, "+-", -1);
632
633                         file_github = g_file_new_for_uri("https://github.com/BestImageViewer/geeqie/releases/download/continuous/Geeqie-latest-x86_64.AppImage");
634                         file_info = g_file_query_info(file_github, "time::modified", G_FILE_QUERY_INFO_NONE, nullptr, nullptr);
635
636                         file_github_date = g_file_info_get_attribute_uint64(file_info, "time::modified");
637
638                         sscanf(version_split[1] + 3, "%4u%2u%2u", &year, &month, &day);
639                         current_version_date.tm_year  = year - 1900;
640                         current_version_date.tm_mon   = month - 1;
641                         current_version_date.tm_mday  = day;
642                         current_version_date.tm_hour  = 0;
643                         current_version_date.tm_min   = 0;
644                         current_version_date.tm_sec   = 0;
645                         current_version_date.tm_isdst = 0;
646
647                         if (file_github_date > mktime(&current_version_date))
648                                 {
649                                 show_notification_message(appimage_data);
650                                 }
651
652                         g_object_unref(file_github);
653                         g_object_unref(file_info);
654                         g_strfreev(version_split);
655                         }
656                 }
657 }
658
659 void appimage_notification()
660 {
661         AppImageData *appimage_data;
662
663         appimage_data = g_new0(AppImageData, 1);
664
665         appimage_data->thread_pool = g_thread_pool_new(appimage_notification_func, appimage_data, 1, FALSE, nullptr);
666         g_thread_pool_push(appimage_data->thread_pool, appimage_data, nullptr);
667 }
668
669 /*
670  *-----------------------------------------------------------------------------
671  * generic file ops dialog routines
672  *-----------------------------------------------------------------------------
673  */
674
675 void file_dialog_close(FileDialog *fdlg)
676 {
677         file_data_unref(fdlg->source_fd);
678         g_free(fdlg->dest_path);
679         if (fdlg->source_list) filelist_free(fdlg->source_list);
680
681         generic_dialog_close(GENERIC_DIALOG(fdlg));
682 }
683
684 FileDialog *file_dialog_new(const gchar *title,
685                             const gchar *role,
686                             GtkWidget *parent,
687                             void (*cancel_cb)(FileDialog *, gpointer), gpointer data)
688 {
689         FileDialog *fdlg = nullptr;
690
691         fdlg = g_new0(FileDialog, 1);
692
693         generic_dialog_setup(GENERIC_DIALOG(fdlg), title,
694                              role, parent, FALSE,
695                              reinterpret_cast<void(*)(GenericDialog *, gpointer)>(cancel_cb), data);
696
697         return fdlg;
698 }
699
700 GtkWidget *file_dialog_add_button(FileDialog *fdlg, const gchar *stock_id, const gchar *text,
701                                   void (*func_cb)(FileDialog *, gpointer), gboolean is_default)
702 {
703         return generic_dialog_add_button(GENERIC_DIALOG(fdlg), stock_id, text,
704                                          reinterpret_cast<void(*)(GenericDialog *, gpointer)>(func_cb), is_default);
705 }
706
707 static void file_dialog_entry_cb(GtkWidget *, gpointer data)
708 {
709         auto fdlg = static_cast<FileDialog *>(data);
710         g_free(fdlg->dest_path);
711         fdlg->dest_path = remove_trailing_slash(gq_gtk_entry_get_text(GTK_ENTRY(fdlg->entry)));
712 }
713
714 static void file_dialog_entry_enter_cb(const gchar *, gpointer data)
715 {
716         auto gd = static_cast<GenericDialog *>(data);
717
718         file_dialog_entry_cb(nullptr, data);
719
720         if (gd->default_cb) gd->default_cb(gd, gd->data);
721 }
722
723 void file_dialog_add_path_widgets(FileDialog *fdlg, const gchar *default_path, const gchar *path,
724                                   const gchar *history_key, const gchar *filter, const gchar *filter_desc)
725 {
726         GtkWidget *tabcomp;
727         GtkWidget *list;
728
729         if (fdlg->entry) return;
730
731         tabcomp = tab_completion_new_with_history(&fdlg->entry, nullptr,
732                   history_key, -1, file_dialog_entry_enter_cb, fdlg);
733         gq_gtk_box_pack_end(GTK_BOX(GENERIC_DIALOG(fdlg)->vbox), tabcomp, FALSE, FALSE, 0);
734         generic_dialog_attach_default(GENERIC_DIALOG(fdlg), fdlg->entry);
735         gtk_widget_show(tabcomp);
736
737         if (path && path[0] == G_DIR_SEPARATOR)
738                 {
739                 fdlg->dest_path = g_strdup(path);
740                 }
741         else
742                 {
743                 const gchar *base;
744
745                 base = tab_completion_set_to_last_history(fdlg->entry);
746
747                 if (!base) base = default_path;
748                 if (!base) base = homedir();
749
750                 if (path)
751                         {
752                         fdlg->dest_path = g_build_filename(base, path, NULL);
753                         }
754                 else
755                         {
756                         fdlg->dest_path = g_strdup(base);
757                         }
758                 }
759
760         list = path_selection_new_with_files(fdlg->entry, fdlg->dest_path, filter, filter_desc);
761         path_selection_add_select_func(fdlg->entry, file_dialog_entry_enter_cb, fdlg);
762         gq_gtk_box_pack_end(GTK_BOX(GENERIC_DIALOG(fdlg)->vbox), list, TRUE, TRUE, 0);
763         gtk_widget_show(list);
764
765         gtk_widget_grab_focus(fdlg->entry);
766         if (fdlg->dest_path)
767                 {
768                 gq_gtk_entry_set_text(GTK_ENTRY(fdlg->entry), fdlg->dest_path);
769                 gtk_editable_set_position(GTK_EDITABLE(fdlg->entry), strlen(fdlg->dest_path));
770                 }
771
772         g_signal_connect(G_OBJECT(fdlg->entry), "changed",
773                          G_CALLBACK(file_dialog_entry_cb), fdlg);
774 }
775
776 #pragma GCC diagnostic push
777 #pragma GCC diagnostic ignored "-Wunused-function"
778 void file_dialog_add_filter_unused(FileDialog *fdlg, const gchar *filter, const gchar *filter_desc, gboolean set)
779 {
780         if (!fdlg->entry) return;
781         path_selection_add_filter(fdlg->entry, filter, filter_desc, set);
782 }
783
784 void file_dialog_clear_filter_unused(FileDialog *fdlg)
785 {
786         if (!fdlg->entry) return;
787         path_selection_clear_filter(fdlg->entry);
788 }
789 #pragma GCC diagnostic pop
790
791 void file_dialog_sync_history(FileDialog *fdlg, gboolean dir_only)
792 {
793         if (!fdlg->dest_path) return;
794
795         if (!dir_only ||
796             (dir_only && isdir(fdlg->dest_path)) )
797                 {
798                 tab_completion_append_to_history(fdlg->entry, fdlg->dest_path);
799                 }
800         else
801                 {
802                 gchar *buf = remove_level_from_path(fdlg->dest_path);
803                 tab_completion_append_to_history(fdlg->entry, buf);
804                 g_free(buf);
805                 }
806 }
807 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */