1 // -*- C++ -*-
2 
3 /*
4  * Gnome Crystal
5  * grid.cc
6  *
7  * Copyright (C) 2010-2011 Jean Bréfort <jean.brefort@normalesup.org>
8  *
9  * This program is free software; you can redistribute it and/or
10  * modify it under the terms of the GNU General Public License as
11  * published by the Free Software Foundation; either version 3 of the
12  * License, or (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License
20  * along with this program; if not, write to the Free Software
21  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
22  * USA
23  */
24 
25 #include "config.h"
26 #include "grid.h"
27 #include <gcugtk/marshalers.h>
28 #include <goffice/goffice.h>
29 #include <glib/gi18n-lib.h>
30 #include <list>
31 #include <set>
32 #include <string>
33 #include <vector>
34 #include <cstring>
35 
36 #include <gsf/gsf-impl-utils.h>
37 
38 #define CURSOR_ON_TIME 800
39 #define CURSOR_OFF_TIME 400
40 
41 struct _GcrGrid
42 {
43 	GtkLayout base;
44 	unsigned cols, rows, allocated_rows;
45 	int col, row, last_row;
46 	int first_visible;
47 	unsigned nb_visible;
48 	int header_width, row_height, width, actual_width, *col_widths,
49 		line_offset, scroll_width, *min_widths, cols_min_width;
50 	int cursor_index, sel_start;
51 	GtkAdjustment *vadj;
52 	GtkWidget *scroll;
53 	std::string *titles;
54 	GType *types;
55 	bool *editable;
56 	std::vector < std::string * > row_data; // storing as string since this is what is displayed
57 	bool cursor_visible;
58 	unsigned long cursor_signal;
59 	std::string *orig_string;
60 	int can_edit;
61 	bool allow_multiple;
62 	bool dragging;
63 	bool selection_locked;
64 	std::set < int > *selected_rows;
65 };
66 
67 static GtkWidgetClass *parent_class;
68 static GdkPixbuf *checked = NULL, *unchecked = NULL;
69 
70 enum {
71 	VALUE_CHANGED,
72 	ROW_SELECTED,
73 	ROW_DELETED,
74 	LAST_SIGNAL
75 };
76 
77 static gulong gcr_grid_signals [LAST_SIGNAL] = { 0, };
78 
79 typedef struct
80 {
81 	GtkLayoutClass parent_class;
82 	void (*value_changed_event) (GcrGrid *grid, unsigned row, unsigned column, gpointer data);
83 	void (*row_added_event) (GcrGrid *grid, unsigned row, gpointer data);
84 	void (*row_selected_event) (GcrGrid *grid, unsigned row, gpointer data);
85 	void (*row_deleted_event) (GcrGrid *grid, unsigned row, gpointer data);
86 } GcrGridClass;
87 
88 // static functions
89 
gcr_grid_validate_change(GcrGrid * grid)90 static bool gcr_grid_validate_change (GcrGrid *grid)
91 {
92 	if (grid->row < 0 || grid->col < 0)
93 		return true;
94 	std::string new_string = grid->row_data[grid->row][grid->col];
95 	if (new_string == *grid->orig_string)
96 		return true;
97 	switch (grid->types[grid->col]) {
98 	case G_TYPE_INT: {
99 		long orig, next;
100 		bool neg = !grid->orig_string->compare (0, strlen ("−"), "−");
101 		char const *str = grid->orig_string->c_str ();
102 		char *buf;
103 		if (neg)
104 			str += strlen ("−");
105 		orig = strtol (str, NULL, 10);
106 		if (neg)
107 			orig = -orig;
108 		neg = !new_string.compare (0, strlen ("−"), "−");
109 		str = new_string.c_str ();
110 		if (neg)
111 			str += strlen ("−");
112 		next = strtol (str, &buf, 10);
113 		if (neg)
114 			next = -next;
115 		if (buf && *buf)
116 			goto error_handler;
117 		buf = (next < 0)? g_strdup_printf ("−%ld", -next): g_strdup_printf ("%ld", next);
118 		grid->row_data[grid->row][grid->col] = buf;
119 		grid->sel_start = grid->cursor_index = strlen (buf);
120 		g_free (buf);
121 		if (orig != next)
122 			g_signal_emit (grid, gcr_grid_signals[VALUE_CHANGED], 0, grid->row, grid->col);
123 		return true;
124 	}
125 	case G_TYPE_UINT: {
126 		unsigned long orig, next;
127 		char *buf;
128 		orig = strtoul (grid->orig_string->c_str (), NULL, 10);
129 		next = strtoul (new_string.c_str (), &buf, 10);
130 		if (buf && *buf)
131 			goto error_handler;
132 		buf = g_strdup_printf ("%lu", next);
133 		grid->row_data[grid->row][grid->col] = buf;
134 		grid->sel_start = grid->cursor_index = strlen (buf);
135 		g_free (buf);
136 		if (orig != next)
137 			g_signal_emit (grid, gcr_grid_signals[VALUE_CHANGED], 0, grid->row, grid->col);
138 		return true;
139 	}
140 	case G_TYPE_DOUBLE: {
141 		double orig, next;
142 		bool neg = !grid->orig_string->compare (0, strlen ("−"), "−");
143 		char const *str = grid->orig_string->c_str ();
144 		char *buf;
145 		if (neg)
146 			str += strlen ("−");
147 		orig = strtod (str, NULL);
148 		if (neg)
149 			orig = -orig;
150 		neg = !new_string.compare (0, strlen ("−"), "−");
151 		str = new_string.c_str ();
152 		if (neg)
153 			str += strlen ("−");
154 		next = strtod (str, &buf);
155 		if (neg)
156 			next = -next;
157 		if (buf && *buf)
158 			goto error_handler;
159 		buf = (next < 0)? g_strdup_printf ("−%f", -next): g_strdup_printf ("%f", next);
160 		grid->row_data[grid->row][grid->col] = buf;
161 		grid->sel_start = grid->cursor_index = strlen (buf);
162 		g_free (buf);
163 		if (orig != next)
164 			g_signal_emit (grid, gcr_grid_signals[VALUE_CHANGED], 0, grid->row, grid->col);
165 		return true;
166 	}
167 	case G_TYPE_BOOLEAN:
168 		break;
169 	default:
170 		break;
171 	}
172 	return false;
173 error_handler:
174 	// directly using gtk to display an error message since we dont't know about Application there
175 	GtkWidget *widget = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (grid))),
176 	                                            GTK_DIALOG_MODAL,
177 	                                            GTK_MESSAGE_ERROR,
178 	                                            GTK_BUTTONS_CLOSE,
179 	                                            _("Invalid data"));
180 	gtk_dialog_run (GTK_DIALOG (widget));
181 	grid->sel_start = 0;
182 	grid->cursor_index = new_string.length ();
183 	return false;
184 }
185 
186 // overriden methods
187 
gcr_grid_draw(GtkWidget * w,cairo_t * cr)188 static gboolean gcr_grid_draw (GtkWidget *w, cairo_t* cr)
189 {
190 	GcrGrid *grid = reinterpret_cast < GcrGrid * > (w);
191 	GtkStyleContext *ctxt = gtk_widget_get_style_context (w);
192 	unsigned i, j;
193 	GtkAllocation alloc;
194 	gtk_widget_get_allocation (w, &alloc);
195 	gtk_style_context_save (ctxt);
196 	gtk_style_context_add_class (ctxt, GTK_STYLE_CLASS_BUTTON);
197 	int pos = grid->header_width, y = grid->line_offset, width;
198 	PangoLayout *l = gtk_widget_create_pango_layout (w, "");
199 	cairo_save (cr);
200 	cairo_set_source_rgb (cr, 0.7, 0.7, 0.7);
201 	cairo_rectangle (cr, 0, 0, grid->width, grid->row_height + 1);
202 	cairo_fill (cr);
203 	cairo_restore (cr);
204 	gtk_render_background (ctxt, cr, 0, 0, grid->header_width + 1, grid->row_height + 1);
205 	gtk_render_frame (ctxt, cr, 0, 0, grid->header_width + 1, grid->row_height + 1);
206 	for (i = 0; i < grid->cols; i++) {
207 		gtk_style_context_set_state (ctxt, (static_cast < int > (i) == grid->col)? GTK_STATE_FLAG_ACTIVE: GTK_STATE_FLAG_NORMAL);
208 		gtk_render_background (ctxt, cr, pos, 0, grid->col_widths[i] + 1, grid->row_height + 1);
209 		gtk_render_frame (ctxt, cr, pos, 0, grid->col_widths[i] + 1, grid->row_height + 1);
210 		pango_layout_set_markup (l, grid->titles[i].c_str (), -1);
211 		pango_layout_get_pixel_size (l, &width, NULL);
212 		cairo_move_to (cr, pos + (grid->col_widths[i] - width) / 2, y);
213 		pango_cairo_show_layout (cr, l);
214 		pos += grid->col_widths[i];
215 	}
216 	gtk_style_context_set_state (ctxt, GTK_STATE_FLAG_NORMAL);
217 	gtk_render_background (ctxt, cr, pos, 0, grid->scroll_width, grid->row_height + 1);
218 	gtk_render_frame (ctxt, cr, pos, 0, grid->scroll_width, grid->row_height + 1);
219 	y = grid->row_height;
220 	cairo_set_line_width (cr, 1.);
221 	// draw row headers
222 	int row = grid->first_visible;
223 	unsigned max = (grid->nb_visible >= grid->rows - grid->first_visible)? grid->rows - grid->first_visible: grid->nb_visible + 1;
224 	for (j = 0; j < max; j++) {
225 		cairo_save (cr);
226 		cairo_set_source_rgb (cr, 0.7, 0.7, 0.7);
227 		cairo_rectangle (cr, 0, y, grid->header_width + 1, grid->row_height + 1);
228 		cairo_fill (cr);
229 		cairo_restore (cr);
230 		gtk_style_context_set_state (ctxt, (row == grid->row ||
231 		                                    grid->selected_rows->find (row) != grid->selected_rows->end ())?
232 		                             		GTK_STATE_FLAG_ACTIVE: GTK_STATE_FLAG_NORMAL);
233 		gtk_render_background (ctxt, cr, 0, y, grid->header_width + 1, grid->row_height + 1);
234 		gtk_render_frame (ctxt, cr, 0, y, grid->header_width + 1, grid->row_height + 1);
235 		char *buf = g_strdup_printf("%d", ++row);
236 		pango_layout_set_text (l, buf, -1);
237 		pango_layout_get_pixel_size (l, &width, NULL);
238 		cairo_move_to (cr, (grid->header_width - width) / 2, y + grid->line_offset);
239 		pango_cairo_show_layout (cr, l);
240 		g_free (buf);
241 		y += grid->row_height;
242 	}
243 	y = grid->row_height;
244 	cairo_save (cr);
245 	cairo_rectangle (cr, grid->header_width, y, alloc.width - grid->header_width, alloc.height - y);
246 	cairo_clip (cr);
247 	// show a rectangle around current selection
248 	if (grid->row >= 0) {
249 		pos = grid->header_width;
250 		cairo_save (cr);
251 		if (grid->col >= 0) {
252 			for (i = 0; static_cast < int > (i) < grid->col; i++)
253 				pos += grid->col_widths[i];
254 			cairo_rectangle (cr, pos + .5,
255 			                 y + (grid->row - grid->first_visible) * grid->row_height + .5,
256 			                 grid->col_widths[grid->col], grid->row_height);
257 		} else
258 				cairo_rectangle (cr, pos + .5,
259 				                 y + (grid->row - grid->first_visible) * grid->row_height + .5,
260 				                 grid->actual_width, grid->row_height);
261 		cairo_set_line_width (cr, 3.);
262 		cairo_stroke_preserve (cr);
263 		cairo_restore (cr);
264 	}
265 	cairo_set_line_width (cr, 1.);
266 	// draw cells
267 	row = grid->first_visible;
268 	for (j = 0; j < max; j++) {
269 		pos = grid->header_width;
270 		for (i = 0; i < grid->cols; i++) {
271 			cairo_save (cr);
272 			cairo_rectangle (cr, pos + .5, y + .5, grid->col_widths[i], grid->row_height);
273 			cairo_set_source_rgb (cr, 0.7, 0.7, 0.7);
274 			cairo_stroke (cr);
275 			cairo_restore (cr);
276 			if (grid->types[i] == G_TYPE_BOOLEAN) {
277 				GdkPixbuf *pixbuf = (grid->row_data[row][i] == "t")? checked: unchecked;
278 				cairo_save (cr);
279 				cairo_translate (cr, pos + .5 * (grid->col_widths[i] - grid->row_height), y);
280 				cairo_rectangle (cr, 2., 2.,
281 				                 grid->row_height - 4, grid->row_height - 4);
282 				gdk_cairo_set_source_pixbuf (cr, pixbuf, 0., 0.);
283 				cairo_fill (cr);
284 				cairo_restore (cr);
285 			} else {
286 				pango_layout_set_text (l, grid->row_data[row][i].c_str(), -1);
287 				pango_layout_get_pixel_size (l, &width, NULL);
288 				pango_layout_set_markup (l, grid->row_data[row][i].c_str(), -1);
289 				if (static_cast < int > (row) == grid->row && static_cast < int > (i) == grid->col) {
290 					if (grid->cursor_index != grid->sel_start) {
291 						PangoAttrList *al = pango_attr_list_new ();
292 						int start, end;
293 						if (grid->cursor_index < grid->sel_start) {
294 							start = grid->cursor_index;
295 							end = grid->sel_start;
296 						} else {
297 							end = grid->cursor_index;
298 							start = grid->sel_start;
299 						}
300 						PangoAttribute *attr = pango_attr_foreground_new (0xffff, 0xffff, 0xffff);
301 						attr->start_index = start;
302 						attr->end_index =end;
303 						pango_attr_list_insert (al, attr);
304 						attr = pango_attr_background_new (0, 0, 0);
305 						attr->start_index = start;
306 						attr->end_index =end;
307 						pango_attr_list_insert (al, attr);
308 						pango_layout_set_attributes (l, al);
309 						pango_attr_list_unref (al);
310 					}
311 					if (grid->cursor_visible) {
312 						PangoRectangle rect;
313 						pango_layout_get_cursor_pos (l, grid->cursor_index, &rect, NULL);
314 						cairo_move_to (cr, pos + (grid->col_widths[i] - width) / 2 + rect.x / PANGO_SCALE + .5, y + grid->line_offset + rect.y / PANGO_SCALE);
315 						cairo_rel_line_to (cr, 0, rect.height / PANGO_SCALE);
316 						cairo_stroke (cr);
317 					}
318 				}
319 				cairo_move_to (cr, pos + (grid->col_widths[i] - width) / 2, y + grid->line_offset);
320 				pango_cairo_show_layout (cr, l);
321 			}
322 			pos += grid->col_widths[i];
323 		}
324 
325 		row++;
326 		y += grid->row_height;
327 	}
328 	cairo_restore (cr);
329 	gtk_style_context_restore (ctxt);
330 	return parent_class->draw (w, cr);
331 }
332 
gcr_grid_get_preferred_height(GtkWidget * w,int * min,int * preferred)333 static void gcr_grid_get_preferred_height (GtkWidget *w, int *min, int *preferred)
334 {
335 	*min = *preferred = reinterpret_cast < GcrGrid * > (w)->row_height * 6 + 1;
336 }
337 
gcr_grid_get_preferred_width(GtkWidget * w,int * min,int * preferred)338 static void gcr_grid_get_preferred_width (GtkWidget *w, int *min, int *preferred)
339 {
340 	*min = *preferred = reinterpret_cast < GcrGrid * > (w)->width;
341 }
342 
gcr_grid_unrealize(GtkWidget * w)343 static void gcr_grid_unrealize (GtkWidget *w)
344 {
345 	GcrGrid *grid = GCR_GRID (w);
346 	if (grid->cursor_signal > 0)
347 		g_source_remove (grid->cursor_signal);
348 	parent_class->unrealize (w);
349 }
350 
gcr_grid_focus_out_event(GtkWidget * widget,G_GNUC_UNUSED GdkEventFocus * event)351 static gboolean gcr_grid_focus_out_event (GtkWidget *widget, G_GNUC_UNUSED GdkEventFocus *event)
352 {
353 	GcrGrid *grid = GCR_GRID (widget);
354 	if (!gcr_grid_validate_change (grid))
355 		gtk_widget_grab_focus (widget);
356 	else {
357 		if (grid->cursor_signal)
358 			g_source_remove (grid->cursor_signal);
359 		grid->cursor_signal = 0;
360 		grid->col = -1;
361 		grid->dragging = false;
362 		gtk_widget_queue_draw (widget);
363 	}
364 	return true;
365 }
366 
gcr_grid_grab_notify(GtkWidget * widget,gboolean)367 static void gcr_grid_grab_notify (GtkWidget *widget, gboolean)
368 {
369 	GCR_GRID (widget)->dragging = false;
370 }
371 
gcr_grid_finalize(GObject * obj)372 static void gcr_grid_finalize (GObject *obj)
373 {
374 	GcrGrid *grid = reinterpret_cast < GcrGrid * > (obj);
375 	delete [] grid->col_widths;
376 	delete [] grid->titles;
377 	delete [] grid->types;
378 	delete [] grid->editable;
379 	for (unsigned i = 0 ; i < grid->rows; i++)
380 		delete [] grid->row_data[i];
381 	delete grid->selected_rows;
382 	reinterpret_cast < GObjectClass * > (parent_class)->finalize (obj);
383 }
384 
gcr_grid_size_allocate(GtkWidget * w,GtkAllocation * alloc)385 static void gcr_grid_size_allocate (GtkWidget *w, GtkAllocation *alloc)
386 {
387 	GcrGrid *grid = GCR_GRID (w);
388 	gtk_layout_move (GTK_LAYOUT (grid), grid->scroll, alloc->width - grid->scroll_width, grid->row_height + 1);
389 	g_object_set (G_OBJECT (grid->scroll), "height-request", alloc->height - grid->row_height - 1, NULL); //default size
390 	grid->nb_visible = alloc->height / grid->row_height - 1; // -1 to avoid counting the header
391 	if (grid->rows) {
392 		gtk_adjustment_set_page_size (grid->vadj, static_cast < double > (grid->nb_visible) / grid->rows);
393 		gtk_adjustment_set_upper (grid->vadj, (grid->nb_visible < grid->rows)? grid->rows - grid->nb_visible: .1);
394 		if (grid->rows < grid->nb_visible + grid->first_visible) {
395 			grid->first_visible = (grid->nb_visible < grid->rows)? grid->rows - grid->nb_visible: 0;
396 			gtk_adjustment_set_value (grid->vadj, grid->first_visible);
397 		}
398 	} else
399 		gtk_adjustment_set_page_size (grid->vadj, 1.);
400 	// adjust column widths
401 	grid->actual_width = alloc->width - grid->header_width - grid->scroll_width;
402 	double ratio = static_cast < double > (grid->actual_width) / grid->cols_min_width;
403 	if (ratio < 0.)
404 		ratio = 1.;
405 	double last_pos = 0., pos = 0.;
406 	for (unsigned i = 0; i < grid->cols; i++) {
407 		pos += grid->min_widths[i];
408 		grid->col_widths[i] = pos * ratio - last_pos;
409 		last_pos += grid->col_widths[i];
410 	}
411 	parent_class->size_allocate (w, alloc);
412 }
413 
on_blink(gpointer data)414 static gint on_blink (gpointer data)
415 {
416 	GcrGrid *grid = GCR_GRID (data);
417 	grid->cursor_signal = g_timeout_add (((grid->cursor_visible)? CURSOR_OFF_TIME: CURSOR_ON_TIME), on_blink, data);
418 
419 	grid->cursor_visible = !grid->cursor_visible;
420 	gtk_widget_queue_draw (GTK_WIDGET (data));
421 	/* Remove ourself */
422 	return false;
423 }
424 
gcr_grid_button_press_event(GtkWidget * widget,GdkEventButton * event)425 static gboolean gcr_grid_button_press_event (GtkWidget *widget, GdkEventButton *event)
426 {
427 	if (event->button != 1)
428 		return false;	// FIXME: at least middle buttons should be accepted to paste data
429 	GcrGrid *grid = GCR_GRID (widget);
430 	if ((event->state & (GDK_CONTROL_MASK | GDK_SHIFT_MASK)) == 0)
431 		grid->selected_rows->clear ();
432 	int value_changed = -1;
433 	int x = grid->first_visible + event->y / grid->row_height - 1, i, new_row, new_col;
434 	new_row = (x < 0 || x >= static_cast < int > (grid->rows))? -1: x;
435 	grid->dragging = true;
436 	if (new_row < 0)
437 		new_col = -1;
438 	else {
439 		new_col = -1;
440 		x = grid->header_width;
441 		if (event->x >= x)
442 			for (i = 0; i < static_cast < int > (grid->cols); i++) {
443 				x += grid->col_widths[i];
444 				if (event->x < x) {
445 					new_col = i;
446 					break;
447 				}
448 			}
449 	}
450 	if (grid->col != new_col || grid->row != new_row) {
451 		if (grid->row >= 0 && !gcr_grid_validate_change (grid))
452 			return true;
453 		if (grid->allow_multiple && event->state & GDK_SHIFT_MASK) {
454 			new_col = -1;
455 			if (new_row != grid->last_row) {
456 				int incr = (grid->last_row > grid->row)? -1: 1;
457 				for (x = grid->last_row; x != grid->row; x += incr)
458 					grid->selected_rows->erase (x);
459 				incr = (new_row > grid->row)? -1: 1;
460 				for (x = new_row; x != grid->row; x += incr)
461 					grid->selected_rows->insert (x);
462 				grid->last_row = new_row;
463 			}
464 		} else if (grid->allow_multiple && event->state & GDK_CONTROL_MASK) {
465 			new_col = -1;
466 			if (grid->selected_rows->find (new_row) != grid->selected_rows->end ()) {
467 				grid->selected_rows->erase (new_row);
468 				new_row = grid->row;
469 			} else
470 				grid->selected_rows->insert (grid->row);
471 			if (new_row != grid->row)
472 				g_signal_emit (grid, gcr_grid_signals[ROW_SELECTED], 0, new_row);
473 			grid->row = grid->last_row = new_row;
474 		} else {
475 			if (new_col >= 0 && !grid->editable[new_col])
476 				new_col = -1;
477 			grid->col = new_col;
478 			if (new_row != grid->row) {
479 				grid->row = grid->last_row = new_row;
480 				g_signal_emit (grid, gcr_grid_signals[ROW_SELECTED], 0, new_row);
481 			}
482 		}
483 	}
484 	// evaluate the cursor position if any
485 	if (grid->col >= 0) {
486 		switch (grid->types[grid->col]) {
487 		case G_TYPE_INT:
488 		case G_TYPE_UINT:
489 		case G_TYPE_DOUBLE:
490 			if (event->type == GDK_BUTTON_PRESS) {
491 				x -=  grid->col_widths[grid->col];
492 				PangoLayout *l = gtk_widget_create_pango_layout (widget, grid->row_data[grid->row][grid->col].c_str());
493 				int xpos, startx;
494 				pango_layout_get_pixel_size (l, &xpos, NULL);
495 				startx = x + (grid->col_widths[grid->col] - xpos) / 2;
496 				xpos = event->x - startx;
497 				int index, trailing;
498 				pango_layout_xy_to_index (l, xpos * PANGO_SCALE, 0, &index, &trailing);
499 				index += trailing;
500 				grid->cursor_index = index;
501 				if ((event->state & GDK_SHIFT_MASK) == 0)
502 					grid->sel_start = index;
503 			} else if (event->type == GDK_2BUTTON_PRESS) {
504 				grid->sel_start = 0;
505 				grid->cursor_index = grid->row_data[grid->row][grid->col].length ();
506 			}
507 			break;
508 		case G_TYPE_BOOLEAN:
509 			grid->cursor_index = -1;
510 			// nothing to do, just wait and see if the mouse button is released inside the cell
511 			// for now toggle the button if the click occurs near enough
512 			x = event->x - x + grid->col_widths[new_col] / 2.;
513 			if (fabs (x) < grid->row_height / 2) {
514 				grid->row_data[grid->row][grid->col] = (grid->row_data[grid->row][grid->col] == "t")? "f": "t";
515 				value_changed = new_col;
516 			}
517 			break;
518 		default:
519 			grid->cursor_index = -1;
520 			g_critical ("Unsupported type.");
521 			break;
522 		}
523 		*grid->orig_string = grid->row_data[grid->row][grid->col];
524 	} else
525 		grid->cursor_index = -1;
526 	if (grid->cursor_index >= 0 && grid->cursor_signal == 0) {
527 		grid->cursor_signal = g_timeout_add (CURSOR_ON_TIME, on_blink, grid);
528 		grid->cursor_visible = true;
529 	} else if (grid->cursor_index < 0 && grid->cursor_signal != 0) {
530 		g_source_remove (grid->cursor_signal);
531 		grid->cursor_signal = 0;
532 	}
533 	if (value_changed >= 0) // a boolean changed
534 		g_signal_emit (grid, gcr_grid_signals[VALUE_CHANGED], 0, new_row, new_col);
535 	gtk_widget_grab_focus (widget);
536 	gtk_widget_queue_draw (widget);
537 	return true;
538 }
539 
gcr_grid_motion_notify_event(GtkWidget * widget,GdkEventMotion * event)540 static gboolean gcr_grid_motion_notify_event (GtkWidget *widget, GdkEventMotion *event)
541 {
542 	GcrGrid *grid = GCR_GRID (widget);
543 	if (!grid->dragging)
544 		return true;
545 	// evaluate the current row
546 	int x = grid->first_visible + event->y / grid->row_height - 1, new_row, incr;
547 	new_row = (x < 0 || x >= static_cast < int > (grid->rows))? -1: x;
548 	// update selection
549 	if (new_row != grid->last_row && grid->allow_multiple) {
550 		incr = (grid->last_row > grid->row)? -1: 1;
551 		for (x = grid->last_row; x != grid->row; x += incr)
552 			grid->selected_rows->erase (x);
553 		incr = (new_row > grid->row)? -1: 1;
554 		for (x = new_row; x != grid->row; x += incr)
555 			grid->selected_rows->insert (x);
556 		grid->last_row = new_row;
557 		grid->col = -1;
558 	} else if (grid->col >= 0) {
559 		int new_col = -1;
560 		x = grid->header_width;
561 		if (event->x >= x)
562 			for (incr = 0; incr < static_cast < int > (grid->cols); incr++) {
563 				x += grid->col_widths[incr];
564 				if (event->x < x) {
565 					new_col = incr;
566 					break;
567 				}
568 			}
569 		if (new_col == grid->col) {
570 			// evaluate the cursor position if any
571 			if (new_col >= 0) {
572 				switch (grid->types[grid->col]) {
573 				case G_TYPE_INT:
574 				case G_TYPE_UINT:
575 				case G_TYPE_DOUBLE: {
576 					x -=  grid->col_widths[grid->col];
577 					PangoLayout *l = gtk_widget_create_pango_layout (widget, grid->row_data[grid->row][grid->col].c_str());
578 					int xpos, startx;
579 					pango_layout_get_pixel_size (l, &xpos, NULL);
580 					startx = x + (grid->col_widths[grid->col] - xpos) / 2;
581 					xpos = event->x - startx;
582 					int index, trailing;
583 					pango_layout_xy_to_index (l, xpos * PANGO_SCALE, 0, &index, &trailing);
584 					grid->cursor_index = index + trailing;
585 					break;
586 				}
587 				default:	// nothing to do
588 					break;
589 				}
590 				*grid->orig_string = grid->row_data[grid->row][grid->col];
591 			}
592 		} else
593 			grid->col = -1;
594 	}
595 	gtk_widget_grab_focus (widget);
596 	gtk_widget_queue_draw (widget);
597 	return true;
598 }
599 
gcr_grid_button_release_event(GtkWidget * widget,G_GNUC_UNUSED GdkEventButton * event)600 static gboolean gcr_grid_button_release_event (GtkWidget *widget, G_GNUC_UNUSED GdkEventButton *event)
601 {
602 	GcrGrid *grid = GCR_GRID (widget);
603 	grid->dragging = false;
604 	return true;
605 }
606 
gcr_grid_scroll_event(GtkWidget * widget,GdkEventScroll * event)607 static gboolean gcr_grid_scroll_event (GtkWidget *widget, GdkEventScroll *event)
608 {
609 	return gtk_widget_event (GCR_GRID (widget)->scroll, reinterpret_cast < GdkEvent * > (event));
610 }
611 
gcr_grid_key_press_event(GtkWidget * widget,GdkEventKey * event)612 static gboolean gcr_grid_key_press_event (GtkWidget *widget, GdkEventKey *event)
613 {
614 	GcrGrid *grid = GCR_GRID (widget);
615 	if (grid->row < 0)
616 		return false;
617 	int new_row = grid->last_row, new_col = grid->col, new_index = grid->cursor_index;
618 	char new_char = 0;
619 	switch (event->keyval) {
620 	case GDK_KEY_Tab:
621 	case GDK_KEY_ISO_Left_Tab:
622 		if (event->state & GDK_CONTROL_MASK)
623 			return false; // give up the grab
624 		if (grid->col > 0 && !gcr_grid_validate_change (grid))
625 			return true;
626 		if (grid->can_edit == 0 && (new_row + 1 < static_cast < int > (grid->rows))) {
627 			new_row++;
628 			break;
629 		}
630 		if (event->state & GDK_SHIFT_MASK) {
631 			do {
632 				if (new_col > 0)
633 					new_col--;
634 				else if (new_row > 0) {
635 					new_row--;
636 					new_col = grid->cols - 1;
637 				} else
638 					return true;
639 			} while (!grid->editable[new_col]);
640 		} else {
641 			do {
642 				if (new_col < static_cast < int > (grid->cols) - 1)
643 					new_col++;
644 				else if (new_row < static_cast < int > (grid->rows) - 1) {
645 					new_row++;
646 					new_col = 0;
647 				} else
648 					return true;
649 			} while (!grid->editable[new_col]);
650 		}
651 		new_index = grid->row_data[new_row][new_col].length ();
652 		grid->sel_start = 0;
653 		break;
654 	case GDK_KEY_Left:
655 	case GDK_KEY_KP_Left:
656 		if (new_index > 0) {
657 			new_index = g_utf8_prev_char (grid->row_data[new_row][new_col].c_str () + grid->cursor_index) - grid->row_data[new_row][new_col].c_str ();
658 			if ((event->state & GDK_SHIFT_MASK) == 0)
659 				grid->sel_start = new_index;
660 		} else {
661 			if (event->state & GDK_SHIFT_MASK) {
662 				if (new_col < static_cast < int > (grid->cols) - 1)
663 					new_col = -1; // select the whole line
664 				grid->sel_start = new_index = -1;
665 			} else {
666 				do {
667 					if (new_col > 0)
668 						new_col--;
669 					else if (new_row > 0) {
670 						new_row--;
671 						new_col = grid->cols - 1;
672 					} else
673 						return true;
674 				} while (!grid->editable[new_col]);
675 				new_index = grid->row_data[new_row][new_col].length ();
676 			}
677 		}
678 		break;
679 	case GDK_KEY_Right:
680 	case GDK_KEY_KP_Right:
681 		if (new_index >= 0 && grid->cursor_index < static_cast < int > (grid->row_data[new_row][new_col].length ())) {
682 			new_index = g_utf8_next_char (grid->row_data[new_row][new_col].c_str () + grid->cursor_index) - grid->row_data[new_row][new_col].c_str ();
683 			if ((event->state & GDK_SHIFT_MASK) == 0)
684 				grid->sel_start = new_index;
685 		} else {
686 			if (event->state & GDK_SHIFT_MASK) {
687 				if (new_col < static_cast < int > (grid->cols) - 1)
688 					new_col = -1; // select the whole line
689 				grid->sel_start = new_index = -1;
690 			} else {
691 				do {
692 					if (new_col < static_cast < int > (grid->cols) - 1)
693 						new_col++;
694 					else if (grid->row < static_cast < int > (grid->rows) - 1) {
695 						new_row++;
696 						new_col = 0;
697 					} else
698 						return true;
699 				} while (!grid->editable[new_col]);
700 				grid->selected_rows->clear ();
701 				grid->sel_start = new_index = 0;
702 			}
703 		}
704 		break;
705 	case GDK_KEY_Up:
706 	case GDK_KEY_KP_Up:
707 		if (event->state & GDK_SHIFT_MASK) {
708 			new_col = -1; // select the whole line
709 			grid->sel_start = new_index = -1;
710 			if (new_row > 0) {
711 				int row = new_row - 1;
712 				if (row < grid->row)
713 					gcr_grid_add_row_to_selection (grid, row);
714 				else
715 					gcr_grid_unselect_row (grid, grid->last_row);
716 				grid->last_row = new_row = row;
717 			}
718 		} else if (grid->row > 0)
719 			new_row--;
720 		break;
721 	case GDK_KEY_Down:
722 	case GDK_KEY_KP_Down:
723 		if (event->state & GDK_SHIFT_MASK) {
724 			new_col = -1; // select the whole line
725 			grid->sel_start = new_index = -1;
726 			if (new_row < static_cast < int > (grid->rows) - 1) {
727 				int row = new_row + 1;
728 				if (row > grid->row)
729 					gcr_grid_add_row_to_selection (grid, row);
730 				else
731 					gcr_grid_unselect_row (grid, grid->last_row);
732 				grid->last_row = new_row = row;
733 			}
734 		} else if (grid->row < static_cast < int > (grid->rows) - 1)
735 			new_row++;
736 		break;
737 	case GDK_KEY_Page_Up:
738 	case GDK_KEY_KP_Page_Up:
739 		if (event->state & GDK_SHIFT_MASK) {
740 			new_col = -1; // select the whole line
741 			grid->sel_start = new_index = -1;
742 			int i, row = (grid->row >= static_cast < int > (grid->nb_visible))?
743 							new_row - grid->nb_visible: 0;
744 			for (i = row; i < new_row; i++) {
745 				if (i < grid->row)
746 					gcr_grid_add_row_to_selection (grid, i);
747 				else
748 					gcr_grid_unselect_row (grid, i);
749 			}
750 			grid->last_row = new_row = row;
751 		} else if (grid->row >= static_cast < int > (grid->nb_visible))
752 			new_row -= grid->nb_visible;
753 		else
754 			new_row = 0;
755 		break;
756 	case GDK_KEY_Page_Down:
757 	case GDK_KEY_KP_Page_Down:
758 		if (event->state & GDK_SHIFT_MASK) {
759 			new_col = -1; // select the whole line
760 			grid->sel_start = new_index = -1;
761 			int i, row = (grid->rows > grid->nb_visible && grid->row < static_cast < int > (grid->rows - grid->nb_visible) - 1)?
762 							new_row + grid->nb_visible: grid->rows - 1;
763 			for (i = row; i > new_row; i--) {
764 				if (i > grid->row)
765 					gcr_grid_add_row_to_selection (grid, i);
766 				else
767 					gcr_grid_unselect_row (grid, i);
768 			}
769 			grid->last_row = new_row = row;
770 		} if (grid->rows > grid->nb_visible && grid->row < static_cast < int > (grid->rows - grid->nb_visible) - 1)
771 			new_row += grid->nb_visible;
772 		else
773 			new_row = grid->rows - 1;
774 		break;
775 	case GDK_KEY_KP_Subtract:
776 	case GDK_KEY_minus:
777 		if (grid->types[new_col] == G_TYPE_INT || grid->types[new_col] == G_TYPE_DOUBLE) {
778 			if ((new_index > 0 && grid->sel_start > 0) || !grid->row_data[new_row][new_col].compare (0, strlen ("−"), "−"))
779 				return true;
780 			grid->row_data[new_row][new_col].insert (0, "−");
781 			grid->sel_start = new_index = strlen ("−");
782 		}
783 		break;
784 	case GDK_KEY_0:
785 	case GDK_KEY_1:
786 	case GDK_KEY_2:
787 	case GDK_KEY_3:
788 	case GDK_KEY_4:
789 	case GDK_KEY_5:
790 	case GDK_KEY_6:
791 	case GDK_KEY_7:
792 	case GDK_KEY_8:
793 	case GDK_KEY_9:
794 		if (new_index < 0)
795 			return true;
796 		new_char = '0' + event->keyval - GDK_KEY_0;
797 		break;
798 	case GDK_KEY_KP_0:
799 	case GDK_KEY_KP_1:
800 	case GDK_KEY_KP_2:
801 	case GDK_KEY_KP_3:
802 	case GDK_KEY_KP_4:
803 	case GDK_KEY_KP_5:
804 	case GDK_KEY_KP_6:
805 	case GDK_KEY_KP_7:
806 	case GDK_KEY_KP_8:
807 	case GDK_KEY_KP_9:
808 		if (new_index < 0)
809 			return true;
810 		new_char = '0' + event->keyval - GDK_KEY_KP_0;
811 		break;
812 	case GDK_KEY_Home:
813 	case GDK_KEY_KP_Home:
814 	case GDK_KEY_KP_Begin:
815 		if (event->state & GDK_CONTROL_MASK) {
816 			new_col = 0;
817 			if (event->state & GDK_SHIFT_MASK)
818 				new_row = 0;
819 			grid->sel_start = 0;
820 			new_index = grid->row_data[new_row][0].length ();
821 			break;
822 		}
823 		if (new_index <= 0)
824 			return true;
825 		new_index = 0;
826 		if ((event->state & GDK_SHIFT_MASK) == 0)
827 			grid->sel_start = 0;
828 		break;
829 	case GDK_KEY_End:
830 	case GDK_KEY_KP_End:
831 		if (event->state & GDK_CONTROL_MASK) {
832 			new_col = grid->cols - 1;
833 			if (event->state & GDK_SHIFT_MASK)
834 				new_row = grid->rows - 1;
835 			grid->sel_start = 0;
836 			new_index = grid->row_data[new_row][new_col].length ();
837 			break;
838 		}
839 		if (new_index == static_cast < int > (grid->row_data[new_row][new_col].length ()))
840 			return true;
841 		new_index = grid->row_data[new_row][new_col].length ();
842 		if ((event->state & GDK_SHIFT_MASK) == 0)
843 			grid->sel_start = new_index;
844 		break;
845 	case GDK_KEY_Return:
846 	case GDK_KEY_KP_Enter:
847 		if (new_index <= 0)
848 			return true;
849 		gcr_grid_validate_change (grid);
850 		return true;
851 	case GDK_KEY_Delete:
852 	case GDK_KEY_KP_Delete:
853 		if (grid->sel_start == grid->cursor_index) {
854 			if (grid->sel_start == static_cast < int > (grid->row_data[new_row][new_col].length ()))
855 				return true;
856 			char const *start =  grid->row_data[new_row][new_col].c_str () + grid->sel_start, *end;
857 			end = g_utf8_next_char (start);
858 			grid->cursor_index = grid->sel_start + (end - start);
859 		} else if (grid->sel_start > grid->cursor_index) {
860 			int buf = grid->sel_start;
861 			grid->sel_start = grid->cursor_index;
862 			grid->cursor_index = buf;
863 		}
864 		grid->row_data[new_row][new_col].erase (grid->sel_start, grid->cursor_index - grid->sel_start);
865 		new_index = grid->sel_start;
866 		break;
867 	case GDK_KEY_BackSpace:
868 		if (grid->sel_start == grid->cursor_index) {
869 			if (grid->sel_start == 0)
870 				return true;
871 			char const *start =  grid->row_data[new_row][new_col].c_str () + grid->sel_start, *end;
872 			end = g_utf8_prev_char (start);
873 			grid->sel_start = grid->cursor_index - (start - end);
874 		} else if (grid->sel_start > grid->cursor_index) {
875 			int buf = grid->sel_start;
876 			grid->sel_start = grid->cursor_index;
877 			grid->cursor_index = buf;
878 		}
879 		grid->row_data[new_row][new_col].erase (grid->sel_start, grid->cursor_index - grid->sel_start);
880 		new_index = grid->sel_start;
881 		break;
882 	case GDK_KEY_period: {
883 		int pos, start, end;
884 		char const *sep;
885 		if (new_index < grid->sel_start) {
886 			start = new_index;
887 			end = grid->sel_start;
888 		} else {
889 			start = grid->sel_start;
890 			end = new_index;
891 		}
892 		if (grid->types[new_col] != G_TYPE_DOUBLE ||
893 		    strcmp (go_locale_get_decimal ()->str, ".") ||
894 		    ((sep = strchr (grid->row_data[new_row][new_col].c_str (), '.')) != NULL &&
895 		    (pos = sep - grid->row_data[new_row][new_col].c_str (),
896 		     pos < start || pos > end)))
897 			return true;
898 		new_char = '.';
899 		break;
900 	}
901 	case GDK_KEY_comma: {
902 		int pos, start, end;
903 		char const *sep;
904 		if (new_index < grid->sel_start) {
905 			start = new_index;
906 			end = grid->sel_start;
907 		} else {
908 			start = grid->sel_start;
909 			end = new_index;
910 		}
911 		if (grid->types[new_col] != G_TYPE_DOUBLE ||
912 		    strcmp (go_locale_get_decimal ()->str, ",") ||
913 		    ((sep = strchr (grid->row_data[new_row][new_col].c_str (), ',')) != NULL &&
914 		    (pos = sep - grid->row_data[new_row][new_col].c_str (),
915 		     pos < start || pos > end)))
916 			return true;
917 		new_char = ',';
918 		break;
919 	}
920 	case GDK_KEY_KP_Decimal: {
921 		int pos, start, end;
922 		char const *sep;
923 		if (new_index < grid->sel_start) {
924 			start = new_index;
925 			end = grid->sel_start;
926 		} else {
927 			start = grid->sel_start;
928 			end = new_index;
929 		}
930 		if (grid->types[new_col] != G_TYPE_DOUBLE ||
931 		    ((sep = strstr (grid->row_data[new_row][new_col].c_str (), go_locale_get_decimal ()->str)) != NULL &&
932 		    (pos = sep - grid->row_data[new_row][new_col].c_str (),
933 		     pos < start || pos > end)))
934 			return true;
935 		// don't add anything before the minus sign
936 		if (new_index == 0 && grid->sel_start == 0 && !grid->row_data[new_row][new_col].compare (0, strlen ("−"), "−"))
937 			return true;
938 		// first delete the selected chars if any
939 		if (new_index != grid->sel_start) {
940 			int length;
941 			if (new_index < grid->sel_start)
942 				length = grid->sel_start - new_index;
943 			else {
944 				length = new_index - grid->sel_start;
945 				new_index = grid->sel_start;
946 			}
947 			grid->row_data[new_row][new_col].erase (new_index, length);
948 		}
949 		if (grid->cursor_index == 0 && !strncmp (grid->row_data[new_row][new_col].c_str (), "−", strlen ("−")))
950 		    return true;	// do not insert a figure before the minus sign
951 		// insert the new char(s)
952 		grid->row_data[new_row][new_col].insert (new_index, go_locale_get_decimal ()->str);
953 		new_index += go_locale_get_decimal ()->len;
954 		grid->sel_start = new_index;
955 		break;
956 	}
957 	case GDK_KEY_space:
958 		if (grid->types[new_col] != G_TYPE_BOOLEAN)
959 			return true;
960 		grid->row_data[new_row][new_col] = (grid->row_data[new_row][new_col] == "t")? "f": "t";
961 		g_signal_emit (grid, gcr_grid_signals[VALUE_CHANGED], 0, new_row, new_col);
962 		break;
963 	default:
964 		return true;
965 	}
966 	if (new_char > 0) {
967 		// don't add aanything before the minus sign
968 		if (new_index == 0 && grid->sel_start == 0 && !grid->row_data[new_row][new_col].compare (0, strlen ("−"), "−"))
969 			return true;
970 		// first delete the selected chars if any
971 		if (new_index != grid->sel_start) {
972 			int length;
973 			if (new_index < grid->sel_start)
974 				length = grid->sel_start - new_index;
975 			else {
976 				length = new_index - grid->sel_start;
977 				new_index = grid->sel_start;
978 			}
979 			grid->row_data[new_row][new_col].erase (new_index, length);
980 		}
981 		// insert the new char
982 		if (grid->cursor_index == 0 && !strncmp (grid->row_data[new_row][new_col].c_str (), "−", strlen ("−")))
983 		    return true;	// do not insert a figure before the minus sign
984 		grid->row_data[new_row][new_col].insert (new_index, 1, new_char);
985 		new_index++;
986 		grid->sel_start = new_index;
987 	}
988 	if (new_row != grid->last_row || new_col != grid->col) {
989 		if (grid->row >= 0 && grid->col >= 0 && !gcr_grid_validate_change (grid))
990 			return true;
991 		if (new_row != grid->row)
992 			g_signal_emit (grid, gcr_grid_signals[ROW_SELECTED], 0, new_row);
993 		if (new_col >= 0 && grid->editable[new_col]) {
994 			grid->row = new_row;
995 			grid->col = new_col;
996 			*grid->orig_string = grid->row_data[new_row][new_col];
997 			int l = grid->orig_string->length ();
998 			if (new_index > l)
999 				new_index = grid->sel_start = l;
1000 		} else if (new_col >= 0) {
1001 			grid->row = grid->col = new_index = -1;
1002 			g_signal_emit (grid, gcr_grid_signals[ROW_SELECTED], 0, -1);
1003 		} else
1004 			grid->col = new_col;
1005 		if ((event->state & GDK_SHIFT_MASK) == 0)
1006 			grid->last_row = grid->row;
1007 	}
1008 	// ensure that the selection is visible
1009 	if (grid->last_row < grid->first_visible)
1010 		grid->first_visible = grid->last_row;
1011 	else if (grid->last_row >= grid->first_visible + static_cast < int > (grid->nb_visible))
1012 		grid->first_visible = grid->last_row + 1 - grid->nb_visible;
1013 	grid->cursor_index = new_index;
1014 	gtk_widget_queue_draw (widget);
1015 	return true;
1016 }
1017 
gcr_grid_class_init(GtkWidgetClass * klass)1018 static void gcr_grid_class_init (GtkWidgetClass *klass)
1019 {
1020 	parent_class = reinterpret_cast < GtkWidgetClass * > (g_type_class_peek_parent (klass));
1021 	klass->draw = gcr_grid_draw;
1022 	klass->size_allocate = gcr_grid_size_allocate;
1023 	klass->button_press_event = gcr_grid_button_press_event;
1024 	klass->motion_notify_event = gcr_grid_motion_notify_event;
1025 	klass->button_release_event = gcr_grid_button_release_event;
1026 	klass->scroll_event = gcr_grid_scroll_event;
1027 	klass->key_press_event = gcr_grid_key_press_event;
1028 	klass->grab_notify = gcr_grid_grab_notify;
1029 	klass->focus_out_event = gcr_grid_focus_out_event;
1030 	reinterpret_cast < GObjectClass * > (klass)->finalize = gcr_grid_finalize;
1031 	klass->get_preferred_height = gcr_grid_get_preferred_height;
1032 	klass->get_preferred_width = gcr_grid_get_preferred_width;
1033 	klass->unrealize = gcr_grid_unrealize;
1034 	gcr_grid_signals[VALUE_CHANGED] = g_signal_new ("value-changed",
1035 	                                                G_TYPE_FROM_CLASS(klass),
1036 				                                    G_SIGNAL_RUN_LAST,
1037 				                                    G_STRUCT_OFFSET(GcrGridClass, value_changed_event),
1038 				                                    NULL, NULL,
1039 				                                    gcu__VOID__UINT_UINT,
1040 				                                    G_TYPE_NONE, 2,
1041 				                                    G_TYPE_UINT, G_TYPE_UINT
1042 				                                    );
1043 	gcr_grid_signals[ROW_SELECTED] = g_signal_new ("row-selected",
1044 	                                            G_TYPE_FROM_CLASS(klass),
1045 				                                G_SIGNAL_RUN_LAST,
1046 				                                G_STRUCT_OFFSET(GcrGridClass, row_selected_event),
1047 				                                NULL, NULL,
1048 				                                g_cclosure_marshal_VOID__INT,
1049 				                                G_TYPE_NONE, 1,
1050 				                                G_TYPE_INT
1051 				                                );
1052 	gcr_grid_signals[ROW_DELETED] = g_signal_new ("row-deleted",
1053 	                                            G_TYPE_FROM_CLASS(klass),
1054 				                                G_SIGNAL_RUN_LAST,
1055 				                                G_STRUCT_OFFSET(GcrGridClass, row_deleted_event),
1056 				                                NULL, NULL,
1057 				                                g_cclosure_marshal_VOID__INT,
1058 				                                G_TYPE_NONE, 1,
1059 				                                G_TYPE_INT
1060 				                                );
1061 }
1062 
1063 static void
gcr_grid_init(G_GNUC_UNUSED GcrGrid * grid)1064 gcr_grid_init (G_GNUC_UNUSED GcrGrid *grid)
1065 {
1066 	grid->col = grid->row = -1; // nothing selected
1067 }
1068 
GSF_CLASS(GcrGrid,gcr_grid,gcr_grid_class_init,gcr_grid_init,GTK_TYPE_LAYOUT)1069 GSF_CLASS (GcrGrid, gcr_grid, gcr_grid_class_init, gcr_grid_init, GTK_TYPE_LAYOUT)
1070 
1071 // signal callbacks
1072 
1073 void gcr_grid_adjustment_changed (GtkAdjustment *adj, GcrGrid *grid)
1074 {
1075 	grid->first_visible = ceil (gtk_adjustment_get_value (adj));
1076 	gtk_widget_queue_draw (GTK_WIDGET (grid));
1077 }
1078 
gcr_grid_new(G_GNUC_UNUSED char const * col_title,GType col_type,...)1079 GtkWidget *gcr_grid_new (G_GNUC_UNUSED char const *col_title, GType col_type, ...)
1080 {
1081 	g_return_val_if_fail (col_title && g_utf8_validate (col_title, -1, NULL), NULL);
1082 	GcrGrid *grid = GCR_GRID (g_object_new (GCR_TYPE_GRID, NULL));
1083 	gtk_widget_add_events (GTK_WIDGET (grid),
1084 						   GDK_POINTER_MOTION_MASK |
1085 						   GDK_BUTTON_MOTION_MASK |
1086 						   GDK_BUTTON_PRESS_MASK |
1087 						   GDK_BUTTON_RELEASE_MASK |
1088 	                       GDK_KEY_PRESS_MASK |
1089     					   GDK_LEAVE_NOTIFY_MASK
1090 						   );
1091 	std::list < char const * > titles;
1092 	std::list < GType > types;
1093 	titles.push_front (col_title);
1094 	types.push_front (col_type);
1095 	va_list args;
1096 	va_start (args, col_type);
1097 	int int_size, double_size, col_size, title_size, height;
1098 	PangoLayout *layout = gtk_widget_create_pango_layout (reinterpret_cast <GtkWidget *> (grid), "000000");
1099 	pango_layout_get_pixel_size (layout, &int_size, &height);
1100 	pango_layout_set_text (layout, "0.00000000", -1);
1101 	pango_layout_get_pixel_size (layout, &double_size, NULL);
1102 	grid->width = 0;
1103 	GtkWidget *w = gtk_button_new_with_label ("00");
1104 	gtk_widget_get_preferred_height (w, &grid->row_height, NULL);
1105 	if (grid->row_height < height)
1106 		grid->row_height = height + 2;
1107 	grid->line_offset = (grid->row_height - height) / 2;
1108 	gtk_widget_get_preferred_width (w, &grid->header_width, NULL);
1109 	g_object_ref_sink (w);
1110 	while (1) {
1111 		col_title = va_arg (args, char const *);
1112 		if (!col_title)
1113 			break;
1114 		col_type = va_arg (args, GType);
1115 		if (g_utf8_validate (col_title, -1, NULL)) {
1116 			titles.push_back (col_title);
1117 			types.push_back (col_type);
1118 		}
1119 	}
1120 	va_end (args);
1121 	grid->cols = titles.size ();
1122 	grid->col_widths = new int[grid->cols];
1123 	grid->min_widths = new int[grid->cols];
1124 	grid->titles = new std::string[grid->cols];
1125 	grid->types = new GType[grid->cols];
1126 	grid->editable = new bool[grid->cols];
1127 	grid->selected_rows = new std::set < int > ();
1128 	unsigned i;
1129 	std::list <char const *>::iterator title = titles.begin ();
1130 	std::list <GType>::iterator type = types.begin ();
1131 	grid->width = grid->header_width;
1132 	grid->cols_min_width = 0;
1133 	for (i = 0; i < grid->cols; i++, title++, type++) {
1134 		switch (*type) {
1135 		case G_TYPE_INT:
1136 		case G_TYPE_UINT:
1137 		case G_TYPE_STRING:
1138 			col_size = int_size;
1139 			break;
1140 		case G_TYPE_DOUBLE:
1141 			col_size = double_size;
1142 			break;
1143 		case G_TYPE_BOOLEAN:
1144 			if (!checked) {
1145 				GtkWidget *ofw = gtk_offscreen_window_new ();
1146 				GtkWidget *layout = gtk_layout_new (NULL, NULL);
1147 				GtkWidget *widget = gtk_check_button_new ();
1148 				GdkRGBA rgba = {1., 1., 1., 1.};
1149 				gtk_widget_override_background_color (layout, GTK_STATE_FLAG_NORMAL, &rgba);
1150 				gtk_widget_set_size_request (layout, grid->row_height - 1, grid->row_height - 1);
1151 				gtk_container_add (GTK_CONTAINER (ofw), layout);
1152 				gtk_layout_put (GTK_LAYOUT (layout), widget, 0, 0);
1153 				gtk_widget_show_all (ofw);
1154 				while (gtk_events_pending ())
1155 					gtk_main_iteration ();
1156 				unchecked = gtk_offscreen_window_get_pixbuf (GTK_OFFSCREEN_WINDOW (ofw));
1157 				gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), true);
1158 				while (gtk_events_pending ())
1159 					gtk_main_iteration ();
1160 				checked = gtk_offscreen_window_get_pixbuf (GTK_OFFSCREEN_WINDOW (ofw));
1161 				gtk_widget_destroy (ofw);
1162 			}
1163 			col_size = grid->row_height;
1164 			break;
1165 		default:
1166 			col_size = 0;
1167 			break;
1168 		}
1169 		// now evaluate the size of the title
1170 		pango_layout_set_markup (layout, *title, -1);
1171 		pango_layout_get_pixel_size (layout, &title_size, NULL);
1172 		if (col_size < title_size)
1173 			col_size = title_size;
1174 		col_size += 6;	// add some padding
1175 		grid->min_widths[i] = col_size;
1176 		grid->cols_min_width += col_size;
1177 		grid->titles[i] = *title;
1178 		grid->types[i] = *type;
1179 		grid->editable[i] = true;
1180 	}
1181 	grid->can_edit = grid->cols;
1182 	grid->width += grid->cols_min_width;
1183 	g_object_unref (layout);
1184 	GdkRGBA rgba = {1.0, 1.0, 1.0, 1.0};
1185 	gtk_widget_override_background_color (reinterpret_cast <GtkWidget *> (grid), GTK_STATE_FLAG_NORMAL, &rgba);
1186 	// add a vertical scrollbar
1187 	grid->vadj = gtk_adjustment_new (0, 0, 1, 1, 10, 0);
1188 	grid->scroll = gtk_scrollbar_new (GTK_ORIENTATION_VERTICAL, grid->vadj);
1189 	g_object_set (G_OBJECT (grid->scroll), "height-request", grid->row_height * 5, NULL); //default size
1190 	gtk_layout_put (GTK_LAYOUT (grid), grid->scroll, grid->width + 1, grid->row_height + 1);
1191 	gtk_widget_get_preferred_width (grid->scroll, &grid->scroll_width, NULL);
1192 	grid->width += grid->scroll_width + 1;
1193 	gtk_widget_set_can_focus(reinterpret_cast <GtkWidget *> (grid), true);
1194 	// add signals
1195 	g_signal_connect (grid->vadj, "value-changed", G_CALLBACK (gcr_grid_adjustment_changed), grid);
1196 	grid->orig_string = new std::string ();
1197 	return reinterpret_cast <GtkWidget *> (grid);
1198 }
1199 
gcr_grid_get_int(GcrGrid * grid,unsigned row,unsigned column)1200 int gcr_grid_get_int (GcrGrid *grid, unsigned row, unsigned column)
1201 {
1202 	g_return_val_if_fail (GCR_IS_GRID (grid) && row < grid->rows && column < grid->cols && grid->types[column] == G_TYPE_INT, 0);
1203 	return grid->row_data[row][column].compare (0, strlen ("−"), "−")?
1204 		atoi (grid->row_data[row][column].c_str ()):
1205 		-atoi (grid->row_data[row][column].c_str () + strlen ("−"));
1206 }
1207 
gcr_grid_get_uint(GcrGrid * grid,unsigned row,unsigned column)1208 unsigned gcr_grid_get_uint (GcrGrid *grid, unsigned row, unsigned column)
1209 {
1210 	g_return_val_if_fail (GCR_IS_GRID (grid) && row < grid->rows && column < grid->cols && grid->types[column] == G_TYPE_UINT, 0);
1211 	return strtoul (grid->row_data[row][column].c_str (), NULL, 10);
1212 }
1213 
gcr_grid_get_double(GcrGrid * grid,unsigned row,unsigned column)1214 double gcr_grid_get_double (GcrGrid *grid, unsigned row, unsigned column)
1215 {
1216 	g_return_val_if_fail (GCR_IS_GRID (grid) && row < grid->rows && column < grid->cols && grid->types[column] == G_TYPE_DOUBLE, go_nan);
1217 	return grid->row_data[row][column].compare (0, strlen ("−"), "−")?
1218 		atof (grid->row_data[row][column].c_str ()):
1219 		-atof (grid->row_data[row][column].c_str ());
1220 }
1221 
gcr_grid_get_string(GcrGrid * grid,unsigned row,unsigned column)1222 char const *gcr_grid_get_string (GcrGrid *grid, unsigned row, unsigned column)
1223 {
1224 	g_return_val_if_fail (GCR_IS_GRID (grid) && row < grid->rows && column < grid->cols && grid->types[column] == G_TYPE_STRING, NULL);
1225 	return grid->row_data[row][column].c_str ();
1226 }
1227 
gcr_grid_get_boolean(GcrGrid * grid,unsigned row,unsigned column)1228 bool gcr_grid_get_boolean (GcrGrid *grid, unsigned row, unsigned column)
1229 {
1230 	g_return_val_if_fail (GCR_IS_GRID (grid) && row < grid->rows && column < grid->cols && grid->types[column] == G_TYPE_BOOLEAN, false);
1231 	return grid->row_data[row][column] == "t";
1232 }
1233 
gcr_grid_set_int(GcrGrid * grid,unsigned row,unsigned column,int value)1234 void gcr_grid_set_int (GcrGrid *grid, unsigned row, unsigned column, int value)
1235 {
1236 	g_return_if_fail (GCR_IS_GRID (grid) && row < grid->rows && column < grid->cols && grid->types[column] == G_TYPE_INT);
1237 	char *buf;
1238 	if (value >= 0)
1239 		buf = g_strdup_printf ("%d", value);
1240 	else
1241 		buf = g_strdup_printf ("−%d", -value);
1242 	grid->row_data[row][column] = buf;
1243 	g_free (buf);
1244 	gtk_widget_queue_draw (GTK_WIDGET (grid));
1245 }
1246 
gcr_grid_set_uint(GcrGrid * grid,unsigned row,unsigned column,unsigned value)1247 void gcr_grid_set_uint (GcrGrid *grid, unsigned row, unsigned column, unsigned value)
1248 {
1249 	g_return_if_fail (GCR_IS_GRID (grid) && row < grid->rows && column < grid->cols && grid->types[column] == G_TYPE_UINT);
1250 	char *buf = g_strdup_printf ("%u", value);
1251 	grid->row_data[row][column] = buf;
1252 	g_free (buf);
1253 	gtk_widget_queue_draw (GTK_WIDGET (grid));
1254 }
1255 
gcr_grid_set_double(GcrGrid * grid,unsigned row,unsigned column,double value)1256 void gcr_grid_set_double (GcrGrid *grid, unsigned row, unsigned column, double value)
1257 {
1258 	g_return_if_fail (GCR_IS_GRID (grid) && row < grid->rows && column < grid->cols && grid->types[column] == G_TYPE_DOUBLE);
1259 	char *buf = g_strdup_printf ("%g", value);
1260 	grid->row_data[row][column] = buf;
1261 	g_free (buf);
1262 	gtk_widget_queue_draw (GTK_WIDGET (grid));
1263 }
1264 
gcr_grid_set_string(GcrGrid * grid,unsigned row,unsigned column,char const * value)1265 void gcr_grid_set_string (GcrGrid *grid, unsigned row, unsigned column, char const *value)
1266 {
1267 	g_return_if_fail (GCR_IS_GRID (grid) && row < grid->rows && column < grid->cols && grid->types[column] == G_TYPE_STRING);
1268 	grid->row_data[row][column] = value;
1269 	gtk_widget_queue_draw (GTK_WIDGET (grid));
1270 }
1271 
gcr_grid_set_boolean(GcrGrid * grid,unsigned row,unsigned column,bool value)1272 void gcr_grid_set_boolean (GcrGrid *grid, unsigned row, unsigned column, bool value)
1273 {
1274 	g_return_if_fail (GCR_IS_GRID (grid) && row < grid->rows && column < grid->cols && grid->types[column] == G_TYPE_BOOLEAN);
1275 	grid->row_data[row][column] = value? "t": "f";
1276 	gtk_widget_queue_draw (GTK_WIDGET (grid));
1277 }
1278 
gcr_grid_append_row(GcrGrid * grid,...)1279 unsigned gcr_grid_append_row (GcrGrid *grid,...)
1280 {
1281 	g_return_val_if_fail (GCR_IS_GRID (grid) && grid->cols > 0, 0);
1282 	unsigned row = grid->rows++, col;
1283 	char *buf;
1284 	if (grid->row_data.capacity () < grid->rows)
1285 		grid->row_data.resize (grid->row_data.capacity () + 5);
1286 	grid->row_data[row] = new std::string[grid->cols];
1287 	va_list args;
1288 	va_start (args, grid);
1289 	for (col = 0; col < grid->cols; col++) {
1290 		switch (grid->types[col]) {
1291 		case G_TYPE_INT: {
1292 			int value = va_arg (args, int);
1293 			if (value >= 0)
1294 				buf = g_strdup_printf ("%d", value);
1295 			else
1296 				buf = g_strdup_printf ("−%d", -value);
1297 			grid->row_data[row][col] = buf;
1298 			g_free (buf);
1299 			break;
1300 		}
1301 		case G_TYPE_UINT:
1302 			buf = g_strdup_printf ("%u", va_arg (args, unsigned));
1303 			grid->row_data[row][col] = buf;
1304 			g_free (buf);
1305 			break;
1306 		case G_TYPE_DOUBLE:
1307 			buf = g_strdup_printf ("%f", va_arg (args, double));
1308 			grid->row_data[row][col] = buf;
1309 			g_free (buf);
1310 			break;
1311 		case G_TYPE_STRING:
1312 			grid->row_data[row][col] = va_arg (args, char const*);
1313 			break;
1314 		case G_TYPE_BOOLEAN:
1315 			grid->row_data[row][col] = (va_arg (args, gboolean))? "t": "f";
1316 			break;
1317 		default:
1318 			// do nothing, unsupported type
1319 			break;
1320 		}
1321 	}
1322 	va_end (args);
1323 	gtk_adjustment_set_page_size (grid->vadj, static_cast < double > (grid->nb_visible) / grid->rows);
1324 	gtk_adjustment_set_upper (grid->vadj, (grid->nb_visible < grid->rows)? grid->rows - grid->nb_visible: .1);
1325 	if (grid->rows < grid->nb_visible + grid->first_visible) {
1326 		grid->first_visible = (grid->nb_visible < grid->rows)? grid->rows - grid->nb_visible: 0;
1327 		gtk_adjustment_set_value (grid->vadj, grid->first_visible);
1328 	}
1329 	gtk_widget_queue_draw (GTK_WIDGET (grid));
1330 	return row;
1331 }
1332 
gcr_grid_delete_row(GcrGrid * grid,unsigned row)1333 void gcr_grid_delete_row (GcrGrid *grid, unsigned row)
1334 {
1335 	g_return_if_fail (GCR_IS_GRID (grid) && grid->rows > row);
1336 	delete [] grid->row_data[row];
1337 	g_signal_emit (grid, gcr_grid_signals[ROW_DELETED], 0, row);
1338 	for (unsigned n = row + 1 ; n < grid->rows; n++)
1339 		grid->row_data[n - 1] = grid->row_data[n];
1340 	grid->rows--;
1341 	std::set < int > decreased;
1342 	std::set < int >::iterator i, end = grid->selected_rows->end ();
1343 	for (i = grid->selected_rows->begin (); i != end; i++)
1344 		if ((*i) > static_cast < int > (row))
1345 			decreased.insert (*i);
1346 	grid->selected_rows->erase (row);
1347 	end = decreased.end ();
1348 	for (i = decreased.begin (); i != end; i++)
1349 		grid->selected_rows->erase (*i);
1350 	for (i = decreased.begin (); i != end; i++)
1351 		grid->selected_rows->insert ((*i) - 1);
1352 	if (grid->row == static_cast < int > (grid->rows)) {
1353 		grid->row = -1;
1354 		g_signal_emit (grid, gcr_grid_signals[ROW_SELECTED], 0, -1);
1355 	}
1356 	if (!grid->selection_locked)
1357 		grid->selected_rows->clear ();
1358 	gtk_widget_queue_draw (GTK_WIDGET (grid));
1359 }
1360 
gcr_grid_delete_selected_rows(GcrGrid * grid)1361 void gcr_grid_delete_selected_rows (GcrGrid *grid)
1362 {
1363 	g_return_if_fail (GCR_IS_GRID (grid));
1364 	if (grid->row == -1)
1365 		return;
1366 	// save row and set it to -1 to not fire ROW_SELECTED when not needed
1367 	int row = grid->row;
1368 	grid->row = -1;
1369 	grid->selection_locked = true;
1370 	gcr_grid_delete_row (grid, row);
1371 	while (!grid->selected_rows->empty ())
1372 		gcr_grid_delete_row (grid, *grid->selected_rows->begin ());
1373 	grid->selected_rows->clear ();
1374 	if (row >= static_cast < int > (grid->rows))
1375 		g_signal_emit (grid, gcr_grid_signals[ROW_SELECTED], 0, -1);
1376 	else
1377 		grid->row = row;
1378 	g_signal_emit (grid, gcr_grid_signals[ROW_SELECTED], 0, -1);
1379 	gtk_widget_queue_draw (GTK_WIDGET (grid));
1380 	grid->selection_locked = false;
1381 }
1382 
gcr_grid_delete_all(GcrGrid * grid)1383 void gcr_grid_delete_all (GcrGrid *grid)
1384 {
1385 	g_return_if_fail (GCR_IS_GRID (grid));
1386 	for (unsigned i = 0; i < grid->rows; i++)
1387 		delete [] grid->row_data[i];
1388 	grid->rows = 0;
1389 	if (grid->row >= 0) {
1390 		grid->row = -1;
1391 		g_signal_emit (grid, gcr_grid_signals[ROW_SELECTED], 0, -1);
1392 	}
1393 	gtk_widget_queue_draw (GTK_WIDGET (grid));
1394 }
1395 
gcr_grid_customize_column(GcrGrid * grid,unsigned column,unsigned chars,bool editable)1396 void gcr_grid_customize_column (GcrGrid *grid, unsigned column, unsigned chars, bool editable)
1397 {
1398 	g_return_if_fail (GCR_IS_GRID (grid) && column < grid->cols);
1399 	if (grid->editable[column])
1400 		grid->can_edit--;
1401 	grid->editable[column] = editable;
1402 	if (editable)
1403 		grid->can_edit++;
1404 	PangoLayout *l = gtk_widget_create_pango_layout (reinterpret_cast <GtkWidget *> (grid), grid->titles[column].c_str ());
1405 	int width, title_width;
1406 	pango_layout_get_pixel_size (l, &title_width, NULL);
1407 	std::string s (chars, 'W');
1408 	pango_layout_set_text (l, s.c_str (), -1);
1409 	pango_layout_get_pixel_size (l, &width, NULL);
1410 	if (width < title_width)
1411 		width = title_width;
1412 	if (width != grid->min_widths[column]) {
1413 		grid->cols_min_width -= grid->min_widths[column];
1414 		grid->min_widths[column] = width;
1415 		grid->cols_min_width += width;
1416 		grid->width = grid->header_width + grid->cols_min_width + grid->scroll_width;
1417 		gtk_widget_queue_resize (GTK_WIDGET (grid));
1418 	}
1419 }
1420 
gcr_grid_set_allow_multiple_selection(GcrGrid * grid,bool allow)1421 void gcr_grid_set_allow_multiple_selection (GcrGrid *grid, bool allow)
1422 {
1423 	grid->allow_multiple = allow;
1424 }
1425 
gcr_grid_for_each_selected(GcrGrid * grid,GridCb cb,void * user_data)1426 void gcr_grid_for_each_selected (GcrGrid *grid, GridCb cb, void *user_data)
1427 {
1428 	g_return_if_fail (GCR_IS_GRID (grid));
1429 	if (grid->row < 0)
1430 		return;
1431 	cb (grid->row, user_data);
1432 	std::set < int >::iterator i, end = grid->selected_rows->end ();
1433 	for (i = grid->selected_rows->begin (); i != end; i++)
1434 		cb (*i, user_data);
1435 }
1436 
gcr_grid_select_all(GcrGrid * grid)1437 void gcr_grid_select_all (GcrGrid *grid)
1438 {
1439 	g_return_if_fail (GCR_IS_GRID (grid)  && grid->allow_multiple);
1440 	if (grid->rows == 0)
1441 		return;
1442 	if (grid->row < 0) {
1443 		grid->row = 0;
1444 		g_signal_emit (grid, gcr_grid_signals[ROW_SELECTED], 0, 0);
1445 	} else if (grid->col > 0 && !gcr_grid_validate_change (grid))
1446 		return;
1447 	for (unsigned i = 0; i < grid->rows; i++)
1448 		if (i != static_cast < unsigned > (grid->row))
1449 			grid->selected_rows->insert (i);
1450 	gtk_widget_queue_draw (GTK_WIDGET (grid));
1451 }
1452 
gcr_grid_add_row_to_selection(GcrGrid * grid,unsigned row)1453 void gcr_grid_add_row_to_selection (GcrGrid *grid, unsigned row)
1454 {
1455 	if (grid->row < 0)
1456 		grid->row = row;
1457 	else if (row != static_cast < unsigned > (grid->row))
1458 		grid->selected_rows->insert (row);
1459 	gtk_widget_queue_draw (GTK_WIDGET (grid));
1460 }
1461 
gcr_grid_unselect_row(GcrGrid * grid,unsigned row)1462 void gcr_grid_unselect_row (GcrGrid *grid, unsigned row)
1463 {
1464 	grid->selected_rows->erase (row);
1465 	gtk_widget_queue_draw (GTK_WIDGET (grid));
1466 }
1467