]> git.tdb.fi Git - ext/subsurface.git/blob - gtk-gui.c
Move the "Import" function from the File menu to the Log menu
[ext/subsurface.git] / gtk-gui.c
1 /* gtk-gui.c */
2 /* gtk UI implementation */
3 /* creates the window and overall layout
4  * divelist, dive info, equipment and printing are handled in their own source files
5  */
6 #include <stdio.h>
7 #include <string.h>
8 #include <stdlib.h>
9 #include <time.h>
10 #include <unistd.h>
11
12 #include "dive.h"
13 #include "divelist.h"
14 #include "display.h"
15 #include "display-gtk.h"
16
17 #include "libdivecomputer.h"
18
19 GtkWidget *main_window;
20 GtkWidget *main_vbox;
21 GtkWidget *error_info_bar;
22 GtkWidget *error_label;
23 GtkWidget *vpane, *hpane;
24 int        error_count;
25
26 const char *divelist_font;
27
28 struct units output_units;
29
30 static GtkWidget *dive_profile;
31
32 visible_cols_t visible_cols = {TRUE, FALSE};
33
34 static const char *default_dive_computer;
35
36 static int is_default_dive_computer(const char *name)
37 {
38         return default_dive_computer && !strcmp(name, default_dive_computer);
39 }
40
41 static void set_default_dive_computer(const char *name)
42 {
43         if (!name || !*name)
44                 return;
45         if (is_default_dive_computer(name))
46                 return;
47         default_dive_computer = name;
48         subsurface_set_conf("dive_computer", PREF_STRING, name);
49 }
50
51 void repaint_dive(void)
52 {
53         update_dive(current_dive);
54         if (dive_profile)
55                 gtk_widget_queue_draw(dive_profile);
56 }
57
58 static char *existing_filename;
59 static gboolean need_icon = TRUE;
60
61 static void on_info_bar_response(GtkWidget *widget, gint response,
62                                  gpointer data)
63 {
64         if (response == GTK_RESPONSE_OK)
65         {
66                 gtk_widget_destroy(widget);
67                 error_info_bar = NULL;
68         }
69 }
70
71 void report_error(GError* error)
72 {
73         if (error == NULL)
74         {
75                 return;
76         }
77         
78         if (error_info_bar == NULL)
79         {
80                 error_count = 1;
81                 error_info_bar = gtk_info_bar_new_with_buttons(GTK_STOCK_OK,
82                                                                GTK_RESPONSE_OK,
83                                                                NULL);
84                 g_signal_connect(error_info_bar, "response", G_CALLBACK(on_info_bar_response), NULL);
85                 gtk_info_bar_set_message_type(GTK_INFO_BAR(error_info_bar),
86                                               GTK_MESSAGE_ERROR);
87                 
88                 error_label = gtk_label_new(error->message);
89                 GtkWidget *container = gtk_info_bar_get_content_area(GTK_INFO_BAR(error_info_bar));
90                 gtk_container_add(GTK_CONTAINER(container), error_label);
91                 
92                 gtk_box_pack_start(GTK_BOX(main_vbox), error_info_bar, FALSE, FALSE, 0);
93                 gtk_widget_show_all(main_vbox);
94         }
95         else
96         {
97                 error_count++;
98                 char buffer[256];
99                 snprintf(buffer, sizeof(buffer), "Failed to open %i files.", error_count);
100                 gtk_label_set(GTK_LABEL(error_label), buffer);
101         }
102 }
103
104 static void file_open(GtkWidget *w, gpointer data)
105 {
106         GtkWidget *dialog;
107         GtkFileFilter *filter;
108
109         dialog = gtk_file_chooser_dialog_new("Open File",
110                 GTK_WINDOW(main_window),
111                 GTK_FILE_CHOOSER_ACTION_OPEN,
112                 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
113                 GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
114                 NULL);
115         gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), TRUE);
116
117         filter = gtk_file_filter_new();
118         gtk_file_filter_add_pattern(filter, "*.xml");
119         gtk_file_filter_add_pattern(filter, "*.XML");
120         gtk_file_filter_add_pattern(filter, "*.sda");
121         gtk_file_filter_add_pattern(filter, "*.SDA");
122         gtk_file_filter_add_mime_type(filter, "text/xml");
123         gtk_file_filter_set_name(filter, "XML file");
124         gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(dialog), filter);
125
126         if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
127                 GSList *filenames, *fn_glist;
128                 char *filename;
129                 filenames = fn_glist = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog));
130                 
131                 GError *error = NULL;
132                 while(filenames != NULL) {
133                         filename = filenames->data;
134                         parse_file(filename, &error);
135                         if (error != NULL)
136                         {
137                                 report_error(error);
138                                 g_error_free(error);
139                                 error = NULL;
140                         }
141                         
142                         g_free(filename);
143                         filenames = g_slist_next(filenames);
144                 }
145                 g_slist_free(fn_glist);
146                 report_dives(FALSE);
147         }
148         gtk_widget_destroy(dialog);
149 }
150
151 static void file_save(GtkWidget *w, gpointer data)
152 {
153         GtkWidget *dialog;
154         dialog = gtk_file_chooser_dialog_new("Save File",
155                 GTK_WINDOW(main_window),
156                 GTK_FILE_CHOOSER_ACTION_SAVE,
157                 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
158                 GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
159                 NULL);
160         gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog), TRUE);
161         if (!existing_filename) {
162                 gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), "Untitled document");
163         } else
164                 gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog), existing_filename);
165
166         if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
167                 char *filename;
168                 filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
169                 save_dives(filename);
170                 g_free(filename);
171                 mark_divelist_changed(FALSE);
172         }
173         gtk_widget_destroy(dialog);
174 }
175
176 static void ask_save_changes()
177 {
178         GtkWidget *dialog, *label, *content;
179         dialog = gtk_dialog_new_with_buttons("Save Changes?",
180                 GTK_WINDOW(main_window), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
181                 GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
182                 GTK_STOCK_NO, GTK_RESPONSE_NO,
183                 NULL);
184         content = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
185         label = gtk_label_new ("You have unsaved changes\nWould you like to save those before exiting the program?");
186         gtk_container_add (GTK_CONTAINER (content), label);
187         gtk_widget_show_all (dialog);
188         gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
189         if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
190                 file_save(NULL,NULL);
191         }
192         gtk_widget_destroy(dialog);
193 }
194
195 static gboolean on_delete(GtkWidget* w, gpointer data)
196 {
197         /* Make sure to flush any modified dive data */
198         update_dive(NULL);
199
200         if (unsaved_changes())
201                 ask_save_changes();
202
203         return FALSE; /* go ahead, kill the program, we're good now */
204 }
205
206 static void on_destroy(GtkWidget* w, gpointer data)
207 {
208         gtk_main_quit();
209 }
210
211 static void quit(GtkWidget *w, gpointer data)
212 {
213         /* Make sure to flush any modified dive data */
214         update_dive(NULL);
215
216         if (unsaved_changes())
217                 ask_save_changes();
218         gtk_main_quit();
219 }
220
221 GtkTreeViewColumn *tree_view_column(GtkWidget *tree_view, int index, const char *title,
222                                 data_func_t data_func, unsigned int flags)
223 {
224         GtkCellRenderer *renderer;
225         GtkTreeViewColumn *col;
226         double xalign = 0.0; /* left as default */
227         PangoAlignment align;
228         gboolean visible;
229
230         align = (flags & ALIGN_LEFT) ? PANGO_ALIGN_LEFT :
231                 (flags & ALIGN_RIGHT) ? PANGO_ALIGN_RIGHT :
232                 PANGO_ALIGN_CENTER;
233         visible = !(flags & INVISIBLE);
234
235         renderer = gtk_cell_renderer_text_new();
236         col = gtk_tree_view_column_new();
237
238         gtk_tree_view_column_set_title(col, title);
239         if (!(flags & UNSORTABLE))
240                 gtk_tree_view_column_set_sort_column_id(col, index);
241         gtk_tree_view_column_set_resizable(col, TRUE);
242         gtk_tree_view_column_pack_start(col, renderer, TRUE);
243         if (data_func)
244                 gtk_tree_view_column_set_cell_data_func(col, renderer, data_func, (void *)(long)index, NULL);
245         else
246                 gtk_tree_view_column_add_attribute(col, renderer, "text", index);
247         gtk_object_set(GTK_OBJECT(renderer), "alignment", align, NULL);
248         switch (align) {
249         case PANGO_ALIGN_LEFT:
250                 xalign = 0.0;
251                 break;
252         case PANGO_ALIGN_CENTER:
253                 xalign = 0.5;
254                 break;
255         case PANGO_ALIGN_RIGHT:
256                 xalign = 1.0;
257                 break;
258         }
259         gtk_cell_renderer_set_alignment(GTK_CELL_RENDERER(renderer), xalign, 0.5);
260         gtk_tree_view_column_set_visible(col, visible);
261         gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), col);
262         return col;
263 }
264
265 static void create_radio(GtkWidget *vbox, const char *w_name, ...)
266 {
267         va_list args;
268         GtkRadioButton *group = NULL;
269         GtkWidget *box, *label;
270
271         box = gtk_hbox_new(TRUE, 10);
272         gtk_box_pack_start(GTK_BOX(vbox), box, FALSE, FALSE, 0);
273
274         label = gtk_label_new(w_name);
275         gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 0);
276
277         va_start(args, w_name);
278         for (;;) {
279                 int enabled;
280                 const char *name;
281                 GtkWidget *button;
282                 void *callback_fn;
283
284                 name = va_arg(args, char *);
285                 if (!name)
286                         break;
287                 callback_fn = va_arg(args, void *);
288                 enabled = va_arg(args, int);
289
290                 button = gtk_radio_button_new_with_label_from_widget(group, name);
291                 group = GTK_RADIO_BUTTON(button);
292                 gtk_box_pack_start(GTK_BOX(box), button, TRUE, TRUE, 0);
293                 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), enabled);
294                 g_signal_connect(button, "toggled", G_CALLBACK(callback_fn), NULL);
295         }
296         va_end(args);
297 }
298
299 #define UNITCALLBACK(name, type, value)                         \
300 static void name(GtkWidget *w, gpointer data)                   \
301 {                                                               \
302         if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(w))) \
303                 menu_units.type = value;                        \
304 }
305
306 static struct units menu_units;
307
308 UNITCALLBACK(set_meter, length, METERS)
309 UNITCALLBACK(set_feet, length, FEET)
310 UNITCALLBACK(set_bar, pressure, BAR)
311 UNITCALLBACK(set_psi, pressure, PSI)
312 UNITCALLBACK(set_liter, volume, LITER)
313 UNITCALLBACK(set_cuft, volume, CUFT)
314 UNITCALLBACK(set_celsius, temperature, CELSIUS)
315 UNITCALLBACK(set_fahrenheit, temperature, FAHRENHEIT)
316 UNITCALLBACK(set_kg, weight, KG)
317 UNITCALLBACK(set_lbs, weight, LBS)
318
319 #define OPTIONCALLBACK(name, option) \
320 static void name(GtkWidget *w, gpointer data) \
321 { \
322         option = GTK_TOGGLE_BUTTON(w)->active; \
323 }
324
325 OPTIONCALLBACK(otu_toggle, visible_cols.otu)
326 OPTIONCALLBACK(sac_toggle, visible_cols.sac)
327 OPTIONCALLBACK(nitrox_toggle, visible_cols.nitrox)
328 OPTIONCALLBACK(temperature_toggle, visible_cols.temperature)
329 OPTIONCALLBACK(cylinder_toggle, visible_cols.cylinder)
330
331 static void event_toggle(GtkWidget *w, gpointer _data)
332 {
333         gboolean *plot_ev = _data;
334
335         *plot_ev = GTK_TOGGLE_BUTTON(w)->active;
336 }
337
338 static void preferences_dialog(GtkWidget *w, gpointer data)
339 {
340         int result;
341         GtkWidget *dialog, *font, *frame, *box, *vbox, *button;
342
343         menu_units = output_units;
344
345         dialog = gtk_dialog_new_with_buttons("Preferences",
346                 GTK_WINDOW(main_window),
347                 GTK_DIALOG_DESTROY_WITH_PARENT,
348                 GTK_STOCK_OK, GTK_RESPONSE_ACCEPT,
349                 GTK_STOCK_CANCEL, GTK_RESPONSE_REJECT,
350                 NULL);
351
352         frame = gtk_frame_new("Units");
353         vbox = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
354         gtk_box_pack_start(GTK_BOX(vbox), frame, FALSE, FALSE, 5);
355
356         box = gtk_vbox_new(FALSE, 6);
357         gtk_container_add(GTK_CONTAINER(frame), box);
358
359         create_radio(box, "Depth:",
360                 "Meter", set_meter, (output_units.length == METERS),
361                 "Feet",  set_feet, (output_units.length == FEET),
362                 NULL);
363
364         create_radio(box, "Pressure:",
365                 "Bar", set_bar, (output_units.pressure == BAR),
366                 "PSI",  set_psi, (output_units.pressure == PSI),
367                 NULL);
368
369         create_radio(box, "Volume:",
370                 "Liter",  set_liter, (output_units.volume == LITER),
371                 "CuFt", set_cuft, (output_units.volume == CUFT),
372                 NULL);
373
374         create_radio(box, "Temperature:",
375                 "Celsius", set_celsius, (output_units.temperature == CELSIUS),
376                 "Fahrenheit",  set_fahrenheit, (output_units.temperature == FAHRENHEIT),
377                 NULL);
378
379         create_radio(box, "Weight:",
380                 "kg", set_kg, (output_units.weight == KG),
381                 "lbs",  set_lbs, (output_units.weight == LBS),
382                 NULL);
383
384         frame = gtk_frame_new("Columns");
385         gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox), frame, FALSE, FALSE, 5);
386
387         box = gtk_hbox_new(FALSE, 6);
388         gtk_container_add(GTK_CONTAINER(frame), box);
389
390         button = gtk_check_button_new_with_label("Show Temp");
391         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), visible_cols.temperature);
392         gtk_box_pack_start(GTK_BOX(box), button, FALSE, FALSE, 6);
393         g_signal_connect(G_OBJECT(button), "toggled", G_CALLBACK(temperature_toggle), NULL);
394
395         button = gtk_check_button_new_with_label("Show Cyl");
396         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), visible_cols.cylinder);
397         gtk_box_pack_start(GTK_BOX(box), button, FALSE, FALSE, 6);
398         g_signal_connect(G_OBJECT(button), "toggled", G_CALLBACK(cylinder_toggle), NULL);
399
400         button = gtk_check_button_new_with_label("Show O" UTF8_SUBSCRIPT_2 "%");
401         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), visible_cols.nitrox);
402         gtk_box_pack_start(GTK_BOX(box), button, FALSE, FALSE, 6);
403         g_signal_connect(G_OBJECT(button), "toggled", G_CALLBACK(nitrox_toggle), NULL);
404
405         button = gtk_check_button_new_with_label("Show SAC");
406         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), visible_cols.sac);
407         gtk_box_pack_start(GTK_BOX(box), button, FALSE, FALSE, 6);
408         g_signal_connect(G_OBJECT(button), "toggled", G_CALLBACK(sac_toggle), NULL);
409
410         button = gtk_check_button_new_with_label("Show OTU");
411         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), visible_cols.otu);
412         gtk_box_pack_start(GTK_BOX(box), button, FALSE, FALSE, 6);
413         g_signal_connect(G_OBJECT(button), "toggled", G_CALLBACK(otu_toggle), NULL);
414
415         font = gtk_font_button_new_with_font(divelist_font);
416         gtk_box_pack_start(GTK_BOX(vbox), font, FALSE, FALSE, 5);
417
418         gtk_widget_show_all(dialog);
419         result = gtk_dialog_run(GTK_DIALOG(dialog));
420         if (result == GTK_RESPONSE_ACCEPT) {
421                 /* Make sure to flush any modified old dive data with old units */
422                 update_dive(NULL);
423
424                 divelist_font = strdup(gtk_font_button_get_font_name(GTK_FONT_BUTTON(font)));
425                 set_divelist_font(divelist_font);
426
427                 output_units = menu_units;
428                 update_dive_list_units();
429                 repaint_dive();
430                 update_dive_list_col_visibility();
431
432                 subsurface_set_conf("feet", PREF_BOOL, BOOL_TO_PTR(output_units.length == FEET));
433                 subsurface_set_conf("psi", PREF_BOOL, BOOL_TO_PTR(output_units.pressure == PSI));
434                 subsurface_set_conf("cuft", PREF_BOOL, BOOL_TO_PTR(output_units.volume == CUFT));
435                 subsurface_set_conf("fahrenheit", PREF_BOOL, BOOL_TO_PTR(output_units.temperature == FAHRENHEIT));
436                 subsurface_set_conf("lbs", PREF_BOOL, BOOL_TO_PTR(output_units.weight == LBS));
437                 subsurface_set_conf("TEMPERATURE", PREF_BOOL, BOOL_TO_PTR(visible_cols.temperature));
438                 subsurface_set_conf("CYLINDER", PREF_BOOL, BOOL_TO_PTR(visible_cols.cylinder));
439                 subsurface_set_conf("NITROX", PREF_BOOL, BOOL_TO_PTR(visible_cols.nitrox));
440                 subsurface_set_conf("SAC", PREF_BOOL, BOOL_TO_PTR(visible_cols.sac));
441                 subsurface_set_conf("OTU", PREF_BOOL, BOOL_TO_PTR(visible_cols.otu));
442                 subsurface_set_conf("divelist_font", PREF_STRING, divelist_font);
443
444                 /* Flush the changes out to the system */
445                 subsurface_flush_conf();
446         }
447         gtk_widget_destroy(dialog);
448 }
449
450 static void create_toggle(const char* label, int *on, void *_data)
451 {
452         GtkWidget *button, *table = _data;
453         int rows, cols, x, y;
454         static int count;
455
456         if (table == NULL) {
457                 /* magic way to reset the number of toggle buttons
458                  * that we have already added - call this before you
459                  * create the dialog */
460                 count = 0;
461                 return;
462         }
463         g_object_get(G_OBJECT(table), "n-columns", &cols, "n-rows", &rows, NULL);
464         if (count > rows * cols) {
465                 gtk_table_resize(GTK_TABLE(table),rows+1,cols);
466                 rows++;
467         }
468         x = count % cols;
469         y = count / cols;
470         button = gtk_check_button_new_with_label(label);
471         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), *on);
472         gtk_table_attach_defaults(GTK_TABLE(table), button, x, x+1, y, y+1);
473         g_signal_connect(G_OBJECT(button), "toggled", G_CALLBACK(event_toggle), on);
474         count++;
475 }
476
477 static void selectevents_dialog(GtkWidget *w, gpointer data)
478 {
479         int result;
480         GtkWidget *dialog, *frame, *vbox, *table;
481
482         dialog = gtk_dialog_new_with_buttons("SelectEvents",
483                 GTK_WINDOW(main_window),
484                 GTK_DIALOG_DESTROY_WITH_PARENT,
485                 GTK_STOCK_OK, GTK_RESPONSE_ACCEPT,
486                 GTK_STOCK_CANCEL, GTK_RESPONSE_REJECT,
487                 NULL);
488         /* initialize the function that fills the table */
489         create_toggle(NULL, NULL, NULL);
490
491         frame = gtk_frame_new("Enable / Disable Events");
492         vbox = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
493         gtk_box_pack_start(GTK_BOX(vbox), frame, FALSE, FALSE, 5);
494
495         table = gtk_table_new(1, 4, TRUE);
496         gtk_container_add(GTK_CONTAINER(frame), table);
497
498         evn_foreach(&create_toggle, table);
499
500         gtk_widget_show_all(dialog);
501         result = gtk_dialog_run(GTK_DIALOG(dialog));
502         if (result == GTK_RESPONSE_ACCEPT) {
503                 repaint_dive();
504         }
505         gtk_widget_destroy(dialog);
506 }
507
508 static void renumber_dialog(GtkWidget *w, gpointer data)
509 {
510         int result;
511         struct dive *dive;
512         GtkWidget *dialog, *frame, *button, *vbox;
513
514         dialog = gtk_dialog_new_with_buttons("Renumber",
515                 GTK_WINDOW(main_window),
516                 GTK_DIALOG_DESTROY_WITH_PARENT,
517                 GTK_STOCK_OK, GTK_RESPONSE_ACCEPT,
518                 GTK_STOCK_CANCEL, GTK_RESPONSE_REJECT,
519                 NULL);
520
521         vbox = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
522
523         frame = gtk_frame_new("New starting number");
524         gtk_box_pack_start(GTK_BOX(vbox), frame, FALSE, FALSE, 5);
525
526         button = gtk_spin_button_new_with_range(1, 50000, 1);
527         gtk_container_add(GTK_CONTAINER(frame), button);
528
529         /*
530          * Do we have a number for the first dive already? Use that
531          * as the default.
532          */
533         dive = get_dive(0);
534         if (dive && dive->number)
535                 gtk_spin_button_set_value(GTK_SPIN_BUTTON(button), dive->number);
536
537         gtk_widget_show_all(dialog);
538         result = gtk_dialog_run(GTK_DIALOG(dialog));
539         if (result == GTK_RESPONSE_ACCEPT) {
540                 int nr = gtk_spin_button_get_value(GTK_SPIN_BUTTON(button));
541                 renumber_dives(nr);
542                 repaint_dive();
543         }
544         gtk_widget_destroy(dialog);
545 }
546
547 static void about_dialog(GtkWidget *w, gpointer data)
548 {
549         const char *logo_property = NULL;
550         GdkPixbuf *logo = NULL;
551
552         if (need_icon) {
553                 GtkWidget *image = gtk_image_new_from_file(subsurface_icon_name());
554
555                 if (image) {
556                         logo = gtk_image_get_pixbuf(GTK_IMAGE(image));
557                         logo_property = "logo";
558                 }
559         }
560
561         gtk_show_about_dialog(NULL,
562                 "program-name", "SubSurface",
563                 "comments", "Half-arsed divelog software in C",
564                 "license", "GPLv2",
565                 "version", VERSION_STRING,
566                 "copyright", "Linus Torvalds 2011",
567                 "logo-icon-name", "subsurface",
568                 /* Must be last: */
569                 logo_property, logo,
570                 NULL);
571 }
572
573 static void view_list(GtkWidget *w, gpointer data)
574 {
575         gtk_paned_set_position(GTK_PANED(vpane), 0);
576 }
577
578 static void view_profile(GtkWidget *w, gpointer data)
579 {
580         gtk_paned_set_position(GTK_PANED(hpane), 0);
581         gtk_paned_set_position(GTK_PANED(vpane), 65535);
582 }
583
584 static void view_info(GtkWidget *w, gpointer data)
585 {
586         gtk_paned_set_position(GTK_PANED(vpane), 65535);
587         gtk_paned_set_position(GTK_PANED(hpane), 65535);
588 }
589
590 /* Ooh. I don't know how to get the half-way size. So I'm just using random numbers */
591 static void view_three(GtkWidget *w, gpointer data)
592 {
593         gtk_paned_set_position(GTK_PANED(hpane), 400);
594         gtk_paned_set_position(GTK_PANED(vpane), 200);
595 }
596
597 static GtkActionEntry menu_items[] = {
598         { "FileMenuAction", GTK_STOCK_FILE, "File", NULL, NULL, NULL},
599         { "LogMenuAction",  GTK_STOCK_FILE, "Log", NULL, NULL, NULL},
600         { "ViewMenuAction",  GTK_STOCK_FILE, "View", NULL, NULL, NULL},
601         { "FilterMenuAction",  GTK_STOCK_FILE, "Filter", NULL, NULL, NULL},
602         { "HelpMenuAction", GTK_STOCK_HELP, "Help", NULL, NULL, NULL},
603         { "OpenFile",       GTK_STOCK_OPEN, NULL,   CTRLCHAR "O", NULL, G_CALLBACK(file_open) },
604         { "SaveFile",       GTK_STOCK_SAVE, NULL,   CTRLCHAR "S", NULL, G_CALLBACK(file_save) },
605         { "Print",          GTK_STOCK_PRINT, NULL,  CTRLCHAR "P", NULL, G_CALLBACK(do_print) },
606         { "Import",         NULL, "Import", NULL, NULL, G_CALLBACK(import_dialog) },
607         { "Preferences",    NULL, "Preferences", PREFERENCE_ACCEL, NULL, G_CALLBACK(preferences_dialog) },
608         { "Renumber",       NULL, "Renumber", NULL, NULL, G_CALLBACK(renumber_dialog) },
609         { "SelectEvents",   NULL, "SelectEvents", NULL, NULL, G_CALLBACK(selectevents_dialog) },
610         { "Quit",           GTK_STOCK_QUIT, NULL,   CTRLCHAR "Q", NULL, G_CALLBACK(quit) },
611         { "About",          GTK_STOCK_ABOUT, NULL,  NULL, NULL, G_CALLBACK(about_dialog) },
612         { "ViewList",       NULL, "List",  CTRLCHAR "1", NULL, G_CALLBACK(view_list) },
613         { "ViewProfile",    NULL, "Profile", CTRLCHAR "2", NULL, G_CALLBACK(view_profile) },
614         { "ViewInfo",       NULL, "Info", CTRLCHAR "3", NULL, G_CALLBACK(view_info) },
615         { "ViewThree",       NULL, "Three", CTRLCHAR "4", NULL, G_CALLBACK(view_three) },
616 };
617 static gint nmenu_items = sizeof (menu_items) / sizeof (menu_items[0]);
618
619 static const gchar* ui_string = " \
620         <ui> \
621                 <menubar name=\"MainMenu\"> \
622                         <menu name=\"FileMenu\" action=\"FileMenuAction\"> \
623                                 <menuitem name=\"Open\" action=\"OpenFile\" /> \
624                                 <menuitem name=\"Save\" action=\"SaveFile\" /> \
625                                 <menuitem name=\"Print\" action=\"Print\" /> \
626                                 <separator name=\"Separator1\"/> \
627                                 <menuitem name=\"Preferences\" action=\"Preferences\" /> \
628                                 <separator name=\"Separator2\"/> \
629                                 <menuitem name=\"Quit\" action=\"Quit\" /> \
630                         </menu> \
631                         <menu name=\"LogMenu\" action=\"LogMenuAction\"> \
632                                 <menuitem name=\"Import\" action=\"Import\" /> \
633                                 <separator name=\"Separator\"/> \
634                                 <menuitem name=\"Renumber\" action=\"Renumber\" /> \
635                                 <menu name=\"View\" action=\"ViewMenuAction\"> \
636                                         <menuitem name=\"List\" action=\"ViewList\" /> \
637                                         <menuitem name=\"Profile\" action=\"ViewProfile\" /> \
638                                         <menuitem name=\"Info\" action=\"ViewInfo\" /> \
639                                         <menuitem name=\"Paned\" action=\"ViewThree\" /> \
640                                 </menu> \
641                         </menu> \
642                         <menu name=\"FilterMenu\" action=\"FilterMenuAction\"> \
643                                 <menuitem name=\"SelectEvents\" action=\"SelectEvents\" /> \
644                         </menu> \
645                         <menu name=\"Help\" action=\"HelpMenuAction\"> \
646                                 <menuitem name=\"About\" action=\"About\" /> \
647                         </menu> \
648                 </menubar> \
649         </ui> \
650 ";
651
652 static GtkWidget *get_menubar_menu(GtkWidget *window, GtkUIManager *ui_manager)
653 {
654         GtkActionGroup *action_group = gtk_action_group_new("Menu");
655         gtk_action_group_add_actions(action_group, menu_items, nmenu_items, 0);
656
657         gtk_ui_manager_insert_action_group(ui_manager, action_group, 0);
658         GError* error = 0;
659         gtk_ui_manager_add_ui_from_string(GTK_UI_MANAGER(ui_manager), ui_string, -1, &error);
660
661         gtk_window_add_accel_group(GTK_WINDOW(window), gtk_ui_manager_get_accel_group(ui_manager));
662         GtkWidget* menu = gtk_ui_manager_get_widget(ui_manager, "/MainMenu");
663
664         return menu;
665 }
666
667 static void switch_page(GtkNotebook *notebook, gint arg1, gpointer user_data)
668 {
669         repaint_dive();
670 }
671
672 void init_ui(int *argcp, char ***argvp)
673 {
674         GtkWidget *win;
675         GtkWidget *notebook;
676         GtkWidget *nb_page;
677         GtkWidget *dive_list;
678         GtkWidget *menubar;
679         GtkWidget *vbox;
680         GdkScreen *screen;
681         GtkIconTheme *icon_theme=NULL;
682         GtkSettings *settings;
683         GtkUIManager *ui_manager;
684
685         gtk_init(argcp, argvp);
686         settings = gtk_settings_get_default();
687         gtk_settings_set_long_property(settings, "gtk_tooltip_timeout", 10, "subsurface setting");
688
689         g_type_init();
690
691         subsurface_open_conf();
692         if (subsurface_get_conf("feet", PREF_BOOL))
693                 output_units.length = FEET;
694         if (subsurface_get_conf("psi", PREF_BOOL))
695                 output_units.pressure = PSI;
696         if (subsurface_get_conf("cuft", PREF_BOOL))
697                 output_units.volume = CUFT;
698         if (subsurface_get_conf("fahrenheit", PREF_BOOL))
699                 output_units.temperature = FAHRENHEIT;
700         if (subsurface_get_conf("lbs", PREF_BOOL))
701                 output_units.weight = LBS;
702         /* an unset key is FALSE - all these are hidden by default */
703         visible_cols.cylinder = PTR_TO_BOOL(subsurface_get_conf("CYLINDER", PREF_BOOL));
704         visible_cols.temperature = PTR_TO_BOOL(subsurface_get_conf("TEMPERATURE", PREF_BOOL));
705         visible_cols.nitrox = PTR_TO_BOOL(subsurface_get_conf("NITROX", PREF_BOOL));
706         visible_cols.otu = PTR_TO_BOOL(subsurface_get_conf("OTU", PREF_BOOL));
707         visible_cols.sac = PTR_TO_BOOL(subsurface_get_conf("SAC", PREF_BOOL));
708
709         divelist_font = subsurface_get_conf("divelist_font", PREF_STRING);
710
711         default_dive_computer = subsurface_get_conf("dive_computer", PREF_STRING);
712
713         error_info_bar = NULL;
714         win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
715         g_set_application_name ("subsurface");
716         /* Let's check if the subsurface icon has been installed or if
717          * we need to try to load it from the current directory */
718         screen = gdk_screen_get_default();
719         if (screen)
720                 icon_theme = gtk_icon_theme_get_for_screen(screen);
721         if (icon_theme) {
722                 if (gtk_icon_theme_has_icon(icon_theme, "subsurface")) {
723                         need_icon = FALSE;
724                         gtk_window_set_default_icon_name ("subsurface");
725                 }
726         }
727         if (need_icon) {
728                 const char *icon_name = subsurface_icon_name();
729                 if (!access(icon_name, R_OK))
730                         gtk_window_set_icon_from_file(GTK_WINDOW(win), icon_name, NULL);
731         }
732         g_signal_connect(G_OBJECT(win), "delete-event", G_CALLBACK(on_delete), NULL);
733         g_signal_connect(G_OBJECT(win), "destroy", G_CALLBACK(on_destroy), NULL);
734         main_window = win;
735
736         vbox = gtk_vbox_new(FALSE, 0);
737         gtk_container_add(GTK_CONTAINER(win), vbox);
738         main_vbox = vbox;
739
740         ui_manager = gtk_ui_manager_new();
741         menubar = get_menubar_menu(win, ui_manager);
742
743         subsurface_ui_setup(settings, menubar, vbox, ui_manager);
744
745         vpane = gtk_vpaned_new();
746         gtk_box_pack_start(GTK_BOX(vbox), vpane, TRUE, TRUE, 3);
747
748         hpane = gtk_hpaned_new();
749         gtk_paned_add1(GTK_PANED(vpane), hpane);
750
751         /* Notebook for dive info vs profile vs .. */
752         notebook = gtk_notebook_new();
753         gtk_paned_add1(GTK_PANED(hpane), notebook);
754         g_signal_connect(notebook, "switch-page", G_CALLBACK(switch_page), NULL);
755
756         /* Create the actual divelist */
757         dive_list = dive_list_create();
758         gtk_widget_set_name(dive_list, "Dive List");
759         gtk_paned_add2(GTK_PANED(vpane), dive_list);
760
761         /* Frame for dive profile */
762         dive_profile = dive_profile_widget();
763         gtk_widget_set_name(dive_profile, "Dive Profile");
764         gtk_paned_add2(GTK_PANED(hpane), dive_profile);
765
766         /* Frame for extended dive info */
767         nb_page = extended_dive_info_widget();
768         gtk_notebook_append_page(GTK_NOTEBOOK(notebook), nb_page, gtk_label_new("Dive Notes"));
769
770         /* Frame for dive equipment */
771         nb_page = equipment_widget();
772         gtk_notebook_append_page(GTK_NOTEBOOK(notebook), nb_page, gtk_label_new("Equipment"));
773
774         /* Frame for single dive statistics */
775         nb_page = single_stats_widget();
776         gtk_notebook_append_page(GTK_NOTEBOOK(notebook), nb_page, gtk_label_new("Dive Info"));
777
778         /* Frame for total dive statistics */
779         nb_page = total_stats_widget();
780         gtk_notebook_append_page(GTK_NOTEBOOK(notebook), nb_page, gtk_label_new("Stats"));
781
782         gtk_widget_set_app_paintable(win, TRUE);
783         gtk_widget_show_all(win);
784
785         return;
786 }
787
788 void run_ui(void)
789 {
790         gtk_main();
791 }
792
793 void exit_ui(void)
794 {
795         subsurface_close_conf();
796 }
797
798 typedef struct {
799         cairo_rectangle_int_t rect;
800         const char *text;
801 } tooltip_record_t;
802
803 static tooltip_record_t *tooltip_rects;
804 static int tooltips;
805
806 void attach_tooltip(int x, int y, int w, int h, const char *text)
807 {
808         cairo_rectangle_int_t *rect;
809         tooltip_rects = realloc(tooltip_rects, (tooltips + 1) * sizeof(tooltip_record_t));
810         rect = &tooltip_rects[tooltips].rect;
811         rect->x = x;
812         rect->y = y;
813         rect->width = w;
814         rect->height = h;
815         tooltip_rects[tooltips].text = text;
816         tooltips++;
817 }
818
819 #define INSIDE_RECT(_r,_x,_y)   ((_r.x <= _x) && (_r.x + _r.width >= _x) && \
820                                 (_r.y <= _y) && (_r.y + _r.height >= _y))
821
822 static gboolean profile_tooltip (GtkWidget *widget, gint x, gint y,
823                         gboolean keyboard_mode, GtkTooltip *tooltip, gpointer user_data)
824 {
825         int i;
826         cairo_rectangle_int_t *drawing_area = user_data;
827         gint tx = x - drawing_area->x; /* get transformed coordinates */
828         gint ty = y - drawing_area->y;
829
830         /* are we over an event marker ? */
831         for (i = 0; i < tooltips; i++) {
832                 if (INSIDE_RECT(tooltip_rects[i].rect, tx, ty)) {
833                         gtk_tooltip_set_text(tooltip,tooltip_rects[i].text);
834                         return TRUE; /* show tooltip */
835                 }
836         }
837         return FALSE; /* don't show tooltip */
838 }
839
840 static gboolean expose_event(GtkWidget *widget, GdkEventExpose *event, gpointer data)
841 {
842         struct dive *dive = current_dive;
843         struct graphics_context gc = { .printer = 0 };
844         static cairo_rectangle_int_t drawing_area;
845
846         /* the drawing area gives TOTAL width * height - x,y is used as the topx/topy offset
847          * so effective drawing area is width-2x * height-2y */
848         drawing_area.width = widget->allocation.width;
849         drawing_area.height = widget->allocation.height;
850         drawing_area.x = drawing_area.width / 20.0;
851         drawing_area.y = drawing_area.height / 20.0;
852
853         gc.cr = gdk_cairo_create(widget->window);
854         g_object_set(widget, "has-tooltip", TRUE, NULL);
855         g_signal_connect(widget, "query-tooltip", G_CALLBACK(profile_tooltip), &drawing_area);
856         init_profile_background(&gc);
857         cairo_paint(gc.cr);
858
859         if (dive) {
860                 if (tooltip_rects) {
861                         free(tooltip_rects);
862                         tooltip_rects = NULL;
863                 }
864                 tooltips = 0;
865                 plot(&gc, &drawing_area, dive);
866         }
867         cairo_destroy(gc.cr);
868
869         return FALSE;
870 }
871
872 GtkWidget *dive_profile_widget(void)
873 {
874         GtkWidget *da;
875
876         da = gtk_drawing_area_new();
877         gtk_widget_set_size_request(da, 350, 250);
878         g_signal_connect(da, "expose_event", G_CALLBACK(expose_event), NULL);
879
880         return da;
881 }
882
883 int process_ui_events(void)
884 {
885         int ret=0;
886
887         while (gtk_events_pending()) {
888                 if (gtk_main_iteration_do(0)) {
889                         ret = 1;
890                         break;
891                 }
892         }
893         return ret;
894 }
895
896 static int fill_computer_list(GtkListStore *store)
897 {
898         int index = -1, i;
899         GtkTreeIter iter;
900         struct device_list *list = device_list;
901
902         for (list = device_list, i = 0 ; list->name ; list++, i++) {
903                 gtk_list_store_append(store, &iter);
904                 gtk_list_store_set(store, &iter,
905                         0, list->name,
906                         1, list->type,
907                         -1);
908                 if (is_default_dive_computer(list->name))
909                         index = i;
910         }
911         return index;
912 }
913
914 static GtkComboBox *dive_computer_selector(GtkWidget *vbox)
915 {
916         GtkWidget *hbox, *combo_box, *frame;
917         GtkListStore *model;
918         GtkCellRenderer *renderer;
919         int default_index;
920
921         hbox = gtk_hbox_new(FALSE, 6);
922         gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 3);
923
924         model = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_INT);
925         default_index = fill_computer_list(model);
926
927         frame = gtk_frame_new("Dive computer");
928         gtk_box_pack_start(GTK_BOX(hbox), frame, FALSE, TRUE, 3);
929
930         combo_box = gtk_combo_box_new_with_model(GTK_TREE_MODEL(model));
931         gtk_container_add(GTK_CONTAINER(frame), combo_box);
932
933         renderer = gtk_cell_renderer_text_new();
934         gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo_box), renderer, TRUE);
935         gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo_box), renderer, "text", 0, NULL);
936
937         gtk_combo_box_set_active(GTK_COMBO_BOX(combo_box), default_index);
938
939         return GTK_COMBO_BOX(combo_box);
940 }
941
942 static GtkEntry *dive_computer_device(GtkWidget *vbox)
943 {
944         GtkWidget *hbox, *entry, *frame;
945
946         hbox = gtk_hbox_new(FALSE, 6);
947         gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 3);
948
949         frame = gtk_frame_new("Device name");
950         gtk_box_pack_start(GTK_BOX(hbox), frame, FALSE, TRUE, 3);
951
952         entry = gtk_entry_new();
953         gtk_container_add(GTK_CONTAINER(frame), entry);
954         gtk_entry_set_text(GTK_ENTRY(entry), subsurface_USB_name());
955
956         return GTK_ENTRY(entry);
957 }
958
959 /* once a file is selected in the FileChooserButton we want to exit the import dialog */
960 static void on_file_set(GtkFileChooserButton *widget, gpointer _data)
961 {
962         GtkDialog *main_dialog = _data;
963
964         gtk_dialog_response(main_dialog, GTK_RESPONSE_ACCEPT);
965 }
966
967 static GtkWidget *xml_file_selector(GtkWidget *vbox, GtkWidget *main_dialog)
968 {
969         GtkWidget *hbox, *frame, *chooser, *dialog;
970         GtkFileFilter *filter;
971
972         hbox = gtk_hbox_new(FALSE, 6);
973         gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 3);
974
975         frame = gtk_frame_new("XML file name");
976         gtk_box_pack_start(GTK_BOX(hbox), frame, FALSE, TRUE, 3);
977         dialog = gtk_file_chooser_dialog_new("Open XML File",
978                 GTK_WINDOW(main_window),
979                 GTK_FILE_CHOOSER_ACTION_OPEN,
980                 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
981                 GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
982                 NULL);
983         gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), FALSE);
984
985         filter = gtk_file_filter_new();
986         gtk_file_filter_add_pattern(filter, "*.xml");
987         gtk_file_filter_add_pattern(filter, "*.XML");
988         gtk_file_filter_add_pattern(filter, "*.sda");
989         gtk_file_filter_add_pattern(filter, "*.SDA");
990         gtk_file_filter_add_mime_type(filter, "text/xml");
991         gtk_file_filter_set_name(filter, "XML file");
992         gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(dialog), filter);
993
994         chooser = gtk_file_chooser_button_new_with_dialog(dialog);
995         g_signal_connect(G_OBJECT(chooser), "file-set", G_CALLBACK(on_file_set), main_dialog);
996
997         gtk_file_chooser_button_set_width_chars(GTK_FILE_CHOOSER_BUTTON(chooser), 30);
998         gtk_container_add(GTK_CONTAINER(frame), chooser);
999
1000         return chooser;
1001 }
1002
1003 static void do_import_file(gpointer data, gpointer user_data)
1004 {
1005         GError *error = NULL;
1006         parse_file(data, &error);
1007
1008         if (error != NULL)
1009         {
1010                 report_error(error);
1011                 g_error_free(error);
1012                 error = NULL;
1013         }
1014 }
1015
1016 static GtkWidget *import_dive_computer(device_data_t *data, GtkBox *vbox)
1017 {
1018         GError *error;
1019         GtkWidget *info, *container, *label;
1020
1021         error = do_import(data);
1022         if (!error)
1023                 return NULL;
1024
1025         info = gtk_info_bar_new();
1026         container = gtk_info_bar_get_content_area(GTK_INFO_BAR(info));
1027         label = gtk_label_new(error->message);
1028         gtk_container_add(GTK_CONTAINER(container), label);
1029         gtk_box_pack_start(vbox, info, FALSE, FALSE, 0);
1030         return info;
1031 }
1032
1033 void import_dialog(GtkWidget *w, gpointer data)
1034 {
1035         int result;
1036         GtkWidget *dialog, *hbox, *vbox, *label, *info = NULL;
1037         GtkComboBox *computer;
1038         GtkEntry *device;
1039         GtkWidget *XMLchooser;
1040         device_data_t devicedata = {
1041                 .devname = NULL,
1042         };
1043
1044         dialog = gtk_dialog_new_with_buttons("Import from dive computer",
1045                 GTK_WINDOW(main_window),
1046                 GTK_DIALOG_DESTROY_WITH_PARENT,
1047                 GTK_STOCK_OK, GTK_RESPONSE_ACCEPT,
1048                 GTK_STOCK_CANCEL, GTK_RESPONSE_REJECT,
1049                 NULL);
1050
1051         vbox = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
1052         label = gtk_label_new("Import: \nLoad XML file or import directly from dive computer");
1053         gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, TRUE, 3);
1054         XMLchooser = xml_file_selector(vbox, dialog);
1055         computer = dive_computer_selector(vbox);
1056         device = dive_computer_device(vbox);
1057         hbox = gtk_hbox_new(FALSE, 6);
1058         gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, TRUE, 3);
1059         devicedata.progress.bar = gtk_progress_bar_new();
1060         gtk_container_add(GTK_CONTAINER(hbox), devicedata.progress.bar);
1061
1062 repeat:
1063         gtk_widget_show_all(dialog);
1064         result = gtk_dialog_run(GTK_DIALOG(dialog));
1065         switch (result) {
1066                 int type;
1067                 GtkTreeIter iter;
1068                 GtkTreeModel *model;
1069                 const char *comp;
1070                 GSList *list;
1071         case GTK_RESPONSE_ACCEPT:
1072                 /* what happened - did the user pick a file? In that case
1073                  * we ignore whether a dive computer model was picked */
1074                 if (info)
1075                         gtk_widget_destroy(info);
1076                 list = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(XMLchooser));
1077                 if (g_slist_length(list) == 0) {
1078                         if (!gtk_combo_box_get_active_iter(computer, &iter))
1079                                 break;
1080                         model = gtk_combo_box_get_model(computer);
1081                         gtk_tree_model_get(model, &iter,
1082                                         0, &comp,
1083                                         1, &type,
1084                                         -1);
1085                         devicedata.type = type;
1086                         devicedata.name = comp;
1087                         devicedata.devname = gtk_entry_get_text(device);
1088                         set_default_dive_computer(devicedata.name);
1089                         info = import_dive_computer(&devicedata, GTK_BOX(vbox));
1090                         if (info)
1091                                 goto repeat;
1092                 } else {
1093                         g_slist_foreach(list,do_import_file,NULL);
1094                         g_slist_free(list);
1095                 }
1096                 break;
1097         default:
1098                 break;
1099         }
1100         gtk_widget_destroy(dialog);
1101
1102         report_dives(TRUE);
1103 }
1104
1105 void update_progressbar(progressbar_t *progress, double value)
1106 {
1107         gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(progress->bar), value);
1108 }
1109
1110
1111 void set_filename(const char *filename)
1112 {
1113         if (!existing_filename && filename)
1114                 existing_filename = strdup(filename);
1115         return;
1116 }