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