Update Spanish translation
[gnumeric.git] / src / item-bar.c
blob4bb55f15155bc525e082dae0e0a2fd2088a488ec
1 /*
2 * A canvas item implementing row/col headers with support for outlining.
4 * Author:
5 * Miguel de Icaza (miguel@kernel.org)
6 * Jody Goldberg (jody@gnome.org)
7 */
9 #include <gnumeric-config.h>
10 #include <gnm-i18n.h>
11 #include <gnumeric.h>
12 #include <item-bar.h>
13 #include <gnm-pane-impl.h>
15 #include <style-color.h>
16 #include <sheet.h>
17 #include <sheet-control-gui.h>
18 #include <sheet-control-gui-priv.h>
19 #include <application.h>
20 #include <selection.h>
21 #include <wbc-gtk-impl.h>
22 #include <gui-util.h>
23 #include <parse-util.h>
24 #include <commands.h>
25 #include <gutils.h>
27 #include <goffice/goffice.h>
28 #include <gsf/gsf-impl-utils.h>
29 #define GNUMERIC_ITEM "BAR"
31 #include <string.h>
33 struct _GnmItemBar {
34 GocItem base;
36 GnmPane *pane;
37 GdkCursor *normal_cursor;
38 GdkCursor *change_cursor;
39 GtkWidget *tip; /* Tip for scrolling */
40 gboolean dragging;
41 gboolean is_col_header;
42 gboolean has_resize_guides;
43 int indent, cell_width, cell_height;
44 int start_selection; /* Where selection started */
45 int colrow_being_resized;
46 int colrow_resize_size;
47 int resize_start_pos;
49 struct {
50 PangoItem *item;
51 PangoGlyphString *glyphs;
52 } pango;
54 /* Style: */
56 /* [ColRowSelectionType] */
57 GdkRGBA selection_colors[3];
58 PangoFont *selection_fonts[3];
59 int selection_font_ascents[3];
60 PangoRectangle selection_logical_sizes[3];
61 GtkStyleContext *styles[3];
63 GdkRGBA grouping_color;
65 GtkBorder padding;
68 typedef GocItemClass GnmItemBarClass;
69 static GocItemClass *parent_class;
71 enum {
72 GNM_ITEM_BAR_PROP_0,
73 GNM_ITEM_BAR_PROP_PANE,
74 GNM_ITEM_BAR_PROP_IS_COL_HEADER
77 static int
78 ib_compute_pixels_from_indent (GnmItemBar *ib, Sheet const *sheet)
80 gboolean is_cols = ib->is_col_header;
81 double const scale =
82 sheet->last_zoom_factor_used *
83 gnm_app_display_dpi_get (is_cols) / 72.;
84 int const indent = is_cols
85 ? sheet->cols.max_outline_level
86 : sheet->rows.max_outline_level;
87 if (!sheet->display_outlines || indent <= 0)
88 return 0;
89 return (int)(ib->padding.left + (indent + 1) * 14 * scale + 0.5);
92 static void
93 ib_dispose_fonts (GnmItemBar *ib)
95 unsigned ui;
97 for (ui = 0; ui < G_N_ELEMENTS (ib->selection_fonts); ui++)
98 g_clear_object (&ib->selection_fonts[ui]);
101 static const GtkStateFlags selection_type_flags[3] = {
102 GTK_STATE_FLAG_NORMAL,
103 GTK_STATE_FLAG_PRELIGHT,
104 GTK_STATE_FLAG_ACTIVE
107 static const char * const selection_styles[3] = {
108 "button.itembar",
109 "button.itembar:hover",
110 "button.itembar:active"
113 static void
114 ib_reload_color_style (GnmItemBar *ib)
116 GocItem *item = GOC_ITEM (ib);
117 GtkStyleContext *context = goc_item_get_style_context (item);
118 unsigned ui;
120 gnm_style_context_get_color (context, GTK_STATE_FLAG_NORMAL,
121 &ib->grouping_color);
123 for (ui = 0; ui < G_N_ELEMENTS (selection_type_flags); ui++) {
124 GtkStateFlags state = selection_type_flags[ui];
125 gnm_style_context_get_color
126 (context, state, &ib->selection_colors[ui]);
131 static void
132 ib_reload_sizing_style (GnmItemBar *ib)
134 GocItem *item = GOC_ITEM (ib);
135 SheetControlGUI * const scg = ib->pane->simple.scg;
136 Sheet const *sheet = scg_sheet (scg);
137 double const zoom_factor = sheet->last_zoom_factor_used;
138 gboolean const char_label =
139 ib->is_col_header && !sheet->convs->r1c1_addresses;
140 unsigned ui;
141 PangoContext *pcontext =
142 gtk_widget_get_pango_context (GTK_WIDGET (item->canvas));
143 PangoLayout *layout = pango_layout_new (pcontext);
144 PangoAttrList *attr_list;
145 GList *item_list;
147 for (ui = 0; ui < G_N_ELEMENTS (selection_type_flags); ui++) {
148 GtkStateFlags state = selection_type_flags[ui];
149 PangoFontDescription *desc;
150 PangoRectangle ink_rect;
151 const char *long_name;
152 GtkStyleContext *context;
154 g_clear_object (&ib->styles[ui]);
155 #if GTK_CHECK_VERSION(3,20,0)
156 context = go_style_context_from_selector (NULL, selection_styles[ui]);
157 #else
158 context = g_object_ref (goc_item_get_style_context (item));
159 #endif
161 ib->styles[ui] = context;
162 gtk_style_context_save (context);
163 #if !GTK_CHECK_VERSION(3,20,0)
164 gtk_style_context_set_state (context, state);
165 #endif
166 gtk_style_context_get (context, state, "font", &desc, NULL);
167 pango_font_description_set_size (desc,
168 zoom_factor * pango_font_description_get_size (desc));
169 ib->selection_fonts[ui] =
170 pango_context_load_font (pcontext, desc);
171 if (!ib->selection_fonts[ui]) {
172 /* Fallback. */
173 pango_font_description_set_family (desc, "Sans");
174 ib->selection_fonts[ui] =
175 pango_context_load_font (pcontext, desc);
179 * Figure out how tall the label can be.
180 * (Note that we avoid J/Q/Y which may go below the line.)
182 pango_layout_set_text (layout,
183 char_label ? "AHW" : "0123456789",
184 -1);
185 pango_layout_set_font_description (layout, desc);
186 pango_font_description_free (desc);
187 pango_layout_get_extents (layout, &ink_rect, NULL);
188 ib->selection_font_ascents[ui] =
189 PANGO_PIXELS (ink_rect.height + ink_rect.y);
191 /* The width of the widest string I can think of + padding */
192 if (ib->is_col_header) {
193 int last = gnm_sheet_get_last_col (sheet);
194 long_name = char_label ? col_name (last) : /* HACK: */row_name (last);
195 } else
196 long_name = row_name (gnm_sheet_get_last_row (sheet));
197 pango_layout_set_text
198 (layout,
199 char_label ? "WWWWWWWWWW" : "8888888888",
200 strlen (long_name));
201 pango_layout_get_extents (layout, NULL,
202 &ib->selection_logical_sizes[ui]);
204 if (state == GTK_STATE_FLAG_NORMAL)
205 gtk_style_context_get_padding (context, state,
206 &ib->padding);
208 gtk_style_context_restore (context);
211 attr_list = pango_attr_list_new ();
212 item_list = pango_itemize (pcontext, "A", 0, 1, attr_list, NULL);
213 pango_attr_list_unref (attr_list);
214 if (ib->pango.item)
215 pango_item_free (ib->pango.item);
216 ib->pango.item = item_list->data;
217 item_list->data = NULL;
218 if (item_list->next != NULL)
219 g_warning ("Leaking pango items");
220 g_list_free (item_list);
222 g_object_unref (layout);
226 * gnm_item_bar_calc_size:
227 * @ib: #GnmItemBar
229 * Scale fonts and sizes by the pixels_per_unit of the associated sheet.
231 * Returns : the size of the fixed dimension.
234 gnm_item_bar_calc_size (GnmItemBar *ib)
236 SheetControlGUI * const scg = ib->pane->simple.scg;
237 Sheet const *sheet = scg_sheet (scg);
238 int size;
239 unsigned ui;
241 ib_dispose_fonts (ib);
242 ib_reload_sizing_style (ib);
244 ib->cell_height = 0;
245 ib->cell_width = 0;
246 for (ui = 0; ui < G_N_ELEMENTS (ib->selection_logical_sizes); ui++) {
247 int h = PANGO_PIXELS (ib->selection_logical_sizes[ui].height) +
248 (ib->padding.top + ib->padding.bottom);
249 int w = PANGO_PIXELS (ib->selection_logical_sizes[ui].width) +
250 (ib->padding.left + ib->padding.right);
251 ib->cell_height = MAX (ib->cell_height, h);
252 ib->cell_width = MAX (ib->cell_width, w);
255 size = ib_compute_pixels_from_indent (ib, sheet);
256 if (size != ib->indent) {
257 ib->indent = size;
258 goc_item_bounds_changed (GOC_ITEM (ib));
261 return ib->indent +
262 (ib->is_col_header ? ib->cell_height : ib->cell_width);
266 * item_bar_normal_font:
267 * @ib: #GnmItemBar
269 * Returns: (transfer full): the bar normal font.
271 PangoFontDescription *
272 item_bar_normal_font (GnmItemBar const *ib)
274 return pango_font_describe (ib->selection_fonts[COL_ROW_NO_SELECTION]);
278 gnm_item_bar_indent (GnmItemBar const *ib)
280 return ib->indent;
283 static void
284 item_bar_update_bounds (GocItem *item)
286 GnmItemBar *ib = GNM_ITEM_BAR (item);
287 item->x0 = 0;
288 item->y0 = 0;
289 if (ib->is_col_header) {
290 item->x1 = G_MAXINT64/2;
291 item->y1 = (ib->cell_height + ib->indent);
292 } else {
293 item->x1 = (ib->cell_width + ib->indent);
294 item->y1 = G_MAXINT64/2;
298 static void
299 item_bar_realize (GocItem *item)
301 GnmItemBar *ib = GNM_ITEM_BAR (item);
302 GdkDisplay *display;
304 parent_class->realize (item);
306 display = gtk_widget_get_display (GTK_WIDGET (item->canvas));
307 ib->normal_cursor =
308 gdk_cursor_new_for_display (display, GDK_LEFT_PTR);
309 ib->change_cursor =
310 gdk_cursor_new_for_display (display,
311 ib->is_col_header
312 ? GDK_SB_H_DOUBLE_ARROW
313 : GDK_SB_V_DOUBLE_ARROW);
315 gnm_item_bar_calc_size (ib);
318 static void
319 item_bar_unrealize (GocItem *item)
321 GnmItemBar *ib = GNM_ITEM_BAR (item);
323 g_clear_object (&ib->change_cursor);
324 g_clear_object (&ib->normal_cursor);
326 parent_class->unrealize (item);
329 static void
330 ib_draw_cell (GnmItemBar const * const ib, cairo_t *cr,
331 ColRowSelectionType const type,
332 char const * const str, GocRect *rect)
334 GtkStyleContext *ctxt = ib->styles[type];
336 g_return_if_fail ((size_t)type < G_N_ELEMENTS (selection_type_flags));
338 cairo_save (cr);
340 gtk_style_context_save (ctxt);
341 #if !GTK_CHECK_VERSION(3,20,0)
342 gtk_style_context_set_state (ctxt, selection_type_flags[type]);
343 #endif
344 gtk_render_background (ctxt, cr, rect->x, rect->y,
345 rect->width + 1, rect->height + 1);
347 /* When we are really small leave out the shadow and the text */
348 if (rect->width >= 2 && rect->height >= 2) {
349 PangoRectangle size;
350 PangoFont *font = ib->selection_fonts[type];
351 int ascent = ib->selection_font_ascents[type];
352 int w, h;
353 GdkRGBA c;
355 g_return_if_fail (font != NULL);
356 g_object_unref (ib->pango.item->analysis.font);
357 ib->pango.item->analysis.font = g_object_ref (font);
358 pango_shape (str, strlen (str),
359 &(ib->pango.item->analysis), ib->pango.glyphs);
360 pango_glyph_string_extents (ib->pango.glyphs, font,
361 NULL, &size);
363 gtk_render_frame (ctxt, cr, rect->x, rect->y,
364 rect->width + 1, rect->height + 1);
365 w = rect->width - (ib->padding.left + ib->padding.right);
366 h = rect->height - (ib->padding.top + ib->padding.bottom);
367 cairo_rectangle (cr, rect->x + 1, rect->y + 1,
368 rect->width - 2, rect->height - 2);
369 cairo_clip (cr);
371 gtk_style_context_get_color (ctxt, selection_type_flags[type], &c);
372 gdk_cairo_set_source_rgba (cr, &c);
374 cairo_translate (cr,
375 rect->x + ib->padding.left +
376 (w - PANGO_PIXELS (size.width)) / 2,
377 rect->y + ib->padding.top + ascent +
378 (h - PANGO_PIXELS (size.height)) / 2);
379 pango_cairo_show_glyph_string (cr, font, ib->pango.glyphs);
381 gtk_style_context_restore (ctxt);
382 cairo_restore (cr);
386 gnm_item_bar_group_size (GnmItemBar const *ib, int max_outline)
388 return max_outline > 0
389 ? (ib->indent - 2) / (max_outline + 1)
390 : 0;
393 static gboolean
394 item_bar_draw_region (GocItem const *item, cairo_t *cr,
395 double x_0, double y_0, double x_1, double y_1)
397 double scale = item->canvas->pixels_per_unit;
398 int x0, x1, y0, y1;
399 GnmItemBar *ib = GNM_ITEM_BAR (item);
400 GnmPane const *pane = ib->pane;
401 SheetControlGUI const *scg = pane->simple.scg;
402 Sheet const *sheet = scg_sheet (scg);
403 SheetView const *sv = scg_view (scg);
404 ColRowInfo const *cri, *next = NULL;
405 int pixels;
406 gboolean prev_visible;
407 gboolean const draw_below = sheet->outline_symbols_below != FALSE;
408 gboolean const draw_right = sheet->outline_symbols_right != FALSE;
409 int prev_level;
410 GocRect rect;
411 GocPoint points[3];
412 gboolean const has_object = scg->wbcg->new_object != NULL || scg->selected_objects != NULL;
413 gboolean const rtl = sheet->text_is_rtl != FALSE;
414 int shadow;
415 int first_line_offset = 1;
416 GtkStyleContext *ctxt = goc_item_get_style_context (item);
418 gtk_style_context_save (ctxt);
419 goc_canvas_c2w (item->canvas, x_0, y_0, &x0, &y0);
420 goc_canvas_c2w (item->canvas, x_1, y_1, &x1, &y1);
422 ib_reload_color_style (ib);
424 if (ib->is_col_header) {
425 int const inc = gnm_item_bar_group_size (ib, sheet->cols.max_outline_level);
426 int const base_pos = .2 * inc;
427 int const len = (inc > 4) ? 4 : inc;
428 int end, total, col = pane->first.col;
429 gboolean const char_label = !sheet->convs->r1c1_addresses;
431 /* shadow type selection must be keep in sync with code in ib_draw_cell */
432 goc_canvas_c2w (item->canvas, pane->first_offset.x / scale, 0, &total, NULL);
433 end = x1;
434 rect.y = ib->indent;
435 rect.height = ib->cell_height;
436 shadow = (col > 0 && !has_object && sv_selection_col_type (sv, col-1) == COL_ROW_FULL_SELECTION)
437 ? GTK_SHADOW_IN : GTK_SHADOW_OUT;
439 if (col > 0) {
440 cri = sheet_col_get_info (sheet, col-1);
441 prev_visible = cri->visible;
442 prev_level = cri->outline_level;
443 } else {
444 prev_visible = TRUE;
445 prev_level = 0;
448 do {
449 if (col >= gnm_sheet_get_max_cols (sheet))
450 return TRUE;
452 /* DO NOT enable resizing all until we get rid of
453 * resize_start_pos. It will be wrong if things ahead
454 * of it move
456 cri = sheet_col_get_info (sheet, col);
457 if (col != -1 && ib->colrow_being_resized == col)
458 /* || sv_is_colrow_selected (sheet, col, TRUE))) */
459 pixels = ib->colrow_resize_size;
460 else
461 pixels = cri->size_pixels;
463 if (cri->visible) {
464 int left, level, i = 0, pos = base_pos;
466 if (rtl) {
467 left = (total -= pixels);
468 rect.x = total;
469 } else {
470 rect.x = left = total;
471 total += pixels;
474 rect.width = pixels;
475 ib_draw_cell (ib, cr,
476 has_object
477 ? COL_ROW_NO_SELECTION
478 : sv_selection_col_type (sv, col),
479 char_label
480 ? col_name (col)
481 : row_name (col),
482 &rect);
484 if (len > 0) {
485 cairo_save (cr);
486 cairo_set_line_width (cr, 2.0);
487 cairo_set_line_cap (cr, CAIRO_LINE_CAP_BUTT);
488 cairo_set_line_join (cr, CAIRO_LINE_JOIN_MITER);
489 gdk_cairo_set_source_rgba (cr, &ib->grouping_color);
490 if (!draw_right) {
491 next = sheet_col_get_info (sheet, col + 1);
492 prev_level = next->outline_level;
493 prev_visible = next->visible;
494 points[0].x = rtl ? (total + pixels) : left;
495 } else
496 points[0].x = total;
498 /* draw the start or end marks and the vertical lines */
499 points[1].x = points[2].x = left + pixels/2;
500 for (level = cri->outline_level; i++ < level ; pos += inc) {
501 points[0].y = points[1].y = pos;
502 points[2].y = pos + len;
503 if (i > prev_level) {
504 cairo_move_to (cr, points[0].x, points[0].y);
505 cairo_line_to (cr, points[1].x, points[1].y);
506 cairo_line_to (cr, points[2].x, points[2].y);
507 } else if (rtl) {
508 cairo_move_to (cr, left - first_line_offset, pos);
509 cairo_line_to (cr, total + pixels, pos);
510 } else {
511 cairo_move_to (cr, left - first_line_offset, pos);
512 cairo_line_to (cr, total, pos);
515 cairo_stroke (cr);
516 first_line_offset = 0;
518 if (draw_right ^ rtl) {
519 if (prev_level > level) {
520 int safety = 0;
521 int top = pos - base_pos + 2; /* inside cell's shadow */
522 int size = inc < pixels ? inc : pixels;
524 if (size > 15)
525 size = 15;
526 else if (size < 6)
527 safety = 6 - size;
529 gtk_style_context_set_state (ctxt, prev_visible ?
530 GTK_STATE_FLAG_NORMAL: GTK_STATE_FLAG_SELECTED);
531 gtk_render_frame (ctxt, cr,
532 left, top+safety, size, size);
533 if (size > 9) {
534 if (!prev_visible) {
535 top++;
536 left++;
537 cairo_move_to (cr, left+size/2, top+3);
538 cairo_line_to (cr, left+size/2, top+size-4);
540 cairo_move_to (cr, left+3, top+size/2);
541 cairo_line_to (cr, left+size-4, top+size/2);
544 if (level > 0) {
545 cairo_move_to (cr, left+pixels/2, pos);
546 cairo_line_to (cr, left+pixels/2, pos+len);
548 } else {
549 if (prev_level > level) {
550 int safety = 0;
551 int top = pos - base_pos + 2; /* inside cell's shadow */
552 int size = inc < pixels ? inc : pixels;
553 int right;
555 if (size > 15)
556 size = 15;
557 else if (size < 6)
558 safety = 6 - size;
560 right = (rtl ? (total + pixels) : total) - size;
561 gtk_style_context_set_state (ctxt, prev_visible ?
562 GTK_STATE_FLAG_NORMAL: GTK_STATE_FLAG_SELECTED);
563 gtk_render_frame (ctxt, cr,
564 right, top+safety, size, size);
565 if (size > 9) {
566 if (!prev_visible) {
567 top++;
568 right++;
569 cairo_move_to (cr, right+size/2, top+3);
570 cairo_line_to (cr, right+size/2, top+size-4);
572 cairo_move_to (cr, right+3, top+size/2);
573 cairo_line_to (cr, right+size-4, top+size/2);
575 } else if (level > 0) {
576 cairo_move_to (cr, left+pixels/2, pos);
577 cairo_line_to (cr, left+pixels/2, pos+len);
580 cairo_stroke (cr);
581 cairo_restore (cr);
584 prev_visible = cri->visible;
585 prev_level = cri->outline_level;
586 ++col;
587 } while ((rtl && end <= total) || (!rtl && total <= end));
588 } else {
589 int const inc = gnm_item_bar_group_size (ib, sheet->rows.max_outline_level);
590 int base_pos = .2 * inc;
591 int const len = (inc > 4) ? 4 : inc;
592 int const end = y1;
593 int const dir = rtl ? -1 : 1;
595 int total = pane->first_offset.y - item->canvas->scroll_y1 * scale;
596 int row = pane->first.row;
598 if (rtl) {
599 base_pos = ib->indent + ib->cell_width - base_pos;
600 /* Move header bar 1 pixel to the left. */
601 rect.x = -1;
602 } else
603 rect.x = ib->indent;
604 rect.width = ib->cell_width;
606 if (row > 0) {
607 cri = sheet_row_get_info (sheet, row-1);
608 prev_visible = cri->visible;
609 prev_level = cri->outline_level;
610 } else {
611 prev_visible = TRUE;
612 prev_level = 0;
615 do {
616 if (row >= gnm_sheet_get_max_rows (sheet))
617 return TRUE;
619 /* DO NOT enable resizing all until we get rid of
620 * resize_start_pos. It will be wrong if things ahead
621 * of it move
623 cri = sheet_row_get_info (sheet, row);
624 if (row != -1 && ib->colrow_being_resized == row)
625 /* || sv_is_colrow_selected (sheet, row, FALSE))) */
626 pixels = ib->colrow_resize_size;
627 else
628 pixels = cri->size_pixels;
630 if (cri->visible) {
631 int level, i = 0, pos = base_pos;
632 int top = total;
634 total += pixels;
635 rect.y = top;
636 rect.height = pixels;
637 ib_draw_cell (ib, cr,
638 has_object
639 ? COL_ROW_NO_SELECTION
640 : sv_selection_row_type (sv, row),
641 row_name (row), &rect);
643 if (len > 0) {
644 cairo_save (cr);
645 cairo_set_line_width (cr, 2.0);
646 cairo_set_line_cap (cr, CAIRO_LINE_CAP_BUTT);
647 cairo_set_line_join (cr, CAIRO_LINE_JOIN_MITER);
648 gdk_cairo_set_source_rgba (cr, &ib->grouping_color);
649 if (!draw_below) {
650 next = sheet_row_get_info (sheet, row + 1);
651 points[0].y = top;
652 } else
653 points[0].y = total;
655 /* draw the start or end marks and the vertical lines */
656 points[1].y = points[2].y = top + pixels/2;
657 for (level = cri->outline_level; i++ < level ; pos += inc * dir) {
658 points[0].x = points[1].x = pos;
659 points[2].x = pos + len * dir;
660 if (draw_below && i > prev_level) {
661 cairo_move_to (cr, points[0].x, points[0].y);
662 cairo_line_to (cr, points[1].x, points[1].y);
663 cairo_line_to (cr, points[2].x, points[2].y);
664 } else if (!draw_below && i > next->outline_level) {
665 cairo_move_to (cr, points[0].x, points[0].y);
666 cairo_line_to (cr, points[1].x, points[1].y);
667 cairo_line_to (cr, points[2].x, points[2].y);
668 } else {
669 cairo_move_to (cr, pos, top - first_line_offset);
670 cairo_line_to (cr, pos, total);
673 cairo_stroke (cr);
674 first_line_offset = 0;
676 if (draw_below) {
677 if (prev_level > level) {
678 int left, safety = 0;
679 int size = inc < pixels ? inc : pixels;
681 if (size > 15)
682 size = 15;
683 else if (size < 6)
684 safety = 6 - size;
686 /* inside cell's shadow */
687 left = pos - dir * (.2 * inc - 2);
688 if (rtl)
689 left -= size;
690 gtk_style_context_set_state (ctxt, prev_visible ?
691 GTK_STATE_FLAG_NORMAL: GTK_STATE_FLAG_SELECTED);
692 gtk_render_frame (ctxt, cr,
693 left+safety, top, size, size);
694 if (size > 9) {
695 if (!prev_visible) {
696 left += dir;
697 top++;
698 cairo_move_to (cr, left+size/2, top+3);
699 cairo_line_to (cr, left+size/2, top+size-4);
701 cairo_move_to (cr, left+3, top+size/2);
702 cairo_line_to (cr, left+size-4, top+size/2);
705 if (level > 0) {
706 cairo_move_to (cr, pos, top+pixels/2);
707 cairo_line_to (cr, pos+len, top+pixels/2);
709 } else {
710 if (next->outline_level > level) {
711 int left, safety = 0;
712 int size = inc < pixels ? inc : pixels;
713 int bottom;
715 if (size > 15)
716 size = 15;
717 else if (size < 6)
718 safety = 6 - size;
720 /* inside cell's shadow */
721 left = pos - dir * (.2 * inc - 2);
722 if (rtl)
723 left -= size;
724 bottom = total - size;
725 gtk_style_context_set_state (ctxt, prev_visible ?
726 GTK_STATE_FLAG_NORMAL: GTK_STATE_FLAG_SELECTED);
727 gtk_render_frame (ctxt,cr,
728 left+safety*dir, bottom, size, size);
729 if (size > 9) {
730 if (!next->visible) {
731 left += dir;
732 top++;
733 cairo_move_to (cr, left+size/2, bottom+3);
734 cairo_line_to (cr, left+size/2, bottom+size-4);
736 cairo_move_to (cr, left+3, bottom+size/2);
737 cairo_line_to (cr, left+size-4, bottom+size/2);
739 } else if (level > 0) {
740 cairo_move_to (cr, pos, top+pixels/2);
741 cairo_line_to (cr, pos+len, top+pixels/2);
744 cairo_stroke (cr);
745 cairo_restore (cr);
748 prev_visible = cri->visible;
749 prev_level = cri->outline_level;
750 ++row;
751 } while (total <= end);
753 gtk_style_context_restore (ctxt);
754 return TRUE;
757 static double
758 item_bar_distance (GocItem *item, double x, double y,
759 GocItem **actual_item)
761 *actual_item = item;
762 return 0.0;
766 * is_pointer_on_division:
767 * @ib: #GnmItemBar
768 * @x: in world coords
769 * @y: in world coords
770 * @the_total:
771 * @the_element:
772 * @minor_pos:
774 * NOTE : this could easily be optimized. We need not start at 0 every time.
775 * We could potentially use the routines in gnm-pane.
777 * Returns non-%NULL if point (@x,@y) is on a division
779 static ColRowInfo const *
780 is_pointer_on_division (GnmItemBar const *ib, gint64 x, gint64 y,
781 gint64 *the_total, int *the_element, gint64 *minor_pos)
783 Sheet *sheet = scg_sheet (ib->pane->simple.scg);
784 ColRowInfo const *cri;
785 gint64 major, minor, total = 0;
786 int i;
788 if (ib->is_col_header) {
789 major = x;
790 minor = y;
791 i = ib->pane->first.col;
792 total = ib->pane->first_offset.x;
793 } else {
794 major = y;
795 minor = x;
796 i = ib->pane->first.row;
797 total = ib->pane->first_offset.y;
799 if (NULL != minor_pos)
800 *minor_pos = minor;
801 if (NULL != the_element)
802 *the_element = -1;
803 for (; total < major; i++) {
804 if (ib->is_col_header) {
805 if (i >= gnm_sheet_get_max_cols (sheet))
806 return NULL;
807 cri = sheet_col_get_info (sheet, i);
808 } else {
809 if (i >= gnm_sheet_get_max_rows (sheet))
810 return NULL;
811 cri = sheet_row_get_info (sheet, i);
814 if (cri->visible) {
815 WBCGtk *wbcg = scg_wbcg (ib->pane->simple.scg);
816 total += cri->size_pixels;
818 if (wbc_gtk_get_guru (wbcg) == NULL &&
819 !wbcg_is_editing (wbcg) &&
820 (total - 4 < major) && (major < total + 4)) {
821 if (the_total)
822 *the_total = total;
823 if (the_element)
824 *the_element = i;
825 return (minor >= ib->indent) ? cri : NULL;
829 if (total > major) {
830 if (the_element)
831 *the_element = i;
832 return NULL;
835 return NULL;
838 /* x & y in world coords */
839 static void
840 ib_set_cursor (GnmItemBar *ib, gint64 x, gint64 y)
842 GdkWindow *window = gtk_widget_get_window (GTK_WIDGET (ib->base.canvas));
843 GdkCursor *cursor = ib->normal_cursor;
845 /* We might be invoked before we are realized */
846 if (NULL == window)
847 return;
848 if (NULL != is_pointer_on_division (ib, x, y, NULL, NULL, NULL))
849 cursor = ib->change_cursor;
850 gdk_window_set_cursor (window, cursor);
853 static void
854 colrow_tip_setlabel (GnmItemBar *ib, gboolean const is_cols, int size_pixels)
856 if (ib->tip != NULL) {
857 char *buffer, *points, *pixels;
858 char const *label = is_cols ? _("Width:") : _("Height");
859 double const scale = 72. / gnm_app_display_dpi_get (!is_cols);
860 double size_points = scale*size_pixels;
862 /* xgettext: This is input to ngettext based on the number of pixels. */
863 pixels = g_strdup_printf (ngettext ("(%d pixel)", "(%d pixels)", size_pixels),
864 size_pixels);
866 if (size_points == gnm_floor (size_points))
867 /* xgettext: This is input to ngettext based on the integer number of points. */
868 points = g_strdup_printf (ngettext (_("%d.00 pt"), _("%d.00 pts"), (int) gnm_floor (size_points)),
869 (int) gnm_floor (size_points));
870 else
871 /* xgettext: The number of points here is always a fractional number, ie. not an integer. */
872 points = g_strdup_printf (_("%.2f pts"), size_points);
874 buffer = g_strconcat (label, " ", points, " ", pixels, NULL);
875 g_free (pixels);
876 g_free (points);
877 gtk_label_set_text (GTK_LABEL (ib->tip), buffer);
878 g_free(buffer);
882 static void
883 item_bar_resize_stop (GnmItemBar *ib, int new_size)
885 if (ib->colrow_being_resized != -1) {
886 if (new_size != 0)
887 scg_colrow_size_set (ib->pane->simple.scg,
888 ib->is_col_header,
889 ib->colrow_being_resized,
890 new_size);
891 ib->colrow_being_resized = -1;
893 if (ib->has_resize_guides) {
894 ib->has_resize_guides = FALSE;
895 scg_size_guide_stop (ib->pane->simple.scg);
897 if (ib->tip != NULL) {
898 gtk_widget_destroy (gtk_widget_get_toplevel (ib->tip));
899 ib->tip = NULL;
903 static gboolean
904 cb_extend_selection (GnmPane *pane, GnmPaneSlideInfo const *info)
906 GnmItemBar * const ib = info->user_data;
907 gboolean const is_cols = ib->is_col_header;
908 scg_colrow_select (pane->simple.scg,
909 is_cols, is_cols ? info->col : info->row, GDK_SHIFT_MASK);
910 return TRUE;
913 static gint
914 outline_button_press (GnmItemBar const *ib, int element, int pixel)
916 SheetControlGUI *scg = ib->pane->simple.scg;
917 Sheet * const sheet = scg_sheet (scg);
918 int inc, step;
920 if (ib->is_col_header) {
921 if (sheet->cols.max_outline_level <= 0)
922 return TRUE;
923 inc = (ib->indent - 2) / (sheet->cols.max_outline_level + 1);
924 } else {
925 if (sheet->rows.max_outline_level <= 0)
926 return TRUE;
927 inc = (ib->indent - 2) / (sheet->rows.max_outline_level + 1);
930 step = pixel / inc;
932 cmd_selection_outline_change (scg_wbc (scg), ib->is_col_header,
933 element, step);
934 return TRUE;
937 static gboolean
938 item_bar_button_pressed (GocItem *item, int button, double x_, double y_)
940 ColRowInfo const *cri;
941 GocCanvas * const canvas = item->canvas;
942 GnmItemBar * const ib = GNM_ITEM_BAR (item);
943 GnmPane * const pane = ib->pane;
944 SheetControlGUI * const scg = pane->simple.scg;
945 SheetControl * const sc = (SheetControl *) pane->simple.scg;
946 Sheet * const sheet = sc_sheet (sc);
947 WBCGtk * const wbcg = scg_wbcg (scg);
948 gboolean const is_cols = ib->is_col_header;
949 gint64 minor_pos, start;
950 int element;
951 GdkEvent *event = goc_canvas_get_cur_event (item->canvas);
952 GdkEventButton *bevent = &event->button;
953 gint64 x = x_ * item->canvas->pixels_per_unit, y = y_ * item->canvas->pixels_per_unit;
955 if (ib->colrow_being_resized != -1 || ib->start_selection != -1) {
956 // This happens with repeated clicks on colrow divider.
957 // Ignore it. Definitely don't regrab.
958 return TRUE;
961 if (button > 3)
962 return FALSE;
964 if (wbc_gtk_get_guru (wbcg) == NULL)
965 scg_mode_edit (scg);
967 cri = is_pointer_on_division (ib, x, y,
968 &start, &element, &minor_pos);
969 if (element < 0)
970 return FALSE;
971 if (minor_pos < ib->indent)
972 return outline_button_press (ib, element, minor_pos);
974 if (button == 3) {
975 if (wbc_gtk_get_guru (wbcg) != NULL)
976 return TRUE;
977 /* If the selection does not contain the current row/col
978 * then clear the selection and add it.
980 if (!sv_is_colrow_selected (sc_view (sc), element, is_cols))
981 scg_colrow_select (scg, is_cols,
982 element, bevent->state);
984 scg_context_menu (scg, event, is_cols, !is_cols);
985 return TRUE;
986 } else if (cri != NULL) {
988 * Record the important bits.
990 * By setting colrow_being_resized to a non -1 value,
991 * we know that we are being resized (used in the
992 * other event handlers).
994 ib->colrow_being_resized = element;
995 ib->resize_start_pos = (is_cols && sheet->text_is_rtl)
996 ? start : (start - cri->size_pixels);
997 ib->colrow_resize_size = cri->size_pixels;
999 if (ib->tip == NULL) {
1000 GtkWidget *cw = GTK_WIDGET (canvas);
1001 int wx, wy;
1002 ib->tip = gnm_create_tooltip (cw);
1003 colrow_tip_setlabel (ib, is_cols, ib->colrow_resize_size);
1004 /* Position above the current point for both
1005 * col and row headers. trying to put it
1006 * beside for row headers often ends up pushing
1007 * the tip under the cursor which can have odd
1008 * effects on the event stream. win32 was
1009 * different from X. */
1011 gnm_canvas_get_position (canvas, &wx, &wy,x, y);
1012 gnm_position_tooltip (ib->tip,
1013 wx, wy, TRUE);
1014 gtk_widget_show_all (gtk_widget_get_toplevel (ib->tip));
1016 } else {
1017 if (wbc_gtk_get_guru (wbcg) != NULL &&
1018 !wbcg_entry_has_logical (wbcg))
1019 return TRUE;
1021 /* If we're editing it is possible for this to fail */
1022 if (!scg_colrow_select (scg, is_cols, element, bevent->state))
1023 return TRUE;
1025 ib->start_selection = element;
1026 gnm_pane_slide_init (pane);
1028 gnm_simple_canvas_grab (item);
1029 return TRUE;
1032 static gboolean
1033 item_bar_2button_pressed (GocItem *item, int button, double x, double y)
1035 GnmItemBar * const ib = GNM_ITEM_BAR (item);
1036 if (button > 3)
1037 return FALSE;
1039 if (button != 3)
1040 item_bar_resize_stop (ib, -1);
1041 return TRUE;
1044 static gboolean
1045 item_bar_enter_notify (GocItem *item, double x_, double y_)
1047 GnmItemBar * const ib = GNM_ITEM_BAR (item);
1048 gint64 x = x_ * item->canvas->pixels_per_unit, y = y_ * item->canvas->pixels_per_unit;
1049 ib_set_cursor (ib, x, y);
1050 return TRUE;
1053 static gboolean
1054 item_bar_motion (GocItem *item, double x_, double y_)
1056 ColRowInfo const *cri;
1057 GocCanvas * const canvas = item->canvas;
1058 GnmItemBar * const ib = GNM_ITEM_BAR (item);
1059 GnmPane * const pane = ib->pane;
1060 SheetControlGUI * const scg = pane->simple.scg;
1061 SheetControl * const sc = (SheetControl *) pane->simple.scg;
1062 Sheet * const sheet = sc_sheet (sc);
1063 gboolean const is_cols = ib->is_col_header;
1064 gint64 pos;
1065 gint64 x = x_ * item->canvas->pixels_per_unit, y = y_ * item->canvas->pixels_per_unit;
1067 if (ib->colrow_being_resized != -1) {
1068 int new_size;
1069 if (!ib->has_resize_guides) {
1070 ib->has_resize_guides = TRUE;
1071 scg_size_guide_start (ib->pane->simple.scg,
1072 ib->is_col_header,
1073 ib->colrow_being_resized,
1074 TRUE);
1077 cri = sheet_colrow_get_info (sheet,
1078 ib->colrow_being_resized, is_cols);
1079 pos = is_cols ? x: y;
1080 new_size = pos - ib->resize_start_pos;
1081 if (is_cols && sheet->text_is_rtl)
1082 new_size += cri->size_pixels;
1084 /* Ensure we always have enough room for the margins */
1085 if (is_cols) {
1086 if (new_size <= (GNM_COL_MARGIN + GNM_COL_MARGIN)) {
1087 new_size = GNM_COL_MARGIN + GNM_COL_MARGIN + 1;
1088 pos = pane->first_offset.x +
1089 scg_colrow_distance_get (scg, TRUE,
1090 pane->first.col,
1091 ib->colrow_being_resized);
1092 pos += new_size;
1094 } else {
1095 if (new_size <= (GNM_ROW_MARGIN + GNM_ROW_MARGIN)) {
1096 new_size = GNM_ROW_MARGIN + GNM_ROW_MARGIN + 1;
1097 pos = pane->first_offset.y +
1098 scg_colrow_distance_get (scg, FALSE,
1099 pane->first.row,
1100 ib->colrow_being_resized);
1101 pos += new_size;
1105 ib->colrow_resize_size = new_size;
1106 colrow_tip_setlabel (ib, is_cols, new_size);
1107 scg_size_guide_motion (scg, is_cols, pos);
1109 /* Redraw the GnmItemBar to show nice incremental progress */
1110 goc_canvas_invalidate (canvas, 0, 0, G_MAXINT/2, G_MAXINT/2);
1112 } else if (ib->start_selection != -1) {
1113 gnm_pane_handle_motion (ib->pane,
1114 canvas, x, y,
1115 GNM_PANE_SLIDE_AT_COLROW_BOUND |
1116 (is_cols ? GNM_PANE_SLIDE_X : GNM_PANE_SLIDE_Y),
1117 cb_extend_selection, ib);
1118 } else
1119 ib_set_cursor (ib, x, y);
1120 return TRUE;
1123 static gboolean
1124 item_bar_button_released (GocItem *item, int button, double x, double y)
1126 GnmItemBar *ib = GNM_ITEM_BAR (item);
1127 if (item == goc_canvas_get_grabbed_item (item->canvas))
1128 gnm_simple_canvas_ungrab (item);
1130 if (ib->colrow_being_resized >= 0) {
1131 if (ib->has_resize_guides)
1132 item_bar_resize_stop (ib, ib->colrow_resize_size);
1133 else
1135 * No need to resize, nothing changed.
1136 * This will handle the case of a double click.
1138 item_bar_resize_stop (ib, 0);
1140 ib->start_selection = -1;
1141 return TRUE;
1144 static void
1145 item_bar_set_property (GObject *obj, guint param_id,
1146 GValue const *value, GParamSpec *pspec)
1148 GnmItemBar *ib = GNM_ITEM_BAR (obj);
1150 switch (param_id){
1151 case GNM_ITEM_BAR_PROP_PANE:
1152 ib->pane = g_value_get_object (value);
1153 break;
1154 case GNM_ITEM_BAR_PROP_IS_COL_HEADER:
1155 ib->is_col_header = g_value_get_boolean (value);
1156 goc_item_bounds_changed (GOC_ITEM (obj));
1157 break;
1161 static void
1162 item_bar_dispose (GObject *obj)
1164 GnmItemBar *ib = GNM_ITEM_BAR (obj);
1165 unsigned ui;
1167 ib_dispose_fonts (ib);
1169 if (ib->tip) {
1170 gtk_widget_destroy (ib->tip);
1171 ib->tip = NULL;
1174 if (ib->pango.glyphs != NULL) {
1175 pango_glyph_string_free (ib->pango.glyphs);
1176 ib->pango.glyphs = NULL;
1178 if (ib->pango.item != NULL) {
1179 pango_item_free (ib->pango.item);
1180 ib->pango.item = NULL;
1182 for (ui = 0; ui < G_N_ELEMENTS(ib->styles); ui++)
1183 g_clear_object (&ib->styles[ui]);
1185 G_OBJECT_CLASS (parent_class)->dispose (obj);
1188 static void
1189 gnm_item_bar_init (GnmItemBar *ib)
1191 ib->base.x0 = 0;
1192 ib->base.y0 = 0;
1193 ib->base.x1 = 0;
1194 ib->base.y1 = 0;
1196 ib->dragging = FALSE;
1197 ib->is_col_header = FALSE;
1198 ib->cell_width = ib->cell_height = 1;
1199 ib->indent = 0;
1200 ib->start_selection = -1;
1202 ib->tip = NULL;
1204 ib->colrow_being_resized = -1;
1205 ib->has_resize_guides = FALSE;
1206 ib->pango.item = NULL;
1207 ib->pango.glyphs = pango_glyph_string_new ();
1209 #if !GTK_CHECK_VERSION(3,20,0)
1210 /* Style-wise we are a button. */
1211 gtk_style_context_add_class
1212 (goc_item_get_style_context (GOC_ITEM (ib)),
1213 GTK_STYLE_CLASS_BUTTON);
1214 #endif
1217 static void
1218 gnm_item_bar_class_init (GObjectClass *gobject_klass)
1220 GocItemClass *item_klass = (GocItemClass *) gobject_klass;
1222 parent_class = g_type_class_peek_parent (gobject_klass);
1224 gobject_klass->dispose = item_bar_dispose;
1225 gobject_klass->set_property = item_bar_set_property;
1226 g_object_class_install_property (gobject_klass, GNM_ITEM_BAR_PROP_PANE,
1227 g_param_spec_object ("pane",
1228 P_("Pane"),
1229 P_("The pane containing the associated grid"),
1230 GNM_PANE_TYPE,
1231 GSF_PARAM_STATIC | G_PARAM_WRITABLE));
1232 g_object_class_install_property (gobject_klass, GNM_ITEM_BAR_PROP_IS_COL_HEADER,
1233 g_param_spec_boolean ("IsColHeader",
1234 P_("IsColHeader"),
1235 P_("Is the item-bar a header for columns or rows"),
1236 FALSE,
1237 GSF_PARAM_STATIC | G_PARAM_WRITABLE));
1239 item_klass->realize = item_bar_realize;
1240 item_klass->unrealize = item_bar_unrealize;
1241 item_klass->draw_region = item_bar_draw_region;
1242 item_klass->update_bounds = item_bar_update_bounds;
1243 item_klass->distance = item_bar_distance;
1244 item_klass->button_pressed = item_bar_button_pressed;
1245 item_klass->button_released = item_bar_button_released;
1246 item_klass->button2_pressed = item_bar_2button_pressed;
1247 item_klass->enter_notify = item_bar_enter_notify;
1248 item_klass->motion = item_bar_motion;
1251 GSF_CLASS (GnmItemBar, gnm_item_bar,
1252 gnm_item_bar_class_init, gnm_item_bar_init,
1253 GOC_TYPE_ITEM)