Commit d2cad06c authored by Stepan Salenikovich's avatar Stepan Salenikovich

chat: refactor chat view out of call view code

This is a preliminary step to integrating out of call chat.

Tuleap: #203
Change-Id: I18a339b4b6b4bc34b415e9f60d25884485d97c36
parent c132342a
......@@ -247,6 +247,8 @@ SET( SRC_FILES
src/ringwelcomeview.cpp
src/recentcontactsview.h
src/recentcontactsview.cpp
src/chatview.h
src/chatview.cpp
)
# compile glib resource files to c code
......
/*
* Copyright (C) 2016 Savoir-faire Linux Inc.
* Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "chatview.h"
#include <gtk/gtk.h>
#include <call.h>
#include <callmodel.h>
#include <contactmethod.h>
#include <person.h>
#include <media/media.h>
#include <media/text.h>
#include <media/textrecording.h>
#include "ringnotify.h"
struct _ChatView
{
GtkBox parent;
};
struct _ChatViewClass
{
GtkBoxClass parent_class;
};
typedef struct _ChatViewPrivate ChatViewPrivate;
struct _ChatViewPrivate
{
GtkWidget *textview_chat;
GtkWidget *button_chat_input;
GtkWidget *entry_chat_input;
GtkWidget *scrolledwindow_chat;
Call *call;
QMetaObject::Connection new_message_connection;
};
G_DEFINE_TYPE_WITH_PRIVATE(ChatView, chat_view, GTK_TYPE_BOX);
#define CHAT_VIEW_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), CHAT_VIEW_TYPE, ChatViewPrivate))
enum {
NEW_MESSAGES_DISPLAYED,
LAST_SIGNAL
};
static guint chat_view_signals[LAST_SIGNAL] = { 0 };
static void
chat_view_dispose(GObject *object)
{
ChatView *view;
ChatViewPrivate *priv;
view = CHAT_VIEW(object);
priv = CHAT_VIEW_GET_PRIVATE(view);
QObject::disconnect(priv->new_message_connection);
G_OBJECT_CLASS(chat_view_parent_class)->dispose(object);
}
static void
send_chat(G_GNUC_UNUSED GtkWidget *widget, ChatView *self)
{
g_return_if_fail(IS_CHAT_VIEW(self));
ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);
/* make sure there is text to send */
const gchar *text = gtk_entry_get_text(GTK_ENTRY(priv->entry_chat_input));
if (text && strlen(text) > 0) {
QMap<QString, QString> messages;
messages["text/plain"] = text;
priv->call->addOutgoingMedia<Media::Text>()->send(messages);
/* clear the entry */
gtk_entry_set_text(GTK_ENTRY(priv->entry_chat_input), "");
}
}
static void
scroll_to_bottom(GtkAdjustment *adjustment, G_GNUC_UNUSED gpointer user_data)
{
gtk_adjustment_set_value(adjustment,
gtk_adjustment_get_upper(adjustment) - gtk_adjustment_get_page_size(adjustment));
}
static void
chat_view_init(ChatView *view)
{
gtk_widget_init_template(GTK_WIDGET(view));
ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(view);
g_signal_connect(priv->button_chat_input, "clicked", G_CALLBACK(send_chat), view);
g_signal_connect(priv->entry_chat_input, "activate", G_CALLBACK(send_chat), view);
/* the adjustment params will change only when the model is created and when
* new messages are added; in these cases we want to scroll to the bottom of
* the chat treeview */
GtkAdjustment *adjustment = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(priv->scrolledwindow_chat));
g_signal_connect(adjustment, "changed", G_CALLBACK(scroll_to_bottom), NULL);
}
static void
chat_view_class_init(ChatViewClass *klass)
{
G_OBJECT_CLASS(klass)->dispose = chat_view_dispose;
gtk_widget_class_set_template_from_resource(GTK_WIDGET_CLASS (klass),
"/cx/ring/RingGnome/chatview.ui");
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), ChatView, textview_chat);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), ChatView, button_chat_input);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), ChatView, entry_chat_input);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), ChatView, scrolledwindow_chat);
chat_view_signals[NEW_MESSAGES_DISPLAYED] = g_signal_new (
"new-messages-displayed",
G_TYPE_FROM_CLASS(klass),
(GSignalFlags) (G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION),
0,
nullptr,
nullptr,
g_cclosure_marshal_VOID__VOID,
G_TYPE_NONE, 0);
}
static void
print_message_to_buffer(const QModelIndex &idx, GtkTextBuffer *buffer)
{
if (idx.isValid()) {
auto message = idx.data().value<QString>().toUtf8();
auto sender = idx.data(static_cast<int>(Media::TextRecording::Role::AuthorDisplayname)).value<QString>().toUtf8();
GtkTextIter iter;
/* unless its the very first message, insert a new line */
if (idx.row() != 0) {
gtk_text_buffer_get_end_iter(buffer, &iter);
gtk_text_buffer_insert(buffer, &iter, "\n", -1);
}
auto format_sender = g_strconcat(sender.constData(), ": ", NULL);
gtk_text_buffer_get_end_iter(buffer, &iter);
gtk_text_buffer_insert_with_tags_by_name(buffer, &iter,
format_sender, -1,
"bold", NULL);
g_free(format_sender);
/* if the sender name is too long, insert a new line after it */
if (sender.length() > 20) {
gtk_text_buffer_get_end_iter(buffer, &iter);
gtk_text_buffer_insert(buffer, &iter, "\n", -1);
}
gtk_text_buffer_get_end_iter(buffer, &iter);
gtk_text_buffer_insert(buffer, &iter, message.constData(), -1);
} else {
g_warning("QModelIndex in im model is not valid");
}
}
static void
parse_chat_model(QAbstractItemModel *model, ChatView *self)
{
g_return_if_fail(IS_CHAT_VIEW(self));
ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);
/* new model, disconnect from the old model updates and clear the text buffer */
QObject::disconnect(priv->new_message_connection);
GtkTextBuffer *new_buffer = gtk_text_buffer_new(NULL);
gtk_text_view_set_buffer(GTK_TEXT_VIEW(priv->textview_chat), new_buffer);
/* add tags to the buffer */
gtk_text_buffer_create_tag(new_buffer, "bold", "weight", PANGO_WEIGHT_BOLD, NULL);
g_object_unref(new_buffer);
/* put all the messages in the im model into the text view */
for (int row = 0; row < model->rowCount(); ++row) {
QModelIndex idx = model->index(row, 0);
print_message_to_buffer(idx, new_buffer);
}
/* append new messages */
priv->new_message_connection = QObject::connect(
model,
&QAbstractItemModel::rowsInserted,
[self, priv, model] (const QModelIndex &parent, int first, int last) {
for (int row = first; row <= last; ++row) {
QModelIndex idx = model->index(row, 0, parent);
print_message_to_buffer(idx, gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->textview_chat)));
g_signal_emit(G_OBJECT(self), chat_view_signals[NEW_MESSAGES_DISPLAYED], 0);
}
}
);
}
GtkWidget *
chat_view_new(Call *call)
{
ChatView *self = CHAT_VIEW(g_object_new(CHAT_VIEW_TYPE, NULL));
ChatViewPrivate *priv = CHAT_VIEW_GET_PRIVATE(self);
priv->call = call;
auto cm = priv->call->peerContactMethod();
parse_chat_model(cm->textRecording()->instantMessagingModel(), self);
return (GtkWidget *)self;
}
/*
* Copyright (C) 2016 Savoir-faire Linux Inc.
* Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifndef _CHATVIEW_H
#define _CHATVIEW_H
#include <gtk/gtk.h>
class Call;
G_BEGIN_DECLS
#define CHAT_VIEW_TYPE (chat_view_get_type ())
#define CHAT_VIEW(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), CHAT_VIEW_TYPE, ChatView))
#define CHAT_VIEW_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), CHAT_VIEW_TYPE, ChatViewClass))
#define IS_CHAT_VIEW(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), CHAT_VIEW_TYPE))
#define IS_CHAT_VIEW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), CHAT_VIEW_TYPE))
typedef struct _ChatView ChatView;
typedef struct _ChatViewClass ChatViewClass;
GType chat_view_get_type (void) G_GNUC_CONST;
GtkWidget *chat_view_new (Call* call);
G_END_DECLS
#endif /* _CHATVIEW_H */
......@@ -50,6 +50,7 @@
#include <account.h>
#include "utils/files.h"
#include <clutter-gtk/clutter-gtk.h>
#include "chatview.h"
static constexpr int CONTROLS_FADE_TIMEOUT = 3000000; /* microseconds */
static constexpr int FADE_DURATION = 500; /* miliseconds */
......@@ -74,15 +75,11 @@ struct _CurrentCallViewPrivate
GtkWidget *label_identity;
GtkWidget *label_status;
GtkWidget *label_duration;
GtkWidget *paned_call;
GtkWidget *frame_video;
GtkWidget *video_widget;
GtkWidget *paned_chat;
GtkWidget *vbox_chat;
GtkWidget *frame_chat;
GtkWidget *togglebutton_chat;
GtkWidget *textview_chat;
GtkWidget *button_chat_input;
GtkWidget *entry_chat_input;
GtkWidget *scrolledwindow_chat;
GtkWidget *button_hangup;
GtkWidget *scalebutton_quality;
GtkWidget *checkbutton_autoquality;
......@@ -98,9 +95,6 @@ struct _CurrentCallViewPrivate
QMetaObject::Connection call_details_connection;
QMetaObject::Connection local_renderer_connection;
QMetaObject::Connection remote_renderer_connection;
QMetaObject::Connection media_added_connection;
QMetaObject::Connection new_message_connection;
QMetaObject::Connection incoming_msg_connection;
GSettings *settings;
......@@ -135,9 +129,6 @@ current_call_view_dispose(GObject *object)
QObject::disconnect(priv->call_details_connection);
QObject::disconnect(priv->local_renderer_connection);
QObject::disconnect(priv->remote_renderer_connection);
QObject::disconnect(priv->media_added_connection);
QObject::disconnect(priv->new_message_connection);
QObject::disconnect(priv->incoming_msg_connection);
g_clear_object(&priv->settings);
......@@ -147,46 +138,28 @@ current_call_view_dispose(GObject *object)
}
static void
chat_toggled(GtkToggleButton *togglebutton, CurrentCallView *self)
show_chat_view(CurrentCallView *self)
{
g_return_if_fail(IS_CURRENT_CALL_VIEW(self));
CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
if (gtk_toggle_button_get_active(togglebutton)) {
gtk_widget_show_all(priv->vbox_chat);
/* create an outgoing media to bring up chat history, if any */
priv->call->addOutgoingMedia<Media::Text>();
/* change focus to the chat entry */
gtk_widget_grab_focus(priv->entry_chat_input);
} else {
gtk_widget_hide(priv->vbox_chat);
}
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->togglebutton_chat), TRUE);
}
static void
send_chat(G_GNUC_UNUSED GtkWidget *widget, CurrentCallView *self)
chat_toggled(GtkToggleButton *togglebutton, CurrentCallView *self)
{
g_return_if_fail(IS_CURRENT_CALL_VIEW(self));
CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
/* make sure there is text to send */
const gchar *text = gtk_entry_get_text(GTK_ENTRY(priv->entry_chat_input));
if (text && strlen(text) > 0) {
QMap<QString, QString> messages;
messages["text/plain"] = text;
priv->call->addOutgoingMedia<Media::Text>()->send(messages);
/* clear the entry */
gtk_entry_set_text(GTK_ENTRY(priv->entry_chat_input), "");
if (gtk_toggle_button_get_active(togglebutton)) {
gtk_widget_show_all(priv->frame_chat);
gtk_widget_grab_focus(priv->frame_chat);
} else {
gtk_widget_hide(priv->frame_chat);
}
}
static void
scroll_to_bottom(GtkAdjustment *adjustment, G_GNUC_UNUSED gpointer user_data)
{
gtk_adjustment_set_value(adjustment,
gtk_adjustment_get_upper(adjustment) - gtk_adjustment_get_page_size(adjustment));
}
gboolean
map_boolean_to_orientation(GValue *value, GVariant *variant, G_GNUC_UNUSED gpointer user_data)
{
......@@ -476,20 +449,13 @@ current_call_view_init(CurrentCallView *view)
/* manually handle the focus of the video widget to be able to focus on the call controls */
g_signal_connect(priv->video_widget, "focus", G_CALLBACK(video_widget_focus), view);
/* toggle whether or not the chat is displayed */
g_signal_connect(priv->togglebutton_chat, "toggled", G_CALLBACK(chat_toggled), view);
g_signal_connect(priv->button_chat_input, "clicked", G_CALLBACK(send_chat), view);
g_signal_connect(priv->entry_chat_input, "activate", G_CALLBACK(send_chat), view);
/* the adjustment params will change only when the model is created and when
* new messages are added; in these cases we want to scroll to the bottom of
* the chat treeview */
GtkAdjustment *adjustment = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(priv->scrolledwindow_chat));
g_signal_connect(adjustment, "changed", G_CALLBACK(scroll_to_bottom), NULL);
// bind the chat location to the gsetting
/* bind the chat orientation to the gsetting */
priv->settings = g_settings_new_full(get_ring_schema(), NULL, NULL);
g_settings_bind_with_mapping(priv->settings, "chat-pane-horizontal",
priv->paned_chat, "orientation",
priv->paned_call, "orientation",
G_SETTINGS_BIND_GET,
map_boolean_to_orientation,
nullptr, nullptr, nullptr);
......@@ -507,8 +473,6 @@ current_call_view_init(CurrentCallView *view)
g_signal_connect(scale, "button-press-event", G_CALLBACK(quality_button_pressed), view);
g_signal_connect(scale, "button-release-event", G_CALLBACK(quality_button_released), view);
}
}
static void
......@@ -525,14 +489,10 @@ current_call_view_class_init(CurrentCallViewClass *klass)
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, label_identity);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, label_status);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, label_duration);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, paned_call);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, frame_video);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, paned_chat);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, vbox_chat);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, frame_chat);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, togglebutton_chat);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, textview_chat);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, button_chat_input);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, entry_chat_input);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, scrolledwindow_chat);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, button_hangup);
gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, scalebutton_quality);
......@@ -575,78 +535,6 @@ update_details(CurrentCallView *view, Call *call)
gtk_label_set_text(GTK_LABEL(priv->label_duration), ba_length.constData());
}
static void
print_message_to_buffer(const QModelIndex &idx, GtkTextBuffer *buffer)
{
if (idx.isValid()) {
auto message = idx.data().value<QString>().toUtf8();
auto sender = idx.data(static_cast<int>(Media::TextRecording::Role::AuthorDisplayname)).value<QString>().toUtf8();
GtkTextIter iter;
/* unless its the very first message, insert a new line */
if (idx.row() != 0) {
gtk_text_buffer_get_end_iter(buffer, &iter);
gtk_text_buffer_insert(buffer, &iter, "\n", -1);
}
auto format_sender = g_strconcat(sender.constData(), ": ", NULL);
gtk_text_buffer_get_end_iter(buffer, &iter);
gtk_text_buffer_insert_with_tags_by_name(buffer, &iter,
format_sender, -1,
"bold", NULL);
g_free(format_sender);
/* if the sender name is too long, insert a new line after it */
if (sender.length() > 20) {
gtk_text_buffer_get_end_iter(buffer, &iter);
gtk_text_buffer_insert(buffer, &iter, "\n", -1);
}
gtk_text_buffer_get_end_iter(buffer, &iter);
gtk_text_buffer_insert(buffer, &iter, message.constData(), -1);
} else {
g_warning("QModelIndex in im model is not valid");
}
}
static void
parse_chat_model(QAbstractItemModel *model, CurrentCallView *self)
{
g_return_if_fail(IS_CURRENT_CALL_VIEW(self));
CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
/* new model, disconnect from the old model updates and clear the text buffer */
QObject::disconnect(priv->new_message_connection);
GtkTextBuffer *new_buffer = gtk_text_buffer_new(NULL);
gtk_text_view_set_buffer(GTK_TEXT_VIEW(priv->textview_chat), new_buffer);
/* add tags to the buffer */
gtk_text_buffer_create_tag(new_buffer, "bold", "weight", PANGO_WEIGHT_BOLD, NULL);
g_object_unref(new_buffer);
/* put all the messages in the im model into the text view */
for (int row = 0; row < model->rowCount(); ++row) {
QModelIndex idx = model->index(row, 0);
print_message_to_buffer(idx, new_buffer);
}
/* append new messages */
priv->new_message_connection = QObject::connect(
model,
&QAbstractItemModel::rowsInserted,
[priv, model] (const QModelIndex &parent, int first, int last) {
for (int row = first; row <= last; ++row) {
QModelIndex idx = model->index(row, 0, parent);
print_message_to_buffer(idx, gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->textview_chat)));
}
}
);
}
static gboolean
on_button_press_in_video_event(GtkWidget *self, GdkEventButton *event, CurrentCallView *view)
{
......@@ -662,24 +550,6 @@ on_button_press_in_video_event(GtkWidget *self, GdkEventButton *event, CurrentCa
return GDK_EVENT_PROPAGATE;
}
void
monitor_incoming_message(CurrentCallView *self, Media::Text *media)
{
g_return_if_fail(IS_CURRENT_CALL_VIEW(self));
CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
/* connect to incoming chat messages to open the chat view */
QObject::disconnect(priv->incoming_msg_connection);
priv->incoming_msg_connection = QObject::connect(
media,
&Media::Text::messageReceived,
[priv] (G_GNUC_UNUSED const QMap<QString,QString>& m) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->togglebutton_chat), TRUE);
}
);
}
void
current_call_view_set_call_info(CurrentCallView *view, const QModelIndex& idx) {
CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(view);
......@@ -750,34 +620,6 @@ current_call_view_set_call_info(CurrentCallView *view, const QModelIndex& idx) {
G_CALLBACK(on_button_press_in_video_event),
view);
/* check if text media is already present */
if (priv->call->hasMedia(Media::Media::Type::TEXT, Media::Media::Direction::IN)) {
Media::Text *text = priv->call->firstMedia<Media::Text>(Media::Media::Direction::IN);
parse_chat_model(text->recording()->instantMessagingModel(), view);
monitor_incoming_message(view, text);
} else if (priv->call->hasMedia(Media::Media::Type::TEXT, Media::Media::Direction::OUT)) {
Media::Text *text = priv->call->firstMedia<Media::Text>(Media::Media::Direction::OUT);
parse_chat_model(text->recording()->instantMessagingModel(), view);
monitor_incoming_message(view, text);
} else {
/* monitor media for messaging text messaging */
priv->media_added_connection = QObject::connect(
priv->call,
&Call::mediaAdded,
[view, priv] (Media::Media* media) {
if (media->type() == Media::Media::Type::TEXT) {
parse_chat_model(((Media::Text*)media)->recording()->instantMessagingModel(), view);
monitor_incoming_message(view, (Media::Text*)media);
QObject::disconnect(priv->media_added_connection);
}
}
);
}
/* check if there were any chat notifications and open the chat view if so */
if (ring_notify_close_chat_notification(priv->call))
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->togglebutton_chat), TRUE);
/* check if auto quality is enabled or not; */
if (const auto& codecModel = priv->call->account()->codecModel()) {
const auto& videoCodecs = codecModel->videoCodecs();
......@@ -793,4 +635,15 @@ current_call_view_set_call_info(CurrentCallView *view, const QModelIndex& idx) {
// different for each codec, so there is no reason to check it here
}
}
/* init chat view */
auto chat_view = chat_view_new(priv->call);
gtk_container_add(GTK_CONTAINER(priv->frame_chat), chat_view);
/* check if there were any chat notifications and open the chat view if so */
if (ring_notify_close_chat_notification(priv->call))
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->togglebutton_chat), TRUE);
/* show chat view on any new incoming messages */
g_signal_connect_swapped(chat_view, "new-messages-displayed", G_CALLBACK(show_chat_view), view);
}
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.10"/>
<template class="ChatView" parent="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<!-- start of chat text view -->
<child>
<object class="GtkScrolledWindow" id="scrolledwindow_chat">
<property name="visible">True</property>
<child>
<object class="GtkTextView" id="textview_chat">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="wrap-mode">word-char</property>
<property name="left-margin">5</property>
<property name="right-margin">5</property>
<property name="height-request">50</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
</packing>
</child>
<!-- end of chat text view -->
<!-- start of chat entry -->
<child>
<object class="GtkBox" id="hbox_chat_input">
<property name="visible">True</property>
<property name="orientation">horizontal</property>
<property name="spacing">5</property>
<child>
<object class="GtkEntry" id="entry_chat_input">
<property name="visible">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
</packing>
</child>
<child>
<object class="GtkButton" id="button_chat_input">
<property name="visible">True</property>
<property name="label" translatable="yes">Send</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>