1 /*
2  * Copyright (C) 2009 - 2012 Vivien Malerba <malerba@gnome-db.org>
3  * Copyright (C) 2010 David King <davidk@openismus.com>
4  * Copyright (C) 2011 Murray Cumming <murrayc@murrayc.com>
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 2 of the License, or (at your option) any later version.
10  *
11  * This library is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public
17  * License along with this library; if not, write to the
18  * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
19  * Boston, MA  02110-1301, USA.
20  */
21 
22 #include <string.h>
23 #include <glib/gi18n-lib.h>
24 #include <libgda/libgda.h>
25 #include "gdaui-cloud.h"
26 #include <gdk/gdkkeysyms.h>
27 #include "internal/popup-container.h"
28 #include "gdaui-data-selector.h"
29 #include <libgda/gda-debug-macros.h>
30 
31 static void gdaui_cloud_class_init (GdauiCloudClass * class);
32 static void gdaui_cloud_init (GdauiCloud *wid);
33 static void gdaui_cloud_dispose (GObject *object);
34 static void gdaui_cloud_set_property (GObject *object,
35 				      guint param_id,
36 				      const GValue *value,
37 				      GParamSpec *pspec);
38 static void gdaui_cloud_get_property (GObject *object,
39 				      guint param_id,
40 				      GValue *value,
41 				      GParamSpec *pspec);
42 
43 /* GdauiDataSelector interface */
44 static void              gdaui_cloud_selector_init (GdauiDataSelectorIface *iface);
45 static GdaDataModel     *cloud_selector_get_model (GdauiDataSelector *iface);
46 static void              cloud_selector_set_model (GdauiDataSelector *iface, GdaDataModel *model);
47 static GArray           *cloud_selector_get_selected_rows (GdauiDataSelector *iface);
48 static GdaDataModelIter *cloud_selector_get_data_set (GdauiDataSelector *iface);
49 static gboolean          cloud_selector_select_row (GdauiDataSelector *iface, gint row);
50 static void              cloud_selector_unselect_row (GdauiDataSelector *iface, gint row);
51 static void              cloud_selector_set_column_visible (GdauiDataSelector *iface, gint column, gboolean visible);
52 
53 struct _GdauiCloudPriv
54 {
55 	GdaDataModel        *model;
56 	GdaDataModelIter    *iter;
57 	gint                 label_column;
58 	gint                 weight_column;
59 	GdauiCloudWeightFunc weight_func;
60 	gpointer             weight_func_data;
61 
62 	gdouble              min_scale;
63 	gdouble              max_scale;
64 
65 	GtkTextBuffer       *tbuffer;
66         GtkWidget           *tview;
67 	GSList              *selected_tags;
68 	GtkSelectionMode     selection_mode;
69 
70         gboolean             hovering_over_link;
71 };
72 
73 enum {
74 	ACTIVATE,
75         LAST_SIGNAL
76 };
77 
78 static guint objects_cloud_signals[LAST_SIGNAL] = { 0 };
79 
80 /* get a pointer to the parents to be able to call their destructor */
81 static GObjectClass *parent_class = NULL;
82 
83 /* properties */
84 enum
85 {
86         PROP_0,
87 	PROP_MODEL,
88 	PROP_LABEL_COLUMN,
89 	PROP_WEIGHT_COLUMN,
90 	PROP_MIN_SCALE,
91 	PROP_MAX_SCALE,
92 };
93 
94 GType
gdaui_cloud_get_type(void)95 gdaui_cloud_get_type (void)
96 {
97 	static GType type = 0;
98 
99 	if (G_UNLIKELY (type == 0)) {
100 		static const GTypeInfo info = {
101 			sizeof (GdauiCloudClass),
102 			(GBaseInitFunc) NULL,
103 			(GBaseFinalizeFunc) NULL,
104 			(GClassInitFunc) gdaui_cloud_class_init,
105 			NULL,
106 			NULL,
107 			sizeof (GdauiCloud),
108 			0,
109 			(GInstanceInitFunc) gdaui_cloud_init,
110 			0
111 		};
112 
113 		static const GInterfaceInfo selector_info = {
114                         (GInterfaceInitFunc) gdaui_cloud_selector_init,
115                         NULL,
116                         NULL
117                 };
118 
119 		type = g_type_register_static (GTK_TYPE_BOX, "GdauiCloud", &info, 0);
120 		g_type_add_interface_static (type, GDAUI_TYPE_DATA_SELECTOR, &selector_info);
121 	}
122 
123 	return type;
124 }
125 
126 static void
cloud_map(GtkWidget * widget)127 cloud_map (GtkWidget *widget)
128 {
129         GTK_WIDGET_CLASS (parent_class)->map (widget);
130 	GtkStyleContext *style_context;
131 	GdkRGBA color;
132 	style_context = gtk_widget_get_style_context (widget);
133 	gtk_style_context_get_background_color (style_context, GTK_STATE_FLAG_NORMAL, &color);
134 	gtk_widget_override_background_color (GDAUI_CLOUD (widget)->priv->tview, GTK_STATE_FLAG_NORMAL, &color);
135 }
136 
137 static void
gdaui_cloud_class_init(GdauiCloudClass * klass)138 gdaui_cloud_class_init (GdauiCloudClass *klass)
139 {
140 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
141 
142 	parent_class = g_type_class_peek_parent (klass);
143 	object_class->dispose = gdaui_cloud_dispose;
144 	GTK_WIDGET_CLASS (object_class)->map = cloud_map;
145 
146 	/* signals */
147         objects_cloud_signals [ACTIVATE] =
148                 g_signal_new ("activate",
149                               G_TYPE_FROM_CLASS (object_class),
150                               G_SIGNAL_RUN_FIRST,
151                               G_STRUCT_OFFSET (GdauiCloudClass, activate),
152                               NULL, NULL,
153                               g_cclosure_marshal_VOID__INT, G_TYPE_NONE, 1, G_TYPE_INT);
154         klass->activate = NULL;
155 
156 	/* Properties */
157         object_class->set_property = gdaui_cloud_set_property;
158         object_class->get_property = gdaui_cloud_get_property;
159 	g_object_class_install_property (object_class, PROP_MODEL,
160 	                                 g_param_spec_object ("model", NULL, NULL,
161 	                                                      GDA_TYPE_DATA_MODEL, G_PARAM_READWRITE));
162 	g_object_class_install_property (object_class, PROP_LABEL_COLUMN,
163 	                                 g_param_spec_int ("label-column", NULL,
164 							   "Column in the data model which contains the "
165 							   "text to display, the column must be a G_TYPE_STRING",
166 							   -1, G_MAXINT, -1, G_PARAM_READWRITE));
167  	g_object_class_install_property (object_class, PROP_WEIGHT_COLUMN,
168 	                                 g_param_spec_int ("weight-column", NULL, NULL,
169 							   -1, G_MAXINT, -1, G_PARAM_READWRITE));
170 	g_object_class_install_property (object_class, PROP_MIN_SCALE,
171 	                                 g_param_spec_double ("min-scale", NULL, NULL,
172 							      .1, 10., .8, G_PARAM_READWRITE));
173 	g_object_class_install_property (object_class, PROP_MAX_SCALE,
174 	                                 g_param_spec_double ("max-scale", NULL, NULL,
175 							      .1, 10., 3., G_PARAM_READWRITE));
176 }
177 
178 static void
gdaui_cloud_selector_init(GdauiDataSelectorIface * iface)179 gdaui_cloud_selector_init (GdauiDataSelectorIface *iface)
180 {
181 	iface->get_model = cloud_selector_get_model;
182 	iface->set_model = cloud_selector_set_model;
183 	iface->get_selected_rows = cloud_selector_get_selected_rows;
184 	iface->get_data_set = cloud_selector_get_data_set;
185 	iface->select_row = cloud_selector_select_row;
186 	iface->unselect_row = cloud_selector_unselect_row;
187 	iface->set_column_visible = cloud_selector_set_column_visible;
188 }
189 
190 static void
sync_iter_with_selection(GdauiCloud * cloud)191 sync_iter_with_selection (GdauiCloud *cloud)
192 {
193 	GSList *list;
194 	gint selrow = -1;
195 
196 	if (! cloud->priv->iter)
197 		return;
198 
199 	/* locate selected row */
200 	for (list = cloud->priv->selected_tags; list; list = list->next) {
201 		gint row;
202 		row = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (list->data), "row")) - 1;
203 		if (row >= 0) {
204 			if (selrow == -1)
205 				selrow = row;
206 			else {
207 				selrow = -1;
208 				break;
209 			}
210 		}
211 	}
212 
213 	/* update iter */
214 	if ((selrow == -1) || !gda_data_model_iter_move_to_row (cloud->priv->iter, selrow)) {
215 		gda_data_model_iter_invalidate_contents (cloud->priv->iter);
216 		g_object_set (G_OBJECT (cloud->priv->iter), "current-row", -1, NULL);
217 	}
218 }
219 
220 static void
update_display(GdauiCloud * cloud)221 update_display (GdauiCloud *cloud)
222 {
223 	GtkTextBuffer *tbuffer;
224         GtkTextIter start, end;
225 
226         /* clean all */
227         tbuffer = cloud->priv->tbuffer;
228         gtk_text_buffer_get_start_iter (tbuffer, &start);
229         gtk_text_buffer_get_end_iter (tbuffer, &end);
230         gtk_text_buffer_delete (tbuffer, &start, &end);
231 	if (cloud->priv->selected_tags) {
232 		g_slist_foreach (cloud->priv->selected_tags, (GFunc) g_object_unref, NULL);
233 		g_slist_free (cloud->priv->selected_tags);
234 		cloud->priv->selected_tags = NULL;
235 		sync_iter_with_selection (cloud);
236 		g_signal_emit_by_name (cloud, "selection-changed");
237 	}
238 
239 	if (!cloud->priv->model)
240 		return;
241 	if (cloud->priv->label_column < 0)
242 		return;
243 	/* check for the data model's column type */
244 	GdaColumn *column;
245 	column = gda_data_model_describe_column (cloud->priv->model, cloud->priv->label_column);
246 	if (!column || (gda_column_get_g_type (column) != G_TYPE_STRING)) {
247 		g_warning (_("Wrong column type for label: expecting a string and got a %s"),
248 			   gda_g_type_to_string (gda_column_get_g_type (column)));
249 		return;
250 	}
251 
252 	gint nrows, i;
253 	nrows = gda_data_model_get_n_rows (cloud->priv->model);
254 
255 	/* compute scale range */
256 	gdouble min_weight = G_MAXDOUBLE, max_weight = G_MINDOUBLE, wrange;
257 	if ((cloud->priv->weight_column >= 0) || cloud->priv->weight_func) {
258 		for (i = 0; i < nrows; i++) {
259 			const GValue *cvalue;
260 			gdouble weight = 1.;
261 			if (cloud->priv->weight_func) {
262 				weight = cloud->priv->weight_func (cloud->priv->model, i,
263 								   cloud->priv->weight_func_data);
264 				min_weight = MIN (min_weight, weight);
265 				max_weight = MAX (max_weight, weight);
266 			}
267 			else {
268 				cvalue = gda_data_model_get_value_at (cloud->priv->model,
269 								      cloud->priv->weight_column, i, NULL);
270 				if (cvalue) {
271 					weight = g_ascii_strtod (gda_value_stringify (cvalue), NULL);
272 					min_weight = MIN (min_weight, weight);
273 					max_weight = MAX (max_weight, weight);
274 				}
275 			}
276 		}
277 	}
278 
279 	if (max_weight > min_weight)
280 		wrange = (cloud->priv->max_scale - cloud->priv->min_scale) / (max_weight - min_weight);
281 	else
282 		wrange = 0.;
283 
284 	gtk_text_buffer_get_start_iter (tbuffer, &start);
285 	for (i = 0; i < nrows; i++) {
286 		const GValue *cvalue;
287 		gdouble weight = 1.;
288 		const gchar *ptr;
289 		GString *string;
290 		cvalue = gda_data_model_get_value_at (cloud->priv->model, cloud->priv->label_column, i, NULL);
291 		if (!cvalue) {
292 			TO_IMPLEMENT;
293 			continue;
294 		}
295 		if (!g_value_get_string (cvalue))
296 			continue;
297 
298 		/* convert spaces to non breaking spaces (0xC2 0xA0 as UTF8) */
299 		string = g_string_new ("");
300 		for (ptr = g_value_get_string (cvalue); *ptr; ptr++) {
301 			if (*ptr == ' ') {
302 				g_string_append_c (string, 0xC2);
303 				g_string_append_c (string, 0xA0);
304 			}
305 			else
306 				g_string_append_c (string, *ptr);
307 		}
308 
309 		if ((cloud->priv->weight_column >= 0) || cloud->priv->weight_func) {
310 			if (cloud->priv->weight_func) {
311 				weight = cloud->priv->weight_func (cloud->priv->model, i,
312 								   cloud->priv->weight_func_data);
313 				weight = cloud->priv->min_scale + wrange * (weight - min_weight);
314 			}
315 			else {
316 				cvalue = gda_data_model_get_value_at (cloud->priv->model,
317 								      cloud->priv->weight_column, i, NULL);
318 				if (cvalue) {
319 					weight = g_ascii_strtod (gda_value_stringify (cvalue), NULL);
320 					weight = cloud->priv->min_scale + wrange * (weight - min_weight);
321 				}
322 			}
323 		}
324 
325 		GtkTextTag *tag;
326 		tag = gtk_text_buffer_create_tag (cloud->priv->tbuffer, NULL,
327 					  "foreground", "#6161F2",
328 					  "scale", weight,
329 					  NULL);
330 		g_object_set_data ((GObject*) tag, "row", GINT_TO_POINTER (i) + 1);
331 		gtk_text_buffer_insert_with_tags (cloud->priv->tbuffer, &start, string->str, -1,
332 						  tag, NULL);
333 		g_string_free (string, TRUE);
334 		gtk_text_buffer_insert (cloud->priv->tbuffer, &start, "   ", -1);
335 	}
336 }
337 
338 static gboolean key_press_event (GtkWidget *text_view, GdkEventKey *event, GdauiCloud *cloud);
339 static void event_after (GtkWidget *text_view, GdkEvent *ev, GdauiCloud *cloud);
340 static gboolean motion_notify_event (GtkWidget *text_view, GdkEventMotion *event, GdauiCloud *cloud);
341 static gboolean visibility_notify_event (GtkWidget *text_view, GdkEventVisibility *event, GdauiCloud *cloud);
342 
343 static void
gdaui_cloud_init(GdauiCloud * cloud)344 gdaui_cloud_init (GdauiCloud *cloud)
345 {
346 	cloud->priv = g_new0 (GdauiCloudPriv, 1);
347 	cloud->priv->min_scale = .8;
348 	cloud->priv->max_scale = 2.;
349 	cloud->priv->selected_tags = NULL;
350 	cloud->priv->selection_mode = GTK_SELECTION_SINGLE;
351 
352 	gtk_orientable_set_orientation (GTK_ORIENTABLE (cloud), GTK_ORIENTATION_VERTICAL);
353 
354 	/* text buffer */
355         cloud->priv->tbuffer = gtk_text_buffer_new (NULL);
356         gtk_text_buffer_create_tag (cloud->priv->tbuffer, "section",
357                                     "weight", PANGO_WEIGHT_BOLD,
358                                     "foreground", "blue", NULL);
359 
360 	/* text view */
361 	GtkWidget *sw, *vbox, *vp;
362         sw = gtk_scrolled_window_new (NULL, NULL);
363         gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw), GTK_POLICY_AUTOMATIC,
364                                         GTK_POLICY_AUTOMATIC);
365         gtk_box_pack_start (GTK_BOX (cloud), sw, TRUE, TRUE, 0);
366 
367         vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
368 	vp = gtk_viewport_new (NULL, NULL);
369         gtk_viewport_set_shadow_type (GTK_VIEWPORT (vp), GTK_SHADOW_NONE);
370 
371         gtk_container_add (GTK_CONTAINER (sw), vp);
372 	gtk_container_add (GTK_CONTAINER (vp), vbox);
373 
374         cloud->priv->tview = gtk_text_view_new_with_buffer (cloud->priv->tbuffer);
375         gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (cloud->priv->tview), GTK_WRAP_WORD);
376         gtk_text_view_set_editable (GTK_TEXT_VIEW (cloud->priv->tview), FALSE);
377         gtk_text_view_set_cursor_visible (GTK_TEXT_VIEW (cloud->priv->tview), FALSE);
378         gtk_box_pack_start (GTK_BOX (vbox), cloud->priv->tview, TRUE, TRUE, 0);
379         gtk_widget_show_all (sw);
380 
381         g_signal_connect (cloud->priv->tview, "key-press-event",
382                           G_CALLBACK (key_press_event), cloud);
383         g_signal_connect (cloud->priv->tview, "event-after",
384                           G_CALLBACK (event_after), cloud);
385         g_signal_connect (cloud->priv->tview, "motion-notify-event",
386                           G_CALLBACK (motion_notify_event), cloud);
387         g_signal_connect (cloud->priv->tview, "visibility-notify-event",
388                           G_CALLBACK (visibility_notify_event), cloud);
389 }
390 
391 /**
392  * gdaui_cloud_new:
393  * @model: a #GdaDataModel
394  *
395  * Creates a new #GdauiCloud widget suitable to display the data in @model
396  *
397  * Returns: (transfer full): the new widget
398  *
399  * Since: 4.2
400  */
401 GtkWidget *
gdaui_cloud_new(GdaDataModel * model,gint label_column,gint weight_column)402 gdaui_cloud_new (GdaDataModel *model, gint label_column, gint weight_column)
403 {
404 	GdauiCloud *cloud;
405 
406 	g_return_val_if_fail (!model || GDA_IS_DATA_MODEL (model), NULL);
407 	if (label_column < -1)
408 		label_column = -1;
409 	if (weight_column < -1)
410 		weight_column = -1;
411 
412 	cloud = (GdauiCloud *) g_object_new (GDAUI_TYPE_CLOUD,
413 					     "label-column", label_column,
414 					     "weight-column", weight_column,
415 					     "model", model, NULL);
416 	return (GtkWidget *) cloud;
417 }
418 
419 static void
gdaui_cloud_dispose(GObject * object)420 gdaui_cloud_dispose (GObject *object)
421 {
422         GdauiCloud *cloud;
423 
424         g_return_if_fail (GDAUI_IS_CLOUD (object));
425 
426         cloud = GDAUI_CLOUD (object);
427 
428         if (cloud->priv) {
429 		if (cloud->priv->selected_tags) {
430 			g_slist_foreach (cloud->priv->selected_tags, (GFunc) g_object_unref, NULL);
431 			g_slist_free (cloud->priv->selected_tags);
432 		}
433                 if (cloud->priv->iter)
434                         g_object_unref (cloud->priv->iter);
435                 if (cloud->priv->model)
436                         g_object_unref (cloud->priv->model);
437 		if (cloud->priv->tbuffer)
438 			g_object_unref (cloud->priv->tbuffer);
439 
440                 g_free (cloud->priv);
441                 cloud->priv = NULL;
442         }
443 
444         /* for the parent class */
445         parent_class->dispose (object);
446 }
447 
448 static void
model_reset_cb(G_GNUC_UNUSED GdaDataModel * model,GdauiCloud * cloud)449 model_reset_cb (G_GNUC_UNUSED GdaDataModel *model, GdauiCloud *cloud)
450 {
451 	update_display (cloud);
452 }
453 
454 static void
gdaui_cloud_set_property(GObject * object,guint param_id,const GValue * value,GParamSpec * pspec)455 gdaui_cloud_set_property (GObject *object,
456 			  guint param_id,
457 			  const GValue *value,
458 			  GParamSpec *pspec)
459 {
460 	GdauiCloud *cloud;
461 	GdaDataModel *model;
462 
463 	cloud = GDAUI_CLOUD (object);
464 
465 	switch (param_id) {
466 	case PROP_MODEL:
467 		model = (GdaDataModel*) g_value_get_object (value);
468 		if (cloud->priv->model != model) {
469 			if (cloud->priv->iter) {
470 				g_object_unref (cloud->priv->iter);
471 				cloud->priv->iter = NULL;
472 			}
473 			if (cloud->priv->model) {
474 				g_signal_handlers_disconnect_by_func (cloud->priv->model,
475 								      G_CALLBACK (model_reset_cb), cloud);
476 				g_object_unref (cloud->priv->model);
477 			}
478 			cloud->priv->model = model;
479 			if (model) {
480 				g_signal_connect (model, "reset",
481 						  G_CALLBACK (model_reset_cb), cloud);
482 				g_object_ref (G_OBJECT (model));
483 			}
484 			update_display (cloud);
485 		}
486 		break;
487 	case PROP_LABEL_COLUMN:
488 		if (cloud->priv->label_column !=  g_value_get_int (value)) {
489 			cloud->priv->label_column = g_value_get_int (value);
490 			update_display (cloud);
491 		}
492 		break;
493 	case PROP_WEIGHT_COLUMN:
494 		if (cloud->priv->weight_column !=  g_value_get_int (value)) {
495 			cloud->priv->weight_column = g_value_get_int (value);
496 			update_display (cloud);
497 		}
498 		break;
499 	case PROP_MIN_SCALE:
500 		if (cloud->priv->min_scale !=  g_value_get_double (value)) {
501 			cloud->priv->min_scale = g_value_get_double (value);
502 			update_display (cloud);
503 		}
504 		break;
505 	case PROP_MAX_SCALE:
506 		if (cloud->priv->max_scale !=  g_value_get_double (value)) {
507 			cloud->priv->max_scale = g_value_get_double (value);
508 			update_display (cloud);
509 		}
510 		break;
511 	default:
512 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
513 		break;
514 	}
515 }
516 
517 static void
gdaui_cloud_get_property(GObject * object,guint param_id,GValue * value,GParamSpec * pspec)518 gdaui_cloud_get_property (GObject *object,
519 			  guint param_id,
520 			  GValue *value,
521 			  GParamSpec *pspec)
522 {
523 	GdauiCloud *cloud;
524 
525 	cloud = GDAUI_CLOUD (object);
526 
527 	switch (param_id) {
528 	case PROP_MODEL:
529 		g_value_set_object (value, cloud->priv->model);
530 		break;
531 	case PROP_LABEL_COLUMN:
532 		g_value_set_int (value, cloud->priv->label_column);
533 		break;
534 	case PROP_WEIGHT_COLUMN:
535 		g_value_set_int (value, cloud->priv->weight_column);
536 		break;
537 	default:
538 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
539 		break;
540 	}
541 }
542 
543 /**
544  * gdaui_cloud_set_selection_mode:
545  * @cloud: a #GdauiCloud widget
546  * @mode: the desired selection mode
547  *
548  * Sets @cloud's selection mode
549  *
550  * Since: 4.2
551  */
552 void
gdaui_cloud_set_selection_mode(GdauiCloud * cloud,GtkSelectionMode mode)553 gdaui_cloud_set_selection_mode   (GdauiCloud *cloud, GtkSelectionMode mode)
554 {
555 	g_return_if_fail (GDAUI_IS_CLOUD (cloud));
556 	if (mode == cloud->priv->selection_mode)
557 		return;
558 	switch (mode) {
559 	case GTK_SELECTION_NONE:
560 		if (cloud->priv->selected_tags) {
561 			/* remove any selection */
562 			GSList *list;
563 			for (list = cloud->priv->selected_tags; list; list = list->next) {
564 				g_object_unref ((GObject*) list->data);
565 				g_object_set ((GObject*) list->data,
566 					      "background-set", FALSE,
567 					      NULL);
568 			}
569 
570 			g_slist_free (cloud->priv->selected_tags);
571 			cloud->priv->selected_tags = NULL;
572 			sync_iter_with_selection (cloud);
573 			g_signal_emit_by_name (cloud, "selection-changed");
574 		}
575 		break;
576 	case GTK_SELECTION_SINGLE:
577 	case GTK_SELECTION_BROWSE:
578 		if (cloud->priv->selected_tags && cloud->priv->selected_tags->next) {
579 			/* cut down to 1 selected node */
580 			GSList *newsel;
581 			newsel = cloud->priv->selected_tags;
582 			cloud->priv->selected_tags = g_slist_remove_link (cloud->priv->selected_tags,
583 									  cloud->priv->selected_tags);
584 			GSList *list;
585 			for (list = cloud->priv->selected_tags; list; list = list->next) {
586 				g_object_unref ((GObject*) list->data);
587 				g_object_set ((GObject*) list->data,
588 					      "background-set", FALSE,
589 					      NULL);
590 			}
591 			g_slist_free (cloud->priv->selected_tags);
592 			cloud->priv->selected_tags = newsel;
593 			sync_iter_with_selection (cloud);
594 			g_signal_emit_by_name (cloud, "selection-changed");
595 		}
596 		break;
597 	case GTK_SELECTION_MULTIPLE:
598 		break;
599 	default:
600 		g_warning ("Unknown selection mode");
601 		return;
602 	}
603 	cloud->priv->selection_mode = mode;
604 }
605 
606 static void
row_clicked(GdauiCloud * cloud,gint row,GtkTextTag * tag)607 row_clicked (GdauiCloud *cloud, gint row, GtkTextTag *tag)
608 {
609 	if (cloud->priv->selection_mode == GTK_SELECTION_NONE) {
610 		/* emit "activate" signal */
611 		g_signal_emit (cloud, objects_cloud_signals [ACTIVATE], 0, row);
612 		return;
613 	}
614 
615 	/* toggle @rows's selection */
616 	if (g_slist_find (cloud->priv->selected_tags, tag)) {
617 		cloud->priv->selected_tags = g_slist_remove (cloud->priv->selected_tags, tag);
618 		g_object_set ((GObject*) tag,
619 			      "background-set", FALSE,
620 			      NULL);
621 		g_object_unref ((GObject*) tag);
622 	}
623 	else {
624 		cloud->priv->selected_tags = g_slist_prepend (cloud->priv->selected_tags, tag);
625 		g_object_ref ((GObject*) tag);
626 		g_object_set ((GObject*) tag,
627 			      "background", "yellow",
628 			      "background-set", TRUE,
629 			      NULL);
630 
631 		GtkTextIter iter;
632 		gtk_text_buffer_get_iter_at_mark (cloud->priv->tbuffer, &iter,
633                                                   gtk_text_buffer_get_insert (cloud->priv->tbuffer));
634 		while (1) {
635 			gunichar guchar;
636 			guchar = gtk_text_iter_get_char (&iter);
637 			if (g_unichar_isspace (guchar) && (guchar != 0x00A0)) {
638 				gtk_text_iter_forward_char (&iter);
639 				break;
640 			}
641 			if (!gtk_text_iter_backward_char (&iter))
642 				break;
643 		}
644 		gtk_text_buffer_place_cursor (cloud->priv->tbuffer, &iter);
645 	}
646 
647 	if ((cloud->priv->selection_mode == GTK_SELECTION_SINGLE) ||
648 	    (cloud->priv->selection_mode == GTK_SELECTION_BROWSE)) {
649 		/* no more than 1 element can be selected */
650 		if (cloud->priv->selected_tags && cloud->priv->selected_tags->next) {
651 			GtkTextTag *tag2;
652 			tag2 = GTK_TEXT_TAG (cloud->priv->selected_tags->next->data);
653 			cloud->priv->selected_tags = g_slist_remove (cloud->priv->selected_tags, tag2);
654 			g_object_set ((GObject*) tag2,
655 				      "background-set", FALSE,
656 				      NULL);
657 			g_object_unref ((GObject*) tag2);
658 		}
659 	}
660 	if (cloud->priv->selection_mode == GTK_SELECTION_BROWSE) {
661 		/* one element is always selected */
662 		if (! cloud->priv->selected_tags) {
663 			cloud->priv->selected_tags = g_slist_prepend (cloud->priv->selected_tags, tag);
664 			g_object_ref ((GObject*) tag);
665 			g_object_set ((GObject*) tag,
666 				      "background", "yellow",
667 				      "background-set", TRUE,
668 				      NULL);
669 		}
670 	}
671 
672 	sync_iter_with_selection (cloud);
673 	g_signal_emit_by_name (cloud, "selection-changed");
674 }
675 
676 static GdkCursor *hand_cursor = NULL;
677 static GdkCursor *regular_cursor = NULL;
678 
679 /* Looks at all tags covering the position (x, y) in the text view,
680  * and if one of them is a link, change the cursor to the "hands" cursor
681  * typically used by web browsers.
682  */
683 static void
set_cursor_if_appropriate(GtkTextView * text_view,gint x,gint y,GdauiCloud * cloud)684 set_cursor_if_appropriate (GtkTextView *text_view, gint x, gint y, GdauiCloud *cloud)
685 {
686 	GSList *tags = NULL, *tagp;
687 	GtkTextIter iter;
688 	gboolean hovering = FALSE;
689 
690 	gtk_text_view_get_iter_at_location (text_view, &iter, x, y);
691 
692 	tags = gtk_text_iter_get_tags (&iter);
693 	for (tagp = tags;  tagp;  tagp = tagp->next) {
694 		GtkTextTag *tag = tagp->data;
695 		gint row;
696 		row = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (tag), "row")) - 1;
697 		if (row >= 0) {
698 			hovering = TRUE;
699 			break;
700 		}
701 	}
702 
703 	if (hovering != cloud->priv->hovering_over_link) {
704 		cloud->priv->hovering_over_link = hovering;
705 
706 		if (cloud->priv->hovering_over_link) {
707 			if (! hand_cursor)
708 				hand_cursor = gdk_cursor_new (GDK_HAND2);
709 			gdk_window_set_cursor (gtk_text_view_get_window (text_view,
710 									 GTK_TEXT_WINDOW_TEXT),
711 					       hand_cursor);
712 		}
713 		else {
714 			if (!regular_cursor)
715 				regular_cursor = gdk_cursor_new (GDK_XTERM);
716 			gdk_window_set_cursor (gtk_text_view_get_window (text_view,
717 									 GTK_TEXT_WINDOW_TEXT),
718 					       regular_cursor);
719 		}
720 	}
721 
722 	if (tags)
723 		g_slist_free (tags);
724 }
725 
726 /*
727  * Also update the cursor image if the window becomes visible
728  * (e.g. when a window covering it got iconified).
729  */
730 static gboolean
visibility_notify_event(GtkWidget * text_view,G_GNUC_UNUSED GdkEventVisibility * event,GdauiCloud * cloud)731 visibility_notify_event (GtkWidget *text_view, G_GNUC_UNUSED GdkEventVisibility *event, GdauiCloud *cloud)
732 {
733 	gint wx, wy, bx, by;
734 	GdkDeviceManager *manager;
735         GdkDevice *pointer;
736 
737         manager = gdk_display_get_device_manager (gtk_widget_get_display (text_view));
738         pointer = gdk_device_manager_get_client_pointer (manager);
739 	gdk_window_get_device_position (gtk_widget_get_window (text_view), pointer, &wx, &wy, NULL);
740 
741 	gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (text_view),
742 					       GTK_TEXT_WINDOW_WIDGET,
743 					       wx, wy, &bx, &by);
744 
745 	set_cursor_if_appropriate (GTK_TEXT_VIEW (text_view), bx, by, cloud);
746 
747 	return FALSE;
748 }
749 
750 /*
751  * Update the cursor image if the pointer moved.
752  */
753 static gboolean
motion_notify_event(GtkWidget * text_view,GdkEventMotion * event,GdauiCloud * cloud)754 motion_notify_event (GtkWidget *text_view, GdkEventMotion *event, GdauiCloud *cloud)
755 {
756 	gint x, y;
757 
758 	gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (text_view),
759 					       GTK_TEXT_WINDOW_WIDGET,
760 					       event->x, event->y, &x, &y);
761 
762 	set_cursor_if_appropriate (GTK_TEXT_VIEW (text_view), x, y, cloud);
763 	return FALSE;
764 }
765 
766 /* Looks at all tags covering the position of iter in the text view,
767  * and if one of them is a link, follow it by showing the page identified
768  * by the data attached to it.
769  */
770 static void
follow_if_link(G_GNUC_UNUSED GtkWidget * text_view,GtkTextIter * iter,GdauiCloud * cloud)771 follow_if_link (G_GNUC_UNUSED GtkWidget *text_view, GtkTextIter *iter, GdauiCloud *cloud)
772 {
773 	GSList *tags = NULL, *tagp;
774 
775 	tags = gtk_text_iter_get_tags (iter);
776 	for (tagp = tags;  tagp;  tagp = tagp->next) {
777 		GtkTextTag *tag = tagp->data;
778 		gint row;
779 		row = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (tag), "row")) - 1;
780 		if (row >= 0) {
781 			row_clicked (cloud, row, tag);
782 			break;
783 		}
784         }
785 
786 	if (tags)
787 		g_slist_free (tags);
788 }
789 
790 /*
791  * Links can also be activated by clicking.
792  */
793 static void
event_after(GtkWidget * text_view,GdkEvent * ev,GdauiCloud * cloud)794 event_after (GtkWidget *text_view, GdkEvent *ev, GdauiCloud *cloud)
795 {
796 	GtkTextIter start, end, iter;
797 	GtkTextBuffer *buffer;
798 	GdkEventButton *event;
799 	gint x, y;
800 
801 	if (ev->type != GDK_BUTTON_RELEASE)
802 		return;
803 
804 	event = (GdkEventButton *)ev;
805 
806 	if (event->button != 1)
807 		return;
808 
809 	buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (text_view));
810 
811 	/* we shouldn't follow a link if the user has selected something */
812 	gtk_text_buffer_get_selection_bounds (buffer, &start, &end);
813 	if (gtk_text_iter_get_offset (&start) != gtk_text_iter_get_offset (&end))
814 		return;
815 
816 	gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (text_view),
817 					       GTK_TEXT_WINDOW_WIDGET,
818 					       event->x, event->y, &x, &y);
819 
820 	gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (text_view), &iter, x, y);
821 
822 	follow_if_link (text_view, &iter, cloud);
823 
824 	return;
825 }
826 
827 /*
828  * Links can be activated by pressing Enter.
829  */
830 static gboolean
key_press_event(GtkWidget * text_view,GdkEventKey * event,GdauiCloud * cloud)831 key_press_event (GtkWidget *text_view, GdkEventKey *event, GdauiCloud *cloud)
832 {
833         GtkTextIter iter;
834         GtkTextBuffer *buffer;
835 
836         switch (event->keyval) {
837         case GDK_KEY_Return:
838         case GDK_KEY_space:
839         case GDK_KEY_KP_Enter:
840                 buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (text_view));
841                 gtk_text_buffer_get_iter_at_mark (buffer, &iter,
842                                                   gtk_text_buffer_get_insert (buffer));
843                 follow_if_link (text_view, &iter, cloud);
844 		return TRUE;
845 	case GDK_KEY_Up:
846 	case GDK_KEY_Down:
847 	case GDK_KEY_Left:
848 	case GDK_KEY_Right:
849 		if ((cloud->priv->selection_mode == GTK_SELECTION_SINGLE) ||
850 		    (cloud->priv->selection_mode == GTK_SELECTION_BROWSE)) {
851 			GtkTextIter iter;
852 			if (cloud->priv->selected_tags) {
853 				GtkTextMark *mark;
854 				mark = gtk_text_buffer_get_insert (cloud->priv->tbuffer);
855 				gtk_text_buffer_get_iter_at_mark (cloud->priv->tbuffer, &iter, mark);
856 			}
857 			else if ((event->keyval == GDK_KEY_Right) || (event->keyval == GDK_KEY_Down))
858 				gtk_text_buffer_get_start_iter (cloud->priv->tbuffer, &iter);
859 			else
860 				gtk_text_buffer_get_end_iter (cloud->priv->tbuffer, &iter);
861 
862 			gtk_text_view_set_cursor_visible (GTK_TEXT_VIEW (cloud->priv->tview), TRUE);
863 			while (1) { /* loop to move the cursor enough positions to change the selected item */
864 				gboolean done = FALSE;
865 				GtkMovementStep mvt_type;
866 				gint mvt_amount;
867 				switch (event->keyval) {
868 				case GDK_KEY_Up:
869 					done = ! gtk_text_view_backward_display_line ((GtkTextView*)cloud->priv->tview, &iter);
870 					mvt_type = GTK_MOVEMENT_DISPLAY_LINES;
871 					mvt_amount = -1;
872 					break;
873 				case GDK_KEY_Down:
874 					done = ! gtk_text_view_forward_display_line ((GtkTextView*)cloud->priv->tview, &iter);
875 					mvt_type = GTK_MOVEMENT_DISPLAY_LINES;
876 					mvt_amount = 1;
877 					break;
878 				case GDK_KEY_Left:
879 					done = ! gtk_text_iter_backward_char (&iter);
880 					mvt_type = GTK_MOVEMENT_VISUAL_POSITIONS;
881 					mvt_amount = -1;
882 					break;
883 				default:
884 				case GDK_KEY_Right:
885 					done = ! gtk_text_iter_forward_char (&iter);
886 					mvt_type = GTK_MOVEMENT_VISUAL_POSITIONS;
887 					mvt_amount = 1;
888 					break;
889 				}
890 				if (done)
891 					break; /* end of treatment as no movement possible */
892 				g_signal_emit_by_name (cloud->priv->tview, "move-cursor",
893 						       mvt_type, mvt_amount, FALSE);
894 
895 				GtkTextMark *mark;
896 				mark = gtk_text_buffer_get_insert (cloud->priv->tbuffer);
897 				gtk_text_buffer_get_iter_at_mark (cloud->priv->tbuffer, &iter, mark);
898 
899 				GSList *tags, *tagp;
900 				done = FALSE;
901 				tags = gtk_text_iter_get_tags (&iter);
902 				for (tagp = tags;  tagp;  tagp = tagp->next) {
903 					GtkTextTag *tag = (GtkTextTag*) tagp->data;
904 					gint row;
905 					row = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (tag), "row")) - 1;
906 					if (row >= 0) {
907 						if ((cloud->priv->selected_tags &&
908 						     (tag != cloud->priv->selected_tags->data)) ||
909 						    !cloud->priv->selected_tags) {
910 							row_clicked (cloud, row, tag);
911 							done = TRUE;
912 							break;
913 						}
914 					}
915 				}
916 				if (tags)
917 					g_slist_free (tags);
918 				if (done) {
919 					GtkTextMark *mark;
920 
921 					mark = gtk_text_buffer_get_insert (cloud->priv->tbuffer);
922 					gtk_text_view_scroll_mark_onscreen ((GtkTextView*)cloud->priv->tview, mark);
923 					break;
924 				}
925 			}
926 			gtk_text_view_set_cursor_visible (GTK_TEXT_VIEW (cloud->priv->tview), FALSE);
927 			return TRUE;
928 		}
929         default:
930                 break;
931         }
932         return FALSE;
933 }
934 
935 typedef struct {
936 	GdauiCloud *cloud;
937 	const gchar *find;
938 } FilterData;
939 
940 static gchar *
prepare_cmp_string(const gchar * str)941 prepare_cmp_string (const gchar *str)
942 {
943 	GString *string;
944 	gchar *ptr, *tmp1, *tmp2;
945 
946 	/* lower case */
947 	tmp1 = g_utf8_strdown (str, -1);
948 
949 	/* normalize */
950 	tmp2 = g_utf8_normalize (tmp1, -1, G_NORMALIZE_DEFAULT);
951 	g_free (tmp1);
952 
953 	/* remove accents */
954 	string = g_string_new ("");
955 	for (ptr = tmp2; *ptr; ptr = g_utf8_next_char (ptr)) {
956 		gunichar uc;
957 		uc = g_utf8_get_char (ptr);
958 		if (! g_unichar_ismark (uc))
959 			g_string_append_unichar (string, uc);
960 	}
961 	return g_string_free (string, FALSE);
962 }
963 
964 static void
text_tag_table_foreach_cb(GtkTextTag * tag,FilterData * fdata)965 text_tag_table_foreach_cb (GtkTextTag *tag, FilterData *fdata)
966 {
967 	const GValue *cvalue;
968 	const gchar *label;
969 	gint row;
970 
971 	if (! fdata->cloud->priv->model)
972 		return;
973 	if (fdata->cloud->priv->label_column < 0)
974 		return;
975 
976 	/* check for the data model's column type */
977 	GdaColumn *column;
978 	column = gda_data_model_describe_column (fdata->cloud->priv->model,
979 						 fdata->cloud->priv->label_column);
980 	if (!column || (gda_column_get_g_type (column) != G_TYPE_STRING)) {
981 		g_warning (_("Wrong column type for label: expecting a string and got a %s"),
982 			   gda_g_type_to_string (gda_column_get_g_type (column)));
983 		return;
984 	}
985 
986 	row = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (tag), "row")) - 1;
987 	if (row < 0)
988 		return;
989 
990 	cvalue = gda_data_model_get_value_at (fdata->cloud->priv->model,
991 					      fdata->cloud->priv->label_column, row, NULL);
992 	if (!cvalue)
993 		return;
994 
995 	label = g_value_get_string (cvalue);
996 
997 	if (!(fdata->find) || !*(fdata->find)) {
998 		g_object_set (tag, "foreground", "#6161F2", NULL);
999 	}
1000 	else {
1001 		gchar *ptr, *lcname, *lcfind;
1002 		lcname = prepare_cmp_string (label);
1003 		lcfind = prepare_cmp_string (fdata->find);
1004 
1005 		ptr = strstr (lcname, lcfind);
1006 		if (!ptr) {
1007 			/* string not present in name */
1008 			g_object_set (tag, "foreground", "#DBDBDB", NULL);
1009 		}
1010 		else if ((ptr == lcname) ||
1011 			 ((*label == '"') && (ptr == lcname+1))) {
1012 			/* string present as start of name */
1013 			g_object_set (tag, "foreground", "#6161F2", NULL);
1014 		}
1015 		else {
1016 			/* string present in name but not at the start */
1017 			g_object_set (tag, "foreground", "#A0A0A0", NULL);
1018 		}
1019 
1020 		g_free (lcname);
1021 		g_free (lcfind);
1022 	}
1023 }
1024 
1025 /**
1026  * gdaui_cloud_filter:
1027  * @cloud: a #GdauiCloud widget
1028  * @filter: (allow-none): the filter to use, or %NULL to remove any filter
1029  *
1030  * Filters the elements displayed in @cloud, by altering their color.
1031  *
1032  * Since: 4.2
1033  */
1034 void
gdaui_cloud_filter(GdauiCloud * cloud,const gchar * filter)1035 gdaui_cloud_filter (GdauiCloud *cloud, const gchar *filter)
1036 {
1037 	g_return_if_fail (GDAUI_IS_CLOUD (cloud));
1038 
1039 	GtkTextTagTable *tags_table = gtk_text_buffer_get_tag_table (cloud->priv->tbuffer);
1040 
1041 	FilterData fdata;
1042 	fdata.cloud = cloud;
1043 	fdata.find = filter;
1044 	gtk_text_tag_table_foreach (tags_table, (GtkTextTagTableForeach) text_tag_table_foreach_cb,
1045 				    (gpointer) &(fdata));
1046 }
1047 
1048 static void
find_entry_changed_cb(GtkWidget * entry,GdauiCloud * cloud)1049 find_entry_changed_cb (GtkWidget *entry, GdauiCloud *cloud)
1050 {
1051 	gchar *find = gtk_editable_get_chars (GTK_EDITABLE (entry), 0, -1);
1052 	gdaui_cloud_filter (cloud, find);
1053 	g_free (find);
1054 }
1055 
1056 /**
1057  * gdaui_cloud_create_filter_widget:
1058  * @cloud: a #GdauiCloud widget
1059  *
1060  * Creates a search widget linked directly to modify @cloud's appearance.
1061  *
1062  * Returns: (transfer full): a new widget
1063  *
1064  * Since: 4.2
1065  */
1066 GtkWidget *
gdaui_cloud_create_filter_widget(GdauiCloud * cloud)1067 gdaui_cloud_create_filter_widget (GdauiCloud *cloud)
1068 {
1069 	GtkWidget *hbox, *label, *wid;
1070 	g_return_val_if_fail (GDAUI_IS_CLOUD (cloud), NULL);
1071 
1072 	hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
1073 
1074 	label = gtk_label_new (_("Find:"));
1075 	gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0);
1076 	wid = gtk_entry_new ();
1077 	g_signal_connect (wid, "changed",
1078 			  G_CALLBACK (find_entry_changed_cb), cloud);
1079 	gtk_box_pack_start (GTK_BOX (hbox), wid, TRUE, TRUE, 0);
1080 	gtk_widget_show_all (hbox);
1081 	gtk_widget_hide (hbox);
1082 
1083 	return hbox;
1084 }
1085 
1086 /**
1087  * gdaui_cloud_set_weight_func:
1088  * @cloud: a #GdauiCloud widget
1089  * @func: (allow-none) (scope notified): a #GdauiCloudWeightFunc function which computes weights, or %NULL to unset
1090  * @data: (allow-none): a pointer to pass as last argument of @func each time it is called, or %NULL
1091  *
1092  * Specifies a function called by @cloud to compute each row's respective weight.
1093  *
1094  * Since: 4.2
1095  */
1096 void
gdaui_cloud_set_weight_func(GdauiCloud * cloud,GdauiCloudWeightFunc func,gpointer data)1097 gdaui_cloud_set_weight_func (GdauiCloud *cloud, GdauiCloudWeightFunc func, gpointer data)
1098 {
1099 	g_return_if_fail (GDAUI_IS_CLOUD (cloud));
1100 	if ((cloud->priv->weight_func != func) || (cloud->priv->weight_func_data != data)) {
1101 		cloud->priv->weight_func = func;
1102 		cloud->priv->weight_func_data = data;
1103 		update_display (cloud);
1104 	}
1105 }
1106 
1107 /* GdauiDataSelector interface */
1108 static GdaDataModel *
cloud_selector_get_model(GdauiDataSelector * iface)1109 cloud_selector_get_model (GdauiDataSelector *iface)
1110 {
1111 	GdauiCloud *cloud;
1112 	cloud = GDAUI_CLOUD (iface);
1113 	return cloud->priv->model;
1114 }
1115 
1116 static void
cloud_selector_set_model(GdauiDataSelector * iface,GdaDataModel * model)1117 cloud_selector_set_model (GdauiDataSelector *iface, GdaDataModel *model)
1118 {
1119 	g_object_set (G_OBJECT (iface), "model", model, NULL);
1120 }
1121 
1122 static GArray *
cloud_selector_get_selected_rows(GdauiDataSelector * iface)1123 cloud_selector_get_selected_rows (GdauiDataSelector *iface)
1124 {
1125 	GArray *retval = NULL;
1126 	GdauiCloud *cloud;
1127 	GSList *list;
1128 
1129 	cloud = GDAUI_CLOUD (iface);
1130 	for (list = cloud->priv->selected_tags; list; list = list->next) {
1131 		gint row;
1132 		row = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (list->data), "row")) - 1;
1133 		if (row >= 0) {
1134 			if (!retval)
1135 				retval = g_array_new (FALSE, FALSE, sizeof (gint));
1136 			g_array_append_val (retval, row);
1137 		}
1138 	}
1139 	return retval;
1140 
1141 }
1142 
1143 static GdaDataModelIter *
cloud_selector_get_data_set(GdauiDataSelector * iface)1144 cloud_selector_get_data_set (GdauiDataSelector *iface)
1145 {
1146 	GdauiCloud *cloud;
1147 
1148 	cloud = GDAUI_CLOUD (iface);
1149 	if (! cloud->priv->iter && cloud->priv->model) {
1150 		cloud->priv->iter = gda_data_model_create_iter (cloud->priv->model);
1151 		sync_iter_with_selection (cloud);
1152 	}
1153 
1154 	return cloud->priv->iter;
1155 }
1156 
1157 typedef struct {
1158 	gint row_to_find;
1159 	GtkTextTag *tag;
1160 } RowLookup;
1161 
1162 static void
text_tag_table_foreach_cb2(GtkTextTag * tag,RowLookup * rl)1163 text_tag_table_foreach_cb2 (GtkTextTag *tag, RowLookup *rl)
1164 {
1165 	if (rl->tag)
1166 		return; /* row already found */
1167 	gint srow;
1168 	srow = GPOINTER_TO_INT (g_object_get_data ((GObject*) tag, "row")) - 1;
1169 	if (srow == rl->row_to_find)
1170 		rl->tag = tag;
1171 }
1172 
1173 static gboolean
cloud_selector_select_row(GdauiDataSelector * iface,gint row)1174 cloud_selector_select_row (GdauiDataSelector *iface, gint row)
1175 {
1176 	GdauiCloud *cloud;
1177 	GSList *list;
1178 
1179 	cloud = GDAUI_CLOUD (iface);
1180 	if (cloud->priv->selection_mode == GTK_SELECTION_NONE)
1181 		return FALSE;
1182 
1183 	/* test if row already selected */
1184 	for (list = cloud->priv->selected_tags; list; list = list->next) {
1185 		gint srow;
1186 		srow = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (list->data), "row")) - 1;
1187 		if (srow == row)
1188 			return TRUE;
1189 	}
1190 
1191 	/* try to select row */
1192 	RowLookup rl;
1193 	rl.row_to_find = row;
1194 	rl.tag = NULL;
1195 	gtk_text_tag_table_foreach (gtk_text_buffer_get_tag_table (cloud->priv->tbuffer),
1196 				    (GtkTextTagTableForeach) text_tag_table_foreach_cb2,
1197 				    (gpointer) &rl);
1198 	if (rl.tag) {
1199 		row_clicked (cloud, row, rl.tag);
1200 		for (list = cloud->priv->selected_tags; list; list = list->next) {
1201 			gint srow;
1202 			srow = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (list->data), "row")) - 1;
1203 			if (srow == row)
1204 				return TRUE;
1205 		}
1206 		return FALSE;
1207 	}
1208 	else
1209 		return FALSE;
1210 }
1211 
1212 static void
cloud_selector_unselect_row(GdauiDataSelector * iface,gint row)1213 cloud_selector_unselect_row (GdauiDataSelector *iface, gint row)
1214 {
1215 	GdauiCloud *cloud;
1216 	GSList *list;
1217 
1218 	cloud = GDAUI_CLOUD (iface);
1219 	if (cloud->priv->selection_mode == GTK_SELECTION_NONE)
1220 		return;
1221 
1222 	/* test if row already selected */
1223 	for (list = cloud->priv->selected_tags; list; list = list->next) {
1224 		gint srow;
1225 		GtkTextTag *tag = (GtkTextTag*) list->data;
1226 		srow = GPOINTER_TO_INT (g_object_get_data ((GObject*) tag, "row")) - 1;
1227 		if (srow == row) {
1228 			cloud->priv->selected_tags = g_slist_remove (cloud->priv->selected_tags, tag);
1229 			g_object_set ((GObject*) tag,
1230 				      "background-set", FALSE,
1231 				      NULL);
1232 			g_object_unref ((GObject*) tag);
1233 
1234 			sync_iter_with_selection (cloud);
1235 			g_signal_emit_by_name (cloud, "selection-changed");
1236 			break;
1237 		}
1238 	}
1239 }
1240 
1241 static void
cloud_selector_set_column_visible(G_GNUC_UNUSED GdauiDataSelector * iface,G_GNUC_UNUSED gint column,G_GNUC_UNUSED gboolean visible)1242 cloud_selector_set_column_visible (G_GNUC_UNUSED GdauiDataSelector *iface, G_GNUC_UNUSED gint column,
1243 				   G_GNUC_UNUSED gboolean visible)
1244 {
1245 	/* nothing to do */
1246 }
1247