1 /********************************************************************\
2  * This program is free software; you can redistribute it and/or    *
3  * modify it under the terms of the GNU General Public License as   *
4  * published by the Free Software Foundation; either version 2 of   *
5  * the License, or (at your option) any later version.              *
6  *                                                                  *
7  * This program is distributed in the hope that it will be useful,  *
8  * but WITHOUT ANY WARRANTY; without even the implied warranty of   *
9  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the    *
10  * GNU General Public License for more details.                     *
11  *                                                                  *
12  * You should have received a copy of the GNU General Public License*
13  * along with this program; if not, contact:                        *
14  *                                                                  *
15  * Free Software Foundation           Voice:  +1-617-542-5942       *
16  * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652       *
17  * Boston, MA  02110-1301,  USA       gnu@gnu.org                   *
18  *                                                                  *
19 \********************************************************************/
20 
21 /*
22  * The Gnucash Header Canvas
23  *
24  * Authors:
25  *     Heath Martin <martinh@pegasus.cc.ucf.edu>
26  *     Dave Peticolas <dave@krondo.com>
27  */
28 
29 #include <config.h>
30 
31 #include <string.h>
32 
33 #include "gnucash-sheet.h"
34 #include "gnucash-sheetP.h"
35 #include "gnucash-color.h"
36 #include "gnucash-style.h"
37 #include "gnucash-cursor.h"
38 #include "gnucash-item-edit.h"
39 #include "gnc-gtk-utils.h"
40 
41 #include "gnucash-header.h"
42 
43 static GtkLayout *parent_class;
44 
45 enum
46 {
47     PROP_0,
48     PROP_SHEET,       /*  the sheet this header is associated with */
49     PROP_CURSOR_NAME, /* the name of the current cursor */
50 };
51 
52 static void
gnc_header_draw_offscreen(GncHeader * header)53 gnc_header_draw_offscreen (GncHeader *header)
54 {
55     SheetBlockStyle *style = header->style;
56     GncItemEdit *item_edit = GNC_ITEM_EDIT(header->sheet->item_editor);
57     Table *table = header->sheet->table;
58     VirtualLocation virt_loc;
59     VirtualCell *vcell;
60     guint32 color_type;
61     GtkStyleContext *stylectxt = gtk_widget_get_style_context (GTK_WIDGET(header));
62     GdkRGBA color;
63     int row_offset;
64     CellBlock *cb;
65     int i;
66     cairo_t *cr;
67 
68     virt_loc.vcell_loc.virt_row = 0;
69     virt_loc.vcell_loc.virt_col = 0;
70     virt_loc.phys_row_offset = 0;
71     virt_loc.phys_col_offset = 0;
72 
73     gtk_style_context_save (stylectxt);
74 
75     // Get the color type and apply the css class
76     color_type = gnc_table_get_color (table, virt_loc, NULL);
77     gnucash_get_style_classes (header->sheet, stylectxt, color_type, FALSE);
78 
79     if (header->surface)
80         cairo_surface_destroy (header->surface);
81     header->surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
82                                                   header->width,
83                                                   header->height);
84 
85     cr = cairo_create (header->surface);
86 
87     // Fill background color of header
88     gtk_render_background (stylectxt, cr, 0, 0, header->width, header->height);
89 
90     gdk_rgba_parse (&color, "black");
91     cairo_set_source_rgb (cr, color.red, color.green, color.blue);
92     cairo_rectangle (cr, 0.5, 0.5, header->width - 1.0, header->height - 1.0);
93     cairo_set_line_width (cr, 1.0);
94     cairo_stroke (cr);
95 
96     // Draw bottom horizontal line, makes bottom line thicker
97     cairo_move_to (cr, 0.5, header->height - 1.5);
98     cairo_line_to (cr, header->width - 1.0, header->height - 1.5);
99     cairo_set_line_cap (cr, CAIRO_LINE_CAP_SQUARE);
100     cairo_set_line_width (cr, 1.0);
101     cairo_stroke (cr);
102 
103     /*font = gnucash_register_font;*/
104 
105     vcell = gnc_table_get_virtual_cell
106             (table, table->current_cursor_loc.vcell_loc);
107     cb = vcell ? vcell->cellblock : NULL;
108     row_offset = 0;
109 
110     for (i = 0; i < style->nrows; i++)
111     {
112         int col_offset = 0;
113         int height = 0, j;
114         virt_loc.phys_row_offset = i;
115 
116         /* TODO: This routine is duplicated in several places.
117            Can we abstract at least the cell drawing routine?
118            That way we'll be sure everything is drawn
119            consistently, and cut down on maintenance issues. */
120 
121         for (j = 0; j < style->ncols; j++)
122         {
123             CellDimensions *cd;
124             double text_x, text_y, text_w, text_h;
125             BasicCell *cell;
126             const char *text;
127             int width;
128             PangoLayout *layout;
129             PangoRectangle logical_rect;
130             GdkRectangle rect;
131             int x_offset;
132 
133             virt_loc.phys_col_offset = j;
134 
135             cd = gnucash_style_get_cell_dimensions (style, i, j);
136             if (!cd) continue;
137 
138             height = cd->pixel_height;
139             if (header->in_resize && (j == header->resize_col))
140                 width = header->resize_col_width;
141             else
142                 width = cd->pixel_width;
143 
144             cell = gnc_cellblock_get_cell (cb, i, j);
145             if (!cell || !cell->cell_name)
146             {
147                 col_offset += width;
148                 continue;
149             }
150 
151             cairo_rectangle (cr, col_offset - 0.5, row_offset + 0.5, width, height);
152             cairo_set_line_width (cr, 1.0);
153             cairo_stroke (cr);
154 
155             virt_loc.vcell_loc =
156                 table->current_cursor_loc.vcell_loc;
157             text = gnc_table_get_label (table, virt_loc);
158             if (!text)
159                 text = "";
160 
161             layout = gtk_widget_create_pango_layout (GTK_WIDGET(header->sheet), text);
162 
163             pango_layout_get_pixel_extents (layout, NULL, &logical_rect);
164 
165             gnucash_sheet_set_text_bounds (header->sheet, &rect,
166                                            col_offset, row_offset, width, height);
167 
168             cairo_save (cr);
169             cairo_rectangle (cr, rect.x, rect.y, rect.width, rect.height);
170             cairo_clip (cr);
171 
172             x_offset = gnucash_sheet_get_text_offset (header->sheet, virt_loc,
173                                                       rect.width, logical_rect.width);
174 
175             gtk_render_layout (stylectxt, cr, rect.x + x_offset,
176                                rect.y + gnc_item_edit_get_padding_border (item_edit, top), layout);
177 
178             cairo_restore (cr);
179             g_object_unref (layout);
180 
181             col_offset += width;
182         }
183         row_offset += height;
184     }
185     gtk_style_context_restore (stylectxt);
186 
187     cairo_destroy (cr);
188 }
189 
190 
191 gint
gnc_header_get_cell_offset(GncHeader * header,gint col,gint * cell_width)192 gnc_header_get_cell_offset (GncHeader *header, gint col, gint *cell_width)
193 {
194     SheetBlockStyle *style = header->style;
195     gint j;
196     gint offset = 0;
197 
198     for (j = 0; j < style->ncols; j++)
199     {
200         CellDimensions *cd;
201 
202         cd = gnucash_style_get_cell_dimensions (style, 0, j);
203         if (!cd) continue;
204 
205         if (j == col)
206         {
207             *cell_width = cd->pixel_width;
208             break;
209         }
210         offset = offset + cd->pixel_width;
211     }
212     return offset;
213 }
214 
215 
216 static gboolean
gnc_header_draw(GtkWidget * header,cairo_t * cr)217 gnc_header_draw (GtkWidget *header, cairo_t *cr)
218 {
219     GnucashSheet *sheet = GNC_HEADER(header)->sheet;
220     GdkWindow *sheet_layout_win = gtk_layout_get_bin_window (GTK_LAYOUT(sheet));
221     gint x, y;
222 
223     // use this to get the scroll x value to align the header
224     gdk_window_get_position (sheet_layout_win, &x, &y);
225 
226     // if the register page is moved to another window, the surface is
227     // not created so test for a surface and create one if null
228     if (GNC_HEADER(header)->surface == NULL)
229         gnc_header_draw_offscreen (GNC_HEADER(header));
230 
231     cairo_set_source_surface (cr, GNC_HEADER(header)->surface, x, 0);
232     cairo_paint (cr);
233 
234     return TRUE;
235 }
236 
237 
238 void
gnc_header_request_redraw(GncHeader * header)239 gnc_header_request_redraw (GncHeader *header)
240 {
241     if (!header->style)
242         return;
243 
244     gnc_header_draw_offscreen (header);
245     gtk_widget_queue_draw (GTK_WIDGET(header));
246 }
247 
248 
249 static void
gnc_header_unrealize(GtkWidget * widget)250 gnc_header_unrealize (GtkWidget *widget)
251 {
252     GncHeader *header = GNC_HEADER(widget);
253     if (header->surface)
254         cairo_surface_destroy (header->surface);
255     header->surface = NULL;
256 
257     if (header->resize_cursor)
258         g_object_unref (header->resize_cursor);
259     header->resize_cursor = NULL;
260 
261     if (header->normal_cursor)
262         g_object_unref (header->normal_cursor);
263     header->normal_cursor = NULL;
264 
265     if (GTK_WIDGET_CLASS(parent_class)->unrealize)
266         GTK_WIDGET_CLASS(parent_class)->unrealize (GTK_WIDGET(header));
267 }
268 
269 
270 static void
gnc_header_finalize(GObject * object)271 gnc_header_finalize (GObject *object)
272 {
273     GncHeader *header;
274 
275     header = GNC_HEADER(object);
276 
277     g_free (header->cursor_name);
278     header->cursor_name = NULL;
279 
280     G_OBJECT_CLASS(parent_class)->finalize (object);
281 }
282 
283 
284 void
gnc_header_reconfigure(GncHeader * header)285 gnc_header_reconfigure (GncHeader *header)
286 {
287     GnucashSheet *sheet;
288     SheetBlockStyle *old_style;
289     int w, h;
290 
291     g_return_if_fail (header != NULL);
292     g_return_if_fail (GNC_IS_HEADER(header));
293 
294     sheet = GNUCASH_SHEET(header->sheet);
295     old_style = header->style;
296 
297     header->style = gnucash_sheet_get_style_from_cursor
298                     (sheet, header->cursor_name);
299 
300     if (header->style == NULL)
301         return;
302 
303     sheet->width = header->style->dimensions->width;
304 
305     w = header->style->dimensions->width;
306     h = header->style->dimensions->height;
307     h *= header->num_phys_rows;
308     h /= header->style->nrows;
309     h += 2;
310 
311     if (header->height != h ||
312             header->width != w  ||
313             header->style != old_style)
314     {
315         header->height = h;
316         header->width = w;
317         gtk_layout_set_size (GTK_LAYOUT(header), w, h);
318         gtk_widget_set_size_request (GTK_WIDGET(header), -1, h);
319         gnc_header_request_redraw (header);
320     }
321 }
322 
323 void
gnc_header_set_header_rows(GncHeader * header,int num_phys_rows)324 gnc_header_set_header_rows (GncHeader *header,
325                             int num_phys_rows)
326 {
327     g_return_if_fail (header != NULL);
328     g_return_if_fail (GNC_IS_HEADER(header));
329 
330     header->num_phys_rows = num_phys_rows;
331 }
332 
333 /*
334  *  Returns FALSE if pointer not on a resize line, else returns
335  *  TRUE. Returns the index of the column to the left in the col
336  *  argument.
337  */
338 static gboolean
pointer_on_resize_line(GncHeader * header,int x,G_GNUC_UNUSED int y,int * col)339 pointer_on_resize_line (GncHeader *header, int x, G_GNUC_UNUSED int y, int *col)
340 {
341     SheetBlockStyle *style = header->style;
342     gboolean on_the_line = FALSE;
343     CellDimensions *cd;
344     int pixels = 0;
345     int j;
346 
347     for (j = 0; j < style->ncols; j++)
348     {
349         cd = gnucash_style_get_cell_dimensions (style, 0, j);
350         if (!cd) continue;
351 
352         pixels += cd->pixel_width;
353         if (x >= pixels - 1 && x <= pixels + 1)
354             on_the_line = TRUE;
355         if (x <= pixels + 1)
356             break;
357     }
358 
359     if (col != NULL)
360         *col = j;
361 
362     return on_the_line;
363 }
364 
365 static int
find_resize_col(GncHeader * header,int col)366 find_resize_col (GncHeader *header, int col)
367 {
368     SheetBlockStyle *style = header->style;
369     CellDimensions *cd;
370     int start = col;
371 
372     if (col < 0 || col >= style->ncols)
373         return -1;
374 
375     /* skip to the right over zero-width columns */
376     while ((col + 1 < style->ncols) &&
377             (cd = gnucash_style_get_cell_dimensions (style, 0, col + 1)) &&
378             cd && (cd->pixel_width == 0))
379         ++col;
380 
381     /* now go back left till we have a resizable column */
382     while (col >= start)
383     {
384         if (gnucash_style_col_is_resizable (style, col))
385             return col;
386         else
387             col--;
388     }
389 
390     /* didn't find a resizable column to the right of col */
391     return -1;
392 }
393 
394 static void
gnc_header_resize_column(GncHeader * header,gint col,gint width)395 gnc_header_resize_column (GncHeader *header, gint col, gint width)
396 {
397     GnucashSheet *sheet = header->sheet;
398 
399     gnucash_sheet_set_col_width (sheet, col, width);
400 
401     gnucash_cursor_configure (GNUCASH_CURSOR(sheet->cursor));
402     gnc_item_edit_configure (gnucash_sheet_get_item_edit (sheet));
403 
404     gnc_header_reconfigure (header);
405 
406     gnucash_sheet_set_scroll_region (sheet);
407     gnucash_sheet_update_adjustments (sheet);
408 
409     gnc_header_request_redraw (header);
410     gnucash_sheet_redraw_all (sheet);
411 }
412 
413 static void
gnc_header_auto_resize_column(GncHeader * header,gint col)414 gnc_header_auto_resize_column (GncHeader *header, gint col)
415 {
416     int width;
417 
418     width = gnucash_sheet_col_max_width (header->sheet, 0, col);
419 
420     gnc_header_resize_column (header, col, width);
421 }
422 
423 static gint
gnc_header_event(GtkWidget * widget,GdkEvent * event)424 gnc_header_event (GtkWidget *widget, GdkEvent *event)
425 {
426     GncHeader *header = GNC_HEADER(widget);
427     GdkWindow *window = gtk_widget_get_window (widget);
428     int x, y;
429     int col;
430 
431     if (!header->resize_cursor)
432         header->resize_cursor = gdk_cursor_new_for_display (gdk_window_get_display (window),
433                                                             GDK_SB_H_DOUBLE_ARROW);
434 
435     switch (event->type)
436     {
437     case GDK_MOTION_NOTIFY:
438         x = event->motion.x;
439         y = event->motion.y;
440 
441         if (header->in_resize)
442         {
443             int change = x - header->resize_x;
444             int new_width = header->resize_col_width + change;
445 
446             if (new_width >= 0)
447             {
448                 header->resize_x = x;
449                 header->resize_col_width = new_width;
450                 gnc_header_request_redraw (header);
451             }
452 
453             break;
454         }
455 
456         if (pointer_on_resize_line (header, x, y, &col) &&
457                 gnucash_style_col_is_resizable (header->style, col))
458             gdk_window_set_cursor (window, header->resize_cursor);
459         else
460             gdk_window_set_cursor (window, header->normal_cursor);
461         break;
462 
463     case GDK_BUTTON_PRESS:
464     {
465         int col;
466 
467         if (event->button.button != 1)
468             break;
469 
470         x = event->button.x;
471         y = event->button.y;
472 
473         if (pointer_on_resize_line (header, x, y, &col))
474             col = find_resize_col (header, col);
475         else
476             col = -1;
477 
478         if (col > -1)
479         {
480             CellDimensions *cd;
481 
482             cd = gnucash_style_get_cell_dimensions
483                  (header->style, 0, col);
484             if (!cd) break;
485 
486             header->in_resize = TRUE;
487             header->resize_col = col;
488             header->resize_col_width = cd->pixel_width;
489             header->resize_x = x;
490         }
491         break;
492     }
493     case GDK_BUTTON_RELEASE:
494     {
495         if (event->button.button != 1)
496             break;
497 
498         if (header->in_resize)
499         {
500             if (header->resize_col_width == 0)
501                 header->resize_col_width = 1;
502 
503             gnc_header_resize_column
504                 (header,
505                  header->resize_col,
506                  header->resize_col_width);
507             header->in_resize = FALSE;
508             header->resize_col = -1;
509             gnc_header_request_redraw (header);
510         }
511         break;
512     }
513 
514     case GDK_2BUTTON_PRESS:
515     {
516         gboolean on_line;
517         int ptr_col;
518         int resize_col;
519 
520         if (event->button.button != 1)
521             break;
522 
523         x = event->button.x;
524         y = event->button.y;
525 
526         on_line = pointer_on_resize_line (header, x, y, &ptr_col);
527 
528         /* If we're on a resize line and the column to the right is zero
529            width, resize that one. */
530         if (on_line)
531             resize_col = find_resize_col (header, ptr_col);
532         else
533             resize_col = ptr_col;
534 
535         if (resize_col > -1)
536         {
537             header->in_resize = FALSE;
538             header->resize_col = -1;
539             gnc_header_auto_resize_column (header, resize_col);
540         }
541     }
542     break;
543 
544     default:
545         break;
546     }
547     return FALSE;
548 }
549 
550 
551 /* Note that g_value_set_object() refs the object, as does
552  * g_object_get(). But g_object_get() only unrefs once when it disgorges
553  * the object, leaving an unbalanced ref, which leaks. So instead of
554  * using g_value_set_object(), use g_value_take_object() which doesn't
555  * ref the object when used in get_property().
556  */
557 static void
gnc_header_get_property(GObject * object,guint param_id,GValue * value,GParamSpec * pspec)558 gnc_header_get_property (GObject *object,
559                          guint param_id,
560                          GValue *value,
561                          GParamSpec *pspec)
562 {
563     GncHeader *header = GNC_HEADER(object);
564 
565     switch (param_id)
566     {
567     case PROP_SHEET:
568         g_value_take_object (value, header->sheet);
569         break;
570     case PROP_CURSOR_NAME:
571         g_value_set_string (value, header->cursor_name);
572         break;
573     default:
574         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, param_id, pspec);
575         break;
576     }
577 }
578 
579 static void
gnc_header_set_property(GObject * object,guint param_id,const GValue * value,GParamSpec * pspec)580 gnc_header_set_property (GObject *object,
581                          guint param_id,
582                          const GValue *value,
583                          GParamSpec *pspec)
584 {
585     GncHeader *header = GNC_HEADER(object);
586     GtkLayout *layout = GTK_LAYOUT(header);
587     gboolean needs_update = FALSE;
588     gchar *old_name;
589 
590     switch (param_id)
591     {
592     case PROP_SHEET:
593         header->sheet = GNUCASH_SHEET(g_value_get_object (value));
594         gtk_scrollable_set_hadjustment (GTK_SCROLLABLE(layout), header->sheet->hadj);
595         needs_update = TRUE;
596         break;
597     case PROP_CURSOR_NAME:
598         old_name = header->cursor_name;
599 
600         header->cursor_name = g_value_dup_string (value);
601         needs_update = !old_name || !header->cursor_name ||
602                        strcmp (old_name, header->cursor_name) != 0;
603         g_free (old_name);
604         break;
605     default:
606         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, param_id, pspec);
607         break;
608     }
609 
610     if ((header->sheet != NULL) && needs_update)
611         gnc_header_reconfigure (header);
612 }
613 
614 
615 static void
gnc_header_init(GncHeader * header)616 gnc_header_init (GncHeader *header)
617 {
618     header->sheet = NULL;
619     header->cursor_name = NULL;
620     header->in_resize = FALSE;
621     header->resize_col = -1;
622     header->resize_cursor = NULL;
623     header->normal_cursor = NULL;
624     header->height = 20;
625     header->width = 400;
626     header->style = NULL;
627 
628     gtk_widget_add_events (GTK_WIDGET(header),
629                           (GDK_EXPOSURE_MASK
630                           | GDK_BUTTON_PRESS_MASK
631                           | GDK_BUTTON_RELEASE_MASK
632                           | GDK_POINTER_MOTION_MASK
633                           | GDK_POINTER_MOTION_HINT_MASK));
634 
635     g_signal_connect (G_OBJECT(header), "configure_event",
636                       G_CALLBACK(gnc_header_reconfigure), NULL);
637     gtk_widget_show_all (GTK_WIDGET(header));
638 }
639 
640 
641 static void
gnc_header_class_init(GncHeaderClass * header_class)642 gnc_header_class_init (GncHeaderClass *header_class)
643 {
644     GObjectClass  *object_class = G_OBJECT_CLASS(header_class);
645     GtkWidgetClass *item_class = GTK_WIDGET_CLASS(header_class);
646 
647     gtk_widget_class_set_css_name (GTK_WIDGET_CLASS(header_class), "gnc-id-header");
648 
649     parent_class = g_type_class_peek_parent (header_class);
650 
651     object_class->finalize = gnc_header_finalize;
652     object_class->get_property = gnc_header_get_property;
653     object_class->set_property = gnc_header_set_property;
654 
655     g_object_class_install_property (object_class,
656                                      PROP_SHEET,
657                                      g_param_spec_object ("sheet",
658                                              "Sheet Value",
659                                              "Sheet Value",
660                                              GNUCASH_TYPE_SHEET,
661                                              G_PARAM_READWRITE));
662     g_object_class_install_property (object_class,
663                                      PROP_CURSOR_NAME,
664                                      g_param_spec_string ("cursor_name",
665                                              "Cursor Name",
666                                              "Cursor Name",
667                                              CURSOR_HEADER,
668                                              G_PARAM_READWRITE));
669 
670 
671     item_class->unrealize = gnc_header_unrealize;
672     item_class->draw      = gnc_header_draw;
673     item_class->event     = gnc_header_event;
674 }
675 
676 
677 GType
gnc_header_get_type(void)678 gnc_header_get_type (void)
679 {
680     static GType gnc_header_type = 0;
681 
682     if (!gnc_header_type)
683     {
684         static const GTypeInfo gnc_header_info =
685         {
686             sizeof (GncHeaderClass),
687             NULL,
688             NULL,
689             (GClassInitFunc) gnc_header_class_init,
690             NULL,
691             NULL,
692             sizeof (GncHeader),
693             0,
694             (GInstanceInitFunc) gnc_header_init
695         };
696 
697         gnc_header_type = g_type_register_static (GTK_TYPE_LAYOUT,
698                           "GncHeader",
699                           &gnc_header_info, 0);
700     }
701 
702     return gnc_header_type;
703 }
704 
705 
706 GtkWidget *
gnc_header_new(GnucashSheet * sheet)707 gnc_header_new (GnucashSheet *sheet)
708 {
709     GtkWidget *layout;
710 
711     layout = g_object_new (GNC_TYPE_HEADER,
712                            "sheet", sheet,
713                            "cursor_name", CURSOR_HEADER,
714                            NULL);
715 
716     sheet->header_item = layout;
717     return layout;
718 }
719 
720 
721