1fed03d19165cc95b61a25fe61511932a228d3d3
[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         if(split)
167         {
168                 g_warning("glk_window_open: splitting of windows not implemented");
169                 return NULL;
170         }
171
172         if(glk_data->root_window != NULL)
173         {
174                 g_warning("glk_window_open: there is already a window");
175                 return NULL;
176         }
177         
178         gdk_threads_enter();
179         
180         /* We only create one window and don't support any more than that */
181         winid_t win = g_new0(struct glk_window_struct, 1);
182         glk_data->root_window = g_node_new(win);
183
184         win->rock = rock;
185         win->type = wintype;
186     win->window_node = glk_data->root_window;
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     /* Put the frame widget into our container */
261     gtk_widget_set_parent(win->frame, GTK_WIDGET(glk_data->self));
262     gtk_widget_queue_resize(GTK_WIDGET(glk_data->self));
263
264         gdk_threads_leave();
265
266         return win;
267 }
268
269 /**
270  * glk_window_close:
271  * @win: Window to close.
272  * @result: Pointer to a #stream_result_t in which to store the write count.
273  *
274  * Closes @win, which is pretty much exactly the opposite of opening a window.
275  * It is legal to close all your windows, or to close the root window (which is
276  * the same thing.) 
277  *
278  * The @result argument is filled with the output character count of the window
279  * stream.
280  */
281 void
282 glk_window_close(winid_t win, stream_result_t *result)
283 {
284         g_return_if_fail(win != NULL);
285
286         switch(win->type)
287         {
288                 case wintype_TextBuffer:
289                         gtk_widget_destroy( gtk_widget_get_parent(win->widget) );
290                         /* TODO: Cancel all input requests */
291                         break;
292
293                 case wintype_Blank:
294                         gtk_widget_destroy(win->widget);
295                         break;
296         }
297
298         stream_close_common(win->window_stream, result);
299
300         g_node_destroy(win->window_node);
301         /* TODO: iterate over child windows, closing them */
302
303         g_free(win);
304 }
305
306 /**
307  * glk_window_clear:
308  * @win: A window.
309  *
310  * Erases the window @win. The meaning of this depends on the window type.
311  *
312  * <itemizedlist>
313  *  <listitem><para>
314  *   Text buffer: This may do any number of things, such as delete all text in 
315  *   the window, or print enough blank lines to scroll all text beyond 
316  *   visibility, or insert a page-break marker which is treated specially by the
317  *   display part of the library.
318  *  </para></listitem>
319  *  <listitem><para>
320  *   Text grid: This will clear the window, filling all positions with blanks.
321  *   The window cursor is moved to the top left corner (position 0,0).
322  *  </para></listitem>
323  *  <listitem><para>
324  *   Graphics: Clears the entire window to its current background color.
325  *  </para></listitem>
326  *  <listitem><para>
327  *   Other window types: No effect. 
328  *  </para></listitem>
329  * </itemizedlist>
330  *
331  * It is illegal to erase a window which has line input pending. 
332  */
333 void
334 glk_window_clear(winid_t win)
335 {
336         g_return_if_fail(win != NULL);
337         g_return_if_fail(win->input_request_type != INPUT_REQUEST_LINE && win->input_request_type != INPUT_REQUEST_LINE_UNICODE);
338         
339         switch(win->type)
340         {
341                 case wintype_Blank:
342                         /* do nothing */
343                         break;
344                         
345                 case wintype_TextBuffer:
346                         /* delete all text in the window */
347                 {
348                         gdk_threads_enter();
349
350                         GtkTextBuffer *buffer = gtk_text_view_get_buffer( GTK_TEXT_VIEW(win->widget) );
351                         GtkTextIter start, end;
352                         gtk_text_buffer_get_bounds(buffer, &start, &end);
353                         gtk_text_buffer_delete(buffer, &start, &end);
354
355                         gdk_threads_leave();
356                 }
357                         break;
358                         
359                 default:
360                         g_warning("glk_window_clear: unsupported window type");
361         }
362 }
363
364 /**
365  * glk_set_window:
366  * @win: A window.
367  *
368  * Sets the current stream to @win's window stream. It is exactly equivalent to
369  * <informalexample><programlisting>
370  *  glk_stream_set_current(glk_window_get_stream(win))
371  * </programlisting></informalexample>
372  */
373 void
374 glk_set_window(winid_t win)
375 {
376         glk_stream_set_current( glk_window_get_stream(win) );
377 }
378
379 /**
380  * glk_window_get_stream:
381  * @win: A window.
382  *
383  * Returns the stream which is associated with @win. Every window has a stream
384  * which can be printed to, but this may not be useful, depending on the window
385  * type. (For example, printing to a blank window's stream has no effect.)
386  *
387  * Returns: The window stream.
388  */
389 strid_t glk_window_get_stream(winid_t win)
390 {
391         g_return_val_if_fail(win != NULL, NULL);
392         return win->window_stream;
393 }
394
395 /**
396  * glk_window_set_echo_stream:
397  * @win: A window.
398  * @str: A stream to attach to the window, or %NULL.
399  *
400  * Attaches the stream @str to @win as a second stream. Any text printed to the
401  * window is also echoed to this second stream, which is called the window's
402  * "echo stream."
403  *
404  * Effectively, any call to glk_put_char() (or the other output commands) which
405  * is directed to @win's window stream, is replicated to @win's echo stream.
406  * This also goes for the style commands such as glk_set_style().
407  *
408  * Note that the echoing is one-way. You can still print text directly to the
409  * echo stream, and it will go wherever the stream is bound, but it does not
410  * back up and appear in the window.
411  *
412  * It is illegal to set a window's echo stream to be its own window stream,
413  * which would create an infinite loop. It is similarly illegal to create a
414  * longer loop (two or more windows echoing to each other.)
415  *
416  * You can reset a window to stop echoing by setting @str to %NULL.
417  */
418 void
419 glk_window_set_echo_stream(winid_t win, strid_t str)
420 {
421         g_return_if_fail(win != NULL);
422         
423         /* Test for an infinite loop */
424         strid_t next = str;
425         for(; next && next->type == STREAM_TYPE_WINDOW; next = next->window->echo_stream)
426         {
427                 if(next == win->window_stream)
428                 {
429                         g_warning("%s: Infinite loop detected", __func__);
430                         win->echo_stream = NULL;
431                         return;
432                 }
433         }
434         
435         win->echo_stream = str;
436 }
437
438 /**
439  * glk_window_get_echo_stream:
440  * @win: A window.
441  *
442  * Returns the echo stream of window @win. If the window has no echo stream (as
443  * is initially the case) then this returns %NULL.
444  *
445  * Returns: A stream, or %NULL.
446  */
447 strid_t
448 glk_window_get_echo_stream(winid_t win)
449 {
450         g_return_val_if_fail(win != NULL, NULL);
451         return win->echo_stream;
452 }
453
454 /**
455  * glk_window_get_size:
456  * @win: A window.
457  * @widthptr: Pointer to a location to store the window's width, or %NULL.
458  * @heightptr: Pointer to a location to store the window's height, or %NULL.
459  *
460  * Simply returns the actual size of the window, in its measurement system.
461  * Either @widthptr or @heightptr can be %NULL, if you only want one 
462  * measurement. (Or, in fact, both, if you want to waste time.)
463  */
464 void
465 glk_window_get_size(winid_t win, glui32 *widthptr, glui32 *heightptr)
466 {
467         g_return_if_fail(win != NULL);
468
469     switch(win->type)
470     {
471         case wintype_Blank:
472             if(widthptr != NULL)
473                 *widthptr = 0;
474             if(heightptr != NULL)
475                 *heightptr = 0;
476             break;
477             
478         case wintype_TextBuffer:
479             /* 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. */
480             gdk_threads_enter();
481             /*if(win->widget->allocation.width == 1 && win->widget->allocation.height == 1)
482             {
483                 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.");
484                 guess the size from the parent window;
485                 break;
486             } */
487             
488             /* Instead, we wait for GTK to draw the widget. This is probably very slow and should be fixed. */
489             while(win->widget->allocation.width == 1 && win->widget->allocation.height == 1)
490             {
491                 /* Release the GDK lock momentarily */
492                 gdk_threads_leave();
493                 gdk_threads_enter();
494                 while(gtk_events_pending())
495                     gtk_main_iteration();
496             }
497                 
498             if(widthptr != NULL)
499                 *widthptr = (glui32)(win->widget->allocation.width / win->unit_width);
500             if(heightptr != NULL)
501                 *heightptr = (glui32)(win->widget->allocation.height / win->unit_height);
502             gdk_threads_leave();
503             
504             break;
505             
506         default:
507             g_warning("glk_window_get_size: Unsupported window type");
508     }
509 }
510
511 /**
512  * glk_window_move_cursor:
513  * @win: A text grid window.
514  * @xpos: Horizontal cursor position.
515  * @ypos: Vertical cursor position.
516  * 
517  * Sets the cursor position. If you move the cursor right past the end of a 
518  * line, it wraps; the next character which is printed will appear at the
519  * beginning of the next line.
520  * 
521  * If you move the cursor below the last line, or when the cursor reaches the
522  * end of the last line, it goes "off the screen" and further output has no
523  * effect. You must call glk_window_move_cursor() or glk_window_clear() to move
524  * the cursor back into the visible region.
525  * 
526  * <note><para>
527  *  Note that the arguments of glk_window_move_cursor() are <type>unsigned 
528  *  int</type>s. This is okay, since there are no negative positions. If you try
529  *  to pass a negative value, Glk will interpret it as a huge positive value,
530  *  and it will wrap or go off the last line.
531  * </para></note>
532  *
533  * <note><para>
534  *  Also note that the output cursor is not necessarily visible. In particular,
535  *  when you are requesting line or character input in a grid window, you cannot
536  *  rely on the cursor position to prompt the player where input is indicated.
537  *  You should print some character prompt at that spot -- a ">" character, for
538  *  example.
539  * </para></note>
540  */
541 void
542 glk_window_move_cursor(winid_t win, glui32 xpos, glui32 ypos)
543 {
544         g_return_if_fail(win != NULL);
545         g_return_if_fail(win->type == wintype_TextGrid);
546         /* TODO: write this function */
547 }