Input history for text buffers and text grids.
[rodin/chimara.git] / libchimara / input.c
index f48aa848fbc3ccda941dcac94e14d1e2474ae6be..605616d0e38c9eb94473ab292c592a984bc1af8c 100644 (file)
@@ -22,7 +22,7 @@ request_char_event_common(winid_t win, gboolean unicode)
        ChimaraGlkPrivate *glk_data = g_private_get(glk_data_key);
 
        win->input_request_type = unicode? INPUT_REQUEST_CHARACTER_UNICODE : INPUT_REQUEST_CHARACTER;
-       g_signal_handler_unblock( G_OBJECT(win->widget), win->keypress_handler );
+       g_signal_handler_unblock( G_OBJECT(win->widget), win->char_input_keypress_handler );
 
        gdk_threads_enter();
        
@@ -93,7 +93,7 @@ glk_cancel_char_event(winid_t win)
        if(win->input_request_type == INPUT_REQUEST_CHARACTER || win->input_request_type == INPUT_REQUEST_CHARACTER_UNICODE)
        {
                win->input_request_type = INPUT_REQUEST_NONE;
-               g_signal_handler_block( G_OBJECT(win->widget), win->keypress_handler );
+               g_signal_handler_block( G_OBJECT(win->widget), win->char_input_keypress_handler );
        }
 }
 
@@ -153,11 +153,13 @@ text_grid_request_line_event_common(winid_t win, glui32 maxlen, gboolean insert,
     gtk_widget_modify_base(win->input_entry, GTK_STATE_NORMAL, &background);
     
     g_signal_connect(win->input_entry, "activate", G_CALLBACK(on_input_entry_activate), win);
+    g_signal_connect(win->input_entry, "key-press-event", G_CALLBACK(on_input_entry_key_press_event), win);
+    win->line_input_entry_changed = g_signal_connect(win->input_entry, "changed", G_CALLBACK(on_input_entry_changed), win);
     
     gtk_widget_show(win->input_entry);
     gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(win->widget), win->input_entry, win->input_anchor);
        
-       g_signal_handler_unblock( G_OBJECT(win->widget), win->keypress_handler );
+       g_signal_handler_unblock( G_OBJECT(win->widget), win->char_input_keypress_handler );
 
        gtk_widget_grab_focus(win->input_entry);
        
@@ -355,12 +357,12 @@ glk_cancel_line_event(winid_t win, event_t *event)
        if(win->input_request_type != INPUT_REQUEST_LINE && win->input_request_type != INPUT_REQUEST_LINE_UNICODE)
                return;
 
-       g_signal_handler_block( G_OBJECT(win->widget), win->keypress_handler );
+       g_signal_handler_block( G_OBJECT(win->widget), win->char_input_keypress_handler );
 
        int chars_written = 0;
 
        if(win->type == wintype_TextGrid) {
-               g_signal_handler_block( G_OBJECT(win->widget), win->keypress_handler );
+               g_signal_handler_block( G_OBJECT(win->widget), win->char_input_keypress_handler );
                chars_written = finish_text_grid_line_input(win, FALSE);
        } else if(win->type == wintype_TextBuffer) {
                GtkTextBuffer *window_buffer = gtk_text_view_get_buffer( GTK_TEXT_VIEW(win->widget) );
@@ -385,31 +387,8 @@ glk_cancel_line_event(winid_t win, event_t *event)
 
 /* Internal function: General callback for signal key-press-event on a text buffer or text grid window. Used in character input on both text buffers and grids, and also in line input on grids, to redirect keystrokes to the line input field. Blocked when not in use. */
 gboolean
-on_window_key_press_event(GtkWidget *widget, GdkEventKey *event, winid_t win)
+on_char_input_key_press_event(GtkWidget *widget, GdkEventKey *event, winid_t win)
 {
-       /* If this is a text grid window, and line input is active, then redirect the key press to the line input GtkEntry */
-       if( win->type == wintype_TextGrid && (win->input_request_type == INPUT_REQUEST_LINE || win->input_request_type == INPUT_REQUEST_LINE_UNICODE) )
-       {
-               if(event->keyval == GDK_Up || event->keyval == GDK_KP_Up
-                   || event->keyval == GDK_Down || event->keyval == GDK_KP_Down
-                   || event->keyval == GDK_Left || event->keyval == GDK_KP_Left
-                   || event->keyval == GDK_Right || event->keyval == GDK_KP_Right
-                   || event->keyval == GDK_Tab || event->keyval == GDK_KP_Tab
-                   || event->keyval == GDK_Page_Up || event->keyval == GDK_KP_Page_Up
-                   || event->keyval == GDK_Page_Down || event->keyval == GDK_KP_Page_Down
-                   || event->keyval == GDK_Home || event->keyval == GDK_KP_Home
-                   || event->keyval == GDK_End || event->keyval == GDK_KP_End)
-                       return FALSE; /* Don't redirect these keys */
-               gtk_widget_grab_focus(win->input_entry);
-               gtk_editable_set_position(GTK_EDITABLE(win->input_entry), -1);
-               gboolean retval = TRUE;
-               g_signal_emit_by_name(win->input_entry, "key-press-event", event, &retval);
-               return retval; /* Block this key event if the entry handled it */
-       }
-       if(win->input_request_type != INPUT_REQUEST_CHARACTER && 
-               win->input_request_type != INPUT_REQUEST_CHARACTER_UNICODE)
-               return FALSE;
-
        glui32 keycode = keyval_to_glk_keycode(event->keyval, win->input_request_type == INPUT_REQUEST_CHARACTER_UNICODE);
 
        ChimaraGlk *glk = CHIMARA_GLK(gtk_widget_get_ancestor(widget, CHIMARA_TYPE_GLK));
@@ -419,11 +398,86 @@ on_window_key_press_event(GtkWidget *widget, GdkEventKey *event, winid_t win)
 
        /* Only one keypress will be handled */
        win->input_request_type = INPUT_REQUEST_NONE;
-       g_signal_handler_block( G_OBJECT(win->widget), win->keypress_handler );
+       g_signal_handler_block( G_OBJECT(win->widget), win->char_input_keypress_handler );
 
        return TRUE;
 }
 
+gboolean
+on_line_input_key_press_event(GtkWidget *widget, GdkEventKey *event, winid_t win)
+{
+       switch(win->type)
+       {
+               case wintype_TextBuffer:
+                       if(event->keyval == GDK_Up || event->keyval == GDK_KP_Up
+                               || event->keyval == GDK_Down || event->keyval == GDK_KP_Down)
+                       {
+                               /* Prevent falling off the end of the history list */
+                               if( (event->keyval == GDK_Up || event->keyval == GDK_KP_Up)
+                                       && win->history_pos && win->history_pos->next == NULL)
+                                       return TRUE;
+                               if( (event->keyval == GDK_Down || event->keyval == GDK_KP_Down)
+                                       && (win->history_pos == NULL || win->history_pos->prev == NULL) )
+                                       return TRUE;
+                       
+                               GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(win->widget));
+                               GtkTextIter start, end;
+                               /* Erase any text that was already typed */
+                               GtkTextMark *input_position = gtk_text_buffer_get_mark(buffer, "input_position");
+                               gtk_text_buffer_get_iter_at_mark(buffer, &start, input_position);
+                               gtk_text_buffer_get_end_iter(buffer, &end);
+                               
+                               if(win->history_pos == NULL) {
+                                       gchar *current_input = gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
+                                       win->history = g_list_prepend(win->history, current_input);
+                                       win->history_pos = win->history;
+                               }
+                               
+                               gtk_text_buffer_delete(buffer, &start, &end);
+               
+                               if(event->keyval == GDK_Up || event->keyval == GDK_KP_Up)
+                               {
+                                       if(win->history_pos)
+                                               win->history_pos = g_list_next(win->history_pos);
+                                       else
+                                               win->history_pos = win->history;
+                               }
+                               else /* down */
+                                       win->history_pos = g_list_previous(win->history_pos);
+               
+                               /* Insert the history item into the window */
+                               gtk_text_buffer_get_end_iter(buffer, &end);
+                               
+                               g_signal_handler_block(buffer, win->insert_text_handler);
+                               gtk_text_buffer_insert_with_tags_by_name(buffer, &end, win->history_pos->data, -1, "input", NULL);
+                               g_signal_handler_unblock(buffer, win->insert_text_handler);
+                               return TRUE;
+                       }
+                       return FALSE;
+
+               /* If this is a text grid window, then redirect the key press to the line input GtkEntry */
+               case wintype_TextGrid:
+               {
+                       if(event->keyval == GDK_Up || event->keyval == GDK_KP_Up
+                               || event->keyval == GDK_Down || event->keyval == GDK_KP_Down
+                               || event->keyval == GDK_Left || event->keyval == GDK_KP_Left
+                               || event->keyval == GDK_Right || event->keyval == GDK_KP_Right
+                               || event->keyval == GDK_Tab || event->keyval == GDK_KP_Tab
+                               || event->keyval == GDK_Page_Up || event->keyval == GDK_KP_Page_Up
+                               || event->keyval == GDK_Page_Down || event->keyval == GDK_KP_Page_Down
+                               || event->keyval == GDK_Home || event->keyval == GDK_KP_Home
+                               || event->keyval == GDK_End || event->keyval == GDK_KP_End)
+                               return FALSE; /* Don't redirect these keys */
+                       gtk_widget_grab_focus(win->input_entry);
+                       gtk_editable_set_position(GTK_EDITABLE(win->input_entry), -1);
+                       gboolean retval = TRUE;
+                       g_signal_emit_by_name(win->input_entry, "key-press-event", event, &retval);
+                       return retval; /* Block this key event if the entry handled it */
+               }
+       }
+       return FALSE;
+}
+
 /* Internal function: finish handling a line input request, for both text grid and text buffer windows. */
 static int
 write_to_window_buffer(winid_t win, const gchar *inserted_text)
@@ -503,6 +557,18 @@ finish_text_buffer_line_input(winid_t win, gboolean emit_signal)
                g_assert(glk);
                g_signal_emit_by_name(glk, "line-input", win->rock, inserted_text);
        }
+       
+       /* Add the text to the window input history */
+       if(win->history_pos != NULL)
+       {
+               g_free(win->history->data);
+               win->history = g_list_delete_link(win->history, win->history);
+       }
+       if(*inserted_text != 0)
+               win->history = g_list_prepend(win->history, g_strdup(inserted_text));   
+       
+       win->history_pos = NULL;
+       
        g_free(inserted_text);
 
        return chars_written;
@@ -546,8 +612,18 @@ finish_text_grid_line_input(winid_t win, gboolean emit_signal)
                g_assert(glk);
                g_signal_emit_by_name(glk, "line-input", win->rock, text);
     }
+       
+       /* Add the text to the window input history */
+       if(win->history_pos != NULL)
+       {
+               g_free(win->history->data);
+               win->history = g_list_delete_link(win->history, win->history);
+       }
+       if(*text != 0)
+               win->history = g_list_prepend(win->history, g_strdup(text));    
+       win->history_pos = NULL;
+       
        g_free(text);
-
        return chars_written;
 }
 
@@ -559,6 +635,13 @@ pasted into the window. */
 void
 after_window_insert_text(GtkTextBuffer *textbuffer, GtkTextIter *location, gchar *text, gint len, winid_t win) 
 {
+       /* Set the history position to NULL and erase the text we were already editing */
+       if(win->history_pos != NULL)
+       {
+               g_free(win->history->data);
+               win->history = g_list_delete_link(win->history, win->history);
+               win->history_pos = NULL;
+       }
        if( strchr(text, '\n') != NULL ) 
        {
                /* Remove signal handlers */
@@ -579,13 +662,67 @@ in a text grid window. */
 void
 on_input_entry_activate(GtkEntry *input_entry, winid_t win)
 {
-       g_signal_handler_block(win->widget, win->keypress_handler);
+       g_signal_handler_block(win->widget, win->char_input_keypress_handler);
 
        int chars_written = finish_text_grid_line_input(win, TRUE);
        ChimaraGlk *glk = CHIMARA_GLK(gtk_widget_get_ancestor(win->widget, CHIMARA_TYPE_GLK));
        event_throw(glk, evtype_LineInput, win, chars_written, 0);
 }
 
+/* Internal function: Callback for signal key-press-event on the line input 
+GtkEntry in a text grid window. */
+gboolean
+on_input_entry_key_press_event(GtkEntry *input_entry, GdkEventKey *event, winid_t win)
+{
+       if(event->keyval == GDK_Up || event->keyval == GDK_KP_Up
+               || event->keyval == GDK_Down || event->keyval == GDK_KP_Down)
+       {
+               /* Prevent falling off the end of the history list */
+               if( (event->keyval == GDK_Up || event->keyval == GDK_KP_Up)
+                       && win->history_pos && win->history_pos->next == NULL)
+                       return TRUE;
+               if( (event->keyval == GDK_Down || event->keyval == GDK_KP_Down)
+                       && (win->history_pos == NULL || win->history_pos->prev == NULL) )
+                       return TRUE;
+       
+               if(win->history_pos == NULL) 
+               {
+                       gchar *current_input = gtk_entry_get_text(input_entry);
+                       win->history = g_list_prepend(win->history, current_input);
+                       win->history_pos = win->history;
+               }
+
+               if(event->keyval == GDK_Up || event->keyval == GDK_KP_Up)
+               {
+                       if(win->history_pos)
+                               win->history_pos = g_list_next(win->history_pos);
+                       else
+                               win->history_pos = win->history;
+               }
+               else /* down */
+                       win->history_pos = g_list_previous(win->history_pos);
+
+               /* Insert the history item into the window */
+               g_signal_handler_block(input_entry, win->line_input_entry_changed);
+               gtk_entry_set_text(input_entry, win->history_pos->data);
+               g_signal_handler_unblock(input_entry, win->line_input_entry_changed);
+               return TRUE;
+       }
+       return FALSE;
+}
+
+void
+on_input_entry_changed(GtkEditable *editable, winid_t win)
+{
+       /* Set the history position to NULL and erase the text we were already editing */
+       if(win->history_pos != NULL)
+       {
+               g_free(win->history->data);
+               win->history = g_list_delete_link(win->history, win->history);
+               win->history_pos = NULL;
+       }
+}
+
 glui32
 keyval_to_glk_keycode(guint keyval, gboolean unicode)
 {
@@ -697,7 +834,7 @@ force_line_input_from_queue(winid_t win, event_t *event)
        else if(win->type == wintype_TextGrid)
        {
                /* Remove signal handlers so the line input doesn't get picked up again */
-               g_signal_handler_block(win->widget, win->keypress_handler);
+               g_signal_handler_block(win->widget, win->char_input_keypress_handler);
                
                /* Insert the forced input into the window */
                gtk_entry_set_text(GTK_ENTRY(win->input_entry), text);