This repository has been archived on 2020-04-11. You can view files and clone it, but cannot push or open issues or pull requests.
gnoemoe/gnoemoe/widgets/gm-world-input-view.c

407 lines
11 KiB
C

#include <string.h>
#include <gtk/gtk.h>
#include <gdk/gdk.h>
#include <gdk/gdkkeysyms.h>
#include "gm-world-input-view.h"
#include "gm-world-view.h"
#include "gm-world.h"
#include "gm-color-table.h"
#include "gm-debug.h"
#define GM_WORLD_INPUT_VIEW_GET_PRIVATE(object)(G_TYPE_INSTANCE_GET_PRIVATE((object), GM_TYPE_WORLD_INPUT_VIEW, GmWorldInputViewPrivate))
void on_gm_world_input_view_changed(GtkTextBuffer *buffer,
GmWorldInputView *view);
struct _GmWorldInputViewPrivate {
GmColorTable *color_table;
GList **history;
GList *position;
gchar *prefix;
gulong changed_id;
guint idle_scroll;
gboolean is_scrolled;
};
/* Signals */
enum {
TEXT_ACTIVATE,
NUM_SIGNALS
};
static guint world_input_view_signals[NUM_SIGNALS] = {0};
static GtkWidgetClass *widget_parent_class;
G_DEFINE_TYPE(GmWorldInputView, gm_world_input_view, GTK_TYPE_TEXT_VIEW)
/* Private functions */
static void
gm_world_input_view_finalize(GObject *object) {
GmWorldInputView *view = GM_WORLD_INPUT_VIEW(object);
gm_world_input_view_set_color_table(view, NULL);
if (view->priv->idle_scroll) {
g_source_remove(view->priv->idle_scroll);
}
G_OBJECT_CLASS(gm_world_input_view_parent_class)->finalize(object);
}
static gchar *
gm_world_input_view_str_new_value(gchar *old, gchar *new) {
g_free(old);
if (new) {
return g_strdup(new);
} else {
return NULL;
}
}
void
gm_world_input_view_set_text(GmWorldInputView *view, gchar *text, gint len) {
GtkTextIter end;
GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view));
g_signal_handlers_block_by_func(buffer, on_gm_world_input_view_changed,
view);
gtk_text_buffer_set_text(buffer, text, len);
gtk_text_buffer_get_end_iter(buffer, &end);
gtk_text_buffer_place_cursor(buffer, &end);
g_signal_handlers_unblock_by_func(buffer, on_gm_world_input_view_changed,
view);
}
static void
gm_world_input_view_reset_prefix(GmWorldInputView *view) {
view->priv->prefix = gm_world_input_view_str_new_value(
view->priv->prefix, NULL);
}
gboolean
gm_world_input_view_key_press_event(GtkWidget *widget, GdkEventKey *event) {
GmWorldInputView *view = GM_WORLD_INPUT_VIEW(widget);
gchar *text;
gboolean result = FALSE, isUp;
GtkTextBuffer *buf;
GtkTextIter start, end, cursor;
GtkTextMark *insert;
GList *item, *found = NULL;
gint line, len;
buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view));
gtk_text_buffer_get_bounds(buf, &start, &end);
switch (event->keyval) {
case GDK_Up: case GDK_Down:
isUp = event->keyval == GDK_Up;
if (!view->priv->history) {
break;
}
insert = gtk_text_buffer_get_insert(buf);
gtk_text_buffer_get_iter_at_mark(buf, &cursor, insert);
line = gtk_text_iter_get_line(&cursor);
if ((isUp && line != 0) || (!isUp && line !=
gtk_text_buffer_get_line_count(buf) - 1)) {
break;
}
text = gtk_text_buffer_get_text(buf, &start, &end, FALSE);
// If the current position is empty then append a new history item
if (!view->priv->position) {
*view->priv->history = g_list_append(*view->priv->history,
g_strdup(text));
view->priv->position = g_list_last(*view->priv->history);
}
// If there is nowhere to move, don't even bother
if ((isUp && !view->priv->position->prev)
|| (!isUp && !view->priv->position->next)) {
result = TRUE;
break;
}
// If the current prefix is NULL then we set the new prefix to
// the current text
if (view->priv->prefix == NULL) {
view->priv->prefix = g_strdup(text);
}
// If the prefix is an empty string then simply advance to the
// next item
if (*(view->priv->prefix) == '\0') {
if (isUp) {
found = view->priv->position->prev;
} else {
found = view->priv->position->next;
}
} else {
// Else find the closest matching history line
item = isUp ? view->priv->position->prev :
view->priv->position->next;
while (item) {
if (strncmp((gchar *)item->data, view->priv->prefix,
strlen(view->priv->prefix)) == 0) {
// Change current position to the matched item
found = item;
break;
}
item = isUp ? item->prev : item->next;
}
}
// If a match is found then set this history text
if (found) {
// Change the data of the current position to the text
// now in the buffer.
view->priv->position->data =
gm_world_input_view_str_new_value(
view->priv->position->data, text);
gm_world_input_view_set_text(view, (gchar *)found->data, -1);
view->priv->position = found;
if (found == g_list_last(*view->priv->history)) {
gm_world_input_view_reset_prefix(view);
}
}
g_free(text);
result = TRUE;
break;
case GDK_Return:
// Emit the text_activate signal
if (!(event->state & GDK_CONTROL_MASK) &&
!(event->state & GDK_SHIFT_MASK)) {
text = gtk_text_buffer_get_text(buf, &start, &end, FALSE);
g_signal_emit(view, world_input_view_signals[TEXT_ACTIVATE], 0,
text);
if (view->priv->history) {
item = g_list_last(*view->priv->history);
if (item) {
item->data = gm_world_input_view_str_new_value(
(gchar *)(item->data), text);
} else {
*view->priv->history =
g_list_append(*view->priv->history,
g_strdup(text));
}
}
len = g_list_length(*view->priv->history);
for (line = 0; line < len - 500; ++line) {
*view->priv->history = g_list_remove(*view->priv->history,
(*view->priv->history)->data);
}
// Append new empty history item which will become our new
// current item
if (view->priv->history) {
*view->priv->history =
g_list_append(*view->priv->history, g_strdup(""));
view->priv->position = g_list_last(*view->priv->history);
}
gm_world_input_view_reset_prefix(view);
// Set textview text to an empty string
gm_world_input_view_set_text(view, "", 0);
on_gm_world_input_view_changed(buf, view);
g_free(text);
result = TRUE;
}
break;
default:
break;
}
if (!result) {
if (widget_parent_class->key_press_event) {
return widget_parent_class->key_press_event(widget, event);
}
}
return result;
}
static void
gm_world_input_view_class_init(GmWorldInputViewClass *klass) {
GObjectClass *object_class = G_OBJECT_CLASS(klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
object_class->finalize = gm_world_input_view_finalize;
widget_class->key_press_event = gm_world_input_view_key_press_event;
widget_parent_class = GTK_WIDGET_CLASS(gm_world_input_view_parent_class);
world_input_view_signals[TEXT_ACTIVATE] =
g_signal_new("text_activate",
G_OBJECT_CLASS_TYPE(object_class),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET(GmWorldInputViewClass, text_activate),
NULL, NULL,
g_cclosure_marshal_VOID__STRING,
G_TYPE_NONE,
1,
G_TYPE_STRING);
g_type_class_add_private(object_class, sizeof(GmWorldInputViewPrivate));
}
static void
gm_world_input_view_init(GmWorldInputView *view) {
view->priv = GM_WORLD_INPUT_VIEW_GET_PRIVATE(view);
view->priv->color_table = NULL;
view->priv->history = NULL;
view->priv->position = NULL;
view->priv->prefix = NULL;
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(view), GTK_WRAP_WORD_CHAR);
gtk_text_view_set_accepts_tab(GTK_TEXT_VIEW(view), TRUE);
gtk_text_view_set_left_margin(GTK_TEXT_VIEW(view), 3);
gtk_text_view_set_right_margin(GTK_TEXT_VIEW(view), 3);
gtk_text_view_set_pixels_above_lines(GTK_TEXT_VIEW(view), 1);
gtk_text_view_set_pixels_below_lines(GTK_TEXT_VIEW(view), 1);
g_signal_connect(gtk_text_view_get_buffer(GTK_TEXT_VIEW(view)), "changed",
G_CALLBACK(on_gm_world_input_view_changed), view);
}
/* Public functions */
GtkWidget *
gm_world_input_view_new() {
GtkWidget *result;
GmColorTable *table = gm_color_table_new();
result = gm_world_input_view_new_with_color_table(table);
g_object_unref(table);
return result;
}
GtkWidget *
gm_world_input_view_new_with_color_table(GmColorTable *color_table) {
GmWorldInputView *view = GM_WORLD_INPUT_VIEW(
g_object_new(GM_TYPE_WORLD_INPUT_VIEW, NULL));
gm_world_input_view_set_color_table(view, color_table);
return GTK_WIDGET(view);
}
void
gm_world_input_view_set_history(GmWorldInputView *view, GList **history) {
view->priv->history = history;
view->priv->position = NULL;
gm_world_input_view_reset_prefix(view);
}
GList **
gm_world_input_view_history(GmWorldInputView *view) {
return view->priv->history;
}
void
gm_world_input_view_set_color_table(GmWorldInputView *view,
GmColorTable *color_table) {
// This will register the view as being schemed by color_table
gm_register_schemed(GTK_WIDGET(view), color_table,
GM_SCHEMED_COLORS | GM_SCHEMED_FONT);
if (view->priv->color_table != NULL) {
g_object_unref(view->priv->color_table);
}
if (color_table != NULL) {
view->priv->color_table = g_object_ref(color_table);
} else {
view->priv->color_table = NULL;
}
}
GmColorTable *
gm_world_input_view_color_table(GmWorldInputView *view) {
return view->priv->color_table;
}
/* Callbacks */
gboolean
idle_scroll(gpointer user_data) {
GmWorldInputView *view = GM_WORLD_INPUT_VIEW(user_data);
GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view));
view->priv->idle_scroll = 0;
gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(view),
gtk_text_buffer_get_insert(buffer), 0.0, TRUE, 0.5, 1.0);
return FALSE;
}
void
on_gm_world_input_view_changed(GtkTextBuffer *buffer,
GmWorldInputView *view) {
GtkTextIter start, end;
GtkWidget *parent = gtk_widget_get_parent(GTK_WIDGET(view));
GtkScrolledWindow *sw;
gint lineheight, y;
if (view->priv->prefix) {
gtk_text_buffer_get_bounds(buffer, &start, &end);
if (gtk_text_iter_equal(&start, &end)) {
// Reset position to the last item in the history
view->priv->position = g_list_last(*view->priv->history);
gm_world_input_view_reset_prefix(view);
}
}
if (!GTK_IS_SCROLLED_WINDOW(parent)) {
return;
}
if (!view->priv->is_scrolled &&
gtk_text_buffer_get_line_count(buffer) > 6) {
sw = GTK_SCROLLED_WINDOW(parent);
view->priv->is_scrolled = TRUE;
gtk_text_buffer_get_start_iter(buffer, &start);
gtk_text_view_get_line_yrange(GTK_TEXT_VIEW(view), &start, &y,
&lineheight);
gtk_widget_set_size_request(parent, -1,
lineheight * 6 +
gtk_container_get_border_width(GTK_CONTAINER(sw))
+ 2);
gtk_scrolled_window_set_policy(sw, GTK_POLICY_NEVER, GTK_POLICY_ALWAYS);
view->priv->idle_scroll = g_idle_add(idle_scroll, view);
} else if (view->priv->is_scrolled &&
gtk_text_buffer_get_line_count(buffer) <= 6) {
sw = GTK_SCROLLED_WINDOW(parent);
view->priv->is_scrolled = FALSE;
gtk_scrolled_window_set_policy(sw, GTK_POLICY_NEVER, GTK_POLICY_NEVER);
gtk_widget_set_size_request(parent, -1, -1);
}
}