#include #include #include #include #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); } }