Initial import of ephy (rev# 7126) from svn
[ephy-soc.git] / src / bookmarks / ephy-topics-entry.c
blob42b3bc436a70b237002878c83ff3589c07c2c9aa
1 /*
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.
20 #include "config.h"
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>
32 #include <string.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;
42 EphyNode *bookmark;
43 GtkListStore *store;
44 GtkEntryCompletion *completion;
45 gboolean lock;
46 char *create;
47 char *key;
50 enum
52 PROP_0,
53 PROP_BOOKMARKS,
54 PROP_BOOKMARK
57 enum
59 COLUMN_NODE,
60 COLUMN_KEY,
61 COLUMN_TITLE,
62 COLUMNS
65 static GObjectClass *parent_class = NULL;
67 GType
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),
77 NULL,
78 NULL,
79 (GClassInitFunc) ephy_topics_entry_class_init,
80 NULL,
81 NULL,
82 sizeof (EphyTopicsEntry),
84 (GInstanceInitFunc) ephy_topics_entry_init
87 type = g_type_register_static (GTK_TYPE_ENTRY,
88 "EphyTopicsEntry",
89 &our_info, 0);
92 return type;
95 static EphyNode *
96 find_topic (EphyTopicsEntry *entry,
97 const char *key)
99 EphyNode *node = NULL;
100 GtkTreeModel *model;
101 GtkTreeIter iter;
102 GValue value = { 0, };
103 gboolean valid;
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);
121 return node;
124 static void
125 insert_text (EphyTopicsEntry *entry,
126 const char *title)
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;
136 if (start == NULL)
137 startpos = 0;
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;
140 else
141 startpos = g_utf8_pointer_to_offset (text, start)+1;
143 if (end == NULL)
144 endpos = -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;
147 else
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 */
158 static void
159 update_widget (EphyTopicsEntry *entry)
161 EphyTopicsEntryPrivate *priv = entry->priv;
162 GtkEditable *editable = GTK_EDITABLE (entry);
164 EphyNode *node;
165 GPtrArray *children, *topics;
166 GtkTreeIter iter;
167 gint i, priority, pos;
168 const char *title;
169 char *tmp1, *tmp2;
170 gboolean is_focus;
172 /* Prevent any changes to the database */
173 if(priv->lock) return;
174 priv->lock = TRUE;
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)
187 continue;
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);
196 if (!is_focus)
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);
217 g_free (tmp2);
218 g_free (tmp1);
222 if (!is_focus)
224 gtk_editable_set_position (editable, -1);
227 g_ptr_array_free (topics, TRUE);
229 priv->lock = FALSE;
232 /* Updates the bookmarks database to match what is in the text entry */
233 static void
234 update_database (EphyTopicsEntry *entry)
236 EphyTopicsEntryPrivate *priv = entry->priv;
238 EphyNode *node;
239 const char *text;
240 char **split;
241 char *tmp;
242 gint i;
244 GtkTreeModel *model;
245 GtkTreeIter iter;
246 GValue value = { 0, };
247 gboolean valid;
249 /* Prevent any changes to the text entry or completion model */
250 if(priv->lock) return;
251 priv->lock = TRUE;
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);
261 g_free (split[i]);
263 split[i] = g_utf8_normalize (tmp, -1, G_NORMALIZE_DEFAULT);
264 g_free (tmp);
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);
270 while (valid)
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)
281 break;
283 if (split[i])
285 split[i][0] = 0;
286 ephy_bookmarks_set_keyword (priv->bookmarks, node,
287 priv->bookmark);
289 else
291 ephy_bookmarks_unset_keyword (priv->bookmarks, node,
292 priv->bookmark);
295 g_value_unset (&value);
296 valid = gtk_tree_model_iter_next (model, &iter);
299 g_strfreev (split);
301 priv->lock = FALSE;
304 /* Updates the search key and topic creation action */
305 static void
306 update_key (EphyTopicsEntry *entry)
308 EphyTopicsEntryPrivate *priv = entry->priv;
309 GtkEditable *editable = GTK_EDITABLE (entry);
310 char *input;
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, ',');
317 if (start == NULL)
318 start = text;
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));
321 else
322 start = g_utf8_next_char (start);
324 if (end == NULL)
325 end = text+strlen(text);
327 /* If there was something we could create, then delete the action. */
328 if (priv->create)
330 gtk_entry_completion_delete_action (priv->completion, 0);
333 g_free (priv->create);
334 g_free (priv->key);
335 priv->create = 0;
336 priv->key = 0;
338 /* Set the priv->create and priv->key appropriately. */
339 if (start != end)
341 input = g_strndup (start, end-start);
342 g_strstrip (input);
343 priv->create = input;
345 input = g_utf8_casefold (input, -1);
346 priv->key = g_utf8_normalize (input, -1, G_NORMALIZE_DEFAULT);
347 g_free (input);
349 if (priv->create[0] == '\0' ||
350 find_topic (entry, priv->key) != NULL)
352 g_free (priv->create);
353 priv->create = 0;
356 /* If there is something we can create, then setup the action. */
357 else
359 input = g_strdup_printf (_("Create topic “%s”"), priv->create);
360 gtk_entry_completion_insert_action_text (priv->completion, 0, input);
361 g_free (input);
366 static gboolean
367 match_func (GtkEntryCompletion *completion,
368 const gchar *key,
369 GtkTreeIter *iter,
370 gpointer user_data)
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);
376 gboolean result;
377 GValue value = { 0, };
378 EphyNode *node;
380 if (priv->key == NULL)
382 return FALSE;
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);
389 if (node == NULL)
391 result = FALSE;
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. */
403 else
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);
410 return result;
413 static void
414 action_cb (GtkEntryCompletion *completion,
415 gint index,
416 gpointer user_data)
418 EphyTopicsEntry *entry = EPHY_TOPICS_ENTRY (gtk_entry_completion_get_entry (completion));
419 EphyTopicsEntryPrivate *priv = entry->priv;
420 char *title;
422 title = g_strdup (priv->create);
424 ephy_bookmarks_add_keyword (priv->bookmarks, title);
425 update_widget (entry);
427 insert_text (entry, title);
428 g_free (title);
431 static gboolean
432 match_selected_cb (GtkEntryCompletion *completion,
433 GtkTreeModel *model,
434 GtkTreeIter *iter,
435 gpointer user_data)
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);
444 return TRUE;
447 static void
448 activate_cb (GtkEditable *editable,
449 gpointer user_data)
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, };
456 GtkTreeModel *model;
457 GtkTreeIter iter;
458 gboolean valid;
460 if (priv->key == NULL || priv->key[0] == '\0')
462 gtk_entry_set_activates_default (GTK_ENTRY (entry), TRUE);
463 return;
465 else
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);
478 if (valid)
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);
489 if (!valid)
491 insert_text (EPHY_TOPICS_ENTRY (editable), g_value_get_string (&value));
492 g_value_unset (&value);
497 static void
498 tree_changed_cb (EphyBookmarks *bookmarks,
499 EphyTopicsEntry *entry)
501 update_widget (entry);
504 static void
505 node_added_cb (EphyNode *parent,
506 EphyNode *child,
507 GObject *object)
509 update_widget (EPHY_TOPICS_ENTRY (object));
512 static void
513 node_changed_cb (EphyNode *parent,
514 EphyNode *child,
515 guint property_id,
516 GObject *object)
518 update_widget (EPHY_TOPICS_ENTRY (object));
521 static void
522 node_removed_cb (EphyNode *parent,
523 EphyNode *child,
524 guint index,
525 GObject *object)
527 update_widget (EPHY_TOPICS_ENTRY (object));
530 static void
531 ephy_topics_entry_set_property (GObject *object,
532 guint prop_id,
533 const GValue *value,
534 GParamSpec *pspec)
536 EphyTopicsEntry *entry = EPHY_TOPICS_ENTRY (object);
537 EphyNode *node;
539 switch (prop_id)
541 case PROP_BOOKMARKS:
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,
552 G_CONNECT_AFTER);
553 break;
554 case PROP_BOOKMARK:
555 entry->priv->bookmark = g_value_get_pointer (value);
556 break;
557 default:
558 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
559 break;
563 static GObject *
564 ephy_topics_entry_constructor (GType type,
565 guint n_construct_properties,
566 GObjectConstructParam *construct_params)
568 GObject *object;
569 EphyTopicsEntry *entry;
570 EphyTopicsEntryPrivate *priv;
572 object = parent_class->constructor (type, n_construct_properties,
573 construct_params);
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);
604 update_key (entry);
605 update_widget (entry);
607 return object;
610 static void
611 ephy_topics_entry_init (EphyTopicsEntry *entry)
613 entry->priv = EPHY_TOPICS_ENTRY_GET_PRIVATE (entry);
617 static void
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);
628 GtkWidget *
629 ephy_topics_entry_new (EphyBookmarks *bookmarks,
630 EphyNode *bookmark)
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,
640 NULL));
642 return GTK_WIDGET (entry);
645 static void
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,
657 PROP_BOOKMARKS,
658 g_param_spec_object ("bookmarks",
659 "Bookmarks set",
660 "Bookmarks set",
661 EPHY_TYPE_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,
667 PROP_BOOKMARK,
668 g_param_spec_pointer ("bookmark",
669 "Bookmark",
670 "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));