aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--configure.ac2
-rw-r--r--extensions/Channel_Interface_Conference.xml400
-rw-r--r--extensions/misc.xml1
-rw-r--r--libempathy-gtk/empathy-contact-menu.c9
-rw-r--r--libempathy-gtk/empathy-contact-selector-dialog.c63
-rw-r--r--libempathy-gtk/empathy-contact-selector-dialog.h2
-rw-r--r--libempathy/empathy-dispatcher.c13
-rw-r--r--libempathy/empathy-tp-chat.c109
-rw-r--r--libempathy/empathy-tp-chat.h1
-rw-r--r--src/Makefile.am1
-rw-r--r--src/empathy-chat-window.c99
-rw-r--r--src/empathy-chat-window.ui7
-rw-r--r--src/empathy-invite-participant-dialog.c65
-rw-r--r--src/empathy-invite-participant-dialog.h46
14 files changed, 784 insertions, 34 deletions
diff --git a/configure.ac b/configure.ac
index 6085e9b35..363e70fb6 100644
--- a/configure.ac
+++ b/configure.ac
@@ -32,7 +32,7 @@ AC_COPYRIGHT([
GLIB_REQUIRED=2.22.0
GTK_REQUIRED=2.18.0
GCONF_REQUIRED=1.2.0
-TELEPATHY_GLIB_REQUIRED=0.9.0
+TELEPATHY_GLIB_REQUIRED=0.9.2
ENCHANT_REQUIRED=1.2.0
ISO_CODES_REQUIRED=0.35
LIBNOTIFY_REQUIRED=0.4.4
diff --git a/extensions/Channel_Interface_Conference.xml b/extensions/Channel_Interface_Conference.xml
new file mode 100644
index 000000000..af3e627b9
--- /dev/null
+++ b/extensions/Channel_Interface_Conference.xml
@@ -0,0 +1,400 @@
+<?xml version="1.0" ?>
+<node name="/Channel_Interface_Conference"
+ xmlns:tp="http://telepathy.freedesktop.org/wiki/DbusSpec#extensions-v0">
+ <tp:copyright>Copyright © 2009 Collabora Limited</tp:copyright>
+ <tp:copyright>Copyright © 2009 Nokia Corporation</tp:copyright>
+ <tp:license xmlns="http://www.w3.org/1999/xhtml">
+ <p>This library 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.1 of the License, or (at your option) any later version.</p>
+
+ <p>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
+ Lesser General Public License for more details.</p>
+
+ <p>You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ 02110-1301, USA.</p>
+ </tp:license>
+ <interface
+ name="org.freedesktop.Telepathy.Channel.Interface.Conference.DRAFT"
+ tp:causes-havoc="experimental">
+ <tp:added version="0.19.0">(draft 1)</tp:added>
+ <tp:requires interface="org.freedesktop.Telepathy.Channel"/>
+ <tp:requires
+ interface="org.freedesktop.Telepathy.Channel.Interface.Group"/>
+
+ <tp:docstring xmlns="http://www.w3.org/1999/xhtml">
+ <p>An interface for multi-user conference channels that can "continue
+ from" one or more individual channels.</p>
+
+ <tp:rationale>
+ <p>This interface addresses freedesktop.org <a
+ href="http://bugs.freedesktop.org/show_bug.cgi?id=24906">bug
+ #24906</a> (GSM-compatible conference calls) and <a
+ href="http://bugs.freedesktop.org/show_bug.cgi?id=24939">bug
+ #24939</a> (upgrading calls and chats to multi-user).
+ See those bugs for rationale and use cases.</p>
+
+ <p>Examples of usage:</p>
+
+ <p>Active and held GSM calls C1, C2 can be merged into a single
+ channel Cn with the Conference interface, by calling
+ <code>CreateChannel({...ChannelType: ...Call,
+ ...<tp:member-ref>InitialChannels</tp:member-ref>: [C1, C2]})</code>
+ which returns Cn.</p>
+
+ <p>An XMPP 1-1 conversation C1 can be continued in a newly created
+ multi-user chatroom Cn by calling
+ <code>CreateChannel({...ChannelType: ...Text,
+ ...<tp:member-ref>InitialChannels</tp:member-ref>: [C1]})</code>
+ which returns Cn.</p>
+
+ <p>An XMPP 1-1 conversation C1 can be continued in a specified
+ multi-user chatroom by calling
+ <code>CreateChannel({...ChannelType: ...Text, ...HandleType: ROOM,
+ ...TargetID: 'telepathy@conf.example.com',
+ ...<tp:member-ref>InitialChannels</tp:member-ref>: [C1]})</code>
+ which returns a Conference channel.</p>
+
+ <p>Either of the XMPP cases could work for Call channels, to
+ upgrade from 1-1 Jingle to multi-user Muji. Any of the XMPP cases
+ could in principle work for link-local XMPP (XEP-0174).</p>
+
+ <p>The underlying switchboard representing an MSN 1-1 conversation C1
+ with a contact X can be moved to a representation as a nameless
+ chatroom, Cn, to which more contacts can be invited, by calling
+ <code>CreateChannel({...ChannelType: ...Text,
+ ...<tp:member-ref>InitialChannels</tp:member-ref>: [C1]})</code>
+ which returns Cn. C1 SHOULD remain open, with no underlying
+ switchboard attached. If X establishes a new switchboard with the
+ local user, C1 SHOULD pick up that switchboard rather than letting
+ it create a new channel.
+ <strong>[FIXME: should it?]</strong>
+ Similarly, if the local user sends a message in C1, then
+ a new switchboard to X should be created and associated with C1.</p>
+
+ <p>XMPP and MSN do not natively have a concept of merging two or more
+ channels C1, C2... into one channel, Cn. However, the GSM-style
+ merging API can be supported on XMPP and MSN, as an API short-cut
+ for upgrading C1 into a conference Cn (which invites the
+ TargetHandle of C1 into Cn), then immediately inviting the
+ TargetHandle of C2, the TargetHandle of C3, etc. into Cn as well.</p>
+
+ <p>With a suitable change of terminology, Skype has behaviour similar
+ to MSN.</p>
+ </tp:rationale>
+
+ <p>The <tp:dbus-ref
+ namespace="org.freedesktop.Telepathy.Channel.Interface"
+ >Group</tp:dbus-ref> MAY have channel-specific handles for participants;
+ clients SHOULD support both Conferences that have channel-specific handles,
+ and those that do not.</p>
+
+ <tp:rationale>
+ <p>In the GSM case, the Conference's Group interface MAY have
+ channel-specific handles, to reflect the fact that the identities of
+ the participants might not be known - it can be possible to know that
+ there is another participant in the Conference, but not know who
+ they are.
+ <strong>[FIXME: fact check from GSM gurus needed]</strong>
+ </p>
+
+ <p>In the XMPP case, the Conference's Group interface SHOULD have
+ channel-specific handles, to reflect the fact that the participants
+ have MUC-specific identities, and the user might also be able to see
+ their global identities, or not.</p>
+
+ <p>In most other cases, including MSN and link-local XMPP, the
+ Conference's Group interface SHOULD NOT have channel-specific
+ handles, since users' identities are always visible.</p>
+ </tp:rationale>
+
+ <p>Connection managers implementing channels with this interface
+ MUST NOT allow the object paths of channels that could be merged
+ into a Conference to be re-used, unless the channel re-using the
+ object path is equivalent to the channel that previously used it.</p>
+
+ <tp:rationale>
+ <p>If you upgrade some channels into a conference, and then close
+ the original channels, <tp:member-ref>InitialChannels</tp:member-ref>
+ (which is immutable) will contain paths to channels which no longer
+ exist. This implies that you should not re-use channel object paths,
+ unless future incarnations of the path are equivalent.</p>
+
+ <p>For instance, on protocols where you can only have
+ zero or one 1-1 text channels with Emily at one time, it would
+ be OK to re-use the same object path for every 1-1 text channel
+ with Emily; but on protocols where this is not true, it would
+ be misleading.</p>
+ </tp:rationale>
+
+ </tp:docstring>
+
+ <property name="Channels" tp:name-for-bindings="Channels"
+ access="read" type="ao">
+ <tp:docstring xmlns="http://www.w3.org/1999/xhtml">
+ <p>The individual <tp:dbus-ref
+ namespace="org.freedesktop.Telepathy">Channel</tp:dbus-ref>s that
+ are continued by this conference, which have the same <tp:dbus-ref
+ namespace="org.freedesktop.Telepathy.Channel"
+ >ChannelType</tp:dbus-ref> as this one, but with <tp:dbus-ref
+ namespace="org.freedesktop.Telepathy.Channel"
+ >TargetHandleType</tp:dbus-ref> = CONTACT.</p>
+
+ <p>This property MUST NOT be requestable.
+ <strong>[FIXME: or would it be better for this one, and not IC, to be
+ requestable?]</strong>
+ </p>
+
+ <p>Change notification is via the
+ <tp:member-ref>ChannelMerged</tp:member-ref> and
+ <tp:member-ref>ChannelRemoved</tp:member-ref> signals.</p>
+ </tp:docstring>
+ </property>
+
+ <signal name="ChannelMerged" tp:name-for-bindings="Channel_Merged">
+ <tp:docstring xmlns="http://www.w3.org/1999/xhtml">
+ <p>Emitted when a new channel is added to the value of
+ <tp:member-ref>Channels</tp:member-ref>.</p>
+ </tp:docstring>
+
+ <arg name="Channel" type="o">
+ <tp:docstring>The channel that was added to
+ <tp:member-ref>Channels</tp:member-ref>.</tp:docstring>
+ </arg>
+ </signal>
+
+ <signal name="ChannelRemoved" tp:name-for-bindings="Channel_Removed">
+ <tp:docstring xmlns="http://www.w3.org/1999/xhtml">
+ <p>Emitted when a channel is removed from the value of
+ <tp:member-ref>Channels</tp:member-ref>, either because it closed
+ or because it was split using the <tp:dbus-ref
+ namespace="org.freedesktop.Telepathy.Channel.Interface"
+ >Splittable.DRAFT.Split</tp:dbus-ref> method.</p>
+
+ <p><strong>[FIXME: relative ordering of this vs. Closed? Do we
+ care?]</strong></p>
+ </tp:docstring>
+
+ <arg name="Channel" type="o">
+ <tp:docstring>The channel that was removed from
+ <tp:member-ref>Channels</tp:member-ref>.</tp:docstring>
+ </arg>
+ </signal>
+
+ <property name="InitialChannels" tp:name-for-bindings="Initial_Channels"
+ access="read" type="ao">
+ <tp:docstring xmlns="http://www.w3.org/1999/xhtml">
+ <p>The initial value of <tp:member-ref>Channels</tp:member-ref>.</p>
+
+ <p>This property SHOULD be requestable. Omitting it from a request is
+ equivalent to providing it with an empty list as value. Requests
+ where its value has at least two elements SHOULD be expected to
+ succeed on any implementation of this interface.</p>
+
+ <p>Whether a request with 0 or 1 elements in the list will succeed is
+ indicated by <tp:member-ref>SupportsNonMerges</tp:member-ref>.</p>
+
+ <tp:rationale>
+ <p>In GSM, a pair of calls can be merged into a conference. In XMPP
+ and MSN, you can create a new chatroom, or upgrade one 1-1 channel
+ into a chatroom; however, on these protocols, it is also possible
+ to fake GSM-style merging by upgrading the first channel, then
+ inviting the targets of all the other channels into it.</p>
+ </tp:rationale>
+
+ <p>If possible, the <tp:member-ref>Channels</tp:member-ref>' states SHOULD
+ NOT be altered by merging them into a conference. However, depending on
+ the protocol, the Channels MAY be placed in a "frozen" state by placing
+ them in this property's value or by calling
+ <tp:dbus-ref
+ namespace="org.freedesktop.Telepathy.Channel.Interface"
+ >MergeableConference.DRAFT.Merge</tp:dbus-ref> on them.
+ <strong>[FIXME: there's nothing in RequestableChannelClasses yet
+ to say what will happen, see #24906 comment 6]</strong></p>
+
+ <tp:rationale>
+ <p>In Jingle, nothing special will happen to merged calls. UIs MAY
+ automatically place calls on hold before merging them, if that is
+ the desired behaviour; this SHOULD always work. Not doing
+ an implicit hold/unhold seems to preserve least-astonishment.</p>
+
+ <p><strong>[FIXME: check whether ring supports faking Hold on both
+ channels, as it probably should: see #24906 comment 6]</strong>
+ </p>
+
+ <p>In GSM, the calls that are merged go into a state similar to
+ Hold, but they cannot be unheld, only split from the conference
+ call using <tp:dbus-ref namespace="org.freedesktop.Telepathy"
+ >Channel.Interface.Splittable.DRAFT.Split</tp:dbus-ref>.</p>
+ </tp:rationale>
+
+ <p>Depending on the protocol, it might be signalled to remote users
+ that this channel is a continuation of all the requested channels,
+ or that it is only a continuation of the first channel in the
+ list.</p>
+
+ <tp:rationale>
+ <p>In MSN, the conference steals the underlying switchboard (protocol
+ construct) from one of its component channels, so the conference
+ appears to remote users to be a continuation of that channel and no
+ other. The connection manager has to make some arbitrary choice, so
+ we arbitrarily mandate that it SHOULD choose the first channel in
+ the list as the one to continue.</p>
+ </tp:rationale>
+
+ <p>This property is immutable.</p>
+ </tp:docstring>
+ </property>
+
+ <property name="InitialInviteeHandles"
+ tp:name-for-bindings="Initial_Invitee_Handles"
+ access="read" type="au" tp:type="Contact_Handle[]">
+ <tp:docstring xmlns="http://www.w3.org/1999/xhtml">
+ <p>A list of additional contacts invited to this conference when it
+ was created.</p>
+
+ <p>This property SHOULD be requestable, and appear in the allowed
+ properties in <tp:dbus-ref
+ namespace="org.freedesktop.Telepathy.Connection.Interface.Requests"
+ >RequestableChannelClasses</tp:dbus-ref>, in all connection
+ managers that can implement its semantics (in practice, this is
+ likely to mean exactly those connection managers where
+ <tp:member-ref>SupportsNonMerges</tp:member-ref> will be true).</p>
+
+ <p>If included in a request, the given contacts are automatically
+ invited into the new channel, as if they had been added with
+ <tp:dbus-ref namespace="org.freedesktop.Telepathy.Channel.Interface"
+ >Group.AddMembers</tp:dbus-ref>(InitialInviteeHandles,
+ <tp:member-ref>InvitationMessage</tp:member-ref> immediately after
+ the channel was created.</p>
+
+ <tp:rationale>
+ <p>This is a simple convenience API for the common case that a UI
+ upgrades a 1-1 chat to a multi-user chat solely in order to invite
+ someone else to participate.</p>
+ </tp:rationale>
+
+ <p>At most one of InitialInviteeHandles and InitialInviteeIDs may
+ appear in each request.</p>
+
+ <p>If the local user was not the initiator of this channel, the
+ <tp:dbus-ref namespace="org.freedesktop.Telepathy.Channel.Interface"
+ >Group.SelfHandle</tp:dbus-ref> SHOULD appear in the value of this
+ property, together with any other contacts invited at the same time
+ (if that information is known).</p>
+
+ <p>This property is immutable.</p>
+ </tp:docstring>
+ </property>
+
+ <property name="InitialInviteeIDs"
+ tp:name-for-bindings="Initial_Invitee_IDs"
+ access="read" type="as">
+ <tp:docstring xmlns="http://www.w3.org/1999/xhtml">
+ <p>A list of additional contacts invited to this conference when it
+ was created.</p>
+
+ <p>This property SHOULD be requestable, as an alternative to
+ <tp:member-ref>InitialInviteeHandles</tp:member-ref>. Its semantics
+ are the same, except that it takes a list of the string
+ representations of contact handles.</p>
+
+ <p>At most one of InitialInviteeHandles and InitialInviteeIDs may
+ appear in each request.</p>
+
+ <p>When a channel is created, the values of InitialInviteeHandles and
+ InitialInviteeIDs MUST correspond to each other.</p>
+
+ <p>This property is immutable.</p>
+ </tp:docstring>
+ </property>
+
+ <property name="InvitationMessage" tp:name-for-bindings="Invitation_Message"
+ access="read" type="s">
+ <tp:docstring xmlns="http://www.w3.org/1999/xhtml">
+ <p>The message that was sent to the
+ <tp:member-ref>InitialInviteeHandles</tp:member-ref> when they were
+ invited.</p>
+
+ <p>This property SHOULD be requestable, and appear in the allowed
+ properties in <tp:dbus-ref
+ namespace="org.freedesktop.Telepathy.Connection.Interface.Requests"
+ >RequestableChannelClasses</tp:dbus-ref>, in protocols where
+ invitations can have an accompanying text message.</p>
+
+ <tp:rationale>
+ <p>This allows invitations with a message to be sent when using
+ <tp:member-ref>InitialInviteeHandles</tp:member-ref> or
+ <tp:member-ref>InitialInviteeIDs</tp:member-ref>.</p>
+ </tp:rationale>
+
+ <p>If the local user was not the initiator of this channel, the
+ message with which they were invited (if any) SHOULD appear in the
+ value of this property.</p>
+
+ <p>This property is immutable.</p>
+ </tp:docstring>
+ </property>
+
+ <property name="SupportsNonMerges"
+ tp:name-for-bindings="Supports_Non_Merges"
+ access="read" type="b">
+ <tp:docstring xmlns="http://www.w3.org/1999/xhtml">
+ <p><strong>[FIXME: needs a better name; or perhaps it could be implied
+ by InitialInviteeHandles being requestable in XMPP/MSN but not in
+ GSM?]</strong></p>
+
+ <p>If true, requests with <tp:member-ref>InitialChannels</tp:member-ref>
+ omitted, empty, or one element long should be expected to succeed.</p>
+
+ <p>This property SHOULD appear in <tp:dbus-ref
+ namespace="org.freedesktop.Telepathy.Connection.Interface.Requests"
+ >RequestableChannelClasses</tp:dbus-ref> for
+ conference channels if and only if its value on those channels will
+ be true.</p>
+
+ <tp:rationale>
+ <p>Putting this in <tp:dbus-ref
+ namespace="org.freedesktop.Telepathy.Connection.Interface.Requests"
+ >RequestableChannelClasses</tp:dbus-ref> means clients can find
+ out whether their request will succeed early enough to do
+ something about it.</p>
+
+ <p>In XMPP, you can request a channel of type ROOM without
+ incorporating any 1-1 chats at all - indeed, this is the normal
+ way to do it - or as a continuation of a single 1-1 chat, and then
+ invite other people in later.</p>
+
+ <p>The sense of this property is a bit awkward, but it avoids making it
+ an anti-capability. If the sense were inverted, then its presence in
+ RequestableChannelClasses would imply that the protocol <em>lacks</em>
+ a feature; as it stands, it is additive. (Contrast with
+ <tp:dbus-ref
+ namespace="org.freedesktop.Telepathy.Channel.Type.StreamedMedia"
+ >ImmutableStreams</tp:dbus-ref>, which is the wrong way around for
+ backwards-compatibility reasons.)</p>
+ </tp:rationale>
+
+ <p>If false, <tp:member-ref>InitialChannels</tp:member-ref> SHOULD be
+ supplied in all requests for this channel class, and contain at least
+ two channels. Requests where this requirement is not met SHOULD fail
+ with NotImplemented.
+ </p>
+
+ <tp:rationale>
+ <p>In GSM, you can only make a conference call by merging at least
+ two channels.
+ <strong>[FIXME: the CM could conceivably fake it, but that would be
+ rather nasty]</strong>
+ </p>
+ </tp:rationale>
+ </tp:docstring>
+ </property>
+
+ </interface>
+</node>
diff --git a/extensions/misc.xml b/extensions/misc.xml
index 746cd9074..6eea49a2d 100644
--- a/extensions/misc.xml
+++ b/extensions/misc.xml
@@ -7,5 +7,6 @@
<xi:include href="Channel_Handler.xml"/>
<xi:include href="Tube_Handler.xml"/>
<xi:include href="Debug.xml" />
+<xi:include href="Channel_Interface_Conference.xml" />
</tp:spec>
diff --git a/libempathy-gtk/empathy-contact-menu.c b/libempathy-gtk/empathy-contact-menu.c
index 4e1adfa90..3d022a8b0 100644
--- a/libempathy-gtk/empathy-contact-menu.c
+++ b/libempathy-gtk/empathy-contact-menu.c
@@ -483,10 +483,7 @@ static void
room_sub_menu_activate_cb (GtkWidget *item,
RoomSubMenuData *data)
{
- TpHandle handle;
- GArray handles = {(gchar *) &handle, 1};
EmpathyTpChat *chat;
- TpChannel *channel;
chat = empathy_chatroom_get_tp_chat (data->chatroom);
if (chat == NULL) {
@@ -495,10 +492,8 @@ room_sub_menu_activate_cb (GtkWidget *item,
}
/* send invitation */
- handle = empathy_contact_get_handle (data->contact);
- channel = empathy_tp_chat_get_channel (chat);
- tp_cli_channel_interface_group_call_add_members (channel, -1, &handles,
- _("Inviting to this room"), NULL, NULL, NULL, NULL);
+ empathy_contact_list_add (EMPATHY_CONTACT_LIST (chat), data->contact,
+ _("Inviting you to this room"));
}
static GtkWidget *
diff --git a/libempathy-gtk/empathy-contact-selector-dialog.c b/libempathy-gtk/empathy-contact-selector-dialog.c
index 58e0fff35..3c59061ae 100644
--- a/libempathy-gtk/empathy-contact-selector-dialog.c
+++ b/libempathy-gtk/empathy-contact-selector-dialog.c
@@ -50,6 +50,7 @@ typedef struct _EmpathyContactSelectorDialogPriv \
EmpathyContactSelectorDialogPriv;
struct _EmpathyContactSelectorDialogPriv {
+ GtkListStore *store;
GtkWidget *account_chooser_label;
GtkWidget *account_chooser;
GtkWidget *entry_id;
@@ -81,13 +82,9 @@ contact_selector_dialog_account_changed_cb (GtkWidget *widget,
EmpathyAccountChooser *chooser;
TpConnection *connection;
GList *members;
- GtkListStore *store;
- GtkEntryCompletion *completion;
/* Remove completions */
- completion = gtk_entry_get_completion (GTK_ENTRY (priv->entry_id));
- store = GTK_LIST_STORE (gtk_entry_completion_get_model (completion));
- gtk_list_store_clear (store);
+ gtk_list_store_clear (priv->store);
/* Get members of the new account */
chooser = EMPATHY_ACCOUNT_CHOOSER (priv->account_chooser);
@@ -125,7 +122,7 @@ contact_selector_dialog_account_changed_cb (GtkWidget *widget,
empathy_contact_get_name (contact),
empathy_contact_get_id (contact));
- gtk_list_store_insert_with_values (store, &iter, -1,
+ gtk_list_store_insert_with_values (priv->store, &iter, -1,
COMPLETION_COL_TEXT, tmpstr,
COMPLETION_COL_ID, empathy_contact_get_id (contact),
COMPLETION_COL_NAME, empathy_contact_get_name (contact),
@@ -241,6 +238,28 @@ account_chooser_filter (TpAccount *account,
return class->account_filter (self, account);
}
+static gboolean
+contact_selector_dialog_filter_visible (GtkTreeModel *model,
+ GtkTreeIter *iter,
+ gpointer data)
+{
+ EmpathyContactSelectorDialog *self = EMPATHY_CONTACT_SELECTOR_DIALOG (data);
+ gboolean r;
+ char *id;
+
+ gtk_tree_model_get (model, iter,
+ COMPLETION_COL_ID, &id,
+ -1);
+
+ /* this must be non-NULL for this function to get called */
+ r = EMPATHY_CONTACT_SELECTOR_DIALOG_GET_CLASS (self)->contact_filter (
+ self, id);
+
+ g_free (id);
+
+ return r;
+}
+
static void
empathy_contact_selector_dialog_init (EmpathyContactSelectorDialog *dialog)
{
@@ -248,7 +267,6 @@ empathy_contact_selector_dialog_init (EmpathyContactSelectorDialog *dialog)
GtkBuilder *gui;
gchar *filename;
GtkEntryCompletion *completion;
- GtkListStore *model;
GtkWidget *content_area;
GtkWidget *table_contact;
@@ -289,19 +307,20 @@ empathy_contact_selector_dialog_init (EmpathyContactSelectorDialog *dialog)
gtk_container_set_border_width (GTK_CONTAINER (dialog), 6);
/* text completion */
+ priv->store = gtk_list_store_new (3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
+
completion = gtk_entry_completion_new ();
- model = gtk_list_store_new (3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
gtk_entry_completion_set_text_column (completion, COMPLETION_COL_TEXT);
gtk_entry_completion_set_match_func (completion,
contact_selector_dialog_match_func,
NULL, NULL);
- gtk_entry_completion_set_model (completion, GTK_TREE_MODEL (model));
+ gtk_entry_completion_set_model (completion, GTK_TREE_MODEL (priv->store));
gtk_entry_set_completion (GTK_ENTRY (priv->entry_id), completion);
g_signal_connect (completion, "match-selected",
G_CALLBACK (contact_selector_dialog_match_selected_cb),
dialog);
g_object_unref (completion);
- g_object_unref (model);
+ g_object_unref (priv->store);
empathy_builder_connect (gui, dialog,
"entry_id", "changed", contact_selector_change_state_button_cb,
@@ -370,6 +389,27 @@ empathy_contact_selector_dialog_set_property (GObject *self,
}
static void
+empathy_contact_selector_dialog_constructed (GObject *dialog)
+{
+ EmpathyContactSelectorDialogPriv *priv = GET_PRIV (dialog);
+
+ if (EMPATHY_CONTACT_SELECTOR_DIALOG_GET_CLASS (dialog)->contact_filter)
+ {
+ GtkEntryCompletion *completion;
+ GtkTreeModel *filter;
+
+ completion = gtk_entry_get_completion (GTK_ENTRY (priv->entry_id));
+ filter = gtk_tree_model_filter_new (GTK_TREE_MODEL (priv->store), NULL);
+
+ gtk_tree_model_filter_set_visible_func (GTK_TREE_MODEL_FILTER (filter),
+ contact_selector_dialog_filter_visible, dialog, NULL);
+
+ gtk_entry_completion_set_model (completion, filter);
+ g_object_unref (filter);
+ }
+}
+
+static void
empathy_contact_selector_dialog_dispose (GObject *object)
{
EmpathyContactSelectorDialogPriv *priv = GET_PRIV (object);
@@ -392,6 +432,9 @@ empathy_contact_selector_dialog_class_init (
g_type_class_add_private (class, sizeof (EmpathyContactSelectorDialogPriv));
+ class->contact_filter = NULL;
+
+ object_class->constructed = empathy_contact_selector_dialog_constructed;
object_class->dispose = empathy_contact_selector_dialog_dispose;
object_class->get_property = empathy_contact_selector_dialog_get_property;
object_class->set_property = empathy_contact_selector_dialog_set_property;
diff --git a/libempathy-gtk/empathy-contact-selector-dialog.h b/libempathy-gtk/empathy-contact-selector-dialog.h
index d8a698ab5..87b2812f4 100644
--- a/libempathy-gtk/empathy-contact-selector-dialog.h
+++ b/libempathy-gtk/empathy-contact-selector-dialog.h
@@ -40,6 +40,8 @@ struct _EmpathyContactSelectorDialogClass {
gboolean (*account_filter) (EmpathyContactSelectorDialog *self,
TpAccount *account);
+ gboolean (*contact_filter) (EmpathyContactSelectorDialog *self,
+ const char *id);
};
struct _EmpathyContactSelectorDialog {
diff --git a/libempathy/empathy-dispatcher.c b/libempathy/empathy-dispatcher.c
index 49be0babe..839dababb 100644
--- a/libempathy/empathy-dispatcher.c
+++ b/libempathy/empathy-dispatcher.c
@@ -1610,6 +1610,19 @@ empathy_dispatcher_call_create_or_ensure_channel (
}
}
+/**
+ * empathy_dispatcher_create_channel:
+ * @self: the EmpathyDispatcher
+ * @connection: the Connection to dispatch on
+ * @request: an a{sv} map of properties for the request, i.e. using tp_asv_new()
+ * @callback: a callback for when the channel arrives (or NULL)
+ * @user_data: optional user data (or NULL)
+ *
+ * When calling this function, #EmpathyDispatcher takes ownership of your
+ * reference to @request. DO NOT unref or destroy @request. When the request is
+ * done, @request will be unreferenced. Take another reference if you want to
+ * keep it around.
+ */
void
empathy_dispatcher_create_channel (EmpathyDispatcher *self,
TpConnection *connection,
diff --git a/libempathy/empathy-tp-chat.c b/libempathy/empathy-tp-chat.c
index e53caefec..381095efc 100644
--- a/libempathy/empathy-tp-chat.c
+++ b/libempathy/empathy-tp-chat.c
@@ -23,15 +23,15 @@
#include <string.h>
-#include <telepathy-glib/channel.h>
-#include <telepathy-glib/dbus.h>
-#include <telepathy-glib/util.h>
-#include <telepathy-glib/interfaces.h>
+#include <telepathy-glib/telepathy-glib.h>
+
+#include <extensions/extensions.h>
#include "empathy-tp-chat.h"
#include "empathy-tp-contact-factory.h"
#include "empathy-contact-monitor.h"
#include "empathy-contact-list.h"
+#include "empathy-dispatcher.h"
#include "empathy-marshal.h"
#include "empathy-time.h"
#include "empathy-utils.h"
@@ -60,6 +60,7 @@ typedef struct {
* (channel doesn't implement the Password interface) */
gboolean got_password_flags;
gboolean ready;
+ gboolean can_upgrade_to_muc;
} EmpathyTpChatPriv;
static void tp_chat_iface_init (EmpathyContactListIface *iface);
@@ -117,17 +118,54 @@ tp_chat_add (EmpathyContactList *list,
const gchar *message)
{
EmpathyTpChatPriv *priv = GET_PRIV (list);
- TpHandle handle;
- GArray handles = {(gchar *) &handle, 1};
- g_return_if_fail (EMPATHY_IS_TP_CHAT (list));
- g_return_if_fail (EMPATHY_IS_CONTACT (contact));
+ if (tp_proxy_has_interface_by_id (priv->channel,
+ TP_IFACE_QUARK_CHANNEL_INTERFACE_GROUP)) {
+ TpHandle handle;
+ GArray handles = {(gchar *) &handle, 1};
- handle = empathy_contact_get_handle (contact);
- tp_cli_channel_interface_group_call_add_members (priv->channel, -1,
- &handles, NULL,
- NULL, NULL, NULL,
- NULL);
+ g_return_if_fail (EMPATHY_IS_TP_CHAT (list));
+ g_return_if_fail (EMPATHY_IS_CONTACT (contact));
+
+ handle = empathy_contact_get_handle (contact);
+ tp_cli_channel_interface_group_call_add_members (priv->channel,
+ -1, &handles, NULL, NULL, NULL, NULL, NULL);
+ } else if (priv->can_upgrade_to_muc) {
+ EmpathyDispatcher *dispatcher;
+ TpConnection *connection;
+ GHashTable *props;
+ const char *object_path;
+ GPtrArray channels = { (gpointer *) &object_path, 1 };
+ const char *invitees[2] = { NULL, };
+
+ dispatcher = empathy_dispatcher_dup_singleton ();
+ connection = tp_channel_borrow_connection (priv->channel);
+
+ invitees[0] = empathy_contact_get_id (contact);
+ object_path = tp_proxy_get_object_path (priv->channel);
+
+ props = tp_asv_new (
+ TP_IFACE_CHANNEL ".ChannelType", G_TYPE_STRING,
+ TP_IFACE_CHANNEL_TYPE_TEXT,
+ TP_IFACE_CHANNEL ".TargetHandleType", G_TYPE_UINT,
+ TP_HANDLE_TYPE_NONE,
+ EMP_IFACE_CHANNEL_INTERFACE_CONFERENCE ".InitialChannels",
+ TP_ARRAY_TYPE_OBJECT_PATH_LIST, &channels,
+ EMP_IFACE_CHANNEL_INTERFACE_CONFERENCE ".InitialInviteeIDs",
+ G_TYPE_STRV, invitees,
+ /* FIXME: InvitationMessage ? */
+ NULL);
+
+ /* Although this is a MUC, it's anonymous, so CreateChannel is
+ * valid.
+ * props now belongs to EmpathyDispatcher, don't free it */
+ empathy_dispatcher_create_channel (dispatcher, connection,
+ props, NULL, NULL);
+
+ g_object_unref (dispatcher);
+ } else {
+ g_warning ("Cannot add to this channel");
+ }
}
static void
@@ -1219,9 +1257,14 @@ tp_chat_constructor (GType type,
handles->len, (TpHandle *) handles->data,
tp_chat_got_added_contacts_cb, NULL, NULL, chat);
+ priv->can_upgrade_to_muc = FALSE;
+
g_signal_connect (priv->channel, "group-members-changed",
G_CALLBACK (tp_chat_group_members_changed_cb), chat);
} else {
+ EmpathyDispatcher *dispatcher = empathy_dispatcher_dup_singleton ();
+ GList *list, *ptr;
+
/* Get the self contact from the connection's self handle */
handle = tp_connection_get_self_handle (connection);
empathy_tp_contact_factory_get_from_handle (priv->factory,
@@ -1233,6 +1276,25 @@ tp_chat_constructor (GType type,
empathy_tp_contact_factory_get_from_handle (priv->factory,
handle, tp_chat_got_remote_contact_cb,
NULL, NULL, chat);
+
+ list = empathy_dispatcher_find_requestable_channel_classes (
+ dispatcher, connection,
+ tp_channel_get_channel_type (priv->channel),
+ TP_HANDLE_TYPE_ROOM, NULL);
+
+ for (ptr = list; ptr; ptr = ptr->next) {
+ GValueArray *array = ptr->data;
+ const char **oprops = g_value_get_boxed (
+ g_value_array_get_nth (array, 1));
+
+ if (tp_strv_contains (oprops, EMP_IFACE_CHANNEL_INTERFACE_CONFERENCE ".InitialChannels")) {
+ priv->can_upgrade_to_muc = TRUE;
+ break;
+ }
+ }
+
+ g_list_free (list);
+ g_object_unref (dispatcher);
}
if (tp_proxy_has_interface_by_id (priv->channel,
@@ -1714,3 +1776,24 @@ empathy_tp_chat_provide_password_finish (EmpathyTpChat *self,
return TRUE;
}
+
+/**
+ * empathy_tp_chat_can_add_contact:
+ *
+ * Returns: %TRUE if empathy_contact_list_add() will work for this channel.
+ * That is if this chat is a 1-to-1 channel that can be upgraded to
+ * a MUC using the Conference interface or if the channel is a MUC.
+ */
+gboolean
+empathy_tp_chat_can_add_contact (EmpathyTpChat *self)
+{
+ EmpathyTpChatPriv *priv;
+
+ g_return_val_if_fail (EMPATHY_IS_TP_CHAT (self), FALSE);
+
+ priv = GET_PRIV (self);
+
+ return priv->can_upgrade_to_muc ||
+ tp_proxy_has_interface_by_id (priv->channel,
+ TP_IFACE_QUARK_CHANNEL_INTERFACE_GROUP);;
+}
diff --git a/libempathy/empathy-tp-chat.h b/libempathy/empathy-tp-chat.h
index 940c36095..6b5fc8d0b 100644
--- a/libempathy/empathy-tp-chat.h
+++ b/libempathy/empathy-tp-chat.h
@@ -96,6 +96,7 @@ void empathy_tp_chat_provide_password_async (EmpathyTpChat *chat,
gboolean empathy_tp_chat_provide_password_finish (EmpathyTpChat *chat,
GAsyncResult *result,
GError **error);
+gboolean empathy_tp_chat_can_add_contact (EmpathyTpChat *self);
G_END_DECLS
diff --git a/src/Makefile.am b/src/Makefile.am
index ecec8923f..3370f9bed 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -44,6 +44,7 @@ empathy_handwritten_source = \
empathy-import-pidgin.c empathy-import-pidgin.h \
empathy-import-utils.c empathy-import-utils.h \
empathy-import-widget.c empathy-import-widget.h \
+ empathy-invite-participant-dialog.c empathy-invite-participant-dialog.h \
empathy-main-window.c empathy-main-window.h \
empathy-new-chatroom-dialog.c empathy-new-chatroom-dialog.h \
empathy-preferences.c empathy-preferences.h \
diff --git a/src/empathy-chat-window.c b/src/empathy-chat-window.c
index 658e59426..5ad66fd26 100644
--- a/src/empathy-chat-window.c
+++ b/src/empathy-chat-window.c
@@ -35,14 +35,14 @@
#include <glib/gi18n.h>
#include <libnotify/notification.h>
-#include <telepathy-glib/account-manager.h>
-#include <telepathy-glib/util.h>
+#include <telepathy-glib/telepathy-glib.h>
#include <libempathy/empathy-contact.h>
#include <libempathy/empathy-message.h>
-#include <libempathy/empathy-dispatcher.h>
#include <libempathy/empathy-chatroom-manager.h>
#include <libempathy/empathy-utils.h>
+#include <libempathy/empathy-tp-contact-factory.h>
+#include <libempathy/empathy-contact-list.h>
#include <libempathy-gtk/empathy-images.h>
#include <libempathy-gtk/empathy-conf.h>
@@ -56,6 +56,7 @@
#include "empathy-chat-window.h"
#include "empathy-about-dialog.h"
+#include "empathy-invite-participant-dialog.h"
#define DEBUG_FLAG EMPATHY_DEBUG_CHAT
#include <libempathy/empathy-debug.h>
@@ -346,6 +347,32 @@ chat_window_menu_context_update (EmpathyChatWindowPriv *priv,
}
static void
+chat_window_conversation_menu_update (EmpathyChatWindowPriv *priv,
+ EmpathyChatWindow *self)
+{
+ EmpathyTpChat *tp_chat;
+ TpConnection *connection;
+ GtkAction *action;
+ gboolean sensitive = FALSE;
+
+ g_return_if_fail (priv->current_chat != NULL);
+
+ action = gtk_ui_manager_get_action (priv->ui_manager,
+ "/chats_menubar/menu_conv/menu_conv_invite_participant");
+ tp_chat = empathy_chat_get_tp_chat (priv->current_chat);
+
+ if (tp_chat != NULL) {
+ connection = empathy_tp_chat_get_connection (tp_chat);
+
+ sensitive = empathy_tp_chat_can_add_contact (tp_chat) &&
+ (tp_connection_get_status (connection, NULL) ==
+ TP_CONNECTION_STATUS_CONNECTED);
+ }
+
+ gtk_action_set_sensitive (action, sensitive);
+}
+
+static void
chat_window_contact_menu_update (EmpathyChatWindowPriv *priv,
EmpathyChatWindow *window)
{
@@ -536,6 +563,8 @@ chat_window_update (EmpathyChatWindow *window)
chat_window_menu_context_update (priv,
num_pages);
+ chat_window_conversation_menu_update (priv, window);
+
chat_window_contact_menu_update (priv,
window);
@@ -819,6 +848,69 @@ chat_window_contacts_toggled_cb (GtkToggleAction *toggle_action,
}
static void
+got_contact_cb (EmpathyTpContactFactory *factory,
+ EmpathyContact *contact,
+ const GError *error,
+ gpointer user_data,
+ GObject *object)
+{
+ EmpathyTpChat *tp_chat = EMPATHY_TP_CHAT (user_data);
+
+ if (error != NULL) {
+ DEBUG ("Failed: %s", error->message);
+ return;
+ } else {
+ empathy_contact_list_add (EMPATHY_CONTACT_LIST (tp_chat),
+ contact, _("Inviting you to this room"));
+ }
+}
+
+static void
+chat_window_invite_participant_activate_cb (GtkAction *action,
+ EmpathyChatWindow *window)
+{
+ EmpathyChatWindowPriv *priv;
+ GtkWidget *dialog;
+ EmpathyTpChat *tp_chat;
+ TpChannel *channel;
+ int response;
+
+ priv = GET_PRIV (window);
+
+ g_return_if_fail (priv->current_chat != NULL);
+
+ tp_chat = empathy_chat_get_tp_chat (priv->current_chat);
+ channel = empathy_tp_chat_get_channel (tp_chat);
+
+ dialog = empathy_invite_participant_dialog_new (
+ GTK_WINDOW (priv->dialog));
+ gtk_widget_show (dialog);
+
+ response = gtk_dialog_run (GTK_DIALOG (dialog));
+
+ if (response == GTK_RESPONSE_ACCEPT) {
+ TpConnection *connection;
+ EmpathyTpContactFactory *factory;
+ const char *id;
+
+ id = empathy_contact_selector_dialog_get_selected (
+ EMPATHY_CONTACT_SELECTOR_DIALOG (dialog), NULL);
+ if (EMP_STR_EMPTY (id)) goto out;
+
+ connection = tp_channel_borrow_connection (channel);
+ factory = empathy_tp_contact_factory_dup_singleton (connection);
+
+ empathy_tp_contact_factory_get_from_id (factory, id,
+ got_contact_cb, tp_chat, NULL, NULL);
+
+ g_object_unref (factory);
+ }
+
+out:
+ gtk_widget_destroy (dialog);
+}
+
+static void
chat_window_close_activate_cb (GtkAction *action,
EmpathyChatWindow *window)
{
@@ -1725,6 +1817,7 @@ empathy_chat_window_init (EmpathyChatWindow *window)
"menu_conv_clear", "activate", chat_window_clear_activate_cb,
"menu_conv_favorite", "toggled", chat_window_favorite_toggled_cb,
"menu_conv_toggle_contacts", "toggled", chat_window_contacts_toggled_cb,
+ "menu_conv_invite_participant", "activate", chat_window_invite_participant_activate_cb,
"menu_conv_close", "activate", chat_window_close_activate_cb,
"menu_edit", "activate", chat_window_edit_activate_cb,
"menu_edit_cut", "activate", chat_window_cut_activate_cb,
diff --git a/src/empathy-chat-window.ui b/src/empathy-chat-window.ui
index 57326a3e3..88d099c16 100644
--- a/src/empathy-chat-window.ui
+++ b/src/empathy-chat-window.ui
@@ -39,6 +39,12 @@
</object>
</child>
<child>
+ <object class="GtkAction" id="menu_conv_invite_participant">
+ <property name="name">menu_conv_invite_participant</property>
+ <property name="label" translatable="yes">Invite _Participant...</property>
+ </object>
+ </child>
+ <child>
<object class="GtkAction" id="menu_conv_close">
<property name="stock_id">gtk-close</property>
<property name="name">menu_conv_close</property>
@@ -145,6 +151,7 @@
<menuitem action="menu_conv_insert_smiley"/>
<menuitem action="menu_conv_favorite"/>
<menuitem action="menu_conv_toggle_contacts"/>
+ <menuitem action="menu_conv_invite_participant"/>
<separator/>
<menuitem action="menu_conv_close"/>
</menu>
diff --git a/src/empathy-invite-participant-dialog.c b/src/empathy-invite-participant-dialog.c
new file mode 100644
index 000000000..70332d1ff
--- /dev/null
+++ b/src/empathy-invite-participant-dialog.c
@@ -0,0 +1,65 @@
+/*
+ * empathy-invite-participant-dialog.c
+ *
+ * EmpathyInviteParticipantDialog
+ *
+ * (c) 2009, Collabora Ltd.
+ *
+ * Authors:
+ * Danielle Madeley <danielle.madeley@collabora.co.uk>
+ */
+
+#include <glib/gi18n.h>
+
+#include "empathy-invite-participant-dialog.h"
+
+G_DEFINE_TYPE (EmpathyInviteParticipantDialog,
+ empathy_invite_participant_dialog, EMPATHY_TYPE_CONTACT_SELECTOR_DIALOG);
+
+static void
+empathy_invite_participant_dialog_class_init (EmpathyInviteParticipantDialogClass *klass)
+{
+}
+
+static void
+empathy_invite_participant_dialog_init (EmpathyInviteParticipantDialog *self)
+{
+ EmpathyContactSelectorDialog *parent = EMPATHY_CONTACT_SELECTOR_DIALOG (self);
+ GtkWidget *label;
+ char *str;
+
+ label = gtk_label_new (NULL);
+ str = g_strdup_printf (
+ "<span size=\"x-large\" weight=\"bold\">%s</span>\n\n%s",
+ _("Invite Participant"),
+ _("Choose a contact to invite into the conversation:"));
+ gtk_label_set_markup (GTK_LABEL (label), str);
+ g_free (str);
+
+ gtk_box_pack_start (GTK_BOX (parent->vbox), label, FALSE, TRUE, 0);
+ /* move to the top -- wish there was a better way to do this */
+ gtk_box_reorder_child (GTK_BOX (parent->vbox), label, 0);
+ gtk_widget_show (label);
+
+ parent->button_action = gtk_dialog_add_button (GTK_DIALOG (self),
+ "Invite", GTK_RESPONSE_ACCEPT);
+ gtk_widget_set_sensitive (parent->button_action, FALSE);
+
+ gtk_window_set_title (GTK_WINDOW (self), _("Invite Participant"));
+ gtk_window_set_role (GTK_WINDOW (self), "invite_participant");
+ empathy_contact_selector_dialog_set_show_account_chooser (
+ EMPATHY_CONTACT_SELECTOR_DIALOG (self), FALSE);
+}
+
+GtkWidget *
+empathy_invite_participant_dialog_new (GtkWindow *parent)
+{
+ GtkWidget *self = g_object_new (EMPATHY_TYPE_INVITE_PARTICIPANT_DIALOG, NULL);
+
+ if (parent != NULL)
+ {
+ gtk_window_set_transient_for (GTK_WINDOW (self), parent);
+ }
+
+ return self;
+}
diff --git a/src/empathy-invite-participant-dialog.h b/src/empathy-invite-participant-dialog.h
new file mode 100644
index 000000000..561e10738
--- /dev/null
+++ b/src/empathy-invite-participant-dialog.h
@@ -0,0 +1,46 @@
+/*
+ * empathy-invite-participant-dialog.h
+ *
+ * EmpathyInviteParticipantDialog
+ *
+ * (c) 2009, Collabora Ltd.
+ *
+ * Authors:
+ * Danielle Madeley <danielle.madeley@collabora.co.uk>
+ */
+
+#ifndef __EMPATHY_INVITE_PARTICIPANT_DIALOG_H__
+#define __EMPATHY_INVITE_PARTICIPANT_DIALOG_H__
+
+#include <gtk/gtk.h>
+
+#include <libempathy-gtk/empathy-contact-selector-dialog.h>
+
+G_BEGIN_DECLS
+
+#define EMPATHY_TYPE_INVITE_PARTICIPANT_DIALOG (empathy_invite_participant_dialog_get_type ())
+#define EMPATHY_INVITE_PARTICIPANT_DIALOG(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), EMPATHY_TYPE_INVITE_PARTICIPANT_DIALOG, EmpathyInviteParticipantDialog))
+#define EMPATHY_INVITE_PARTICIPANT_DIALOG_CLASS(obj) (G_TYPE_CHECK_CLASS_CAST ((obj), EMPATHY_TYPE_INVITE_PARTICIPANT_DIALOG, EmpathyInviteParticipantDialogClass))
+#define EMPATHY_IS_INVITE_PARTICIPANT_DIALOG(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), EMPATHY_TYPE_INVITE_PARTICIPANT_DIALOG))
+#define EMPATHY_IS_INVITE_PARTICIPANT_DIALOG_CLASS(obj) (G_TYPE_CHECK_CLASS_TYPE ((obj), EMPATHY_TYPE_INVITE_PARTICIPANT_DIALOG))
+#define EMPATHY_INVITE_PARTICIPANT_DIALOG_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), EMPATHY_TYPE_INVITE_PARTICIPANT_DIALOG, EmpathyInviteParticipantDialogClass))
+
+typedef struct _EmpathyInviteParticipantDialog EmpathyInviteParticipantDialog;
+typedef struct _EmpathyInviteParticipantDialogClass EmpathyInviteParticipantDialogClass;
+
+struct _EmpathyInviteParticipantDialog
+{
+ EmpathyContactSelectorDialog parent;
+};
+
+struct _EmpathyInviteParticipantDialogClass
+{
+ EmpathyContactSelectorDialogClass parent_class;
+};
+
+GType empathy_invite_participant_dialog_get_type (void);
+GtkWidget *empathy_invite_participant_dialog_new (GtkWindow *parent);
+
+G_END_DECLS
+
+#endif