talkatu/talkatu

Move the demo to blueprint-compiler
default tip
7 days ago, Gary Kramlich
8c78e806f7c7
Move the demo to blueprint-compiler

Testing Done:
* Ran the demo checked just about everything.
* Used `gresource extract` to verify that the XML looked close enough to the old version.

Reviewed at https://reviews.imfreedom.org/r/1765/
/*
* 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 <gtk/gtk.h>
#include <glib/gi18n-lib.h>
#include <talkatu/talkatuactiongroup.h>
#include <talkatu/talkatubuffer.h>
#include <talkatu/talkatuenums.h>
#include <talkatu/talkatutag.h>
#include <talkatu/talkatutagtable.h>
/**
* TalkatuBuffer:
*
* A #GtkTextBuffer subclass that will automatically apply formatting according
* to the actions in a #GSimpleAction group.
*/
/**
* TalkatuBufferClass:
* @insert_markup: The insert_markup vfunc is called to insert already rendered
* text into the #TalkatuBuffer.
* @create_action_group: The create_action_group vfunc is called to get a
* #GSimpleAction group of actions that the
* #TalkatuBuffer supports.
*
* The backing class to #TalkatuBuffer.
*/
/**
* TALKATU_BUFFER_LINK_TARGET_ATTRIBUTE:
*
* The name of the attribute set on #GtkTextTags that contain the URL for
* links.
*/
/**
* TalkatuBufferStyle:
* @TALKATU_BUFFER_STYLE_RICH: Format the buffer character by character.
* @TALKATU_BUFFER_STYLE_WHOLE: Format the buffer as a whole.
*
* The format style of a #TalkatuBuffer.
*/
typedef struct _TalkatuBufferPrivate {
TalkatuBufferStyle style;
GSimpleActionGroup *action_group;
GSList *tags;
} TalkatuBufferPrivate;
enum {
PROP_0,
PROP_STYLE,
PROP_ACTION_GROUP,
N_PROPERTIES,
};
static GParamSpec *properties[N_PROPERTIES] = {NULL,};
G_DEFINE_TYPE_WITH_PRIVATE(TalkatuBuffer, talkatu_buffer, GTK_TYPE_TEXT_BUFFER);
#define TALKATU_BUFFER_MARK_LAST_INSERT "talkatu-last-insert"
/******************************************************************************
* Callbacks
*****************************************************************************/
static void
talkatu_buffer_action_toggled_cb(GActionGroup *action_group,
gchar *name,
GVariant *value,
gpointer data)
{
TalkatuBufferPrivate *priv = talkatu_buffer_get_instance_private(data);
const gchar *tag_name = NULL;
tag_name = talkatu_tag_name_for_action_name(name);
if(tag_name == NULL) {
return;
}
if(g_variant_get_boolean(value)) {
/* apply the tag */
priv->tags = g_slist_prepend(priv->tags, (gpointer)tag_name);
} else {
/* remove the tag */
priv->tags = g_slist_remove(priv->tags, (gpointer)tag_name);
}
}
/******************************************************************************
* TalkatuBuffer Stuff
*****************************************************************************/
static void
talkatu_buffer_real_insert_markup(TalkatuBuffer *buffer,
GtkTextIter *pos,
const gchar *new_text,
gint new_text_length)
{
GTK_TEXT_BUFFER_CLASS(talkatu_buffer_parent_class)->insert_text(
GTK_TEXT_BUFFER(buffer),
pos,
new_text,
new_text_length
);
}
static GSimpleActionGroup *
talkatu_buffer_default_creation_action_group(TalkatuBuffer *buffer) {
return g_simple_action_group_new();
}
/******************************************************************************
* GtkTextBuffer Stuff
*****************************************************************************/
static void
talkatu_buffer_toggle_tag(GtkTextBuffer *buffer,
GSimpleActionGroup *action_group,
GtkTextTag *tag,
gboolean state)
{
GAction *action = NULL;
const gchar *action_name = NULL;
gchar *name = NULL;
/* GtkTextTag doesn't have an accessor for it's name property, so
* just grab it with g_object_get.
*/
g_object_get(G_OBJECT(tag), "name", &name, NULL);
if(name == NULL) {
return;
}
action_name = talkatu_action_name_for_tag_name(name);
if(action_name == NULL) {
g_warning(_("failed to find action for tag %s"), name);
g_free(name);
return;
}
g_free(name);
action = g_action_map_lookup_action(G_ACTION_MAP(action_group), action_name);
if(action != NULL) {
g_simple_action_set_state(G_SIMPLE_ACTION(action), g_variant_new_boolean(state));
} else {
g_warning(_("failed to find action %s"), action_name);
}
}
static void
talkatu_buffer_insert_text(GtkTextBuffer *buffer,
GtkTextIter *pos,
const gchar *new_text,
gint new_text_length)
{
TalkatuBufferPrivate *priv = NULL;
GSList *tag = NULL;
GtkTextIter *start = NULL;
/* need to figure out if this is normal/expected... */
if(new_text_length == 0) {
g_message("called with empty text, determine if this is expected");
return;
}
priv = talkatu_buffer_get_instance_private(TALKATU_BUFFER(buffer));
/* chain up to the parent */
GTK_TEXT_BUFFER_CLASS(talkatu_buffer_parent_class)->insert_text(
buffer,
pos,
new_text,
new_text_length
);
/* now apply the tags as necessary. pos has been revalidated */
start = gtk_text_iter_copy(pos);
gtk_text_iter_backward_chars(start, new_text_length);
/* apply all tags to that text */
for(tag = priv->tags; tag; tag = tag->next) {
gtk_text_buffer_apply_tag_by_name(buffer, tag->data, start, pos);
}
gtk_text_iter_free(start);
}
static void
talkatu_buffer_mark_set(GtkTextBuffer *buffer,
const GtkTextIter *location,
GtkTextMark *mark)
{
TalkatuBufferPrivate *priv = NULL;
GSList *tags = NULL, *tag = NULL;
GtkTextMark *mark_last_insert = NULL;
if(mark != gtk_text_buffer_get_insert(buffer)) {
return;
}
priv = talkatu_buffer_get_instance_private(TALKATU_BUFFER(buffer));
/* the insertion point has moved, reset all of the tags from the old
* insertion point to their defaults, which should be false.
*/
mark_last_insert = gtk_text_buffer_get_mark(buffer, TALKATU_BUFFER_MARK_LAST_INSERT);
/* if the mark doesn't exist, create it */
if(mark_last_insert == NULL) {
mark_last_insert = gtk_text_buffer_create_mark(
buffer,
TALKATU_BUFFER_MARK_LAST_INSERT,
location,
FALSE
);
} else {
/* we have a last insertion point, so we need to get the tags there
* and reset their states.
*/
GtkTextIter iter;
gtk_text_buffer_get_iter_at_mark(buffer, &iter, mark_last_insert);
tags = gtk_text_iter_get_tags(&iter);
for(tag = tags; tag; tag = tag->next) {
talkatu_buffer_toggle_tag(buffer, priv->action_group, tag->data, FALSE);
}
g_slist_free(tags);
}
tags = gtk_text_iter_get_tags(location);
for(tag = tags; tag; tag = tag->next) {
talkatu_buffer_toggle_tag(buffer, priv->action_group, tag->data, TRUE);
}
gtk_text_buffer_move_mark(buffer, mark_last_insert, location);
}
/******************************************************************************
* Helpers
*****************************************************************************/
static void
talkatu_buffer_set_style(TalkatuBuffer *buffer, TalkatuBufferStyle style) {
TalkatuBufferPrivate *priv = talkatu_buffer_get_instance_private(buffer);
priv->style = style;
g_object_notify(G_OBJECT(buffer), "style");
}
static void
talkatu_buffer_set_action_group(TalkatuBuffer *buffer,
GSimpleActionGroup *action_group)
{
TalkatuBufferPrivate *priv = talkatu_buffer_get_instance_private(buffer);
/* clear the tags */
g_slist_free(priv->tags);
priv->tags = NULL;
/* clean up the old one */
if(priv->action_group) {
g_signal_handlers_disconnect_by_func(
priv->action_group,
talkatu_buffer_action_toggled_cb,
buffer
);
g_object_unref(G_OBJECT(priv->action_group));
priv->action_group = NULL;
}
if(G_IS_SIMPLE_ACTION_GROUP(action_group)) {
priv->action_group = g_object_ref(action_group);
g_signal_connect(
action_group,
"action-state-changed",
G_CALLBACK(talkatu_buffer_action_toggled_cb),
buffer
);
}
}
/******************************************************************************
* GObject Stuff
*****************************************************************************/
static void
talkatu_buffer_get_property(GObject *obj,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
TalkatuBuffer *buffer = TALKATU_BUFFER(obj);
switch(prop_id) {
case PROP_STYLE:
g_value_set_enum(value, talkatu_buffer_get_style(buffer));
break;
case PROP_ACTION_GROUP:
g_value_set_object(value, talkatu_buffer_get_action_group(buffer));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
break;
}
}
static void
talkatu_buffer_set_property(GObject *obj,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
TalkatuBuffer *buffer = TALKATU_BUFFER(obj);
switch(prop_id) {
case PROP_STYLE:
talkatu_buffer_set_style(buffer, g_value_get_enum(value));
break;
case PROP_ACTION_GROUP:
talkatu_buffer_set_action_group(buffer, g_value_get_object(value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
break;
}
}
static GObject *
talkatu_buffer_constructor(GType type,
guint n_params,
GObjectConstructParam *params)
{
GObjectConstructParam *param;
gint i = 0;
for(i = 0, param = params; i < n_params; i++, param++) {
const gchar *name = g_param_spec_get_name(param->pspec);
if(g_ascii_strcasecmp("tag-table", name) == 0) {
if(g_value_get_object(param->value) == NULL) {
g_value_set_object(param->value, talkatu_tag_table_new());
}
}
}
return G_OBJECT_CLASS(talkatu_buffer_parent_class)->constructor(type, n_params, params);
}
static void
talkatu_buffer_finalize(GObject *obj) {
TalkatuBufferPrivate *priv = talkatu_buffer_get_instance_private(TALKATU_BUFFER(obj));
g_clear_object(&priv->action_group);
G_OBJECT_CLASS(talkatu_buffer_parent_class)->finalize(obj);
}
static void
talkatu_buffer_init(TalkatuBuffer *buffer) {
// gtk_text_buffer_register_serialize_tagset(GTK_TEXT_BUFFER(buffer), NULL);
// gtk_text_buffer_register_deserialize_tagset(GTK_TEXT_BUFFER(buffer), NULL);
}
static void
talkatu_buffer_class_init(TalkatuBufferClass *klass) {
GObjectClass *obj_class = G_OBJECT_CLASS(klass);
GtkTextBufferClass *text_buffer_class = GTK_TEXT_BUFFER_CLASS(klass);
obj_class->get_property = talkatu_buffer_get_property;
obj_class->set_property = talkatu_buffer_set_property;
obj_class->constructor = talkatu_buffer_constructor;
obj_class->finalize = talkatu_buffer_finalize;
text_buffer_class->insert_text = talkatu_buffer_insert_text;
text_buffer_class->mark_set = talkatu_buffer_mark_set;
klass->insert_markup = talkatu_buffer_real_insert_markup;
klass->create_action_group = talkatu_buffer_default_creation_action_group;
properties[PROP_STYLE] = g_param_spec_enum(
"style", "style", "The format style of the buffer",
TALKATU_TYPE_BUFFER_STYLE,
TALKATU_BUFFER_STYLE_RICH,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY
);
properties[PROP_ACTION_GROUP] = g_param_spec_object(
"action-group", "action-group", "The action group for this buffer",
G_TYPE_ACTION_GROUP,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY
);
g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
}
/******************************************************************************
* Public API
*****************************************************************************/
/**
* talkatu_buffer_new:
* @action_group: An optional #GSimpleAction group to use.
*
* This is a simple #GtkTextBuffer subclass that contains the shared behavior
* for the other Talkatu text buffers.
*
* Returns: (transfer full): The #TalkatuBuffer instance.
*/
GtkTextBuffer *
talkatu_buffer_new(GSimpleActionGroup *action_group) {
return GTK_TEXT_BUFFER(g_object_new(
TALKATU_TYPE_BUFFER,
"action-group", action_group,
NULL
));
}
/**
* talkatu_buffer_get_style:
* @buffer: A #TalkatuBuffer instance.
*
* Gets format style of @buffer.
*
* Returns: The format style of @buffer.
*/
TalkatuBufferStyle
talkatu_buffer_get_style(TalkatuBuffer *buffer) {
TalkatuBufferPrivate *priv = NULL;
g_return_val_if_fail(TALKATU_IS_BUFFER(buffer), TALKATU_BUFFER_STYLE_RICH);
priv = talkatu_buffer_get_instance_private(buffer);
return priv->style;
}
/**
* talkatu_buffer_get_action_group:
* @buffer: A #TalkatuBuffer instance.
*
* A #TalkatuBuffer can support multiple actions, whether it's formatting
* of text, or being able to insert images, code, etc. This function is called
* by #TalkatuView to map them to keybindings as well as the format
* toolbar.
*
* Returns: (transfer none): The #GSimpleActionGroup the @buffer supports.
*/
GSimpleActionGroup *
talkatu_buffer_get_action_group(TalkatuBuffer *buffer) {
TalkatuBufferPrivate *priv = NULL;
g_return_val_if_fail(TALKATU_IS_BUFFER(buffer), NULL);
priv = talkatu_buffer_get_instance_private(buffer);
if(priv->action_group == NULL) {
TalkatuBufferClass *klass = TALKATU_BUFFER_GET_CLASS(buffer);
GSimpleActionGroup *action_group = NULL;
g_return_val_if_fail(klass != NULL, NULL);
g_return_val_if_fail(klass->create_action_group != NULL, NULL);
action_group = klass->create_action_group(buffer);
talkatu_buffer_set_action_group(buffer, action_group);
g_object_unref(G_OBJECT(action_group));
}
return priv->action_group;
}
/**
* talkatu_buffer_insert_markup:
* @buffer: The #TalkatuBuffer instance.
* @pos: The #GtkTextIter where the text should be inserted.
* @new_text: The new text to insert.
* @new_text_length: The len of @new_text.
*
* Inserts text that will be or already is marked up. Calling this tells
* @buffer to not apply the currently selected format to the newly inserted
* text, which is what it does when text is normally inserted.
*/
void
talkatu_buffer_insert_markup(TalkatuBuffer *buffer,
GtkTextIter *pos,
const gchar *new_text,
gint new_text_length)
{
TalkatuBufferClass *klass = NULL;
g_return_if_fail(TALKATU_IS_BUFFER(buffer));
klass = TALKATU_BUFFER_GET_CLASS(buffer);
if(klass && klass->insert_markup) {
klass->insert_markup(buffer, pos, new_text, new_text_length);
}
}
/**
* talkatu_buffer_insert_markup_with_tags_by_name:
* @buffer: The #TalkatuBuffer instance.
* @pos: The #GtkTextIter where the text should be inserted.
* @new_text: UTF-8 text.
* @new_text_length: The len of @new_text, or -1.
* @first_tag_name: The name of the first tag to apply to @new_text.
* @...: Additional tags to apply to @new_text.
*
* Similar to #talkatu_buffer_insert_markup but allows you to specify
* tags to apply to the newly inserted text.
*/
void
talkatu_buffer_insert_markup_with_tags_by_name(TalkatuBuffer *buffer,
GtkTextIter *pos,
const gchar *new_text,
gint new_text_length,
const gchar *first_tag_name,
...)
{
GtkTextIter start;
gint start_offset;
va_list vargs;
const gchar *tag_name = NULL;
g_return_if_fail(TALKATU_IS_BUFFER(buffer));
g_return_if_fail(pos != NULL);
g_return_if_fail(new_text != NULL);
g_return_if_fail(gtk_text_iter_get_buffer(pos) == GTK_TEXT_BUFFER(buffer));
start_offset = gtk_text_iter_get_offset(pos);
talkatu_buffer_insert_markup(buffer, pos, new_text, new_text_length);
gtk_text_buffer_get_iter_at_offset(GTK_TEXT_BUFFER(buffer), &start, start_offset);
va_start(vargs, first_tag_name);
tag_name = first_tag_name;
while(tag_name != NULL) {
gtk_text_buffer_apply_tag_by_name(
GTK_TEXT_BUFFER(buffer),
tag_name,
&start,
pos
);
tag_name = va_arg(vargs, const gchar *);
}
va_end(vargs);
}
/**
* talkatu_buffer_insert_link:
* @buffer: The #TalkatuBuffer instance.
* @pos: The #GtkTextIter where to insert the link.
* @display_text: The Text to display for the link.
* @url: The url to link to.
*
* Inserts a link into @buffer with the given @url and @display_text. If
* @display_text is not given, @url will be used.
*/
void
talkatu_buffer_insert_link(TalkatuBuffer *buffer,
GtkTextIter *pos,
const gchar *display_text,
const gchar *url)
{
GtkTextTagTable *tag_table = NULL;
GtkTextTag *link_tag = NULL;
GtkTextIter start;
gint start_offset = 0;
glong len = 0;
g_return_if_fail(TALKATU_IS_BUFFER(buffer));
g_return_if_fail(pos != NULL);
g_return_if_fail(url != NULL);
g_return_if_fail(gtk_text_iter_get_buffer(pos) == GTK_TEXT_BUFFER(buffer));
/* figure out what text we're displaying */
if(display_text != NULL) {
len = g_utf8_strlen(display_text, -1);
}
if(len == 0) {
display_text = url;
}
/* insert the text and get the starting iterator */
start_offset = gtk_text_iter_get_offset(pos);
talkatu_buffer_insert_markup(buffer, pos, display_text, -1);
gtk_text_buffer_get_iter_at_offset(GTK_TEXT_BUFFER(buffer), &start, start_offset);
/* create the link_tag */
tag_table = gtk_text_buffer_get_tag_table(GTK_TEXT_BUFFER(buffer));
link_tag = gtk_text_tag_new(NULL);
g_object_set_data_full(
G_OBJECT(link_tag),
TALKATU_BUFFER_LINK_TARGET_ATTRIBUTE,
g_strdup(url),
g_free
);
gtk_text_tag_table_add(tag_table, link_tag);
/* now apply the tags */
gtk_text_buffer_apply_tag_by_name(GTK_TEXT_BUFFER(buffer), TALKATU_TAG_ANCHOR, &start, pos);
gtk_text_buffer_apply_tag(GTK_TEXT_BUFFER(buffer), link_tag, &start, pos);
}
/**
* talkatu_buffer_clear:
* @buffer: The #TalkatuBuffer instance.
*
* Clears all text out of the buffer.
*/
void
talkatu_buffer_clear(TalkatuBuffer *buffer) {
GtkTextIter start, end;
g_return_if_fail(TALKATU_IS_BUFFER(buffer));
gtk_text_buffer_get_bounds(GTK_TEXT_BUFFER(buffer), &start, &end);
gtk_text_buffer_delete(GTK_TEXT_BUFFER(buffer), &start, &end);
}
/**
* talkatu_buffer_get_plain_text:
* @buffer: The #TalkatuBuffer instance.
*
* Returns the text from the buffer without markup.
*
* Returns (transfer full): A copy of the text from @buffer.
*/
gchar *
talkatu_buffer_get_plain_text(TalkatuBuffer *buffer) {
GtkTextIter start, end;
g_return_val_if_fail(TALKATU_IS_BUFFER(buffer), NULL);
gtk_text_buffer_get_bounds(GTK_TEXT_BUFFER(buffer), &start, &end);
return gtk_text_buffer_get_text(GTK_TEXT_BUFFER(buffer), &start, &end, FALSE);
}
/**
* talkatu_buffer_get_is_empty:
* @buffer: The #TalkatuBuffer instance.
*
* Returns whether or not @buffer has any text in it.
*
* Returns: %TRUE if empty, %FALSE otherwise.
*/
gboolean
talkatu_buffer_get_is_empty(TalkatuBuffer *buffer) {
GtkTextIter start, end;
g_return_val_if_fail(TALKATU_IS_BUFFER(buffer), FALSE);
gtk_text_buffer_get_bounds(GTK_TEXT_BUFFER(buffer), &start, &end);
return gtk_text_iter_equal(&start, &end);
}