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
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"
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
;
39 gulong notify_cursor_position_id
;
40 gulong notify_text_id
;
52 e_category_completion
,
53 GTK_TYPE_ENTRY_COMPLETION
)
55 /* Forward Declarations */
58 category_completion_track_entry (GtkEntryCompletion
*completion
);
61 category_completion_build_model (GtkEntryCompletion
*completion
)
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
;
75 GdkPixbuf
*pixbuf
= NULL
;
78 /* Only add user-visible categories. */
79 if (!e_categories_is_searchable (category
)) {
81 list
= g_list_delete_link (list
, list
);
85 filename
= e_categories_dup_icon_file_for (category
);
86 if (filename
!= NULL
&& *filename
!= '\0')
87 pixbuf
= gdk_pixbuf_new_from_file (filename
, NULL
);
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
);
97 store
, &iter
, COLUMN_PIXBUF
, pixbuf
,
98 COLUMN_CATEGORY
, category
, COLUMN_NORMALIZED
,
105 g_object_unref (pixbuf
);
108 list
= g_list_delete_link (list
, list
);
111 gtk_entry_completion_set_model (completion
, GTK_TREE_MODEL (store
));
115 category_completion_categories_changed_cb (GObject
*some_private_object
,
116 GtkEntryCompletion
*completion
)
118 category_completion_build_model (completion
);
122 category_completion_complete (GtkEntryCompletion
*completion
,
123 const gchar
*category
)
125 GtkEditable
*editable
;
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. */
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. */
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
);
179 category_completion_is_match (GtkEntryCompletion
*completion
,
183 ECategoryCompletionPrivate
*priv
;
186 GValue value
= { 0, };
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
)
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
);
209 category_completion_update_prefix (GtkEntryCompletion
*completion
)
211 ECategoryCompletionPrivate
*priv
;
212 GtkEditable
*editable
;
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
);
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. */
249 cp
= g_utf8_next_char (cp
);
250 if (g_unichar_isspace (g_utf8_get_char (cp
)))
251 cp
= g_utf8_next_char (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. */
261 end
= text
+ strlen (text
);
265 if (priv
->create
!= NULL
)
266 gtk_entry_completion_delete_action (completion
, 0);
268 g_free (priv
->create
);
271 g_free (priv
->prefix
);
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);
284 if (*priv
->create
== '\0') {
285 g_free (priv
->create
);
290 valid
= gtk_tree_model_get_iter_first (model
, &iter
);
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
);
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
);
313 category_completion_sanitize_suffix (GtkEntry
*entry
,
314 GdkEventFocus
*event
,
315 GtkEntryCompletion
*completion
)
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
);
324 gint len
= strlen (text
), old_len
= len
;
326 while (len
> 0 && (text
[len
-1] == ' ' || text
[len
- 1] == ','))
329 if (old_len
!= len
) {
330 gchar
*tmp
= g_strndup (text
, len
);
332 gtk_entry_set_text (entry
, tmp
);
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
);
360 priv
->last_known_entry
= gtk_entry_completion_get_entry (completion
);
361 if (priv
->last_known_entry
== NULL
)
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
);
375 priv
->last_known_entry
, "focus-out-event",
376 G_CALLBACK (category_completion_sanitize_suffix
), completion
);
378 category_completion_update_prefix (completion
);
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
),
411 category_completion_build_model (completion
);
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
);
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
),
449 /* Chain up to parent's finalize() method. */
450 G_OBJECT_CLASS (e_category_completion_parent_class
)->finalize (object
);
454 category_completion_match_selected (GtkEntryCompletion
*completion
,
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
);
468 category_completion_action_activated (GtkEntryCompletion
*completion
,
471 ECategoryCompletionPrivate
*priv
;
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
);
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
;
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:
513 e_category_completion_new (void)
515 return g_object_new (E_TYPE_CATEGORY_COMPLETION
, NULL
);