2 * Copyright © 2002-2004 Marco Pesenti Gritti <mpeseng@tin.it>
3 * Copyright © 2005, 2006 Peter Harvey <pah06@uow.edu.au>
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22 #include "ephy-topics-entry.h"
23 #include "ephy-nodes-cover.h"
24 #include "ephy-node-common.h"
25 #include "ephy-bookmarks.h"
26 #include "ephy-debug.h"
28 #include <glib/gi18n.h>
29 #include <gtk/gtktreeselection.h>
30 #include <gtk/gtkentry.h>
31 #include <gtk/gtkentrycompletion.h>
34 static void ephy_topics_entry_class_init (EphyTopicsEntryClass
*klass
);
35 static void ephy_topics_entry_init (EphyTopicsEntry
*editor
);
37 #define EPHY_TOPICS_ENTRY_GET_PRIVATE(object)(G_TYPE_INSTANCE_GET_PRIVATE ((object), EPHY_TYPE_TOPICS_ENTRY, EphyTopicsEntryPrivate))
39 struct _EphyTopicsEntryPrivate
41 EphyBookmarks
*bookmarks
;
44 GtkEntryCompletion
*completion
;
65 static GObjectClass
*parent_class
= NULL
;
68 ephy_topics_entry_get_type (void)
70 static GType type
= 0;
72 if (G_UNLIKELY (type
== 0))
74 const GTypeInfo our_info
=
76 sizeof (EphyTopicsEntryClass
),
79 (GClassInitFunc
) ephy_topics_entry_class_init
,
82 sizeof (EphyTopicsEntry
),
84 (GInstanceInitFunc
) ephy_topics_entry_init
87 type
= g_type_register_static (GTK_TYPE_ENTRY
,
96 find_topic (EphyTopicsEntry
*entry
,
99 EphyNode
*node
= NULL
;
102 GValue value
= { 0, };
105 /* Loop through our table and set/unset topics appropriately */
106 model
= GTK_TREE_MODEL (entry
->priv
->store
);
107 valid
= gtk_tree_model_get_iter_first (model
, &iter
);
108 while (valid
&& node
== NULL
)
110 gtk_tree_model_get_value (model
, &iter
, COLUMN_KEY
, &value
);
111 if (strcmp(g_value_get_string (&value
), key
) == 0)
113 g_value_unset (&value
);
114 gtk_tree_model_get_value (model
, &iter
, COLUMN_NODE
, &value
);
115 node
= g_value_get_pointer (&value
);
117 g_value_unset (&value
);
118 valid
= gtk_tree_model_iter_next (model
, &iter
);
125 insert_text (EphyTopicsEntry
*entry
,
128 GtkEditable
*editable
= GTK_EDITABLE (entry
);
130 const gchar
*text
= gtk_entry_get_text (GTK_ENTRY (entry
));
131 const gchar
*midpoint
= g_utf8_offset_to_pointer (text
, gtk_editable_get_position (editable
));
132 const gchar
*start
= g_utf8_strrchr (text
, (gssize
)(midpoint
-text
), ',');
133 const gchar
*end
= g_utf8_strchr (midpoint
, -1, ',');
134 int startpos
, endpos
;
138 else if (g_unichar_isspace (g_utf8_get_char (g_utf8_next_char (start
))))
139 startpos
= g_utf8_pointer_to_offset (text
, start
)+2;
141 startpos
= g_utf8_pointer_to_offset (text
, start
)+1;
145 else if (g_unichar_isspace (g_utf8_get_char (g_utf8_next_char (end
))))
146 endpos
= g_utf8_pointer_to_offset (text
, end
)+2;
148 endpos
= g_utf8_pointer_to_offset (text
, end
)+1;
150 /* Replace the text in the current position with the title */
151 gtk_editable_delete_text (editable
, startpos
, endpos
);
152 gtk_editable_insert_text (editable
, title
, strlen(title
), &startpos
);
153 gtk_editable_insert_text (editable
, ", ", 2, &startpos
);
154 gtk_editable_set_position (editable
, startpos
);
157 /* Updates the text entry and the completion model to match the database */
159 update_widget (EphyTopicsEntry
*entry
)
161 EphyTopicsEntryPrivate
*priv
= entry
->priv
;
162 GtkEditable
*editable
= GTK_EDITABLE (entry
);
165 GPtrArray
*children
, *topics
;
167 gint i
, priority
, pos
;
172 /* Prevent any changes to the database */
173 if(priv
->lock
) return;
176 node
= ephy_bookmarks_get_keywords (priv
->bookmarks
);
177 children
= ephy_node_get_children (node
);
178 topics
= g_ptr_array_sized_new (children
->len
);
180 for (i
= 0; i
< children
->len
; i
++)
182 node
= g_ptr_array_index (children
, i
);
184 priority
= ephy_node_get_property_int
185 (node
, EPHY_NODE_KEYWORD_PROP_PRIORITY
);
186 if (priority
!= EPHY_NODE_NORMAL_PRIORITY
)
189 g_ptr_array_add (topics
, node
);
192 g_ptr_array_sort (topics
, ephy_bookmarks_compare_topic_pointers
);
193 gtk_list_store_clear (priv
->store
);
195 g_object_get (entry
, "is-focus", &is_focus
, NULL
);
198 gtk_editable_delete_text (editable
, 0, -1);
201 for (pos
= -1, i
= 0; i
< topics
->len
; i
++)
203 node
= g_ptr_array_index (topics
, i
);
204 title
= ephy_node_get_property_string (node
, EPHY_NODE_KEYWORD_PROP_NAME
);
206 if (!is_focus
&& ephy_node_has_child (node
, priv
->bookmark
))
208 gtk_editable_insert_text (editable
, title
, -1, &pos
);
209 gtk_editable_insert_text (editable
, ", ", -1, &pos
);
212 tmp1
= g_utf8_casefold (title
, -1);
213 tmp2
= g_utf8_normalize (tmp1
, -1, G_NORMALIZE_DEFAULT
);
214 gtk_list_store_append (priv
->store
, &iter
);
215 gtk_list_store_set (priv
->store
, &iter
, COLUMN_NODE
, node
,
216 COLUMN_TITLE
, title
, COLUMN_KEY
, tmp2
, -1);
224 gtk_editable_set_position (editable
, -1);
227 g_ptr_array_free (topics
, TRUE
);
232 /* Updates the bookmarks database to match what is in the text entry */
234 update_database (EphyTopicsEntry
*entry
)
236 EphyTopicsEntryPrivate
*priv
= entry
->priv
;
246 GValue value
= { 0, };
249 /* Prevent any changes to the text entry or completion model */
250 if(priv
->lock
) return;
253 /* Get the list of strings input by the user */
254 text
= gtk_entry_get_text (GTK_ENTRY (entry
));
255 split
= g_strsplit (text
, ",", 0);
256 for (i
=0; split
[i
]; i
++)
258 g_strstrip (split
[i
]);
260 tmp
= g_utf8_casefold (split
[i
], -1);
263 split
[i
] = g_utf8_normalize (tmp
, -1, G_NORMALIZE_DEFAULT
);
267 /* Loop through the completion model and set/unset topics appropriately */
268 model
= GTK_TREE_MODEL (priv
->store
);
269 valid
= gtk_tree_model_get_iter_first (model
, &iter
);
272 gtk_tree_model_get_value (model
, &iter
, COLUMN_NODE
, &value
);
273 node
= g_value_get_pointer (&value
);
274 g_value_unset (&value
);
276 gtk_tree_model_get_value (model
, &iter
, COLUMN_KEY
, &value
);
277 text
= g_value_get_string (&value
);
279 for (i
=0; split
[i
]; i
++)
280 if (strcmp (text
, split
[i
]) == 0)
286 ephy_bookmarks_set_keyword (priv
->bookmarks
, node
,
291 ephy_bookmarks_unset_keyword (priv
->bookmarks
, node
,
295 g_value_unset (&value
);
296 valid
= gtk_tree_model_iter_next (model
, &iter
);
304 /* Updates the search key and topic creation action */
306 update_key (EphyTopicsEntry
*entry
)
308 EphyTopicsEntryPrivate
*priv
= entry
->priv
;
309 GtkEditable
*editable
= GTK_EDITABLE (entry
);
312 const gchar
*text
= gtk_entry_get_text (GTK_ENTRY (entry
));
313 const gchar
*midpoint
= g_utf8_offset_to_pointer (text
, gtk_editable_get_position (editable
));
314 const gchar
*start
= g_utf8_strrchr (text
, (gssize
)(midpoint
-text
), ',');
315 const gchar
*end
= g_utf8_strchr (midpoint
, -1, ',');
319 else if (g_unichar_isspace (g_utf8_get_char (g_utf8_next_char (start
))))
320 start
= g_utf8_next_char (g_utf8_next_char (start
));
322 start
= g_utf8_next_char (start
);
325 end
= text
+strlen(text
);
327 /* If there was something we could create, then delete the action. */
330 gtk_entry_completion_delete_action (priv
->completion
, 0);
333 g_free (priv
->create
);
338 /* Set the priv->create and priv->key appropriately. */
341 input
= g_strndup (start
, end
-start
);
343 priv
->create
= input
;
345 input
= g_utf8_casefold (input
, -1);
346 priv
->key
= g_utf8_normalize (input
, -1, G_NORMALIZE_DEFAULT
);
349 if (priv
->create
[0] == '\0' ||
350 find_topic (entry
, priv
->key
) != NULL
)
352 g_free (priv
->create
);
356 /* If there is something we can create, then setup the action. */
359 input
= g_strdup_printf (_("Create topic “%s”"), priv
->create
);
360 gtk_entry_completion_insert_action_text (priv
->completion
, 0, input
);
367 match_func (GtkEntryCompletion
*completion
,
372 EphyTopicsEntry
*entry
= EPHY_TOPICS_ENTRY (gtk_entry_completion_get_entry (completion
));
373 EphyTopicsEntryPrivate
*priv
= entry
->priv
;
374 GtkTreeModel
*model
= gtk_entry_completion_get_model (completion
);
377 GValue value
= { 0, };
380 if (priv
->key
== NULL
)
385 /* If no node at all (this happens for unknown reasons) then don't show. */
386 gtk_tree_model_get_value (model
, iter
, COLUMN_NODE
, &value
);
387 node
= g_value_get_pointer (&value
);
388 g_value_unset (&value
);
394 /* If it's already selected, don't show it unless we're editing it. */
395 else if (ephy_node_has_child (node
, priv
->bookmark
))
397 gtk_tree_model_get_value (model
, iter
, COLUMN_KEY
, &value
);
398 result
= (strcmp (g_value_get_string (&value
), priv
->key
) == 0);
399 g_value_unset (&value
);
402 /* If it's not selected, show it if it matches. */
405 gtk_tree_model_get_value (model
, iter
, COLUMN_KEY
, &value
);
406 result
= (g_str_has_prefix (g_value_get_string (&value
), priv
->key
));
407 g_value_unset (&value
);
414 action_cb (GtkEntryCompletion
*completion
,
418 EphyTopicsEntry
*entry
= EPHY_TOPICS_ENTRY (gtk_entry_completion_get_entry (completion
));
419 EphyTopicsEntryPrivate
*priv
= entry
->priv
;
422 title
= g_strdup (priv
->create
);
424 ephy_bookmarks_add_keyword (priv
->bookmarks
, title
);
425 update_widget (entry
);
427 insert_text (entry
, title
);
432 match_selected_cb (GtkEntryCompletion
*completion
,
437 EphyTopicsEntry
*entry
= EPHY_TOPICS_ENTRY (gtk_entry_completion_get_entry (completion
));
438 GValue value
= { 0, };
440 gtk_tree_model_get_value (model
, iter
, COLUMN_TITLE
, &value
);
441 insert_text (entry
, g_value_get_string (&value
));
442 g_value_unset (&value
);
448 activate_cb (GtkEditable
*editable
,
451 EphyTopicsEntry
*entry
= EPHY_TOPICS_ENTRY (editable
);
452 EphyTopicsEntryPrivate
*priv
= entry
->priv
;
453 GtkEntryCompletion
*completion
= gtk_entry_get_completion (GTK_ENTRY (entry
));
455 GValue value
= { 0, };
460 if (priv
->key
== NULL
|| priv
->key
[0] == '\0')
462 gtk_entry_set_activates_default (GTK_ENTRY (entry
), TRUE
);
467 gtk_entry_set_activates_default (GTK_ENTRY (entry
), FALSE
);
470 /* Loop through the completion model and find the first item to use, if any. */
471 model
= GTK_TREE_MODEL (priv
->store
);
472 valid
= gtk_tree_model_get_iter_first (model
, &iter
);
473 while (valid
&& !match_func (completion
, NULL
, &iter
, NULL
))
475 valid
= gtk_tree_model_iter_next (model
, &iter
);
480 gtk_tree_model_get_value (model
, &iter
, COLUMN_TITLE
, &value
);
482 /* See if there were any others. */
483 valid
= gtk_tree_model_iter_next (model
, &iter
);
484 while (valid
&& !match_func (completion
, NULL
, &iter
, NULL
))
486 valid
= gtk_tree_model_iter_next (model
, &iter
);
491 insert_text (EPHY_TOPICS_ENTRY (editable
), g_value_get_string (&value
));
492 g_value_unset (&value
);
498 tree_changed_cb (EphyBookmarks
*bookmarks
,
499 EphyTopicsEntry
*entry
)
501 update_widget (entry
);
505 node_added_cb (EphyNode
*parent
,
509 update_widget (EPHY_TOPICS_ENTRY (object
));
513 node_changed_cb (EphyNode
*parent
,
518 update_widget (EPHY_TOPICS_ENTRY (object
));
522 node_removed_cb (EphyNode
*parent
,
527 update_widget (EPHY_TOPICS_ENTRY (object
));
531 ephy_topics_entry_set_property (GObject
*object
,
536 EphyTopicsEntry
*entry
= EPHY_TOPICS_ENTRY (object
);
542 entry
->priv
->bookmarks
= g_value_get_object (value
);
543 node
= ephy_bookmarks_get_keywords (entry
->priv
->bookmarks
);
544 ephy_node_signal_connect_object (node
, EPHY_NODE_CHILD_ADDED
,
545 (EphyNodeCallback
) node_added_cb
, object
);
546 ephy_node_signal_connect_object (node
, EPHY_NODE_CHILD_CHANGED
,
547 (EphyNodeCallback
) node_changed_cb
, object
);
548 ephy_node_signal_connect_object (node
, EPHY_NODE_CHILD_REMOVED
,
549 (EphyNodeCallback
) node_removed_cb
, object
);
550 g_signal_connect_object (entry
->priv
->bookmarks
, "tree-changed",
551 G_CALLBACK (tree_changed_cb
), entry
,
555 entry
->priv
->bookmark
= g_value_get_pointer (value
);
558 G_OBJECT_WARN_INVALID_PROPERTY_ID (object
, prop_id
, pspec
);
564 ephy_topics_entry_constructor (GType type
,
565 guint n_construct_properties
,
566 GObjectConstructParam
*construct_params
)
569 EphyTopicsEntry
*entry
;
570 EphyTopicsEntryPrivate
*priv
;
572 object
= parent_class
->constructor (type
, n_construct_properties
,
574 entry
= EPHY_TOPICS_ENTRY (object
);
575 priv
= EPHY_TOPICS_ENTRY_GET_PRIVATE (object
);
577 priv
->store
= gtk_list_store_new (3, G_TYPE_POINTER
, G_TYPE_STRING
, G_TYPE_STRING
);
578 priv
->completion
= gtk_entry_completion_new ();
580 gtk_entry_completion_set_model (priv
->completion
, GTK_TREE_MODEL (priv
->store
));
581 gtk_entry_completion_set_text_column (priv
->completion
, COLUMN_TITLE
);
582 gtk_entry_completion_set_popup_completion (priv
->completion
, TRUE
);
583 gtk_entry_completion_set_popup_single_match (priv
->completion
, TRUE
);
584 gtk_entry_completion_set_match_func (priv
->completion
, match_func
, NULL
, NULL
);
585 gtk_entry_set_completion (GTK_ENTRY (entry
), priv
->completion
);
587 g_signal_connect (priv
->completion
, "match-selected",
588 G_CALLBACK (match_selected_cb
), NULL
);
589 g_signal_connect (priv
->completion
, "action-activated",
590 G_CALLBACK (action_cb
), NULL
);
592 g_signal_connect (object
, "activate",
593 G_CALLBACK (activate_cb
), NULL
);
595 g_signal_connect (object
, "changed",
596 G_CALLBACK (update_database
), NULL
);
597 g_signal_connect (object
, "notify::is-focus",
598 G_CALLBACK (update_widget
), NULL
);
599 g_signal_connect (object
, "notify::cursor-position",
600 G_CALLBACK (update_key
), NULL
);
601 g_signal_connect (object
, "notify::text",
602 G_CALLBACK (update_key
), NULL
);
605 update_widget (entry
);
611 ephy_topics_entry_init (EphyTopicsEntry
*entry
)
613 entry
->priv
= EPHY_TOPICS_ENTRY_GET_PRIVATE (entry
);
618 ephy_topics_entry_finalize (GObject
*object
)
620 EphyTopicsEntry
*entry
= EPHY_TOPICS_ENTRY (object
);
622 g_free (entry
->priv
->create
);
623 g_free (entry
->priv
->key
);
625 parent_class
->finalize (object
);
629 ephy_topics_entry_new (EphyBookmarks
*bookmarks
,
632 EphyTopicsEntry
*entry
;
634 g_assert (bookmarks
!= NULL
);
636 entry
= EPHY_TOPICS_ENTRY (g_object_new
637 (EPHY_TYPE_TOPICS_ENTRY
,
638 "bookmarks", bookmarks
,
639 "bookmark", bookmark
,
642 return GTK_WIDGET (entry
);
646 ephy_topics_entry_class_init (EphyTopicsEntryClass
*klass
)
648 GObjectClass
*object_class
= G_OBJECT_CLASS (klass
);
650 parent_class
= g_type_class_peek_parent (klass
);
652 object_class
->set_property
= ephy_topics_entry_set_property
;
653 object_class
->constructor
= ephy_topics_entry_constructor
;
654 object_class
->finalize
= ephy_topics_entry_finalize
;
656 g_object_class_install_property (object_class
,
658 g_param_spec_object ("bookmarks",
662 G_PARAM_WRITABLE
| G_PARAM_CONSTRUCT_ONLY
|
663 G_PARAM_STATIC_NAME
| G_PARAM_STATIC_NICK
|
664 G_PARAM_STATIC_BLURB
));
666 g_object_class_install_property (object_class
,
668 g_param_spec_pointer ("bookmark",
671 G_PARAM_WRITABLE
| G_PARAM_CONSTRUCT_ONLY
|
672 G_PARAM_STATIC_NAME
| G_PARAM_STATIC_NICK
|
673 G_PARAM_STATIC_BLURB
));
675 g_type_class_add_private (object_class
, sizeof(EphyTopicsEntryPrivate
));