* Talkatu - GTK widgets for chat applications
* Copyright (C) 2017-2020 Gary Kramlich <grim@reaperworld.com>
* This library 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 library 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 library; if not, see <https://www.gnu.org/licenses/>.
#include <glib/gi18n-lib.h>
#include "talkatu/talkatuactiongroup.h"
#include "talkatu/talkatuattachment.h"
#include "talkatu/talkatuattachmentdialog.h"
#include "talkatu/talkatubuffer.h"
#include "talkatu/talkatulinkdialog.h"
#include "talkatu/talkatumarkup.h"
#include "talkatu/talkatumessage.h"
#include "talkatu/talkatusimpleattachment.h"
#include "talkatu/talkatutag.h"
* A #GSimpleActionGroup subclass that contains all of the actions for
* formatting text in Talkatu. They are typically created by
* #TalkatuBuffer subclasses.
* TalkatuActionGroupClass:
* @action_activated: The class handler for the
* #TalkatuActionGroup::action-activated signal.
* The backing class of a #TalkatuActionGroup.
* TALKATU_ACTION_FORMAT_BOLD:
* A constant that represents the bold font style action.
* TALKATU_ACTION_FORMAT_ITALIC:
* A constant that represents the italic font style action.
* TALKATU_ACTION_FORMAT_UNDERLINE:
* A constant that represents the underline font style action.
* TALKATU_ACTION_FORMAT_STRIKETHROUGH:
* A constant that represents the strike through font style action.
* TALKATU_ACTION_FORMAT_GROW:
* A constant that represents the increase font size action.
* TALKATU_ACTION_FORMAT_SHRINK:
* A constant that represents the decrease font size action.
* TALKATU_ACTION_FORMAT_RESET:
* A constant that represents the reset all formatting action.
* TALKATU_ACTION_ATTACH_FILE:
* A constant that represents the attach file action.
* TALKATU_ACTION_INSERT_LINK:
* A constant that presents the action to activate when the user wants to
} TalkatuActionGroupPrivate;
static GParamSpec *properties[N_PROPERTIES] = {NULL,};
static guint signals[LAST_SIGNAL] = {0, };
G_DEFINE_TYPE_WITH_PRIVATE(TalkatuActionGroup, talkatu_action_group, G_TYPE_SIMPLE_ACTION_GROUP)
/******************************************************************************
*****************************************************************************/
talkatu_action_group_set_buffer(TalkatuActionGroup *ag, GtkTextBuffer *buffer) {
TalkatuActionGroupPrivate *priv = talkatu_action_group_get_instance_private(ag);
g_return_if_fail(TALKATU_IS_BUFFER(buffer));
g_set_object(&priv->buffer, buffer);
talkatu_action_group_emit_action_activated(TalkatuActionGroup *ag, GAction *action) {
g_object_get(G_OBJECT(action), "name", &name, NULL);
g_signal_emit(ag, signals[SIG_ACTION_ACTIVATED], 0, action, name);
talkatu_action_group_action_activated(GSimpleAction *action,
TalkatuActionGroup *ag = TALKATU_ACTION_GROUP(data);
talkatu_action_group_emit_action_activated(ag, G_ACTION(action));
/******************************************************************************
*****************************************************************************/
talkatu_action_group_action_added(GActionGroup *ag,
GAction *action = g_action_map_lookup_action(G_ACTION_MAP(ag), name);
guint sig_id = g_signal_connect(
G_CALLBACK(talkatu_action_group_action_activated),
"talkatu-action-group-activate-signal-id",
talkatu_action_group_action_removed(GActionGroup *ag,
GAction *action = g_action_map_lookup_action(G_ACTION_MAP(ag), name);
guint sig_id = GPOINTER_TO_UINT(g_object_get_data(
"talkatu-action-group-activate-signal-id"
g_signal_handler_disconnect(G_OBJECT(action), sig_id);
talkatu_action_activate(GSimpleAction *action,
g_message(_("activating action %s"), g_action_get_name(G_ACTION(action)));
talkatu_action_toggle(GSimpleAction *action,
g_message(_("toggling action %s"), g_action_get_name(G_ACTION(action)));
state = g_action_get_state(G_ACTION(action));
g_variant_new_boolean(!g_variant_get_boolean(state))
talkatu_action_format_toggle(GSimpleAction *action, GVariant *state,
TalkatuActionGroup *ag = TALKATU_ACTION_GROUP(data);
TalkatuActionGroupPrivate *priv = NULL;
TalkatuBufferStyle style;
priv = talkatu_action_group_get_instance_private(ag);
style = talkatu_buffer_get_style(TALKATU_BUFFER(priv->buffer));
if(style == TALKATU_BUFFER_STYLE_WHOLE) {
gtk_text_buffer_get_bounds(priv->buffer, &start, &end);
if(gtk_text_buffer_get_selection_bounds(priv->buffer, &start, &end)) {
const gchar *tag_name = NULL, *name = NULL;
name = g_action_get_name(G_ACTION(action));
tag_name = talkatu_tag_name_for_action_name(name);
gtk_text_buffer_begin_user_action(priv->buffer);
if(g_variant_get_boolean(state)) {
gtk_text_buffer_apply_tag_by_name(priv->buffer, tag_name, &start, &end);
gtk_text_buffer_remove_tag_by_name(priv->buffer, tag_name, &start, &end);
gtk_text_buffer_end_user_action(priv->buffer);
g_simple_action_set_state(action, state);
talkatu_action_reset_activate(GSimpleAction *act, GVariant *parameter,
TalkatuActionGroup *ag = TALKATU_ACTION_GROUP(data);
TalkatuActionGroupPrivate *priv = NULL;
TalkatuBufferStyle style;
gchar **actions = NULL, **action = NULL;
priv = talkatu_action_group_get_instance_private(ag);
style = talkatu_buffer_get_style(TALKATU_BUFFER(priv->buffer));
/* if there's no selection or this is a whole buffer format, select the
if(!gtk_text_buffer_get_selection_bounds(priv->buffer, &start, &end) || style == TALKATU_BUFFER_STYLE_WHOLE) {
gtk_text_buffer_get_bounds(priv->buffer, &start, &end);
gtk_text_buffer_select_range(priv->buffer, &start, &end);
/* run through all of the talkatu tags and remove them from the
gtk_text_buffer_remove_tag_by_name(priv->buffer, TALKATU_TAG_BOLD, &start, &end);
gtk_text_buffer_remove_tag_by_name(priv->buffer, TALKATU_TAG_ITALIC, &start, &end);
gtk_text_buffer_remove_tag_by_name(priv->buffer, TALKATU_TAG_UNDERLINE, &start, &end);
gtk_text_buffer_remove_tag_by_name(priv->buffer, TALKATU_TAG_STRIKETHROUGH, &start, &end);
gtk_text_buffer_remove_tag_by_name(priv->buffer, TALKATU_TAG_SUBSCRIPT, &start, &end);
gtk_text_buffer_remove_tag_by_name(priv->buffer, TALKATU_TAG_SUPERSCRIPT, &start, &end);
gtk_text_buffer_remove_tag_by_name(priv->buffer, TALKATU_TAG_PRE, &start, &end);
gtk_text_buffer_remove_tag_by_name(priv->buffer, TALKATU_TAG_CODE, &start, &end);
gtk_text_buffer_remove_tag_by_name(priv->buffer, TALKATU_TAG_SEARCH, &start, &end);
gtk_text_buffer_remove_tag_by_name(priv->buffer, TALKATU_TAG_H1, &start, &end);
gtk_text_buffer_remove_tag_by_name(priv->buffer, TALKATU_TAG_H2, &start, &end);
gtk_text_buffer_remove_tag_by_name(priv->buffer, TALKATU_TAG_H3, &start, &end);
gtk_text_buffer_remove_tag_by_name(priv->buffer, TALKATU_TAG_H4, &start, &end);
gtk_text_buffer_remove_tag_by_name(priv->buffer, TALKATU_TAG_H5, &start, &end);
gtk_text_buffer_remove_tag_by_name(priv->buffer, TALKATU_TAG_H6, &start, &end);
gtk_text_buffer_remove_tag_by_name(priv->buffer, TALKATU_TAG_ANCHOR, &start, &end);
/* Now run through all the actions and deactivate any that are active by
actions = g_action_group_list_actions(G_ACTION_GROUP(ag));
for(action = actions; *action != NULL; action++) {
const GVariantType *state_type = NULL;
gboolean enabled = FALSE;
g_action_group_query_action(
if(g_variant_type_equal(state_type, G_VARIANT_TYPE_BOOLEAN)) {
if(g_variant_get_boolean(state)) {
GAction *real_action = g_action_map_lookup_action(G_ACTION_MAP(ag), *action);
g_action_activate(real_action, NULL);
talkatu_action_attach_file_attach_response_cb(GtkDialog *dialog, gint response,
if(response == GTK_RESPONSE_CANCEL) {
/* we call this separately for GTK_RESPONSE_CANCEL because
* GTK_RESPONSE_DELETE_EVENT already destroys the dialog.
gtk_widget_destroy(GTK_WIDGET(dialog));
} else if(response == GTK_RESPONSE_ACCEPT) {
TalkatuActionGroup *ag = TALKATU_ACTION_GROUP(data);
TalkatuActionGroupPrivate *priv = NULL;
TalkatuAttachment *attachment = NULL;
TalkatuAttachmentDialog *adialog = TALKATU_ATTACHMENT_DIALOG(dialog);
const gchar *comment = NULL;
priv = talkatu_action_group_get_instance_private(ag);
/* Set the message to the comment from the dialog. */
comment = talkatu_attachment_dialog_get_comment(adialog);
talkatu_markup_set_html(TALKATU_BUFFER(priv->buffer), comment, -1);
/* Add the attachment to the message */
attachment = talkatu_attachment_dialog_get_attachment(adialog);
talkatu_message_add_attachment(TALKATU_MESSAGE(priv->input), attachment);
g_object_unref(G_OBJECT(attachment));
talkatu_input_send_message(priv->input);
gtk_widget_destroy(GTK_WIDGET(dialog));
talkatu_action_attach_file_response_cb(GtkDialog *dialog, gint response,
if(response == GTK_RESPONSE_CANCEL) {
/* we call this separately for GTK_RESPONSE_CANCEL because
* GTK_RESPONSE_DELETE_EVENT already destroys the dialog.
gtk_widget_destroy(GTK_WIDGET(dialog));
} else if(response == GTK_RESPONSE_ACCEPT) {
TalkatuActionGroup *ag = TALKATU_ACTION_GROUP(data);
TalkatuActionGroupPrivate *priv = NULL;
TalkatuAttachment *attachment = NULL;
GtkWidget *attach_dialog = NULL;
gchar *filename = NULL, *content_type = NULL, *comment = NULL;
priv = talkatu_action_group_get_instance_private(ag);
filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
content_type = g_content_type_guess(filename, NULL, 0, NULL);
attachment = talkatu_simple_attachment_new(G_GUINT64_CONSTANT(0),
talkatu_attachment_set_local_uri(attachment, filename);
gtk_widget_destroy(GTK_WIDGET(dialog));
comment = talkatu_markup_get_html(priv->buffer, NULL);
attach_dialog = talkatu_attachment_dialog_new(attachment, comment);
g_signal_connect(G_OBJECT(attach_dialog), "response",
G_CALLBACK(talkatu_action_attach_file_attach_response_cb),
gtk_widget_show_all(attach_dialog);
talkatu_action_attach_file(GSimpleAction *action, GVariant *parameter,
TalkatuActionGroup *ag = TALKATU_ACTION_GROUP(data);
TalkatuActionGroupPrivate *priv = NULL;
GtkWidget *dialog = NULL;
priv = talkatu_action_group_get_instance_private(ag);
g_return_if_fail(TALKATU_IS_INPUT(priv->input));
dialog = gtk_file_chooser_dialog_new(_("Attach file..."),
GTK_FILE_CHOOSER_ACTION_OPEN,
_("Cancel"), GTK_RESPONSE_CANCEL,
_("Open"), GTK_RESPONSE_ACCEPT,
g_signal_connect(G_OBJECT(dialog), "response",
G_CALLBACK(talkatu_action_attach_file_response_cb),
gtk_widget_show_all(dialog);
talkatu_action_insert_link(GSimpleAction *action, GVariant *parameter,
TalkatuActionGroup *ag = TALKATU_ACTION_GROUP(data);
TalkatuActionGroupPrivate *priv = NULL;
GtkWidget *link_dialog = talkatu_link_dialog_new();
priv = talkatu_action_group_get_instance_private(ag);
if(gtk_dialog_run(GTK_DIALOG(link_dialog)) == GTK_RESPONSE_ACCEPT) {
GtkTextTagTable *table = gtk_text_buffer_get_tag_table(priv->buffer);
GtkTextTag *anchor, *anchor_data;
GtkTextMark *insert_mark = NULL;
gchar *label = NULL, *url = NULL;
/* if any text is selected, delete it */
if(gtk_text_buffer_get_has_selection(priv->buffer)) {
gtk_text_buffer_delete_selection(priv->buffer, TRUE, TRUE);
/* grab our inputs from the dialog */
url = talkatu_link_dialog_get_url(TALKATU_LINK_DIALOG(link_dialog));
label = talkatu_link_dialog_get_display_text(TALKATU_LINK_DIALOG(link_dialog));
/* find the anchor tag from the table */
anchor = gtk_text_tag_table_lookup(table, TALKATU_TAG_ANCHOR);
g_return_if_fail(GTK_IS_TEXT_TAG(anchor));
/* now create an anonymous tag that will hold the url.
* This should probably be dedupped at some point, but pidgin 2 didn't
anchor_data = gtk_text_tag_new(NULL);
g_object_set_data_full(G_OBJECT(anchor_data), "talkatu-anchor-url", url, g_free);
gtk_text_tag_table_add(table, anchor_data);
insert_mark = gtk_text_buffer_get_insert(priv->buffer);
gtk_text_buffer_get_iter_at_mark(priv->buffer, &insert, insert_mark);
gtk_text_buffer_insert_with_tags(
gtk_widget_destroy(link_dialog);
/******************************************************************************
*****************************************************************************/
talkatu_action_group_get_property(GObject *obj,
TalkatuActionGroup *ag = TALKATU_ACTION_GROUP(obj);
g_value_set_object(value, talkatu_action_group_get_buffer(ag));
g_value_set_object(value, talkatu_action_group_get_input(ag));
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
talkatu_action_group_set_property(GObject *obj,
TalkatuActionGroup *ag = TALKATU_ACTION_GROUP(obj);
talkatu_action_group_set_buffer(ag, g_value_get_object(value));
talkatu_action_group_set_input(ag, g_value_get_object(value));
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
talkatu_action_group_finalize(GObject *obj) {
TalkatuActionGroupPrivate *priv = talkatu_action_group_get_instance_private(TALKATU_ACTION_GROUP(obj));
g_clear_object(&priv->buffer);
g_clear_object(&priv->input);
G_OBJECT_CLASS(talkatu_action_group_parent_class)->finalize(obj);
talkatu_action_group_constructed(GObject *object) {
TalkatuActionGroup *ag = TALKATU_ACTION_GROUP(object);
GActionEntry entries[] = {
.name = TALKATU_ACTION_FORMAT_BOLD,
.activate = talkatu_action_toggle,
.change_state = talkatu_action_format_toggle
.name = TALKATU_ACTION_FORMAT_ITALIC,
.activate = talkatu_action_toggle,
.change_state = talkatu_action_format_toggle
.name = TALKATU_ACTION_FORMAT_UNDERLINE,
.activate = talkatu_action_toggle,
.change_state = talkatu_action_format_toggle
.name = TALKATU_ACTION_FORMAT_STRIKETHROUGH,
.activate = talkatu_action_toggle,
.change_state = talkatu_action_format_toggle
.name = TALKATU_ACTION_FORMAT_GROW,
.activate = talkatu_action_activate,
.name = TALKATU_ACTION_FORMAT_SHRINK,
.activate = talkatu_action_activate,
.name = TALKATU_ACTION_FORMAT_RESET,
.activate = talkatu_action_reset_activate,
.name = TALKATU_ACTION_ATTACH_FILE,
.activate = talkatu_action_attach_file,
.name = TALKATU_ACTION_INSERT_LINK,
.activate = talkatu_action_insert_link,
.change_state = talkatu_action_format_toggle,
G_CALLBACK(talkatu_action_group_action_added),
G_CALLBACK(talkatu_action_group_action_removed),
g_action_map_add_action_entries(G_ACTION_MAP(ag), entries,
G_N_ELEMENTS(entries), ag);
/* disable all of the actions by default */
for(i = 0; i < G_N_ELEMENTS(entries); i++) {
action = g_action_map_lookup_action(G_ACTION_MAP(ag), entries[i].name);
g_simple_action_set_enabled(G_SIMPLE_ACTION(action), FALSE);
talkatu_action_group_init(TalkatuActionGroup *group) {
talkatu_action_group_class_init(TalkatuActionGroupClass *klass) {
GObjectClass *obj_class = G_OBJECT_CLASS(klass);
obj_class->constructed = talkatu_action_group_constructed;
obj_class->get_property = talkatu_action_group_get_property;
obj_class->set_property = talkatu_action_group_set_property;
obj_class->finalize = talkatu_action_group_finalize;
/* setup our properties */
* TalkatuActionGroup::buffer:
* The #TalkatuBuffer that this action group is tied to.
properties[PROP_BUFFER] = g_param_spec_object(
"buffer", "buffer", "The buffer to work on",
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS
* TalkatuActionGroup::input:
* The #TalkatuInput that this action group is tied to.
properties[PROP_INPUT] = g_param_spec_object(
"input", "input", "The input to work on",
G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS
g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
* TalkatuActionGroup::action-activated
* @talkatuactiongroup: The #TalkatuActionGroup instance.
* @arg1: The #GAction that was activated.
* @user_data: User supplied data.
* Emitted when one of the actions in @talkatuactiongroup are activated.
* This is a convenience signal so people don't have to connect to every
signals[SIG_ACTION_ACTIVATED] = g_signal_new(
G_TYPE_FROM_CLASS(klass),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(TalkatuActionGroupClass, action_activated),
/******************************************************************************
*****************************************************************************/
* talkatu_action_group_new:
* @buffer: The #GtkTextBuffer to bind this action group to.
* Creates a new #TalkatuActionGroup bound to @buffer.
* Returns: (transfer full): The new #TalkatuActionGroup instance.
talkatu_action_group_new(GtkTextBuffer *buffer) {
TALKATU_TYPE_ACTION_GROUP,
* talkatu_action_group_get_buffer:
* @ag: The #TalkatuActionGroup instance.
* #TalkatuActionGroup's are bound to a specific #GtkTextBuffer. This function
* return the one that @ag is bound to.
* Returns: (transfer none): The #GtkTextBuffer that @ag is bound to.
talkatu_action_group_get_buffer(TalkatuActionGroup *ag) {
TalkatuActionGroupPrivate *priv = NULL;
g_return_val_if_fail(TALKATU_IS_ACTION_GROUP(ag), NULL);
priv = talkatu_action_group_get_instance_private(ag);
* talkatu_action_group_set_input:
* @ag: The #TalkatuActionGroup instance.
* @input: A #TalkatuInput instance or %NULL.
* Binds @ag to @input for actions that need to work against the input widget.
talkatu_action_group_set_input(TalkatuActionGroup *ag, TalkatuInput *input) {
TalkatuActionGroupPrivate *priv = NULL;
g_return_if_fail(TALKATU_IS_ACTION_GROUP(ag));
priv = talkatu_action_group_get_instance_private(ag);
if(g_set_object(&priv->input, input)) {
g_object_notify_by_pspec(G_OBJECT(ag), properties[PROP_INPUT]);
* talkatu_action_group_get_input:
* @ag: The #TalkatuActionGroup instance.
* Gets the #TalkatuInput bound to @ag.
* Returns: (transfer none): The #TalkatuInput that @ag is bound to.
talkatu_action_group_get_input(TalkatuActionGroup *ag) {
TalkatuActionGroupPrivate *priv = NULL;
g_return_val_if_fail(TALKATU_IS_ACTION_GROUP(ag), NULL);
priv = talkatu_action_group_get_instance_private(ag);
* talkatu_action_group_enable_action:
* @ag: The #TalkatuActionGroup instance.
* @name: The name of the action to enable.
* By default a #TalkatuActionGroup's actions are all disabled. To enable them
* you must call this function one at time.
talkatu_action_group_enable_action(TalkatuActionGroup *ag, const gchar *name) {
g_return_if_fail(TALKATU_IS_ACTION_GROUP(ag));
g_return_if_fail(name != NULL);
action = g_action_map_lookup_action(G_ACTION_MAP(ag), name);
g_simple_action_set_enabled(G_SIMPLE_ACTION(action), TRUE);
* talkatu_action_name_for_tag_name:
* @tag_name: The name of the tag to lookup.
* Looks up a #GAction for the the tag named @tag_name.
* Returns: The #GAction if one is found, otherwise %NULL.
talkatu_action_name_for_tag_name(const gchar *tag_name) {
if(g_ascii_strcasecmp(tag_name, TALKATU_TAG_BOLD) == 0) {
return TALKATU_ACTION_FORMAT_BOLD;
} else if(g_ascii_strcasecmp(tag_name, TALKATU_TAG_ITALIC) == 0) {
return TALKATU_ACTION_FORMAT_ITALIC;
} else if(g_ascii_strcasecmp(tag_name, TALKATU_TAG_UNDERLINE) == 0) {
return TALKATU_ACTION_FORMAT_UNDERLINE;
} else if(g_ascii_strcasecmp(tag_name, TALKATU_TAG_STRIKETHROUGH) == 0) {
return TALKATU_ACTION_FORMAT_STRIKETHROUGH;
} else if(g_ascii_strcasecmp(tag_name, TALKATU_TAG_ANCHOR) == 0) {
return TALKATU_ACTION_INSERT_LINK;
* talkatu_action_group_activate_format:
* @ag: The #TalkatuActionGroup instance.
* @format_name: The name of the format to activate.
* Activates action named @format_name. This will apply it to a selection if
talkatu_action_group_activate_format(TalkatuActionGroup *ag, const gchar *format_name) {
g_return_if_fail(TALKATU_IS_ACTION_GROUP(ag));
action = g_action_map_lookup_action(G_ACTION_MAP(ag), format_name);
g_action_activate(action, NULL);
* talkatu_action_group_get_action_activated:
* @ag: The #TalkatuActionGroup instance.
* @name: The name of the action to check.
* Returns that state of the action named @name. If @name doesn't exist or is
* not a toggle action, FALSE is returned.
* Returns: Whether or not @name is toggled.
talkatu_action_group_get_action_activated(TalkatuActionGroup *ag, const gchar *name) {
const GVariantType *state_type = NULL;
gboolean enabled = FALSE;
g_return_val_if_fail(TALKATU_IS_ACTION_GROUP(ag), FALSE);
g_return_val_if_fail(name != NULL, FALSE);
g_action_group_query_action(
if(g_variant_type_equal(state_type, G_VARIANT_TYPE_BOOLEAN)) {
return g_variant_get_boolean(state);
* talkatu_action_group_get_activated_formats:
* @ag: The #TalkatuActionGroup instance.
* Returns an array of all actions that are activated. This values must be
* free'd with g_strfreev.
* Returns: (transfer full): A list of actions that are activated.
talkatu_action_group_get_activated_formats(TalkatuActionGroup *ag) {
GSList *activated = NULL, *l = NULL;
gchar **actions = NULL, **action = NULL;
actions = g_action_group_list_actions(G_ACTION_GROUP(ag));
for(action = actions; *action != NULL; action++) {
if(talkatu_action_group_get_action_activated(ag, *action)) {
activated = g_slist_prepend(activated, *action);
ret = g_new(gchar *, g_slist_length(activated) + 1);
for(l = activated, idx = 0; l != NULL; l = l->next, idx++) {
ret[idx] = (gchar *)l->data;