preliminary support for splitting of windows. Does not respect window sizes yet....
[rodin/chimara.git] / src / window.c
1 #include "window.h"
2 #include "chimara-glk-private.h"
3
4 extern ChimaraGlkPrivate *glk_data;
5
6 /**
7  * glk_window_iterate:
8  * @win: A window, or %NULL.
9  * @rockptr: Return location for the next window's rock, or %NULL.
10  *
11  * Iterates over the list of windows; if @win is %NULL, it returns the first
12  * window, otherwise the next window after @win. If there are no more, it
13  * returns #NULL. The window's rock is stored in @rockptr. If you don't want
14  * the rocks to be returned, you may set @rockptr to %NULL.
15  *
16  * The order in which windows are returned is arbitrary. The root window is
17  * not necessarily first, nor is it necessarily last. The order may change
18  * every time you create or destroy a window, invalidating the iteration.
19  *
20  * Returns: the next window, or %NULL if there are no more.
21  */
22 winid_t
23 glk_window_iterate(winid_t win, glui32 *rockptr)
24 {
25         GNode *retnode;
26         
27         if(win == NULL)
28                 retnode = glk_data->root_window;
29         else
30         {
31                 GNode *node = win->window_node;
32                 if( G_NODE_IS_LEAF(node) )
33                 {
34                         while(node && node->next == NULL)
35                                 node = node->parent;
36                         if(node)
37                                 retnode = node->next;
38                         else
39                                 retnode = NULL;
40                 }
41                 else
42                         retnode = g_node_first_child(node);
43         }
44         winid_t retval = retnode? (winid_t)retnode->data : NULL;
45                 
46         /* Store the window's rock in rockptr */
47         if(retval && rockptr)
48                 *rockptr = glk_window_get_rock(retval);
49                 
50         return retval;
51 }
52
53 /**
54  * glk_window_get_rock:
55  * @win: A window.
56  * 
57  * Returns @win's rock value. Pair windows always have rock 0; all other windows
58  * have the rock value you created them with.
59  *
60  * Returns: A rock value.
61  */
62 glui32
63 glk_window_get_rock(winid_t win)
64 {
65         g_return_val_if_fail(win != NULL, 0);
66         return win->rock;
67 }
68
69 /**
70  * glk_window_get_type:
71  * @win: A window.
72  *
73  * Returns @win's type, one of #wintype_Blank, #wintype_Pair,
74  * #wintype_TextBuffer, #wintype_TextGrid, or #wintype_Graphics.
75  *
76  * Returns: The window's type.
77  */
78 glui32
79 glk_window_get_type(winid_t win)
80 {
81         g_return_val_if_fail(win != NULL, 0);
82         return win->type;
83 }
84
85 /**
86  * glk_window_get_parent:
87  * @win: A window.
88  *
89  * Returns the window @win's parent window. If @win is the root window, this
90  * returns %NULL, since the root window has no parent. Remember that the parent
91  * of every window is a pair window; other window types are always childless.
92  *
93  * Returns: A window.
94  */
95 winid_t
96 glk_window_get_parent(winid_t win)
97 {
98         g_return_val_if_fail(win != NULL, NULL);
99         /* Value will also be NULL if win is the root window */
100         return (winid_t)win->window_node->parent->data;
101 }
102
103 /**
104  * glk_window_get_sibling:
105  * @win: A window.
106  *
107  * Returns the other child of the window @win's parent. If @win is the
108  * root window, this returns %NULL.
109  *
110  * Returns: A window, or %NULL.
111  */
112 winid_t
113 glk_window_get_sibling(winid_t win)
114 {
115         g_return_val_if_fail(win != NULL, NULL);
116         
117         if(G_NODE_IS_ROOT(win->window_node))
118                 return NULL;
119         if(win->window_node->next)
120                 return (winid_t)win->window_node->next;
121         return (winid_t)win->window_node->prev;
122 }
123
124 /**
125  * glk_window_get_root:
126  * 
127  * Returns the root window. If there are no windows, this returns #NULL.
128  *
129  * Returns: A window, or #NULL.
130  */
131 winid_t
132 glk_window_get_root()
133 {
134         if(glk_data->root_window == NULL)
135                 return NULL;
136         return (winid_t)glk_data->root_window->data;
137 }
138
139 /**
140  * glk_window_open:
141  * @split: The window to split to create the new window. Must be 0 if there
142  * are no windows yet.
143  * @method: Position of the new window and method of size computation. One of
144  * #winmethod_Above, #winmethod_Below, #winmethod_Left, or #winmethod_Right
145  * OR'ed with #winmethod_Fixed or #winmethod_Proportional. If @wintype is
146  * #wintype_Blank, then #winmethod_Fixed is not allowed.
147  * @size: Size of the new window, in percentage points if @method is
148  * #winmethod_Proportional, otherwise in characters if @wintype is 
149  * #wintype_TextBuffer or #wintype_TextGrid, or pixels if @wintype is
150  * #wintype_Graphics.
151  * @wintype: Type of the new window. One of #wintype_Blank, #wintype_TextGrid,
152  * #wintype_TextBuffer, or #wintype_Graphics.
153  * @rock: The new window's rock value.
154  *
155  * If there are no windows, create a new root window. @split must be 0, and
156  * @method and @size are ignored. Otherwise, split window @split into two, with
157  * position, size, and type specified by @method, @size, and @wintype. See the
158  * Glk documentation for the window placement algorithm.
159  *
160  * Returns: the new window.
161  */
162 winid_t
163 glk_window_open(winid_t split, glui32 method, glui32 size, glui32 wintype, 
164                 glui32 rock)
165 {
166         /*
167         if(split)
168         {
169                 g_warning("glk_window_open: splitting of windows not implemented");
170                 return NULL;
171         }
172         */
173
174         if(split == NULL && glk_data->root_window != NULL)
175         {
176                 g_warning("glk_window_open: there is already a root window");
177                 return NULL;
178         }
179         
180         gdk_threads_enter();
181         
182         /* We only create one window and don't support any more than that */
183         winid_t win = g_new0(struct glk_window_struct, 1);
184         win->rock = rock;
185         win->type = wintype;
186         win->window_node = g_node_new(win);
187
188         switch(wintype)
189         {
190                 case wintype_Blank:
191                 {
192                         /* A blank window will be a label without any text */
193                         GtkWidget *label = gtk_label_new("");
194                         gtk_widget_show(label);
195                         
196                         win->widget = label;
197                         win->frame = label;
198                         /* A blank window has no size */
199                         win->unit_width = 0;
200                         win->unit_height = 0;
201                         /* You can print to a blank window's stream, but it does nothing */
202                         win->window_stream = window_stream_new(win);
203                         win->echo_stream = NULL;
204                 }
205                         break;
206                         
207                 case wintype_TextBuffer:
208                 {
209                         GtkWidget *scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
210                         GtkWidget *textview = gtk_text_view_new();
211                         GtkTextBuffer *textbuffer = gtk_text_view_get_buffer( GTK_TEXT_VIEW(textview) );
212
213                         gtk_text_view_set_wrap_mode( GTK_TEXT_VIEW(textview), GTK_WRAP_WORD_CHAR );
214                         gtk_text_view_set_editable( GTK_TEXT_VIEW(textview), FALSE );
215
216                         gtk_container_add( GTK_CONTAINER(scrolledwindow), textview );
217                         gtk_widget_show_all(scrolledwindow);
218
219                         win->widget = textview;
220                         win->frame = scrolledwindow;
221                         /* Determine the size of a "0" character in pixels" */
222                         PangoLayout *zero = gtk_widget_create_pango_layout(textview, "0");
223                         pango_layout_get_pixel_size( zero, &(win->unit_width), &(win->unit_height) );
224                         g_object_unref(zero);
225                         
226                         /* Set the other parameters */
227                         win->window_stream = window_stream_new(win);
228                         win->echo_stream = NULL;
229                         win->input_request_type = INPUT_REQUEST_NONE;
230                         win->line_input_buffer = NULL;
231                         win->line_input_buffer_unicode = NULL;
232
233                         /* Connect signal handlers */
234                         win->keypress_handler = g_signal_connect( G_OBJECT(textview), "key-press-event", G_CALLBACK(on_window_key_press_event), win );
235                         g_signal_handler_block( G_OBJECT(textview), win->keypress_handler );
236
237                         win->insert_text_handler = g_signal_connect_after( G_OBJECT(textbuffer), "insert-text", G_CALLBACK(after_window_insert_text), win );
238                         g_signal_handler_block( G_OBJECT(textbuffer), win->insert_text_handler );
239
240                         /* Create an editable tag to indicate uneditable parts of the window
241                         (for line input) */
242                         gtk_text_buffer_create_tag(textbuffer, "uneditable", "editable", FALSE, "editable-set", TRUE, NULL);
243
244                         /* Mark the position where the user will input text */
245                         GtkTextIter end;
246                         gtk_text_buffer_get_end_iter(textbuffer, &end);
247                         gtk_text_buffer_create_mark(textbuffer, "input_position", &end, TRUE);
248                 }
249                         break;
250                         
251                 default:
252                         gdk_threads_leave();
253                         g_warning("%s: unsupported window type", __func__);
254                         g_free(win);
255                         g_node_destroy(glk_data->root_window);
256                         glk_data->root_window = NULL;
257                         return NULL;
258         }
259
260         if(split)
261         {
262                 /* When splitting, construct a new parent window
263                  * copying most characteristics from the window that is being split */
264                 winid_t pair = g_new0(struct glk_window_struct, 1);
265                 pair->rock = 0;
266                 pair->type = wintype_Pair;
267                 pair->window_node = g_node_new(pair);
268                 pair->unit_width = split->unit_width;
269                 pair->unit_height = split->unit_height;
270                 pair->window_stream = NULL;
271                 pair->echo_stream = NULL;
272
273                 /* Insert the new window into the window tree */
274                 if(split->window_node->parent == NULL)
275                 {
276                         glk_data->root_window = pair->window_node;
277                 } else {
278                         g_node_append(split->window_node->parent, pair->window_node);
279                         g_node_unlink(split->window_node);
280                 }
281
282                 /* Keep track of the parent widget of the window that is being split */
283                 GtkWidget* old_parent = gtk_widget_get_parent(split->frame);
284                 gtk_widget_ref(split->frame);
285                 gtk_widget_unparent(split->frame);
286
287                 /* Place the windows in the correct order */
288                 switch(method & winmethod_DirMask)
289                 {
290                         case winmethod_Left:
291                                 pair->widget = gtk_hbox_new(FALSE, 0);
292                                 gtk_box_pack_end(GTK_BOX(pair->widget), split->frame, TRUE, TRUE, 0);
293                                 gtk_box_pack_end(GTK_BOX(pair->widget), win->frame, TRUE, TRUE, 0);
294                                 g_node_append(pair->window_node, split->window_node);
295                                 g_node_append(pair->window_node, win->window_node);
296                                 break;
297                         case winmethod_Right:
298                                 pair->widget = gtk_hbox_new(FALSE, 0);
299                                 gtk_box_pack_end(GTK_BOX(pair->widget), win->frame, TRUE, TRUE, 0);
300                                 gtk_box_pack_end(GTK_BOX(pair->widget), split->frame, TRUE, TRUE, 0);
301                                 g_node_append(pair->window_node, win->window_node);
302                                 g_node_append(pair->window_node, split->window_node);
303                                 break;
304                         case winmethod_Above:
305                                 pair->widget = gtk_vbox_new(FALSE, 0);
306                                 gtk_box_pack_end(GTK_BOX(pair->widget), split->frame, TRUE, TRUE, 0);
307                                 gtk_box_pack_end(GTK_BOX(pair->widget), win->frame, TRUE, TRUE, 0);
308                                 g_node_append(pair->window_node, split->window_node);
309                                 g_node_append(pair->window_node, win->window_node);
310                                 break;
311                         case winmethod_Below:
312                                 pair->widget = gtk_vbox_new(FALSE, 0);
313                                 gtk_box_pack_end(GTK_BOX(pair->widget), win->frame, TRUE, TRUE, 0);
314                                 gtk_box_pack_end(GTK_BOX(pair->widget), split->frame, TRUE, TRUE, 0);
315                                 g_node_append(pair->window_node, win->window_node);
316                                 g_node_append(pair->window_node, split->window_node);
317                                 break;
318                 }
319                 gtk_widget_unref(split->frame);
320
321                 /* TODO: set the new size of the windows */
322
323                 pair->frame = pair->widget;
324                 gtk_widget_set_parent(pair->widget, old_parent);
325                 gtk_widget_show(pair->widget);
326         } else {
327                 /* Set the window as root window */
328                 glk_data->root_window = win->window_node;
329                 gtk_widget_set_parent(win->frame, GTK_WIDGET(glk_data->self));
330                 gtk_widget_queue_resize(GTK_WIDGET(glk_data->self));
331         }
332
333         gdk_threads_leave();
334
335         return win;
336 }
337
338 /**
339  * glk_window_close:
340  * @win: Window to close.
341  * @result: Pointer to a #stream_result_t in which to store the write count.
342  *
343  * Closes @win, which is pretty much exactly the opposite of opening a window.
344  * It is legal to close all your windows, or to close the root window (which is
345  * the same thing.) 
346  *
347  * The @result argument is filled with the output character count of the window
348  * stream.
349  */
350 void
351 glk_window_close(winid_t win, stream_result_t *result)
352 {
353         GNode* parent_node;
354
355         g_return_if_fail(win != NULL);
356
357         gdk_threads_enter();
358
359         switch(win->type)
360         {
361                 case wintype_TextBuffer:
362                         gtk_widget_destroy(win->frame);
363
364                         /* TODO: Cancel all input requests */
365                         break;
366
367                 case wintype_Blank:
368                         gtk_widget_destroy(win->widget);
369                         break;
370
371                 case wintype_Pair:
372                 {
373                         GNode* left_child = g_node_first_child(win->window_node);
374                         GNode* right_child = g_node_last_child(win->window_node);
375
376                         glk_window_close((winid_t) left_child->data, result);
377                         glk_window_close((winid_t) right_child->data, result);
378
379                         gtk_widget_destroy(win->widget);
380                 }
381                         break;
382
383                 default:
384                         g_warning("%s: unsupported window type", __func__);
385                         gdk_threads_leave();
386                         return;
387         }
388
389         stream_close_common(win->window_stream, result);
390
391         /* Parent window changes from a split window into the sibling window */
392         if( (parent_node = win->window_node->parent) != NULL )
393         {
394                 winid_t pair = (winid_t) parent_node->data;
395                 if(parent_node->parent == NULL)
396                 {
397                         if(parent_node->next)
398                                 glk_data->root_window = parent_node->next;
399                         else if(parent_node->prev)
400                                 glk_data->root_window = parent_node->prev;
401                 } else {
402                         if(parent_node->next)
403                                 g_node_append(parent_node->parent, parent_node->next);
404                         else if(parent_node->prev)
405                                 g_node_append(parent_node->parent, parent_node->prev);
406                 }
407
408                 g_node_unlink(parent_node);
409                 g_free(pair);
410         }
411
412         g_node_destroy(win->window_node);
413         g_free(win);
414
415         gdk_threads_leave();
416 }
417
418 /**
419  * glk_window_clear:
420  * @win: A window.
421  *
422  * Erases the window @win. The meaning of this depends on the window type.
423  *
424  * <itemizedlist>
425  *  <listitem><para>
426  *   Text buffer: This may do any number of things, such as delete all text in 
427  *   the window, or print enough blank lines to scroll all text beyond 
428  *   visibility, or insert a page-break marker which is treated specially by the
429  *   display part of the library.
430  *  </para></listitem>
431  *  <listitem><para>
432  *   Text grid: This will clear the window, filling all positions with blanks.
433  *   The window cursor is moved to the top left corner (position 0,0).
434  *  </para></listitem>
435  *  <listitem><para>
436  *   Graphics: Clears the entire window to its current background color.
437  *  </para></listitem>
438  *  <listitem><para>
439  *   Other window types: No effect. 
440  *  </para></listitem>
441  * </itemizedlist>
442  *
443  * It is illegal to erase a window which has line input pending. 
444  */
445 void
446 glk_window_clear(winid_t win)
447 {
448         g_return_if_fail(win != NULL);
449         g_return_if_fail(win->input_request_type != INPUT_REQUEST_LINE && win->input_request_type != INPUT_REQUEST_LINE_UNICODE);
450         
451         switch(win->type)
452         {
453                 case wintype_Blank:
454                 case wintype_Pair:
455                         /* do nothing */
456                         break;
457                         
458                 case wintype_TextBuffer:
459                         /* delete all text in the window */
460                 {
461                         gdk_threads_enter();
462
463                         GtkTextBuffer *buffer = gtk_text_view_get_buffer( GTK_TEXT_VIEW(win->widget) );
464                         GtkTextIter start, end;
465                         gtk_text_buffer_get_bounds(buffer, &start, &end);
466                         gtk_text_buffer_delete(buffer, &start, &end);
467
468                         gdk_threads_leave();
469                 }
470                         break;
471                 
472                 default:
473                         g_warning("glk_window_clear: unsupported window type");
474         }
475 }
476
477 /**
478  * glk_set_window:
479  * @win: A window.
480  *
481  * Sets the current stream to @win's window stream. It is exactly equivalent to
482  * <informalexample><programlisting>
483  *  glk_stream_set_current(glk_window_get_stream(win))
484  * </programlisting></informalexample>
485  */
486 void
487 glk_set_window(winid_t win)
488 {
489         glk_stream_set_current( glk_window_get_stream(win) );
490 }
491
492 /**
493  * glk_window_get_stream:
494  * @win: A window.
495  *
496  * Returns the stream which is associated with @win. Every window has a stream
497  * which can be printed to, but this may not be useful, depending on the window
498  * type. (For example, printing to a blank window's stream has no effect.)
499  *
500  * Returns: The window stream.
501  */
502 strid_t glk_window_get_stream(winid_t win)
503 {
504         g_return_val_if_fail(win != NULL, NULL);
505         return win->window_stream;
506 }
507
508 /**
509  * glk_window_set_echo_stream:
510  * @win: A window.
511  * @str: A stream to attach to the window, or %NULL.
512  *
513  * Attaches the stream @str to @win as a second stream. Any text printed to the
514  * window is also echoed to this second stream, which is called the window's
515  * "echo stream."
516  *
517  * Effectively, any call to glk_put_char() (or the other output commands) which
518  * is directed to @win's window stream, is replicated to @win's echo stream.
519  * This also goes for the style commands such as glk_set_style().
520  *
521  * Note that the echoing is one-way. You can still print text directly to the
522  * echo stream, and it will go wherever the stream is bound, but it does not
523  * back up and appear in the window.
524  *
525  * It is illegal to set a window's echo stream to be its own window stream,
526  * which would create an infinite loop. It is similarly illegal to create a
527  * longer loop (two or more windows echoing to each other.)
528  *
529  * You can reset a window to stop echoing by setting @str to %NULL.
530  */
531 void
532 glk_window_set_echo_stream(winid_t win, strid_t str)
533 {
534         g_return_if_fail(win != NULL);
535         
536         /* Test for an infinite loop */
537         strid_t next = str;
538         for(; next && next->type == STREAM_TYPE_WINDOW; next = next->window->echo_stream)
539         {
540                 if(next == win->window_stream)
541                 {
542                         g_warning("%s: Infinite loop detected", __func__);
543                         win->echo_stream = NULL;
544                         return;
545                 }
546         }
547         
548         win->echo_stream = str;
549 }
550
551 /**
552  * glk_window_get_echo_stream:
553  * @win: A window.
554  *
555  * Returns the echo stream of window @win. If the window has no echo stream (as
556  * is initially the case) then this returns %NULL.
557  *
558  * Returns: A stream, or %NULL.
559  */
560 strid_t
561 glk_window_get_echo_stream(winid_t win)
562 {
563         g_return_val_if_fail(win != NULL, NULL);
564         return win->echo_stream;
565 }
566
567 /**
568  * glk_window_get_size:
569  * @win: A window.
570  * @widthptr: Pointer to a location to store the window's width, or %NULL.
571  * @heightptr: Pointer to a location to store the window's height, or %NULL.
572  *
573  * Simply returns the actual size of the window, in its measurement system.
574  * Either @widthptr or @heightptr can be %NULL, if you only want one 
575  * measurement. (Or, in fact, both, if you want to waste time.)
576  */
577 void
578 glk_window_get_size(winid_t win, glui32 *widthptr, glui32 *heightptr)
579 {
580         g_return_if_fail(win != NULL);
581
582     switch(win->type)
583     {
584         case wintype_Blank:
585             if(widthptr != NULL)
586                 *widthptr = 0;
587             if(heightptr != NULL)
588                 *heightptr = 0;
589             break;
590             
591         case wintype_TextBuffer:
592             /* 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. */
593             gdk_threads_enter();
594             /*if(win->widget->allocation.width == 1 && win->widget->allocation.height == 1)
595             {
596                 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.");
597                 guess the size from the parent window;
598                 break;
599             } */
600             
601             /* Instead, we wait for GTK to draw the widget. This is probably very slow and should be fixed. */
602             while(win->widget->allocation.width == 1 && win->widget->allocation.height == 1)
603             {
604                 /* Release the GDK lock momentarily */
605                 gdk_threads_leave();
606                 gdk_threads_enter();
607                 while(gtk_events_pending())
608                     gtk_main_iteration();
609             }
610                 
611             if(widthptr != NULL)
612                 *widthptr = (glui32)(win->widget->allocation.width / win->unit_width);
613             if(heightptr != NULL)
614                 *heightptr = (glui32)(win->widget->allocation.height / win->unit_height);
615             gdk_threads_leave();
616             
617             break;
618             
619         default:
620             g_warning("glk_window_get_size: Unsupported window type");
621     }
622 }
623
624 /**
625  * glk_window_move_cursor:
626  * @win: A text grid window.
627  * @xpos: Horizontal cursor position.
628  * @ypos: Vertical cursor position.
629  * 
630  * Sets the cursor position. If you move the cursor right past the end of a 
631  * line, it wraps; the next character which is printed will appear at the
632  * beginning of the next line.
633  * 
634  * If you move the cursor below the last line, or when the cursor reaches the
635  * end of the last line, it goes "off the screen" and further output has no
636  * effect. You must call glk_window_move_cursor() or glk_window_clear() to move
637  * the cursor back into the visible region.
638  * 
639  * <note><para>
640  *  Note that the arguments of glk_window_move_cursor() are <type>unsigned 
641  *  int</type>s. This is okay, since there are no negative positions. If you try
642  *  to pass a negative value, Glk will interpret it as a huge positive value,
643  *  and it will wrap or go off the last line.
644  * </para></note>
645  *
646  * <note><para>
647  *  Also note that the output cursor is not necessarily visible. In particular,
648  *  when you are requesting line or character input in a grid window, you cannot
649  *  rely on the cursor position to prompt the player where input is indicated.
650  *  You should print some character prompt at that spot -- a ">" character, for
651  *  example.
652  * </para></note>
653  */
654 void
655 glk_window_move_cursor(winid_t win, glui32 xpos, glui32 ypos)
656 {
657         g_return_if_fail(win != NULL);
658         g_return_if_fail(win->type == wintype_TextGrid);
659         /* TODO: write this function */
660 }