/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
* Authors:
* Chris Lahey <clahey@ximian.com>
* Jon Trowbridge <trow@ximian.com>
*
* Copyright (C) 2000, 2001 Ximian, Inc.
*/
#include <config.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <gal/e-text/e-text-model-repos.h>
#include <libgnome/gnome-i18n.h>
#include <addressbook/gui/contact-editor/e-contact-editor.h>
#include "e-select-names-text-model.h"
#include "e-addressbook-util.h"
static FILE *out = NULL; /* stream for debugging spew */
/* Object argument IDs */
enum {
PROP_0,
PROP_SOURCE,
};
static void e_select_names_text_model_class_init (ESelectNamesTextModelClass *klass);
static void e_select_names_text_model_init (ESelectNamesTextModel *model);
static void e_select_names_text_model_dispose (GObject *object);
static void e_select_names_text_model_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec);
static void e_select_names_text_model_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec);
static void e_select_names_text_model_set_source (ESelectNamesTextModel *model, ESelectNamesModel *source);
static const gchar *e_select_names_text_model_get_text (ETextModel *model);
static void e_select_names_text_model_set_text (ETextModel *model, const gchar *text);
static void e_select_names_text_model_insert (ETextModel *model, gint position, const gchar *text);
static void e_select_names_text_model_insert_length (ETextModel *model, gint position, const gchar *text, gint length);
static void e_select_names_text_model_delete (ETextModel *model, gint position, gint length);
static gint e_select_names_text_model_obj_count (ETextModel *model);
static const gchar *e_select_names_text_model_get_nth_obj (ETextModel *model, gint n, gint *len);
static void e_select_names_text_model_activate_obj (ETextModel *model, gint n);
static ETextModelClass *parent_class;
#define PARENT_TYPE e_text_model_get_type()
/**
* e_select_names_text_model_get_type:
* @void:
*
* Registers the &ESelectNamesTextModel class if necessary, and returns the type ID
* associated to it.
*
* Return value: The type ID of the &ESelectNamesTextModel class.
**/
GtkType
e_select_names_text_model_get_type (void)
{
static GType type = 0;
if (!type) {
static const GTypeInfo info = {
sizeof (ESelectNamesTextModelClass),
NULL, /* base_init */
NULL, /* base_finalize */
(GClassInitFunc) e_select_names_text_model_class_init,
NULL, /* class_finalize */
NULL, /* class_data */
sizeof (ESelectNamesTextModel),
0, /* n_preallocs */
(GInstanceInitFunc) e_select_names_text_model_init,
};
type = g_type_register_static (PARENT_TYPE, "ESelectNamesTextModel", &info, 0);
}
return type;
}
static void
e_select_names_text_model_class_init (ESelectNamesTextModelClass *klass)
{
GObjectClass *object_class;
ETextModelClass *text_model_class;
object_class = G_OBJECT_CLASS(klass);
text_model_class = E_TEXT_MODEL_CLASS(klass);
parent_class = g_type_class_peek_parent (klass);
object_class->dispose = e_select_names_text_model_dispose;
object_class->get_property = e_select_names_text_model_get_property;
object_class->set_property = e_select_names_text_model_set_property;
g_object_class_install_property (object_class, PROP_SOURCE,
g_param_spec_object ("source",
_("Source"),
/*_( */"XXX blurb" /*)*/,
E_TYPE_SELECT_NAMES_MODEL,
G_PARAM_READWRITE));
text_model_class->get_text = e_select_names_text_model_get_text;
text_model_class->set_text = e_select_names_text_model_set_text;
text_model_class->insert = e_select_names_text_model_insert;
text_model_class->insert_length = e_select_names_text_model_insert_length;
text_model_class->delete = e_select_names_text_model_delete;
text_model_class->obj_count = e_select_names_text_model_obj_count;
text_model_class->get_nth_obj = e_select_names_text_model_get_nth_obj;
text_model_class->object_activated = e_select_names_text_model_activate_obj;
if (getenv ("EVO_DEBUG_SELECT_NAMES_TEXT_MODEL")) {
out = fopen ("/tmp/evo-debug-select-names-text-model", "w");
if (out)
setvbuf (out, NULL, _IONBF, 0);
}
}
static void
dump_model (ESelectNamesTextModel *text_model)
{
ESelectNamesModel *model = text_model->source;
gint i;
if (out == NULL)
return;
fprintf (out, "\n*** Model State: count=%d\n", e_select_names_model_count (model));
for (i=0; i<e_select_names_model_count (model); ++i)
fprintf (out, "[%d] \"%s\" %s\n", i,
e_select_names_model_get_string (model, i),
e_select_names_model_get_card (model, i) ? "<card>" : "");
fprintf (out, "\n");
}
static void
e_select_names_text_model_init (ESelectNamesTextModel *model)
{
const gchar *default_sep;
model->last_magic_comma_pos = -1;
if (getenv ("EVOLUTION_DISABLE_MAGIC_COMMA"))
default_sep = ",";
else
default_sep = ", ";
e_select_names_text_model_set_separator (model, default_sep);
}
static void
e_select_names_text_model_dispose (GObject *object)
{
ESelectNamesTextModel *model;
model = E_SELECT_NAMES_TEXT_MODEL (object);
if (model->text) {
g_free (model->text);
model->text = NULL;
}
if (model->sep) {
g_free (model->sep);
model->sep = NULL;
}
e_select_names_text_model_set_source (model, NULL);
if (G_OBJECT_CLASS(parent_class)->dispose)
G_OBJECT_CLASS(parent_class)->dispose(object);
}
static void
e_select_names_text_model_set_property (GObject *object, guint prop_id,
const GValue *value, GParamSpec *pspec)
{
ESelectNamesTextModel *model;
model = E_SELECT_NAMES_TEXT_MODEL (object);
switch (prop_id) {
case PROP_SOURCE:
e_select_names_text_model_set_source(model, E_SELECT_NAMES_MODEL (g_value_get_object(value)));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
e_select_names_text_model_get_property (GObject *object, guint prop_id,
GValue *value, GParamSpec *pspec)
{
ESelectNamesTextModel *model;
model = E_SELECT_NAMES_TEXT_MODEL (object);
switch (prop_id) {
case PROP_SOURCE:
g_value_set_object (value, model->source);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
resize_cb (ESelectNamesModel *source, gint index, gint old_len, gint new_len, ETextModel *model)
{
EReposDeleteShift repos_del;
EReposInsertShift repos_ins;
gint pos;
gint seplen = E_SELECT_NAMES_TEXT_MODEL (model)->seplen;
e_select_names_model_name_pos (source, seplen, index, &pos, NULL);
if (new_len < old_len) {
repos_del.model = model;
repos_del.pos = pos;
repos_del.len = old_len - new_len;
e_text_model_reposition (model, e_repos_delete_shift, &repos_del);
} else if (old_len < new_len) {
repos_ins.model = model;
repos_ins.pos = pos;
repos_ins.len = new_len - old_len;
e_text_model_reposition (model, e_repos_insert_shift, &repos_ins);
}
}
static void
changed_cb (ESelectNamesModel *source, ETextModel *model)
{
ESelectNamesTextModel *text_model = E_SELECT_NAMES_TEXT_MODEL (model);
g_free (text_model->text);
text_model->text = NULL;
e_text_model_changed (model);
}
static void
e_select_names_text_model_set_source (ESelectNamesTextModel *model,
ESelectNamesModel *source)
{
if (source == model->source)
return;
if (model->source) {
g_signal_handler_disconnect (model->source, model->source_changed_id);
g_signal_handler_disconnect (model->source, model->source_resize_id);
g_object_unref (model->source);
}
model->source = source;
if (model->source) {
g_object_ref (model->source);
model->source_changed_id = g_signal_connect (model->source,
"changed",
G_CALLBACK (changed_cb),
model);
model->source_resize_id = g_signal_connect (model->source,
"resized",
G_CALLBACK (resize_cb),
model);
}
}
ETextModel *
e_select_names_text_model_new (ESelectNamesModel *source)
{
ETextModel *model = g_object_new (E_TYPE_SELECT_NAMES_TEXT_MODEL, NULL);
e_select_names_text_model_set_source (E_SELECT_NAMES_TEXT_MODEL (model), source);
return model;
}
void
e_select_names_text_model_set_separator (ESelectNamesTextModel *model, const char *sep)
{
g_return_if_fail (E_IS_SELECT_NAMES_TEXT_MODEL (model));
g_return_if_fail (sep && *sep);
g_free (model->sep);
model->sep = g_strdup (sep);
model->seplen = strlen (sep);
}
static const gchar *
e_select_names_text_model_get_text (ETextModel *model)
{
ESelectNamesTextModel *snm = E_SELECT_NAMES_TEXT_MODEL(model);
if (snm == NULL)
return "";
else if (snm->text == NULL)
snm->text = e_select_names_model_get_textification (snm->source, snm->sep);
return snm->text;
}
static void
e_select_names_text_model_set_text (ETextModel *model, const gchar *text)
{
ESelectNamesTextModel *snm = E_SELECT_NAMES_TEXT_MODEL(model);
e_select_names_model_delete_all (snm->source);
e_select_names_text_model_insert (model, 0, text);
}
static void
e_select_names_text_model_insert (ETextModel *model, gint position, const gchar *text)
{
e_select_names_text_model_insert_length (model, position, text, strlen (text));
}
static void
e_select_names_text_model_insert_length (ETextModel *model, gint pos, const gchar *text, gint length)
{
ESelectNamesTextModel *text_model = E_SELECT_NAMES_TEXT_MODEL (model);
ESelectNamesModel *source = text_model->source;
gint i;
if (out) {
gchar *tmp = g_strndup (text, length);
fprintf (out, ">> insert \"%s\" (len=%d) at %d\n", tmp, length, pos);
g_free (tmp);
}
pos = CLAMP (pos, 0, strlen (e_select_names_model_get_textification (source, text_model->sep)));
/* We want to control all cursor motions ourselves, rather than taking hints
from the ESelectNamesModel. */
g_signal_handler_block (source, text_model->source_resize_id);
/* We handle this one character at a time. */
for (i = 0; i < length && text[i]; ++i) {
gint index, start_pos, text_len;
gboolean inside_quote = FALSE;
text_model->last_magic_comma_pos = -1;
if (out)
fprintf (out, "processing [%c]\n", text[i]);
e_select_names_model_text_pos (source, text_model->seplen, pos, &index, &start_pos, &text_len);
if (out)
fprintf (out, "index=%d start_pos=%d text_len=%d\n", index, start_pos, text_len);
if (text[i] == *text_model->sep && index >= 0) { /* Is this a quoted or an unquoted separator we are dealing with? */
const EDestination *dest = e_select_names_model_get_destination (source, index);
if (dest) {
const gchar *str = e_destination_get_textrep (dest, FALSE);
gint j;
if (out)
fprintf (out, "str=%s pos=%d\n", str, pos);
for (j=0; j<pos-start_pos && str[j]; ++j)
if (str[j] == '"') {
inside_quote = !inside_quote;
if (out)
fprintf (out, "flip to %d at %d\n", start_pos+j, inside_quote);
}
}
if (out)
fprintf (out, inside_quote ? "inside quote\n" : "not inside quote\n");
}
if (text[i] == *text_model->sep && !inside_quote) {
/* This is the case of hitting , first thing in an empty entry */
if (index == -1) {
EReposAbsolute repos;
e_select_names_model_insert (source, 0, e_destination_new ());
e_select_names_model_insert (source, 0, e_destination_new ());
repos.model = model;
repos.pos = -1; /* At end */
e_text_model_reposition (model, e_repos_absolute, &repos);
} else if (pos <= start_pos || pos == start_pos + text_len) {
EReposInsertShift repos;
gint ins_point = index;
if (text_len != 0 && pos == start_pos + text_len)
++ins_point;
/* Block adjacent blank cards. */
if (! ((ins_point < e_select_names_model_count (source) &&
(e_select_names_model_get_string (source, ins_point) == NULL))
|| (ins_point > 0 && (e_select_names_model_get_string (source, ins_point-1) == NULL)))) {
e_select_names_model_insert (source, ins_point, e_destination_new ());
repos.model = model;
repos.pos = pos;
repos.len = text_model->seplen;
e_text_model_reposition (model, e_repos_insert_shift, &repos);
pos += text_model->seplen;
}
} else {
EReposInsertShift repos;
gint offset = MAX (pos - start_pos, 0);
const gchar *str = e_select_names_model_get_string (source, index);
gchar *str1 = g_strndup (str, offset);
gchar *str2 = g_strdup (str+offset);
EDestination *d1 = e_destination_new (), *d2 = e_destination_new ();
e_destination_set_raw (d1, str1);
e_destination_set_raw (d2, str2);
e_select_names_model_replace (source, index, d1);
e_select_names_model_insert (source, index+1, d2);
g_free (str1);
g_free (str2);
repos.model = model;
repos.pos = pos;
repos.len = text_model->seplen;
e_text_model_reposition (model, e_repos_insert_shift, &repos);
pos += text_model->seplen;
}
if (text_model->seplen > 1)
text_model->last_magic_comma_pos = pos;
} else {
EReposInsertShift repos;
gint offset = MAX (pos - start_pos, 0);
const gchar *str;
gchar *new_str = NULL;
gint this_length = 1;
gboolean whitespace = isspace ((gint) text[i]);
str = index >= 0 ? e_select_names_model_get_string (source, index) : NULL;
if (str && *str) {
if (pos <= start_pos) {
if (whitespace) {
/* swallow leading whitespace */
this_length = 0;
} else {
/* Adjust for our "magic white space" */
/* FIXME: This code does the wrong thing if seplen > 2 */
new_str = g_strdup_printf("%c%s%s", text[i], pos < start_pos ? " " : "", str);
if (pos < start_pos)
++this_length;
}
} else {
new_str = g_strdup_printf ("%.*s%c%s", offset, str, text[i], str + offset);
}
} else {
if (whitespace) {
/* swallow leading whitespace */
this_length = 0;
} else {
new_str = g_strdup_printf ("%c", text[i]);
}
}
if (new_str) {
EDestination *dest;
dest = index >= 0 ? e_destination_copy (e_select_names_model_get_destination (source, index)) : e_destination_new ();
e_destination_set_raw (dest, new_str);
e_select_names_model_replace (source, index, dest);
/* e_select_names_model_replace (source, index, dest); */
if (this_length > 0) {
repos.model = model;
repos.pos = pos;
repos.len = this_length;
e_text_model_reposition (model, e_repos_insert_shift, &repos);
pos += this_length;
}
g_free (new_str);
}
}
}
dump_model (text_model);
g_signal_handler_unblock (source, text_model->source_resize_id);
}
static void
e_select_names_text_model_delete (ETextModel *model, gint pos, gint length)
{
ESelectNamesTextModel *text_model = E_SELECT_NAMES_TEXT_MODEL (model);
ESelectNamesModel *source = text_model->source;
gint index, start_pos, text_len, offset;
if (out) {
const gchar *str = e_select_names_model_get_textification (source, text_model->sep);
gint i, len;
fprintf (out, ">> delete %d at pos %d\n", length, pos);
len = strlen (str);
for (i=0; i<pos && i<len; ++i)
fprintf (out, "%c", str[i]);
fprintf (out, "[");
for (i=pos; i<pos+length && i<len; ++i)
fprintf (out, "%c", str[i]);
fprintf (out, "]");
for (i=pos+length; i<len; ++i)
fprintf (out, "%c", str[i]);
fprintf (out, "\n");
}
if (length < 0)
return;
if (text_model->last_magic_comma_pos == pos+1 && length == 1) {
pos -= text_model->seplen-1;
if (pos >= 0)
length = text_model->seplen;
text_model->last_magic_comma_pos = -1;
}
e_select_names_model_text_pos (source, text_model->seplen, pos, &index, &start_pos, &text_len);
if (out)
fprintf (out, "index=%d, start_pos=%d, text_len=%d\n", index, start_pos, text_len);
/* We want to control all cursor motions ourselves, rather than taking hints
from the ESelectNamesModel. */
g_signal_handler_block (source, E_SELECT_NAMES_TEXT_MODEL (model)->source_resize_id);
/* First, we handle a few tricky cases. */
if (pos < start_pos) {
EReposAbsolute repos;
repos.model = model;
repos.pos = pos;
e_text_model_reposition (model, e_repos_absolute, &repos);
length -= start_pos - pos;
if (length > 0)
e_select_names_text_model_delete (model, start_pos, length);
goto finished;
}
if (pos == start_pos + text_len) {
/* We are positioned right at the end of an entry, possibly right in front of a comma. */
if (index+1 < e_select_names_model_count (source)) {
EReposDeleteShift repos;
EDestination *new_dest;
const gchar *str1 = e_select_names_model_get_string (source, index);
const gchar *str2 = e_select_names_model_get_string (source, index+1);
gchar *new_str;
while (str1 && *str1 && isspace ((gint) *str1))
++str1;
while (str2 && *str2 && isspace ((gint) *str2))
++str2;
if (str1 && str2)
new_str = g_strdup_printf ("%s%s%s", str1, text_model->sep+1, str2);
else if (str1)
new_str = g_strdup (str1);
else if (str2)
new_str = g_strdup (str2);
else
new_str = g_strdup ("");
if (out)
fprintf (out, "joining \"%s\" and \"%s\" to \"%s\"\n", str1, str2, new_str);
e_select_names_model_delete (source, index+1);
new_dest = e_destination_new ();
e_destination_set_raw (new_dest, new_str);
e_select_names_model_replace (source, index, new_dest);
g_free (new_str);
repos.model = model;
repos.pos = pos;
repos.len = text_model->seplen;
e_text_model_reposition (model, e_repos_delete_shift, &repos);
if (length > 1)
e_select_names_text_model_delete (model, pos, length-1);
} else {
/* If we are at the end of the last entry (which we must be if we end up in this block),
we can just do nothing. So this else-block is here just to give us a place to
put this comment. */
}
goto finished;
}
if (pos + length > start_pos + text_len) {
/* Uh oh... our changes straddle two objects. */
if (pos == start_pos) { /* Delete the whole thing */
EReposDeleteShift repos;
e_select_names_model_delete (source, index);
if (out)
fprintf (out, "deleted all of %d\n", index);
repos.model = model;
repos.pos = pos;
repos.len = text_len + text_model->seplen;
e_text_model_reposition (model, e_repos_delete_shift, &repos);
length -= text_len + text_model->seplen;
if (length > 0)
e_select_names_text_model_delete (model, pos, length);
} else {
/* Delete right up to the end, and then call e_select_names_text_model_delete again
to finish the job. */
gint len1, len2;
len1 = text_len - (pos - start_pos);
len2 = length - len1;
if (out)
fprintf (out, "two-stage delete: %d, %d\n", len1, len2);
e_select_names_text_model_delete (model, pos, len1);
e_select_names_text_model_delete (model, pos, len2);
}
goto finished;
}
/* Our changes are confined to just one entry. */
if (length > 0) {
const gchar *str;
gchar *new_str;
offset = pos - start_pos;
str = e_select_names_model_get_string (source, index);
new_str = str ? g_strdup_printf ("%.*s%s", offset, str, str + offset + length) : NULL;
if (new_str) {
EReposDeleteShift repos;
EDestination *dest;
dest = index >= 0 ? e_destination_copy (e_select_names_model_get_destination (source, index)) : e_destination_new ();
e_destination_set_raw (dest, new_str);
e_select_names_model_replace (source, index, dest);
if (out)
fprintf (out, "new_str: \"%s\"\n", new_str);
g_free (new_str);
repos.model = model;
repos.pos = pos;
repos.len = length;
e_text_model_reposition (model, e_repos_delete_shift, &repos);
} else {
EReposDeleteShift repos;
e_select_names_model_delete (source, index);
if (out)
fprintf (out, "deleted %d\n", index);
repos.model = model;
repos.pos = pos;
repos.len = text_model->seplen;
e_text_model_reposition (model, e_repos_delete_shift, &repos);
}
}
finished:
E_SELECT_NAMES_TEXT_MODEL (model)->last_magic_comma_pos = -1;
g_signal_handler_unblock (source, E_SELECT_NAMES_TEXT_MODEL (model)->source_resize_id);
dump_model (E_SELECT_NAMES_TEXT_MODEL (model));
}
static gint
e_select_names_text_model_obj_count (ETextModel *model)
{
ESelectNamesModel *source = E_SELECT_NAMES_TEXT_MODEL (model)->source;
gint i, count;
count = i = e_select_names_model_count (source);
while (i > 0) {
const EDestination *dest;
--i;
dest = e_select_names_model_get_destination (source, i);
if (e_destination_get_card (dest) == NULL)
--count;
}
return count;
}
static gint
nth_obj_index (ESelectNamesModel *source, gint n)
{
gint i, N;
i = 0;
N = e_select_names_model_count (source);
do {
const EDestination *dest = e_select_names_model_get_destination (source, i);
if (e_destination_get_card (dest))
--n;
++i;
} while (n >= 0 && i < N);
if (i <= N)
--i;
else
i = -1;
return i;
}
static const gchar *
e_select_names_text_model_get_nth_obj (ETextModel *model, gint n, gint *len)
{
ESelectNamesTextModel *text_model = E_SELECT_NAMES_TEXT_MODEL (model);
ESelectNamesModel *source = text_model->source;
gint i, pos;
i = nth_obj_index (source, n);
if (i < 0)
return NULL;
e_select_names_model_name_pos (source, text_model->seplen, i, &pos, len);
if (pos < 0)
return NULL;
if (text_model->text == NULL)
text_model->text = e_select_names_model_get_textification (source, text_model->sep);
return text_model->text + pos;
}
static void
e_select_names_text_model_activate_obj (ETextModel *model, gint n)
{
ESelectNamesModel *source = E_SELECT_NAMES_TEXT_MODEL (model)->source;
ECard *card;
gint i;
i = nth_obj_index (source, n);
g_return_if_fail (i >= 0);
card = e_select_names_model_get_card (source, i);
g_return_if_fail (card != NULL);
/* present read-only contact editor when someone double clicks from here */
if (e_card_evolution_list (card)) {
EContactListEditor *ce;
ce = e_addressbook_show_contact_list_editor (e_card_get_book(card), card, FALSE, FALSE);
e_contact_list_editor_raise (ce);
}
else {
EContactEditor *ce;
ce = e_addressbook_show_contact_editor (e_card_get_book(card), card, FALSE, FALSE);
e_contact_editor_raise (ce);
}
}