* Added timer support
[rodin/chimara.git] / src / input.c
1 #include "charset.h"
2 #include "magic.h"
3 #include "input.h"
4
5 /** 
6  * glk_request_char_event:
7  * @win: A window to request char events from.
8  *
9  * Request input of a Latin-1 character or special key. A window cannot have 
10  * requests for both character and line input at the same time. Nor can it have
11  * requests for character input of both types (Latin-1 and Unicode). It is
12  * illegal to call glk_request_char_event() if the window already has a pending
13  * request for either character or line input. 
14  */
15 void
16 glk_request_char_event(winid_t win)
17 {
18         VALID_WINDOW(win, return);
19         g_return_if_fail(win->input_request_type == INPUT_REQUEST_NONE);
20         g_return_if_fail(win->type != wintype_TextBuffer || win->type != wintype_TextGrid);
21
22         win->input_request_type = INPUT_REQUEST_CHARACTER;
23         g_signal_handler_unblock( G_OBJECT(win->widget), win->keypress_handler );
24 }
25
26 /** 
27  * glk_request_char_event_uni:
28  * @win: A window to request char events from.
29  *
30  * Request input of a Unicode character or special key. See 
31  * glk_request_char_event().
32  */
33 void
34 glk_request_char_event_uni(winid_t win)
35 {
36         VALID_WINDOW(win, return);
37         g_return_if_fail(win->input_request_type == INPUT_REQUEST_NONE);
38         g_return_if_fail(win->type != wintype_TextBuffer || win->type != wintype_TextGrid);
39
40         win->input_request_type = INPUT_REQUEST_CHARACTER_UNICODE;
41         g_signal_handler_unblock( G_OBJECT(win->widget), win->keypress_handler );
42 }
43
44 /**
45  * glk_cancel_char_event:
46  * @win: A window to cancel the latest char event request on.
47  *
48  * Cancels the last char event request on the given window.
49  */
50 void
51 glk_cancel_char_event(winid_t win)
52 {
53         /* TODO: write me */
54         VALID_WINDOW(win, return);
55         g_return_if_fail(win->input_request_type == INPUT_REQUEST_NONE);
56         g_return_if_fail(win->type != wintype_TextBuffer || win->type != wintype_TextGrid);
57 }
58
59 /* Internal function: Request either latin-1 or unicode line input, in a text grid window. */
60 static void
61 text_grid_request_line_event_common(winid_t win, glui32 maxlen, gboolean insert, gchar *inserttext)
62 {
63         GtkTextBuffer *buffer = gtk_text_view_get_buffer( GTK_TEXT_VIEW(win->widget) );
64
65     GtkTextMark *cursor = gtk_text_buffer_get_mark(buffer, "cursor_position");
66     GtkTextIter start_iter, end_iter;
67     gtk_text_buffer_get_iter_at_mark(buffer, &start_iter, cursor);
68     
69     /* Determine the maximum length of the line input */
70     gint cursorpos = gtk_text_iter_get_line_offset(&start_iter);
71     /* Odd; the Glk spec says the maximum input length is
72     windowwidth - 1 - cursorposition. I say no, because if cursorposition is
73     zero, then the input should fill the whole line. FIXME??? */
74     win->input_length = MIN(win->width - cursorpos, win->line_input_buffer_max_len);
75     end_iter = start_iter;
76     gtk_text_iter_set_line_offset(&end_iter, cursorpos + win->input_length);
77     
78         /* If the buffer currently has a selection with one bound in the middle of
79         the input field, then deselect it. Otherwise the input field gets trashed */
80         GtkTextIter start_sel, end_sel;
81         if( gtk_text_buffer_get_selection_bounds(buffer, &start_sel, &end_sel) )
82         {
83                 if( gtk_text_iter_in_range(&start_sel, &start_iter, &end_iter) )
84                         gtk_text_buffer_place_cursor(buffer, &end_sel);
85                 if( gtk_text_iter_in_range(&end_sel, &start_iter, &end_iter) )
86                         gtk_text_buffer_place_cursor(buffer, &start_sel);
87         }
88         
89     /* Erase the text currently in the input field and replace it with a GtkEntry */
90     gtk_text_buffer_delete(buffer, &start_iter, &end_iter);
91     win->input_anchor = gtk_text_buffer_create_child_anchor(buffer, &start_iter);
92     win->input_entry = gtk_entry_new();
93         /* Set the entry's font to match that of the window */
94     GtkRcStyle *style = gtk_widget_get_modifier_style(win->widget);     /* Don't free */
95         gtk_widget_modify_font(win->input_entry, style->font_desc);
96         /* Make the entry as small as possible to fit with the text */
97         gtk_entry_set_has_frame(GTK_ENTRY(win->input_entry), FALSE);
98         GtkBorder border = { 0, 0, 0, 0 };
99         gtk_entry_set_inner_border(GTK_ENTRY(win->input_entry), &border);
100     gtk_entry_set_max_length(GTK_ENTRY(win->input_entry), win->input_length);
101     gtk_entry_set_width_chars(GTK_ENTRY(win->input_entry), win->input_length);
102     
103     /* Insert pre-entered text if needed */
104     if(insert)
105         gtk_entry_set_text(GTK_ENTRY(win->input_entry), inserttext);
106     
107     /* Set background color of entry (TODO: implement as property) */
108     GdkColor background;
109         gdk_color_parse("grey", &background);
110     gtk_widget_modify_base(win->input_entry, GTK_STATE_NORMAL, &background);
111     
112     g_signal_connect(win->input_entry, "activate", G_CALLBACK(on_input_entry_activate), win);
113     
114     gtk_widget_show(win->input_entry);
115     gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(win->widget), win->input_entry, win->input_anchor);
116         
117         g_signal_handler_unblock( G_OBJECT(win->widget), win->keypress_handler );
118 }
119     
120 /* Internal function: Request either latin-1 or unicode line input, in a text buffer window. */
121 static void
122 text_buffer_request_line_event_common(winid_t win, glui32 maxlen, gboolean insert, gchar *inserttext)
123 {
124         GtkTextBuffer *buffer = gtk_text_view_get_buffer( GTK_TEXT_VIEW(win->widget) );
125
126     /* Move the input_position mark to the end of the window_buffer */
127     GtkTextMark *input_position = gtk_text_buffer_get_mark(buffer, "input_position");
128     GtkTextIter end_iter;
129     gtk_text_buffer_get_end_iter(buffer, &end_iter);
130     gtk_text_buffer_move_mark(buffer, input_position, &end_iter);
131
132     /* Set the entire contents of the window_buffer as uneditable
133      * (so input can only be entered at the end) */
134     GtkTextIter start_iter;
135     gtk_text_buffer_get_start_iter(buffer, &start_iter);
136     gtk_text_buffer_remove_tag_by_name(buffer, "uneditable", &start_iter, &end_iter);
137     gtk_text_buffer_apply_tag_by_name(buffer, "uneditable", &start_iter, &end_iter);
138     
139     /* Insert pre-entered text if needed */
140     if(insert)
141         gtk_text_buffer_insert(buffer, &end_iter, inserttext, -1);
142     
143     /* Scroll to input point */
144     gtk_text_view_scroll_mark_onscreen(GTK_TEXT_VIEW(win->widget), input_position);
145     
146     gtk_text_view_set_editable(GTK_TEXT_VIEW(win->widget), TRUE);
147     g_signal_handler_unblock(buffer, win->insert_text_handler);
148 }
149
150 /**
151  * glk_request_line_event:
152  * @win: A text buffer or text grid window to request line input on.
153  * @buf: A buffer of at least @maxlen bytes.
154  * @maxlen: Length of the buffer.
155  * @initlen: The number of characters in @buf to pre-enter.
156  *
157  * Requests input of a line of Latin-1 characters. A window cannot have requests
158  * for both character and line input at the same time. Nor can it have requests
159  * for line input of both types (Latin-1 and Unicode). It is illegal to call
160  * glk_request_line_event() if the window already has a pending request for
161  * either character or line input.
162  * 
163  * The @buf argument is a pointer to space where the line input will be stored.
164  * (This may not be %NULL.) @maxlen is the length of this space, in bytes; the
165  * library will not accept more characters than this. If @initlen is nonzero,
166  * then the first @initlen bytes of @buf will be entered as pre-existing input
167  * -- just as if the player had typed them himself. (The player can continue
168  * composing after this pre-entered input, or delete it or edit as usual.)
169  * 
170  * The contents of the buffer are undefined until the input is completed (either
171  * by a line input event, or glk_cancel_line_event(). The library may or may not
172  * fill in the buffer as the player composes, while the input is still pending;
173  * it is illegal to change the contents of the buffer yourself. 
174  */
175 void
176 glk_request_line_event(winid_t win, char* buf, glui32 maxlen, glui32 initlen)
177 {
178         VALID_WINDOW(win, return);
179         g_return_if_fail(buf);
180         g_return_if_fail(win->input_request_type == INPUT_REQUEST_NONE);
181         g_return_if_fail(win->type != wintype_TextBuffer || win->type != wintype_TextGrid);
182         g_return_if_fail(initlen <= maxlen);
183
184         win->input_request_type = INPUT_REQUEST_LINE;
185         win->line_input_buffer = buf;
186         win->line_input_buffer_max_len = maxlen;
187
188         gchar *inserttext = (initlen > 0)? g_strndup(buf, initlen) : g_strdup("");
189         switch(win->type)
190         {
191             case wintype_TextBuffer:
192                 text_buffer_request_line_event_common(win, maxlen, (initlen > 0), inserttext);
193                 break;
194             case wintype_TextGrid:
195                 text_grid_request_line_event_common(win, maxlen, (initlen > 0), inserttext);
196                 break;
197     }
198         g_free(inserttext);
199 }
200
201 /**
202  * glk_request_line_event_uni:
203  * @win: A text buffer or text grid window to request line input on.
204  * @buf: A buffer of at least @maxlen characters.
205  * @maxlen: Length of the buffer.
206  * @initlen: The number of characters in @buf to pre-enter.
207  *
208  * Request input of a line of Unicode characters. This works the same as
209  * glk_request_line_event(), except the result is stored in an array of
210  * <type>glui32</type> values instead of an array of characters, and the values
211  * may be any valid Unicode code points.
212  * 
213  * The result will be in Unicode Normalization Form C. This basically means that
214  * composite characters will be single characters where possible, instead of
215  * sequences of base and combining marks. See 
216  * <ulink url="http://www.unicode.org/reports/tr15/">Unicode Standard Annex #15
217  * </ulink> for the details.
218  */
219 void
220 glk_request_line_event_uni(winid_t win, glui32 *buf, glui32 maxlen, glui32 initlen)
221 {
222         VALID_WINDOW(win, return);
223         g_return_if_fail(buf);
224         g_return_if_fail(win->input_request_type == INPUT_REQUEST_NONE);
225         g_return_if_fail(win->type != wintype_TextBuffer || win->type != wintype_TextGrid);
226         g_return_if_fail(initlen <= maxlen);
227
228         win->input_request_type = INPUT_REQUEST_LINE_UNICODE;
229         win->line_input_buffer_unicode = buf;
230         win->line_input_buffer_max_len = maxlen;
231
232         gchar *utf8;
233         if(initlen > 0) {
234                 utf8 = convert_ucs4_to_utf8(buf, initlen);
235                 if(utf8 == NULL)
236                         return;
237         }
238         else
239                 utf8 = g_strdup("");
240
241     switch(win->type)
242         {
243             case wintype_TextBuffer:
244                 text_buffer_request_line_event_common(win, maxlen, (initlen > 0), utf8);
245                 break;
246             case wintype_TextGrid:
247                 text_grid_request_line_event_common(win, maxlen, (initlen > 0), utf8);
248                 break;
249     }           
250         g_free(utf8);
251 }
252
253 /**
254  * glk_cancel_line_event:
255  * @win: A text buffer or text grid window to cancel line input on.
256  * @event: Will be filled in if the user had already input something.
257  *
258  * This cancels a pending request for line input. (Either Latin-1 or Unicode.)
259  *
260  * The event pointed to by the event argument will be filled in as if the
261  * player had hit enter, and the input composed so far will be stored in the
262  * buffer; see below. If you do not care about this information, pass NULL as
263  * the event argument. (The buffer will still be filled.) 
264  *
265  * For convenience, it is legal to call glk_cancel_line_event() even if there
266  * is no line input request on that window. The event type will be set to
267  * evtype_None in this case.
268  */
269 void
270 glk_cancel_line_event(winid_t win, event_t *event)
271 {
272         /* TODO: write me */
273         VALID_WINDOW(win, return);
274         g_return_if_fail(win->input_request_type == INPUT_REQUEST_NONE);
275         g_return_if_fail(win->type != wintype_TextBuffer || win->type != wintype_TextGrid);
276
277         event = NULL;
278 }
279
280 /* Internal function: General callback for signal key-press-event on a text buffer or text grid window. Used in character input on both text buffers and grids, and also in line input on grids, to redirect keystrokes to the line input field. Blocked when not in use. */
281 gboolean
282 on_window_key_press_event(GtkWidget *widget, GdkEventKey *event, winid_t win)
283 {
284         /* If this is a text grid window, and line input is active, then redirect the key press to the line input GtkEntry */
285         if( win->type == wintype_TextGrid && (win->input_request_type == INPUT_REQUEST_LINE || win->input_request_type == INPUT_REQUEST_LINE_UNICODE) )
286         {
287                 if(event->keyval == GDK_Up || event->keyval == GDK_KP_Up
288                     || event->keyval == GDK_Down || event->keyval == GDK_KP_Down
289                     || event->keyval == GDK_Left || event->keyval == GDK_KP_Left
290                     || event->keyval == GDK_Right || event->keyval == GDK_KP_Right
291                     || event->keyval == GDK_Tab || event->keyval == GDK_KP_Tab
292                     || event->keyval == GDK_Page_Up || event->keyval == GDK_KP_Page_Up
293                     || event->keyval == GDK_Page_Down || event->keyval == GDK_KP_Page_Down
294                     || event->keyval == GDK_Home || event->keyval == GDK_KP_Home
295                     || event->keyval == GDK_End || event->keyval == GDK_KP_End)
296                         return FALSE; /* Don't redirect these keys */
297                 gtk_widget_grab_focus(win->input_entry);
298                 gtk_editable_set_position(GTK_EDITABLE(win->input_entry), -1);
299                 gboolean retval = TRUE;
300                 g_signal_emit_by_name(win->input_entry, "key-press-event", event, &retval);
301                 return retval; /* Block this key event if the entry handled it */
302         }
303         if(win->input_request_type != INPUT_REQUEST_CHARACTER && 
304                 win->input_request_type != INPUT_REQUEST_CHARACTER_UNICODE)
305                 return FALSE;
306
307         int keycode;
308
309         switch(event->keyval) {
310                 case GDK_Up:
311                 case GDK_KP_Up: keycode = keycode_Up; break;
312                 case GDK_Down: 
313                 case GDK_KP_Down: keycode = keycode_Down; break;
314                 case GDK_Left:
315                 case GDK_KP_Left: keycode = keycode_Left; break;
316                 case GDK_Right:
317                 case GDK_KP_Right: keycode = keycode_Right; break;
318                 case GDK_Linefeed:
319                 case GDK_Return:
320                 case GDK_KP_Enter: keycode = keycode_Return; break;
321                 case GDK_Delete:
322                 case GDK_BackSpace:
323                 case GDK_KP_Delete: keycode = keycode_Delete; break;
324                 case GDK_Escape: keycode = keycode_Escape; break;
325                 case GDK_Tab: 
326                 case GDK_KP_Tab: keycode = keycode_Tab; break;
327                 case GDK_Page_Up:
328                 case GDK_KP_Page_Up: keycode = keycode_PageUp; break;
329                 case GDK_Page_Down:
330                 case GDK_KP_Page_Down: keycode = keycode_PageDown; break;
331                 case GDK_Home:
332                 case GDK_KP_Home: keycode = keycode_Home; break;
333                 case GDK_End:
334                 case GDK_KP_End: keycode = keycode_End; break;
335                 case GDK_F1: 
336                 case GDK_KP_F1: keycode = keycode_Func1; break;
337                 case GDK_F2: 
338                 case GDK_KP_F2: keycode = keycode_Func2; break;
339                 case GDK_F3: 
340                 case GDK_KP_F3: keycode = keycode_Func3; break;
341                 case GDK_F4: 
342                 case GDK_KP_F4: keycode = keycode_Func4; break;
343                 case GDK_F5: keycode = keycode_Func5; break;
344                 case GDK_F6: keycode = keycode_Func6; break;
345                 case GDK_F7: keycode = keycode_Func7; break;
346                 case GDK_F8: keycode = keycode_Func8; break;
347                 case GDK_F9: keycode = keycode_Func9; break;
348                 case GDK_F10: keycode = keycode_Func10; break;
349                 case GDK_F11: keycode = keycode_Func11; break;
350                 case GDK_F12: keycode = keycode_Func12; break;
351                 default:
352                         keycode = gdk_keyval_to_unicode(event->keyval);
353                         /* If keycode is 0, then keyval was not recognized; also return
354                         unknown if Latin-1 input was requested and the character is not in
355                         Latin-1 */
356                         if(keycode == 0 || (win->input_request_type == INPUT_REQUEST_CHARACTER && keycode > 255))
357                                 keycode = keycode_Unknown;      
358         }
359
360         event_throw(evtype_CharInput, win, keycode, 0);
361         
362         /* Only one keypress will be handled */
363         win->input_request_type = INPUT_REQUEST_NONE;
364         g_signal_handler_block( G_OBJECT(win->widget), win->keypress_handler );
365
366         return TRUE;
367 }
368
369 /* Internal function: finish handling a line input request, for both text grid and text buffer windows. */
370 static void
371 end_line_input_request(winid_t win, const gchar *inserted_text)
372 {
373     /* Convert the string from UTF-8 to Latin-1 or Unicode */
374     if(win->input_request_type == INPUT_REQUEST_LINE) 
375     {
376         gsize bytes_written;
377         gchar *latin1 = convert_utf8_to_latin1(inserted_text, &bytes_written);
378         
379         if(latin1 == NULL)
380         {
381             event_throw(evtype_LineInput, win, 0, 0);
382             return;
383         }
384
385         /* Place input in the echo stream */
386         if(win->echo_stream != NULL) 
387             glk_put_string_stream(win->echo_stream, latin1);
388
389         /* Copy the string (bytes_written does not include the NULL at the end) */
390         int copycount = MIN(win->line_input_buffer_max_len, bytes_written);
391         memcpy(win->line_input_buffer, latin1, copycount);
392         g_free(latin1);
393         event_throw(evtype_LineInput, win, copycount, 0);
394     }
395     else if(win->input_request_type == INPUT_REQUEST_LINE_UNICODE) 
396     {
397         glong items_written;
398         gunichar *unicode = convert_utf8_to_ucs4(inserted_text, &items_written);
399         
400         if(unicode == NULL)
401         {
402             event_throw(evtype_LineInput, win, 0, 0);
403             return;
404         }
405
406         /* Place input in the echo stream */
407         if(win->echo_stream != NULL) 
408             glk_put_string_stream_uni(win->echo_stream, unicode);
409
410         /* Copy the string (but not the NULL at the end) */
411         int copycount = MIN(win->line_input_buffer_max_len, items_written);
412         memcpy(win->line_input_buffer_unicode, unicode, copycount * sizeof(gunichar));
413         g_free(unicode);
414         event_throw(evtype_LineInput, win, copycount, 0);
415     }
416     else 
417         WARNING("Wrong input request type");
418
419     win->input_request_type = INPUT_REQUEST_NONE;
420 }
421
422 /* Internal function: Callback for signal insert-text on a text buffer window.
423 Runs after the default handler has already inserted the text.
424 FIXME: This function assumes that newline was the last character typed into the
425 window. That assumption is wrong if, for example, text containing a newline was
426 pasted into the window. */
427 void
428 after_window_insert_text(GtkTextBuffer *textbuffer, GtkTextIter *location, gchar *text, gint len, winid_t win) 
429 {
430         if( strchr(text, '\n') != NULL ) 
431         {
432                 /* Remove signal handlers */
433                 GtkTextBuffer *window_buffer = gtk_text_view_get_buffer( GTK_TEXT_VIEW(win->widget) );
434                 g_signal_handler_block(window_buffer, win->insert_text_handler);
435                 
436                 /* Make the window uneditable again and retrieve the text that was input */
437                 gchar *inserted_text;
438                 GtkTextIter start_iter, end_iter;
439
440         gtk_text_view_set_editable(GTK_TEXT_VIEW(win->widget), FALSE);
441         GtkTextMark *input_position = gtk_text_buffer_get_mark(window_buffer, "input_position");
442         gtk_text_buffer_get_iter_at_mark(window_buffer, &start_iter, input_position);
443         gtk_text_buffer_get_end_iter(window_buffer, &end_iter);
444                 gtk_text_iter_backward_cursor_position(&end_iter); /* don't include \n */
445         
446         inserted_text = gtk_text_buffer_get_text(window_buffer, &start_iter, &end_iter, FALSE);
447
448         end_line_input_request(win, inserted_text);
449         g_free(inserted_text);
450         }
451 }
452
453 /* Internal function: Callback for signal activate on the line input GtkEntry
454 in a text grid window. */
455 void
456 on_input_entry_activate(GtkEntry *input_entry, winid_t win)
457 {
458         GtkTextBuffer *buffer = gtk_text_view_get_buffer( GTK_TEXT_VIEW(win->widget) );
459         
460         gchar *text = g_strdup(gtk_entry_get_text(input_entry));
461         /* Move the focus back into the text view */
462         gtk_widget_grab_focus(win->widget);
463         /* Remove entry widget from text view */
464         /* Should be ok even though this is the widget's own signal handler */
465         gtk_container_remove( GTK_CONTAINER(win->widget), GTK_WIDGET(input_entry) );
466         win->input_entry = NULL;
467         /* Delete the child anchor */
468         GtkTextIter start, end;
469         gtk_text_buffer_get_iter_at_child_anchor(buffer, &start, win->input_anchor);
470         end = start;
471         gtk_text_iter_forward_char(&end); /* Point after the child anchor */
472         gtk_text_buffer_delete(buffer, &start, &end);
473         win->input_anchor = NULL;
474         
475     gchar *spaces = g_strnfill(win->input_length - g_utf8_strlen(text, -1), ' ');
476     gchar *text_to_insert = g_strconcat(text, spaces, NULL);
477         g_free(spaces);
478     gtk_text_buffer_insert(buffer, &start, text_to_insert, -1);
479     g_free(text_to_insert);
480     
481         g_signal_handler_block( G_OBJECT(win->widget), win->keypress_handler );
482         
483     end_line_input_request(win, text);
484         g_free(text);
485 }
486