Evolution consists of entirely too many small utility libraries, which increases linking and loading time, places a burden on higher layers of the application (e.g. modules) which has to remember to link to all the small in-tree utility libraries, and makes it difficult to generate API documentation for these utility libraries in one Gtk-Doc module. Merge the following utility libraries under the umbrella of libeutil, and enforce a single-include policy on libeutil so we can reorganize the files as desired without disrupting its pseudo-public API. libemail-utils/libemail-utils.la libevolution-utils/libevolution-utils.la filter/libfilter.la widgets/e-timezone-dialog/libetimezonedialog.la widgets/menus/libmenus.la widgets/misc/libemiscwidgets.la widgets/table/libetable.la widgets/text/libetext.la This also merges libedataserverui from the Evolution-Data-Server module, since Evolution is its only consumer nowadays, and I'd like to make some improvements to those APIs without concern for backward-compatibility. And finally, start a Gtk-Doc module for libeutil. It's going to be a project just getting all the symbols _listed_ much less _documented_. But the skeletal structure is in place and I'm off to a good start.
506 lines
13 KiB
C
506 lines
13 KiB
C
/*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the License, or (at your option) version 3.
|
|
*
|
|
* 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
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with the program; if not, see <http://www.gnu.org/licenses/>
|
|
*
|
|
* Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
|
|
*/
|
|
|
|
#include "e-category-completion.h"
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include <config.h>
|
|
#endif
|
|
|
|
#include <string.h>
|
|
#include <glib/gi18n-lib.h>
|
|
|
|
#include <libedataserver/libedataserver.h>
|
|
|
|
#define E_CATEGORY_COMPLETION_GET_PRIVATE(obj) \
|
|
(G_TYPE_INSTANCE_GET_PRIVATE \
|
|
((obj), E_TYPE_CATEGORY_COMPLETION, ECategoryCompletionPrivate))
|
|
|
|
struct _ECategoryCompletionPrivate {
|
|
GtkWidget *last_known_entry;
|
|
gchar *create;
|
|
gchar *prefix;
|
|
};
|
|
|
|
enum {
|
|
COLUMN_PIXBUF,
|
|
COLUMN_CATEGORY,
|
|
COLUMN_NORMALIZED,
|
|
NUM_COLUMNS
|
|
};
|
|
|
|
G_DEFINE_TYPE (
|
|
ECategoryCompletion,
|
|
e_category_completion,
|
|
GTK_TYPE_ENTRY_COMPLETION)
|
|
|
|
/* Forward Declarations */
|
|
|
|
static void
|
|
category_completion_track_entry (GtkEntryCompletion *completion);
|
|
|
|
static void
|
|
category_completion_build_model (GtkEntryCompletion *completion)
|
|
{
|
|
GtkListStore *store;
|
|
GList *list;
|
|
|
|
store = gtk_list_store_new (
|
|
NUM_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING);
|
|
|
|
list = e_categories_get_list ();
|
|
while (list != NULL) {
|
|
const gchar *category = list->data;
|
|
const gchar *filename;
|
|
gchar *normalized;
|
|
gchar *casefolded;
|
|
GdkPixbuf *pixbuf = NULL;
|
|
GtkTreeIter iter;
|
|
|
|
/* Only add user-visible categories. */
|
|
if (!e_categories_is_searchable (category)) {
|
|
list = g_list_delete_link (list, list);
|
|
continue;
|
|
}
|
|
|
|
filename = e_categories_get_icon_file_for (category);
|
|
if (filename != NULL && *filename != '\0')
|
|
pixbuf = gdk_pixbuf_new_from_file (filename, NULL);
|
|
|
|
normalized = g_utf8_normalize (
|
|
category, -1, G_NORMALIZE_DEFAULT);
|
|
casefolded = g_utf8_casefold (normalized, -1);
|
|
|
|
gtk_list_store_append (store, &iter);
|
|
|
|
gtk_list_store_set (
|
|
store, &iter, COLUMN_PIXBUF, pixbuf,
|
|
COLUMN_CATEGORY, category, COLUMN_NORMALIZED,
|
|
casefolded, -1);
|
|
|
|
g_free (normalized);
|
|
g_free (casefolded);
|
|
|
|
if (pixbuf != NULL)
|
|
g_object_unref (pixbuf);
|
|
|
|
list = g_list_delete_link (list, list);
|
|
}
|
|
|
|
gtk_entry_completion_set_model (completion, GTK_TREE_MODEL (store));
|
|
}
|
|
|
|
static void
|
|
category_completion_categories_changed_cb (GObject *some_private_object,
|
|
GtkEntryCompletion *completion)
|
|
{
|
|
category_completion_build_model (completion);
|
|
}
|
|
|
|
static void
|
|
category_completion_complete (GtkEntryCompletion *completion,
|
|
const gchar *category)
|
|
{
|
|
GtkEditable *editable;
|
|
GtkWidget *entry;
|
|
const gchar *text;
|
|
const gchar *cp;
|
|
gint start_pos;
|
|
gint end_pos;
|
|
glong offset;
|
|
|
|
entry = gtk_entry_completion_get_entry (completion);
|
|
|
|
editable = GTK_EDITABLE (entry);
|
|
text = gtk_entry_get_text (GTK_ENTRY (entry));
|
|
|
|
/* Get the cursor position as a character offset. */
|
|
offset = gtk_editable_get_position (editable);
|
|
|
|
/* Find the rightmost comma before the cursor. */
|
|
cp = g_utf8_offset_to_pointer (text, offset);
|
|
cp = g_utf8_strrchr (text, (gssize) (cp - text), ',');
|
|
|
|
/* Calculate the selection start position as a character offset. */
|
|
if (cp == NULL)
|
|
offset = 0;
|
|
else {
|
|
cp = g_utf8_next_char (cp);
|
|
if (g_unichar_isspace (g_utf8_get_char (cp)))
|
|
cp = g_utf8_next_char (cp);
|
|
offset = g_utf8_pointer_to_offset (text, cp);
|
|
}
|
|
start_pos = (gint) offset;
|
|
|
|
/* Find the leftmost comma after the cursor. */
|
|
cp = g_utf8_offset_to_pointer (text, offset);
|
|
cp = g_utf8_strchr (cp, -1, ',');
|
|
|
|
/* Calculate the selection end position as a character offset. */
|
|
if (cp == NULL)
|
|
offset = -1;
|
|
else {
|
|
cp = g_utf8_next_char (cp);
|
|
if (g_unichar_isspace (g_utf8_get_char (cp)))
|
|
cp = g_utf8_next_char (cp);
|
|
offset = g_utf8_pointer_to_offset (text, cp);
|
|
}
|
|
end_pos = (gint) offset;
|
|
|
|
/* Complete the partially typed category. */
|
|
gtk_editable_delete_text (editable, start_pos, end_pos);
|
|
gtk_editable_insert_text (editable, category, -1, &start_pos);
|
|
gtk_editable_insert_text (editable, ",", 1, &start_pos);
|
|
gtk_editable_set_position (editable, start_pos);
|
|
}
|
|
|
|
static gboolean
|
|
category_completion_is_match (GtkEntryCompletion *completion,
|
|
const gchar *key,
|
|
GtkTreeIter *iter)
|
|
{
|
|
ECategoryCompletionPrivate *priv;
|
|
GtkTreeModel *model;
|
|
GtkWidget *entry;
|
|
GValue value = { 0, };
|
|
gboolean match;
|
|
|
|
priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion);
|
|
entry = gtk_entry_completion_get_entry (completion);
|
|
model = gtk_entry_completion_get_model (completion);
|
|
|
|
/* XXX This would be easier if GtkEntryCompletion had an 'entry'
|
|
* property that we could listen to for notifications. */
|
|
if (entry != priv->last_known_entry)
|
|
category_completion_track_entry (completion);
|
|
|
|
if (priv->prefix == NULL)
|
|
return FALSE;
|
|
|
|
gtk_tree_model_get_value (model, iter, COLUMN_NORMALIZED, &value);
|
|
match = g_str_has_prefix (g_value_get_string (&value), priv->prefix);
|
|
g_value_unset (&value);
|
|
|
|
return match;
|
|
}
|
|
|
|
static void
|
|
category_completion_update_prefix (GtkEntryCompletion *completion)
|
|
{
|
|
ECategoryCompletionPrivate *priv;
|
|
GtkEditable *editable;
|
|
GtkTreeModel *model;
|
|
GtkWidget *entry;
|
|
GtkTreeIter iter;
|
|
const gchar *text;
|
|
const gchar *start;
|
|
const gchar *end;
|
|
const gchar *cp;
|
|
gboolean valid;
|
|
gchar *input;
|
|
glong offset;
|
|
|
|
priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion);
|
|
entry = gtk_entry_completion_get_entry (completion);
|
|
model = gtk_entry_completion_get_model (completion);
|
|
|
|
/* XXX This would be easier if GtkEntryCompletion had an 'entry'
|
|
* property that we could listen to for notifications. */
|
|
if (entry != priv->last_known_entry) {
|
|
category_completion_track_entry (completion);
|
|
return;
|
|
}
|
|
|
|
editable = GTK_EDITABLE (entry);
|
|
text = gtk_entry_get_text (GTK_ENTRY (entry));
|
|
|
|
/* Get the cursor position as a character offset. */
|
|
offset = gtk_editable_get_position (editable);
|
|
|
|
/* Find the rightmost comma before the cursor. */
|
|
cp = g_utf8_offset_to_pointer (text, offset);
|
|
cp = g_utf8_strrchr (text, (gsize) (cp - text), ',');
|
|
|
|
/* Mark the start of the prefix. */
|
|
if (cp == NULL)
|
|
start = text;
|
|
else {
|
|
cp = g_utf8_next_char (cp);
|
|
if (g_unichar_isspace (g_utf8_get_char (cp)))
|
|
cp = g_utf8_next_char (cp);
|
|
start = cp;
|
|
}
|
|
|
|
/* Find the leftmost comma after the cursor. */
|
|
cp = g_utf8_offset_to_pointer (text, offset);
|
|
cp = g_utf8_strchr (cp, -1, ',');
|
|
|
|
/* Mark the end of the prefix. */
|
|
if (cp == NULL)
|
|
end = text + strlen (text);
|
|
else
|
|
end = cp;
|
|
|
|
if (priv->create != NULL)
|
|
gtk_entry_completion_delete_action (completion, 0);
|
|
|
|
g_free (priv->create);
|
|
priv->create = NULL;
|
|
|
|
g_free (priv->prefix);
|
|
priv->prefix = NULL;
|
|
|
|
if (start == end)
|
|
return;
|
|
|
|
input = g_strstrip (g_strndup (start, end - start));
|
|
priv->create = input;
|
|
|
|
input = g_utf8_normalize (input, -1, G_NORMALIZE_DEFAULT);
|
|
priv->prefix = g_utf8_casefold (input, -1);
|
|
g_free (input);
|
|
|
|
if (*priv->create == '\0') {
|
|
g_free (priv->create);
|
|
priv->create = NULL;
|
|
return;
|
|
}
|
|
|
|
valid = gtk_tree_model_get_iter_first (model, &iter);
|
|
while (valid) {
|
|
GValue value = { 0, };
|
|
|
|
gtk_tree_model_get_value (
|
|
model, &iter, COLUMN_NORMALIZED, &value);
|
|
if (strcmp (g_value_get_string (&value), priv->prefix) == 0) {
|
|
g_value_unset (&value);
|
|
g_free (priv->create);
|
|
priv->create = NULL;
|
|
return;
|
|
}
|
|
g_value_unset (&value);
|
|
|
|
valid = gtk_tree_model_iter_next (model, &iter);
|
|
}
|
|
|
|
input = g_strdup_printf (_("Create category \"%s\""), priv->create);
|
|
gtk_entry_completion_insert_action_text (completion, 0, input);
|
|
g_free (input);
|
|
}
|
|
|
|
static gboolean
|
|
category_completion_sanitize_suffix (GtkEntry *entry,
|
|
GdkEventFocus *event,
|
|
GtkEntryCompletion *completion)
|
|
{
|
|
const gchar *text;
|
|
|
|
g_return_val_if_fail (entry != NULL, FALSE);
|
|
g_return_val_if_fail (completion != NULL, FALSE);
|
|
|
|
text = gtk_entry_get_text (entry);
|
|
if (text) {
|
|
gint len = strlen (text), old_len = len;
|
|
|
|
while (len > 0 && (text[len -1] == ' ' || text[len - 1] == ','))
|
|
len--;
|
|
|
|
if (old_len != len) {
|
|
gchar *tmp = g_strndup (text, len);
|
|
|
|
gtk_entry_set_text (entry, tmp);
|
|
|
|
g_free (tmp);
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static void
|
|
category_completion_track_entry (GtkEntryCompletion *completion)
|
|
{
|
|
ECategoryCompletionPrivate *priv;
|
|
|
|
priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion);
|
|
|
|
if (priv->last_known_entry != NULL) {
|
|
g_signal_handlers_disconnect_matched (
|
|
priv->last_known_entry, G_SIGNAL_MATCH_DATA,
|
|
0, 0, NULL, NULL, completion);
|
|
g_object_unref (priv->last_known_entry);
|
|
}
|
|
|
|
g_free (priv->prefix);
|
|
priv->prefix = NULL;
|
|
|
|
priv->last_known_entry = gtk_entry_completion_get_entry (completion);
|
|
if (priv->last_known_entry == NULL)
|
|
return;
|
|
|
|
g_object_ref (priv->last_known_entry);
|
|
|
|
g_signal_connect_swapped (
|
|
priv->last_known_entry, "notify::cursor-position",
|
|
G_CALLBACK (category_completion_update_prefix), completion);
|
|
|
|
g_signal_connect_swapped (
|
|
priv->last_known_entry, "notify::text",
|
|
G_CALLBACK (category_completion_update_prefix), completion);
|
|
|
|
g_signal_connect (
|
|
priv->last_known_entry, "focus-out-event",
|
|
G_CALLBACK (category_completion_sanitize_suffix), completion);
|
|
|
|
category_completion_update_prefix (completion);
|
|
}
|
|
|
|
static void
|
|
category_completion_constructed (GObject *object)
|
|
{
|
|
GtkCellRenderer *renderer;
|
|
GtkEntryCompletion *completion;
|
|
|
|
/* Chain up to parent's constructed() method. */
|
|
G_OBJECT_CLASS (e_category_completion_parent_class)->constructed (object);
|
|
|
|
completion = GTK_ENTRY_COMPLETION (object);
|
|
|
|
gtk_entry_completion_set_match_func (
|
|
completion, (GtkEntryCompletionMatchFunc)
|
|
category_completion_is_match, NULL, NULL);
|
|
|
|
gtk_entry_completion_set_text_column (completion, COLUMN_CATEGORY);
|
|
|
|
renderer = gtk_cell_renderer_pixbuf_new ();
|
|
gtk_cell_layout_pack_start (
|
|
GTK_CELL_LAYOUT (completion), renderer, FALSE);
|
|
gtk_cell_layout_add_attribute (
|
|
GTK_CELL_LAYOUT (completion),
|
|
renderer, "pixbuf", COLUMN_PIXBUF);
|
|
gtk_cell_layout_reorder (
|
|
GTK_CELL_LAYOUT (completion), renderer, 0);
|
|
|
|
e_categories_register_change_listener (
|
|
G_CALLBACK (category_completion_categories_changed_cb),
|
|
completion);
|
|
|
|
category_completion_build_model (completion);
|
|
}
|
|
|
|
static void
|
|
category_completion_dispose (GObject *object)
|
|
{
|
|
ECategoryCompletionPrivate *priv;
|
|
|
|
priv = E_CATEGORY_COMPLETION_GET_PRIVATE (object);
|
|
|
|
if (priv->last_known_entry != NULL) {
|
|
g_signal_handlers_disconnect_matched (
|
|
priv->last_known_entry, G_SIGNAL_MATCH_DATA,
|
|
0, 0, NULL, NULL, object);
|
|
g_object_unref (priv->last_known_entry);
|
|
priv->last_known_entry = NULL;
|
|
}
|
|
|
|
/* Chain up to parent's dispose() method. */
|
|
G_OBJECT_CLASS (e_category_completion_parent_class)->dispose (object);
|
|
}
|
|
|
|
static void
|
|
category_completion_finalize (GObject *object)
|
|
{
|
|
ECategoryCompletionPrivate *priv;
|
|
|
|
priv = E_CATEGORY_COMPLETION_GET_PRIVATE (object);
|
|
|
|
g_free (priv->create);
|
|
g_free (priv->prefix);
|
|
|
|
e_categories_unregister_change_listener (
|
|
G_CALLBACK (category_completion_categories_changed_cb),
|
|
object);
|
|
|
|
/* Chain up to parent's finalize() method. */
|
|
G_OBJECT_CLASS (e_category_completion_parent_class)->finalize (object);
|
|
}
|
|
|
|
static gboolean
|
|
category_completion_match_selected (GtkEntryCompletion *completion,
|
|
GtkTreeModel *model,
|
|
GtkTreeIter *iter)
|
|
{
|
|
GValue value = { 0, };
|
|
|
|
gtk_tree_model_get_value (model, iter, COLUMN_CATEGORY, &value);
|
|
category_completion_complete (completion, g_value_get_string (&value));
|
|
g_value_unset (&value);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
category_completion_action_activated (GtkEntryCompletion *completion,
|
|
gint index)
|
|
{
|
|
ECategoryCompletionPrivate *priv;
|
|
gchar *category;
|
|
|
|
priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion);
|
|
|
|
category = g_strdup (priv->create);
|
|
e_categories_add (category, NULL, NULL, TRUE);
|
|
category_completion_complete (completion, category);
|
|
g_free (category);
|
|
}
|
|
|
|
static void
|
|
e_category_completion_class_init (ECategoryCompletionClass *class)
|
|
{
|
|
GObjectClass *object_class;
|
|
GtkEntryCompletionClass *entry_completion_class;
|
|
|
|
g_type_class_add_private (class, sizeof (ECategoryCompletionPrivate));
|
|
|
|
object_class = G_OBJECT_CLASS (class);
|
|
object_class->constructed = category_completion_constructed;
|
|
object_class->dispose = category_completion_dispose;
|
|
object_class->finalize = category_completion_finalize;
|
|
|
|
entry_completion_class = GTK_ENTRY_COMPLETION_CLASS (class);
|
|
entry_completion_class->match_selected = category_completion_match_selected;
|
|
entry_completion_class->action_activated = category_completion_action_activated;
|
|
}
|
|
|
|
static void
|
|
e_category_completion_init (ECategoryCompletion *category_completion)
|
|
{
|
|
category_completion->priv =
|
|
E_CATEGORY_COMPLETION_GET_PRIVATE (category_completion);
|
|
}
|
|
|
|
/**
|
|
* e_category_completion_new:
|
|
*
|
|
* Since: 2.26
|
|
**/
|
|
GtkEntryCompletion *
|
|
e_category_completion_new (void)
|
|
{
|
|
return g_object_new (E_TYPE_CATEGORY_COMPLETION, NULL);
|
|
}
|