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.
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.
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/>
15 * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
18 #include "e-category-completion.h"
25 #include <glib/gi18n-lib.h>
27 #include <libedataserver/libedataserver.h>
29 #define E_CATEGORY_COMPLETION_GET_PRIVATE(obj) \
30 (G_TYPE_INSTANCE_GET_PRIVATE \
31 ((obj), E_TYPE_CATEGORY_COMPLETION, ECategoryCompletionPrivate))
33 struct _ECategoryCompletionPrivate {
34 GtkWidget *last_known_entry;
48 e_category_completion,
49 GTK_TYPE_ENTRY_COMPLETION)
51 /* Forward Declarations */
54 category_completion_track_entry (GtkEntryCompletion *completion);
57 category_completion_build_model (GtkEntryCompletion *completion)
62 store = gtk_list_store_new (
63 NUM_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING);
65 list = e_categories_get_list ();
66 while (list != NULL) {
67 const gchar *category = list->data;
68 const gchar *filename;
71 GdkPixbuf *pixbuf = NULL;
74 /* Only add user-visible categories. */
75 if (!e_categories_is_searchable (category)) {
76 list = g_list_delete_link (list, list);
80 filename = e_categories_get_icon_file_for (category);
81 if (filename != NULL && *filename != '\0')
82 pixbuf = gdk_pixbuf_new_from_file (filename, NULL);
84 normalized = g_utf8_normalize (
85 category, -1, G_NORMALIZE_DEFAULT);
86 casefolded = g_utf8_casefold (normalized, -1);
88 gtk_list_store_append (store, &iter);
91 store, &iter, COLUMN_PIXBUF, pixbuf,
92 COLUMN_CATEGORY, category, COLUMN_NORMALIZED,
99 g_object_unref (pixbuf);
101 list = g_list_delete_link (list, list);
104 gtk_entry_completion_set_model (completion, GTK_TREE_MODEL (store));
108 category_completion_categories_changed_cb (GObject *some_private_object,
109 GtkEntryCompletion *completion)
111 category_completion_build_model (completion);
115 category_completion_complete (GtkEntryCompletion *completion,
116 const gchar *category)
118 GtkEditable *editable;
126 entry = gtk_entry_completion_get_entry (completion);
128 editable = GTK_EDITABLE (entry);
129 text = gtk_entry_get_text (GTK_ENTRY (entry));
131 /* Get the cursor position as a character offset. */
132 offset = gtk_editable_get_position (editable);
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), ',');
138 /* Calculate the selection start position as a character offset. */
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);
147 start_pos = (gint) offset;
149 /* Find the leftmost comma after the cursor. */
150 cp = g_utf8_offset_to_pointer (text, offset);
151 cp = g_utf8_strchr (cp, -1, ',');
153 /* Calculate the selection end position as a character offset. */
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);
162 end_pos = (gint) offset;
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);
172 category_completion_is_match (GtkEntryCompletion *completion,
176 ECategoryCompletionPrivate *priv;
179 GValue value = { 0, };
182 priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion);
183 entry = gtk_entry_completion_get_entry (completion);
184 model = gtk_entry_completion_get_model (completion);
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);
191 if (priv->prefix == NULL)
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);
202 category_completion_update_prefix (GtkEntryCompletion *completion)
204 ECategoryCompletionPrivate *priv;
205 GtkEditable *editable;
217 priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion);
218 entry = gtk_entry_completion_get_entry (completion);
219 model = gtk_entry_completion_get_model (completion);
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);
228 editable = GTK_EDITABLE (entry);
229 text = gtk_entry_get_text (GTK_ENTRY (entry));
231 /* Get the cursor position as a character offset. */
232 offset = gtk_editable_get_position (editable);
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), ',');
238 /* Mark the start of the prefix. */
242 cp = g_utf8_next_char (cp);
243 if (g_unichar_isspace (g_utf8_get_char (cp)))
244 cp = g_utf8_next_char (cp);
248 /* Find the leftmost comma after the cursor. */
249 cp = g_utf8_offset_to_pointer (text, offset);
250 cp = g_utf8_strchr (cp, -1, ',');
252 /* Mark the end of the prefix. */
254 end = text + strlen (text);
258 if (priv->create != NULL)
259 gtk_entry_completion_delete_action (completion, 0);
261 g_free (priv->create);
264 g_free (priv->prefix);
270 input = g_strstrip (g_strndup (start, end - start));
271 priv->create = input;
273 input = g_utf8_normalize (input, -1, G_NORMALIZE_DEFAULT);
274 priv->prefix = g_utf8_casefold (input, -1);
277 if (*priv->create == '\0') {
278 g_free (priv->create);
283 valid = gtk_tree_model_get_iter_first (model, &iter);
285 GValue value = { 0, };
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);
295 g_value_unset (&value);
297 valid = gtk_tree_model_iter_next (model, &iter);
300 input = g_strdup_printf (_("Create category \"%s\""), priv->create);
301 gtk_entry_completion_insert_action_text (completion, 0, input);
306 category_completion_sanitize_suffix (GtkEntry *entry,
307 GdkEventFocus *event,
308 GtkEntryCompletion *completion)
312 g_return_val_if_fail (entry != NULL, FALSE);
313 g_return_val_if_fail (completion != NULL, FALSE);
315 text = gtk_entry_get_text (entry);
317 gint len = strlen (text), old_len = len;
319 while (len > 0 && (text[len -1] == ' ' || text[len - 1] == ','))
322 if (old_len != len) {
323 gchar *tmp = g_strndup (text, len);
325 gtk_entry_set_text (entry, tmp);
335 category_completion_track_entry (GtkEntryCompletion *completion)
337 ECategoryCompletionPrivate *priv;
339 priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion);
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);
348 g_free (priv->prefix);
351 priv->last_known_entry = gtk_entry_completion_get_entry (completion);
352 if (priv->last_known_entry == NULL)
355 g_object_ref (priv->last_known_entry);
357 g_signal_connect_swapped (
358 priv->last_known_entry, "notify::cursor-position",
359 G_CALLBACK (category_completion_update_prefix), completion);
361 g_signal_connect_swapped (
362 priv->last_known_entry, "notify::text",
363 G_CALLBACK (category_completion_update_prefix), completion);
366 priv->last_known_entry, "focus-out-event",
367 G_CALLBACK (category_completion_sanitize_suffix), completion);
369 category_completion_update_prefix (completion);
373 category_completion_constructed (GObject *object)
375 GtkCellRenderer *renderer;
376 GtkEntryCompletion *completion;
378 /* Chain up to parent's constructed() method. */
379 G_OBJECT_CLASS (e_category_completion_parent_class)->constructed (object);
381 completion = GTK_ENTRY_COMPLETION (object);
383 gtk_entry_completion_set_match_func (
384 completion, (GtkEntryCompletionMatchFunc)
385 category_completion_is_match, NULL, NULL);
387 gtk_entry_completion_set_text_column (completion, COLUMN_CATEGORY);
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);
398 e_categories_register_change_listener (
399 G_CALLBACK (category_completion_categories_changed_cb),
402 category_completion_build_model (completion);
406 category_completion_dispose (GObject *object)
408 ECategoryCompletionPrivate *priv;
410 priv = E_CATEGORY_COMPLETION_GET_PRIVATE (object);
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;
420 /* Chain up to parent's dispose() method. */
421 G_OBJECT_CLASS (e_category_completion_parent_class)->dispose (object);
425 category_completion_finalize (GObject *object)
427 ECategoryCompletionPrivate *priv;
429 priv = E_CATEGORY_COMPLETION_GET_PRIVATE (object);
431 g_free (priv->create);
432 g_free (priv->prefix);
434 e_categories_unregister_change_listener (
435 G_CALLBACK (category_completion_categories_changed_cb),
438 /* Chain up to parent's finalize() method. */
439 G_OBJECT_CLASS (e_category_completion_parent_class)->finalize (object);
443 category_completion_match_selected (GtkEntryCompletion *completion,
447 GValue value = { 0, };
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);
457 category_completion_action_activated (GtkEntryCompletion *completion,
460 ECategoryCompletionPrivate *priv;
463 priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion);
465 category = g_strdup (priv->create);
466 e_categories_add (category, NULL, NULL, TRUE);
467 category_completion_complete (completion, category);
472 e_category_completion_class_init (ECategoryCompletionClass *class)
474 GObjectClass *object_class;
475 GtkEntryCompletionClass *entry_completion_class;
477 g_type_class_add_private (class, sizeof (ECategoryCompletionPrivate));
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;
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;
490 e_category_completion_init (ECategoryCompletion *category_completion)
492 category_completion->priv =
493 E_CATEGORY_COMPLETION_GET_PRIVATE (category_completion);
497 * e_category_completion_new:
502 e_category_completion_new (void)
504 return g_object_new (E_TYPE_CATEGORY_COMPLETION, NULL);