/*
* Copyright (C) 2002-2004 Marco Pesenti Gritti <mpeseng@tin.it>
* Copyright (C) 2005, 2006 Peter Harvey <pah06@uow.edu.au>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
#include "config.h"
#include "ephy-topics-entry.h"
#include "ephy-nodes-cover.h"
#include "ephy-node-common.h"
#include "ephy-bookmarks.h"
#include "ephy-debug.h"
#include <glib/gi18n.h>
#include <gtk/gtktreeselection.h>
#include <gtk/gtkentry.h>
#include <gtk/gtkentrycompletion.h>
#include <string.h>
static void ephy_topics_entry_class_init (EphyTopicsEntryClass *klass);
static void ephy_topics_entry_init (EphyTopicsEntry *editor);
#define EPHY_TOPICS_ENTRY_GET_PRIVATE(object)(G_TYPE_INSTANCE_GET_PRIVATE ((object), EPHY_TYPE_TOPICS_ENTRY, EphyTopicsEntryPrivate))
struct _EphyTopicsEntryPrivate
{
EphyBookmarks *bookmarks;
EphyNode *bookmark;
GtkListStore *store;
GtkEntryCompletion *completion;
gboolean lock;
char *create;
char *key;
};
enum
{
PROP_0,
PROP_BOOKMARKS,
PROP_BOOKMARK
};
enum
{
COLUMN_NODE,
COLUMN_KEY,
COLUMN_TITLE,
COLUMNS
};
static GObjectClass *parent_class = NULL;
GType
ephy_topics_entry_get_type (void)
{
static GType type = 0;
if (G_UNLIKELY (type == 0))
{
static const GTypeInfo our_info =
{
sizeof (EphyTopicsEntryClass),
NULL,
NULL,
(GClassInitFunc) ephy_topics_entry_class_init,
NULL,
NULL,
sizeof (EphyTopicsEntry),
0,
(GInstanceInitFunc) ephy_topics_entry_init
};
type = g_type_register_static (GTK_TYPE_ENTRY,
"EphyTopicsEntry",
&our_info, 0);
}
return type;
}
static EphyNode *
find_topic (EphyTopicsEntry *entry,
const char *key)
{
EphyNode *node = NULL;
GtkTreeModel *model;
GtkTreeIter iter;
GValue value = { 0, };
gboolean valid;
/* Loop through our table and set/unset topics appropriately */
model = GTK_TREE_MODEL (entry->priv->store);
valid = gtk_tree_model_get_iter_first (model, &iter);
while (valid && node == NULL)
{
gtk_tree_model_get_value (model, &iter, COLUMN_KEY, &value);
if (strcmp(g_value_get_string (&value), key) == 0)
{
g_value_unset (&value);
gtk_tree_model_get_value (model, &iter, COLUMN_NODE, &value);
node = g_value_get_pointer (&value);
}
g_value_unset (&value);
valid = gtk_tree_model_iter_next (model, &iter);
}
return node;
}
static void
insert_text (EphyTopicsEntry *entry,
const char *title)
{
GtkEditable *editable = GTK_EDITABLE (entry);
const gchar *text = gtk_entry_get_text (GTK_ENTRY (entry));
const gchar *midpoint = g_utf8_offset_to_pointer (text, gtk_editable_get_position (editable));
const gchar *start = g_utf8_strrchr (text, (gssize)(midpoint-text), ',');
const gchar *end = g_utf8_strchr (midpoint, -1, ',');
int startpos, endpos;
if (start == NULL)
startpos = 0;
else if (g_unichar_isspace (g_utf8_get_char (g_utf8_next_char (start))))
startpos = g_utf8_pointer_to_offset (text, start)+2;
else
startpos = g_utf8_pointer_to_offset (text, start)+1;
if (end == NULL)
endpos = -1;
else if (g_unichar_isspace (g_utf8_get_char (g_utf8_next_char (end))))
endpos = g_utf8_pointer_to_offset (text, end)+2;
else
endpos = g_utf8_pointer_to_offset (text, end)+1;
/* Replace the text in the current position with the title */
gtk_editable_delete_text (editable, startpos, endpos);
gtk_editable_insert_text (editable, title, strlen(title), &startpos);
gtk_editable_insert_text (editable, ", ", 2, &startpos);
gtk_editable_set_position (editable, startpos);
}
/* Updates the text entry and the completion model to match the database */
static void
update_widget (EphyTopicsEntry *entry)
{
EphyTopicsEntryPrivate *priv = entry->priv;
GtkEditable *editable = GTK_EDITABLE (entry);
EphyNode *node;
GPtrArray *children, *topics;
GtkTreeIter iter;
gint i, priority, pos;
const char *title;
char *tmp1, *tmp2;
gboolean is_focus;
/* Prevent any changes to the database */
if(priv->lock) return;
priv->lock = TRUE;
node = ephy_bookmarks_get_keywords (priv->bookmarks);
children = ephy_node_get_children (node);
topics = g_ptr_array_sized_new (children->len);
for (i = 0; i < children->len; i++)
{
node = g_ptr_array_index (children, i);
priority = ephy_node_get_property_int
(node, EPHY_NODE_KEYWORD_PROP_PRIORITY);
if (priority != EPHY_NODE_NORMAL_PRIORITY)
continue;
g_ptr_array_add (topics, node);
}
g_ptr_array_sort (topics, ephy_bookmarks_compare_topic_pointers);
gtk_list_store_clear (priv->store);
g_object_get (entry, "is-focus", &is_focus, NULL);
if (!is_focus)
{
gtk_editable_delete_text (editable, 0, -1);
}
for (pos = -1, i = 0; i < topics->len; i++)
{
node = g_ptr_array_index (topics, i);
title = ephy_node_get_property_string (node, EPHY_NODE_KEYWORD_PROP_NAME);
if (!is_focus && ephy_node_has_child (node, priv->bookmark))
{
gtk_editable_insert_text (editable, title, -1, &pos);
gtk_editable_insert_text (editable, ", ", -1, &pos);
}
tmp1 = g_utf8_casefold (title, -1);
tmp2 = g_utf8_normalize (tmp1, -1, G_NORMALIZE_DEFAULT);
gtk_list_store_append (priv->store, &iter);
gtk_list_store_set (priv->store, &iter, COLUMN_NODE, node,
COLUMN_TITLE, title, COLUMN_KEY, tmp2, -1);
g_free (tmp2);
g_free (tmp1);
}
if (!is_focus)
{
gtk_editable_set_position (editable, -1);
}
g_ptr_array_free (topics, TRUE);
priv->lock = FALSE;
}
/* Updates the bookmarks database to match what is in the text entry */
static void
update_database (EphyTopicsEntry *entry)
{
EphyTopicsEntryPrivate *priv = entry->priv;
EphyNode *node;
const char *text;
char **split;
char *tmp;
gint i;
GtkTreeModel *model;
GtkTreeIter iter;
GValue value = { 0, };
gboolean valid;
/* Prevent any changes to the text entry or completion model */
if(priv->lock) return;
priv->lock = TRUE;
/* Get the list of strings input by the user */
text = gtk_entry_get_text (GTK_ENTRY (entry));
split = g_strsplit (text, ",", 0);
for (i=0; split[i]; i++)
{
g_strstrip (split[i]);
tmp = g_utf8_casefold (split[i], -1);
g_free (split[i]);
split[i] = g_utf8_normalize (tmp, -1, G_NORMALIZE_DEFAULT);
g_free (tmp);
}
/* Loop through the completion model and set/unset topics appropriately */
model = GTK_TREE_MODEL (priv->store);
valid = gtk_tree_model_get_iter_first (model, &iter);
while (valid)
{
gtk_tree_model_get_value (model, &iter, COLUMN_NODE, &value);
node = g_value_get_pointer (&value);
g_value_unset (&value);
gtk_tree_model_get_value (model, &iter, COLUMN_KEY, &value);
text = g_value_get_string (&value);
for (i=0; split[i]; i++)
if (strcmp (text, split[i]) == 0)
break;
if (split[i])
{
split[i][0] = 0;
ephy_bookmarks_set_keyword (priv->bookmarks, node,
priv->bookmark);
}
else
{
ephy_bookmarks_unset_keyword (priv->bookmarks, node,
priv->bookmark);
}
g_value_unset (&value);
valid = gtk_tree_model_iter_next (model, &iter);
}
g_strfreev (split);
priv->lock = FALSE;
}
/* Updates the search key and topic creation action */
static void
update_key (EphyTopicsEntry *entry)
{
EphyTopicsEntryPrivate *priv = entry->priv;
GtkEditable *editable = GTK_EDITABLE (entry);
char *input;
const gchar *text = gtk_entry_get_text (GTK_ENTRY (entry));
const gchar *midpoint = g_utf8_offset_to_pointer (text, gtk_editable_get_position (editable));
const gchar *start = g_utf8_strrchr (text, (gssize)(midpoint-text), ',');
const gchar *end = g_utf8_strchr (midpoint, -1, ',');
if (start == NULL)
start = text;
else if (g_unichar_isspace (g_utf8_get_char (g_utf8_next_char (start))))
start = g_utf8_next_char (g_utf8_next_char (start));
else
start = g_utf8_next_char (start);
if (end == NULL)
end = text+strlen(text);
/* If there was something we could create, then delete the action. */
if (priv->create)
{
gtk_entry_completion_delete_action (priv->completion, 0);
}
g_free (priv->create);
g_free (priv->key);
priv->create = 0;
priv->key = 0;
/* Set the priv->create and priv->key appropriately. */
if (start != end)
{
input = g_strndup (start, end-start);
g_strstrip (input);
priv->create = input;
input = g_utf8_casefold (input, -1);
priv->key = g_utf8_normalize (input, -1, G_NORMALIZE_DEFAULT);
g_free (input);
if (priv->create[0] == '\0' ||
find_topic (entry, priv->key) != NULL)
{
g_free (priv->create);
priv->create = 0;
}
/* If there is something we can create, then setup the action. */
else
{
input = g_strdup_printf (_("Create topic “%s”"), priv->create);
gtk_entry_completion_insert_action_text (priv->completion, 0, input);
g_free (input);
}
}
}
static gboolean
match_func (GtkEntryCompletion *completion,
const gchar *key,
GtkTreeIter *iter,
gpointer user_data)
{
EphyTopicsEntry *entry = EPHY_TOPICS_ENTRY (gtk_entry_completion_get_entry (completion));
EphyTopicsEntryPrivate *priv = entry->priv;
GtkTreeModel *model = gtk_entry_completion_get_model (completion);
gboolean result;
GValue value = { 0, };
EphyNode *node;
if (priv->key == NULL)
{
return FALSE;
}
/* If no node at all (this happens for unknown reasons) then don't show. */
gtk_tree_model_get_value (model, iter, COLUMN_NODE, &value);
node = g_value_get_pointer (&value);
g_value_unset (&value);
if (node == NULL)
{
result = FALSE;
}
/* If it's already selected, don't show it unless we're editing it. */
else if (ephy_node_has_child (node, priv->bookmark))
{
gtk_tree_model_get_value (model, iter, COLUMN_KEY, &value);
result = (strcmp (g_value_get_string (&value), priv->key) == 0);
g_value_unset (&value);
}
/* If it's not selected, show it if it matches. */
else
{
gtk_tree_model_get_value (model, iter, COLUMN_KEY, &value);
result = (g_str_has_prefix (g_value_get_string (&value), priv->key));
g_value_unset (&value);
}
return result;
}
static void
action_cb (GtkEntryCompletion *completion,
gint index,
gpointer user_data)
{
EphyTopicsEntry *entry = EPHY_TOPICS_ENTRY (gtk_entry_completion_get_entry (completion));
EphyTopicsEntryPrivate *priv = entry->priv;
char *title;
title = g_strdup (priv->create);
ephy_bookmarks_add_keyword (priv->bookmarks, title);
update_widget (entry);
insert_text (entry, title);
g_free (title);
}
static gboolean
match_selected_cb (GtkEntryCompletion *completion,
GtkTreeModel *model,
GtkTreeIter *iter,
gpointer user_data)
{
EphyTopicsEntry *entry = EPHY_TOPICS_ENTRY (gtk_entry_completion_get_entry (completion));
GValue value = { 0, };
gtk_tree_model_get_value (model, iter, COLUMN_TITLE, &value);
insert_text (entry, g_value_get_string (&value));
g_value_unset (&value);
return TRUE;
}
static void
activate_cb (GtkEditable *editable,
gpointer user_data)
{
EphyTopicsEntry *entry = EPHY_TOPICS_ENTRY (editable);
EphyTopicsEntryPrivate *priv = entry->priv;
GtkEntryCompletion *completion = gtk_entry_get_completion (GTK_ENTRY (entry));
GValue value = { 0, };
GtkTreeModel *model;
GtkTreeIter iter;
gboolean valid;
if (priv->key == NULL || priv->key[0] == '\0')
{
gtk_entry_set_activates_default (GTK_ENTRY (entry), TRUE);
return;
}
else
{
gtk_entry_set_activates_default (GTK_ENTRY (entry), FALSE);
}
/* Loop through the completion model and find the first item to use, if any. */
model = GTK_TREE_MODEL (priv->store);
valid = gtk_tree_model_get_iter_first (model, &iter);
while (valid && !match_func (completion, NULL, &iter, NULL))
{
valid = gtk_tree_model_iter_next (model, &iter);
}
if (valid)
{
gtk_tree_model_get_value (model, &iter, COLUMN_TITLE, &value);
/* See if there were any others. */
valid = gtk_tree_model_iter_next (model, &iter);
while (valid && !match_func (completion, NULL, &iter, NULL))
{
valid = gtk_tree_model_iter_next (model, &iter);
}
if (!valid)
{
insert_text (EPHY_TOPICS_ENTRY (editable), g_value_get_string (&value));
g_value_unset (&value);
}
}
}
static void
tree_changed_cb (EphyBookmarks *bookmarks,
EphyTopicsEntry *entry)
{
update_widget (entry);
}
static void
node_added_cb (EphyNode *parent,
EphyNode *child,
GObject *object)
{
update_widget (EPHY_TOPICS_ENTRY (object));
}
static void
node_changed_cb (EphyNode *parent,
EphyNode *child,
guint property_id,
GObject *object)
{
update_widget (EPHY_TOPICS_ENTRY (object));
}
static void
node_removed_cb (EphyNode *parent,
EphyNode *child,
guint index,
GObject *object)
{
update_widget (EPHY_TOPICS_ENTRY (object));
}
static void
ephy_topics_entry_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
EphyTopicsEntry *entry = EPHY_TOPICS_ENTRY (object);
EphyNode *node;
switch (prop_id)
{
case PROP_BOOKMARKS:
entry->priv->bookmarks = g_value_get_object (value);
node = ephy_bookmarks_get_keywords (entry->priv->bookmarks);
ephy_node_signal_connect_object (node, EPHY_NODE_CHILD_ADDED,
(EphyNodeCallback) node_added_cb, object);
ephy_node_signal_connect_object (node, EPHY_NODE_CHILD_CHANGED,
(EphyNodeCallback) node_changed_cb, object);
ephy_node_signal_connect_object (node, EPHY_NODE_CHILD_REMOVED,
(EphyNodeCallback) node_removed_cb, object);
g_signal_connect_object (entry->priv->bookmarks, "tree-changed",
G_CALLBACK (tree_changed_cb), entry,
G_CONNECT_AFTER);
break;
case PROP_BOOKMARK:
entry->priv->bookmark = g_value_get_pointer (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static GObject *
ephy_topics_entry_constructor (GType type,
guint n_construct_properties,
GObjectConstructParam *construct_params)
{
GObject *object;
EphyTopicsEntry *entry;
EphyTopicsEntryPrivate *priv;
object = parent_class->constructor (type, n_construct_properties,
construct_params);
entry = EPHY_TOPICS_ENTRY (object);
priv = EPHY_TOPICS_ENTRY_GET_PRIVATE (object);
priv->store = gtk_list_store_new (3, G_TYPE_POINTER, G_TYPE_STRING, G_TYPE_STRING);
priv->completion = gtk_entry_completion_new ();
gtk_entry_completion_set_model (priv->completion, GTK_TREE_MODEL (priv->store));
gtk_entry_completion_set_text_column (priv->completion, COLUMN_TITLE);
gtk_entry_completion_set_popup_completion (priv->completion, TRUE);
gtk_entry_completion_set_popup_single_match (priv->completion, TRUE);
gtk_entry_completion_set_match_func (priv->completion, match_func, NULL, NULL);
gtk_entry_set_completion (GTK_ENTRY (entry), priv->completion);
g_signal_connect (priv->completion, "match-selected",
G_CALLBACK (match_selected_cb), NULL);
g_signal_connect (priv->completion, "action-activated",
G_CALLBACK (action_cb), NULL);
g_signal_connect (object, "activate",
G_CALLBACK (activate_cb), NULL);
g_signal_connect (object, "changed",
G_CALLBACK (update_database), NULL);
g_signal_connect (object, "notify::is-focus",
G_CALLBACK (update_widget), NULL);
g_signal_connect (object, "notify::cursor-position",
G_CALLBACK (update_key), NULL);
g_signal_connect (object, "notify::text",
G_CALLBACK (update_key), NULL);
update_key (entry);
update_widget (entry);
return object;
}
static void
ephy_topics_entry_init (EphyTopicsEntry *entry)
{
entry->priv = EPHY_TOPICS_ENTRY_GET_PRIVATE (entry);
}
static void
ephy_topics_entry_finalize (GObject *object)
{
EphyTopicsEntry *entry = EPHY_TOPICS_ENTRY (object);
g_free (entry->priv->create);
g_free (entry->priv->key);
parent_class->finalize (object);
}
GtkWidget *
ephy_topics_entry_new (EphyBookmarks *bookmarks,
EphyNode *bookmark)
{
EphyTopicsEntry *entry;
g_assert (bookmarks != NULL);
entry = EPHY_TOPICS_ENTRY (g_object_new
(EPHY_TYPE_TOPICS_ENTRY,
"bookmarks", bookmarks,
"bookmark", bookmark,
NULL));
return GTK_WIDGET (entry);
}
static void
ephy_topics_entry_class_init (EphyTopicsEntryClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
parent_class = g_type_class_peek_parent (klass);
object_class->set_property = ephy_topics_entry_set_property;
object_class->constructor = ephy_topics_entry_constructor;
object_class->finalize = ephy_topics_entry_finalize;
g_object_class_install_property (object_class,
PROP_BOOKMARKS,
g_param_spec_object ("bookmarks",
"Bookmarks set",
"Bookmarks set",
EPHY_TYPE_BOOKMARKS,
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
G_PARAM_STATIC_BLURB));
g_object_class_install_property (object_class,
PROP_BOOKMARK,
g_param_spec_pointer ("bookmark",
"Bookmark",
"Bookmark",
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
G_PARAM_STATIC_BLURB));
g_type_class_add_private (object_class, sizeof(EphyTopicsEntryPrivate));
}