Implemented line input in text grid windows using a GtkEntry.
[rodin/chimara.git] / src / window.c
index cd2ff7aa04e8886c6e3417020452a71222286934..8351378363e343a1450b40469a6647958e531ad7 100644 (file)
@@ -1,7 +1,7 @@
 #include "window.h"
+#include "chimara-glk-private.h"
 
-/* Global tree of all windows */
-static GNode *root_window = NULL;
+extern ChimaraGlkPrivate *glk_data;
 
 /**
  * glk_window_iterate:
@@ -25,7 +25,7 @@ glk_window_iterate(winid_t win, glui32 *rockptr)
        GNode *retnode;
        
        if(win == NULL)
-               retnode = root_window;
+               retnode = glk_data->root_window;
        else
        {
                GNode *node = win->window_node;
@@ -90,7 +90,7 @@ glk_window_get_type(winid_t win)
  * returns %NULL, since the root window has no parent. Remember that the parent
  * of every window is a pair window; other window types are always childless.
  *
- * Returns: A window.
+ * Returns: A window, or %NULL.
  */
 winid_t
 glk_window_get_parent(winid_t win)
@@ -124,16 +124,25 @@ glk_window_get_sibling(winid_t win)
 /**
  * glk_window_get_root:
  * 
- * Returns the root window. If there are no windows, this returns #NULL.
+ * Returns the root window. If there are no windows, this returns %NULL.
  *
- * Returns: A window, or #NULL.
+ * Returns: A window, or %NULL.
  */
 winid_t
 glk_window_get_root()
 {
-       if(root_window == NULL)
+       if(glk_data->root_window == NULL)
                return NULL;
-       return (winid_t)root_window->data;
+       return (winid_t)glk_data->root_window->data;
+}
+
+/* Determine the size of a "0" character in pixels */
+static void
+text_window_get_char_size(GtkWidget *textview, int *width, int *height)
+{
+    PangoLayout *zero = gtk_widget_create_pango_layout(textview, "0");
+    pango_layout_get_pixel_size(zero, width, height);
+    g_object_unref(zero);
 }
 
 /**
@@ -157,41 +166,33 @@ glk_window_get_root()
  * position, size, and type specified by @method, @size, and @wintype. See the
  * Glk documentation for the window placement algorithm.
  *
- * Returns: the new window.
+ * Returns: the new window, or %NULL on error.
  */
 winid_t
 glk_window_open(winid_t split, glui32 method, glui32 size, glui32 wintype, 
                 glui32 rock)
 {
-       extern GtkBuilder *builder;
-
+       /*
        if(split)
        {
                g_warning("glk_window_open: splitting of windows not implemented");
                return NULL;
        }
+       */
 
-       if(root_window != NULL)
+       if(split == NULL && glk_data->root_window != NULL)
        {
-               g_warning("glk_window_open: there is already a window");
+               g_warning("glk_window_open: there is already a root window");
                return NULL;
        }
+       
+       gdk_threads_enter();
+       
        /* We only create one window and don't support any more than that */
        winid_t win = g_new0(struct glk_window_struct, 1);
-       root_window = g_node_new(win);
-
        win->rock = rock;
        win->type = wintype;
-
-       gdk_threads_enter();
-
-       GtkBox *vbox = GTK_BOX( gtk_builder_get_object(builder, "vbox") );                      
-       if(vbox == NULL)
-       {
-               error_dialog(NULL, NULL, "Could not find vbox");
-               gdk_threads_leave();
-               return NULL;
-       }
+       win->window_node = g_node_new(win);
 
        switch(wintype)
        {
@@ -199,16 +200,55 @@ glk_window_open(winid_t split, glui32 method, glui32 size, glui32 wintype,
                {
                        /* A blank window will be a label without any text */
                        GtkWidget *label = gtk_label_new("");
-                       gtk_box_pack_end(vbox, label, TRUE, TRUE, 0);
                        gtk_widget_show(label);
                        
                        win->widget = label;
+                       win->frame = label;
+                       /* A blank window has no size */
+                       win->unit_width = 0;
+                       win->unit_height = 0;
                        /* You can print to a blank window's stream, but it does nothing */
                        win->window_stream = window_stream_new(win);
                        win->echo_stream = NULL;
                }
                        break;
+               
+               case wintype_TextGrid:
+               {
+                   GtkWidget *scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
+                   GtkWidget *textview = gtk_text_view_new();
+                   
+                   gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW(scrolledwindow), GTK_POLICY_NEVER, GTK_POLICY_NEVER );
+                   
+                   gtk_text_view_set_wrap_mode( GTK_TEXT_VIEW(textview), GTK_WRAP_CHAR );
+                   gtk_text_view_set_editable( GTK_TEXT_VIEW(textview), FALSE );
+                   
+                   gtk_container_add( GTK_CONTAINER(scrolledwindow), textview );
+                   gtk_widget_show_all(scrolledwindow);
+                               
+                       /* Set the window's font */
+                   /* TODO: Use Pango to pick out a monospace font on the system */
+                       PangoFontDescription *font = pango_font_description_from_string("Monospace");
+                       gtk_widget_modify_font(textview, font);
+                       pango_font_description_free(font);
+                   
+                   win->widget = textview;
+                   win->frame = scrolledwindow;
+                   text_window_get_char_size( textview, &(win->unit_width), &(win->unit_height) );
+                       
+                       /* Set the other parameters (width and height are set later) */
+                       win->window_stream = window_stream_new(win);
+                       win->echo_stream = NULL;
+                       win->input_request_type = INPUT_REQUEST_NONE;
+                       win->line_input_buffer = NULL;
+                       win->line_input_buffer_unicode = NULL;
                        
+                       /* Connect signal handlers */
+                       win->keypress_handler = g_signal_connect( G_OBJECT(textview), "key-press-event", G_CALLBACK(on_window_key_press_event), win );
+                       g_signal_handler_block( G_OBJECT(textview), win->keypress_handler );
+               }
+                   break;
+               
                case wintype_TextBuffer:
                {
                        GtkWidget *scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
@@ -219,10 +259,13 @@ glk_window_open(winid_t split, glui32 method, glui32 size, glui32 wintype,
                        gtk_text_view_set_editable( GTK_TEXT_VIEW(textview), FALSE );
 
                        gtk_container_add( GTK_CONTAINER(scrolledwindow), textview );
-                       gtk_box_pack_end(vbox, scrolledwindow, TRUE, TRUE, 0);
                        gtk_widget_show_all(scrolledwindow);
 
                        win->widget = textview;
+                       win->frame = scrolledwindow;
+            text_window_get_char_size( textview, &(win->unit_width), &(win->unit_height) );
+                       
+                       /* Set the other parameters */
                        win->window_stream = window_stream_new(win);
                        win->echo_stream = NULL;
                        win->input_request_type = INPUT_REQUEST_NONE;
@@ -251,12 +294,114 @@ glk_window_open(winid_t split, glui32 method, glui32 size, glui32 wintype,
                        gdk_threads_leave();
                        g_warning("%s: unsupported window type", __func__);
                        g_free(win);
+                       g_node_destroy(glk_data->root_window);
+                       glk_data->root_window = NULL;
                        return NULL;
        }
 
-       gdk_threads_leave();
+       if(split)
+       {
+               /* When splitting, construct a new parent window
+                * copying most characteristics from the window that is being split */
+               winid_t pair = g_new0(struct glk_window_struct, 1);
+               pair->rock = 0;
+               pair->type = wintype_Pair;
+               pair->window_node = g_node_new(pair);
+               pair->unit_width = split->unit_width;
+               pair->unit_height = split->unit_height;
+               pair->window_stream = NULL;
+               pair->echo_stream = NULL;
+
+               /* Insert the new window into the window tree */
+               if(split->window_node->parent == NULL)
+               {
+                       glk_data->root_window = pair->window_node;
+               } else {
+                       g_node_append(split->window_node->parent, pair->window_node);
+                       g_node_unlink(split->window_node);
+               }
+
+               /* Keep track of the parent widget of the window that is being split */
+               GtkWidget* old_parent = gtk_widget_get_parent(split->frame);
+               gtk_widget_ref(split->frame);
+               gtk_widget_unparent(split->frame);
 
-       win->window_node = root_window;
+               /* Place the windows in the correct order */
+               switch(method & winmethod_DirMask)
+               {
+                       case winmethod_Left:
+                               pair->widget = gtk_hbox_new(FALSE, 0);
+                               gtk_box_pack_end(GTK_BOX(pair->widget), split->frame, TRUE, TRUE, 0);
+                               gtk_box_pack_end(GTK_BOX(pair->widget), win->frame, TRUE, TRUE, 0);
+                               g_node_append(pair->window_node, split->window_node);
+                               g_node_append(pair->window_node, win->window_node);
+                               break;
+                       case winmethod_Right:
+                               pair->widget = gtk_hbox_new(FALSE, 0);
+                               gtk_box_pack_end(GTK_BOX(pair->widget), win->frame, TRUE, TRUE, 0);
+                               gtk_box_pack_end(GTK_BOX(pair->widget), split->frame, TRUE, TRUE, 0);
+                               g_node_append(pair->window_node, win->window_node);
+                               g_node_append(pair->window_node, split->window_node);
+                               break;
+                       case winmethod_Above:
+                               pair->widget = gtk_vbox_new(FALSE, 0);
+                               gtk_box_pack_end(GTK_BOX(pair->widget), split->frame, TRUE, TRUE, 0);
+                               gtk_box_pack_end(GTK_BOX(pair->widget), win->frame, TRUE, TRUE, 0);
+                               g_node_append(pair->window_node, split->window_node);
+                               g_node_append(pair->window_node, win->window_node);
+                               break;
+                       case winmethod_Below:
+                               pair->widget = gtk_vbox_new(FALSE, 0);
+                               gtk_box_pack_end(GTK_BOX(pair->widget), win->frame, TRUE, TRUE, 0);
+                               gtk_box_pack_end(GTK_BOX(pair->widget), split->frame, TRUE, TRUE, 0);
+                               g_node_append(pair->window_node, win->window_node);
+                               g_node_append(pair->window_node, split->window_node);
+                               break;
+               }
+               gtk_widget_unref(split->frame);
+
+               /* TODO: set the new size of the windows */
+
+               pair->frame = pair->widget;
+               gtk_widget_set_parent(pair->widget, old_parent);
+               gtk_widget_show(pair->widget);
+       } else {
+               /* Set the window as root window */
+               glk_data->root_window = win->window_node;
+               gtk_widget_set_parent(win->frame, GTK_WIDGET(glk_data->self));
+               gtk_widget_queue_resize(GTK_WIDGET(glk_data->self));
+       }
+
+    /* For text grid windows, wait until GTK draws the window (see note in glk_window_get_size() ), calculate the size and fill the buffer with blanks. */
+    if(wintype == wintype_TextGrid)
+    {
+        while(win->widget->allocation.width == 1 && win->widget->allocation.height == 1)
+        {
+            /* Release the GDK lock momentarily */
+            gdk_threads_leave();
+            gdk_threads_enter();
+            while(gtk_events_pending())
+                gtk_main_iteration();
+        }
+        win->width = (glui32)(win->widget->allocation.width / win->unit_width);
+        win->height = (glui32)(win->widget->allocation.height / win->unit_height);
+                
+        /* Mark the cursor position */
+        GtkTextIter begin;
+        GtkTextBuffer *buffer = gtk_text_view_get_buffer( GTK_TEXT_VIEW(win->widget) );
+        gtk_text_buffer_get_start_iter(buffer, &begin);
+        gtk_text_buffer_create_mark(buffer, "cursor_position", &begin, TRUE);
+        
+        /* Fill the buffer with blanks and move the cursor to the upper left */
+        gdk_threads_leave();
+        glk_window_clear(win);
+        gdk_threads_enter();
+        
+        /* Apparently this only works after the window has been realized */
+        gtk_text_view_set_overwrite( GTK_TEXT_VIEW(win->widget), TRUE );
+    }
+
+       gdk_threads_leave();
 
        return win;
 }
@@ -276,26 +421,69 @@ glk_window_open(winid_t split, glui32 method, glui32 size, glui32 wintype,
 void
 glk_window_close(winid_t win, stream_result_t *result)
 {
+       GNode* parent_node;
+
        g_return_if_fail(win != NULL);
 
+       gdk_threads_enter();
+
        switch(win->type)
        {
+               case wintype_Blank:
+                       gtk_widget_destroy(win->widget);
+                       break;
+       
+           case wintype_TextGrid:
                case wintype_TextBuffer:
-                       gtk_widget_destroy( gtk_widget_get_parent(win->widget) );
+                       gtk_widget_destroy(win->frame);
                        /* TODO: Cancel all input requests */
                        break;
 
-               case wintype_Blank:
+               case wintype_Pair:
+               {
+                       GNode* left_child = g_node_first_child(win->window_node);
+                       GNode* right_child = g_node_last_child(win->window_node);
+
+                       glk_window_close((winid_t) left_child->data, result);
+                       glk_window_close((winid_t) right_child->data, result);
+
                        gtk_widget_destroy(win->widget);
+               }
                        break;
+
+               default:
+                       g_warning("%s: unsupported window type", __func__);
+                       gdk_threads_leave();
+                       return;
        }
 
        stream_close_common(win->window_stream, result);
 
-       g_node_destroy(win->window_node);
-       /* TODO: iterate over child windows, closing them */
+       /* Parent window changes from a split window into the sibling window */
+       if( (parent_node = win->window_node->parent) != NULL )
+       {
+               winid_t pair = (winid_t) parent_node->data;
+               if(parent_node->parent == NULL)
+               {
+                       if(parent_node->next)
+                               glk_data->root_window = parent_node->next;
+                       else if(parent_node->prev)
+                               glk_data->root_window = parent_node->prev;
+               } else {
+                       if(parent_node->next)
+                               g_node_append(parent_node->parent, parent_node->next);
+                       else if(parent_node->prev)
+                               g_node_append(parent_node->parent, parent_node->prev);
+               }
 
+               g_node_unlink(parent_node);
+               g_free(pair);
+       }
+
+       g_node_destroy(win->window_node);
        g_free(win);
+
+       gdk_threads_leave();
 }
 
 /**
@@ -334,9 +522,38 @@ glk_window_clear(winid_t win)
        switch(win->type)
        {
                case wintype_Blank:
+               case wintype_Pair:
                        /* do nothing */
                        break;
-                       
+               
+               case wintype_TextGrid:
+                   /* fill the buffer with blanks */
+               {
+                   gdk_threads_enter();
+                   
+            /* Manually put newlines at the end of each row of characters in the buffer; manual newlines make resizing the window's grid easier. */
+            gchar *blanks = g_strnfill(win->width, ' ');
+            gchar **blanklines = g_new0(gchar *, win->height + 1);
+            int count;
+            for(count = 0; count < win->height; count++)
+                blanklines[count] = blanks;
+            blanklines[win->height] = NULL;
+            gchar *text = g_strjoinv("\n", blanklines);
+            g_free(blanklines); /* not g_strfreev() */
+            g_free(blanks);
+            
+            GtkTextBuffer *textbuffer = gtk_text_view_get_buffer( GTK_TEXT_VIEW(win->widget) );
+            gtk_text_buffer_set_text(textbuffer, text, -1);
+            g_free(text);
+            
+            GtkTextIter begin;
+            gtk_text_buffer_get_start_iter(textbuffer, &begin);
+            gtk_text_buffer_move_mark_by_name(textbuffer, "cursor_position", &begin);
+                   
+                   gdk_threads_leave();
+               }
+                   break;
+               
                case wintype_TextBuffer:
                        /* delete all text in the window */
                {
@@ -350,7 +567,7 @@ glk_window_clear(winid_t win)
                        gdk_threads_leave();
                }
                        break;
-                       
+               
                default:
                        g_warning("glk_window_clear: unsupported window type");
        }
@@ -461,17 +678,54 @@ glk_window_get_size(winid_t win, glui32 *widthptr, glui32 *heightptr)
 {
        g_return_if_fail(win != NULL);
 
-       /* TODO: Write this function */
-       /* For a text buffer window: Return the number of rows and columns which
-       would be available _if_ the window was filled with "0" (zero) characters in
-       the "normal" font. */
-       if(widthptr != NULL) {
-               *widthptr = 0;
-       }
-
-       if(heightptr != NULL) {
-               *heightptr = 0;
-       }
+    switch(win->type)
+    {
+        case wintype_Blank:
+            if(widthptr != NULL)
+                *widthptr = 0;
+            if(heightptr != NULL)
+                *heightptr = 0;
+            break;
+            
+        case wintype_TextGrid:
+            /* The text grid caches its width and height */
+            if(widthptr != NULL)
+                *widthptr = win->width;
+            if(heightptr != NULL)
+                *heightptr = win->height;
+            break;
+            
+        case wintype_TextBuffer:
+            /* TODO: Glk wants to be able to get its windows' sizes as soon as they are created, but GTK doesn't decide on their sizes until they are drawn. The drawing happens somewhere in an idle function. A good method would be to make an educated guess of the window's size using the ChimaraGlk widget's size. */
+            gdk_threads_enter();
+            /*if(win->widget->allocation.width == 1 && win->widget->allocation.height == 1)
+            {
+                g_warning("glk_window_get_size: The Glk program requested the size of a window before it was allocated screen space by GTK. The window size is just an educated guess.");
+                guess the size from the parent window;
+                break;
+            } */
+            
+            /* Instead, we wait for GTK to draw the widget. This is probably very slow and should be fixed. */
+            while(win->widget->allocation.width == 1 && win->widget->allocation.height == 1)
+            {
+                /* Release the GDK lock momentarily */
+                gdk_threads_leave();
+                gdk_threads_enter();
+                while(gtk_events_pending())
+                    gtk_main_iteration();
+            }
+                
+            if(widthptr != NULL)
+                *widthptr = (glui32)(win->widget->allocation.width / win->unit_width);
+            if(heightptr != NULL)
+                *heightptr = (glui32)(win->widget->allocation.height / win->unit_height);
+            gdk_threads_leave();
+            
+            break;
+            
+        default:
+            g_warning("glk_window_get_size: Unsupported window type");
+    }
 }
 
 /**
@@ -509,5 +763,27 @@ glk_window_move_cursor(winid_t win, glui32 xpos, glui32 ypos)
 {
        g_return_if_fail(win != NULL);
        g_return_if_fail(win->type == wintype_TextGrid);
-       /* TODO: write this function */
+       
+       /* Calculate actual position if cursor is moved past the right edge */
+       if(xpos >= win->width)
+       {
+           ypos += xpos / win->width;
+           xpos %= win->width;
+       }
+       /* Go to the end if the cursor is moved off the bottom edge */
+       if(ypos >= win->height)
+       {
+           xpos = win->width - 1;
+           ypos = win->height - 1;
+       }
+       
+       gdk_threads_enter();
+       
+       GtkTextBuffer *buffer = gtk_text_view_get_buffer( GTK_TEXT_VIEW(win->widget) );
+       GtkTextIter newpos;
+       /* There must actually be a character at xpos, or the following function will choke */
+       gtk_text_buffer_get_iter_at_line_offset(buffer, &newpos, ypos, xpos);
+       gtk_text_buffer_move_mark_by_name(buffer, "cursor_position", &newpos);
+       
+       gdk_threads_leave();
 }