1 /********************************************************************\
2  * gnc-dense-cal.c : a custom densely-dispalyed calendar widget     *
3  * Copyright (C) 2002,2006 Joshua Sled <jsled@asynchronous.org>     *
4  *                                                                  *
5  * This program is free software; you can redistribute it and/or    *
6  * modify it under the terms of the GNU General Public License as   *
7  * published by the Free Software Foundation; either version 2 of   *
8  * the License, or (at your option) any later version.              *
9  *                                                                  *
10  * This program is distributed in the hope that it will be useful,  *
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of   *
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the    *
13  * GNU General Public License for more details.                     *
14  *                                                                  *
15  * You should have received a copy of the GNU General Public License*
16  * along with this program; if not, contact:                        *
17  *                                                                  *
18  * Free Software Foundation           Voice:  +1-617-542-5942       *
19  * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652       *
20  * Boston, MA  02110-1301,  USA       gnu@gnu.org                   *
21 \********************************************************************/
22 
23 #include <config.h>
24 
25 #include "gnc-dense-cal.h"
26 #include "gnc-dense-cal-model.h"
27 #include "gnc-engine.h"
28 #include "gnc-gtk-utils.h"
29 #include <glib.h>
30 #include <glib/gi18n.h>
31 #include <gtk/gtk.h>
32 #include <math.h>
33 #include <stdlib.h>
34 #include "gnc-date.h"
35 #include "dialog-utils.h"
36 #include <qoflog.h>
37 
38 static const QofLogModule log_module = G_LOG_DOMAIN;
39 
40 /**
41  * Marking ...
42  *
43  * We want a facility to mark multiple days on the calendar.  This facility
44  * should be efficient in display.  It will take an array+count of GDates on
45  * which to mark the calendar.  Dates outside of the visible calendar range
46  * will be ignored.
47  *
48  * Markings will be manipulated in tagged sets for markings related to the
49  * same event; in order to efficiently process these sets [removal,
50  * primarily], we will keep a multiple data structures.
51  *
52  *
53  * We need to be able to perform the following actions:
54  * . Add a new mark-set, returning a calendar-unique tag.
55  * . Remove a mark-set by tag.
56  * . Iterate over all days in the calendar, listing which markings are active
57  *   on that day.
58  *
59  * The markings in the calendar will be internally represented as an array of
60  * GLists, with each item in the list pointing to a gdc_mark_data structure.
61  * The gdc_mark_data structures will contain:
62  * . the external/caller marker tag
63  * . the marker indication [color, when supported]
64  * . a GList of all instances of the marker in the visible calendar, by
65  *   'marks' index.
66  *
67  * The list of gdc_mark_data structures itself will be a top-level list in the
68  * GncDenseCal structure.
69  **/
70 
71 static const int DENSE_CAL_DEFAULT_WIDTH = 15;
72 static const int DENSE_CAL_DEFAULT_HEIGHT = 105;
73 static const int MINOR_BORDER_SIZE = 1;
74 static const int COL_BORDER_SIZE = 3;
75 
76 #undef G_LOG_DOMAIN
77 #define G_LOG_DOMAIN "gnc.gui.dense-cal"
78 
79 static void gnc_dense_cal_class_init(GncDenseCalClass *klass);
80 static void gnc_dense_cal_init(GncDenseCal *dcal);
81 static void gnc_dense_cal_finalize(GObject *object);
82 static void gnc_dense_cal_dispose(GObject *object);
83 static void gnc_dense_cal_realize(GtkWidget *widget, gpointer user_data);
84 static void gnc_dense_cal_configure(GtkWidget *widget, GdkEventConfigure *event, gpointer user_data);
85 static void gnc_dense_cal_draw_to_buffer(GncDenseCal *dcal);
86 static gboolean gnc_dense_cal_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data);
87 
88 static void gdc_reconfig(GncDenseCal *dcal);
89 
90 static void gdc_free_all_mark_data(GncDenseCal *dcal);
91 
92 static void _gdc_compute_min_size(GncDenseCal *dcal,
93                                   guint *min_width, guint *min_height);
94 static void _gdc_set_cal_min_size_req(GncDenseCal *dcal);
95 static gint gnc_dense_cal_motion_notify(GtkWidget      *widget,
96                                         GdkEventMotion *event);
97 static gint gnc_dense_cal_button_press(GtkWidget *widget,
98                                        GdkEventButton *evt);
99 
100 static void _gdc_view_option_changed(GtkComboBox *widget, gpointer user_data);
101 
102 static inline int day_width_at(GncDenseCal *dcal, guint xScale);
103 static inline int day_width(GncDenseCal *dcal);
104 static inline int day_height_at(GncDenseCal *dcal, guint yScale);
105 static inline int day_height(GncDenseCal *dcal);
106 static inline int week_width_at(GncDenseCal *dcal, guint xScale);
107 static inline int week_width(GncDenseCal *dcal);
108 static inline int week_height_at(GncDenseCal *dcal, guint yScale);
109 static inline int week_height(GncDenseCal *dcal);
110 static inline int col_width_at(GncDenseCal *dcal, guint xScale);
111 static inline int col_width(GncDenseCal *dcal);
112 
113 static inline int col_height(GncDenseCal *dcal);
114 static inline int num_cols(GncDenseCal *dcal);
115 
116 static void _gnc_dense_cal_set_month(GncDenseCal *dcal, GDateMonth mon, gboolean redraw);
117 static void _gnc_dense_cal_set_year(GncDenseCal *dcal, guint year, gboolean redraw);
118 
119 
120 /**
121  * Returns the total number of weeks to display in the calendar [irrespective
122  * of columns/weeks-per-col].
123  **/
124 static inline int num_weeks(GncDenseCal *dcal);
125 /**
126  * Returns the number of weeks per column.  Note that this is the number of
127  * weeks needed to display the longest column.
128  **/
129 static int num_weeks_per_col(GncDenseCal *dcal);
130 
131 /* hotspot calculation */
132 static gint wheres_this(GncDenseCal *dcal, int x, int y);
133 
134 static void recompute_x_y_scales(GncDenseCal *dcal);
135 static void recompute_mark_storage(GncDenseCal *dcal);
136 static void recompute_extents(GncDenseCal *dcal);
137 static void populate_hover_window(GncDenseCal *dcal);
138 
139 static void month_coords(GncDenseCal *dcal, int monthOfCal, GList **outList);
140 static void doc_coords(GncDenseCal *dcal, int dayOfCal,
141                        int *x1, int *y1, int *x2, int *y2);
142 
143 static void gdc_mark_add(GncDenseCal *dcal, guint tag, gchar *name, gchar *info, guint size, GDate **dateArray);
144 static void gdc_mark_remove(GncDenseCal *dcal, guint mark_to_remove, gboolean redraw);
145 
146 static void gdc_add_tag_markings(GncDenseCal *cal, guint tag);
147 static void gdc_add_markings(GncDenseCal *cal);
148 static void gdc_remove_markings(GncDenseCal *cal);
149 
150 static GObject *parent_class = NULL;
151 
152 #define MONTH_NAME_BUFSIZE 10
153 
154 /* Takes the number of months since January, in the range 0 to
155  * 11. Returns the abbreviated month name according to the current
156  * locale.*/
157 static const gchar*
month_name(int mon)158 month_name(int mon)
159 {
160     static gchar buf[MONTH_NAME_BUFSIZE];
161     GDate date;
162     gint arbitrary_year = 1977;
163 
164     memset(buf, 0, MONTH_NAME_BUFSIZE);
165     g_date_clear(&date, 1);
166 
167     g_date_set_year(&date, arbitrary_year);
168     g_date_set_day(&date, 1);
169     // g_date API is 1..12 (not 0..11)
170     g_date_set_month(&date, mon + 1);
171     g_date_strftime(buf, MONTH_NAME_BUFSIZE, "%b", &date);
172 
173     return buf;
174 }
175 
176 /* Takes the number of days since Sunday, in the range 0 to 6. Returns
177  * the abbreviated weekday name according to the current locale. */
178 static void
day_label(gchar * buf,int buf_len,int dow)179 day_label(gchar *buf, int buf_len, int dow)
180 {
181     gnc_dow_abbrev(buf, buf_len, dow);
182     /* Use only the first two characters */
183     if (g_utf8_strlen(buf, -1) > 2)
184     {
185         gchar *pointer = g_utf8_offset_to_pointer(buf, 2);
186         *pointer = '\0';
187     }
188 }
189 
190 GType
gnc_dense_cal_get_type()191 gnc_dense_cal_get_type()
192 {
193     static GType dense_cal_type = 0;
194 
195     if (dense_cal_type == 0)
196     {
197         static const GTypeInfo dense_cal_info =
198         {
199             sizeof (GncDenseCalClass),
200             NULL,
201             NULL,
202             (GClassInitFunc) gnc_dense_cal_class_init,
203             NULL,
204             NULL,
205             sizeof (GncDenseCal),
206             0,
207             (GInstanceInitFunc) gnc_dense_cal_init,
208             NULL
209         };
210 
211         dense_cal_type = g_type_register_static(GTK_TYPE_BOX,
212                                                 "GncDenseCal",
213                                                 &dense_cal_info, 0);
214     }
215 
216     return dense_cal_type;
217 }
218 
219 static void
gnc_dense_cal_class_init(GncDenseCalClass * klass)220 gnc_dense_cal_class_init(GncDenseCalClass *klass)
221 {
222     GObjectClass *object_class;
223     GtkWidgetClass *widget_class;
224 
225     object_class = G_OBJECT_CLASS (klass);
226     widget_class = GTK_WIDGET_CLASS (klass);
227 
228     gtk_widget_class_set_css_name (GTK_WIDGET_CLASS(klass), "calendar");
229 
230     parent_class = g_type_class_peek_parent (klass);
231 
232     object_class->finalize = gnc_dense_cal_finalize;
233     object_class->dispose = gnc_dense_cal_dispose;
234 
235     widget_class->motion_notify_event = gnc_dense_cal_motion_notify;
236     widget_class->button_press_event = gnc_dense_cal_button_press;
237 }
238 
239 enum _GdcViewOptsColumns
240 {
241     VIEW_OPTS_COLUMN_LABEL = 0,
242     VIEW_OPTS_COLUMN_NUM_MONTHS
243 };
244 
245 static GtkListStore *_cal_view_options = NULL;
246 static GtkListStore*
_gdc_get_view_options(void)247 _gdc_get_view_options(void)
248 {
249     if (_cal_view_options == NULL)
250     {
251         _cal_view_options = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_INT);
252         gtk_list_store_insert_with_values(_cal_view_options, NULL, G_MAXINT, 0, _("12 months"), 1, 12, -1);
253         gtk_list_store_insert_with_values(_cal_view_options, NULL, G_MAXINT, 0, _("6 months"), 1, 6, -1);
254         gtk_list_store_insert_with_values(_cal_view_options, NULL, G_MAXINT, 0, _("4 months"), 1, 4, -1);
255         gtk_list_store_insert_with_values(_cal_view_options, NULL, G_MAXINT, 0, _("3 months"), 1, 3, -1);
256         gtk_list_store_insert_with_values(_cal_view_options, NULL, G_MAXINT, 0, _("2 months"), 1, 2, -1);
257         gtk_list_store_insert_with_values(_cal_view_options, NULL, G_MAXINT, 0, _("1 month"), 1, 1, -1);
258     }
259 
260     return _cal_view_options;
261 }
262 
263 static void
gnc_dense_cal_init(GncDenseCal * dcal)264 gnc_dense_cal_init(GncDenseCal *dcal)
265 {
266     GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET(dcal));
267 
268     gtk_orientable_set_orientation (GTK_ORIENTABLE(dcal), GTK_ORIENTATION_VERTICAL);
269 
270     // Set the style context for this widget so it can be easily manipulated with css
271     gnc_widget_style_context_add_class (GTK_WIDGET(dcal), "calendar");
272 
273     // Set the name of this widget so it can be easily manipulated with css
274     gtk_widget_set_name (GTK_WIDGET(dcal), "gnc-id-dense-calendar");
275 
276     gtk_style_context_add_class (context, GTK_STYLE_CLASS_CALENDAR);
277     {
278         GtkTreeModel *options;
279         GtkCellRenderer *text_rend;
280 
281         options = GTK_TREE_MODEL(_gdc_get_view_options());
282         dcal->view_options = GTK_COMBO_BOX(gtk_combo_box_new_with_model(options));
283         gtk_combo_box_set_active(GTK_COMBO_BOX(dcal->view_options), 0);
284         text_rend = GTK_CELL_RENDERER(gtk_cell_renderer_text_new());
285         gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(dcal->view_options), text_rend, TRUE);
286         gtk_cell_layout_add_attribute(GTK_CELL_LAYOUT(dcal->view_options),
287                                       text_rend, "text", VIEW_OPTS_COLUMN_LABEL);
288         g_signal_connect(G_OBJECT(dcal->view_options), "changed", G_CALLBACK(_gdc_view_option_changed), (gpointer)dcal);
289     }
290 
291     {
292         GtkWidget *hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
293         GtkWidget *label = gtk_label_new (_("View"));
294 
295         gtk_box_set_homogeneous (GTK_BOX (hbox), FALSE);
296         gtk_widget_set_halign (label, GTK_ALIGN_END);
297         gtk_widget_set_margin_end (label, 5);
298         gtk_box_pack_start(GTK_BOX(hbox), label, TRUE, TRUE, 0);
299         gtk_box_pack_start(GTK_BOX(hbox), GTK_WIDGET(dcal->view_options), FALSE, FALSE, 0);
300 
301         gtk_box_pack_start(GTK_BOX(dcal), GTK_WIDGET(hbox), FALSE, FALSE, 0);
302     }
303     dcal->cal_drawing_area = GTK_DRAWING_AREA(gtk_drawing_area_new());
304 
305     gtk_widget_add_events(GTK_WIDGET(dcal->cal_drawing_area), (GDK_EXPOSURE_MASK
306                           | GDK_BUTTON_PRESS_MASK
307                           | GDK_BUTTON_RELEASE_MASK
308                           | GDK_POINTER_MOTION_MASK
309                           | GDK_POINTER_MOTION_HINT_MASK));
310     gtk_box_pack_start(GTK_BOX(dcal), GTK_WIDGET(dcal->cal_drawing_area), TRUE, TRUE, 0);
311     g_signal_connect(G_OBJECT(dcal->cal_drawing_area), "draw", G_CALLBACK(gnc_dense_cal_draw), (gpointer)dcal);
312     g_signal_connect(G_OBJECT(dcal->cal_drawing_area), "realize", G_CALLBACK(gnc_dense_cal_realize), (gpointer)dcal);
313     g_signal_connect(G_OBJECT(dcal->cal_drawing_area), "configure_event", G_CALLBACK(gnc_dense_cal_configure), (gpointer)dcal);
314 
315     dcal->disposed = FALSE;
316     dcal->initialized = FALSE;
317     dcal->markData = NULL;
318     dcal->numMarks = 0;
319     dcal->marks = NULL;
320     dcal->lastMarkTag = 0;
321 
322     dcal->showPopup = FALSE;
323 
324     dcal->transPopup = GTK_WINDOW(gtk_window_new(GTK_WINDOW_POPUP));
325     {
326         GtkWidget *vbox, *hbox;
327         GtkWidget *l;
328         GtkListStore *tree_data;
329         GtkTreeView *tree_view;
330 
331         vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 5);
332         gtk_box_set_homogeneous (GTK_BOX (vbox), FALSE);
333         hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 5);
334         gtk_box_set_homogeneous (GTK_BOX (hbox), FALSE);
335 
336         gtk_widget_set_name (GTK_WIDGET(dcal->transPopup), "gnc-id-dense-calendar-popup");
337 
338         l = gtk_label_new(_("Date: "));
339         gtk_widget_set_margin_start (l, 5);
340         gtk_container_add(GTK_CONTAINER(hbox), l);
341         l = gtk_label_new("YY/MM/DD");
342         g_object_set_data(G_OBJECT(dcal->transPopup), "dateLabel", l);
343         gtk_container_add(GTK_CONTAINER(hbox), l);
344         gtk_container_add(GTK_CONTAINER(vbox), hbox);
345 
346         gtk_container_add(GTK_CONTAINER(vbox), gtk_separator_new (GTK_ORIENTATION_HORIZONTAL));
347 
348         tree_data = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING);
349         tree_view = GTK_TREE_VIEW(gtk_tree_view_new_with_model(GTK_TREE_MODEL(tree_data)));
350         gtk_tree_view_insert_column_with_attributes(tree_view, -1, _("Name"), gtk_cell_renderer_text_new(), "text", 0, NULL);
351         gtk_tree_view_insert_column_with_attributes(tree_view, -1, _("Frequency"), gtk_cell_renderer_text_new(), "text", 1, NULL);
352         gtk_tree_selection_set_mode (gtk_tree_view_get_selection (GTK_TREE_VIEW(tree_view)), GTK_SELECTION_NONE);
353         g_object_set_data(G_OBJECT(dcal->transPopup), "model", tree_data);
354         g_object_unref (tree_data);
355         gtk_container_add(GTK_CONTAINER(vbox), GTK_WIDGET(tree_view));
356 
357         gtk_container_add(GTK_CONTAINER(dcal->transPopup), vbox);
358 
359         gtk_window_set_resizable(GTK_WINDOW(dcal->transPopup), FALSE);
360 
361         gtk_widget_realize(GTK_WIDGET(dcal->transPopup));
362     }
363 
364     /* Deal with the various label sizes. */
365     {
366         PangoLayout *layout = gtk_widget_create_pango_layout(GTK_WIDGET(dcal), NULL);
367         GtkStyleContext *stylectxt = gtk_widget_get_style_context (GTK_WIDGET(dcal));
368         GtkStateFlags state_flags = gtk_style_context_get_state (stylectxt);
369         gint font_size_reduction_units = 1;
370         PangoFontDescription *font_desc;
371         GtkCssProvider *provider;
372         gint font_size, px_size;
373         gint i;
374         gint maxWidth, maxHeight;
375         gchar *px_str, *widget_css;
376         gdouble dpi;
377 
378         gtk_style_context_get (stylectxt, state_flags,
379                                GTK_STYLE_PROPERTY_FONT, &font_desc, NULL);
380         font_size = pango_font_description_get_size(font_desc);
381 
382         provider = gtk_css_provider_new();
383         dpi = gdk_screen_get_resolution (gdk_screen_get_default ());
384         px_size = ((font_size / PANGO_SCALE) - font_size_reduction_units) * (dpi / 72.);
385         px_str = g_strdup_printf("%i", px_size);
386         widget_css = g_strconcat ("*{\n  font-size:", px_str, "px;\n}\n", NULL);
387 
388         gtk_css_provider_load_from_data (provider, widget_css, -1, NULL);
389         gtk_style_context_add_provider (stylectxt, GTK_STYLE_PROVIDER (provider),
390                                    GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
391         g_object_unref (provider);
392         g_free (px_str);
393         g_free (widget_css);
394 
395         pango_font_description_free (font_desc);
396 
397         maxWidth = maxHeight = 0;
398         for (i = 0; i < 12; i++)
399         {
400             gint w, h;
401             pango_layout_set_text(layout, month_name(i), -1);
402             pango_layout_get_pixel_size(layout, &w, &h);
403             maxWidth = MAX(maxWidth, w);
404             maxHeight = MAX(maxHeight, h);
405         }
406 
407         // these two were reversed, before...
408         dcal->label_width    = maxWidth;
409         dcal->label_height   = maxHeight;
410 
411         g_object_unref(layout);
412     }
413 
414     dcal->month = G_DATE_JANUARY;
415     dcal->year  = 1970;
416 
417     dcal->numMonths = 12;
418     dcal->monthsPerCol = 3;
419     dcal->leftPadding = 4;
420     dcal->topPadding = 4;
421 
422     {
423     GDate now;
424     g_date_clear (&now, 1);
425         gnc_gdate_set_today (&now);
426         _gnc_dense_cal_set_month(dcal, g_date_get_month(&now), FALSE);
427         _gnc_dense_cal_set_year(dcal, g_date_get_year(&now), FALSE);
428     }
429 
430     recompute_extents(dcal);
431     recompute_mark_storage(dcal);
432 
433     /* Compute initial scaling factors; will be increased when we're
434      * allocated enough space to scale up. */
435     {
436         PangoLayout *layout;
437         int width_88, height_88;
438         int width_XXX, height_XXX;
439 
440         layout = gtk_widget_create_pango_layout(GTK_WIDGET(dcal), NULL);
441 
442         pango_layout_set_text(layout, "88", -1);
443         pango_layout_get_pixel_size(layout, &width_88, &height_88);
444 
445         pango_layout_set_text(layout, "XXX", -1);
446         pango_layout_get_pixel_size(layout, &width_XXX, &height_XXX);
447 
448         dcal->min_x_scale = dcal->x_scale = width_88 + 2;
449         dcal->min_y_scale = dcal->y_scale = MAX(floor((float)width_XXX / 3.), height_88 + 2);
450 
451         dcal->dayLabelHeight = height_88;
452 
453         g_object_unref(layout);
454     }
455 
456     dcal->initialized = TRUE;
457 
458 
459     dcal->week_starts_monday = 0;
460     {
461         gchar **parts;
462         const char *week_start_str;
463 
464         /* Use this renaming macro to avoid extraction of the message
465            string into the gnucash.pot file when calling xgettext. */
466 #define dgettext_noextract dgettext
467         /* Translators: This string must not show up in gnucash.pot as
468            it is looked up in the "gtk20" translation domain
469            instead. */
470         week_start_str = dgettext_noextract("gtk20", "calendar:week_start:0");
471 #undef dgettext_noextract
472 
473         parts = g_strsplit(week_start_str, ":", 3);
474         if (parts[0] != NULL
475                 && parts[1] != NULL
476                 && parts[2] != NULL)
477         {
478             if (strcmp("1", parts[2]) == 0)
479                 dcal->week_starts_monday = 1;
480         }
481         g_strfreev(parts);
482     }
483 
484     gtk_widget_show_all(GTK_WIDGET(dcal));
485 }
486 
487 static void
_gdc_set_cal_min_size_req(GncDenseCal * dcal)488 _gdc_set_cal_min_size_req(GncDenseCal *dcal)
489 {
490     guint min_width, min_height;
491 
492     _gdc_compute_min_size(dcal, &min_width, &min_height);
493     gtk_widget_set_size_request(GTK_WIDGET(dcal->cal_drawing_area), min_width, min_height);
494 }
495 
496 GtkWidget*
gnc_dense_cal_new(void)497 gnc_dense_cal_new(void)
498 {
499     GncDenseCal *dcal;
500     dcal = g_object_new(GNC_TYPE_DENSE_CAL, NULL);
501     return GTK_WIDGET(dcal);
502 }
503 
504 GtkWidget*
gnc_dense_cal_new_with_model(GncDenseCalModel * model)505 gnc_dense_cal_new_with_model(GncDenseCalModel *model)
506 {
507     GncDenseCal *cal = GNC_DENSE_CAL(gnc_dense_cal_new());
508     gnc_dense_cal_set_model(cal, model);
509     return GTK_WIDGET(cal);
510 }
511 
512 static void
recompute_first_of_month_offset(GncDenseCal * dcal)513 recompute_first_of_month_offset(GncDenseCal *dcal)
514 {
515     GDate *tmpDate;
516 
517     tmpDate = g_date_new_dmy(1, dcal->month, dcal->year);
518     dcal->firstOfMonthOffset = g_date_get_weekday(tmpDate) % 7;
519     g_date_free(tmpDate);
520 }
521 
522 void
gnc_dense_cal_set_month(GncDenseCal * dcal,GDateMonth mon)523 gnc_dense_cal_set_month(GncDenseCal *dcal, GDateMonth mon)
524 {
525     _gnc_dense_cal_set_month(dcal, mon, TRUE);
526 }
527 
528 static void
_gnc_dense_cal_set_month(GncDenseCal * dcal,GDateMonth mon,gboolean redraw)529 _gnc_dense_cal_set_month(GncDenseCal *dcal, GDateMonth mon, gboolean redraw)
530 {
531     GTimer *t = g_timer_new();
532     if (dcal->month == mon)
533         return;
534     dcal->month = mon;
535     g_timer_start(t);
536     recompute_first_of_month_offset(dcal);
537     DEBUG("recompute_first_of_month_offset: %f", g_timer_elapsed(t, NULL) * 1000.);
538     g_timer_start(t);
539     recompute_extents(dcal);
540     DEBUG("recompute_extents: %f", g_timer_elapsed(t, NULL) * 1000.);
541     if (redraw && gtk_widget_get_realized(GTK_WIDGET(dcal)))
542     {
543         g_timer_start(t);
544         recompute_x_y_scales(dcal);
545         DEBUG("recompute_x_y_scales: %f", g_timer_elapsed(t, NULL) * 1000.);
546         g_timer_start(t);
547         gnc_dense_cal_draw_to_buffer(dcal);
548         DEBUG("draw_to_buffer: %f", g_timer_elapsed(t, NULL) * 1000.);
549         g_timer_start(t);
550         gtk_widget_queue_draw(GTK_WIDGET(dcal->cal_drawing_area));
551         DEBUG("queue_draw: %f", g_timer_elapsed(t, NULL) * 1000.);
552     }
553     g_timer_stop(t);
554     g_timer_destroy(t);
555 }
556 
557 void
gnc_dense_cal_set_year(GncDenseCal * dcal,guint year)558 gnc_dense_cal_set_year(GncDenseCal *dcal, guint year)
559 {
560     _gnc_dense_cal_set_year(dcal, year, TRUE);
561 }
562 
563 static void
_gnc_dense_cal_set_year(GncDenseCal * dcal,guint year,gboolean redraw)564 _gnc_dense_cal_set_year(GncDenseCal *dcal, guint year, gboolean redraw)
565 {
566     if (dcal->year == year)
567         return;
568     dcal->year = year;
569     recompute_first_of_month_offset(dcal);
570     recompute_extents(dcal);
571     if (redraw && gtk_widget_get_realized(GTK_WIDGET(dcal)))
572     {
573         recompute_x_y_scales(dcal);
574         gnc_dense_cal_draw_to_buffer(dcal);
575         gtk_widget_queue_draw(GTK_WIDGET(dcal->cal_drawing_area));
576     }
577 }
578 
579 void
gnc_dense_cal_set_num_months(GncDenseCal * dcal,guint num_months)580 gnc_dense_cal_set_num_months(GncDenseCal *dcal, guint num_months)
581 {
582     {
583         GtkListStore *options = _gdc_get_view_options();
584         GtkTreeIter view_opts_iter, iter_closest_to_req;
585         int closest_index_distance = G_MAXINT;
586 
587         // find closest list value to num_months
588         if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(options), &view_opts_iter))
589         {
590             g_critical("no view options?");
591             return;
592         }
593 
594         do
595         {
596             gint months_val, delta_months;
597 
598             gtk_tree_model_get(GTK_TREE_MODEL(options), &view_opts_iter, VIEW_OPTS_COLUMN_NUM_MONTHS, &months_val, -1);
599             delta_months = abs(months_val - (int)num_months);
600             if (delta_months < closest_index_distance)
601             {
602                 iter_closest_to_req = view_opts_iter;
603                 closest_index_distance = delta_months;
604             }
605         }
606         while (closest_index_distance != 0
607                 && (gtk_tree_model_iter_next(GTK_TREE_MODEL(options), &view_opts_iter)));
608 
609         // set iter on view
610         g_signal_handlers_block_by_func(dcal->view_options, _gdc_view_option_changed, dcal);
611         gtk_combo_box_set_active_iter(GTK_COMBO_BOX(dcal->view_options), &iter_closest_to_req);
612         g_signal_handlers_unblock_by_func(dcal->view_options, _gdc_view_option_changed, dcal);
613     }
614 
615     dcal->numMonths = num_months;
616     recompute_extents(dcal);
617     recompute_mark_storage(dcal);
618     if (gtk_widget_get_realized(GTK_WIDGET(dcal)))
619     {
620         recompute_x_y_scales(dcal);
621         gnc_dense_cal_draw_to_buffer(dcal);
622         gtk_widget_queue_draw(GTK_WIDGET(dcal->cal_drawing_area));
623     }
624 }
625 
626 guint
gnc_dense_cal_get_num_months(GncDenseCal * dcal)627 gnc_dense_cal_get_num_months(GncDenseCal *dcal)
628 {
629     return dcal->numMonths;
630 }
631 
632 void
gnc_dense_cal_set_months_per_col(GncDenseCal * dcal,guint monthsPerCol)633 gnc_dense_cal_set_months_per_col(GncDenseCal *dcal, guint monthsPerCol)
634 {
635     dcal->monthsPerCol = monthsPerCol;
636     recompute_x_y_scales(dcal);
637 }
638 
639 GDateMonth
gnc_dense_cal_get_month(GncDenseCal * dcal)640 gnc_dense_cal_get_month(GncDenseCal *dcal)
641 {
642     return dcal->month;
643 }
644 
645 GDateYear
gnc_dense_cal_get_year(GncDenseCal * dcal)646 gnc_dense_cal_get_year(GncDenseCal *dcal)
647 {
648     return dcal->year;
649 }
650 
651 static void
gnc_dense_cal_dispose(GObject * object)652 gnc_dense_cal_dispose (GObject *object)
653 {
654     GncDenseCal *dcal;
655     g_return_if_fail(object != NULL);
656     g_return_if_fail(GNC_IS_DENSE_CAL(object));
657 
658     dcal = GNC_DENSE_CAL(object);
659 
660     if (dcal->disposed)
661         return;
662     dcal->disposed = TRUE;
663 
664     if (gtk_widget_get_realized(GTK_WIDGET(dcal->transPopup)))
665     {
666         gtk_widget_hide(GTK_WIDGET(dcal->transPopup));
667         gtk_widget_destroy(GTK_WIDGET(dcal->transPopup));
668         dcal->transPopup = NULL;
669     }
670 
671     if (dcal->surface)
672     {
673         cairo_surface_destroy (dcal->surface);
674         dcal->surface = NULL;
675     }
676 
677     /* FIXME: we have a bunch of cleanup to do, here. */
678 
679     gdc_free_all_mark_data(dcal);
680 
681     g_object_unref(G_OBJECT(dcal->model));
682 
683     if (G_OBJECT_CLASS (parent_class)->dispose)
684         G_OBJECT_CLASS(parent_class)->dispose(object);
685 }
686 
687 static void
gnc_dense_cal_finalize(GObject * object)688 gnc_dense_cal_finalize (GObject *object)
689 {
690     g_return_if_fail (object != NULL);
691     g_return_if_fail (GNC_IS_DENSE_CAL (object));
692 
693     if (G_OBJECT_CLASS (parent_class)->finalize)
694         G_OBJECT_CLASS(parent_class)->finalize(object);
695 }
696 
697 static void
gnc_dense_cal_configure(GtkWidget * widget,GdkEventConfigure * event,gpointer user_data)698 gnc_dense_cal_configure(GtkWidget *widget,
699                         GdkEventConfigure *event,
700                         gpointer user_data)
701 {
702     GncDenseCal *dcal = GNC_DENSE_CAL(user_data);
703     recompute_x_y_scales(dcal);
704     gdc_reconfig(dcal);
705     gtk_widget_queue_draw_area(widget,
706                                event->x, event->y,
707                                event->width, event->height);
708 }
709 
710 static void
gnc_dense_cal_realize(GtkWidget * widget,gpointer user_data)711 gnc_dense_cal_realize (GtkWidget *widget, gpointer user_data)
712 {
713     GncDenseCal *dcal;
714 
715     g_return_if_fail(widget != NULL);
716     g_return_if_fail(GNC_IS_DENSE_CAL (user_data));
717     dcal = GNC_DENSE_CAL(user_data);
718 
719     recompute_x_y_scales(dcal);
720     gdc_reconfig(dcal);
721 }
722 
723 static void
gdc_reconfig(GncDenseCal * dcal)724 gdc_reconfig(GncDenseCal *dcal)
725 {
726     GtkWidget *widget;
727     GtkAllocation alloc;
728 
729     if (dcal->surface)
730         cairo_surface_destroy (dcal->surface);
731 
732     widget = GTK_WIDGET(dcal->cal_drawing_area);
733     gtk_widget_get_allocation (widget, &alloc);
734     dcal->surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
735                                                 alloc.width,
736                                                 alloc.height);
737     gnc_dense_cal_draw_to_buffer(dcal);
738 }
739 
740 static void
_gdc_compute_min_size(GncDenseCal * dcal,guint * min_width,guint * min_height)741 _gdc_compute_min_size(GncDenseCal *dcal, guint *min_width, guint *min_height)
742 {
743     if (min_width != NULL)
744     {
745         *min_width =
746             (dcal->leftPadding * 2)
747             + (num_cols(dcal) * (col_width_at(dcal, dcal->min_x_scale)
748                                  + dcal->label_width))
749             + ((num_cols(dcal) - 1) * COL_BORDER_SIZE);
750     }
751 
752     if (min_height != NULL)
753     {
754         *min_height =
755             (dcal->topPadding * 2)
756             + MINOR_BORDER_SIZE
757             + dcal->dayLabelHeight
758             + (num_weeks_per_col(dcal)
759                * week_height_at(dcal, dcal->min_y_scale));
760     }
761 }
762 
763 static void
recompute_x_y_scales(GncDenseCal * dcal)764 recompute_x_y_scales(GncDenseCal *dcal)
765 {
766     int denom;
767     int width, height;
768 
769     width = DENSE_CAL_DEFAULT_WIDTH;
770     height = DENSE_CAL_DEFAULT_HEIGHT;
771     if (dcal->initialized)
772     {
773         GtkAllocation alloc;
774         gtk_widget_get_allocation (GTK_WIDGET(dcal->cal_drawing_area), &alloc);
775         width  = alloc.width;
776         height = alloc.height;
777     }
778 
779     /* FIXME: there's something slightly wrong in the x_scale computation that
780      * lets us draw larger than our area. */
781     denom = 7 * num_cols(dcal);
782     g_assert(denom != 0);
783     dcal->x_scale = ((gint)(width
784                             - (dcal->leftPadding * 2)
785                             - (num_cols(dcal) * ((8 * MINOR_BORDER_SIZE)
786                                     + dcal->label_width))
787                             - ((num_cols(dcal) - 1) * COL_BORDER_SIZE))
788                      / denom);
789     dcal->x_scale = MAX(dcal->x_scale, dcal->min_x_scale);
790 
791     denom = num_weeks_per_col(dcal);
792     g_assert(denom != 0);
793     dcal->y_scale = ((gint)(height
794                             - (dcal->topPadding * 2)
795                             - MINOR_BORDER_SIZE
796                             - dcal->dayLabelHeight
797                             - (num_weeks_per_col(dcal) - 1
798                                * MINOR_BORDER_SIZE))
799                      / denom);
800     dcal->y_scale = MAX(dcal->y_scale, dcal->min_y_scale);
801 
802     _gdc_set_cal_min_size_req(dcal);
803 }
804 
805 static void
gdc_free_all_mark_data(GncDenseCal * dcal)806 gdc_free_all_mark_data(GncDenseCal *dcal)
807 {
808     int i;
809     GList *l;
810     for (i = 0; i < dcal->numMarks; i++)
811     {
812         /* Each of these just contains an elt of dcal->markData,
813          * which we're about to free, below... */
814         g_list_free(dcal->marks[i]);
815     }
816     g_free(dcal->marks);
817     dcal->marks = NULL;
818     /* Remove the old mark data. */
819     for (l = dcal->markData; l; l = l->next)
820     {
821         g_list_free(((gdc_mark_data*)l->data)->ourMarks);
822         g_free((gdc_mark_data*)l->data);
823     }
824     g_list_free(dcal->markData);
825     dcal->markData = NULL;
826 }
827 
828 static void
recompute_mark_storage(GncDenseCal * dcal)829 recompute_mark_storage(GncDenseCal *dcal)
830 {
831     if (dcal->marks == NULL)
832         goto createNew;
833     gdc_free_all_mark_data(dcal);
834 
835 createNew:
836     dcal->numMarks = num_weeks(dcal) * 7;
837     dcal->marks = g_new0(GList*, dcal->numMarks);
838     if (dcal->model)
839         gdc_add_markings(dcal);
840 }
841 
842 static void
recompute_extents(GncDenseCal * dcal)843 recompute_extents(GncDenseCal *dcal)
844 {
845     GDate date;
846     gint start_week, end_week;
847 
848     g_date_clear(&date, 1);
849     g_date_set_dmy(&date, 1, dcal->month, dcal->year);
850     start_week = (dcal->week_starts_monday
851                   ? g_date_get_monday_week_of_year(&date)
852                   : g_date_get_sunday_week_of_year(&date));
853     g_date_add_months(&date, dcal->numMonths);
854     end_week = (dcal->week_starts_monday
855                 ? g_date_get_monday_week_of_year(&date)
856                 : g_date_get_sunday_week_of_year(&date));
857     if (g_date_get_year(&date) != dcal->year)
858     {
859         end_week += (dcal->week_starts_monday
860                      ? g_date_get_monday_weeks_in_year(dcal->year)
861                      : g_date_get_sunday_weeks_in_year(dcal->year));
862     }
863     dcal->num_weeks = end_week - start_week + 1;
864 }
865 
866 static void
free_rect(gpointer data,gpointer ud)867 free_rect(gpointer data, gpointer ud)
868 {
869     g_free((GdkRectangle*)data);
870 }
871 
872 static gboolean
gnc_dense_cal_draw(GtkWidget * widget,cairo_t * cr,gpointer user_data)873 gnc_dense_cal_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data)
874 {
875     GncDenseCal *dcal;
876 
877     g_return_val_if_fail(widget != NULL, FALSE);
878     g_return_val_if_fail(GNC_IS_DENSE_CAL(user_data), FALSE);
879 
880     dcal = GNC_DENSE_CAL(user_data);
881 
882     cairo_save (cr);
883     cairo_set_source_surface (cr, dcal->surface, 0, 0);
884     cairo_paint (cr);
885     cairo_restore (cr);
886     return TRUE;
887 }
888 
889 #define LOG_AND_RESET(timer, msg) do { DEBUG("%s: %f", msg, g_timer_elapsed(timer, NULL) * 1000.); g_timer_reset(timer); } while (0);
890 
891 static void
gnc_dense_cal_draw_to_buffer(GncDenseCal * dcal)892 gnc_dense_cal_draw_to_buffer(GncDenseCal *dcal)
893 {
894     GtkWidget *widget;
895     GtkStyleContext *stylectxt;
896     GtkStateFlags state_flags;
897     GtkAllocation alloc;
898     gint i;
899     int maxWidth;
900     PangoLayout *layout;
901     GTimer *timer;
902     cairo_t *cr;
903     gchar *primary_color_class, *secondary_color_class, *marker_color_class;
904 
905     timer = g_timer_new();
906     DEBUG("drawing");
907     widget = GTK_WIDGET(dcal);
908 
909     if (!dcal->surface)
910         return;
911 
912     g_timer_start(timer);
913     cr = cairo_create (dcal->surface);
914     layout = gtk_widget_create_pango_layout(GTK_WIDGET(dcal), NULL);
915     LOG_AND_RESET(timer, "create_pango_layout");
916 
917     gtk_widget_get_allocation (GTK_WIDGET(dcal->cal_drawing_area), &alloc);
918     stylectxt = gtk_widget_get_style_context (GTK_WIDGET(dcal->cal_drawing_area));
919     state_flags = gtk_style_context_get_state (stylectxt);
920 
921     gtk_style_context_add_class (stylectxt, GTK_STYLE_CLASS_BACKGROUND);
922     gtk_style_context_add_class (stylectxt, GTK_STYLE_CLASS_CALENDAR);
923 
924     gtk_render_background (stylectxt, cr, 0, 0,
925                            cairo_image_surface_get_width (dcal->surface),
926                            cairo_image_surface_get_height (dcal->surface));
927 
928     gtk_style_context_remove_class (stylectxt, GTK_STYLE_CLASS_BACKGROUND);
929 
930     /* get the colors */
931     {
932          GdkRGBA color;
933          gchar *class_extension = NULL;
934 
935          gtk_style_context_get_color (stylectxt, GTK_STATE_FLAG_NORMAL, &color);
936 
937           if (gnc_is_dark_theme (&color))
938               class_extension = "-dark";
939 
940           primary_color_class = g_strconcat ("primary", class_extension, NULL);
941           secondary_color_class = g_strconcat ("secondary", class_extension, NULL);
942           marker_color_class = g_strconcat ("markers", class_extension, NULL);
943     }
944 
945     /* lets confirm text height size */
946     pango_layout_set_text(layout, "S", -1);
947     pango_layout_get_pixel_size(layout, NULL, &dcal->dayLabelHeight);
948 
949     /* Fill in alternating month colors. */
950     {
951         gint i;
952         GdkRectangle *rect;
953         GList *mcList, *mcListIter;
954 
955         /* reset all of the month position offsets. */
956         for (i = 0; i < 12; i++)
957         {
958             dcal->monthPositions[i].x = dcal->monthPositions[i].y = -1;
959         }
960 
961         gtk_style_context_save (stylectxt);
962 
963         /* Paint the weeks for the upcoming N months. */
964         for (i = 0; i < dcal->numMonths; i++)
965         {
966             mcList = NULL;
967             month_coords(dcal, i, &mcList);
968             dcal->monthPositions[i].x
969             = floor(i / dcal->monthsPerCol)
970               * (col_width(dcal) + COL_BORDER_SIZE);
971             dcal->monthPositions[i].y = ((GdkRectangle*)mcList->next->next->next->data)->y;
972             for (mcListIter = mcList; mcListIter != NULL; mcListIter = mcListIter->next)
973             {
974                 rect = (GdkRectangle*)mcListIter->data;
975                 gtk_style_context_save (stylectxt);
976 
977                 if (i % 2 == 0)
978                     gtk_style_context_add_class (stylectxt, primary_color_class);
979                 else
980                     gtk_style_context_add_class (stylectxt, secondary_color_class);
981 
982                 gtk_render_background (stylectxt, cr, rect->x, rect->y, rect->width, rect->height);
983                 gtk_style_context_restore (stylectxt);
984             }
985             g_list_foreach(mcList, free_rect, NULL);
986             g_list_free(mcList);
987         }
988         gtk_style_context_restore (stylectxt);
989     }
990     LOG_AND_RESET(timer, "alternating month colors");
991 
992 
993     /* Highlight the marked days. */
994     {
995         int i;
996         int x1, x2, y1, y2;
997 
998         gtk_style_context_save (stylectxt);
999         gtk_style_context_add_class (stylectxt, marker_color_class);
1000         gtk_style_context_add_class (stylectxt, GTK_STYLE_CLASS_VIEW);
1001         gtk_style_context_set_state (stylectxt, GTK_STATE_FLAG_SELECTED);
1002 
1003         for (i = 0; i < dcal->numMarks; i++)
1004         {
1005             if (dcal->marks[i] != NULL)
1006             {
1007                 int center_x, center_y, radius;
1008 
1009                 doc_coords(dcal, i, &x1, &y1, &x2, &y2);
1010                 center_x = (x1 + x2 ) / 2;
1011                 center_y = (y1 + y2 ) / 2;
1012                 radius = MIN((x2 - x1), (y2 - y1)) * .75;
1013 
1014                 // try to compensate for row height/width being odd or even
1015                 if (((y2 - y1) % 2) != 0)
1016                     center_y = center_y + 1;
1017 
1018                 if (((x2 - x1) % 2) != 0)
1019                     center_x = center_x + 1;
1020 
1021                 gtk_render_background (stylectxt, cr,
1022                                        center_x - (radius + 2), center_y - radius,
1023                                         (radius * 2) + 4, radius * 2);
1024             }
1025         }
1026         gtk_style_context_restore (stylectxt);
1027     }
1028     LOG_AND_RESET(timer, "marked days");
1029 
1030     for (i = 0; i < num_cols(dcal); i++)
1031     {
1032         GdkRGBA color;
1033         gint x, y, w, h;
1034         gint j;
1035 
1036         cairo_save (cr);
1037         gdk_rgba_parse (&color, "black");
1038 
1039         x = dcal->leftPadding
1040             + (i * (col_width(dcal) + COL_BORDER_SIZE))
1041             + dcal->label_height + 1;
1042         y = dcal->topPadding + dcal->dayLabelHeight;
1043         w = col_width(dcal) - COL_BORDER_SIZE - dcal->label_width;
1044         h = col_height(dcal);
1045 
1046         gtk_style_context_save (stylectxt);
1047 
1048         /* draw the outside border [inside the month labels] */
1049         gtk_style_context_add_class (stylectxt, GTK_STYLE_CLASS_FRAME);
1050 
1051         gtk_render_frame (stylectxt, cr, x, y, w + 1, h + 1);
1052 
1053         gnc_style_context_get_border_color (stylectxt, state_flags, &color);
1054         cairo_set_source_rgb (cr, color.red, color.green, color.blue);
1055         cairo_set_line_width (cr, 1);
1056 
1057         /* draw the week separations */
1058         for (j = 0; j < num_weeks_per_col(dcal); j++)
1059         {
1060             gint wy = y + (j * week_height(dcal));
1061             cairo_move_to (cr, x, wy + 0.5);
1062             cairo_line_to (cr, x + w, wy + 0.5);
1063             cairo_stroke (cr);
1064         }
1065 
1066         /* draw the day separations */
1067         for (j = 1; j < 7; j++)
1068         {
1069             gint dx = x + (j * day_width(dcal));
1070             cairo_move_to (cr, dx + 0.5, y);
1071             cairo_line_to (cr, dx + 0.5, y + col_height(dcal));
1072             cairo_stroke (cr);
1073         }
1074         cairo_restore (cr);
1075         gtk_style_context_restore (stylectxt);
1076 
1077 
1078         /* draw the day of the week labels */
1079         pango_layout_set_text(layout, "88", -1);
1080         pango_layout_get_pixel_size(layout, &maxWidth, NULL);
1081 
1082         if (dcal->x_scale > maxWidth)
1083         {
1084             gtk_style_context_save (stylectxt);
1085             gtk_style_context_add_class (stylectxt, GTK_STYLE_CLASS_HEADER);
1086 
1087             gtk_render_background (stylectxt, cr, x, y - dcal->dayLabelHeight, (day_width(dcal) * 7) + 1, dcal->dayLabelHeight);
1088 
1089             for (j = 0; j < 7; j++)
1090             {
1091                 int day_label_width;
1092                 gint label_x_offset, label_y_offset;
1093                 gint day_label_str_len = 4;
1094                 gchar day_label_str[day_label_str_len+1];
1095                 day_label(day_label_str, day_label_str_len, (j + dcal->week_starts_monday) % 7);
1096                 pango_layout_set_text(layout, day_label_str, -1);
1097                 pango_layout_get_pixel_size(layout, &day_label_width, NULL);
1098                 label_x_offset = x
1099                                  + (j * day_width(dcal))
1100                                  + (day_width(dcal) / 2)
1101                                  - (day_label_width / 2);
1102                 label_y_offset = y - dcal->dayLabelHeight;
1103                 pango_layout_set_text(layout, day_label_str, -1);
1104                 gtk_render_layout (stylectxt, cr, label_x_offset, label_y_offset, layout);
1105             }
1106             gtk_style_context_restore (stylectxt);
1107         }
1108     }
1109     LOG_AND_RESET(timer, "lines and labels");
1110 
1111 
1112     /* Month labels. */
1113     {
1114         gint i;
1115         gint x_offset = dcal->leftPadding;
1116 
1117         gtk_style_context_save (stylectxt);
1118         gtk_style_context_add_class (stylectxt, GTK_STYLE_CLASS_HEADER);
1119 
1120         for (i = 0; i < 12; i++)
1121         {
1122             if (dcal->monthPositions[i].x == -1)
1123                 break;
1124 
1125             gtk_render_background (stylectxt, cr, dcal->monthPositions[i].x + x_offset, dcal->topPadding,
1126                                    dcal->dayLabelHeight + 1, col_height(dcal) + dcal->dayLabelHeight + 1);
1127         }
1128 
1129         for (i = 0; i < 12; i++)
1130         {
1131             guint idx;
1132 
1133             if (dcal->monthPositions[i].x == -1)
1134                 break;
1135             idx = (dcal->month - 1 + i) % 12;
1136             pango_layout_set_text(layout, month_name(idx), -1);
1137             cairo_save (cr);
1138             cairo_translate (cr, dcal->monthPositions[i].x + x_offset, dcal->monthPositions[i].y);
1139             cairo_rotate (cr, -G_PI / 2.);
1140             gtk_render_layout (stylectxt, cr, 0, 0, layout);
1141             cairo_restore (cr);
1142         }
1143         gtk_style_context_restore (stylectxt);
1144     }
1145     LOG_AND_RESET(timer, "month labels");
1146 
1147 
1148     /* Day number strings [dates] */
1149     {
1150         GDate d, eoc;
1151         gint doc;
1152         gchar dayNumBuf[4];
1153         gint numW, numH;
1154         gint x1, y1, x2, y2, w, h;
1155 
1156         gtk_style_context_save (stylectxt);
1157         gtk_style_context_add_class (stylectxt, "day-number");
1158 
1159         cairo_save (cr);
1160         g_date_set_dmy(&d, 1, dcal->month, dcal->year);
1161         eoc = d;
1162         g_date_add_months(&eoc, dcal->numMonths);
1163         for (doc = 0; g_date_get_julian(&d) < g_date_get_julian(&eoc); g_date_add_days(&d, 1), doc++)
1164         {
1165             doc_coords(dcal, doc, &x1, &y1, &x2, &y2);
1166             memset(dayNumBuf, 0, 4);
1167             snprintf(dayNumBuf, 4, "%d", g_date_get_day(&d));
1168             pango_layout_set_text(layout, dayNumBuf, -1);
1169             pango_layout_get_pixel_size(layout, &numW, &numH);
1170             w = (x2 - x1) + 1;
1171             h = (y2 - y1) + 1;
1172             gtk_render_layout (stylectxt, cr, x1 + (w / 2) - (numW / 2), y1 + (h / 2) - (numH / 2), layout);
1173         }
1174         cairo_restore (cr);
1175         gtk_style_context_restore (stylectxt);
1176     }
1177     LOG_AND_RESET(timer, "dates");
1178 
1179     gtk_widget_get_allocation (widget, &alloc);
1180     gtk_widget_queue_draw_area(GTK_WIDGET(dcal),
1181                                alloc.x,
1182                                alloc.y,
1183                                alloc.width,
1184                                alloc.height);
1185 
1186     LOG_AND_RESET(timer, "queue draw");
1187 
1188     g_free (primary_color_class);
1189     g_free (secondary_color_class);
1190     g_free (marker_color_class);
1191 
1192     g_object_unref(layout);
1193     cairo_destroy (cr);
1194 
1195     g_timer_destroy(timer);
1196 }
1197 
1198 static void
populate_hover_window(GncDenseCal * dcal)1199 populate_hover_window(GncDenseCal *dcal)
1200 {
1201     GtkWidget *w;
1202     GDate *date;
1203     static const int MAX_STRFTIME_BUF_LEN = 64;
1204     gchar strftimeBuf[MAX_STRFTIME_BUF_LEN];
1205 
1206     if (dcal->doc >= 0)
1207     {
1208         GObject *o;
1209         GtkListStore *model;
1210         GList *l;
1211 
1212         w = GTK_WIDGET(g_object_get_data(G_OBJECT(dcal->transPopup), "dateLabel"));
1213         date = g_date_new_dmy(1, dcal->month, dcal->year);
1214         g_date_add_days(date, dcal->doc);
1215         /* Note: the ISO date format (%F or equivalently
1216          * %Y-%m-%d) is not a good idea here since many
1217          * locales will want to use a very different date
1218          * format. Please leave the specification of the date
1219          * format up to the locale and use %x here.  */
1220         g_date_strftime(strftimeBuf, MAX_STRFTIME_BUF_LEN - 1, "%x", date);
1221         gtk_label_set_text(GTK_LABEL(w), strftimeBuf);
1222 
1223         o = G_OBJECT(dcal->transPopup);
1224         model = GTK_LIST_STORE(g_object_get_data(o, "model"));
1225         gtk_list_store_clear(model);
1226         for (l = dcal->marks[dcal->doc]; l; l = l->next)
1227         {
1228             GtkTreeIter iter;
1229             gdc_mark_data *gdcmd;
1230 
1231             gdcmd = (gdc_mark_data*)l->data;
1232             gtk_list_store_insert(model, &iter, INT_MAX);
1233             gtk_list_store_set(model, &iter, 0, (gdcmd->name ? gdcmd->name : _("(unnamed)")), 1, gdcmd->info, -1);
1234         }
1235 
1236         // if there are no rows, add one
1237         if (gtk_tree_model_iter_n_children (GTK_TREE_MODEL(model), NULL) == 0)
1238         {
1239             GtkTreeIter iter;
1240             gtk_list_store_insert(model, &iter, -1);
1241         }
1242 
1243         // make sure all pending events are processed
1244         while(gtk_events_pending())
1245             gtk_main_iteration();
1246 
1247         g_date_free(date);
1248     }
1249 }
1250 
1251 static gint
gnc_dense_cal_button_press(GtkWidget * widget,GdkEventButton * evt)1252 gnc_dense_cal_button_press(GtkWidget *widget,
1253                            GdkEventButton *evt)
1254 {
1255     GdkWindow *win = gdk_screen_get_root_window (gtk_widget_get_screen (widget));
1256     GdkMonitor *mon = gdk_display_get_monitor_at_window (gtk_widget_get_display (widget), win);
1257     GdkRectangle work_area_size;
1258     GtkAllocation alloc;
1259     GncDenseCal *dcal = GNC_DENSE_CAL(widget);
1260     gint win_xpos = evt->x_root + 5;
1261     gint win_ypos = evt->y_root + 5;
1262 
1263     gdk_monitor_get_workarea (mon, &work_area_size);
1264 
1265     dcal->screen_width = work_area_size.width;
1266     dcal->screen_height = work_area_size.height;
1267 
1268     dcal->doc = wheres_this(dcal, evt->x, evt->y);
1269     dcal->showPopup = ~(dcal->showPopup);
1270     if (dcal->showPopup && dcal->doc >= 0)
1271     {
1272         // Do the move twice in case the WM is ignoring the first one
1273         // because the window hasn't been shown, yet.  The WM is free
1274         // to ignore our move and place windows according to it's own
1275         // strategy, but hopefully it'll listen to us.  Certainly the
1276         // second move after show_all'ing the window should do the
1277         // trick with a bit of flicker.
1278         gtk_window_move(GTK_WINDOW(dcal->transPopup), evt->x_root + 5, evt->y_root + 5);
1279 
1280         populate_hover_window(dcal);
1281         gtk_widget_queue_resize(GTK_WIDGET(dcal->transPopup));
1282         gtk_widget_show_all(GTK_WIDGET(dcal->transPopup));
1283 
1284         gtk_widget_get_allocation(GTK_WIDGET(dcal->transPopup), &alloc);
1285 
1286         if (evt->x_root + 5 + alloc.width > dcal->screen_width)
1287             win_xpos = evt->x_root - 2 - alloc.width;
1288 
1289         if (evt->y_root + 5 + alloc.height > dcal->screen_height)
1290             win_ypos = evt->y_root - 2 - alloc.height;
1291 
1292         gtk_window_move(GTK_WINDOW(dcal->transPopup), win_xpos, win_ypos);
1293     }
1294     else
1295     {
1296         dcal->doc = -1;
1297         gtk_widget_hide(GTK_WIDGET(dcal->transPopup));
1298     }
1299     return TRUE;
1300 }
1301 
1302 static gint
gnc_dense_cal_motion_notify(GtkWidget * widget,GdkEventMotion * event)1303 gnc_dense_cal_motion_notify(GtkWidget *widget,
1304                             GdkEventMotion *event)
1305 {
1306     GncDenseCal *dcal;
1307     GtkAllocation alloc;
1308     gint doc;
1309     int unused;
1310     GdkModifierType unused2;
1311     gint win_xpos = event->x_root + 5;
1312     gint win_ypos = event->y_root + 5;
1313 
1314     dcal = GNC_DENSE_CAL(widget);
1315     if (!dcal->showPopup)
1316         return FALSE;
1317 
1318     /* As per https://www.gtk.org/tutorial/sec-eventhandling.html */
1319     if (event->is_hint)
1320     {
1321         GdkSeat *seat = gdk_display_get_default_seat (gdk_window_get_display (event->window));
1322         GdkDevice *pointer = gdk_seat_get_pointer (seat);
1323 
1324         gdk_window_get_device_position (event->window, pointer,  &unused,  &unused, &unused2);
1325     }
1326 
1327     doc = wheres_this(dcal, event->x, event->y);
1328     if (doc >= 0)
1329     {
1330         if (dcal->doc != doc) // if we are on the same day, no need to reload
1331         {
1332             dcal->doc = doc;
1333             populate_hover_window(dcal);
1334             gtk_widget_queue_resize(GTK_WIDGET(dcal->transPopup));
1335             gtk_widget_show_all(GTK_WIDGET(dcal->transPopup));
1336         }
1337         gtk_widget_get_allocation(GTK_WIDGET(dcal->transPopup), &alloc);
1338 
1339         if (event->x_root + 5 + alloc.width > dcal->screen_width)
1340             win_xpos = event->x_root - 2 - alloc.width;
1341 
1342         if (event->y_root + 5 + alloc.height > dcal->screen_height)
1343             win_ypos = event->y_root - 2 - alloc.height;
1344 
1345         gtk_window_move(GTK_WINDOW(dcal->transPopup), win_xpos, win_ypos);
1346     }
1347     else
1348     {
1349         dcal->doc = -1;
1350         gtk_widget_hide(GTK_WIDGET(dcal->transPopup));
1351     }
1352     return TRUE;
1353 }
1354 
1355 
1356 static void
_gdc_view_option_changed(GtkComboBox * widget,gpointer user_data)1357 _gdc_view_option_changed(GtkComboBox *widget, gpointer user_data)
1358 {
1359     GtkTreeIter iter;
1360     GtkTreeModel *model;
1361     gint months_val;
1362 
1363     model = GTK_TREE_MODEL(gtk_combo_box_get_model(widget));
1364     if (!gtk_combo_box_get_active_iter(widget, &iter))
1365         return;
1366     gtk_tree_model_get(model, &iter, VIEW_OPTS_COLUMN_NUM_MONTHS, &months_val, -1);
1367     DEBUG("changing to %d months", months_val);
1368     gnc_dense_cal_set_num_months(GNC_DENSE_CAL(user_data), months_val);
1369 }
1370 
1371 static inline int
day_width_at(GncDenseCal * dcal,guint xScale)1372 day_width_at(GncDenseCal *dcal, guint xScale)
1373 {
1374     return xScale + MINOR_BORDER_SIZE;
1375 }
1376 
1377 static inline int
day_width(GncDenseCal * dcal)1378 day_width(GncDenseCal *dcal)
1379 {
1380     return day_width_at(dcal, dcal->x_scale);
1381 }
1382 
1383 static inline int
day_height_at(GncDenseCal * dcal,guint yScale)1384 day_height_at(GncDenseCal *dcal, guint yScale)
1385 {
1386     return yScale + MINOR_BORDER_SIZE;
1387 }
1388 
1389 static inline int
day_height(GncDenseCal * dcal)1390 day_height(GncDenseCal *dcal)
1391 {
1392     return day_height_at(dcal, dcal->y_scale);
1393 }
1394 
1395 static inline int
week_width_at(GncDenseCal * dcal,guint xScale)1396 week_width_at(GncDenseCal *dcal, guint xScale)
1397 {
1398     return day_width_at(dcal, xScale) * 7;
1399 }
1400 
1401 static inline int
week_width(GncDenseCal * dcal)1402 week_width(GncDenseCal *dcal)
1403 {
1404     return week_width_at(dcal, dcal->x_scale);
1405 }
1406 
1407 static inline int
week_height_at(GncDenseCal * dcal,guint yScale)1408 week_height_at(GncDenseCal *dcal, guint yScale)
1409 {
1410     return day_height_at(dcal, yScale);
1411 }
1412 
1413 static inline int
week_height(GncDenseCal * dcal)1414 week_height(GncDenseCal *dcal)
1415 {
1416     return week_height_at(dcal, dcal->y_scale);
1417 }
1418 
1419 static inline int
col_width_at(GncDenseCal * dcal,guint xScale)1420 col_width_at(GncDenseCal *dcal, guint xScale)
1421 {
1422     return (week_width_at(dcal, xScale)
1423             + dcal->label_width
1424             + COL_BORDER_SIZE);
1425 }
1426 
1427 static inline int
col_width(GncDenseCal * dcal)1428 col_width(GncDenseCal *dcal)
1429 {
1430     return col_width_at(dcal, dcal->x_scale);
1431 }
1432 
1433 static inline int
col_height(GncDenseCal * dcal)1434 col_height(GncDenseCal *dcal)
1435 {
1436     return week_height(dcal) * num_weeks_per_col(dcal);
1437 }
1438 
1439 static inline int
num_cols(GncDenseCal * dcal)1440 num_cols(GncDenseCal *dcal)
1441 {
1442     return ceil((float)dcal->numMonths / (float)dcal->monthsPerCol);
1443 }
1444 
1445 static inline int
num_weeks(GncDenseCal * dcal)1446 num_weeks(GncDenseCal *dcal)
1447 {
1448     return dcal->num_weeks;
1449 }
1450 
1451 static
num_weeks_per_col(GncDenseCal * dcal)1452 int num_weeks_per_col(GncDenseCal *dcal)
1453 {
1454     int num_weeks_toRet, numCols, i;
1455     GDate *start, *end;
1456     int startWeek, endWeek;
1457 
1458     start = g_date_new();
1459     end = g_date_new();
1460 
1461     num_weeks_toRet = 0;
1462     numCols = num_cols(dcal);
1463 
1464     for (i = 0; i < numCols; i++)
1465     {
1466         g_date_set_dmy(start, 1,
1467                        ((dcal->month - 1
1468                          + (i * dcal->monthsPerCol)) % 12)
1469                        + 1,
1470                        dcal->year + floor((dcal->month - 1
1471                                            + (i * dcal->monthsPerCol))
1472                                           / 12));
1473         *end = *start;
1474         /* Add the smaller of (the number of months in the
1475          * calendar-display, minus the number of months shown in the
1476          * previous columns) or (the number of months in a column) */
1477         g_date_add_months(end, MIN(dcal->numMonths,
1478                                    MIN(dcal->monthsPerCol,
1479                                        dcal->numMonths
1480                                        - ((i - 1)
1481                                           * dcal->monthsPerCol))));
1482         g_date_subtract_days(end, 1);
1483         startWeek = (dcal->week_starts_monday
1484                      ? g_date_get_monday_week_of_year(start)
1485                      : g_date_get_sunday_week_of_year(start));
1486         endWeek = (dcal->week_starts_monday
1487                    ? g_date_get_monday_week_of_year(end)
1488                    : g_date_get_sunday_week_of_year(end));
1489         if (endWeek < startWeek)
1490         {
1491             endWeek += (dcal->week_starts_monday
1492                         ? g_date_get_monday_weeks_in_year(g_date_get_year(start))
1493                         : g_date_get_sunday_weeks_in_year(g_date_get_year(start)));
1494         }
1495         num_weeks_toRet = MAX(num_weeks_toRet, (endWeek - startWeek) + 1);
1496     }
1497     g_date_free(start);
1498     g_date_free(end);
1499     return num_weeks_toRet;
1500 }
1501 
1502 /**
1503  * @param monthOfCal 0-based; offset of calendar's first month.
1504  * @param outList A GList in which to place GdkRectangle's of the extents of
1505  * each week.  4 or 5 GdkRectangle*s will be added to the list, as per the
1506  * size of the month.
1507  **/
1508 static void
month_coords(GncDenseCal * dcal,int monthOfCal,GList ** outList)1509 month_coords(GncDenseCal *dcal, int monthOfCal, GList **outList)
1510 {
1511     gint weekRow, colNum, previousMonthsInCol, monthOffset;
1512     gint start;
1513     GDate *startD, *endD;
1514     GdkRectangle *rect;
1515     gint startWk, endWk;
1516 
1517     if (monthOfCal > dcal->numMonths)
1518         return;
1519 
1520     colNum = floor(monthOfCal / dcal->monthsPerCol);
1521     monthOffset = colNum * dcal->monthsPerCol;
1522     previousMonthsInCol = MAX(0, (monthOfCal % dcal->monthsPerCol));
1523 
1524     startD = g_date_new();
1525     endD = g_date_new();
1526 
1527     /* Calculate the number of weeks in the column before the month we're
1528      * interested in. */
1529     weekRow = 0;
1530     if (previousMonthsInCol > 0)
1531     {
1532         g_date_set_dmy(startD, 1,
1533                        ((dcal->month - 1 + monthOffset) % 12) + 1,
1534                        dcal->year + floor((dcal->month - 1 + monthOffset) / 12));
1535         /* get the week of the top of the column */
1536         startWk = (dcal->week_starts_monday
1537                    ? g_date_get_monday_week_of_year(startD)
1538                    : g_date_get_sunday_week_of_year(startD));
1539         /* get the week of the end of the previous months */
1540         *endD = *startD;
1541         g_date_add_months(endD, previousMonthsInCol);
1542         g_date_subtract_days(endD, 1);
1543         endWk = (dcal->week_starts_monday
1544                  ? g_date_get_monday_week_of_year(endD)
1545                  : g_date_get_sunday_week_of_year(endD));
1546         if (endWk < startWk)
1547         {
1548             endWk += (dcal->week_starts_monday
1549                       ? g_date_get_monday_weeks_in_year(g_date_get_year(startD))
1550                       : g_date_get_sunday_weeks_in_year(g_date_get_year(startD)));
1551         }
1552         /* determine how many weeks are before the month we're
1553          * interested in. */
1554         weekRow = endWk - startWk;
1555         if (g_date_get_weekday(endD) == (dcal->week_starts_monday ? G_DATE_SUNDAY : G_DATE_SATURDAY))
1556         {
1557             weekRow++;
1558         }
1559     }
1560 
1561     g_date_set_dmy(startD, 1,
1562                    ((dcal->month - 1 + monthOfCal) % 12) + 1,
1563                    dcal->year + floor((dcal->month - 1 + monthOfCal) / 12));
1564     *endD = *startD;
1565     g_date_add_months(endD, 1);
1566     g_date_subtract_days(endD, 1);
1567     /* Get the first week. */
1568     {
1569         start = (g_date_get_weekday(startD) - dcal->week_starts_monday) % 7;
1570         rect = g_new0(GdkRectangle, 1);
1571         rect->x = dcal->leftPadding
1572                   + MINOR_BORDER_SIZE
1573                   + (colNum * (col_width(dcal) + COL_BORDER_SIZE))
1574                   + dcal->label_height
1575                   + (start * day_width(dcal));
1576         rect->y = dcal->topPadding
1577                   + dcal->dayLabelHeight
1578                   + MINOR_BORDER_SIZE
1579                   + (weekRow * week_height(dcal));
1580         rect->width = (7 - start) * day_width(dcal);
1581         rect->height = week_height(dcal);
1582         *outList = g_list_append(*outList, (gpointer)rect);
1583         rect = NULL;
1584     }
1585 
1586     /* Get the middle weeks. */
1587     {
1588         gint i, weekStart, weekEnd;
1589 
1590         weekStart = (dcal->week_starts_monday
1591                      ? g_date_get_monday_week_of_year(startD)
1592                      : g_date_get_sunday_week_of_year(startD)) + 1;
1593         weekEnd = (dcal->week_starts_monday
1594                    ? g_date_get_monday_week_of_year(endD)
1595                    : g_date_get_sunday_week_of_year(endD));
1596         for (i = weekStart; i < weekEnd; i++)
1597         {
1598             rect = g_new0(GdkRectangle, 1);
1599             rect->x = dcal->leftPadding
1600                       + MINOR_BORDER_SIZE
1601                       + dcal->label_height
1602                       + (colNum * (col_width(dcal) + COL_BORDER_SIZE));
1603             rect->y = dcal->topPadding
1604                       + dcal->dayLabelHeight
1605                       + MINOR_BORDER_SIZE
1606                       + ((weekRow + (i - weekStart) + 1) * week_height(dcal));
1607             rect->width  = week_width(dcal);
1608             rect->height = week_height(dcal);
1609 
1610             *outList = g_list_append(*outList, (gpointer)rect);
1611             rect = NULL;
1612         }
1613     }
1614 
1615     /* Get the last week. */
1616     {
1617         gint end_week_of_year = g_date_get_sunday_week_of_year(endD);
1618         gint start_week_of_year = g_date_get_sunday_week_of_year(startD);
1619         if (dcal->week_starts_monday == 1)
1620         {
1621             end_week_of_year = g_date_get_monday_week_of_year(endD);
1622             start_week_of_year = g_date_get_monday_week_of_year(startD);
1623         }
1624 
1625         rect = g_new0(GdkRectangle, 1);
1626         rect->x = dcal->leftPadding
1627                   + MINOR_BORDER_SIZE
1628                   + dcal->label_height
1629                   + (colNum * (col_width(dcal) + COL_BORDER_SIZE));
1630         rect->y = dcal->topPadding
1631                   + MINOR_BORDER_SIZE
1632                   + dcal->dayLabelHeight
1633                   + ((weekRow
1634                       + (end_week_of_year - start_week_of_year))
1635                      * week_height(dcal));
1636         rect->width = (((g_date_get_weekday(endD) - dcal->week_starts_monday) % 7) + 1) * day_width(dcal);
1637         rect->height = week_height(dcal);
1638 
1639         *outList = g_list_append(*outList, (gpointer)rect);
1640         rect = NULL;
1641     }
1642 
1643     g_date_free(startD);
1644     g_date_free(endD);
1645 }
1646 
1647 /* FIXME: make this more like month_coords */
1648 static void
doc_coords(GncDenseCal * dcal,int dayOfCal,int * x1,int * y1,int * x2,int * y2)1649 doc_coords(GncDenseCal *dcal, int dayOfCal,
1650            int *x1, int *y1, int *x2, int *y2)
1651 {
1652     GDate d;
1653     gint docMonth;
1654     gint d_week_of_cal, top_of_col_week_of_cal;
1655     gint colNum, dayCol, weekRow;
1656 
1657     /* FIXME: add range checks */
1658     g_date_set_dmy(&d, 1, dcal->month, dcal->year);
1659     g_date_add_days(&d, dayOfCal);
1660     docMonth = g_date_get_month(&d);
1661     if (g_date_get_year(&d) != dcal->year)
1662     {
1663         docMonth += 12;
1664     }
1665     colNum  = floor((float)(docMonth - dcal->month) / (float)dcal->monthsPerCol);
1666     dayCol  = (g_date_get_weekday(&d) - dcal->week_starts_monday) % 7;
1667     d_week_of_cal = g_date_get_sunday_week_of_year(&d);
1668     if (dcal->week_starts_monday == 1)
1669     {
1670         d_week_of_cal = g_date_get_monday_week_of_year(&d);
1671     }
1672     g_date_set_dmy(&d, 1, dcal->month, dcal->year);
1673     g_date_add_months(&d, (colNum * dcal->monthsPerCol));
1674     top_of_col_week_of_cal = (dcal->week_starts_monday
1675                               ? g_date_get_monday_week_of_year(&d)
1676                               : g_date_get_sunday_week_of_year(&d));
1677     if (d_week_of_cal < top_of_col_week_of_cal)
1678     {
1679         gint week_offset;
1680         week_offset = g_date_get_sunday_weeks_in_year(dcal->year);
1681         if (dcal->week_starts_monday == 1)
1682         {
1683             week_offset = g_date_get_monday_weeks_in_year(dcal->year);
1684         }
1685         d_week_of_cal += week_offset;
1686     }
1687     weekRow = d_week_of_cal - top_of_col_week_of_cal;
1688 
1689     /* top-left corner */
1690     /* FIXME: this has the math to make the mark-cells come out right,
1691      * which it shouldn't. */
1692     *x1 = dcal->leftPadding
1693           + MINOR_BORDER_SIZE
1694           + dcal->label_height
1695           + (colNum * (col_width(dcal) + COL_BORDER_SIZE))
1696           + (dayCol * day_width(dcal))
1697           + (day_width(dcal) / 4);
1698     *y1 = dcal->topPadding
1699           + MINOR_BORDER_SIZE
1700           + dcal->dayLabelHeight
1701           + (weekRow * week_height(dcal))
1702           + (day_height(dcal) / 4);
1703 
1704     *x2 = *x1 + (day_width(dcal) / 2);
1705     *y2 = *y1 + (day_height(dcal) / 2);
1706 }
1707 
1708 /**
1709  * Given x,y coordinates, returns the day-of-cal under the mouse; will return
1710  * '-1' if invalid.
1711  **/
1712 static gint
wheres_this(GncDenseCal * dcal,int x,int y)1713 wheres_this(GncDenseCal *dcal, int x, int y)
1714 {
1715     gint colNum, weekRow, dayCol, dayOfCal;
1716     GDate d, startD;
1717     GtkAllocation alloc;
1718 
1719     x -= dcal->leftPadding;
1720     y -= dcal->topPadding;
1721 
1722     if ((x < 0) || (y < 0))
1723     {
1724         return -1;
1725     }
1726     gtk_widget_get_allocation (GTK_WIDGET(dcal), &alloc);
1727     if ((x >= alloc.width)
1728             || (y >= alloc.height))
1729     {
1730         return -1;
1731     }
1732 
1733     /* "outside of displayed table" check */
1734     if (x >= (num_cols(dcal) * (col_width(dcal) + COL_BORDER_SIZE)))
1735     {
1736         return -1;
1737     }
1738     if (y >= dcal->dayLabelHeight + col_height(dcal))
1739     {
1740         return -1;
1741     }
1742 
1743     /* coords -> year-relative-values */
1744     colNum = floor(x / (col_width(dcal) + COL_BORDER_SIZE));
1745 
1746     x %= (col_width(dcal) + COL_BORDER_SIZE);
1747     x -= dcal->label_width;
1748     if (x < 0)
1749     {
1750         return -1;
1751     }
1752     if (x >= day_width(dcal) * 7)
1753     {
1754         return -1;
1755     }
1756 
1757     y -= dcal->dayLabelHeight;
1758     if (y < 0)
1759     {
1760         return -1;
1761     }
1762 
1763     dayCol = floor((float)x / (float)day_width(dcal));
1764     weekRow = floor((float)y / (float)week_height(dcal));
1765 
1766     g_date_set_dmy(&startD, 1, dcal->month, dcal->year);
1767     d = startD;
1768     g_date_add_months(&d, (colNum * dcal->monthsPerCol));
1769     dayCol -= ((g_date_get_weekday(&d) - dcal->week_starts_monday) % 7);
1770     if (weekRow == 0)
1771     {
1772         if (dayCol < 0)
1773         {
1774             return -1;
1775         }
1776     }
1777     g_date_add_days(&d, dayCol + (weekRow * 7));
1778 
1779     /* Check to make sure we're within the column's displayed range. */
1780     {
1781         GDate ccd;
1782         g_date_set_dmy(&ccd, 1, dcal->month, dcal->year);
1783         g_date_add_months(&ccd, (colNum + 1) * dcal->monthsPerCol);
1784         if (g_date_get_julian(&d) >= g_date_get_julian(&ccd))
1785         {
1786             return -1;
1787         }
1788     }
1789 
1790     dayOfCal = g_date_get_julian(&d) - g_date_get_julian(&startD);
1791 
1792     /* one more check before returning... */
1793     g_date_subtract_months(&d, dcal->numMonths);
1794     if (g_date_get_julian(&d) >= g_date_get_julian(&startD))
1795     {
1796         /* we're past the end of the displayed calendar, thus -1 */
1797         DEBUG("%d >= %d", g_date_get_julian(&d), g_date_get_julian(&startD));
1798         return -1;
1799     }
1800 
1801     return dayOfCal;
1802 }
1803 
1804 static gint
gdc_get_doc_offset(GncDenseCal * dcal,GDate * d)1805 gdc_get_doc_offset(GncDenseCal *dcal, GDate *d)
1806 {
1807     gint toRet;
1808     /* soc == start-of-calendar */
1809     GDate soc;
1810 
1811     g_date_clear(&soc, 1);
1812     g_date_set_dmy(&soc, 1, dcal->month, dcal->year);
1813     /* ensure not before calendar start. */
1814     if (g_date_get_julian(d) < g_date_get_julian(&soc))
1815         return -1;
1816     /* do computation here, since we're going to change the
1817      * start-of-calendar date. */
1818     toRet = g_date_get_julian(d) - g_date_get_julian(&soc);
1819     /* ensure not after end of visible calendar. */
1820     g_date_add_months(&soc, dcal->numMonths);
1821     if (g_date_get_julian(d) >= g_date_get_julian(&soc))
1822         return -1;
1823     /* return pre-computed value. */
1824     return toRet;
1825 }
1826 
1827 static void
gdc_add_tag_markings(GncDenseCal * cal,guint tag)1828 gdc_add_tag_markings(GncDenseCal *cal, guint tag)
1829 {
1830     gchar *name, *info;
1831     gint num_marks, idx;
1832     GDate **dates;
1833     GDate *calDate;
1834 
1835     // copy the values into the old marking function.
1836     name = gnc_dense_cal_model_get_name(cal->model, tag);
1837     info = gnc_dense_cal_model_get_info(cal->model, tag);
1838     num_marks = gnc_dense_cal_model_get_instance_count(cal->model, tag);
1839 
1840     if (num_marks == 0)
1841         goto cleanup;
1842 
1843     dates = g_new0(GDate*, num_marks);
1844     calDate = g_date_new_dmy(1, cal->month, cal->year);
1845 
1846     for (idx = 0; idx < num_marks; idx++)
1847     {
1848         dates[idx] = g_date_new();
1849         gnc_dense_cal_model_get_instance(cal->model, tag, idx, dates[idx]);
1850 
1851     }
1852     if (g_date_valid(dates[0]))
1853     {
1854         if (g_date_get_julian(dates[0]) < g_date_get_julian(calDate))
1855         {
1856             /* Oops, first marking is earlier than months displayed.
1857              * Choose new first month and recalculate all markings for all
1858              * tags. Their offsets are all wrong with the newly added month(s).
1859              */
1860             _gnc_dense_cal_set_month(cal, g_date_get_month(dates[0]), FALSE);
1861             _gnc_dense_cal_set_year(cal, g_date_get_year(dates[0]), FALSE);
1862 
1863             gdc_remove_markings (cal);
1864             gdc_add_markings (cal);
1865         }
1866         else
1867             gdc_mark_add(cal, tag, name, info, num_marks, dates);
1868     }
1869     else
1870     {
1871         g_warning("Bad date, skipped.");
1872     }
1873 
1874     for (idx = 0; idx < num_marks; idx++)
1875     {
1876         g_date_free(dates[idx]);
1877     }
1878     g_free(dates);
1879     g_date_free(calDate);
1880 
1881 cleanup:
1882     g_free(info);
1883 }
1884 
1885 static void
gdc_add_markings(GncDenseCal * cal)1886 gdc_add_markings(GncDenseCal *cal)
1887 {
1888     GList *tags;
1889     tags = gnc_dense_cal_model_get_contained(cal->model);
1890     for (; tags != NULL; tags = tags->next)
1891     {
1892         guint tag = GPOINTER_TO_UINT(tags->data);
1893         gdc_add_tag_markings(cal, tag);
1894     }
1895     g_list_free(tags);
1896 }
1897 
1898 static void
gdc_remove_markings(GncDenseCal * cal)1899 gdc_remove_markings(GncDenseCal *cal)
1900 {
1901     GList *tags;
1902     tags = gnc_dense_cal_model_get_contained(cal->model);
1903     for (; tags != NULL; tags = tags->next)
1904     {
1905         guint tag = GPOINTER_TO_UINT(tags->data);
1906         gdc_mark_remove(cal, tag, FALSE);
1907     }
1908     g_list_free(tags);
1909 }
1910 
1911 static void
gdc_model_added_cb(GncDenseCalModel * model,guint added_tag,gpointer user_data)1912 gdc_model_added_cb(GncDenseCalModel *model, guint added_tag, gpointer user_data)
1913 {
1914     GncDenseCal *cal = GNC_DENSE_CAL(user_data);
1915     DEBUG("gdc_model_added_cb update\n");
1916     gdc_add_tag_markings(cal, added_tag);
1917 }
1918 
1919 static void
gdc_model_update_cb(GncDenseCalModel * model,guint update_tag,gpointer user_data)1920 gdc_model_update_cb(GncDenseCalModel *model, guint update_tag, gpointer user_data)
1921 {
1922     GncDenseCal *cal = GNC_DENSE_CAL (user_data);
1923     gint num_marks = 0;
1924     DEBUG ("gdc_model_update_cb update for tag [%d]\n", update_tag);
1925     num_marks = gnc_dense_cal_model_get_instance_count (cal->model, update_tag);
1926     // We need to redraw if there are no mark, to ensure they're all erased.
1927     gdc_mark_remove (cal, update_tag, num_marks==0);
1928     gdc_add_tag_markings (cal, update_tag);
1929 
1930 }
1931 
1932 static void
gdc_model_removing_cb(GncDenseCalModel * model,guint remove_tag,gpointer user_data)1933 gdc_model_removing_cb(GncDenseCalModel *model, guint remove_tag, gpointer user_data)
1934 {
1935     GncDenseCal *cal = GNC_DENSE_CAL(user_data);
1936     DEBUG("gdc_model_removing_cb update [%d]\n", remove_tag);
1937     gdc_mark_remove(cal, remove_tag, TRUE);
1938 }
1939 
1940 void
gnc_dense_cal_set_model(GncDenseCal * cal,GncDenseCalModel * model)1941 gnc_dense_cal_set_model(GncDenseCal *cal, GncDenseCalModel *model)
1942 {
1943     if (cal->model != NULL)
1944     {
1945         gdc_remove_markings(cal);
1946         g_object_unref(G_OBJECT(cal->model));
1947         cal->model = NULL;
1948     }
1949     cal->model = model;
1950     g_object_ref(G_OBJECT(model));
1951     g_signal_connect(G_OBJECT(cal->model), "added", (GCallback)gdc_model_added_cb, cal);
1952     g_signal_connect(G_OBJECT(cal->model), "update", (GCallback)gdc_model_update_cb, cal);
1953     g_signal_connect(G_OBJECT(cal->model), "removing", (GCallback)gdc_model_removing_cb, cal);
1954 
1955     gdc_add_markings(cal);
1956 }
1957 
1958 /**
1959  * Marks the given array of GDate*s on the calendar with the given name.
1960  **/
1961 static void
gdc_mark_add(GncDenseCal * dcal,guint tag,gchar * name,gchar * info,guint size,GDate ** dateArray)1962 gdc_mark_add(GncDenseCal *dcal,
1963              guint tag,
1964              gchar *name,
1965              gchar *info,
1966              guint size,
1967              GDate **dateArray)
1968 {
1969     guint i;
1970     gint doc;
1971     gdc_mark_data *newMark;
1972     GDate *d;
1973 
1974     if (size == 0)
1975     {
1976         g_error("0 size not allowed\n");
1977         return;
1978     }
1979 
1980     newMark = g_new0(gdc_mark_data, 1);
1981     newMark->name = NULL;
1982     if (name)
1983         newMark->name = g_strdup(name);
1984     newMark->info = NULL;
1985     if (info)
1986         newMark->info = g_strdup(info);
1987     newMark->tag = tag;
1988     newMark->ourMarks = NULL;
1989     DEBUG("saving mark with tag [%d]\n", newMark->tag);
1990 
1991     for (i = 0; i < size; i++)
1992     {
1993         d = dateArray[i];
1994         doc = gdc_get_doc_offset(dcal, d);
1995         if (doc < 0)
1996             continue;
1997         if (doc >= dcal->numMarks)
1998         {
1999             /* It's not going to get any better, so just
2000              * stop processing. */
2001             break;
2002         }
2003         dcal->marks[doc] = g_list_append(dcal->marks[doc], newMark);
2004         newMark->ourMarks = g_list_append(newMark->ourMarks,
2005                                           GINT_TO_POINTER(doc));
2006     }
2007     dcal->markData = g_list_append(dcal->markData, (gpointer)newMark);
2008     gnc_dense_cal_draw_to_buffer(dcal);
2009     gtk_widget_queue_draw(GTK_WIDGET(dcal->cal_drawing_area));
2010 }
2011 
2012 static void
gdc_mark_remove(GncDenseCal * dcal,guint mark_to_remove,gboolean redraw)2013 gdc_mark_remove(GncDenseCal *dcal, guint mark_to_remove, gboolean redraw)
2014 {
2015     GList *iter, *calendar_marks;
2016     gint day_of_cal;
2017     gdc_mark_data *mark_data;
2018 
2019     /* Ignore non-realistic marks */
2020     if ((gint)mark_to_remove == -1)
2021     {
2022         DEBUG("mark_to_remove = -1");
2023         return;
2024     }
2025 
2026     mark_data = NULL;
2027     for (iter = dcal->markData; iter != NULL; iter = iter->next)
2028     {
2029         mark_data = (gdc_mark_data*)iter->data;
2030         if (mark_data->tag == mark_to_remove)
2031             break;
2032     }
2033     if (iter == NULL)
2034     {
2035         PINFO("couldn't find tag [%d]", mark_to_remove);
2036         return;
2037     }
2038     if (mark_data == NULL)
2039     {
2040         DEBUG("mark_data == null");
2041         return;
2042     }
2043 
2044     for (calendar_marks = mark_data->ourMarks; calendar_marks != NULL; calendar_marks = calendar_marks->next)
2045     {
2046         day_of_cal = GPOINTER_TO_INT(calendar_marks->data);
2047         dcal->marks[day_of_cal] = g_list_remove(dcal->marks[day_of_cal], mark_data);
2048     }
2049     g_list_free(mark_data->ourMarks);
2050     dcal->markData = g_list_remove(dcal->markData, mark_data);
2051     g_free(mark_data);
2052 
2053     if (redraw)
2054     {
2055         gnc_dense_cal_draw_to_buffer(dcal);
2056         gtk_widget_queue_draw(GTK_WIDGET(dcal->cal_drawing_area));
2057     }
2058 }
2059