talkatu/talkatu

bf428b4a68da
Move TalkatuHistory from a GtkTextView base to GtkListBox

Make TalkatuInput return the local time if a timestamp has not yet been set.

Add an toggle button for setting the message's edited state

Added TalkatuHistoryRow which is what will be used to display message

Add TalkatuHistoryRow to the documentation.

Add TalkatuScrolledWindow that subclasses GtkScrolledWindow but autoscrolls and has keybindings callbacks for page up and page down.

Testing Done:
Compile, docs, and manual testing in the demo. The i18n stuff is broken for unrelated reasons, which I'll fix in another review request.

Bugs closed: TALKATU-78

Reviewed at https://reviews.imfreedom.org/r/56/
/*
* talkatu
* Copyright (C) 2017-2019 Gary Kramlich <grim@reaperworld.com>
*
* 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 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see <http://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>
/**
* SECTION:talkatubuffer
* @Title: Text Buffer
* @Short_description: Common behavior for Talkatu text buffers.
*
* Talkatu implements a couple of different types of text buffers. This API
* tries to abstract the commonalities between them and make it easier to
* create text buffers for additional markup types.
*/
/**
* TALKATU_TYPE_BUFFER:
*
* The standard _get_type macro for #TalkatuBuffer.
*/
/**
* 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.
*
* Get's 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.
*
* Similiar 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);
}