FIXME: do not work - gtk_label_set_ellipsize
[geeqie.git] / src / bar.cc
1 /*
2  * Copyright (C) 2004 John Ellis
3  * Copyright (C) 2008 - 2016 The Geeqie Team
4  *
5  * Author: Vladimir Nadvornik
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 "main.h"
23 #include "bar.h"
24
25 #include "bar-histogram.h"
26 #include "filedata.h"
27 #include "layout.h"
28 #include "metadata.h"
29 #include "rcfile.h"
30 #include "ui-menu.h"
31 #include "ui-misc.h"
32
33 struct KnownPanes
34 {
35         PaneType type;
36         const gchar *id;
37         const gchar *title;
38         const gchar *config;
39 };
40
41 static const gchar default_config_histogram[] =
42 "<gq>"
43 "    <layout id = '_current_'>"
44 "        <bar>"
45 "            <pane_histogram id = 'histogram' expanded = 'true' histogram_channel = '4' histogram_mode = '0' />"
46 "        </bar>"
47 "    </layout>"
48 "</gq>";
49
50 static const gchar default_config_title[] =
51 "<gq>"
52 "    <layout id = '_current_'>"
53 "        <bar>"
54 "            <pane_comment id = 'title' expanded = 'true' key = 'Xmp.dc.title' height = '40' />"
55 "        </bar>"
56 "    </layout>"
57 "</gq>";
58
59 static const gchar default_config_headline[] =
60 "<gq>"
61 "    <layout id = '_current_'>"
62 "        <bar>"
63 "            <pane_comment id = 'headline' expanded = 'true' key = 'Xmp.photoshop.Headline'  height = '40' />"
64 "        </bar>"
65 "    </layout>"
66 "</gq>";
67
68 static const gchar default_config_keywords[] =
69 "<gq>"
70 "    <layout id = '_current_'>"
71 "        <bar>"
72 "            <pane_keywords id = 'keywords' expanded = 'true' key = '" KEYWORD_KEY "' />"
73 "        </bar>"
74 "    </layout>"
75 "</gq>";
76
77 static const gchar default_config_comment[] =
78 "<gq>"
79 "    <layout id = '_current_'>"
80 "        <bar>"
81 "            <pane_comment id = 'comment' expanded = 'true' key = '" COMMENT_KEY "' height = '150' />"
82 "        </bar>"
83 "    </layout>"
84 "</gq>";
85 static const gchar default_config_rating[] =
86 "<gq>"
87 "    <layout id = '_current_'>"
88 "        <bar>"
89 "            <pane_rating id = 'rating' expanded = 'true' />"
90 "        </bar>"
91 "    </layout>"
92 "</gq>";
93
94 static const gchar default_config_exif[] =
95 "<gq>"
96 "    <layout id = '_current_'>"
97 "        <bar>"
98 "            <pane_exif id = 'exif' expanded = 'true' >"
99 "                <entry key = 'formatted.Camera' if_set = 'true' editable = 'false' />"
100 "                <entry key = 'formatted.DateTime' if_set = 'true' editable = 'false' />"
101 "                <entry key = 'formatted.localtime' if_set = 'true' editable = 'false' />"
102 "                <entry key = 'formatted.ShutterSpeed' if_set = 'true' editable = 'false' />"
103 "                <entry key = 'formatted.Aperture' if_set = 'true' editable = 'false' />"
104 "                <entry key = 'formatted.ExposureBias' if_set = 'true' editable = 'false' />"
105 "                <entry key = 'formatted.ISOSpeedRating' if_set = 'true' editable = 'false' />"
106 "                <entry key = 'formatted.FocalLength' if_set = 'true' editable = 'false' />"
107 "                <entry key = 'formatted.FocalLength35mmFilm' if_set = 'true' editable = 'false' />"
108 "                <entry key = 'formatted.Flash' if_set = 'true' editable = 'false' />"
109 "                <entry key = 'Exif.Photo.ExposureProgram' if_set = 'true' editable = 'false' />"
110 "                <entry key = 'Exif.Photo.MeteringMode' if_set = 'true' editable = 'false' />"
111 "                <entry key = 'Exif.Photo.LightSource' if_set = 'true' editable = 'false' />"
112 "                <entry key = 'formatted.ColorProfile' if_set = 'true' editable = 'false' />"
113 "                <entry key = 'formatted.SubjectDistance' if_set = 'true' editable = 'false' />"
114 "                <entry key = 'formatted.Resolution' if_set = 'true' editable = 'false' />"
115 "                <entry key = '" ORIENTATION_KEY "' if_set = 'true' editable = 'false' />"
116 "                <entry key = 'formatted.star_rating' if_set = 'true' editable = 'false' />"
117 "            </pane_exif>"
118 "        </bar>"
119 "    </layout>"
120 "</gq>";
121
122 static const gchar default_config_file_info[] =
123 "<gq>"
124 "    <layout id = '_current_'>"
125 "        <bar>"
126 "            <pane_exif id = 'file_info' expanded = 'true' >"
127 "                <entry key = 'file.mode' if_set = 'false' editable = 'false' />"
128 "                <entry key = 'file.date' if_set = 'false' editable = 'false' />"
129 "                <entry key = 'file.size' if_set = 'false' editable = 'false' />"
130 "                <entry key = 'file.owner' if_set = 'false' editable = 'false' />"
131 "                <entry key = 'file.group' if_set = 'false' editable = 'false' />"
132 "                <entry key = 'file.class' if_set = 'false' editable = 'false' />"
133 "                <entry key = 'file.link' if_set = 'false' editable = 'false' />"
134 "            </pane_exif>"
135 "        </bar>"
136 "    </layout>"
137 "</gq>";
138
139 static const gchar default_config_location[] =
140 "<gq>"
141 "    <layout id = '_current_'>"
142 "        <bar>"
143 "            <pane_exif id = 'location' expanded = 'true' >"
144 "                <entry key = 'formatted.GPSPosition' if_set = 'true' editable = 'false' />"
145 "                <entry key = 'formatted.GPSAltitude' if_set = 'true' editable = 'false' />"
146 "                <entry key = 'formatted.timezone' if_set = 'true' editable = 'false' />"
147 "                <entry key = 'Xmp.photoshop.Country' if_set = 'false' editable = 'true' />"
148 "                <entry key = 'Xmp.iptc.CountryCode' if_set = 'false' editable = 'true' />"
149 "                <entry key = 'Xmp.photoshop.State' if_set = 'false' editable = 'true' />"
150 "                <entry key = 'Xmp.photoshop.City' if_set = 'false' editable = 'true' />"
151 "                <entry key = 'Xmp.iptc.Location' if_set = 'false' editable = 'true' />"
152 "            </pane_exif>"
153 "        </bar>"
154 "    </layout>"
155 "</gq>";
156
157 static const gchar default_config_copyright[] =
158 "<gq>"
159 "    <layout id = '_current_'>"
160 "        <bar>"
161 "            <pane_exif id = 'copyright' expanded = 'true' >"
162 "                <entry key = 'Xmp.dc.creator' if_set = 'true' editable = 'false' />"
163 "                <entry key = 'Xmp.dc.contributor' if_set = 'true' editable = 'false' />"
164 "                <entry key = 'Xmp.dc.rights' if_set = 'false' editable = 'false' />"
165 "            </pane_exif>"
166 "        </bar>"
167 "    </layout>"
168 "</gq>";
169
170 #ifdef HAVE_LIBCHAMPLAIN
171 #ifdef HAVE_LIBCHAMPLAIN_GTK
172 static const gchar default_config_gps[] =
173 "<gq>"
174 "    <layout id = '_current_'>"
175 "        <bar>"
176 "            <pane_gps id = 'gps' expanded = 'true'"
177 "                      map-id = 'osm-mapnik'"
178 "                      zoom-level = '8'"
179 "                      latitude = '50116666'"
180 "                      longitude = '8683333' />"
181 "        </bar>"
182 "    </layout>"
183 "</gq>";
184 #endif
185 #endif
186
187 static const KnownPanes known_panes[] = {
188 /* default sidebar */
189         {PANE_HISTOGRAM,        "histogram",    N_("Histogram"),        default_config_histogram},
190         {PANE_COMMENT,          "title",        N_("Title"),            default_config_title},
191         {PANE_KEYWORDS,         "keywords",     N_("Keywords"),         default_config_keywords},
192         {PANE_COMMENT,          "comment",      N_("Comment"),          default_config_comment},
193         {PANE_RATING,           "rating",       N_("Star Rating"),      default_config_rating},
194         {PANE_COMMENT,          "headline",     N_("Headline"),         default_config_headline},
195         {PANE_EXIF,             "exif",         N_("Exif"),             default_config_exif},
196 /* other pre-configured panes */
197         {PANE_EXIF,             "file_info",    N_("File info"),        default_config_file_info},
198         {PANE_EXIF,             "location",     N_("Location and GPS"), default_config_location},
199         {PANE_EXIF,             "copyright",    N_("Copyright"),        default_config_copyright},
200 #ifdef HAVE_LIBCHAMPLAIN
201 #ifdef HAVE_LIBCHAMPLAIN_GTK
202         {PANE_GPS,              "gps",  N_("GPS Map"),  default_config_gps},
203 #endif
204 #endif
205         {PANE_UNDEF,            nullptr,                nullptr,                        nullptr}
206 };
207
208 struct BarData
209 {
210         GtkWidget *widget;
211         GtkWidget *vbox;
212         FileData *fd;
213         GtkWidget *label_file_name;
214         GtkWidget *add_button;
215
216         LayoutWindow *lw;
217         gint width;
218 };
219
220 static const gchar *bar_pane_get_default_config(const gchar *id);
221
222 static void bar_expander_move(GtkWidget *, gpointer data, gboolean up, gboolean single_step)
223 {
224         auto expander = static_cast<GtkWidget *>(data);
225         GtkWidget *box;
226         gint pos;
227
228         if (!expander) return;
229         box = gtk_widget_get_ancestor(expander, GTK_TYPE_BOX);
230         if (!box) return;
231
232         gtk_container_child_get(GTK_CONTAINER(box), expander, "position", &pos, NULL);
233
234         if (single_step)
235                 {
236                 pos = up ? (pos - 1) : (pos + 1);
237                 if (pos < 0) pos = 0;
238                 }
239         else
240                 {
241                 pos = up ? 0 : -1;
242                 }
243
244         gtk_box_reorder_child(GTK_BOX(box), expander, pos);
245 }
246
247
248 static void bar_expander_move_up_cb(GtkWidget *widget, gpointer data)
249 {
250         bar_expander_move(widget, data, TRUE, TRUE);
251 }
252
253 static void bar_expander_move_down_cb(GtkWidget *widget, gpointer data)
254 {
255         bar_expander_move(widget, data, FALSE, TRUE);
256 }
257
258 static void bar_expander_move_top_cb(GtkWidget *widget, gpointer data)
259 {
260         bar_expander_move(widget, data, TRUE, FALSE);
261 }
262
263 static void bar_expander_move_bottom_cb(GtkWidget *widget, gpointer data)
264 {
265         bar_expander_move(widget, data, FALSE, FALSE);
266 }
267
268 static void height_spin_changed_cb(GtkSpinButton *spin, gpointer data)
269 {
270
271         gtk_widget_set_size_request(GTK_WIDGET(data), -1, gtk_spin_button_get_value_as_int(spin));
272 }
273
274 static void height_spin_key_press_cb(GtkEventControllerKey *, gint keyval, guint, GdkModifierType, gpointer data)
275 {
276         if ((keyval == GDK_KEY_Return || keyval == GDK_KEY_Escape))
277                 {
278                 g_object_unref(GTK_WIDGET(data));
279                 }
280 }
281
282 static void expander_height_cb(GtkWindow *widget, GdkEvent *, gpointer)
283 {
284         gq_gtk_widget_destroy(GTK_WIDGET(widget));
285 }
286
287 static void bar_expander_height_cb(GtkWidget *, gpointer data)
288 {
289         auto expander = static_cast<GtkWidget *>(data);
290         GtkWidget *spin;
291         GtkWidget *window;
292         GtkWidget *data_box;
293         GList *list;
294         gint x, y;
295         gint w, h;
296         GdkDisplay *display;
297         GdkSeat *seat;
298         GdkDevice *device;
299         GtkEventController *controller;
300
301         display = gdk_display_get_default();
302         seat = gdk_display_get_default_seat(display);
303         device = gdk_seat_get_pointer(seat);
304         gdk_device_get_position(device, nullptr, &x, &y);
305
306         list = gtk_container_get_children(GTK_CONTAINER(expander));
307         data_box = static_cast<GtkWidget *>(list->data);
308
309 #ifdef HAVE_GTK4
310         window = gtk_window_new();
311 #else
312         window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
313 #endif
314
315         gtk_window_set_modal(GTK_WINDOW(window), TRUE);
316         gtk_window_set_decorated(GTK_WINDOW(window), FALSE);
317         gq_gtk_window_set_keep_above(GTK_WINDOW(window), TRUE);
318         gtk_window_set_default_size(GTK_WINDOW(window), 50, 30); //** @FIXME set these values in a more sensible way */
319         g_signal_connect(window, "key-press-event", G_CALLBACK(expander_height_cb), nullptr);
320
321         gq_gtk_window_move(GTK_WINDOW(window), x, y);
322         gtk_widget_show(window);
323
324         gtk_widget_get_size_request(GTK_WIDGET(data_box), &w, &h);
325
326         spin = gtk_spin_button_new_with_range(1, 1000, 1);
327         g_signal_connect(G_OBJECT(spin), "value-changed", G_CALLBACK(height_spin_changed_cb), data_box);
328         controller = gtk_event_controller_key_new(spin);
329         g_signal_connect(controller, "key-pressed", G_CALLBACK(height_spin_key_press_cb), window);
330
331         gtk_spin_button_set_value(GTK_SPIN_BUTTON(spin), h);
332         gq_gtk_container_add(GTK_WIDGET(window), spin);
333         gtk_widget_show(spin);
334         gtk_widget_grab_focus(GTK_WIDGET(spin));
335
336         g_list_free(list);
337 }
338
339 static void bar_expander_delete_cb(GtkWidget *, gpointer data)
340 {
341         auto expander = static_cast<GtkWidget *>(data);
342         g_object_unref(expander);
343 }
344
345 static void bar_expander_add_cb(GtkWidget *widget, gpointer)
346 {
347         const KnownPanes *pane = known_panes;
348         auto id = static_cast<const gchar *>(g_object_get_data(G_OBJECT(widget), "pane_add_id"));
349         const gchar *config;
350
351         if (!id) return;
352
353         while (pane->id)
354                 {
355                 if (strcmp(pane->id, id) == 0) break;
356                 pane++;
357                 }
358         if (!pane->id) return;
359
360         config = bar_pane_get_default_config(id);
361         if (config) load_config_from_buf(config, strlen(config), FALSE);
362
363 }
364
365
366 static void bar_menu_popup(GtkWidget *widget)
367 {
368         GtkWidget *menu;
369         GtkWidget *bar;
370         GtkWidget *expander;
371         BarData *bd;
372         gboolean display_height_option = FALSE;
373         gchar const *label;
374
375         label = gtk_expander_get_label(GTK_EXPANDER(widget));
376         display_height_option = (g_strcmp0(label, "Comment") == 0) ||
377                                                         (g_strcmp0(label, "Rating") == 0) ||
378                                                         (g_strcmp0(label, "Title") == 0) ||
379                                                         (g_strcmp0(label, "Headline") == 0) ||
380                                                         (g_strcmp0(label, "Keywords") == 0) ||
381                                                         (g_strcmp0(label, "GPS Map") == 0);
382
383         bd = static_cast<BarData *>(g_object_get_data(G_OBJECT(widget), "bar_data"));
384         if (bd)
385                 {
386                 expander = nullptr;
387                 bar = widget;
388                 }
389         else
390                 {
391                 expander = widget;
392                 bar = gtk_widget_get_parent(widget);
393                 while (bar && !g_object_get_data(G_OBJECT(bar), "bar_data"))
394                         bar = gtk_widget_get_parent(bar);
395                 if (!bar) return;
396                 }
397
398         menu = popup_menu_short_lived();
399
400         if (expander)
401                 {
402                 menu_item_add_icon(menu, _("Move to _top"), GQ_ICON_GO_TOP, G_CALLBACK(bar_expander_move_top_cb), expander);
403                 menu_item_add_icon(menu, _("Move _up"), GQ_ICON_GO_UP, G_CALLBACK(bar_expander_move_up_cb), expander);
404                 menu_item_add_icon(menu, _("Move _down"), GQ_ICON_GO_DOWN, G_CALLBACK(bar_expander_move_down_cb), expander);
405                 menu_item_add_icon(menu, _("Move to _bottom"), GQ_ICON_GO_BOTTOM, G_CALLBACK(bar_expander_move_bottom_cb), expander);
406                 menu_item_add_divider(menu);
407
408                 if (gtk_expander_get_expanded(GTK_EXPANDER(expander)) && display_height_option)
409                         {
410                         menu_item_add_icon(menu, _("Height..."), GQ_ICON_PREFERENCES, G_CALLBACK(bar_expander_height_cb), expander);
411                         menu_item_add_divider(menu);
412                         }
413
414                 menu_item_add_icon(menu, _("Remove"), GQ_ICON_DELETE, G_CALLBACK(bar_expander_delete_cb), expander);
415                 menu_item_add_divider(menu);
416                 }
417
418         gtk_menu_popup_at_pointer(GTK_MENU(menu), nullptr);
419 }
420
421 static void bar_menu_add_popup(GtkWidget *widget)
422 {
423         GtkWidget *menu;
424         GtkWidget *bar;
425         const KnownPanes *pane = known_panes;
426
427         bar = widget;
428
429         menu = popup_menu_short_lived();
430
431         while (pane->id)
432                 {
433                 GtkWidget *item;
434                 item = menu_item_add_icon(menu, _(pane->title), GQ_ICON_ADD, G_CALLBACK(bar_expander_add_cb), bar);
435                 g_object_set_data(G_OBJECT(item), "pane_add_id", const_cast<gchar *>(pane->id));
436                 pane++;
437                 }
438
439         gtk_menu_popup_at_pointer(GTK_MENU(menu), nullptr);
440 }
441
442
443 static gboolean bar_menu_cb(GtkWidget *widget, GdkEventButton *bevent, gpointer)
444 {
445         if (bevent->button == MOUSE_BUTTON_RIGHT)
446                 {
447                 bar_menu_popup(widget);
448                 return TRUE;
449                 }
450         return FALSE;
451 }
452
453 static void bar_expander_cb(GObject *object, GParamSpec *, gpointer)
454 {
455         GtkExpander *expander;
456         GtkWidget *child;
457
458         expander = GTK_EXPANDER(object);
459         child = gtk_bin_get_child(GTK_BIN(expander));
460
461         if (gtk_expander_get_expanded(expander))
462                 {
463                 gq_gtk_widget_show_all(child);
464                 }
465         else
466                 {
467                 gtk_widget_hide(child);
468                 }
469 }
470
471 static gboolean bar_menu_add_cb(GtkWidget *widget, GdkEventButton *, gpointer)
472 {
473         bar_menu_add_popup(widget);
474         return TRUE;
475 }
476
477
478 static void bar_pane_set_fd_cb(GtkWidget *expander, gpointer data)
479 {
480         GtkWidget *widget = gtk_bin_get_child(GTK_BIN(expander));
481         auto pd = static_cast<PaneData *>(g_object_get_data(G_OBJECT(widget), "pane_data"));
482         if (!pd) return;
483         if (pd->pane_set_fd) pd->pane_set_fd(widget, static_cast<FileData *>(data));
484 }
485
486 void bar_set_fd(GtkWidget *bar, FileData *fd)
487 {
488         BarData *bd;
489         bd = static_cast<BarData *>(g_object_get_data(G_OBJECT(bar), "bar_data"));
490         if (!bd) return;
491
492         file_data_unref(bd->fd);
493         bd->fd = file_data_ref(fd);
494
495         gtk_container_foreach(GTK_CONTAINER(bd->vbox), bar_pane_set_fd_cb, fd);
496
497         gtk_label_set_text(GTK_LABEL(bd->label_file_name), (bd->fd) ? bd->fd->name : "");
498
499 }
500
501 static void bar_pane_notify_selection_cb(GtkWidget *expander, gpointer data)
502 {
503         GtkWidget *widget = gtk_bin_get_child(GTK_BIN(expander));
504         auto pd = static_cast<PaneData *>(g_object_get_data(G_OBJECT(widget), "pane_data"));
505         if (!pd) return;
506         if (pd->pane_notify_selection) pd->pane_notify_selection(widget, GPOINTER_TO_INT(data));
507 }
508
509 void bar_notify_selection(GtkWidget *bar, gint count)
510 {
511         BarData *bd;
512         bd = static_cast<BarData *>(g_object_get_data(G_OBJECT(bar), "bar_data"));
513         if (!bd) return;
514
515         gtk_container_foreach(GTK_CONTAINER(bd->vbox), bar_pane_notify_selection_cb, GINT_TO_POINTER(count));
516 }
517
518 gboolean bar_event(GtkWidget *bar, GdkEvent *event)
519 {
520         BarData *bd;
521         GList *list, *work;
522         gboolean ret = FALSE;
523
524         bd = static_cast<BarData *>(g_object_get_data(G_OBJECT(bar), "bar_data"));
525         if (!bd) return FALSE;
526
527         list = gtk_container_get_children(GTK_CONTAINER(bd->vbox));
528
529         work = list;
530         while (work)
531                 {
532                 GtkWidget *widget = gtk_bin_get_child(GTK_BIN(work->data));
533                 auto pd = static_cast<PaneData *>(g_object_get_data(G_OBJECT(widget), "pane_data"));
534                 if (!pd) continue;
535
536                 if (pd->pane_event && pd->pane_event(widget, event))
537                         {
538                         ret = TRUE;
539                         break;
540                         }
541                 work = work->next;
542                 }
543         g_list_free(list);
544         return ret;
545 }
546
547 GtkWidget *bar_find_pane_by_id(GtkWidget *bar, PaneType type, const gchar *id)
548 {
549         BarData *bd;
550         GList *list, *work;
551         GtkWidget *ret = nullptr;
552
553         if (!id || !id[0]) return nullptr;
554
555         bd = static_cast<BarData *>(g_object_get_data(G_OBJECT(bar), "bar_data"));
556         if (!bd) return nullptr;
557
558         list = gtk_container_get_children(GTK_CONTAINER(bd->vbox));
559
560         work = list;
561         while (work)
562                 {
563                 GtkWidget *widget = gtk_bin_get_child(GTK_BIN(work->data));
564                 auto pd = static_cast<PaneData *>(g_object_get_data(G_OBJECT(widget), "pane_data"));
565                 if (!pd) continue;
566
567                 if (type == pd->type && strcmp(id, pd->id) == 0)
568                         {
569                         ret = widget;
570                         break;
571                         }
572                 work = work->next;
573                 }
574         g_list_free(list);
575         return ret;
576 }
577
578 void bar_clear(GtkWidget *bar)
579 {
580         BarData *bd;
581         GList *list;
582
583         bd = static_cast<BarData *>(g_object_get_data(G_OBJECT(bar), "bar_data"));
584         if (!bd) return;
585
586         list = gtk_container_get_children(GTK_CONTAINER(bd->vbox));
587
588         g_list_free_full(list, reinterpret_cast<GDestroyNotify>(g_object_unref));
589 }
590
591 void bar_write_config(GtkWidget *bar, GString *outstr, gint indent)
592 {
593         BarData *bd;
594         GList *list, *work;
595
596         if (!bar) return;
597
598         bd = static_cast<BarData *>(g_object_get_data(G_OBJECT(bar), "bar_data"));
599         if (!bd) return;
600
601         WRITE_NL(); WRITE_STRING("<bar ");
602         write_bool_option(outstr, indent, "enabled", gtk_widget_get_visible(bar));
603         write_uint_option(outstr, indent, "width", bd->width);
604         WRITE_STRING(">");
605
606         indent++;
607         WRITE_NL(); WRITE_STRING("<clear/>");
608
609         list = gtk_container_get_children(GTK_CONTAINER(bd->vbox));
610         work = list;
611         while (work)
612                 {
613                 auto expander = static_cast<GtkWidget *>(work->data);
614                 GtkWidget *widget = gtk_bin_get_child(GTK_BIN(expander));
615                 auto pd = static_cast<PaneData *>(g_object_get_data(G_OBJECT(widget), "pane_data"));
616                 if (!pd) continue;
617
618                 pd->expanded = gtk_expander_get_expanded(GTK_EXPANDER(expander));
619
620                 if (pd->pane_write_config)
621                         pd->pane_write_config(widget, outstr, indent);
622
623                 work = work->next;
624                 }
625         g_list_free(list);
626         indent--;
627         WRITE_NL(); WRITE_STRING("</bar>");
628 }
629
630 void bar_update_expander(GtkWidget *pane)
631 {
632         auto pd = static_cast<PaneData *>(g_object_get_data(G_OBJECT(pane), "pane_data"));
633         GtkWidget *expander;
634
635         if (!pd) return;
636
637         expander = gtk_widget_get_parent(pane);
638
639         gtk_expander_set_expanded(GTK_EXPANDER(expander), pd->expanded);
640 }
641
642 void bar_add(GtkWidget *bar, GtkWidget *pane)
643 {
644         GtkWidget *expander;
645         auto bd = static_cast<BarData *>(g_object_get_data(G_OBJECT(bar), "bar_data"));
646         auto pd = static_cast<PaneData *>(g_object_get_data(G_OBJECT(pane), "pane_data"));
647
648         if (!bd) return;
649
650         pd->lw = bd->lw;
651         pd->bar = bar;
652
653         expander = gtk_expander_new(nullptr);
654         DEBUG_NAME(expander);
655         if (pd && pd->title)
656                 {
657                 gtk_expander_set_label_widget(GTK_EXPANDER(expander), pd->title);
658                 gtk_widget_show(pd->title);
659                 }
660
661         gq_gtk_box_pack_start(GTK_BOX(bd->vbox), expander, FALSE, TRUE, 0);
662
663         g_signal_connect(expander, "button_release_event", G_CALLBACK(bar_menu_cb), bd);
664         g_signal_connect(expander, "notify::expanded", G_CALLBACK(bar_expander_cb), pd);
665
666         gq_gtk_container_add(GTK_WIDGET(expander), pane);
667
668         gtk_expander_set_expanded(GTK_EXPANDER(expander), pd->expanded);
669
670         gtk_widget_show(expander);
671
672         if (bd->fd && pd && pd->pane_set_fd) pd->pane_set_fd(pane, bd->fd);
673
674 }
675
676 void bar_populate_default(GtkWidget *)
677 {
678         const gchar *populate_id[] = {"histogram", "title", "keywords", "comment", "rating", "exif", nullptr};
679         const gchar **id = populate_id;
680
681         while (*id)
682                 {
683                 const gchar *config = bar_pane_get_default_config(*id);
684                 if (config) load_config_from_buf(config, strlen(config), FALSE);
685                 id++;
686                 }
687 }
688
689 static void bar_size_allocate(GtkWidget *, GtkAllocation *, gpointer data)
690 {
691         auto bd = static_cast<BarData *>(data);
692
693         bd->width = gtk_paned_get_position(GTK_PANED(bd->lw->utility_paned));
694 }
695
696 #pragma GCC diagnostic push
697 #pragma GCC diagnostic ignored "-Wunused-function"
698 gint bar_get_width_unused(GtkWidget *bar)
699 {
700         BarData *bd;
701
702         bd = static_cast<BarData *>(g_object_get_data(G_OBJECT(bar), "bar_data"));
703         if (!bd) return 0;
704
705         return bd->width;
706 }
707 #pragma GCC diagnostic pop
708
709 void bar_close(GtkWidget *bar)
710 {
711         BarData *bd;
712
713         bd = static_cast<BarData *>(g_object_get_data(G_OBJECT(bar), "bar_data"));
714         if (!bd) return;
715
716         /* @FIXME This causes a g_object_unref failed error on exit */
717         gq_gtk_widget_destroy(bd->widget);
718 }
719
720 static void bar_destroy(GtkWidget *, gpointer data)
721 {
722         auto bd = static_cast<BarData *>(data);
723
724         file_data_unref(bd->fd);
725         g_free(bd);
726 }
727
728 #ifdef HAVE_LIBCHAMPLAIN_GTK
729 /**
730    @FIXME this is an ugly hack that works around this bug:
731    https://bugzilla.gnome.org/show_bug.cgi?id=590692
732    http://bugzilla.openedhand.com/show_bug.cgi?id=1751
733    it should be removed as soon as a better solution exists
734 */
735
736 static void bar_unrealize_clutter_fix_cb(GtkWidget *widget, gpointer)
737 {
738         GtkWidget *child = gtk_bin_get_child(GTK_BIN(widget));
739         if (child) gtk_widget_unrealize(child);
740 }
741 #endif
742
743 GtkWidget *bar_new(LayoutWindow *lw)
744 {
745         BarData *bd;
746         GtkWidget *box;
747         GtkWidget *scrolled;
748         GtkWidget *tbar;
749         GtkWidget *add_box;
750
751         bd = g_new0(BarData, 1);
752
753         bd->lw = lw;
754
755         bd->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, PREF_PAD_GAP);
756         DEBUG_NAME(bd->widget);
757         g_object_set_data(G_OBJECT(bd->widget), "bar_data", bd);
758         g_signal_connect(G_OBJECT(bd->widget), "destroy",
759                          G_CALLBACK(bar_destroy), bd);
760
761         g_signal_connect(G_OBJECT(bd->widget), "size-allocate",
762                          G_CALLBACK(bar_size_allocate), bd);
763
764         g_signal_connect(G_OBJECT(bd->widget), "button_release_event", G_CALLBACK(bar_menu_cb), bd);
765
766         bd->width = SIDEBAR_DEFAULT_WIDTH;
767
768         box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
769         DEBUG_NAME(box);
770
771         bd->label_file_name = gtk_label_new("");
772         gtk_label_set_ellipsize(GTK_LABEL(bd->label_file_name), PANGO_ELLIPSIZE_END);
773         gtk_label_set_selectable(GTK_LABEL(bd->label_file_name), TRUE);
774         gtk_label_set_xalign(GTK_LABEL(bd->label_file_name), 0.5);
775         gtk_label_set_yalign(GTK_LABEL(bd->label_file_name), 0.5);
776
777         gq_gtk_box_pack_start(GTK_BOX(box), bd->label_file_name, TRUE, TRUE, 0);
778         gtk_widget_show(bd->label_file_name);
779
780         gq_gtk_box_pack_start(GTK_BOX(bd->widget), box, FALSE, FALSE, 0);
781         gtk_widget_show(box);
782
783         scrolled = gq_gtk_scrolled_window_new(nullptr, nullptr);
784         DEBUG_NAME(scrolled);
785         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
786                 GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
787         gq_gtk_box_pack_start(GTK_BOX(bd->widget), scrolled, TRUE, TRUE, 0);
788         gtk_widget_show(scrolled);
789
790
791         bd->vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
792         gq_gtk_container_add(GTK_WIDGET(scrolled), bd->vbox);
793         gtk_viewport_set_shadow_type(GTK_VIEWPORT(gtk_bin_get_child(GTK_BIN(scrolled))), GTK_SHADOW_NONE);
794
795         add_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
796         DEBUG_NAME(add_box);
797         gq_gtk_box_pack_end(GTK_BOX(bd->widget), add_box, FALSE, FALSE, 0);
798         tbar = pref_toolbar_new(add_box);
799         bd->add_button = pref_toolbar_button(tbar, GQ_ICON_ADD, _("Add"), FALSE,
800                                              _("Add Pane"), G_CALLBACK(bar_menu_add_cb), bd);
801         gtk_widget_show(add_box);
802
803 #ifdef HAVE_LIBCHAMPLAIN_GTK
804         g_signal_connect(G_OBJECT(gtk_bin_get_child(GTK_BIN(scrolled))), "unrealize", G_CALLBACK(bar_unrealize_clutter_fix_cb), NULL);
805 #endif
806
807         gq_gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_NONE);
808         gtk_widget_show(bd->vbox);
809         return bd->widget;
810 }
811
812
813 GtkWidget *bar_update_from_config(GtkWidget *bar, const gchar **attribute_names, const gchar **attribute_values, LayoutWindow *lw, gboolean startup)
814 {
815         gboolean enabled = TRUE;
816         gint width = SIDEBAR_DEFAULT_WIDTH;
817
818         while (*attribute_names)
819                 {
820                 const gchar *option = *attribute_names++;
821                 const gchar *value = *attribute_values++;
822
823                 if (READ_BOOL_FULL("enabled", enabled)) continue;
824                 if (READ_INT_FULL("width", width)) continue;
825
826
827                 log_printf("unknown attribute %s = %s\n", option, value);
828                 }
829
830         if (startup)
831                 {
832                 gtk_paned_set_position(GTK_PANED(lw->utility_paned), width);
833                 }
834
835         if (enabled)
836                 {
837                 gtk_widget_show(bar);
838                 }
839         else
840                 {
841                 gtk_widget_hide(bar);
842                 }
843         return bar;
844 }
845
846 GtkWidget *bar_new_from_config(LayoutWindow *lw, const gchar **attribute_names, const gchar **attribute_values)
847 {
848         GtkWidget *bar = bar_new(lw);
849         return bar_update_from_config(bar, attribute_names, attribute_values, lw, TRUE);
850 }
851
852 GtkWidget *bar_pane_expander_title(const gchar *title)
853 {
854         GtkWidget *widget = gtk_label_new(title);
855
856         pref_label_bold(widget, TRUE, FALSE);
857         gtk_label_set_ellipsize(GTK_LABEL(widget), PANGO_ELLIPSIZE_END);
858
859         return widget;
860 }
861
862 gboolean bar_pane_translate_title(PaneType type, const gchar *id, gchar **title)
863 {
864         const KnownPanes *pane = known_panes;
865
866         if (!title) return FALSE;
867         while (pane->id)
868                 {
869                 if (pane->type == type && strcmp(pane->id, id) == 0) break;
870                 pane++;
871                 }
872         if (!pane->id) return FALSE;
873
874         if (*title && **title && strcmp(pane->title, *title) != 0) return FALSE;
875
876         g_free(*title);
877         *title = g_strdup(_(pane->title));
878         return TRUE;
879 }
880
881 static const gchar *bar_pane_get_default_config(const gchar *id)
882 {
883         const KnownPanes *pane = known_panes;
884
885         while (pane->id)
886                 {
887                 if (strcmp(pane->id, id) == 0) break;
888                 pane++;
889                 }
890         if (!pane->id) return nullptr;
891         return pane->config;
892 }
893
894 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */