preliminary support for splitting of windows. Does not respect window sizes yet....
[rodin/chimara.git] / src / window.c
index 67fc15b0a393edc4f7bf19cc565d341d89d6f13e..5f50da8d4f5c873e25369ac443b53884ceeb9f83 100644 (file)
@@ -1,23 +1,23 @@
 #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:
- * @win: A window, or #NULL.
- * @rockptr: Return location for the next window's rock, or #NULL.
+ * @win: A window, or %NULL.
+ * @rockptr: Return location for the next window's rock, or %NULL.
  *
- * Iterates over the list of windows; if @win is #NULL, it returns the first
+ * Iterates over the list of windows; if @win is %NULL, it returns the first
  * window, otherwise the next window after @win. If there are no more, it
  * returns #NULL. The window's rock is stored in @rockptr. If you don't want
- * the rocks to be returned, you may set @rockptr to #NULL.
+ * the rocks to be returned, you may set @rockptr to %NULL.
  *
  * The order in which windows are returned is arbitrary. The root window is
  * not necessarily first, nor is it necessarily last. The order may change
  * every time you create or destroy a window, invalidating the iteration.
  *
- * Returns: the next window, or #NULL if there are no more.
+ * Returns: the next window, or %NULL if there are no more.
  */
 winid_t
 glk_window_iterate(winid_t win, glui32 *rockptr)
@@ -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;
@@ -54,7 +54,8 @@ glk_window_iterate(winid_t win, glui32 *rockptr)
  * glk_window_get_rock:
  * @win: A window.
  * 
- * Returns the window @win's rock value. Pair windows always have rock 0.
+ * Returns @win's rock value. Pair windows always have rock 0; all other windows
+ * have the rock value you created them with.
  *
  * Returns: A rock value.
  */
@@ -69,7 +70,7 @@ glk_window_get_rock(winid_t win)
  * glk_window_get_type:
  * @win: A window.
  *
- * Returns the window @win's type, one of #wintype_Blank, #wintype_Pair,
+ * Returns @win's type, one of #wintype_Blank, #wintype_Pair,
  * #wintype_TextBuffer, #wintype_TextGrid, or #wintype_Graphics.
  *
  * Returns: The window's type.
@@ -78,7 +79,7 @@ glui32
 glk_window_get_type(winid_t win)
 {
        g_return_val_if_fail(win != NULL, 0);
-       return win->window_type;
+       return win->type;
 }
 
 /**
@@ -86,7 +87,7 @@ glk_window_get_type(winid_t win)
  * @win: A window.
  *
  * Returns the window @win's parent window. If @win is the root window, this
- * returns #NULL, since the root window has no parent. Remember that the parent
+ * 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.
@@ -104,9 +105,9 @@ glk_window_get_parent(winid_t win)
  * @win: A window.
  *
  * Returns the other child of the window @win's parent. If @win is the
- * root window, this returns #NULL.
+ * root window, this returns %NULL.
  *
- * Returns: A window, or NULL.
+ * Returns: A window, or %NULL.
  */
 winid_t
 glk_window_get_sibling(winid_t win)
@@ -130,9 +131,9 @@ glk_window_get_sibling(winid_t win)
 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;
 }
 
 /**
@@ -162,105 +163,312 @@ 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 new_window = g_new0(struct glk_window_struct, 1);
-       root_window = g_node_new(new_window);
-
-       new_window->rock = rock;
-       new_window->window_type = wintype;
-
-       GtkBox *vbox = GTK_BOX( gtk_builder_get_object(builder, "vbox") );                      
-       if(vbox == NULL)
-       {
-               error_dialog(NULL, NULL, "Could not find vbox");
-               return NULL;
-       }
+       winid_t win = g_new0(struct glk_window_struct, 1);
+       win->rock = rock;
+       win->type = wintype;
+       win->window_node = g_node_new(win);
 
        switch(wintype)
        {
                case wintype_Blank:
                {
                        /* A blank window will be a label without any text */
-                       GtkWidget *window = gtk_label_new("");
-                       gtk_box_pack_end(vbox, window, TRUE, TRUE, 0);
-                       gtk_widget_show(window);
+                       GtkWidget *label = gtk_label_new("");
+                       gtk_widget_show(label);
                        
-                       new_window->widget = window;
+                       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 */
-                       new_window->window_stream = window_stream_new(new_window);
-                       new_window->echo_stream = NULL;
+                       win->window_stream = window_stream_new(win);
+                       win->echo_stream = NULL;
                }
                        break;
                        
                case wintype_TextBuffer:
                {
-                       GtkWidget *scroll_window = gtk_scrolled_window_new(NULL, NULL);
-                       GtkWidget *window = gtk_text_view_new();
-                       gtk_container_add( GTK_CONTAINER(scroll_window), window );
-                       gtk_box_pack_end(vbox, scroll_window, TRUE, TRUE, 0);
-                       gtk_widget_show_all(scroll_window);
-
-                       new_window->widget = window;
-                       new_window->window_stream = window_stream_new(new_window);
-                       new_window->echo_stream = NULL;
-                       new_window->input_request_type = INPUT_REQUEST_NONE;
-                       new_window->line_input_buffer = NULL;
-                       new_window->line_input_buffer_unicode = NULL;
+                       GtkWidget *scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
+                       GtkWidget *textview = gtk_text_view_new();
+                       GtkTextBuffer *textbuffer = gtk_text_view_get_buffer( GTK_TEXT_VIEW(textview) );
+
+                       gtk_text_view_set_wrap_mode( GTK_TEXT_VIEW(textview), GTK_WRAP_WORD_CHAR );
+                       gtk_text_view_set_editable( GTK_TEXT_VIEW(textview), FALSE );
+
+                       gtk_container_add( GTK_CONTAINER(scrolledwindow), textview );
+                       gtk_widget_show_all(scrolledwindow);
+
+                       win->widget = textview;
+                       win->frame = scrolledwindow;
+                       /* Determine the size of a "0" character in pixels" */
+                       PangoLayout *zero = gtk_widget_create_pango_layout(textview, "0");
+                       pango_layout_get_pixel_size( zero, &(win->unit_width), &(win->unit_height) );
+                       g_object_unref(zero);
+                       
+                       /* Set the other parameters */
+                       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 );
+
+                       win->insert_text_handler = g_signal_connect_after( G_OBJECT(textbuffer), "insert-text", G_CALLBACK(after_window_insert_text), win );
+                       g_signal_handler_block( G_OBJECT(textbuffer), win->insert_text_handler );
+
+                       /* Create an editable tag to indicate uneditable parts of the window
+                       (for line input) */
+                       gtk_text_buffer_create_tag(textbuffer, "uneditable", "editable", FALSE, "editable-set", TRUE, NULL);
+
+                       /* Mark the position where the user will input text */
+                       GtkTextIter end;
+                       gtk_text_buffer_get_end_iter(textbuffer, &end);
+                       gtk_text_buffer_create_mark(textbuffer, "input_position", &end, TRUE);
                }
                        break;
                        
                default:
-                       g_warning("glk_window_open: unsupported window type");
-                       g_free(new_window);
+                       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;
        }
 
-       new_window->window_node = root_window;
+       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);
+
+               /* 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));
+       }
+
+       gdk_threads_leave();
 
-       return new_window;
+       return win;
+}
+
+/**
+ * glk_window_close:
+ * @win: Window to close.
+ * @result: Pointer to a #stream_result_t in which to store the write count.
+ *
+ * Closes @win, which is pretty much exactly the opposite of opening a window.
+ * It is legal to close all your windows, or to close the root window (which is
+ * the same thing.) 
+ *
+ * The @result argument is filled with the output character count of the window
+ * stream.
+ */
+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_TextBuffer:
+                       gtk_widget_destroy(win->frame);
+
+                       /* TODO: Cancel all input requests */
+                       break;
+
+               case wintype_Blank:
+                       gtk_widget_destroy(win->widget);
+                       break;
+
+               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);
+
+       /* 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();
 }
 
 /**
  * glk_window_clear:
  * @win: A window.
  *
- * Erases the window @win.
+ * Erases the window @win. The meaning of this depends on the window type.
+ *
+ * <itemizedlist>
+ *  <listitem><para>
+ *   Text buffer: This may do any number of things, such as delete all text in 
+ *   the window, or print enough blank lines to scroll all text beyond 
+ *   visibility, or insert a page-break marker which is treated specially by the
+ *   display part of the library.
+ *  </para></listitem>
+ *  <listitem><para>
+ *   Text grid: This will clear the window, filling all positions with blanks.
+ *   The window cursor is moved to the top left corner (position 0,0).
+ *  </para></listitem>
+ *  <listitem><para>
+ *   Graphics: Clears the entire window to its current background color.
+ *  </para></listitem>
+ *  <listitem><para>
+ *   Other window types: No effect. 
+ *  </para></listitem>
+ * </itemizedlist>
+ *
+ * It is illegal to erase a window which has line input pending. 
  */
 void
 glk_window_clear(winid_t win)
 {
        g_return_if_fail(win != NULL);
+       g_return_if_fail(win->input_request_type != INPUT_REQUEST_LINE && win->input_request_type != INPUT_REQUEST_LINE_UNICODE);
        
-       switch(win->window_type)
+       switch(win->type)
        {
                case wintype_Blank:
+               case wintype_Pair:
                        /* do nothing */
                        break;
                        
                case wintype_TextBuffer:
                        /* delete all text in the window */
                {
-                       GtkTextBuffer *buffer = 
-                               gtk_text_view_get_buffer( GTK_TEXT_VIEW(win->widget) );
+                       gdk_threads_enter();
+
+                       GtkTextBuffer *buffer = gtk_text_view_get_buffer( GTK_TEXT_VIEW(win->widget) );
                        GtkTextIter start, end;
                        gtk_text_buffer_get_bounds(buffer, &start, &end);
                        gtk_text_buffer_delete(buffer, &start, &end);
+
+                       gdk_threads_leave();
                }
                        break;
-                       
+               
                default:
                        g_warning("glk_window_clear: unsupported window type");
        }
@@ -270,7 +478,10 @@ glk_window_clear(winid_t win)
  * glk_set_window:
  * @win: A window.
  *
- * Sets the current stream to @win's window stream.
+ * Sets the current stream to @win's window stream. It is exactly equivalent to
+ * <informalexample><programlisting>
+ *  glk_stream_set_current(glk_window_get_stream(win))
+ * </programlisting></informalexample>
  */
 void
 glk_set_window(winid_t win)
@@ -282,7 +493,9 @@ glk_set_window(winid_t win)
  * glk_window_get_stream:
  * @win: A window.
  *
- * Gets the stream associated with @win.
+ * Returns the stream which is associated with @win. Every window has a stream
+ * which can be printed to, but this may not be useful, depending on the window
+ * type. (For example, printing to a blank window's stream has no effect.)
  *
  * Returns: The window stream.
  */
@@ -295,7 +508,7 @@ strid_t glk_window_get_stream(winid_t win)
 /**
  * glk_window_set_echo_stream:
  * @win: A window.
- * @str: A stream to attach to the window, or #NULL.
+ * @str: A stream to attach to the window, or %NULL.
  *
  * Attaches the stream @str to @win as a second stream. Any text printed to the
  * window is also echoed to this second stream, which is called the window's
@@ -313,7 +526,7 @@ strid_t glk_window_get_stream(winid_t win)
  * which would create an infinite loop. It is similarly illegal to create a
  * longer loop (two or more windows echoing to each other.)
  *
- * You can reset a window to stop echoing by setting @str to #NULL.
+ * You can reset a window to stop echoing by setting @str to %NULL.
  */
 void
 glk_window_set_echo_stream(winid_t win, strid_t str)
@@ -321,20 +534,18 @@ glk_window_set_echo_stream(winid_t win, strid_t str)
        g_return_if_fail(win != NULL);
        
        /* Test for an infinite loop */
-       strid_t next_str;
-       for(next_str = str;
-               next_str != NULL && next_str->stream_type == STREAM_TYPE_WINDOW;
-               next_str = next_str->window->echo_stream)
+       strid_t next = str;
+       for(; next && next->type == STREAM_TYPE_WINDOW; next = next->window->echo_stream)
        {
-               if(next_str == win->window_stream)
+               if(next == win->window_stream)
                {
-                       g_warning("glk_window_set_echo_stream: Infinite loop detected");
+                       g_warning("%s: Infinite loop detected", __func__);
                        win->echo_stream = NULL;
                        return;
                }
        }
        
-       win->echo_stream = str; 
+       win->echo_stream = str;
 }
 
 /**
@@ -342,9 +553,9 @@ glk_window_set_echo_stream(winid_t win, strid_t str)
  * @win: A window.
  *
  * Returns the echo stream of window @win. If the window has no echo stream (as
- * is initially the case) then this returns #NULL.
+ * is initially the case) then this returns %NULL.
  *
- * Returns: A stream, or #NULL.
+ * Returns: A stream, or %NULL.
  */
 strid_t
 glk_window_get_echo_stream(winid_t win)
@@ -353,3 +564,97 @@ glk_window_get_echo_stream(winid_t win)
        return win->echo_stream;
 }
 
+/**
+ * glk_window_get_size:
+ * @win: A window.
+ * @widthptr: Pointer to a location to store the window's width, or %NULL.
+ * @heightptr: Pointer to a location to store the window's height, or %NULL.
+ *
+ * Simply returns the actual size of the window, in its measurement system.
+ * Either @widthptr or @heightptr can be %NULL, if you only want one 
+ * measurement. (Or, in fact, both, if you want to waste time.)
+ */
+void
+glk_window_get_size(winid_t win, glui32 *widthptr, glui32 *heightptr)
+{
+       g_return_if_fail(win != NULL);
+
+    switch(win->type)
+    {
+        case wintype_Blank:
+            if(widthptr != NULL)
+                *widthptr = 0;
+            if(heightptr != NULL)
+                *heightptr = 0;
+            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");
+    }
+}
+
+/**
+ * glk_window_move_cursor:
+ * @win: A text grid window.
+ * @xpos: Horizontal cursor position.
+ * @ypos: Vertical cursor position.
+ * 
+ * Sets the cursor position. If you move the cursor right past the end of a 
+ * line, it wraps; the next character which is printed will appear at the
+ * beginning of the next line.
+ * 
+ * If you move the cursor below the last line, or when the cursor reaches the
+ * end of the last line, it goes "off the screen" and further output has no
+ * effect. You must call glk_window_move_cursor() or glk_window_clear() to move
+ * the cursor back into the visible region.
+ * 
+ * <note><para>
+ *  Note that the arguments of glk_window_move_cursor() are <type>unsigned 
+ *  int</type>s. This is okay, since there are no negative positions. If you try
+ *  to pass a negative value, Glk will interpret it as a huge positive value,
+ *  and it will wrap or go off the last line.
+ * </para></note>
+ *
+ * <note><para>
+ *  Also note that the output cursor is not necessarily visible. In particular,
+ *  when you are requesting line or character input in a grid window, you cannot
+ *  rely on the cursor position to prompt the player where input is indicated.
+ *  You should print some character prompt at that spot -- a ">" character, for
+ *  example.
+ * </para></note>
+ */
+void
+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 */
+}