Assamese translation updated
[platform/upstream/evolution-data-server.git] / libedataserverui / e-category-completion.c
1 /*
2  * This program is free software; you can redistribute it and/or
3  * modify it under the terms of the GNU Lesser General Public
4  * License as published by the Free Software Foundation; either
5  * version 2 of the License, or (at your option) version 3.
6  *
7  * This program is distributed in the hope that it will be useful,
8  * but WITHOUT ANY WARRANTY; without even the implied warranty of
9  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
10  * Lesser General Public License for more details.
11  *
12  * You should have received a copy of the GNU Lesser General Public
13  * License along with the program; if not, see <http://www.gnu.org/licenses/>
14  *
15  * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
16  */
17
18 #include "e-category-completion.h"
19
20 #ifdef HAVE_CONFIG_H
21 #include <config.h>
22 #endif
23
24 #include <string.h>
25 #include <glib/gi18n-lib.h>
26
27 #include <libedataserver/libedataserver.h>
28
29 #define E_CATEGORY_COMPLETION_GET_PRIVATE(obj) \
30         (G_TYPE_INSTANCE_GET_PRIVATE \
31         ((obj), E_TYPE_CATEGORY_COMPLETION, ECategoryCompletionPrivate))
32
33 struct _ECategoryCompletionPrivate {
34         GtkWidget *last_known_entry;
35         gchar *create;
36         gchar *prefix;
37 };
38
39 enum {
40         COLUMN_PIXBUF,
41         COLUMN_CATEGORY,
42         COLUMN_NORMALIZED,
43         NUM_COLUMNS
44 };
45
46 G_DEFINE_TYPE (
47         ECategoryCompletion,
48         e_category_completion,
49         GTK_TYPE_ENTRY_COMPLETION)
50
51 /* Forward Declarations */
52
53 static void
54 category_completion_track_entry (GtkEntryCompletion *completion);
55
56 static void
57 category_completion_build_model (GtkEntryCompletion *completion)
58 {
59         GtkListStore *store;
60         GList *list;
61
62         store = gtk_list_store_new (
63                 NUM_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING);
64
65         list = e_categories_get_list ();
66         while (list != NULL) {
67                 const gchar *category = list->data;
68                 const gchar *filename;
69                 gchar *normalized;
70                 gchar *casefolded;
71                 GdkPixbuf *pixbuf = NULL;
72                 GtkTreeIter iter;
73
74                 /* Only add user-visible categories. */
75                 if (!e_categories_is_searchable (category)) {
76                         list = g_list_delete_link (list, list);
77                         continue;
78                 }
79
80                 filename = e_categories_get_icon_file_for (category);
81                 if (filename != NULL && *filename != '\0')
82                         pixbuf = gdk_pixbuf_new_from_file (filename, NULL);
83
84                 normalized = g_utf8_normalize (
85                         category, -1, G_NORMALIZE_DEFAULT);
86                 casefolded = g_utf8_casefold (normalized, -1);
87
88                 gtk_list_store_append (store, &iter);
89
90                 gtk_list_store_set (
91                         store, &iter, COLUMN_PIXBUF, pixbuf,
92                         COLUMN_CATEGORY, category, COLUMN_NORMALIZED,
93                         casefolded, -1);
94
95                 g_free (normalized);
96                 g_free (casefolded);
97
98                 if (pixbuf != NULL)
99                         g_object_unref (pixbuf);
100
101                 list = g_list_delete_link (list, list);
102         }
103
104         gtk_entry_completion_set_model (completion, GTK_TREE_MODEL (store));
105 }
106
107 static void
108 category_completion_categories_changed_cb (GObject *some_private_object,
109                                            GtkEntryCompletion *completion)
110 {
111         category_completion_build_model (completion);
112 }
113
114 static void
115 category_completion_complete (GtkEntryCompletion *completion,
116                               const gchar *category)
117 {
118         GtkEditable *editable;
119         GtkWidget *entry;
120         const gchar *text;
121         const gchar *cp;
122         gint start_pos;
123         gint end_pos;
124         glong offset;
125
126         entry = gtk_entry_completion_get_entry (completion);
127
128         editable = GTK_EDITABLE (entry);
129         text = gtk_entry_get_text (GTK_ENTRY (entry));
130
131         /* Get the cursor position as a character offset. */
132         offset = gtk_editable_get_position (editable);
133
134         /* Find the rightmost comma before the cursor. */
135         cp = g_utf8_offset_to_pointer (text, offset);
136         cp = g_utf8_strrchr (text, (gssize) (cp - text), ',');
137
138         /* Calculate the selection start position as a character offset. */
139         if (cp == NULL)
140                 offset = 0;
141         else {
142                 cp = g_utf8_next_char (cp);
143                 if (g_unichar_isspace (g_utf8_get_char (cp)))
144                         cp = g_utf8_next_char (cp);
145                 offset = g_utf8_pointer_to_offset (text, cp);
146         }
147         start_pos = (gint) offset;
148
149         /* Find the leftmost comma after the cursor. */
150         cp = g_utf8_offset_to_pointer (text, offset);
151         cp = g_utf8_strchr (cp, -1, ',');
152
153         /* Calculate the selection end position as a character offset. */
154         if (cp == NULL)
155                 offset = -1;
156         else {
157                 cp = g_utf8_next_char (cp);
158                 if (g_unichar_isspace (g_utf8_get_char (cp)))
159                         cp = g_utf8_next_char (cp);
160                 offset = g_utf8_pointer_to_offset (text, cp);
161         }
162         end_pos = (gint) offset;
163
164         /* Complete the partially typed category. */
165         gtk_editable_delete_text (editable, start_pos, end_pos);
166         gtk_editable_insert_text (editable, category, -1, &start_pos);
167         gtk_editable_insert_text (editable, ",", 1, &start_pos);
168         gtk_editable_set_position (editable, start_pos);
169 }
170
171 static gboolean
172 category_completion_is_match (GtkEntryCompletion *completion,
173                               const gchar *key,
174                               GtkTreeIter *iter)
175 {
176         ECategoryCompletionPrivate *priv;
177         GtkTreeModel *model;
178         GtkWidget *entry;
179         GValue value = { 0, };
180         gboolean match;
181
182         priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion);
183         entry = gtk_entry_completion_get_entry (completion);
184         model = gtk_entry_completion_get_model (completion);
185
186         /* XXX This would be easier if GtkEntryCompletion had an 'entry'
187          *     property that we could listen to for notifications. */
188         if (entry != priv->last_known_entry)
189                 category_completion_track_entry (completion);
190
191         if (priv->prefix == NULL)
192                 return FALSE;
193
194         gtk_tree_model_get_value (model, iter, COLUMN_NORMALIZED, &value);
195         match = g_str_has_prefix (g_value_get_string (&value), priv->prefix);
196         g_value_unset (&value);
197
198         return match;
199 }
200
201 static void
202 category_completion_update_prefix (GtkEntryCompletion *completion)
203 {
204         ECategoryCompletionPrivate *priv;
205         GtkEditable *editable;
206         GtkTreeModel *model;
207         GtkWidget *entry;
208         GtkTreeIter iter;
209         const gchar *text;
210         const gchar *start;
211         const gchar *end;
212         const gchar *cp;
213         gboolean valid;
214         gchar *input;
215         glong offset;
216
217         priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion);
218         entry = gtk_entry_completion_get_entry (completion);
219         model = gtk_entry_completion_get_model (completion);
220
221         /* XXX This would be easier if GtkEntryCompletion had an 'entry'
222          *     property that we could listen to for notifications. */
223         if (entry != priv->last_known_entry) {
224                 category_completion_track_entry (completion);
225                 return;
226         }
227
228         editable = GTK_EDITABLE (entry);
229         text = gtk_entry_get_text (GTK_ENTRY (entry));
230
231         /* Get the cursor position as a character offset. */
232         offset = gtk_editable_get_position (editable);
233
234         /* Find the rightmost comma before the cursor. */
235         cp = g_utf8_offset_to_pointer (text, offset);
236         cp = g_utf8_strrchr (text, (gsize) (cp - text), ',');
237
238         /* Mark the start of the prefix. */
239         if (cp == NULL)
240                 start = text;
241         else {
242                 cp = g_utf8_next_char (cp);
243                 if (g_unichar_isspace (g_utf8_get_char (cp)))
244                         cp = g_utf8_next_char (cp);
245                 start = cp;
246         }
247
248         /* Find the leftmost comma after the cursor. */
249         cp = g_utf8_offset_to_pointer (text, offset);
250         cp = g_utf8_strchr (cp, -1, ',');
251
252         /* Mark the end of the prefix. */
253         if (cp == NULL)
254                 end = text + strlen (text);
255         else
256                 end = cp;
257
258         if (priv->create != NULL)
259                 gtk_entry_completion_delete_action (completion, 0);
260
261         g_free (priv->create);
262         priv->create = NULL;
263
264         g_free (priv->prefix);
265         priv->prefix = NULL;
266
267         if (start == end)
268                 return;
269
270         input = g_strstrip (g_strndup (start, end - start));
271         priv->create = input;
272
273         input = g_utf8_normalize (input, -1, G_NORMALIZE_DEFAULT);
274         priv->prefix = g_utf8_casefold (input, -1);
275         g_free (input);
276
277         if (*priv->create == '\0') {
278                 g_free (priv->create);
279                 priv->create = NULL;
280                 return;
281         }
282
283         valid = gtk_tree_model_get_iter_first (model, &iter);
284         while (valid) {
285                 GValue value = { 0, };
286
287                 gtk_tree_model_get_value (
288                         model, &iter, COLUMN_NORMALIZED, &value);
289                 if (strcmp (g_value_get_string (&value), priv->prefix) == 0) {
290                         g_value_unset (&value);
291                         g_free (priv->create);
292                         priv->create = NULL;
293                         return;
294                 }
295                 g_value_unset (&value);
296
297                 valid = gtk_tree_model_iter_next (model, &iter);
298         }
299
300         input = g_strdup_printf (_("Create category \"%s\""), priv->create);
301         gtk_entry_completion_insert_action_text (completion, 0, input);
302         g_free (input);
303 }
304
305 static gboolean
306 category_completion_sanitize_suffix (GtkEntry *entry,
307                                      GdkEventFocus *event,
308                                      GtkEntryCompletion *completion)
309 {
310         const gchar *text;
311
312         g_return_val_if_fail (entry != NULL, FALSE);
313         g_return_val_if_fail (completion != NULL, FALSE);
314
315         text = gtk_entry_get_text (entry);
316         if (text) {
317                 gint len = strlen (text), old_len = len;
318
319                 while (len > 0 && (text[len -1] == ' ' || text[len - 1] == ','))
320                         len--;
321
322                 if (old_len != len) {
323                         gchar *tmp = g_strndup (text, len);
324
325                         gtk_entry_set_text (entry, tmp);
326
327                         g_free (tmp);
328                 }
329         }
330
331         return FALSE;
332 }
333
334 static void
335 category_completion_track_entry (GtkEntryCompletion *completion)
336 {
337         ECategoryCompletionPrivate *priv;
338
339         priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion);
340
341         if (priv->last_known_entry != NULL) {
342                 g_signal_handlers_disconnect_matched (
343                         priv->last_known_entry, G_SIGNAL_MATCH_DATA,
344                         0, 0, NULL, NULL, completion);
345                 g_object_unref (priv->last_known_entry);
346         }
347
348         g_free (priv->prefix);
349         priv->prefix = NULL;
350
351         priv->last_known_entry = gtk_entry_completion_get_entry (completion);
352         if (priv->last_known_entry == NULL)
353                 return;
354
355         g_object_ref (priv->last_known_entry);
356
357         g_signal_connect_swapped (
358                 priv->last_known_entry, "notify::cursor-position",
359                 G_CALLBACK (category_completion_update_prefix), completion);
360
361         g_signal_connect_swapped (
362                 priv->last_known_entry, "notify::text",
363                 G_CALLBACK (category_completion_update_prefix), completion);
364
365         g_signal_connect (
366                 priv->last_known_entry, "focus-out-event",
367                 G_CALLBACK (category_completion_sanitize_suffix), completion);
368
369         category_completion_update_prefix (completion);
370 }
371
372 static void
373 category_completion_constructed (GObject *object)
374 {
375         GtkCellRenderer *renderer;
376         GtkEntryCompletion *completion;
377
378         /* Chain up to parent's constructed() method. */
379         G_OBJECT_CLASS (e_category_completion_parent_class)->constructed (object);
380
381         completion = GTK_ENTRY_COMPLETION (object);
382
383         gtk_entry_completion_set_match_func (
384                 completion, (GtkEntryCompletionMatchFunc)
385                 category_completion_is_match, NULL, NULL);
386
387         gtk_entry_completion_set_text_column (completion, COLUMN_CATEGORY);
388
389         renderer = gtk_cell_renderer_pixbuf_new ();
390         gtk_cell_layout_pack_start (
391                 GTK_CELL_LAYOUT (completion), renderer, FALSE);
392         gtk_cell_layout_add_attribute (
393                 GTK_CELL_LAYOUT (completion),
394                 renderer, "pixbuf", COLUMN_PIXBUF);
395         gtk_cell_layout_reorder (
396                 GTK_CELL_LAYOUT (completion), renderer, 0);
397
398         e_categories_register_change_listener (
399                 G_CALLBACK (category_completion_categories_changed_cb),
400                 completion);
401
402         category_completion_build_model (completion);
403 }
404
405 static void
406 category_completion_dispose (GObject *object)
407 {
408         ECategoryCompletionPrivate *priv;
409
410         priv = E_CATEGORY_COMPLETION_GET_PRIVATE (object);
411
412         if (priv->last_known_entry != NULL) {
413                 g_signal_handlers_disconnect_matched (
414                         priv->last_known_entry, G_SIGNAL_MATCH_DATA,
415                         0, 0, NULL, NULL, object);
416                 g_object_unref (priv->last_known_entry);
417                 priv->last_known_entry = NULL;
418         }
419
420         /* Chain up to parent's dispose() method. */
421         G_OBJECT_CLASS (e_category_completion_parent_class)->dispose (object);
422 }
423
424 static void
425 category_completion_finalize (GObject *object)
426 {
427         ECategoryCompletionPrivate *priv;
428
429         priv = E_CATEGORY_COMPLETION_GET_PRIVATE (object);
430
431         g_free (priv->create);
432         g_free (priv->prefix);
433
434         e_categories_unregister_change_listener (
435                 G_CALLBACK (category_completion_categories_changed_cb),
436                 object);
437
438         /* Chain up to parent's finalize() method. */
439         G_OBJECT_CLASS (e_category_completion_parent_class)->finalize (object);
440 }
441
442 static gboolean
443 category_completion_match_selected (GtkEntryCompletion *completion,
444                                     GtkTreeModel *model,
445                                     GtkTreeIter *iter)
446 {
447         GValue value = { 0, };
448
449         gtk_tree_model_get_value (model, iter, COLUMN_CATEGORY, &value);
450         category_completion_complete (completion, g_value_get_string (&value));
451         g_value_unset (&value);
452
453         return TRUE;
454 }
455
456 static void
457 category_completion_action_activated (GtkEntryCompletion *completion,
458                                       gint index)
459 {
460         ECategoryCompletionPrivate *priv;
461         gchar *category;
462
463         priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion);
464
465         category = g_strdup (priv->create);
466         e_categories_add (category, NULL, NULL, TRUE);
467         category_completion_complete (completion, category);
468         g_free (category);
469 }
470
471 static void
472 e_category_completion_class_init (ECategoryCompletionClass *class)
473 {
474         GObjectClass *object_class;
475         GtkEntryCompletionClass *entry_completion_class;
476
477         g_type_class_add_private (class, sizeof (ECategoryCompletionPrivate));
478
479         object_class = G_OBJECT_CLASS (class);
480         object_class->constructed = category_completion_constructed;
481         object_class->dispose = category_completion_dispose;
482         object_class->finalize = category_completion_finalize;
483
484         entry_completion_class = GTK_ENTRY_COMPLETION_CLASS (class);
485         entry_completion_class->match_selected = category_completion_match_selected;
486         entry_completion_class->action_activated = category_completion_action_activated;
487 }
488
489 static void
490 e_category_completion_init (ECategoryCompletion *category_completion)
491 {
492         category_completion->priv =
493                 E_CATEGORY_COMPLETION_GET_PRIVATE (category_completion);
494 }
495
496 /**
497  * e_category_completion_new:
498  *
499  * Since: 2.26
500  **/
501 GtkEntryCompletion *
502 e_category_completion_new (void)
503 {
504         return g_object_new (E_TYPE_CATEGORY_COMPLETION, NULL);
505 }