diff options
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | display.h | 8 | ||||
-rw-r--r-- | dive.h | 9 | ||||
-rw-r--r-- | divelist.c | 248 | ||||
-rw-r--r-- | equipment.c | 68 | ||||
-rw-r--r-- | gtk-gui.c | 16 | ||||
-rw-r--r-- | info.c | 28 | ||||
-rw-r--r-- | packaging/windows/subsurface.nsi | 265 | ||||
-rw-r--r-- | print.c | 16 | ||||
-rw-r--r-- | profile.c | 38 | ||||
-rw-r--r-- | save-xml.c | 2 | ||||
-rw-r--r-- | statistics.c | 301 |
12 files changed, 827 insertions, 174 deletions
@@ -209,4 +209,4 @@ doc: $(MAKE) -C Documentation doc clean: - rm -f $(OBJS) *~ $(NAME) + rm -f $(OBJS) *~ $(NAME) $(NAME).exe @@ -3,6 +3,10 @@ #include <cairo.h> +#define DPI_SCREEN 72.0 +#define SCALE_SCREEN 1.0 +#define SCALE_PRINT (1.0 / DPI_SCREEN) + extern void repaint_dive(void); extern void do_print(void); @@ -22,7 +26,9 @@ struct graphics_context { double topy, bottomy; }; -extern void plot(struct graphics_context *gc, cairo_rectangle_int_t *drawing_area, struct dive *dive); +typedef enum { SC_SCREEN, SC_PRINT } scale_mode_t; + +extern void plot(struct graphics_context *gc, cairo_rectangle_t *drawing_area, struct dive *dive, scale_mode_t scale); extern void init_profile_background(struct graphics_context *gc); extern void attach_tooltip(int x, int y, int w, int h, const char *text); @@ -263,6 +263,10 @@ struct dive { struct sample sample[]; }; +/* this is a global spot for a temporary dive structure that we use to + * be able to edit a dive without unintended side effects */ +extern struct dive edit_dive; + extern GList *dive_trip_list; extern gboolean autogroup; /* random threashold: three days without diving -> new trip @@ -272,6 +276,7 @@ extern gboolean autogroup; #define UNGROUPED_DIVE(_dive) ((_dive)->tripflag == NO_TRIP) #define DIVE_IN_TRIP(_dive) ((_dive)->tripflag == IN_TRIP) +#define DIVE_NEEDS_TRIP(_dive) ((_dive)->tripflag == TF_NONE) #define NEXT_TRIP(_entry) ((_entry) ? g_list_next(_entry) : (dive_trip_list)) #define PREV_TRIP(_entry) ((_entry) ? g_list_previous(_entry) : g_list_last(dive_trip_list)) #define DIVE_TRIP(_trip) ((struct dive *)(_trip)->data) @@ -396,6 +401,8 @@ extern void clear_equipment_widgets(void); extern void show_dive_stats(struct dive *); extern void clear_stats_widgets(void); +extern void show_yearly_stats(void); + extern void update_dive(struct dive *new_dive); extern void save_dives(const char *filename); @@ -467,4 +474,6 @@ extern const char *existing_filename; extern const char *subsurface_default_filename(void); #define AIR_PERMILLE 209 +#define FRACTION(n,x) ((unsigned)(n)/(x)),((unsigned)(n)%(x)) + #endif /* DIVE_H */ diff --git a/divelist.c b/divelist.c index c52a94191..aedcdf03e 100644 --- a/divelist.c +++ b/divelist.c @@ -65,6 +65,9 @@ enum { DIVELIST_COLUMNS }; +static void turn_dive_into_trip(GtkTreePath *path); +static void merge_dive_into_trip_above_cb(GtkWidget *menuitem, GtkTreePath *path); + #ifdef DEBUG_MODEL static gboolean dump_model_entry(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) @@ -136,6 +139,39 @@ static void first_leaf(GtkTreeModel *model, GtkTreeIter *iter, int *diveidx) } } +static GtkTreePath *path_match; + +static gboolean match_dive(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) +{ + int idx; + struct dive *dive = data; + + gtk_tree_model_get(model, iter, DIVE_INDEX, &idx, -1); + if (idx >= 0) + if (get_dive(idx)->when == dive->when) { + path_match = gtk_tree_path_copy(path); + return TRUE; + } + return FALSE; +} + +static GtkTreePath *get_path_from(struct dive *dive) +{ + gtk_tree_model_foreach(TREEMODEL(dive_list), match_dive, dive); + return path_match; +} + +static struct dive *dive_from_path(GtkTreePath *path) +{ + GtkTreeIter iter; + int idx; + + gtk_tree_model_get_iter(MODEL(dive_list), &iter, path); + gtk_tree_model_get(MODEL(dive_list), &iter, DIVE_INDEX, &idx, -1); + return get_dive(idx); + +} + /* make sure that if we expand a summary row that is selected, the children show up as selected, too */ void row_expanded_cb(GtkTreeView *tree_view, GtkTreeIter *iter, GtkTreePath *path, gpointer data) @@ -927,6 +963,49 @@ static struct dive *create_and_hookup_trip_from_dive(struct dive *dive) return dive_trip; } +/* check that a dive should be in a trip starting at 'when' + * first the regular check (dive is before the trip start, but within the + * threshold) + * then for dives that are after the trip start we walk back to the dive + * that starts at when and check on the way that there is no ungrouped + * dive and no break beyond the 3 day threshold between dives that + * haven't already been assigned to this trip */ +static gboolean dive_can_be_in_trip(int idx, struct dive *dive_trip) +{ + struct dive *dive, *pdive; + int i = idx; + time_t when = dive_trip->when; + + dive = get_dive(idx); + /* if the dive is before the trip start but within the threshold + * then just accept it, otherwise reject it */ + if (dive->when < when) { + if (DIVE_FITS_TRIP(dive, dive_trip)) + return TRUE; + else + return FALSE; + } + + while (--i >= 0) { + pdive = get_dive(i); + /* an ungrouped dive cannot be in the middle of a trip + * also, if there are two consecutive dives that are too far apart + * that aren't both already labeled as 'in trip' (which shows that + * this was manually done so we shouldn't override that) */ + if ( UNGROUPED_DIVE(pdive) || + (! (DIVE_IN_TRIP(pdive) && DIVE_IN_TRIP(dive)) && + dive->when - pdive->when > TRIP_THRESHOLD)) { + return FALSE; + } + if (pdive->when == when) + /* done - we have reached the first dive in the trip */ + return TRUE; + dive = pdive; + } + /* we should never get here */ + return TRUE; +} + static void fill_dive_list(void) { int i; @@ -965,17 +1044,27 @@ static void fill_dive_list(void) /* first dives that go to the top level */ parent_ptr = NULL; dive_trip = NULL; - } else if (autogroup && !DIVE_IN_TRIP(dive)) { - if ( ! dive_trip || ! DIVE_FITS_TRIP(dive, dive_trip)) { - /* allocate new trip - all fields default to 0 - and get filled in further down */ - dive_trip = create_and_hookup_trip_from_dive(dive); - dive_trip->tripflag = IN_TRIP; /* this marks an autogen trip */ - trip = FIND_TRIP(dive_trip->when); + } else if (autogroup && DIVE_NEEDS_TRIP(dive)){ + /* if we already have existing trips there are up to two trips that this + * could potentially be part of. Let's try the one we are on, first */ + if (! (dive_trip && dive_can_be_in_trip(i, dive_trip))) { + /* there could be a better trip in our list already */ + trip = find_matching_trip(dive->when); + if (! (trip && dive_can_be_in_trip(i, DIVE_TRIP(trip)))) { + /* seems like neither of these trips work, so create + * a new one; all fields default to 0 and get filled + * in further down */ + parent_ptr = NULL; + dive_trip = create_and_hookup_trip_from_dive(dive); + dive_trip->tripflag = IN_TRIP; + trip = FIND_TRIP(dive_trip->when); + } + if (trip) + dive_trip = DIVE_TRIP(trip); } } else if (DIVE_IN_TRIP(dive)) { - trip = find_matching_trip(dive->when); - dive_trip = DIVE_TRIP(trip); + trip = find_matching_trip(dive->when); + dive_trip = DIVE_TRIP(trip); } else { /* dive is not in a trip and we aren't autogrouping */ dive_trip = NULL; @@ -1188,13 +1277,7 @@ void edit_selected_dives_cb(GtkWidget *menuitem, gpointer data) void edit_dive_from_path_cb(GtkWidget *menuitem, GtkTreePath *path) { - GtkTreeIter iter; - int idx; - struct dive *dive; - - gtk_tree_model_get_iter(MODEL(dive_list), &iter, path); - gtk_tree_model_get(MODEL(dive_list), &iter, DIVE_INDEX, &idx, -1); - dive = get_dive(idx); + struct dive *dive = dive_from_path(path); edit_multi_dive_info(dive); } @@ -1310,6 +1393,48 @@ static GtkTreeIter *move_dive_between_trips(GtkTreeIter *dive_iter, GtkTreeIter return new_iter; } +/* this gets called when we are on a top level dive and we know that the previous + * top level node is a trip; if multiple consecutive dives are selected, they are + * all merged into the previous trip*/ +static void merge_dive_into_trip_above_cb(GtkWidget *menuitem, GtkTreePath *path) +{ + int idx; + GtkTreeIter dive_iter, trip_iter, prev_iter; + GtkTreePath *trip_path; + struct dive *dive, *prev_dive; + + /* get the path and iter for the trip and the last dive of that trip */ + trip_path = gtk_tree_path_copy(path); + (void)gtk_tree_path_prev(trip_path); + gtk_tree_model_get_iter(MODEL(dive_list), &trip_iter, trip_path); + gtk_tree_model_get_iter(MODEL(dive_list), &dive_iter, path); + gtk_tree_model_iter_nth_child(MODEL(dive_list), &prev_iter, &trip_iter, + gtk_tree_model_iter_n_children(MODEL(dive_list), &trip_iter) - 1); + gtk_tree_model_get(MODEL(dive_list), &dive_iter, DIVE_INDEX, &idx, -1); + dive = get_dive(idx); + gtk_tree_model_get(MODEL(dive_list), &prev_iter, DIVE_INDEX, &idx, -1); + prev_dive = get_dive(idx); + /* add the dive to the trip */ + for (;;) { + dive->divetrip = prev_dive->divetrip; + dive->tripflag = IN_TRIP; + (void)move_dive_between_trips(&dive_iter, NULL, &trip_iter, NULL, TRUE); + prev_dive = dive; + /* by merging the dive into the trip above the path now points to the next + top level entry. If that iter exists, it's also a dive and both this dive + and that next dive are selected, continue merging dives into the trip */ + if (!gtk_tree_model_get_iter(MODEL(dive_list), &dive_iter, path)) + break; + gtk_tree_model_get(MODEL(dive_list), &dive_iter, DIVE_INDEX, &idx, -1); + if (idx < 0) + break; + dive = get_dive(idx); + if (!dive->selected || !prev_dive->selected) + break; + } + mark_divelist_changed(TRUE); +} + static void turn_dive_into_trip(GtkTreePath *path) { GtkTreeIter iter, *newiter, newparent; @@ -1390,10 +1515,25 @@ static void insert_trip_before(GtkTreePath *path) static void insert_trip_before_cb(GtkWidget *menuitem, GtkTreePath *path) { /* is this splitting a trip or turning a dive into a trip? */ - if (gtk_tree_path_get_depth(path) == 2) + if (gtk_tree_path_get_depth(path) == 2) { insert_trip_before(path); - else + } else { /* this is a top level dive */ + struct dive *dive, *next_dive; + GtkTreePath *next_path; + + dive = dive_from_path(path); turn_dive_into_trip(path); + /* if the dive was selected and the next dive was selected, too, + * then all of them should be part of the new trip */ + if (dive->selected) { + next_path = gtk_tree_path_copy(path); + gtk_tree_path_next(next_path); + next_dive = dive_from_path(next_path); + if (next_dive && next_dive->selected) + merge_dive_into_trip_above_cb(menuitem, next_path); + } + } + mark_divelist_changed(TRUE); } static void remove_from_trip(GtkTreePath *path) @@ -1492,6 +1632,7 @@ static void remove_from_trip_cb(GtkWidget *menuitem, GtkTreePath *path) /* just remove the dive the mouse pointer is on */ remove_from_trip(path); } + mark_divelist_changed(TRUE); } void remove_trip(GtkTreePath *trippath, gboolean force_no_trip) @@ -1540,6 +1681,7 @@ void remove_trip(GtkTreePath *trippath, gboolean force_no_trip) void remove_trip_cb(GtkWidget *menuitem, GtkTreePath *trippath) { remove_trip(trippath, TRUE); + mark_divelist_changed(TRUE); } void merge_trips_cb(GtkWidget *menuitem, GtkTreePath *trippath) @@ -1570,6 +1712,55 @@ void merge_trips_cb(GtkWidget *menuitem, GtkTreePath *trippath) free(DIVE_TRIP(trip)); delete_trip(trip); gtk_tree_store_remove(STORE(dive_list), &thistripiter); + mark_divelist_changed(TRUE); +} + +/* this gets called with path pointing to a dive, either in the top level + * or as part of a trip */ +static void delete_dive_cb(GtkWidget *menuitem, GtkTreePath *path) +{ + GtkTreeIter iter; + int idx, i; + struct dive *dive, *pdive, *ndive; + GtkTreeView *tree_view = GTK_TREE_VIEW(dive_list.tree_view); + GtkTreeSelection *selection = gtk_tree_view_get_selection(tree_view); + + gtk_tree_model_get_iter(MODEL(dive_list), &iter, path); + gtk_tree_model_get(MODEL(dive_list), &iter, DIVE_INDEX, &idx, -1); + dive = get_dive(idx); + if (dive->divetrip) { + /* we could be displaying the list model, in which case we can't find out + * if this is part of a trip and the only dive in that trip from the model, + * so let's do this the manual way */ + pdive = get_dive(idx - 1); + ndive = get_dive(idx + 1); + if (! (pdive && pdive->divetrip == dive->divetrip) && + ! (ndive && ndive->divetrip == dive->divetrip)) { + /* if this is the only dive in the trip, remove the trip - the + * dive list update below will deal with making sure the treemodel + * is correct */ + GList *trip = find_matching_trip(dive->when); + delete_trip(trip); + free(dive->divetrip); + } + } + /* simply remove the dive and recreate the divelist + * (we can't just manipulate the tree_view as the indices for dives change) */ + for (i = idx; i < dive_table.nr - 1; i++) + dive_table.dives[i] = dive_table.dives[i+1]; + dive_table.nr--; + free(dive); + dive_list_update_dives(); + /* now make sure the same dives stay selected and if necessary their trips are expanded */ + for_each_dive(i, dive) { + if (dive->selected) { + GtkTreePath *path = get_path_from(dive); + if (MODEL(dive_list) == TREEMODEL(dive_list)) + gtk_tree_view_expand_to_path(tree_view, path); + gtk_tree_selection_select_path(selection, path); + } + } + mark_divelist_changed(TRUE); } static void popup_divelist_menu(GtkTreeView *tree_view, GtkTreeModel *model, int button, GdkEventButton *event) @@ -1622,6 +1813,9 @@ static void popup_divelist_menu(GtkTreeView *tree_view, GtkTreeModel *model, int g_signal_connect(menuitem, "activate", G_CALLBACK(remove_trip_cb), path); gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem); } else { + menuitem = gtk_menu_item_new_with_label("Delete Dive"); + g_signal_connect(menuitem, "activate", G_CALLBACK(delete_dive_cb), path); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem); dive = get_dive(idx); /* if we right click on selected dive(s), edit those */ if (dive->selected) { @@ -1638,14 +1832,24 @@ static void popup_divelist_menu(GtkTreeView *tree_view, GtkTreeModel *model, int } /* only offer trip editing options when we are displaying the tree model */ if (dive_list.model == dive_list.treemodel) { - int depth; - int *indices = gtk_tree_path_get_indices_with_depth(path, &depth); - + int depth = gtk_tree_path_get_depth(path); + int *indices = gtk_tree_path_get_indices(path); + /* top level dive or child dive that is not the first child */ if (depth == 1 || indices[1] > 0) { - menuitem = gtk_menu_item_new_with_label("Add new trip above"); + menuitem = gtk_menu_item_new_with_label("Create new trip above"); g_signal_connect(menuitem, "activate", G_CALLBACK(insert_trip_before_cb), path); gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem); } + prevpath = gtk_tree_path_copy(path); + /* top level dive with a trip right before it */ + if (depth == 1 && + gtk_tree_path_prev(prevpath) && + gtk_tree_model_get_iter(MODEL(dive_list), &previter, prevpath) && + gtk_tree_model_iter_n_children(model, &previter)) { + menuitem = gtk_menu_item_new_with_label("Add to trip above"); + g_signal_connect(menuitem, "activate", G_CALLBACK(merge_dive_into_trip_above_cb), path); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem); + } if (DIVE_IN_TRIP(dive)) { if (dive->selected && amount_selected > 1) menuitem = gtk_menu_item_new_with_label("Remove selected dives from trip"); diff --git a/equipment.c b/equipment.c index e81302c92..adfb02989 100644 --- a/equipment.c +++ b/equipment.c @@ -46,6 +46,7 @@ struct equipment_list { static struct equipment_list cylinder_list[2], weightsystem_list[2]; +struct dive edit_dive; struct cylinder_widget { int index, changed; @@ -55,6 +56,7 @@ struct cylinder_widget { GtkSpinButton *size, *pressure; GtkWidget *start, *end, *pressure_button; GtkWidget *o2, *he, *gasmix_button; + int w_idx; }; struct ws_widget { @@ -63,6 +65,7 @@ struct ws_widget { GtkWidget *hbox; GtkComboBox *description; GtkSpinButton *weight; + int w_idx; }; /* we want bar - so let's not use our unit functions */ @@ -206,7 +209,14 @@ static void cylinder_cb(GtkComboBox *combo_box, gpointer data) GtkTreeModel *model = gtk_combo_box_get_model(combo_box); int ml, mbar; struct cylinder_widget *cylinder = data; - cylinder_t *cyl = current_dive->cylinder + cylinder->index; + struct dive *dive; + cylinder_t *cyl; + + if (cylinder->w_idx == W_IDX_PRIMARY) + dive = current_dive; + else + dive = &edit_dive; + cyl = dive->cylinder + cylinder->index; /* Did the user set it to some non-standard value? */ if (!get_active_item(combo_box, &iter, cylinder_model)) { @@ -244,7 +254,13 @@ static void weight_cb(GtkComboBox *combo_box, gpointer data) GtkTreeModel *model = gtk_combo_box_get_model(combo_box); int weight; struct ws_widget *ws_widget = data; - weightsystem_t *ws = current_dive->weightsystem + ws_widget->index; + struct dive *dive; + weightsystem_t *ws; + if (ws_widget->w_idx == W_IDX_PRIMARY) + dive = current_dive; + else + dive = &edit_dive; + ws = dive->weightsystem + ws_widget->index; /* Did the user set it to some non-standard value? */ if (!get_active_item(combo_box, &iter, weightsystem_model)) { @@ -1047,7 +1063,7 @@ static void ws_widget(GtkWidget *vbox, struct ws_widget *ws_widget, GtkListStore ws_widget->weight = GTK_SPIN_BUTTON(widget); } -static int edit_cylinder_dialog(int index, cylinder_t *cyl) +static int edit_cylinder_dialog(int index, cylinder_t *cyl, int w_idx) { int success; GtkWidget *dialog, *vbox; @@ -1055,9 +1071,13 @@ static int edit_cylinder_dialog(int index, cylinder_t *cyl) struct dive *dive; cylinder.index = index; + cylinder.w_idx = w_idx; cylinder.changed = 0; - dive = current_dive; + if (w_idx == W_IDX_PRIMARY) + dive = current_dive; + else + dive = &edit_dive; if (!dive) return 0; *cyl = dive->cylinder[index]; @@ -1079,9 +1099,11 @@ static int edit_cylinder_dialog(int index, cylinder_t *cyl) if (success) { record_cylinder_changes(cyl, &cylinder); dive->cylinder[index] = *cyl; - mark_divelist_changed(TRUE); - update_cylinder_related_info(dive); - flush_divelist(dive); + if (w_idx == W_IDX_PRIMARY) { + mark_divelist_changed(TRUE); + update_cylinder_related_info(dive); + flush_divelist(dive); + } } gtk_widget_destroy(dialog); @@ -1089,7 +1111,7 @@ static int edit_cylinder_dialog(int index, cylinder_t *cyl) return success; } -static int edit_weightsystem_dialog(int index, weightsystem_t *ws) +static int edit_weightsystem_dialog(int index, weightsystem_t *ws, int w_idx) { int success; GtkWidget *dialog, *vbox; @@ -1097,9 +1119,13 @@ static int edit_weightsystem_dialog(int index, weightsystem_t *ws) struct dive *dive; weightsystem_widget.index = index; + weightsystem_widget.w_idx = w_idx; weightsystem_widget.changed = 0; - dive = current_dive; + if (w_idx == W_IDX_PRIMARY) + dive = current_dive; + else + dive = &edit_dive; if (!dive) return 0; *ws = dive->weightsystem[index]; @@ -1121,8 +1147,10 @@ static int edit_weightsystem_dialog(int index, weightsystem_t *ws) if (success) { record_weightsystem_changes(ws, &weightsystem_widget); dive->weightsystem[index] = *ws; - mark_divelist_changed(TRUE); - flush_divelist(dive); + if (w_idx == W_IDX_PRIMARY) { + mark_divelist_changed(TRUE); + flush_divelist(dive); + } } gtk_widget_destroy(dialog); @@ -1158,7 +1186,7 @@ static void edit_cb(GtkButton *button, int w_idx) return; index = get_model_index(model, &iter); - if (!edit_cylinder_dialog(index, &cyl)) + if (!edit_cylinder_dialog(index, &cyl, w_idx)) return; set_one_cylinder(&cyl, model, &iter); @@ -1174,7 +1202,7 @@ static void add_cb(GtkButton *button, int w_idx) GtkTreeSelection *selection; cylinder_t cyl; - if (!edit_cylinder_dialog(index, &cyl)) + if (!edit_cylinder_dialog(index, &cyl, w_idx)) return; gtk_list_store_append(model, &iter); @@ -1205,7 +1233,10 @@ static void del_cb(GtkButton *button, int w_idx) index = get_model_index(model, &iter); - dive = current_dive; + if (w_idx == W_IDX_PRIMARY) + dive = current_dive; + else + dive = &edit_dive; if (!dive) return; cyl = dive->cylinder + index; @@ -1241,7 +1272,7 @@ static void ws_edit_cb(GtkButton *button, int w_idx) return; index = get_model_index(model, &iter); - if (!edit_weightsystem_dialog(index, &ws)) + if (!edit_weightsystem_dialog(index, &ws, w_idx)) return; set_one_weightsystem(&ws, model, &iter); @@ -1257,7 +1288,7 @@ static void ws_add_cb(GtkButton *button, int w_idx) GtkTreeSelection *selection; weightsystem_t ws; - if (!edit_weightsystem_dialog(index, &ws)) + if (!edit_weightsystem_dialog(index, &ws, w_idx)) return; gtk_list_store_append(model, &iter); @@ -1288,7 +1319,10 @@ static void ws_del_cb(GtkButton *button, int w_idx) index = get_model_index(model, &iter); - dive = current_dive; + if (w_idx == W_IDX_PRIMARY) + dive = current_dive; + else + dive = &edit_dive; if (!dive) return; ws = dive->weightsystem + index; @@ -816,10 +816,10 @@ static void about_dialog(GtkWidget *w, gpointer data) gtk_show_about_dialog(NULL, "program-name", "SubSurface", - "comments", "Half-arsed divelog software in C", + "comments", "Multi-platform divelog software in C", "license", "GPLv2", "version", VERSION_STRING, - "copyright", "Linus Torvalds 2011", + "copyright", "Linus Torvalds, Dirk Hohndel, and others, 2011, 2012", "logo-icon-name", "subsurface", /* Must be last: */ logo_property, logo, @@ -878,6 +878,7 @@ static GtkActionEntry menu_items[] = { { "AddDive", GTK_STOCK_ADD, "Add Dive", NULL, NULL, G_CALLBACK(add_dive_cb) }, { "Preferences", GTK_STOCK_PREFERENCES, "Preferences", PREFERENCE_ACCEL, NULL, G_CALLBACK(preferences_dialog) }, { "Renumber", NULL, "Renumber", NULL, NULL, G_CALLBACK(renumber_dialog) }, + { "YearlyStats", NULL, "Yearly Statistics", NULL, NULL, G_CALLBACK(show_yearly_stats) }, { "SelectEvents", NULL, "SelectEvents", NULL, NULL, G_CALLBACK(selectevents_dialog) }, { "Quit", GTK_STOCK_QUIT, NULL, CTRLCHAR "Q", NULL, G_CALLBACK(quit) }, { "About", GTK_STOCK_ABOUT, NULL, NULL, NULL, G_CALLBACK(about_dialog) }, @@ -915,6 +916,7 @@ static const gchar* ui_string = " \ <menuitem name=\"Renumber\" action=\"Renumber\" /> \ <menuitem name=\"Autogroup\" action=\"Autogroup\" /> \ <menuitem name=\"Toggle Zoom\" action=\"ToggleZoom\" /> \ + <menuitem name=\"YearlyStats\" action=\"YearlyStats\" /> \ <menu name=\"View\" action=\"ViewMenuAction\"> \ <menuitem name=\"List\" action=\"ViewList\" /> \ <menuitem name=\"Profile\" action=\"ViewProfile\" /> \ @@ -1094,7 +1096,7 @@ void exit_ui(void) } typedef struct { - cairo_rectangle_int_t rect; + cairo_rectangle_t rect; const char *text; } tooltip_record_t; @@ -1103,7 +1105,7 @@ static int tooltips; void attach_tooltip(int x, int y, int w, int h, const char *text) { - cairo_rectangle_int_t *rect; + cairo_rectangle_t *rect; tooltip_rects = realloc(tooltip_rects, (tooltips + 1) * sizeof(tooltip_record_t)); rect = &tooltip_rects[tooltips].rect; rect->x = x; @@ -1121,7 +1123,7 @@ static gboolean profile_tooltip (GtkWidget *widget, gint x, gint y, gboolean keyboard_mode, GtkTooltip *tooltip, gpointer user_data) { int i; - cairo_rectangle_int_t *drawing_area = user_data; + cairo_rectangle_t *drawing_area = user_data; gint tx = x - drawing_area->x; /* get transformed coordinates */ gint ty = y - drawing_area->y; @@ -1139,7 +1141,7 @@ static gboolean expose_event(GtkWidget *widget, GdkEventExpose *event, gpointer { struct dive *dive = current_dive; struct graphics_context gc = { .printer = 0 }; - static cairo_rectangle_int_t drawing_area; + static cairo_rectangle_t drawing_area; /* the drawing area gives TOTAL width * height - x,y is used as the topx/topy offset * so effective drawing area is width-2x * height-2y */ @@ -1160,7 +1162,7 @@ static gboolean expose_event(GtkWidget *widget, GdkEventExpose *event, gpointer tooltip_rects = NULL; } tooltips = 0; - plot(&gc, &drawing_area, dive); + plot(&gc, &drawing_area, dive, SC_SCREEN); } cairo_destroy(gc.cr); @@ -504,7 +504,7 @@ static weightsystem_t remember_ws[MAX_WEIGHTSYSTEMS]; #define CYL_BYTES sizeof(cylinder_t) * MAX_CYLINDERS #define WS_BYTES sizeof(weightsystem_t) * MAX_WEIGHTSYSTEMS -void save_equipment_data(struct dive *dive) +static void save_equipment_data(struct dive *dive) { if (dive) { memcpy(remember_cyl, dive->cylinder, CYL_BYTES); @@ -606,13 +606,19 @@ int edit_multi_dive_info(struct dive *single_dive) } } } - - dive_info_widget(vbox, master, &info, multi); - show_dive_equipment(master, W_IDX_SECONDARY); - save_equipment_data(master); + /* edit a temporary copy of the master dive; + * edit_dive is a global dive structure that is modified by the + * cylinder / weightsystem dialogs if we open W_IDX_SECONDARY + * edit widgets as we do here */ + memcpy(&edit_dive, master, sizeof(struct dive)); + + dive_info_widget(vbox, &edit_dive, &info, multi); + show_dive_equipment(&edit_dive, W_IDX_SECONDARY); + save_equipment_data(&edit_dive); gtk_widget_show_all(dialog); success = gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT; if (success) { + mark_divelist_changed(TRUE); /* Update the non-current selected dives first */ if (!single_dive) { int i; @@ -622,19 +628,23 @@ int edit_multi_dive_info(struct dive *single_dive) if (dive == master || !dive->selected) continue; /* copy all "info" fields */ - save_dive_info_changes(dive, master, &info); + save_dive_info_changes(dive, &edit_dive, &info); /* copy the cylinders / weightsystems */ - update_equipment_data(dive, master); + update_equipment_data(dive, &edit_dive); /* this is extremely inefficient... it loops through all dives to find the right one - but we KNOW the index already */ + update_cylinder_related_info(dive); flush_divelist(dive); } } /* Update the master dive last! */ - save_dive_info_changes(master, master, &info); - update_equipment_data(master, master); + save_dive_info_changes(master, &edit_dive, &info); + update_equipment_data(master, &edit_dive); + update_cylinder_related_info(master); flush_divelist(master); + process_selected_dives(); + update_dive(master); } gtk_widget_destroy(dialog); diff --git a/packaging/windows/subsurface.nsi b/packaging/windows/subsurface.nsi index 280991f69..1828c118a 100644 --- a/packaging/windows/subsurface.nsi +++ b/packaging/windows/subsurface.nsi @@ -1,105 +1,174 @@ -# this installer creator needs to be run with +# +# Subsurface NSIS installer script +# +# This installer creator needs to be run with: # makensis subsurface.nsi # -# it assumes that packaging/windows/dll is a symlink to +# It assumes that packaging/windows/dll is a symlink to # the directory in which the required Windows DLLs are installed # (in my case that's /usr/i686-w64-mingw32/sys-root/mingw/bin) # -# define the name of the installer -outfile "subsurface-installer.exe" -Name subsurface - -# some data for the package to identify itself -VIProductVersion "1.1.9.0" -VIAddVersionKey ProductName subsurface -VIAddVersionKey FileDescription "subsurface diving log program" -VIAddVersionKey LegalCopyright "GPL v.2" -VIAddVersionKey ProductVersion "1.1" -VIAddVersionKey FileVersion "1.1" - -# icon to use for the installer -Icon .\subsurface.ico - -# the installer needs to be run with admin privileges -RequestExecutionLevel admin - -# pop up a little dialog that tells the user that we're about to -# install subsurface -Function .onInit - MessageBox MB_YESNO "This will install subsurface. Do you wish to continue?" IDYES gogogo - Abort - gogogo: -FunctionEnd - -# define the directory to install to, the desktop in this case as specified -# by the predefined $DESKTOP variable -installDir "$PROGRAMFILES\subsurface" - -# default section + +#-------------------------------- +# Include Modern UI + + !include "MUI2.nsh" + +#-------------------------------- +# General + + # Program version + !define SUBSURFACE_VERSION "1.2" + + # VIProductVersion requires version in x.x.x.x format + !define SUBSURFACE_VIPRODUCTVERSION "1.2.0.0" + + # Installer name and filename + Name "Subsurface" + Caption "Subsurface ${SUBSURFACE_VERSION} Setup" + OutFile "subsurface-${SUBSURFACE_VERSION}.exe" + + # Icon to use for the installer + !define MUI_ICON "subsurface.ico" + + # Default installation folder + InstallDir "$PROGRAMFILES\Subsurface" + + # Get installation folder from registry if available + InstallDirRegKey HKCU "Software\Subsurface" "" + + # Request application privileges + RequestExecutionLevel user + +#-------------------------------- +# Version information + + VIProductVersion "${SUBSURFACE_VIPRODUCTVERSION}" + VIAddVersionKey "ProductName" "Subsurface" + VIAddVersionKey "FileDescription" "Subsurface - an open source dive log program." + VIAddVersionKey "FileVersion" "${SUBSURFACE_VERSION}" + VIAddVersionKey "LegalCopyright" "GPL v.2" + VIAddVersionKey "ProductVersion" "${SUBSURFACE_VERSION}" + +#-------------------------------- +# Settings + + # Show a warn on aborting installation + !define MUI_ABORTWARNING + + # Defines the target start menu folder + !define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKCU" + !define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\Subsurface" + !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder" + +#-------------------------------- +# Variables + + Var StartMenuFolder + +#-------------------------------- +# Pages + + # Installer pages + !insertmacro MUI_PAGE_LICENSE "..\..\gpl-2.0.txt" + !insertmacro MUI_PAGE_DIRECTORY + !insertmacro MUI_PAGE_STARTMENU Application $StartMenuFolder + !insertmacro MUI_PAGE_INSTFILES + + # Uninstaller pages + !insertmacro MUI_UNPAGE_CONFIRM + !insertmacro MUI_UNPAGE_INSTFILES + +#-------------------------------- +# Languages + + !insertmacro MUI_LANGUAGE "English" + +#-------------------------------- +# Default installer section + Section -# define the output path for this file -setOutPath $INSTDIR - -SetShellVarContext all - -# create directory in the Start menu -CreateDirectory "$SMPROGRAMS\subsurface" - -# create Start menu shortcut -createShortCut "$SMPROGRAMS\subsurface\subsurface.lnk" "$INSTDIR\subsurface.exe" - -#create uninstaller and corresponding shortcut in Start menu -writeUninstaller "$INSTDIR\subsurface-uninstall.exe" -createShortCut "$SMPROGRAMS\subsurface\uninstall-subsurface.lnk" "$INSTDIR\subsurface-uninstall.exe" - -# define what to install and place it in the output path -file /oname=subsurface.exe ../../subsurface.exe -file /oname=subsurface.ico subsurface.ico -file /oname=subsurface.svg ../../subsurface.svg -file /oname=libatk-1.0-0.dll dll/libatk-1.0-0.dll -file /oname=libcairo-2.dll dll/libcairo-2.dll -file /oname=libdivecomputer-0.dll dll\libdivecomputer-0.dll -file /oname=libffi-5.dll dll\libffi-5.dll -file /oname=libfontconfig-1.dll dll\libfontconfig-1.dll -file /oname=libfreetype-6.dll dll\libfreetype-6.dll -file /oname=libgdk_pixbuf-2.0-0.dll dll\libgdk_pixbuf-2.0-0.dll -file /oname=libgdk-win32-2.0-0.dll dll\libgdk-win32-2.0-0.dll -file /oname=libgio-2.0-0.dll dll\libgio-2.0-0.dll -file /oname=libglib-2.0-0.dll dll\libglib-2.0-0.dll -file /oname=libgmodule-2.0-0.dll dll\libgmodule-2.0-0.dll -file /oname=libgobject-2.0-0.dll dll\libgobject-2.0-0.dll -file /oname=libgthread-2.0-0.dll dll\libgthread-2.0-0.dll -file /oname=libgtk-win32-2.0-0.dll dll\libgtk-win32-2.0-0.dll -file /oname=libintl-8.dll dll\libintl-8.dll -file /oname=libjasper-1.dll dll\libjasper-1.dll -file /oname=libjpeg-8.dll dll\libjpeg-8.dll -file /oname=libpango-1.0-0.dll dll\libpango-1.0-0.dll -file /oname=libpangocairo-1.0-0.dll dll\libpangocairo-1.0-0.dll -file /oname=libpangoft2-1.0-0.dll dll\libpangoft2-1.0-0.dll -file /oname=libpangowin32-1.0-0.dll dll\libpangowin32-1.0-0.dll -file /oname=libpixman-1-0.dll dll\libpixman-1-0.dll -file /oname=libpng15-15.dll dll\libpng15-15.dll -file /oname=libtiff-5.dll dll\libtiff-5.dll -file /oname=libxml2-2.dll dll\libxml2-2.dll -file /oname=libxslt-1.dll dll\libxslt-1.dll -file /oname=pthreadGC2.dll dll\pthreadGC2.dll -file /oname=zlib1.dll dll\zlib1.dll -file /oname=libusb-1.0.dll dll\libusb-1.0.dll -file /oname=SuuntoSDM.xslt ../../xslt/SuuntoSDM.xslt -file /oname=jdivelog2subsurface.xslt ../../xslt/jdivelog2subsurface.xslt -sectionEnd - -section "uninstall" - SetShellVarContext all - delete "$INSTDIR\subsurface-uninstall.exe" - delete "$INSTDIR\*.*" - RMDir "$INSTDIR" - delete "$SMPROGRAMS\subsurface\uninstall-subsurface.lnk" - delete "$SMPROGRAMS\subsurface\subsurface.lnk" - RMDir "$SMPROGRAMS\subsurface" - - MessageBox MB_YESNO "Do you wish to keep subsurface's registry settings?" IDYES end - DeleteRegKey HKCU "SOFTWARE\subsurface" - end: -sectionEnd + # Installation path + SetOutPath "$INSTDIR" + + # Delete any already installed DLLs to avoid buildup of various + # versions of the same library when upgrading + Delete "$INSTDIR\*.dll" + + # Files to include in installer + file /oname=subsurface.exe ..\..\subsurface.exe + file /oname=subsurface.ico subsurface.ico + file /oname=subsurface.svg ..\..\subsurface.svg + file /oname=libatk-1.0-0.dll dll\libatk-1.0-0.dll + file /oname=libcairo-2.dll dll\libcairo-2.dll + file /oname=libdivecomputer-0.dll dll\libdivecomputer-0.dll + file /oname=libffi-6.dll dll\libffi-6.dll + file /oname=libfontconfig-1.dll dll\libfontconfig-1.dll + file /oname=libfreetype-6.dll dll\libfreetype-6.dll + file /oname=libgdk_pixbuf-2.0-0.dll dll\libgdk_pixbuf-2.0-0.dll + file /oname=libgdk-win32-2.0-0.dll dll\libgdk-win32-2.0-0.dll + file /oname=libgio-2.0-0.dll dll\libgio-2.0-0.dll + file /oname=libglib-2.0-0.dll dll\libglib-2.0-0.dll + file /oname=libgmodule-2.0-0.dll dll\libgmodule-2.0-0.dll + file /oname=libgobject-2.0-0.dll dll\libgobject-2.0-0.dll + file /oname=libgthread-2.0-0.dll dll\libgthread-2.0-0.dll + file /oname=libgtk-win32-2.0-0.dll dll\libgtk-win32-2.0-0.dll + file /oname=libintl-8.dll dll\libintl-8.dll + file /oname=libjasper-1.dll dll\libjasper-1.dll + file /oname=libjpeg-62.dll dll\libjpeg-62.dll + file /oname=libpango-1.0-0.dll dll\libpango-1.0-0.dll + file /oname=libpangocairo-1.0-0.dll dll\libpangocairo-1.0-0.dll + file /oname=libpangoft2-1.0-0.dll dll\libpangoft2-1.0-0.dll + file /oname=libpangowin32-1.0-0.dll dll\libpangowin32-1.0-0.dll + file /oname=libpixman-1-0.dll dll\libpixman-1-0.dll + file /oname=libpng15-15.dll dll\libpng15-15.dll + file /oname=libtiff-3.dll dll\libtiff-3.dll + file /oname=libxml2-2.dll dll\libxml2-2.dll + file /oname=libxslt-1.dll dll\libxslt-1.dll + file /oname=pthreadGC2.dll dll\pthreadGC2.dll + file /oname=zlib1.dll dll\zlib1.dll + file /oname=libusb-1.0.dll dll\libusb-1.0.dll + file /oname=SuuntoSDM.xslt ..\..\xslt\SuuntoSDM.xslt + file /oname=jdivelog2subsurface.xslt ..\..\xslt\jdivelog2subsurface.xslt + file /oname=iconv.dll dll\iconv.dll + + # Store installation folder in registry + WriteRegStr HKCU "Software\Subsurface" "" $INSTDIR + + # Create shortcuts + !insertmacro MUI_STARTMENU_WRITE_BEGIN Application + CreateDirectory "$SMPROGRAMS\$StartMenuFolder" + CreateShortCut "$SMPROGRAMS\$StartMenuFolder\Subsurface.lnk" "$INSTDIR\subsurface.exe" + CreateShortCut "$SMPROGRAMS\$StartMenuFolder\Uninstall Subsurface.lnk" "$INSTDIR\Uninstall.exe" + !insertmacro MUI_STARTMENU_WRITE_END + + # Create the uninstaller + WriteUninstaller "$INSTDIR\Uninstall.exe" + +SectionEnd + +#-------------------------------- +# Uninstaller section + +Section "Uninstall" + + # Delete installed files + Delete "$INSTDIR\*.dll" + Delete "$INSTDIR\*.xslt" + Delete "$INSTDIR\subsurface.exe" + Delete "$INSTDIR\subsurface.ico" + Delete "$INSTDIR\subsurface.svg" + Delete "$INSTDIR\Uninstall.exe" + RMDir "$INSTDIR" + + # Remove shortcuts + !insertmacro MUI_STARTMENU_GETFOLDER Application $StartMenuFolder + Delete "$SMPROGRAMS\$StartMenuFolder\Subsurface.lnk" + Delete "$SMPROGRAMS\$StartMenuFolder\Uninstall Subsurface.lnk" + RMDir "$SMPROGRAMS\$StartMenuFolder" + + # Remove registry entries + DeleteRegKey /ifempty HKCU "Software\Subsurface" + +SectionEnd @@ -34,7 +34,7 @@ static struct dive *get_dive_for_printing(int idx) static void set_font(PangoLayout *layout, PangoFontDescription *font, double size, int align) { - pango_font_description_set_size(font, size * PANGO_SCALE); + pango_font_description_set_size(font, size * PANGO_SCALE * SCALE_PRINT); pango_layout_set_font_description(layout, font); pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); pango_layout_set_alignment(layout, align); @@ -149,7 +149,8 @@ static void show_dive_text(struct dive *dive, cairo_t *cr, double w, static void show_table_header(cairo_t *cr, double w, double h, PangoFontDescription *font) { - int maxwidth, maxheight, colwidth, i, curwidth; + int i; + double maxwidth, maxheight, colwidth, curwidth; PangoLayout *layout; char headers[7][80]= { "Dive#", "Date", "Depth", "Time", "Master", "Buddy", "Location" }; @@ -190,7 +191,8 @@ static void show_dive_table(struct dive *dive, cairo_t *cr, double w, { double depth; const char *unit; - int len, decimals, maxwidth, maxheight, colwidth, curwidth; + int len, decimals; + double maxwidth, maxheight, colwidth, curwidth; PangoLayout *layout; struct tm *tm; char buffer[160], divenr[20]; @@ -245,7 +247,7 @@ static void show_dive_table(struct dive *dive, cairo_t *cr, double w, pango_layout_set_justify(layout, 1); pango_cairo_show_layout(cr, layout); curwidth = curwidth + (colwidth / 2); - + // Col 4: Time len = snprintf(buffer, sizeof(buffer), "%d min",(dive->duration.seconds+59) / 60); @@ -284,13 +286,13 @@ static void show_dive_table(struct dive *dive, cairo_t *cr, double w, static void show_dive_profile(struct dive *dive, cairo_t *cr, double w, double h) { - cairo_rectangle_int_t drawing_area = { w/20.0, h/20.0, w, h}; + cairo_rectangle_t drawing_area = { w/20.0, h/20.0, w, h}; struct graphics_context gc = { .printer = 1, .cr = cr }; cairo_save(cr); - plot(&gc, &drawing_area, dive); + plot(&gc, &drawing_area, dive, SC_PRINT); cairo_restore(cr); } @@ -533,7 +535,7 @@ void do_print(void) repaint_dive(); print = gtk_print_operation_new(); - gtk_print_operation_set_unit(print, GTK_UNIT_POINTS); + gtk_print_operation_set_unit(print, GTK_UNIT_INCH); if (settings != NULL) gtk_print_operation_set_print_settings(print, settings); g_signal_connect(print, "create-custom-widget", G_CALLBACK(print_dialog), NULL); @@ -16,6 +16,11 @@ int selected_dive = 0; char zoomed_plot = 0; +static double plot_scale = SCALE_SCREEN; + +#define cairo_set_line_width_scaled(cr, w) \ + cairo_set_line_width((cr), (w) * plot_scale); + typedef enum { STABLE, SLOW, MODERATE, FAST, CRAZY } velocity_t; /* Plot info with smoothing, velocity indication @@ -245,7 +250,7 @@ static void plot_text(struct graphics_context *gc, const text_render_options_t * vsnprintf(buffer, sizeof(buffer), fmt, args); va_end(args); - cairo_set_font_size(cr, tro->size); + cairo_set_font_size(cr, tro->size * plot_scale); cairo_font_extents(cr, &fe); cairo_text_extents(cr, buffer, &extents); dx = tro->hpos * extents.width + extents.x_bearing; @@ -485,7 +490,7 @@ static void plot_depth_profile(struct graphics_context *gc, struct plot_info *pi gc->leftx = 0; gc->rightx = maxtime; gc->topy = 0; gc->bottomy = 1.0; set_source_rgba(gc, TIME_GRID); - cairo_set_line_width(gc->cr, 2); + cairo_set_line_width_scaled(gc->cr, 2); for (i = incr; i < maxtime; i += incr) { move_to(gc, i, 0); @@ -542,13 +547,13 @@ static void plot_depth_profile(struct graphics_context *gc, struct plot_info *pi gc->topy = 0; gc->bottomy = maxdepth; cairo_pattern_t *pat; - pat = cairo_pattern_create_linear (0.0, 0.0, 0.0, 256.0); + pat = cairo_pattern_create_linear (0.0, 0.0, 0.0, 256.0 * plot_scale); pattern_add_color_stop_rgba (gc, pat, 1, DEPTH_BOTTOM); pattern_add_color_stop_rgba (gc, pat, 0, DEPTH_TOP); cairo_set_source(gc->cr, pat); cairo_pattern_destroy(pat); - cairo_set_line_width(gc->cr, 2); + cairo_set_line_width_scaled(gc->cr, 2); entry = pi->entry; move_to(gc, 0, 0); @@ -655,7 +660,7 @@ static void plot_temperature_profile(struct graphics_context *gc, struct plot_in if (!setup_temperature_limits(gc, pi)) return; - cairo_set_line_width(gc->cr, 2); + cairo_set_line_width_scaled(gc->cr, 2); set_source_rgba(gc, TEMP_PLOT); for (i = 0; i < pi->nr; i++) { struct plot_data *entry = pi->entry + i; @@ -730,7 +735,7 @@ static void plot_cylinder_pressure(struct graphics_context *gc, struct plot_info if (!get_cylinder_pressure_range(gc, pi)) return; - cairo_set_line_width(gc->cr, 2); + cairo_set_line_width_scaled(gc->cr, 2); for (i = 0; i < pi->nr; i++) { int mbar; @@ -1374,13 +1379,28 @@ static struct plot_info *create_plot_info(struct dive *dive, int nr_samples, str return analyze_plot_info(pi); } -void plot(struct graphics_context *gc, cairo_rectangle_int_t *drawing_area, struct dive *dive) +static void plot_set_scale(scale_mode_t scale) +{ + switch (scale) { + default: + case SC_SCREEN: + plot_scale = SCALE_SCREEN; + break; + case SC_PRINT: + plot_scale = SCALE_PRINT; + break; + } +} + +void plot(struct graphics_context *gc, cairo_rectangle_t *drawing_area, struct dive *dive, scale_mode_t scale) { struct plot_info *pi; static struct sample fake[4]; struct sample *sample = dive->sample; int nr = dive->samples; + plot_set_scale(scale); + if (!nr) { /* The dive has no samples, so create a few fake ones. This assumes an ascent/descent rate of 9 m/min, which is just below the limit for FAST. */ @@ -1401,7 +1421,7 @@ void plot(struct graphics_context *gc, cairo_rectangle_int_t *drawing_area, stru pi = create_plot_info(dive, nr, sample); cairo_translate(gc->cr, drawing_area->x, drawing_area->y); - cairo_set_line_width(gc->cr, 1); + cairo_set_line_width_scaled(gc->cr, 1); cairo_set_line_cap(gc->cr, CAIRO_LINE_CAP_ROUND); cairo_set_line_join(gc->cr, CAIRO_LINE_JOIN_ROUND); @@ -1435,7 +1455,7 @@ void plot(struct graphics_context *gc, cairo_rectangle_int_t *drawing_area, stru gc->topy = 0; gc->bottomy = 1.0; set_source_rgba(gc, BOUNDING_BOX); - cairo_set_line_width(gc->cr, 1); + cairo_set_line_width_scaled(gc->cr, 1); move_to(gc, 0, 0); line_to(gc, 0, 1); line_to(gc, 1, 1); diff --git a/save-xml.c b/save-xml.c index 3bea2adfa..7ce0afbc1 100644 --- a/save-xml.c +++ b/save-xml.c @@ -7,8 +7,6 @@ #include "dive.h" -#define FRACTION(n,x) ((unsigned)(n)/(x)),((unsigned)(n)%(x)) - static void show_milli(FILE *f, const char *pre, int value, const char *unit, const char *post) { int i; diff --git a/statistics.c b/statistics.c index 674efbb94..5e1c8804a 100644 --- a/statistics.c +++ b/statistics.c @@ -13,6 +13,7 @@ #include <stdlib.h> #include <stdarg.h> #include <time.h> +#include <gdk/gdkkeysyms.h> #include "dive.h" #include "display.h" @@ -54,6 +55,7 @@ typedef struct { static total_stats_widget_t stats_w; typedef struct { + int period; duration_t total_time; /* avg_time is simply total_time / nr -- let's not keep this */ duration_t shortest_time; @@ -74,7 +76,31 @@ typedef struct { static stats_t stats; static stats_t stats_selection; - +static stats_t *stats_monthly = NULL; +static stats_t *stats_yearly = NULL; + +GtkWidget *yearly_tree = NULL; + +enum { + YEAR, + DIVES, + TOTAL_TIME, + AVERAGE_TIME, + SHORTEST_TIME, + LONGEST_TIME, + AVG_DEPTH, + MIN_DEPTH, + MAX_DEPTH, + AVG_SAC, + MIN_SAC, + MAX_SAC, + AVG_TEMP, + MIN_TEMP, + MAX_TEMP, + N_COLUMNS +}; + +static char * get_time_string(int seconds, int maxdays); static void process_dive(struct dive *dp, stats_t *stats) { @@ -117,10 +143,234 @@ static void process_dive(struct dive *dp, stats_t *stats) } } +static void init_tree() +{ + GtkCellRenderer *renderer; + GtkTreeViewColumn *column; + GtkTreeStore *store; + int i; + PangoFontDescription *font_desc = pango_font_description_from_string(divelist_font); + + gtk_widget_modify_font(yearly_tree, font_desc); + pango_font_description_free(font_desc); + + renderer = gtk_cell_renderer_text_new (); + char *columns[] = { + "Year\n > Month", "#", "Duration\nTotal", "\nAverage", + "\nShortest", "\nLongest", "Depth\nAverage", "\nMinimum", + "\nMaximum", "SAC\nAverage", "\nMinimum", "\nMaximum", "Temperature\nAverage", + "\nMinimum", "\nMaximum" }; + + /* Add all the columns to the tree view */ + for (i = 0; i < N_COLUMNS; ++i) { + column = gtk_tree_view_column_new(); + gtk_tree_view_column_set_title(column, columns[i]); + gtk_tree_view_append_column(GTK_TREE_VIEW(yearly_tree), column); + renderer = gtk_cell_renderer_text_new(); + gtk_tree_view_column_pack_start(column, renderer, TRUE); + gtk_tree_view_column_add_attribute(column, renderer, "text", i); + gtk_tree_view_column_set_resizable(column, TRUE); + } + + /* Field types */ + store = gtk_tree_store_new ( + N_COLUMNS, // Columns in structure + G_TYPE_STRING, // Period (year or month) + G_TYPE_STRING, // Number of dives + G_TYPE_STRING, // Total duration + G_TYPE_STRING, // Average dive duation + G_TYPE_STRING, // Shortest dive + G_TYPE_STRING, // Longest dive + G_TYPE_STRING, // Average depth + G_TYPE_STRING, // Shallowest dive + G_TYPE_STRING, // Deepest dive + G_TYPE_STRING, // Average air consumption (SAC) + G_TYPE_STRING, // Minimum SAC + G_TYPE_STRING, // Maximum SAC + G_TYPE_STRING, // Average temperature + G_TYPE_STRING, // Minimum temperature + G_TYPE_STRING // Maximum temperature + ); + + gtk_tree_view_set_model (GTK_TREE_VIEW (yearly_tree), GTK_TREE_MODEL (store)); + g_object_unref (store); +} + +static void add_row_to_tree(GtkTreeStore *store, char *value, int index, GtkTreeIter *row_iter, GtkTreeIter *parent) +{ + gtk_tree_store_append(store, row_iter, parent); + gtk_tree_store_set(store, row_iter, index, value, -1); +} + +static void add_cell_to_tree(GtkTreeStore *store, char *value, int index, GtkTreeIter *parent) +{ + gtk_tree_store_set(store, parent, index, value, -1); +} +static char * get_minutes(int seconds) +{ + static char buf[80]; + snprintf(buf, sizeof(buf), "%d:%.2d", FRACTION(seconds, 60)); + return buf; +} + +static void add_cell(GtkTreeStore *store, GtkTreeIter *parent, unsigned int val, int cell, gboolean depth_not_volume) +{ + double value; + int decimals; + const char *unit; + char value_str[40]; + + if (depth_not_volume) { + value = get_depth_units(val, &decimals, &unit); + snprintf(value_str, sizeof(value_str), "%.*f %s", decimals, value, unit); + } else { + value = get_volume_units(val, &decimals, &unit); + snprintf(value_str, sizeof(value_str), "%.*f %s/min", decimals, value, unit); + } + add_cell_to_tree(store, value_str, cell, parent); +} + +static void process_interval_stats(stats_t stats_interval, GtkTreeIter *parent, GtkTreeIter *row) +{ + double value; + const char *unit; + char value_str[40]; + GtkTreeStore *store; + + store = GTK_TREE_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(yearly_tree))); + + /* Year or month */ + snprintf(value_str, sizeof(value_str), "%d", stats_interval.period); + add_row_to_tree(store, value_str, 0, row, parent); + /* Dives */ + snprintf(value_str, sizeof(value_str), "%d", stats_interval.selection_size); + add_cell_to_tree(store, value_str, 1, row); + /* Total duration */ + add_cell_to_tree(store, get_time_string(stats_interval.total_time.seconds, 0), 2, row); + /* Average dive duration */ + add_cell_to_tree(store, get_minutes(stats_interval.total_time.seconds / stats_interval.selection_size), 3, row); + /* Shortest duration */ + add_cell_to_tree(store, get_minutes(stats_interval.shortest_time.seconds), 4, row); + /* Longest duration */ + add_cell_to_tree(store, get_minutes(stats_interval.longest_time.seconds), 5, row); + /* Average depth */ + add_cell(store, row, stats_interval.avg_depth.mm, 6, TRUE); + /* Smallest maximum depth */ + add_cell(store, row, stats_interval.min_depth.mm, 7, TRUE); + /* Deepest maximum depth */ + add_cell(store, row, stats_interval.max_depth.mm, 8, TRUE); + /* Average air consumption */ + add_cell(store, row, stats_interval.avg_sac.mliter, 9, FALSE); + /* Smallest average air consumption */ + add_cell(store, row, stats_interval.min_sac.mliter, 10, FALSE); + /* Biggest air consumption */ + add_cell(store, row, stats_interval.max_sac.mliter, 11, FALSE); + /* Average water temperature */ + value = get_temp_units(stats_interval.min_temp, &unit); + if (stats_interval.combined_temp && stats_interval.combined_count) { + snprintf(value_str, sizeof(value_str), "%.1f %s", stats_interval.combined_temp / (stats_interval.combined_count * 1.0), unit); + add_cell_to_tree(store, value_str, 12, row); + } else { + add_cell_to_tree(store, "", 12, row); + } + /* Coldest water temperature */ + snprintf(value_str, sizeof(value_str), "%.1f %s\t", value, unit); + add_cell_to_tree(store, value_str, 13, row); + /* Warmest water temperature */ + value = get_temp_units(stats_interval.max_temp, &unit); + snprintf(value_str, sizeof(value_str), "%.1f %s", value, unit); + add_cell_to_tree(store, value_str, 14, row); +} + +void clear_statistics() +{ + GtkTreeStore *store; + + store = GTK_TREE_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(yearly_tree))); + gtk_tree_store_clear(store); + yearly_tree = NULL; +} + +static gboolean on_delete(GtkWidget *window, GdkEvent *event, gpointer data) +{ + clear_statistics(); + gtk_widget_destroy(window); + return TRUE; +} + +static void key_press_event(GtkWidget *window, GdkEventKey *event, gpointer data) +{ + if ((event->string != NULL && event->keyval == GDK_Escape) || + (event->string != NULL && event->keyval == GDK_w && event->state & GDK_CONTROL_MASK)) { + clear_statistics(); + gtk_widget_destroy(window); + } +} + +void update_yearly_stats() +{ + int i, j, combined_months, month = 0; + GtkTreeIter year_iter, month_iter; + GtkTreeStore *store; + + store = GTK_TREE_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(yearly_tree))); + gtk_tree_store_clear(store); + + for (i = 0; stats_yearly != NULL && stats_yearly[i].period; ++i) { + process_interval_stats(stats_yearly[i], NULL, &year_iter); + combined_months = 0; + + for (j = 0; combined_months < stats_yearly[i].selection_size; ++j) { + combined_months += stats_monthly[month].selection_size; + process_interval_stats(stats_monthly[month++], &year_iter, &month_iter); + } + } +} + +void show_yearly_stats() +{ + GtkWidget *window; + GtkWidget *sw; + + if (yearly_tree) + return; + + window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + sw = gtk_scrolled_window_new (NULL, NULL); + yearly_tree = gtk_tree_view_new (); + + gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER); + gtk_window_set_default_size(GTK_WINDOW(window), 640, 480); + gtk_window_set_title(GTK_WINDOW(window), "Yearly Statistics"); + gtk_container_set_border_width(GTK_CONTAINER(window), 5); + GTK_WINDOW(window)->allow_shrink = TRUE; + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (sw), GTK_SHADOW_ETCHED_IN); + + gtk_container_add (GTK_CONTAINER (sw), yearly_tree); + gtk_container_add (GTK_CONTAINER (window), sw); + + /* Display the yearly statistics on top level + * Monthly statistics are available by expanding a year */ + init_tree(); + update_yearly_stats(); + + g_signal_connect (G_OBJECT (window), "key_press_event", G_CALLBACK (key_press_event), NULL); + g_signal_connect (G_OBJECT (window), "delete-event", G_CALLBACK (on_delete), NULL); + gtk_widget_show_all(window); +} + static void process_all_dives(struct dive *dive, struct dive **prev_dive) { int idx; struct dive *dp; + struct tm *tm; + int current_year = 0; + int current_month = 0; + int year_iter = 0; + int month_iter = 0; + int prev_month = 0, prev_year = 0; + unsigned int size; *prev_dive = NULL; memset(&stats, 0, sizeof(stats)); @@ -129,6 +379,23 @@ static void process_all_dives(struct dive *dive, struct dive **prev_dive) stats.min_depth.mm = dive_table.dives[0]->maxdepth.mm; stats.selection_size = dive_table.nr; } + + /* allocate sufficient space to hold the worst + * case (one dive per year or all dives during + * one month) for yearly and monthly statistics*/ + + if (stats_yearly != NULL) { + free(stats_yearly); + free(stats_monthly); + } + size = sizeof(stats_t) * (dive_table.nr + 1); + stats_yearly = malloc(size); + stats_monthly = malloc(size); + if (!stats_yearly || !stats_monthly) + return; + memset(stats_yearly, 0, size); + memset(stats_monthly, 0, size); + /* this relies on the fact that the dives in the dive_table * are in chronological order */ for (idx = 0; idx < dive_table.nr; idx++) { @@ -139,7 +406,39 @@ static void process_all_dives(struct dive *dive, struct dive **prev_dive) *prev_dive = dive_table.dives[idx-1]; } process_dive(dp, &stats); + + /* yearly statistics */ + tm = gmtime(&dp->when); + if (current_year == 0) + current_year = tm->tm_year + 1900; + + if (current_year != tm->tm_year + 1900) { + current_year = tm->tm_year + 1900; + process_dive(dp, &(stats_yearly[++year_iter])); + } else + process_dive(dp, &(stats_yearly[year_iter])); + + stats_yearly[year_iter].selection_size++; + stats_yearly[year_iter].period = current_year; + + /* monthly statistics */ + if (current_month == 0) { + current_month = tm->tm_mon + 1; + } else { + if (current_month != tm->tm_mon + 1) + current_month = tm->tm_mon + 1; + if (prev_month != current_month || prev_year != current_year) + month_iter++; + } + + process_dive(dp, &(stats_monthly[month_iter])); + stats_monthly[month_iter].selection_size++; + stats_monthly[month_iter].period = current_month; + prev_month = current_month; + prev_year = current_year; } + if (yearly_tree) + update_yearly_stats(); } /* make sure we skip the selected summary entries */ |