1 /**************************************************************************/
2 /*  Klavaro - a flexible touch typing tutor                               */
3 /*  Copyright (C) from 2005 until 2008 Felipe Castro                      */
4 /*  Copyright (C) from 2009 until 2019 The Free Software Foundation       */
5 /*                                                                        */
6 /*  This program is free software, licensed under the terms of the GNU    */
7 /*  General Public License as published by the Free Software Foundation,  */
8 /*  either version 3 of the License, or (at your option) any later        */
9 /*  version. You should have received a copy of the GNU General Public    */
10 /*  License along with this program. If not,                              */
11 /*  see <https://www.gnu.org/licenses/>.                                  */
12 /**************************************************************************/
13 
14 /*
15  * Shared tutor window tasks
16  */
17 #include <math.h>
18 #include <string.h>
19 #include <time.h>
20 #include <stdio.h>
21 #include <stdlib.h>
22 #include <locale.h>
23 #include <errno.h>
24 #include <glib.h>
25 #include <glib/gstdio.h>
26 #include <gtk/gtk.h>
27 
28 #include "main.h"
29 #include "auxiliar.h"
30 #include "callbacks.h"
31 #include "translation.h"
32 #include "keyboard.h"
33 #include "cursor.h"
34 #include "basic.h"
35 #include "adaptability.h"
36 #include "velocity.h"
37 #include "fluidness.h"
38 #include "accuracy.h"
39 #include "top10.h"
40 #include "tutor.h"
41 
42 extern GtkCssProvider *keyb_css;
43 
44 #define MAX_TOUCH_TICS 4000
45 struct
46 {
47 	TutorType type;
48 	TutorQuery query;
49 	GTimer *tmr;
50 	gdouble elapsed_time;
51 	gdouble touch_time[MAX_TOUCH_TICS + 1];
52 	guint ttidx;
53 	gint n_touchs;
54 	gint n_errors;
55 	gint retro_pos;
56 	gint correcting;
57 } tutor;
58 
59 struct
60 {
61 	struct
62 	{
63 		double accuracy;
64 		double speed;
65 	} basic;
66 	struct
67 	{
68 		double accuracy;
69 		double speed;
70 		double accuracy_learning;
71 		double accuracy_improving;
72 		double accuracy_reaching;
73 	} adapt;
74 	struct
75 	{
76 		double accuracy;
77 		double speed;
78 		double speed_crawling;
79 		double speed_stepping;
80 		double speed_walking;
81 		double speed_jogging;
82 		double speed_running;
83 		double speed_professional;
84 		double speed_racer;
85 		double speed_flying;
86 	} velo;
87 	struct
88 	{
89 		double accuracy;
90 		double speed;
91 		double fluidity;
92 		double fluidity_stumbling;
93 		double speed_flying;
94 	} fluid;
95 } goal;
96 
97 extern gchar *OTHER_DEFAULT;
98 
99 /*******************************************************************************
100  * Interface functions
101  */
102 TutorType
tutor_get_type()103 tutor_get_type ()
104 {
105 	return (tutor.type);
106 }
107 
108 gchar *
tutor_get_type_name()109 tutor_get_type_name ()
110 {
111 	static gchar type_name[4][6] = { "basic", "adapt", "velo", "fluid" };
112 
113 	return (type_name[tutor.type]);
114 }
115 
116 gboolean
tutor_is_tibetan()117 tutor_is_tibetan ()
118 {
119 	gchar *code;
120 	gboolean is_tibt;
121 
122 	if (tutor.type == TT_BASIC || tutor.type == TT_ADAPT)
123 	{
124 		code = keyb_get_country_code (keyb_get_name ());
125 		is_tibt = g_str_equal (code, "bo");
126 		g_free (code);
127 		return (is_tibt);
128 	}
129 	else
130 	{
131 		return (g_str_has_prefix (main_preferences_get_string ("interface", "language"), "bo"));
132 	}
133 }
134 
135 TutorQuery
tutor_get_query()136 tutor_get_query ()
137 {
138 	return (tutor.query);
139 }
140 
141 void
tutor_set_query(TutorQuery query)142 tutor_set_query (TutorQuery query)
143 {
144 	tutor.query = query;
145 }
146 
147 gint
tutor_get_correcting()148 tutor_get_correcting ()
149 {
150 	if (tutor.type != TT_FLUID)
151 		return (FALSE);
152 	return (tutor.correcting);
153 }
154 
155 void
tutor_init_timers()156 tutor_init_timers ()
157 {
158 	tutor.tmr = g_timer_new ();
159 }
160 
161 #define GOAL_GSET(MODULE, GOAL, DEFAULT_VAL) \
162 	if (main_preferences_exist ("goals", #MODULE "_" #GOAL))\
163 		goal.MODULE.GOAL = (gdouble) main_preferences_get_int ("goals", #MODULE "_" #GOAL);\
164 	else\
165 	{\
166 		goal.MODULE.GOAL = DEFAULT_VAL;\
167 		main_preferences_set_int ("goals", #MODULE "_" #GOAL, DEFAULT_VAL);\
168 	}
169 #define LEVEL_GSET(MODULE, GOAL, DEFAULT_VAL) \
170 	if (main_preferences_exist ("levels", #MODULE "_" #GOAL))\
171 		goal.MODULE.GOAL = (gdouble) main_preferences_get_int ("levels", #MODULE "_" #GOAL);\
172 	else\
173 	{\
174 		goal.MODULE.GOAL = DEFAULT_VAL;\
175 		main_preferences_set_int ("levels", #MODULE "_" #GOAL, DEFAULT_VAL);\
176 	}
177 void
tutor_init_goals()178 tutor_init_goals ()
179 {
180 	GOAL_GSET (basic, accuracy, 95);
181 	GOAL_GSET (basic, speed, 10);
182 	GOAL_GSET (adapt, accuracy, 98);
183 	GOAL_GSET (adapt, speed, 10);
184 	GOAL_GSET (velo, accuracy, 95);
185 	GOAL_GSET (velo, speed, 50);
186 	GOAL_GSET (fluid, accuracy, 97);
187 	GOAL_GSET (fluid, speed, 50);
188 	GOAL_GSET (fluid, fluidity, 70);
189 
190 	LEVEL_GSET (adapt, accuracy_learning, 50);
191 	LEVEL_GSET (adapt, accuracy_improving, 90);
192 	LEVEL_GSET (adapt, accuracy_reaching, 95);
193 
194 	LEVEL_GSET (velo, speed_crawling, 10);
195 	LEVEL_GSET (velo, speed_stepping, 20);
196 	LEVEL_GSET (velo, speed_walking, 30);
197 	LEVEL_GSET (velo, speed_jogging, 40);
198 	LEVEL_GSET (velo, speed_running, 60);
199 	LEVEL_GSET (velo, speed_professional, 70);
200 	LEVEL_GSET (velo, speed_racer, 80);
201 	LEVEL_GSET (velo, speed_flying, 90);
202 
203 	LEVEL_GSET (fluid, fluidity_stumbling, 60);
204 	LEVEL_GSET (fluid, speed_flying, 90);
205 
206 	/*
207 	g_printf ("basic accur: %.0f\n", goal.basic.accuracy);
208 	g_printf ("basic speed: %.0f\n", goal.basic.speed);
209 	g_printf ("adapt accur: %.0f\n", goal.adapt.accuracy);
210 	g_printf ("adapt speed: %.0f\n", goal.adapt.speed);
211 	g_printf ("velo accur: %.0f\n", goal.velo.accuracy);
212 	g_printf ("velo speed: %.0f\n", goal.velo.speed);
213 	g_printf ("fluid accur: %.0f\n", goal.fluid.accuracy);
214 	g_printf ("fluid speed: %.0f\n", goal.fluid.speed);
215 	g_printf ("fluid fluidity: %.0f\n", goal.fluid.fluidity);
216 
217 	g_printf ("adapt learning: %.0f\n", goal.adapt.accuracy_learning);
218 	g_printf ("adapt improving: %.0f\n", goal.adapt.accuracy_improving);
219 	g_printf ("adapt reaching: %.0f\n", goal.adapt.accuracy_reaching);
220 
221 	g_printf ("velo crawling: %.0f\n", goal.velo.speed_crawling);
222 	g_printf ("velo stepping: %.0f\n", goal.velo.speed_stepping);
223 	g_printf ("velo walking: %.0f\n", goal.velo.speed_walking);
224 	g_printf ("velo jogging: %.0f\n", goal.velo.speed_jogging);
225 	g_printf ("velo running: %.0f\n", goal.velo.speed_running);
226 	g_printf ("velo professional: %.0f\n", goal.velo.speed_professional);
227 	g_printf ("velo racer: %.0f\n", goal.velo.speed_racer);
228 	g_printf ("velo flying: %.0f\n", goal.velo.speed_flying);
229 
230 	g_printf ("fluid stumbling: %.0f\n", goal.fluid.fluidity_stumbling);
231 	g_printf ("fluid flying: %.0f\n", goal.fluid.speed_flying);
232 	 */
233 }
234 
235 gdouble
tutor_goal_accuracy()236 tutor_goal_accuracy ()
237 {
238 	switch (tutor.type)
239 	{
240 		case TT_BASIC: return goal.basic.accuracy;
241 		case TT_ADAPT: return goal.adapt.accuracy;
242 		case TT_VELO: return goal.velo.accuracy;
243 		case TT_FLUID: return goal.fluid.accuracy;
244 	}
245 	return -1.0;
246 }
247 
248 gdouble
tutor_goal_speed()249 tutor_goal_speed ()
250 {
251 	switch (tutor.type)
252 	{
253 		case TT_BASIC: return goal.basic.speed;
254 		case TT_ADAPT: return goal.adapt.speed;
255 		case TT_VELO: return goal.velo.speed;
256 		case TT_FLUID: return goal.fluid.speed;
257 	}
258 	return -1.0;
259 }
260 
261 gdouble
tutor_goal_fluidity()262 tutor_goal_fluidity ()
263 {
264 	switch (tutor.type)
265 	{
266 		case TT_BASIC:
267 		case TT_ADAPT:
268 		case TT_VELO: return 0.0;
269 		case TT_FLUID: return goal.fluid.fluidity;
270 	}
271 	return -1.0;
272 }
273 
274 gdouble
tutor_goal_level(guint n)275 tutor_goal_level (guint n)
276 {
277 	switch (tutor.type)
278 	{
279 		case TT_ADAPT: if (n > 2) return -1.0; return (&goal.adapt.accuracy_learning) [n];
280 		case TT_VELO: if (n > 7) return -2.0; return (&goal.velo.speed_crawling) [n];
281 		case TT_FLUID: if (n > 1) return -3.0; return (&goal.fluid.fluidity_stumbling) [n];
282 	}
283 	return -4.0;
284 }
285 
286 /**********************************************************************
287  * Initialize the course
288  */
289 void
tutor_init(TutorType tt_type)290 tutor_init (TutorType tt_type)
291 {
292 	gchar *tmp_title = NULL;
293 	gchar *tmp_name = NULL;
294 	GtkWidget *wg;
295 
296 	gtk_widget_hide (get_wg ("window_main"));
297 	gtk_widget_hide (get_wg ("window_keyboard"));
298 	gtk_widget_hide (get_wg ("dialog_info"));
299 	gtk_widget_hide (get_wg ("aboutdialog_klavaro"));
300 	gtk_widget_hide (get_wg ("togglebutton_toomuch_errors"));
301 	gtk_widget_show (get_wg ("window_tutor"));
302 	gtk_widget_grab_focus (get_wg ("entry_mesg"));
303 
304 	tutor.type = tt_type;
305 	cursor_set_blink (FALSE);
306 
307 	/******************************
308 	 * Set the layout for each exercise type
309 	 */
310 	gtk_widget_hide (get_wg ("button_tutor_top10"));
311 	gtk_widget_hide (get_wg ("entry_custom_basic_lesson"));
312 	if (tutor.type == TT_BASIC || tutor.type == TT_FLUID)
313 	{
314 		gtk_widget_show (get_wg ("label_lesson"));
315 		gtk_widget_show (get_wg ("spinbutton_lesson"));
316 		gtk_widget_show (get_wg ("vseparator_tutor_2"));
317 		if (tutor.type == TT_BASIC)
318 		{
319 			gtk_widget_show (get_wg ("togglebutton_edit_basic_lesson"));
320 			gtk_widget_hide (get_wg ("button_tutor_top10"));
321 			gtk_widget_show (get_wg ("button_tutor_show_keyb"));
322 			gtk_label_set_text (GTK_LABEL (get_wg ("label_lesson")), _("Lesson:"));
323 			callbacks_shield_set (TRUE);
324 			gtk_spin_button_set_range (GTK_SPIN_BUTTON (get_wg ("spinbutton_lesson")), 1, MAX_BASIC_LESSONS);
325 			callbacks_shield_set (FALSE);
326 		}
327 		else
328 		{
329 			gtk_widget_hide (get_wg ("togglebutton_edit_basic_lesson"));
330 			gtk_widget_show (get_wg ("button_tutor_top10"));
331 			gtk_widget_hide (get_wg ("button_tutor_show_keyb"));
332 			gtk_label_set_text (GTK_LABEL (get_wg ("label_lesson")), _("Paragraphs:"));
333 			callbacks_shield_set (TRUE);
334 			gtk_spin_button_set_range (GTK_SPIN_BUTTON (get_wg ("spinbutton_lesson")), 0, 10);
335 			callbacks_shield_set (FALSE);
336 		}
337 	}
338 	else
339 	{
340 		gtk_widget_hide (get_wg ("label_lesson"));
341 		gtk_widget_hide (get_wg ("spinbutton_lesson"));
342 		gtk_widget_hide (get_wg ("vseparator_tutor_2"));
343 		gtk_widget_hide (get_wg ("togglebutton_edit_basic_lesson"));
344 		gtk_widget_hide (get_wg ("button_tutor_show_keyb"));
345 		gtk_widget_hide (get_wg ("button_tutor_top10"));
346 	}
347 
348 	if (tutor.type == TT_BASIC || tutor.type == TT_ADAPT)
349 	{
350 		gtk_widget_hide (get_wg ("button_tutor_other"));
351 		gtk_widget_hide (get_wg ("vseparator_tutor_1"));
352 	}
353 	else
354 	{
355 		gtk_widget_show (get_wg ("button_tutor_other"));
356 		gtk_widget_show (get_wg ("vseparator_tutor_1"));
357 	}
358 
359 	/******************************
360 	 * Set decoration texts and tips
361 	 */
362 	switch (tutor.type)
363 	{
364 	case TT_BASIC:
365 		tmp_title = g_strdup (_("Klavaro - Basic Course"));
366 		tmp_name = g_strdup ("");
367 		break;
368 
369 	case TT_ADAPT:
370 		tmp_title = g_strdup (_("Klavaro - Adaptability"));
371 		tmp_name =
372 			g_strdup (_
373 				  ("Adaptability exercises: automating the fingers"
374 				   " responses, typing over all the keyboard."));
375 		break;
376 
377 	case TT_VELO:
378 		tmp_title = g_strdup (_("Klavaro - Velocity"));
379 		tmp_name = g_strdup (_("Velocity exercises: accelerate typing real words."));
380 		break;
381 
382 	case TT_FLUID:
383 		tmp_title = g_strdup (_("Klavaro - Fluidness"));
384 		tmp_name =
385 			g_strdup (_("Fluidness exercises: accuracy typing good sense paragraphs."));
386 		break;
387 	}
388 	gtk_window_set_title (get_win ("window_tutor"), tmp_title);
389 	wg = get_wg ("label_heading");
390 	gtk_label_set_text (GTK_LABEL (wg), tmp_name);
391 	g_free (tmp_title);
392 	g_free (tmp_name);
393 
394 	/******************************
395 	 * Set tooltips of tutor entry (drag and drop)
396 	 */
397 	if (tutor.type == TT_VELO || tutor.type == TT_FLUID)
398 		gtk_widget_set_tooltip_text (get_wg ("entry_mesg"), _("Drag and drop text here to practice with it."));
399 	else
400 		gtk_widget_set_tooltip_text (get_wg ("entry_mesg"), "");
401 
402 	/******************************
403 	 * Set specific variables
404 	 */
405 	tutor.query = QUERY_INTRO;
406 	if (tutor.type == TT_BASIC)
407 	{
408 		basic_init ();
409 		if (basic_get_lesson () > 1)
410 		{
411 			tutor_process_touch ('\0');
412 			return;
413 		}
414 	}
415 	else if (tutor.type == TT_VELO)
416 	{
417 		velo_init ();
418 	}
419 	else if (tutor.type == TT_FLUID)
420 	{
421 		fluid_init ();
422 	}
423 	tutor_update ();
424 }
425 
426 
427 /**********************************************************************
428  * Update what is shown in the tutor window.
429  */
430 void
tutor_update()431 tutor_update ()
432 {
433 	switch (tutor.query)
434 	{
435 	case QUERY_INTRO:
436 		tutor_update_intro ();
437 		break;
438 
439 	case QUERY_START:
440 		tutor_update_start ();
441 		break;
442 
443 	case QUERY_PROCESS_TOUCHS:
444 		break;
445 
446 	case QUERY_END:
447 		tutor_message (_("End of exercise. Press [Enter] to start another."));
448 		break;
449 	}
450 }
451 
452 void
tutor_update_intro()453 tutor_update_intro ()
454 {
455 	gchar *tmp_name;
456 	gchar *text;
457 	gchar *color_bg;
458 	GdkRGBA color;
459 	GtkWidget *wg;
460 	GtkLabel *wg_label;
461 	GtkTextView *wg_text;
462 	GtkAdjustment *scroll;
463 	GtkTextIter start;
464 	GtkTextIter end;
465 	GtkStyleContext *sc;
466 
467 	if (tutor.type == TT_BASIC)
468 	{
469 		callbacks_shield_set (TRUE);
470 		gtk_spin_button_set_value (GTK_SPIN_BUTTON
471 					   (get_wg ("spinbutton_lesson")),
472 					   basic_get_lesson ());
473 		callbacks_shield_set (FALSE);
474 
475 		wg_label = GTK_LABEL (get_wg ("label_heading"));
476 		gtk_label_set_text (wg_label, _("Learning the key positions."));
477 	}
478 
479 	tutor_message (_("Press any key to start the exercise. "));
480 
481 	tmp_name = g_strconcat ("_", tutor_get_type_name (), "_intro.txt", NULL);
482 	text = trans_read_text (tmp_name);
483 	g_free (tmp_name);
484 
485 	wg_text = GTK_TEXT_VIEW (get_wg ("text_tutor"));
486 	gtk_text_buffer_set_text (gtk_text_view_get_buffer (wg_text), text, -1);
487 	g_free (text);
488 
489 	gtk_text_buffer_get_bounds (gtk_text_view_get_buffer (wg_text), &start, &end);
490 	gtk_text_buffer_apply_tag_by_name (gtk_text_view_get_buffer (wg_text), "lesson_font", &start, &end);
491 	gtk_text_buffer_apply_tag_by_name (gtk_text_view_get_buffer (wg_text), "text_intro", &start, &end);
492 
493 	/*
494 	 * Apply tutor background color and font to the intro
495 	 */
496 	if (main_preferences_exist ("colors", "text_intro_bg"))
497 		color_bg = main_preferences_get_string ("colors", "text_intro_bg");
498 	else
499 		color_bg = g_strdup (TUTOR_WHITE);
500 	/* Check if altcolor applies */
501 	if (main_altcolor_get_boolean ("colors", "altcolor") == TRUE)
502 	{
503 		g_free (color_bg);
504 		color_bg = main_altcolor_get_string ("colors", "text_intro_bg");
505 	}
506 	gdk_rgba_parse (&color, color_bg);
507 	/* FIXME: update deprecated function for background color */
508 	gtk_widget_override_background_color (get_wg ("text_tutor"), GTK_STATE_FLAG_INSENSITIVE, &color);
509 	g_free (color_bg);
510 
511 	wg = get_wg ("scrolledwindow_tutor_main");
512 	scroll = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (wg));
513 	gtk_adjustment_set_value (scroll, 0);
514 
515 	callbacks_shield_set (TRUE);
516 	gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (get_wg ("togglebutton_tutor_intro")), TRUE);
517 	callbacks_shield_set (FALSE);
518 }
519 
520 void
tutor_update_start()521 tutor_update_start ()
522 {
523 	gchar *tmp_name;
524 	gchar *text;
525 	gchar *color_bg;
526 	GdkRGBA color;
527 	GtkWidget *wg;
528 	GtkTextBuffer *buf;
529 	GtkTextIter start;
530 	GtkTextIter end;
531 	GtkAdjustment *scroll;
532 
533 	/*
534 	 * Delete all the text on tutor window
535 	 */
536 	wg = get_wg ("text_tutor");
537 	buf = gtk_text_view_get_buffer (GTK_TEXT_VIEW (wg));
538 	gtk_text_buffer_set_text (buf, "", -1);
539 
540 	if (tutor.type == TT_BASIC)
541 	{
542 		callbacks_shield_set (TRUE);
543 		gtk_spin_button_set_value (GTK_SPIN_BUTTON (get_wg ("spinbutton_lesson")),
544 					   basic_get_lesson ());
545 		callbacks_shield_set (FALSE);
546 
547 		tmp_name = g_ucs4_to_utf8 (basic_get_char_set (), -1, NULL, NULL, NULL);
548 		text = g_strdup_printf ("%s %s", _("Keys:"), tmp_name);
549 		g_free (tmp_name);
550 		wg = get_wg ("label_heading");
551 		gtk_label_set_text (GTK_LABEL (wg), text);
552 		g_free (text);
553 
554 		basic_draw_lesson ();
555 	}
556 
557 	switch (tutor.type)
558 	{
559 	case TT_BASIC:
560 		break;
561 	case TT_ADAPT:
562 		adapt_draw_random_pattern ();
563 		break;
564 	case TT_VELO:
565 		if (main_velo_txt ())
566 			fluid_draw_random_paragraphs ();
567 		else
568 			velo_draw_random_words ();
569 		break;
570 	case TT_FLUID:
571 		fluid_draw_random_paragraphs ();
572 	}
573 
574 	/*
575 	 * Apply tutor background color and font to the text
576 	 */
577 	if (main_preferences_exist ("colors", "char_untouched_bg"))
578 		color_bg = main_preferences_get_string ("colors", "char_untouched_bg");
579 	else
580 		color_bg = g_strdup (TUTOR_CREAM);
581 	/* Check if altcolor applies */
582 	if (main_altcolor_get_boolean ("colors", "altcolor") == TRUE)
583 	{
584 		g_free (color_bg);
585 		color_bg = main_altcolor_get_string ("colors", "char_untouched_bg");
586 	}
587 	/* FIXME: update deprecated function for background color */
588 	gdk_rgba_parse (&color, color_bg);
589 	gtk_widget_override_background_color (get_wg ("text_tutor"), GTK_STATE_FLAG_INSENSITIVE, &color);
590 	g_free (color_bg);
591 
592 	gtk_text_buffer_get_bounds (buf, &start, &end);
593 	gtk_text_buffer_apply_tag_by_name (buf, "lesson_font", &start, &end);
594 	gtk_text_buffer_apply_tag_by_name (buf, "char_untouched", &start, &end);
595 
596 	/* Trying to minimize wrapping toggle because of cursor blinking:
597 	*/
598 	end = start;
599 	while (gtk_text_iter_forward_word_end (&end))
600 	{
601 		gtk_text_buffer_apply_tag_by_name (buf, "char_keep_wrap", &start, &end);
602 		start = end;
603 		if (! gtk_text_iter_forward_char (&end))
604 			break;
605 		/* This second one seems to not be needed. FIXME ?
606 		gtk_text_buffer_apply_tag_by_name (buf, "char_keep_wrap2", &start, &end);
607 		*/
608 		start = end;
609 	}
610 
611 	if (tutor.type == TT_FLUID)
612 		tmp_name = g_strconcat (_("Start typing when you are ready. "), " ",
613 			       _("Use backspace to correct errors."), " ", NULL);
614 	else
615 		tmp_name = g_strdup (_("Start typing when you are ready. "));
616 
617 	tutor_message (tmp_name);
618 	g_free (tmp_name);
619 
620 	wg = get_wg ("scrolledwindow_tutor_main");
621 	scroll = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (wg));
622 	gtk_adjustment_set_value (scroll, 0);
623 
624 	callbacks_shield_set (TRUE);
625 	gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (get_wg ("togglebutton_tutor_intro")), FALSE);
626 	callbacks_shield_set (FALSE);
627 }
628 
629 
630 /**********************************************************************
631  * Respond to each touch of the user, according to the tutor.query mode
632  */
633 void
tutor_process_touch(gunichar user_chr)634 tutor_process_touch (gunichar user_chr)
635 {
636 	gboolean go_on;
637 	gchar *u8ch;
638 	GtkTextView *wg_text;
639 	GtkTextBuffer *wg_buffer;
640 	GtkTextIter start;
641 
642 	wg_text = GTK_TEXT_VIEW (get_wg ("text_tutor"));
643 	wg_buffer = gtk_text_view_get_buffer (wg_text);
644 
645 	switch (tutor.query)
646 	{
647 	case QUERY_PROCESS_TOUCHS:
648 		break;
649 
650 	case QUERY_INTRO:
651 		tutor.query = QUERY_START;
652 		tutor_update ();
653 		tutor.n_touchs = 0;
654 		tutor.n_errors = 0;
655 		tutor.retro_pos = 0;
656 		tutor.correcting = 0;
657 		tutor.ttidx = 0;
658 		gtk_text_buffer_get_start_iter (wg_buffer, &start);
659 		gtk_text_buffer_place_cursor (wg_buffer, &start);
660 		cursor_set_blink (TRUE);
661 		cursor_on (NULL);
662 		accur_sort ();
663 
664 		switch (tutor.type)
665 		{
666 		case TT_BASIC:
667 			hints_update_from_char (cursor_get_char ());
668 			tutor_speak_char ();
669 			return;
670 		case TT_ADAPT:
671 			tutor_speak_char ();
672 			return;
673 		case TT_VELO:
674 			tutor_speak_word ();
675 			return;
676 		case TT_FLUID:
677 			return;
678 		}
679 		return;
680 
681 	case QUERY_START:
682 		tutor.query = QUERY_PROCESS_TOUCHS;
683 		callbacks_shield_set (TRUE);
684 		u8ch = g_malloc0 (7);
685 		if (g_unichar_to_utf8 (user_chr, u8ch) > 0)
686 			tutor_message (u8ch);
687 		else
688 			tutor_message ("");
689 		g_free (u8ch);
690 		callbacks_shield_set (FALSE);
691 
692 		g_timer_start (tutor.tmr);
693 		if (tutor.type == TT_FLUID)
694 			tutor_eval_forward_backward (user_chr);
695 		else
696 			tutor_eval_forward (user_chr);
697 
698 		switch (tutor.type)
699 		{
700 		case TT_BASIC:
701 			hints_update_from_char (cursor_get_char ());
702 			tutor_speak_char ();
703 			return;
704 		case TT_ADAPT:
705 			tutor_speak_char ();
706 			return;
707 		case TT_VELO:
708 			tutor_speak_word ();
709 			return;
710 		case TT_FLUID:
711 			return;
712 		}
713 		return;
714 
715 	case QUERY_END:
716 		if (user_chr == UPSYM)
717 		{
718 			basic_set_lesson_increased (FALSE);
719 			tutor.query = QUERY_INTRO;
720 			tutor_process_touch (L'\0');
721 		}
722 		else if (user_chr == (gunichar) 8 && tutor.type == TT_BASIC)
723 		{
724 			basic_set_lesson (basic_get_lesson () - 1);
725 			basic_init_char_set ();
726 			basic_set_lesson_increased (FALSE);
727 			tutor.query = QUERY_INTRO;
728 			tutor_process_touch (L'\0');
729 		}
730 		else
731 		{
732 			tutor_beep ();
733 			tutor_update ();
734 		}
735 		return;
736 	}
737 
738 	/* It is time to analise the correctness of typing.
739 	 */
740 	if (tutor.type == TT_FLUID)
741 		go_on = tutor_eval_forward_backward (user_chr);
742 	else
743 		go_on = tutor_eval_forward (user_chr);
744 
745 	if (go_on == FALSE)
746 	{
747 		cursor_set_blink (FALSE);
748 		cursor_off (NULL);
749 		tutor.elapsed_time = g_timer_elapsed (tutor.tmr, NULL);
750 
751 		tutor_calc_stats ();
752 		tutor.query = QUERY_END;
753 		tutor_update ();
754 		tutor_beep ();
755 	}
756 	else
757 	{
758 		switch (tutor.type)
759 		{
760 		case TT_BASIC:
761 			cursor_on (NULL);
762 			hints_update_from_char (cursor_get_char ());
763 			tutor_speak_char ();
764 			return;
765 		case TT_ADAPT:
766 			cursor_on (NULL);
767 			tutor_speak_char ();
768 			return;
769 		case TT_VELO:
770 			cursor_on (NULL);
771 			tutor_speak_word ();
772 			return;
773 		case TT_FLUID:
774 			cursor_off (NULL);
775 			return;
776 		}
777 	}
778 }
779 
780 /**********************************************************************
781  * Advances the cursor one position and test for correctness,
782  * in the shared tutor window.
783  * Updates the variables:
784  * cursor_pos, n_touchs and n_errors.
785  */
786 gboolean
tutor_eval_forward(gunichar user_chr)787 tutor_eval_forward (gunichar user_chr)
788 {
789 	gunichar real_chr;
790 
791 	if (user_chr == L'\b' || user_chr == L'\t')
792 	{
793 		tutor_beep ();
794 		return (TRUE);
795 	}
796 
797 	gsize n_touchs = g_unichar_fully_decompose (user_chr, FALSE, NULL, 0);
798 	tutor.n_touchs += (int) n_touchs;
799 
800 	real_chr = cursor_get_char ();
801 
802 	// Minimizing the line breaking bug:
803 	if (user_chr == UPSYM && real_chr == L' ')
804 		user_chr = L' ';
805 
806 	/*
807 	 * Compare the user char with the real char and set the color
808 	 */
809 	if (user_chr == real_chr)
810 	{
811 		if (tutor.ttidx < MAX_TOUCH_TICS)
812 		{
813 			tutor.touch_time[tutor.ttidx] =
814 				g_timer_elapsed (tutor.tmr, NULL) - tutor.touch_time[tutor.ttidx];
815 			tutor.ttidx++;
816 			tutor.touch_time[tutor.ttidx] = g_timer_elapsed (tutor.tmr, NULL);
817 			if (tutor.type != TT_BASIC)
818 				accur_correct (real_chr, tutor.touch_time[tutor.ttidx-1]);
819 		}
820 		cursor_paint_char ("char_correct");
821 	}
822 	else
823 	{
824 		tutor.touch_time[tutor.ttidx] = g_timer_elapsed (tutor.tmr, NULL);
825 		if (tutor.type != TT_BASIC)
826 			accur_wrong (real_chr);
827 		cursor_paint_char ("char_wrong");
828 		tutor.n_errors += (int) n_touchs;
829 		tutor_beep ();
830 	}
831 
832 
833 	/*
834 	 * Go forward and test end of text
835 	 */
836 	if (cursor_advance (1) != 1)
837 		return (FALSE);
838 
839 	/*
840 	 * Test end of text
841 	 */
842 	if (cursor_get_char () == L'\n')
843 		if (cursor_advance (1) != 1)
844 			return (FALSE);
845 
846 	return (TRUE);
847 }
848 
849 /**********************************************************************
850  * Like the previous, but allows to go back and forth.
851  */
852 gboolean
tutor_eval_forward_backward(gunichar user_chr)853 tutor_eval_forward_backward (gunichar user_chr)
854 {
855 	gunichar real_chr;
856 
857 	/*
858 	 * Work on backspaces
859 	 * L'\t' means an hyper <Ctrl> + <Backspace>
860 	 */
861 	if (user_chr == L'\b' || user_chr == L'\t')
862 	{
863 		tutor.touch_time[tutor.ttidx] = g_timer_elapsed (tutor.tmr, NULL);
864 
865 		/*
866 		 * Test for end of errors to be corrected
867 		 */
868 		if (tutor.retro_pos == 0)
869 		{
870 			tutor_beep ();
871 			return (TRUE);
872 		}
873 
874 		/*
875 		 * Go backwards and test for begin of the text
876 		 */
877 		if (cursor_advance (-1) != -1)
878 		{
879 			tutor_beep ();
880 			return (TRUE);
881 		}
882 
883 		/*
884 		 * Test for start of line
885 		 */
886 		if (cursor_get_char () == L'\n')
887 			if (cursor_advance (-1) != -1)
888 			{
889 				tutor_beep ();
890 				return (TRUE);
891 			}
892 
893 		/*
894 		 * Reinitialize the color (no visible effect at all...)
895 		 */
896 		cursor_paint_char ("char_untouched");
897 
898 		/*
899 		 * Update state
900 		 */
901 		tutor.retro_pos--;
902 		tutor.correcting++;
903 
904 		if (user_chr == L'\t')
905 			tutor_eval_forward_backward (L'\t');
906 		return (TRUE);
907 	}
908 
909 	real_chr = cursor_get_char ();
910 
911 	// Minimizing the line-breaking bug:
912 	if (user_chr == UPSYM && real_chr == L' ')
913 		user_chr = L' ';
914 
915 	/*
916 	 * Compare the user char with the real char and set the color
917 	 */
918 	if (user_chr == real_chr && tutor.retro_pos == 0)
919 	{
920 	        gsize n_touchs = g_unichar_fully_decompose (user_chr, FALSE, NULL, 0);
921 	        tutor.n_touchs += (int) n_touchs;
922 		if (tutor.ttidx < MAX_TOUCH_TICS)
923 		{
924 			tutor.touch_time[tutor.ttidx] =
925 				g_timer_elapsed (tutor.tmr, NULL) - tutor.touch_time[tutor.ttidx];
926 			tutor.ttidx++;
927 			tutor.touch_time[tutor.ttidx] = g_timer_elapsed (tutor.tmr, NULL);
928 		}
929 		if (tutor.correcting != 0)
930 		{
931 			cursor_paint_char ("char_retouched");
932 			tutor.n_errors += (int) n_touchs;
933 		}
934 		else
935 		{
936 			cursor_paint_char ("char_correct");
937 			accur_correct (real_chr, tutor.touch_time[tutor.ttidx-1]);
938 		}
939 	}
940 	else
941 	{
942 		tutor.touch_time[tutor.ttidx] = g_timer_elapsed (tutor.tmr, NULL);
943 		cursor_paint_char ("char_wrong");
944 		tutor.retro_pos++;
945 		if (tutor.retro_pos == 1)
946 			accur_wrong (real_chr);
947 		tutor_beep ();
948 	}
949 
950 	if (tutor.correcting > 0)
951 		tutor.correcting--;
952 
953 	/*
954 	 * Go forward and test end of text
955 	 */
956 	if (cursor_advance (1) != 1)
957 	{
958 		if (tutor.retro_pos == 0)
959 			return (FALSE);
960 		tutor.retro_pos--;
961 	}
962 
963 	/*
964 	 * Test end of paragraph
965 	 */
966 
967 	if (cursor_get_char () == L'\n')
968 		if (cursor_advance (1) != 1 && tutor.retro_pos == 0)
969 			return (FALSE);
970 
971 	return (TRUE);
972 }
973 
974 
975 /**********************************************************************
976  * Calculate the final results
977  */
978 void
tutor_calc_stats()979 tutor_calc_stats ()
980 {
981 	guint i = 0;
982 	gint minutes;
983 	gint seconds;
984 	gboolean may_log = TRUE;
985 	gdouble accuracy;
986 	gdouble touchs_per_second;
987 	gdouble velocity;
988 	gdouble fluidness;
989 	gdouble standard_deviation = 0;
990 	gdouble sum;
991 	gdouble average = 0;
992 	gdouble sample;
993 	gchar *contest_ps = NULL;
994 	gchar *tmp_locale;
995 	gchar *tmp_str = NULL;
996 	gchar *tmp_str2 = NULL;
997 	gchar *tmp_name;
998 	gchar *tmp;
999 	FILE *fh;
1000 	time_t tmp_time;
1001 	struct tm *ltime;
1002 	GtkWidget *wg;
1003 	GtkTextBuffer *buf;
1004 	GtkTextIter start;
1005 	GtkTextIter end;
1006 	Statistics stat;
1007 
1008 	/*
1009 	 * Calculate statistics
1010 	 */
1011 	minutes = ((gulong) tutor.elapsed_time) / 60;
1012 	seconds = ((gulong) tutor.elapsed_time) % 60;
1013 
1014 	accuracy = 100 * (1.0 - (gfloat) tutor.n_errors / tutor.n_touchs);
1015 	touchs_per_second = (gdouble) (tutor.n_touchs - tutor.n_errors) / tutor.elapsed_time;
1016 	velocity = 12 * touchs_per_second;
1017 
1018 	/* Average for touch timing
1019 	 */
1020 	sum = 0;
1021 	for (i = 2; i < tutor.ttidx; i++)
1022 	{
1023 		if (tutor.touch_time[i] <= 0)
1024 			tutor.touch_time[i] = 1.0e-8;
1025 		sample = sqrt (1 / tutor.touch_time[i]);
1026 		sum += sample;
1027 	}
1028 	if (i == 2)
1029 		i++;
1030 	average = sum / (i - 2);
1031 	if (average <= 0)
1032 		average = 1.0e-9;
1033 
1034 	/* Standard deviation for touch timing
1035 	 */
1036 	sum = 0;
1037 	for (i = 2; i < tutor.ttidx; i++)
1038 	{
1039 		sample = sqrt (1 / tutor.touch_time[i]);
1040 		sum += (sample - average) * (sample - average);
1041 	}
1042 	if (i < 4)
1043 		i = 4;
1044 	standard_deviation = sqrt (sum / (i - 3));
1045 
1046 	/* "Magic" fluidness calculation
1047 	 */
1048 	fluidness = 100 * (1 - standard_deviation / average);
1049 	if (fluidness < 2)
1050 		fluidness = 2;
1051 	stat.score = 0;
1052 
1053 	/* Verify if logging is allowed
1054 	 */
1055 	may_log = TRUE;
1056 	if (tutor.type == TT_FLUID)
1057 		if (tutor.n_touchs < MIN_CHARS_TO_LOG)
1058 		{
1059 			gdk_display_beep (gdk_display_get_default ());
1060 			contest_ps = g_strdup_printf (_("ps.: logging not performed for this session: "
1061 						  "the number of typed characters (%i) must be greater than %i."),
1062 						 tutor.n_touchs, MIN_CHARS_TO_LOG);
1063 			may_log = FALSE;
1064 		}
1065 	if (may_log)
1066 	{
1067 		/*
1068 		 * Changing to "C" locale: remember to copy the previous value!
1069 		 */
1070 		tmp_locale = g_strdup (setlocale (LC_NUMERIC, NULL));
1071 		if (tmp_locale != NULL)
1072 			setlocale (LC_NUMERIC, "C");
1073 
1074 		/* Logging
1075 		 */
1076 		tmp_name =
1077 			g_strconcat (main_path_stats (), G_DIR_SEPARATOR_S "stat_", tutor_get_type_name (), ".txt",
1078 				     NULL);
1079 		assert_user_dir ();
1080 		if (!(fh = (FILE *) g_fopen (tmp_name, "r")))
1081 		{
1082 			fh = (FILE *) g_fopen (tmp_name, "w");
1083 			if (tutor.type == TT_BASIC)
1084 				fprintf (fh, "Accuracy\tVelocity\tFluidness\tDate\tHour\tLesson\tKeyboard\n");
1085 			else if (tutor.type == TT_ADAPT)
1086 				fprintf (fh, "Accuracy\tVelocity\tFluidness\tDate\tHour\tKeyboard\tLanguage\n");
1087 			else
1088 				fprintf (fh, "Accuracy\tVelocity\tFluidness\tDate\tHour\tLesson\tLanguage\n");
1089 		}
1090 		else
1091 		{
1092 			fclose (fh);
1093 			fh = (FILE *) g_fopen (tmp_name, "a");
1094 		}
1095 		if (fh)
1096 		{
1097 			tmp_time = time (NULL);
1098 			ltime = localtime (&tmp_time);
1099 			fprintf (fh,
1100 				 "%.2f\t%.2f\t%.2f\t%i-%2.2i-%2.2i\t%2.2i:%2.2i\t",
1101 				 accuracy, velocity, fluidness,
1102 				 (ltime->tm_year) + 1900, (ltime->tm_mon) + 1,
1103 				 (ltime->tm_mday), (ltime->tm_hour), (ltime->tm_min));
1104 			switch (tutor.type)
1105 			{
1106 			case TT_BASIC:
1107 				fprintf (fh, "%2.2i\t", basic_get_lesson ());
1108 				break;
1109 			case TT_ADAPT:
1110 				tmp = g_strdup (keyb_get_name ());
1111 				for (i=0; tmp[i]; i++)
1112 					tmp[i] = (tmp[i] == ' ') ? '_' : tmp[i];
1113 				fprintf (fh, "%s\t", tmp);
1114 				g_free (tmp);
1115 				break;
1116 			case TT_VELO:
1117 				tmp = g_strdup (velo_get_dict_name ());
1118 				for (i=0; tmp[i]; i++)
1119 					tmp[i] = (tmp[i] == ' ') ? '_' : tmp[i];
1120 				fprintf (fh, "%s\t", tmp);
1121 				g_free (tmp);
1122 				break;
1123 			case TT_FLUID:
1124 				tmp = g_strdup (fluid_get_paragraph_name ());
1125 				for (i=0; tmp[i]; i++)
1126 					tmp[i] = (tmp[i] == ' ') ? '_' : tmp[i];
1127 				fprintf (fh, "%s\t", tmp);
1128 				g_free (tmp);
1129 				break;
1130 			}
1131 
1132 			if (tutor.type == TT_BASIC)
1133 			{
1134 				tmp = g_strdup (keyb_get_name ());
1135 				for (i=0; tmp[i]; i++)
1136 					tmp[i] = (tmp[i] == ' ') ? '_' : tmp[i];
1137 				fprintf (fh, "%s\n", tmp );
1138 				g_free (tmp);
1139 			}
1140 			else
1141 				fprintf (fh, "%s\n", trans_get_current_language ());
1142 
1143 			fclose (fh);
1144 		}
1145 		else
1146 			g_message ("not able to log on this file:\n %s", tmp_name);
1147 		g_free (tmp_name);
1148 
1149 		/* Log the touch times deviation results of the last session
1150 		 */
1151 		tmp_name = g_strconcat (main_path_stats (), G_DIR_SEPARATOR_S, "deviation_",
1152 				tutor_get_type_name (), ".txt", NULL);
1153 		if ((fh = (FILE *) g_fopen (tmp_name, "w")))
1154 		{
1155 			g_message ("writing touch timing deviation results at:\n %s", tmp_name);
1156 			fprintf (fh, "i\tdt(i)\t1/sqrt(dt(i))\tAver: %g\tStd: %g\tFluidity: %g\n", average, standard_deviation, fluidness);
1157 			for (i = 1; i < tutor.ttidx; i++)
1158 				fprintf (fh, "%i\t%g\t%g\n", i, tutor.touch_time[i],
1159 						1.0 / sqrt(tutor.touch_time[i] > 0 ? tutor.touch_time[i] : 1.0e-9));
1160 			fclose (fh);
1161 		}
1162 		else
1163 			g_message ("not able to log on this file:\n %s", tmp_name);
1164 		g_free (tmp_name);
1165 
1166 		/* Fluidity specific results
1167 		 */
1168 		if (tutor.type == TT_FLUID)
1169 		{
1170 			/* Add results to Top 10
1171 			 */
1172 			tmp_name = main_preferences_get_string ("interface", "language");
1173 			stat.lang[0] = ((tmp_name[0] == 'C') ? 'e' : tmp_name[0]);
1174 			stat.lang[1] = ((tmp_name[0] == 'C') ? 'n' : tmp_name[1]);
1175 			stat.genv = (UNIX_OK ? 'x' : 'w');
1176 			stat.when = time (NULL);
1177 			stat.nchars = tutor.n_touchs;
1178 			stat.accur = accuracy;
1179 			stat.velo = velocity;
1180 			stat.fluid = fluidness;
1181 			stat.score = top10_calc_score (&stat);
1182 
1183 			g_free (tmp_name);
1184 			tmp = main_preferences_get_string ("tutor", "keyboard");
1185 			tmp_name = g_strdup_printf ("%s [%s]", g_get_real_name (), tmp);
1186 			g_free (tmp);
1187 			stat.name_len = strlen (tmp_name);
1188 			if (stat.name_len > MAX_NAME_LEN)
1189 				stat.name_len = MAX_NAME_LEN;
1190 			strncpy (stat.name, tmp_name, stat.name_len + 1);
1191 			g_free (tmp_name);
1192 
1193 			top10_read_stats (LOCAL, -1);
1194 			if (tutor_char_distribution_approved ())
1195 			{
1196 				if (top10_validate_stat (&stat))
1197 				{
1198 					if (top10_compare_insert_stat (&stat, LOCAL))
1199 					{
1200 						contest_ps = g_strdup (_("ps.: you have entered the Top 10 list, great!"));
1201 						top10_write_stats (LOCAL, -1);
1202 						if (main_preferences_get_boolean ("game", "autopublish"))
1203 						{
1204 							top10_show_stats (LOCAL);
1205 							top10_show_stats (GLOBAL);
1206 							top10_global_publish (NULL);
1207 						}
1208 					}
1209 				}
1210 				else
1211 					contest_ps = g_strdup (":-P");
1212 			}
1213 			else
1214 				contest_ps = g_strdup (_("ps.: the text you just typed doesn't seem to be similar"
1215 						   " to ordinary texts in the language currently selected:"
1216 						   " we can't account for it in the 'Top 10' contest."));
1217 
1218 			/* Anyway, log also the scoring
1219 			 */
1220 			tmp_name = g_build_filename (main_path_stats (), "scores_fluid.txt", NULL);
1221 			assert_user_dir ();
1222 			if (!g_file_test (tmp_name, G_FILE_TEST_IS_REGULAR))
1223 			{
1224 				fh = (FILE *) g_fopen (tmp_name, "w");
1225 				fprintf (fh, "Score\tDate\tTime\t# chars\tLang\n");
1226 			}
1227 			else
1228 				fh = (FILE *) g_fopen (tmp_name, "a");
1229 			if (fh)
1230 			{
1231 				ltime = localtime (&stat.when);
1232 				fprintf (fh,
1233 					 "%3.3f\t%i-%2.2i-%2.2i\t%2.2i:%2.2i\t%i\t%s\n",
1234 					 stat.score, (ltime->tm_year) + 1900, (ltime->tm_mon) + 1,
1235 					 (ltime->tm_mday), (ltime->tm_hour), (ltime->tm_min),
1236 					 stat.nchars, trans_get_current_language ());
1237 				fclose (fh);
1238 			}
1239 			else
1240 				g_message ("not able to log on this file:\n %s", tmp_name);
1241 			g_free (tmp_name);
1242 		}
1243 
1244 		/* Coming back to the right locale
1245 		 */
1246 		if (tmp_locale != NULL)
1247 		{
1248 			setlocale (LC_NUMERIC, tmp_locale);
1249 			g_free (tmp_locale);
1250 		}
1251 	}
1252 
1253 	/* Print statistics
1254 	 */
1255 	wg = get_wg ("text_tutor");
1256 	buf = gtk_text_view_get_buffer (GTK_TEXT_VIEW (wg));
1257 
1258 	/* Begin the accuracy */
1259 	tmp_str = g_strconcat ("\n", _("STATISTICS"), "\n",
1260 			       _("Elapsed time:"), " %i ",
1261 			       dngettext (PACKAGE, "minute and", "minutes and", minutes),
1262 			       " %i ", dngettext (PACKAGE, "second", "seconds", seconds),
1263 			       "\n", _("Error ratio:"), " %i/%i\n", _("Accuracy:"), " ", NULL);
1264 
1265 	tmp_str2 = g_strdup_printf (tmp_str, minutes, seconds, tutor.n_errors, tutor.n_touchs);
1266 	g_free (tmp_str);
1267 	gtk_text_buffer_insert_at_cursor (buf, tmp_str2, strlen (tmp_str2));
1268 
1269 	/* Paint the accuracy */
1270 	g_free (tmp_str2);
1271 	tmp_str2 = g_strdup_printf ("%.1f%%", accuracy);
1272 	gtk_text_buffer_insert_at_cursor (buf, tmp_str2, strlen (tmp_str2));
1273 
1274 	gtk_text_buffer_get_end_iter (buf, &start);
1275 	gtk_text_buffer_get_end_iter (buf, &end);
1276 	gtk_text_iter_backward_cursor_positions (&start, strlen (tmp_str2));
1277 	if (accuracy > tutor_goal_accuracy ())
1278 		gtk_text_buffer_apply_tag_by_name (buf, "char_correct", &start, &end);
1279 	else
1280 		gtk_text_buffer_apply_tag_by_name (buf, "char_wrong", &start, &end);
1281 
1282 	// Finish the accuracy
1283 	g_free (tmp_str2);
1284 	tmp_str2 = g_strdup_printf ("\t\t%s %.0f%%\n", _("Goal:"), tutor_goal_accuracy ());
1285 	gtk_text_buffer_insert_at_cursor (buf, tmp_str2, strlen (tmp_str2));
1286 
1287 	if (tutor.type == TT_VELO || tutor.type == TT_FLUID)
1288 	{
1289 		// Begin the CPS
1290 		g_free (tmp_str2);
1291 		tmp_str2 = g_strdup_printf ("%s ", _("Characters per second:"));
1292 		gtk_text_buffer_insert_at_cursor (buf, tmp_str2, strlen (tmp_str2));
1293 
1294 		// Paint the CPS
1295 		g_free (tmp_str2);
1296 		tmp_str2 = g_strdup_printf ("%.2f", velocity / 12);
1297 		gtk_text_buffer_insert_at_cursor (buf, tmp_str2, strlen (tmp_str2));
1298 
1299 		gtk_text_buffer_get_end_iter (buf, &start);
1300 		gtk_text_buffer_get_end_iter (buf, &end);
1301 		gtk_text_iter_backward_cursor_positions (&start, strlen (tmp_str2));
1302 		if (velocity > tutor_goal_speed ())
1303 			gtk_text_buffer_apply_tag_by_name (buf, "char_correct", &start, &end);
1304 		else
1305 			gtk_text_buffer_apply_tag_by_name (buf, "char_wrong", &start, &end);
1306 
1307 		// Finish the CPS
1308 		g_free (tmp_str2);
1309 		tmp_str2 = g_strdup_printf ("\t\t%s %.1f %s\n", _("Goal:"), tutor_goal_speed () / 12, _("(CPS)"));
1310 		gtk_text_buffer_insert_at_cursor (buf, tmp_str2, strlen (tmp_str2));
1311 
1312 		// Begin the WPM
1313 		g_free (tmp_str2);
1314 		tmp_str2 = g_strdup_printf ("%s ", _("Words per minute:"));
1315 		gtk_text_buffer_insert_at_cursor (buf, tmp_str2, strlen (tmp_str2));
1316 
1317 		// Paint the WPM
1318 		g_free (tmp_str2);
1319 		tmp_str2 = g_strdup_printf ("%.1f", velocity);
1320 		gtk_text_buffer_insert_at_cursor (buf, tmp_str2, strlen (tmp_str2));
1321 
1322 		gtk_text_buffer_get_end_iter (buf, &start);
1323 		gtk_text_buffer_get_end_iter (buf, &end);
1324 		gtk_text_iter_backward_cursor_positions (&start, strlen (tmp_str2));
1325 		if (velocity > tutor_goal_speed ())
1326 			gtk_text_buffer_apply_tag_by_name (buf, "char_correct", &start, &end);
1327 		else
1328 			gtk_text_buffer_apply_tag_by_name (buf, "char_wrong", &start, &end);
1329 
1330 		// Finish the WPM
1331 		g_free (tmp_str2);
1332 		tmp_str2 = g_strdup_printf ("\t\t%s %.0f %s\n", _("Goal:"), tutor_goal_speed (), _("(WPM)"));
1333 		gtk_text_buffer_insert_at_cursor (buf, tmp_str2, strlen (tmp_str2));
1334 
1335 		if (tutor.type == TT_FLUID)
1336 		{
1337 			// Begin the fluidity
1338 			g_free (tmp_str2);
1339 			tmp_str2 = g_strdup_printf ("%s ", _("Fluidness:"));
1340 			gtk_text_buffer_insert_at_cursor (buf, tmp_str2, strlen (tmp_str2));
1341 
1342 			// Paint the fluidity
1343 			g_free (tmp_str2);
1344 			tmp_str2 = g_strdup_printf ("%.1f%%", fluidness);
1345 			gtk_text_buffer_insert_at_cursor (buf, tmp_str2, strlen (tmp_str2));
1346 
1347 			gtk_text_buffer_get_end_iter (buf, &start);
1348 			gtk_text_buffer_get_end_iter (buf, &end);
1349 			gtk_text_iter_backward_cursor_positions (&start, strlen (tmp_str2));
1350 			if (fluidness > tutor_goal_fluidity ())
1351 				gtk_text_buffer_apply_tag_by_name (buf, "char_correct", &start, &end);
1352 			else
1353 				gtk_text_buffer_apply_tag_by_name (buf, "char_wrong", &start, &end);
1354 
1355 			// Finish the fluidity and scores
1356 			g_free (tmp_str2);
1357 			tmp_str2 = g_strdup_printf ("\t\t%s %.0f%%\n%s: %f\n",
1358 				       	_("Goal:"), tutor_goal_fluidity (),
1359 				       	_("Score"), stat.score);
1360 			gtk_text_buffer_insert_at_cursor (buf, tmp_str2, strlen (tmp_str2));
1361 		}
1362 	}
1363 
1364 	// Begin the comments
1365 	g_free (tmp_str2);
1366 	tmp_str2 = g_strdup_printf ("\n%s\n", _("Comments:"));
1367 	gtk_text_buffer_insert_at_cursor (buf, tmp_str2, strlen (tmp_str2));
1368 
1369 	switch (tutor.type)
1370 	{
1371 	case TT_BASIC:
1372 		basic_comment (accuracy);
1373 		break;
1374 	case TT_ADAPT:
1375 		adapt_comment (accuracy);
1376 		break;
1377 	case TT_VELO:
1378 		velo_comment (accuracy, velocity);
1379 		break;
1380 	case TT_FLUID:
1381 		fluid_comment (accuracy, velocity, fluidness);
1382 		if (contest_ps != NULL)
1383 		{
1384 			gtk_text_buffer_insert_at_cursor (buf, "\n", 1);
1385 			gtk_text_buffer_insert_at_cursor (buf, contest_ps, strlen (contest_ps));
1386 			g_free (contest_ps);
1387 		}
1388 		break;
1389 	}
1390 	g_free (tmp_str2);
1391 
1392 	/* This is needed to repaint the final comments with normal black foreground. */
1393 	gtk_text_buffer_get_bounds (buf, &start, &end);
1394 	gtk_text_buffer_apply_tag_by_name (buf, "char_keep_wrap", &start, &end);
1395 
1396 	gtk_text_view_scroll_mark_onscreen (GTK_TEXT_VIEW (wg), gtk_text_buffer_get_insert (buf));
1397 }
1398 
1399 /**********************************************************************
1400  * Ensure the user is not trying to type with weird texts in the fluidness contest
1401  */
1402 #define DECEIVENESS_LIMIT 0.205
1403 gboolean
tutor_char_distribution_approved()1404 tutor_char_distribution_approved ()
1405 {
1406 	guint i, j;
1407 	gfloat deceiveness;
1408 	gchar *tmp_code;
1409 	gchar *tmp_name;
1410 
1411 	struct MODEL
1412 	{
1413 		gchar *text;
1414 		Char_Distribution dist;
1415 	} model;
1416 
1417 	struct EXAM
1418 	{
1419 		gchar *text;
1420 		Char_Distribution dist;
1421 	} exam;
1422 
1423 	GtkWidget *wg;
1424 	GtkTextBuffer *buf;
1425 	GtkTextIter start;
1426 	GtkTextIter end;
1427 
1428 	/* Get model text
1429 	 */
1430 	tmp_code = main_preferences_get_string ("interface", "language");
1431 	tmp_name = g_strconcat (main_path_data (), G_DIR_SEPARATOR_S, tmp_code, ".paragraphs", NULL);
1432 	g_free (tmp_code);
1433 	if (!g_file_get_contents (tmp_name, &model.text, NULL, NULL))
1434 	{
1435 		g_free (tmp_name);
1436 		tmp_name = trans_lang_get_similar_file_name (".paragraphs");
1437 		if (!g_file_get_contents (tmp_name, &model.text, NULL, NULL))
1438 		{
1439 			g_message ("Can't read file:\n %s\n So, not logging your score.", tmp_name);
1440 			g_free (tmp_name);
1441 			return FALSE;
1442 		}
1443 	}
1444 
1445 	/* Get text under examination
1446 	 */
1447 	wg = get_wg ("text_tutor");
1448 	buf = gtk_text_view_get_buffer (GTK_TEXT_VIEW (wg));
1449 	gtk_text_buffer_get_bounds (buf, &start, &end);
1450 	exam.text = gtk_text_buffer_get_text (buf, &start, &end, FALSE);
1451 
1452 	/* Get char distributions
1453 	 */
1454 	tutor_char_distribution_count (model.text, &model.dist);
1455 	tutor_char_distribution_count (exam.text, &exam.dist);
1456 
1457 	/* Compare both distributions
1458 	 */
1459 	deceiveness = 0;
1460 	for (i = 0; i < 9 && deceiveness < 1.0e+6; i++)
1461 	{
1462 		for (j = 0; j < exam.dist.size; j++)
1463 			if (model.dist.ch[i].letter == exam.dist.ch[j].letter)
1464 			{
1465 				deceiveness +=
1466 					powf ((exam.dist.ch[j].freq - model.dist.ch[i].freq), 2);
1467 				break;
1468 			}
1469 		if (j == exam.dist.size)
1470 		{
1471 			deceiveness += 1.0e+7;
1472 			break;
1473 		}
1474 	}
1475 	deceiveness = sqrtf (deceiveness / 9);
1476 
1477 	g_print ("Corpus file: %s\n", tmp_name);
1478 	if (deceiveness < DECEIVENESS_LIMIT)
1479 		g_print ("\tDeviation: %.3f. OK, it is less than %.3f.\n", deceiveness, DECEIVENESS_LIMIT);
1480 	else
1481 		g_print ("\tDeviation: %.3f! It should be less than %.3f.\n", deceiveness, DECEIVENESS_LIMIT);
1482 
1483 	g_free (tmp_name);
1484 	g_free (model.text);
1485 	g_free (exam.text);
1486 	return (deceiveness < DECEIVENESS_LIMIT);
1487 }
1488 
1489 /**********************************************************************
1490  * Count relative frequency of letters in text
1491  */
1492 void
tutor_char_distribution_count(gchar * text,Char_Distribution * dist)1493 tutor_char_distribution_count (gchar * text, Char_Distribution * dist)
1494 {
1495 	gchar *pt;
1496 	gunichar ch;
1497 	gsize i, j;
1498 
1499 	pt = text;
1500 
1501 	dist->size = 0;
1502 	dist->total = 0;
1503 	while ((ch = g_utf8_get_char (pt)) != L'\0')
1504 	{
1505 		/* Only count letters
1506 		 */
1507 		if (!g_unichar_isalpha (ch))
1508 		{
1509 			pt = g_utf8_next_char (pt);
1510 			continue;
1511 		}
1512 		ch = g_unichar_tolower (ch);
1513 
1514 		/* Verify if ch was already counted
1515 		 */
1516 		for (i = 0; i < dist->size; i++)
1517 		{
1518 			if (ch == dist->ch[i].letter)
1519 			{
1520 				dist->ch[i].count++;
1521 				dist->total++;
1522 				break;
1523 			}
1524 		}
1525 
1526 		/* If ch was not counted yet, start to do it
1527 		 */
1528 		if (i == dist->size && i < MAX_ALPHABET_LEN)
1529 		{
1530 			dist->ch[dist->size].letter = ch;
1531 			dist->ch[dist->size].count = 1;
1532 			dist->total++;
1533 			dist->size++;
1534 		}
1535 
1536 		pt = g_utf8_next_char (pt);
1537 	}
1538 
1539 	/* Sort the list
1540 	 */
1541 	for (i = 1; i < dist->size; i++)
1542 	{
1543 		gunichar aletter;
1544 		guint acount;
1545 
1546 		if (dist->ch[i].count > dist->ch[i - 1].count)
1547 			for (j = i; j > 0; j--)
1548 			{
1549 				if (dist->ch[j].count <= dist->ch[j - 1].count)
1550 					break;
1551 
1552 				aletter = dist->ch[j - 1].letter;
1553 				dist->ch[j - 1].letter = dist->ch[j].letter;
1554 				dist->ch[j].letter = aletter;
1555 
1556 				acount = dist->ch[j - 1].count;
1557 				dist->ch[j - 1].count = dist->ch[j].count;
1558 				dist->ch[j].count = acount;
1559 			}
1560 	}
1561 
1562 	/* Write the relative frequency
1563 	 */
1564 	for (i = 0; i < dist->size; i++)
1565 		dist->ch[i].freq = ((gfloat) dist->ch[i].count) / ((gfloat) dist->ch[0].count);
1566 
1567 	/*
1568 	   for (i = 0; i < dist->size; i++)
1569 	   g_message ("Char: %x, count: %u, freq:%g", dist->ch[i].letter, dist->ch[i].count, dist->ch[i].freq);
1570 	   g_message ("Total: %u  / Size: %u ------------------------------", dist->total, dist->size);
1571 	 */
1572 }
1573 
1574 /**********************************************************************
1575  * Formats and draws one paragraph at the tutor window
1576  */
1577 void
tutor_draw_paragraph(gchar * utf8_text)1578 tutor_draw_paragraph (gchar * utf8_text)
1579 {
1580 	static gchar *tmp1 = NULL;
1581 	static gchar *tmp2 = NULL;
1582 	gchar *ptr;
1583 	GtkTextBuffer *buf;
1584 
1585 	//g_free (tmp1);
1586 	//g_free (tmp2);
1587 
1588 	if (g_utf8_strrchr (utf8_text, -1, L'\n') == NULL)
1589 	{
1590 		g_message ("paragraph not terminated by carriage return: adding one.");
1591 		tmp1 = g_strconcat (utf8_text, "\n", NULL);
1592 	}
1593 	else
1594 		tmp1 = g_strdup (utf8_text);
1595 
1596 	ptr = g_utf8_strrchr (tmp1, -1, L'\n');
1597 	if (ptr)
1598 		*ptr = '\0';
1599 	else
1600 		g_error ("draw_paragraph () ==> string error");
1601 
1602 	tmp2 = g_strconcat (tmp1, keyb_get_utf8_paragraph_symbol (), "\n", NULL);
1603 	g_free (tmp1);
1604 
1605 	buf = gtk_text_view_get_buffer (GTK_TEXT_VIEW (get_wg ("text_tutor")));
1606 	gtk_text_buffer_insert_at_cursor (buf, tmp2, -1);
1607 	g_free (tmp2);
1608 }
1609 
1610 /**********************************************************************
1611  * Load the list of files to include in the set of "other exercises"
1612  */
1613 void
tutor_load_list_other(gchar * file_name_end,GtkListStore * list)1614 tutor_load_list_other (gchar * file_name_end, GtkListStore * list)
1615 {
1616 	gchar *tmp_str;
1617 	gchar *dentry;
1618 	GDir *dir;
1619 	GtkTreeIter iter;
1620 	static gchar *defstr = NULL;
1621 
1622 	if (defstr == NULL)
1623 		defstr = g_strdup (OTHER_DEFAULT);
1624 
1625 	gtk_list_store_clear (list);
1626 	gtk_list_store_append (list, &iter);
1627 	gtk_list_store_set (list, &iter, 0, defstr, -1);
1628 
1629 	assert_user_dir ();
1630 	dir = g_dir_open (main_path_user (), 0, NULL);
1631 	while ((dentry = g_strdup (g_dir_read_name (dir))) != NULL)
1632 	{
1633 		if (strlen (dentry) < 5)
1634 		{
1635 			g_free (dentry);
1636 			continue;
1637 		}
1638 		if (!(tmp_str = strrchr (dentry, '.')))
1639 		{
1640 			g_free (dentry);
1641 			continue;
1642 		}
1643 		if (! g_str_equal (file_name_end, tmp_str))
1644 		{
1645 			g_free (dentry);
1646 			continue;
1647 		}
1648 
1649 		*(strrchr (dentry, '.')) = '\0';
1650 		gtk_list_store_append (list, &iter);
1651 		gtk_list_store_set (list, &iter, 0, dentry, -1);
1652 		g_free (dentry);
1653 	}
1654 	g_dir_close (dir);
1655 
1656 	gtk_widget_set_sensitive (get_wg ("button_other_remove"), FALSE);
1657 	gtk_widget_set_sensitive (get_wg ("label_other_rename"), FALSE);
1658 	gtk_widget_set_sensitive (get_wg ("entry_other_rename"), FALSE);
1659 	gtk_widget_set_sensitive (get_wg ("button_other_apply"), FALSE);
1660 }
1661 
1662 void
tutor_other_rename(const gchar * new_tx,const gchar * old_tx)1663 tutor_other_rename (const gchar *new_tx, const gchar *old_tx)
1664 {
1665 	if (! g_str_equal (new_tx, old_tx) &&
1666 	    ! g_str_equal (new_tx, OTHER_DEFAULT) &&
1667 	    ! g_str_equal (new_tx, "") &&
1668 	    g_strrstr (old_tx, "*") == NULL )
1669 	{
1670 		gchar *old_name;
1671 		gchar *new_name;
1672 		gchar *old_file;
1673 		gchar *new_file;
1674 
1675 		if (tutor.type == TT_VELO)
1676 		{
1677 			old_name = g_strconcat (old_tx, ".words", NULL);
1678 			new_name = g_strconcat (new_tx, ".words", NULL);
1679 		}
1680 		else
1681 		{
1682 			old_name = g_strconcat (old_tx, ".paragraphs", NULL);
1683 			new_name = g_strconcat (new_tx, ".paragraphs", NULL);
1684 		}
1685 		old_file = g_build_filename (main_path_user (), old_name, NULL);
1686 		new_file = g_build_filename (main_path_user (), new_name, NULL);
1687 
1688 		if (g_file_test (new_file, G_FILE_TEST_IS_REGULAR))
1689 		{
1690 			g_message ("File already exists, not renaming.\n\t%s\n", new_file);
1691 			gdk_display_beep (gdk_display_get_default ());
1692 		}
1693 		else
1694 		{
1695 			g_printf ("Renaming from:\n\t%s\nTo:\n\t%s\n", old_file, new_file);
1696 			if (g_rename (old_file, new_file))
1697 			{
1698 				g_printf ("Fail: %s\n", strerror (errno));
1699 			}
1700 			else
1701 				g_printf ("Success!\n");
1702 		}
1703 		g_free (old_name);
1704 		g_free (new_name);
1705 		g_free (old_file);
1706 		g_free (new_file);
1707 	}
1708 }
1709 
1710 /**********************************************************************
1711  * Put 'mesg' in the message entry line of the shared tutor window
1712  */
1713 void
tutor_message(gchar * mesg)1714 tutor_message (gchar * mesg)
1715 {
1716 	gint pos = 0;
1717 	GtkWidget *wg;
1718 
1719 	if (mesg == NULL)
1720 	{
1721 		g_message ("tutor_message() --> not showing NULL message!");
1722 		return;
1723 	}
1724 
1725 	wg = get_wg ("entry_mesg");
1726 	callbacks_shield_set (TRUE);
1727 	gtk_editable_delete_text (GTK_EDITABLE (wg), 0, -1);
1728 	gtk_editable_insert_text (GTK_EDITABLE (wg), g_strdup (mesg), strlen (mesg), &pos);
1729 	gtk_editable_set_position (GTK_EDITABLE (wg), -1);
1730 	callbacks_shield_set (FALSE);
1731 }
1732 
1733 /**********************************************************************
1734  * Beeps (or not) at the user, in the tutor window
1735  */
1736 void
tutor_beep()1737 tutor_beep ()
1738 {
1739 	if (main_preferences_get_boolean ("tutor", "tutor_beep"))
1740 		gdk_display_beep (gdk_display_get_default ());
1741 }
1742 
1743 /**********************************************************************
1744  * Speak some phrase
1745  */
1746 void
tutor_speak_string(gchar * string,gboolean wait)1747 tutor_speak_string (gchar *string, gboolean wait)
1748 {
1749 	gchar *tmp_code;
1750 	gchar *command;
1751 	static gboolean espeak_OK = TRUE;
1752 	static GtkWidget *wg = NULL;
1753 
1754 	if (wg == NULL)
1755 		wg = get_wg ("checkbutton_speech");
1756 
1757 	if (espeak_OK == FALSE)
1758 		return;
1759 	if (!gtk_widget_get_visible (wg) )
1760 		return;
1761 	if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (wg)) )
1762 		return;
1763 
1764 	/* Translators: your language code (first 2 letters of your po-file)*/
1765 	tmp_code = g_strdup (_("en"));
1766 	tmp_code[2] = '\0';
1767 	if (wait)
1768 	{
1769 		command = g_strdup_printf ("espeak -v%s -k1 \"%s\"", tmp_code, string);
1770 #ifdef G_OS_UNIX
1771 		espeak_OK = g_spawn_command_line_sync (command, NULL, NULL, NULL, NULL);
1772 #else
1773 		espeak_OK = ! system (command);
1774 #endif
1775 	}
1776 	else
1777 	{
1778 #ifdef G_OS_UNIX
1779 		if (g_utf8_strlen (string, -1) == 1 || tutor.type == TT_VELO)
1780 			command = g_strdup_printf ("espeak -v%s -k1 --punct '%s'", tmp_code, string);
1781 		else
1782 			command = g_strdup_printf ("espeak -v%s -k1 \"%s\"", tmp_code, string);
1783 		espeak_OK = g_spawn_command_line_async (command, NULL);
1784 #else
1785 		if (g_utf8_strlen (string, -1) == 1 || tutor.type == TT_VELO)
1786 			command = g_strdup_printf ("espeak -v%s -k1 --punct \"%s\"", tmp_code, string);
1787 		else
1788 			command = g_strdup_printf ("espeak -v%s -k1 \"%s\"", tmp_code, string);
1789 		espeak_OK = ! system (command);
1790 #endif
1791 	}
1792 	if (espeak_OK == FALSE)
1793 		g_message ("Espeak not installed, so we'll say nothing:\n %s", command);
1794 	g_free (tmp_code);
1795 	g_free (command);
1796 }
1797 
1798 /**********************************************************************
1799  * Control delayed tips for the finger to be used
1800  */
1801 gboolean
tutor_delayed_finger_tip(gpointer unich)1802 tutor_delayed_finger_tip (gpointer unich)
1803 {
1804 	gchar *finger;
1805 	gunichar *uch = (gunichar*) unich;
1806 	static gint counter = 0;
1807 
1808 	if (unich == NULL)
1809 	{
1810 		counter++;
1811 		return FALSE;
1812 	}
1813 	counter--;
1814 
1815 	if (counter > 0)
1816 		return FALSE;
1817 
1818 	finger = hints_finger_name_from_char (*uch);
1819 	tutor_speak_string (finger, TRUE);
1820 	g_free (finger);
1821 
1822 	return FALSE;
1823 }
1824 
1825 /**********************************************************************
1826  * Speak the current character to be typed
1827  */
1828 void
tutor_speak_char()1829 tutor_speak_char ()
1830 {
1831 	gchar ut8[100];
1832 	static gunichar uch;
1833 
1834 	if (tutor.type == TT_BASIC)
1835 	{
1836 		g_timeout_add (3000, (GSourceFunc) tutor_delayed_finger_tip, (gpointer) &uch);
1837 		tutor_delayed_finger_tip (NULL);
1838 	}
1839 
1840 	uch = cursor_get_char ();
1841 	switch (uch)
1842 	{
1843 	case L' ':
1844 		strcpy (ut8, _("space"));
1845 		break;
1846 	case L'y':
1847 	case L'Y':
1848 		/* Translators: the name of letter Y */
1849 		strcpy (ut8, _("wye"));
1850 		break;
1851 	case UPSYM:
1852 		/* Translators: the name of the Return key */
1853 		strcpy (ut8, _("enter"));
1854 		break;
1855 	case L'%':
1856 		strcpy (ut8, "%");
1857 		break;
1858 	case L'\'':
1859 		strcpy (ut8, _("apostrophe"));
1860 		break;
1861 	case L'\"':
1862 		/* Translators: double quote symbol: " */
1863 		strcpy (ut8, _("quote"));
1864 		break;
1865 	case L'&':
1866 		/* Translators: ampersand symbol: & */
1867 		strcpy (ut8, _("ampersand"));
1868 		break;
1869 	default:
1870 		ut8[g_unichar_to_utf8 (uch, ut8)] = '\0';
1871 	}
1872 
1873 	tutor_speak_string (ut8, FALSE);
1874 
1875 }
1876 
1877 /**********************************************************************
1878  * Speak the next word to be typed
1879  */
1880 void
tutor_speak_word()1881 tutor_speak_word ()
1882 {
1883 	gunichar uch[100];
1884 	gchar *ut8;
1885 	gint i;
1886 
1887 	if (cursor_advance (-1) != -1)
1888 		uch[0] = L' ';
1889 	else
1890 	{
1891 		uch[0] = cursor_get_char ();
1892 		cursor_advance (1);
1893 	}
1894 	if (uch[0] == L' ' || uch[0] == UPSYM || uch[0] == L'\n' || uch[0] == L'\r')
1895 	{
1896 		for (i = 0; i < 100; i++)
1897 		{
1898 			uch[i] = cursor_get_char ();
1899 			if (uch[i] == L' ' || uch[i] == UPSYM || uch[i] == L'\n' || uch[i] == L'\r')
1900 				break;
1901 			cursor_advance (1);
1902 		}
1903 		cursor_advance (-i);
1904 	}
1905 	else
1906 		return;
1907 
1908 	ut8 = g_ucs4_to_utf8 (uch, i, NULL, NULL, NULL);
1909 	if (ut8)
1910 		tutor_speak_string (ut8, FALSE);
1911 	g_free (ut8);
1912 }
1913