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