Updated Spanish translation
[evolution.git] / e-util / e-category-completion.c
blob8210f72f60a8fcbb2c5cc59851f87dd8205eb5db
1 /*
2 * This program is free software; you can redistribute it and/or modify it
3 * under the terms of the GNU Lesser General Public License as published by
4 * the Free Software Foundation.
6 * This program is distributed in the hope that it will be useful, but
7 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
8 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
9 * for more details.
11 * You should have received a copy of the GNU Lesser General Public License
12 * along with this program; if not, see <http://www.gnu.org/licenses/>.
14 * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
17 #include "e-category-completion.h"
19 #ifdef HAVE_CONFIG_H
20 #include <config.h>
21 #endif
23 #include <string.h>
24 #include <glib/gi18n-lib.h>
26 #include <libedataserver/libedataserver.h>
28 #include "e-misc-utils.h"
30 #define E_CATEGORY_COMPLETION_GET_PRIVATE(obj) \
31 (G_TYPE_INSTANCE_GET_PRIVATE \
32 ((obj), E_TYPE_CATEGORY_COMPLETION, ECategoryCompletionPrivate))
34 struct _ECategoryCompletionPrivate {
35 GtkWidget *last_known_entry;
36 gchar *create;
37 gchar *prefix;
39 gulong notify_cursor_position_id;
40 gulong notify_text_id;
43 enum {
44 COLUMN_PIXBUF,
45 COLUMN_CATEGORY,
46 COLUMN_NORMALIZED,
47 NUM_COLUMNS
50 G_DEFINE_TYPE (
51 ECategoryCompletion,
52 e_category_completion,
53 GTK_TYPE_ENTRY_COMPLETION)
55 /* Forward Declarations */
57 static void
58 category_completion_track_entry (GtkEntryCompletion *completion);
60 static void
61 category_completion_build_model (GtkEntryCompletion *completion)
63 GtkListStore *store;
64 GList *list;
66 store = gtk_list_store_new (
67 NUM_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING);
69 list = e_categories_dup_list ();
70 while (list != NULL) {
71 const gchar *category = list->data;
72 gchar *filename;
73 gchar *normalized;
74 gchar *casefolded;
75 GdkPixbuf *pixbuf = NULL;
76 GtkTreeIter iter;
78 /* Only add user-visible categories. */
79 if (!e_categories_is_searchable (category)) {
80 g_free (list->data);
81 list = g_list_delete_link (list, list);
82 continue;
85 filename = e_categories_dup_icon_file_for (category);
86 if (filename != NULL && *filename != '\0')
87 pixbuf = gdk_pixbuf_new_from_file (filename, NULL);
88 g_free (filename);
90 normalized = g_utf8_normalize (
91 category, -1, G_NORMALIZE_DEFAULT);
92 casefolded = g_utf8_casefold (normalized, -1);
94 gtk_list_store_append (store, &iter);
96 gtk_list_store_set (
97 store, &iter, COLUMN_PIXBUF, pixbuf,
98 COLUMN_CATEGORY, category, COLUMN_NORMALIZED,
99 casefolded, -1);
101 g_free (normalized);
102 g_free (casefolded);
104 if (pixbuf != NULL)
105 g_object_unref (pixbuf);
107 g_free (list->data);
108 list = g_list_delete_link (list, list);
111 gtk_entry_completion_set_model (completion, GTK_TREE_MODEL (store));
114 static void
115 category_completion_categories_changed_cb (GObject *some_private_object,
116 GtkEntryCompletion *completion)
118 category_completion_build_model (completion);
121 static void
122 category_completion_complete (GtkEntryCompletion *completion,
123 const gchar *category)
125 GtkEditable *editable;
126 GtkWidget *entry;
127 const gchar *text;
128 const gchar *cp;
129 gint start_pos;
130 gint end_pos;
131 glong offset;
133 entry = gtk_entry_completion_get_entry (completion);
135 editable = GTK_EDITABLE (entry);
136 text = gtk_entry_get_text (GTK_ENTRY (entry));
138 /* Get the cursor position as a character offset. */
139 offset = gtk_editable_get_position (editable);
141 /* Find the rightmost comma before the cursor. */
142 cp = g_utf8_offset_to_pointer (text, offset);
143 cp = g_utf8_strrchr (text, (gssize) (cp - text), ',');
145 /* Calculate the selection start position as a character offset. */
146 if (cp == NULL)
147 offset = 0;
148 else {
149 cp = g_utf8_next_char (cp);
150 if (g_unichar_isspace (g_utf8_get_char (cp)))
151 cp = g_utf8_next_char (cp);
152 offset = g_utf8_pointer_to_offset (text, cp);
154 start_pos = (gint) offset;
156 /* Find the leftmost comma after the cursor. */
157 cp = g_utf8_offset_to_pointer (text, offset);
158 cp = g_utf8_strchr (cp, -1, ',');
160 /* Calculate the selection end position as a character offset. */
161 if (cp == NULL)
162 offset = -1;
163 else {
164 cp = g_utf8_next_char (cp);
165 if (g_unichar_isspace (g_utf8_get_char (cp)))
166 cp = g_utf8_next_char (cp);
167 offset = g_utf8_pointer_to_offset (text, cp);
169 end_pos = (gint) offset;
171 /* Complete the partially typed category. */
172 gtk_editable_delete_text (editable, start_pos, end_pos);
173 gtk_editable_insert_text (editable, category, -1, &start_pos);
174 gtk_editable_insert_text (editable, ",", 1, &start_pos);
175 gtk_editable_set_position (editable, start_pos);
178 static gboolean
179 category_completion_is_match (GtkEntryCompletion *completion,
180 const gchar *key,
181 GtkTreeIter *iter)
183 ECategoryCompletionPrivate *priv;
184 GtkTreeModel *model;
185 GtkWidget *entry;
186 GValue value = { 0, };
187 gboolean match;
189 priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion);
190 entry = gtk_entry_completion_get_entry (completion);
191 model = gtk_entry_completion_get_model (completion);
193 /* XXX This would be easier if GtkEntryCompletion had an 'entry'
194 * property that we could listen to for notifications. */
195 if (entry != priv->last_known_entry)
196 category_completion_track_entry (completion);
198 if (priv->prefix == NULL)
199 return FALSE;
201 gtk_tree_model_get_value (model, iter, COLUMN_NORMALIZED, &value);
202 match = g_str_has_prefix (g_value_get_string (&value), priv->prefix);
203 g_value_unset (&value);
205 return match;
208 static void
209 category_completion_update_prefix (GtkEntryCompletion *completion)
211 ECategoryCompletionPrivate *priv;
212 GtkEditable *editable;
213 GtkTreeModel *model;
214 GtkWidget *entry;
215 GtkTreeIter iter;
216 const gchar *text;
217 const gchar *start;
218 const gchar *end;
219 const gchar *cp;
220 gboolean valid;
221 gchar *input;
222 glong offset;
224 priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion);
225 entry = gtk_entry_completion_get_entry (completion);
226 model = gtk_entry_completion_get_model (completion);
228 /* XXX This would be easier if GtkEntryCompletion had an 'entry'
229 * property that we could listen to for notifications. */
230 if (entry != priv->last_known_entry) {
231 category_completion_track_entry (completion);
232 return;
235 editable = GTK_EDITABLE (entry);
236 text = gtk_entry_get_text (GTK_ENTRY (entry));
238 /* Get the cursor position as a character offset. */
239 offset = gtk_editable_get_position (editable);
241 /* Find the rightmost comma before the cursor. */
242 cp = g_utf8_offset_to_pointer (text, offset);
243 cp = g_utf8_strrchr (text, (gsize) (cp - text), ',');
245 /* Mark the start of the prefix. */
246 if (cp == NULL)
247 start = text;
248 else {
249 cp = g_utf8_next_char (cp);
250 if (g_unichar_isspace (g_utf8_get_char (cp)))
251 cp = g_utf8_next_char (cp);
252 start = cp;
255 /* Find the leftmost comma after the cursor. */
256 cp = g_utf8_offset_to_pointer (text, offset);
257 cp = g_utf8_strchr (cp, -1, ',');
259 /* Mark the end of the prefix. */
260 if (cp == NULL)
261 end = text + strlen (text);
262 else
263 end = cp;
265 if (priv->create != NULL)
266 gtk_entry_completion_delete_action (completion, 0);
268 g_free (priv->create);
269 priv->create = NULL;
271 g_free (priv->prefix);
272 priv->prefix = NULL;
274 if (start == end)
275 return;
277 input = g_strstrip (g_strndup (start, end - start));
278 priv->create = input;
280 input = g_utf8_normalize (input, -1, G_NORMALIZE_DEFAULT);
281 priv->prefix = g_utf8_casefold (input, -1);
282 g_free (input);
284 if (*priv->create == '\0') {
285 g_free (priv->create);
286 priv->create = NULL;
287 return;
290 valid = gtk_tree_model_get_iter_first (model, &iter);
291 while (valid) {
292 GValue value = { 0, };
294 gtk_tree_model_get_value (
295 model, &iter, COLUMN_NORMALIZED, &value);
296 if (strcmp (g_value_get_string (&value), priv->prefix) == 0) {
297 g_value_unset (&value);
298 g_free (priv->create);
299 priv->create = NULL;
300 return;
302 g_value_unset (&value);
304 valid = gtk_tree_model_iter_next (model, &iter);
307 input = g_strdup_printf (_("Create category \"%s\""), priv->create);
308 gtk_entry_completion_insert_action_text (completion, 0, input);
309 g_free (input);
312 static gboolean
313 category_completion_sanitize_suffix (GtkEntry *entry,
314 GdkEventFocus *event,
315 GtkEntryCompletion *completion)
317 const gchar *text;
319 g_return_val_if_fail (entry != NULL, FALSE);
320 g_return_val_if_fail (completion != NULL, FALSE);
322 text = gtk_entry_get_text (entry);
323 if (text) {
324 gint len = strlen (text), old_len = len;
326 while (len > 0 && (text[len -1] == ' ' || text[len - 1] == ','))
327 len--;
329 if (old_len != len) {
330 gchar *tmp = g_strndup (text, len);
332 gtk_entry_set_text (entry, tmp);
334 g_free (tmp);
338 return FALSE;
341 static void
342 category_completion_track_entry (GtkEntryCompletion *completion)
344 ECategoryCompletionPrivate *priv;
346 priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion);
348 if (priv->last_known_entry != NULL) {
349 g_signal_handlers_disconnect_matched (
350 priv->last_known_entry, G_SIGNAL_MATCH_DATA,
351 0, 0, NULL, NULL, completion);
352 e_signal_disconnect_notify_handler (priv->last_known_entry, &priv->notify_cursor_position_id);
353 e_signal_disconnect_notify_handler (priv->last_known_entry, &priv->notify_text_id);
354 g_object_unref (priv->last_known_entry);
357 g_free (priv->prefix);
358 priv->prefix = NULL;
360 priv->last_known_entry = gtk_entry_completion_get_entry (completion);
361 if (priv->last_known_entry == NULL)
362 return;
364 g_object_ref (priv->last_known_entry);
366 priv->notify_cursor_position_id = e_signal_connect_notify_swapped (
367 priv->last_known_entry, "notify::cursor-position",
368 G_CALLBACK (category_completion_update_prefix), completion);
370 priv->notify_text_id = e_signal_connect_notify_swapped (
371 priv->last_known_entry, "notify::text",
372 G_CALLBACK (category_completion_update_prefix), completion);
374 g_signal_connect (
375 priv->last_known_entry, "focus-out-event",
376 G_CALLBACK (category_completion_sanitize_suffix), completion);
378 category_completion_update_prefix (completion);
381 static void
382 category_completion_constructed (GObject *object)
384 GtkCellRenderer *renderer;
385 GtkEntryCompletion *completion;
387 /* Chain up to parent's constructed() method. */
388 G_OBJECT_CLASS (e_category_completion_parent_class)->constructed (object);
390 completion = GTK_ENTRY_COMPLETION (object);
392 gtk_entry_completion_set_match_func (
393 completion, (GtkEntryCompletionMatchFunc)
394 category_completion_is_match, NULL, NULL);
396 gtk_entry_completion_set_text_column (completion, COLUMN_CATEGORY);
398 renderer = gtk_cell_renderer_pixbuf_new ();
399 gtk_cell_layout_pack_start (
400 GTK_CELL_LAYOUT (completion), renderer, FALSE);
401 gtk_cell_layout_add_attribute (
402 GTK_CELL_LAYOUT (completion),
403 renderer, "pixbuf", COLUMN_PIXBUF);
404 gtk_cell_layout_reorder (
405 GTK_CELL_LAYOUT (completion), renderer, 0);
407 e_categories_register_change_listener (
408 G_CALLBACK (category_completion_categories_changed_cb),
409 completion);
411 category_completion_build_model (completion);
414 static void
415 category_completion_dispose (GObject *object)
417 ECategoryCompletionPrivate *priv;
419 priv = E_CATEGORY_COMPLETION_GET_PRIVATE (object);
421 if (priv->last_known_entry != NULL) {
422 g_signal_handlers_disconnect_matched (
423 priv->last_known_entry, G_SIGNAL_MATCH_DATA,
424 0, 0, NULL, NULL, object);
425 e_signal_disconnect_notify_handler (priv->last_known_entry, &priv->notify_cursor_position_id);
426 e_signal_disconnect_notify_handler (priv->last_known_entry, &priv->notify_text_id);
427 g_object_unref (priv->last_known_entry);
428 priv->last_known_entry = NULL;
431 /* Chain up to parent's dispose() method. */
432 G_OBJECT_CLASS (e_category_completion_parent_class)->dispose (object);
435 static void
436 category_completion_finalize (GObject *object)
438 ECategoryCompletionPrivate *priv;
440 priv = E_CATEGORY_COMPLETION_GET_PRIVATE (object);
442 g_free (priv->create);
443 g_free (priv->prefix);
445 e_categories_unregister_change_listener (
446 G_CALLBACK (category_completion_categories_changed_cb),
447 object);
449 /* Chain up to parent's finalize() method. */
450 G_OBJECT_CLASS (e_category_completion_parent_class)->finalize (object);
453 static gboolean
454 category_completion_match_selected (GtkEntryCompletion *completion,
455 GtkTreeModel *model,
456 GtkTreeIter *iter)
458 GValue value = { 0, };
460 gtk_tree_model_get_value (model, iter, COLUMN_CATEGORY, &value);
461 category_completion_complete (completion, g_value_get_string (&value));
462 g_value_unset (&value);
464 return TRUE;
467 static void
468 category_completion_action_activated (GtkEntryCompletion *completion,
469 gint index)
471 ECategoryCompletionPrivate *priv;
472 gchar *category;
474 priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion);
476 category = g_strdup (priv->create);
477 e_categories_add (category, NULL, NULL, TRUE);
478 category_completion_complete (completion, category);
479 g_free (category);
482 static void
483 e_category_completion_class_init (ECategoryCompletionClass *class)
485 GObjectClass *object_class;
486 GtkEntryCompletionClass *entry_completion_class;
488 g_type_class_add_private (class, sizeof (ECategoryCompletionPrivate));
490 object_class = G_OBJECT_CLASS (class);
491 object_class->constructed = category_completion_constructed;
492 object_class->dispose = category_completion_dispose;
493 object_class->finalize = category_completion_finalize;
495 entry_completion_class = GTK_ENTRY_COMPLETION_CLASS (class);
496 entry_completion_class->match_selected = category_completion_match_selected;
497 entry_completion_class->action_activated = category_completion_action_activated;
500 static void
501 e_category_completion_init (ECategoryCompletion *category_completion)
503 category_completion->priv =
504 E_CATEGORY_COMPLETION_GET_PRIVATE (category_completion);
508 * e_category_completion_new:
510 * Since: 2.26
512 GtkEntryCompletion *
513 e_category_completion_new (void)
515 return g_object_new (E_TYPE_CATEGORY_COMPLETION, NULL);