4 cellwriter -- a character recognition input method
5 Copyright (C) 2007 Michael Levin <risujin@risujin.org>
7 This program is free software; you can redistribute it and/or
8 modify it under the terms of the GNU General Public License
9 as published by the Free Software Foundation; either version 2
10 of the License, or (at your option) any later version.
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
17 You should have received a copy of the GNU General Public License
18 along with this program; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
31 #include <libgnome/libgnome.h>
35 extern int strength_sum;
37 void recognize_init(void);
38 void recognize_sync(void);
39 void samples_write(void);
40 void sample_read(void);
41 void update_enabled_samples(void);
42 int samples_loaded(void);
45 extern int training, corrections, rewrites, characters, inputs;
47 void cell_widget_cleanup(void);
50 void options_sync(void);
53 extern int key_recycles, key_overwrites, key_disable_overwrite;
56 void bad_keycodes_write(void);
57 void bad_keycodes_read(void);
60 Variable argument parsing
63 char *nvav(int *plen, const char *fmt, va_list va)
65 static char buffer[2][16000];
70 len = g_vsnprintf(buffer[which], sizeof(buffer[which]), fmt, va);
76 char *nva(int *plen, const char *fmt, ...)
82 string = nvav(plen, fmt, va);
87 char *va(const char *fmt, ...)
93 string = nvav(NULL, fmt, va);
102 static int check_color_range(int value)
111 void scale_gdk_color(const GdkColor *base, GdkColor *out, double value)
113 out->red = check_color_range(base->red * value);
114 out->green = check_color_range(base->green * value);
115 out->blue = check_color_range(base->blue * value);
118 void gdk_color_to_hsl(const GdkColor *src,
119 double *hue, double *sat, double *lit)
120 /* Source: http://en.wikipedia.org/wiki/HSV_color_space
121 #Conversion_from_RGB_to_HSL_or_HSV */
123 double max = src->red, min = src->red;
125 /* Find largest and smallest channel */
126 if (src->green > max)
128 if (src->green < min)
135 /* Hue depends on max/min */
138 else if (max == src->red) {
139 *hue = (src->green - src->blue) / (max - min) / 6.;
142 } else if (max == src->green)
143 *hue = ((src->blue - src->red) / (max - min) + 2.) / 6.;
144 else if (max == src->blue)
145 *hue = ((src->red - src->green) / (max - min) + 4.) / 6.;
148 *lit = (max + min) / 2 / 65535;
150 /* Saturation depends on lightness */
153 else if (*lit <= 0.5)
154 *sat = (max - min) / (max + min);
156 *sat = (max - min) / (65535 * 2 - (max + min));
159 void hsl_to_gdk_color(GdkColor *src, double hue, double sat, double lit)
160 /* Source: http://en.wikipedia.org/wiki/HSV_color_space
161 #Conversion_from_RGB_to_HSL_or_HSV */
168 hue -= (int)hue - 1.;
180 /* Special case for gray */
182 src->red = lit * 65535;
183 src->green = lit * 65535;
184 src->blue = lit * 65535;
188 q = (lit < 0.5) ? lit * (1 + sat) : lit + sat - (lit * sat);
193 for (i = 0; i < 3; i++) {
200 else if (t[i] >= 0.5)
201 t[i] = p + ((q - p) * 6 * (2 / 3. - t[i]));
202 else if (t[i] >= 1 / 6.)
205 t[i] = p + ((q - p) * 6 * t[i]);
207 src->red = t[0] * 65535;
208 src->green = t[1] * 65535;
209 src->blue = t[2] * 65535;
212 void shade_gdk_color(const GdkColor *base, GdkColor *out, double value)
214 double hue, sat, lit;
216 gdk_color_to_hsl(base, &hue, &sat, &lit);
219 hsl_to_gdk_color(out, hue, sat, lit);
222 void highlight_gdk_color(const GdkColor *base, GdkColor *out, double value)
223 /* Shades brighter or darker depending on the luminance of the base color */
225 double lum = (0.3 * base->red + 0.59 * base->green +
226 0.11 * base->blue) / 65535;
228 value = lum < 0.5 ? 1. + value : 1. - value;
229 shade_gdk_color(base, out, value);
236 /* Profile format version */
237 #define PROFILE_VERSION 0
239 int profile_read_only, keyboard_only = FALSE;
241 static GIOChannel *channel;
242 static char profile_buf[4096], *profile_end = NULL, profile_swap,
243 *force_profile = NULL, *profile_tmp = NULL;
244 static int force_read_only;
246 static int is_space(int ch)
248 return ch == ' ' || ch == '\t' || ch == '\r';
251 static int profile_open_channel(const char *type, const char *path)
252 /* Tries to open a profile channel, returns TRUE if it succeeds */
254 GError *error = NULL;
256 if (!g_file_test(path, G_FILE_TEST_IS_REGULAR) &&
257 g_file_test(path, G_FILE_TEST_EXISTS)) {
258 g_warning("Failed to open %s profile '%s': Not a regular file",
262 channel = g_io_channel_new_file(path, profile_read_only ? "r" : "w",
266 g_warning("Failed to open %s profile '%s' for %s: %s",
267 type, path, profile_read_only ? "reading" : "writing",
273 static void create_user_dir(void)
274 /* Make sure the user directory exists */
278 path = g_build_filename(g_get_home_dir(), "." PACKAGE, NULL);
279 if (g_mkdir_with_parents(path, 0755))
280 g_warning("Failed to create user directory '%s'", path);
284 static int profile_open_read(void)
285 /* Open the profile file for reading. Returns TRUE if the profile was opened
290 profile_read_only = TRUE;
292 /* Try opening a command-line specified profile first */
294 profile_open_channel("command-line specified", force_profile))
297 /* Open user's profile */
298 path = g_build_filename(g_get_home_dir(), "." PACKAGE, "profile", NULL);
299 if (profile_open_channel("user's", path)) {
305 /* Open system profile */
306 path = g_build_filename(PKGDATADIR, "profile", NULL);
307 if (profile_open_channel("system", path)) {
316 static int profile_open_write(void)
317 /* Open a temporary profile file for writing. Returns TRUE if the profile was
318 opened successfully. */
323 if (force_read_only) {
324 g_debug("Not saving profile, opened in read-only mode");
327 profile_read_only = FALSE;
329 /* Open a temporary file as a channel */
331 fd = g_file_open_tmp(PACKAGE ".XXXXXX", &profile_tmp, &error);
333 g_warning("Failed to open tmp file while saving "
334 "profile: %s", error->message);
337 channel = g_io_channel_unix_new(fd);
339 g_warning("Failed to create channel from temporary file");
346 static int move_file(char *from, char *to)
347 /* The standard library rename() cannot move across filesystems so we need a
348 function that can emulate that. This function will copy a file, byte-by-byte
349 but is not as safe as rename(). */
351 GError *error = NULL;
352 GIOChannel *src_channel, *dest_channel;
355 /* Open source file for reading */
356 src_channel = g_io_channel_new_file(from, "r", &error);
358 g_warning("move_file() failed to open src '%s': %s",
359 from, error->message);
363 /* Open destination file for writing */
364 dest_channel = g_io_channel_new_file(to, "w", &error);
366 g_warning("move_file() failed to open dest '%s': %s",
368 g_io_channel_unref(src_channel);
372 /* Copy data in blocks */
374 gsize bytes_read, bytes_written;
376 /* Read a block in */
377 g_io_channel_read_chars(src_channel, buffer, sizeof (buffer),
378 &bytes_read, &error);
379 if (bytes_read < 1 || error)
382 /* Write the block out */
383 g_io_channel_write_chars(dest_channel, buffer, bytes_read,
384 &bytes_written, &error);
385 if (bytes_written < bytes_read || error) {
386 g_warning("move_file() error writing to '%s': %s",
388 g_io_channel_unref(src_channel);
389 g_io_channel_unref(dest_channel);
395 g_io_channel_unref(src_channel);
396 g_io_channel_unref(dest_channel);
398 g_debug("move_file() copied '%s' to '%s'", from, to);
400 /* Should be safe to delete the old file now */
402 log_errno("move_file() failed to delete src");
407 static int profile_close(void)
408 /* Close the currently open profile and, if we just wrote the profile to a
409 temporary file, move it in place of the old profile */
415 g_io_channel_unref(channel);
417 if (!profile_tmp || profile_read_only)
420 /* For some bizarre reason we may not have managed to create the
422 if (!g_file_test(profile_tmp, G_FILE_TEST_EXISTS)) {
423 g_warning("Tmp profile '%s' does not exist", profile_tmp);
427 /* Use command-line specified profile path first then the user's
428 home directory profile */
429 path = force_profile;
431 path = g_build_filename(g_get_home_dir(),
432 "." PACKAGE, "profile", NULL);
434 if (g_file_test(path, G_FILE_TEST_EXISTS)) {
435 g_message("Replacing '%s' with '%s'", path, profile_tmp);
437 /* Don't write over non-regular files */
438 if (!g_file_test(path, G_FILE_TEST_IS_REGULAR)) {
439 g_warning("Old profile '%s' is not a regular file",
444 /* Remove the old profile */
446 log_errno("Failed to delete old profile");
451 g_message("Creating new profile '%s'", path);
453 /* Move the temporary profile file in place of the old one */
454 if (rename(profile_tmp, path)) {
455 log_errno("rename() failed to move tmp profile in place");
456 if (!move_file(profile_tmp, path))
460 if (path != force_profile)
465 g_warning("Recover tmp profile at '%s'", profile_tmp);
469 const char *profile_read(void)
470 /* Read a token from the open profile */
472 GError *error = NULL;
478 profile_end = profile_buf;
479 *profile_end = profile_swap;
483 /* Get the next token from the buffer */
484 for (; is_space(*profile_end); profile_end++);
486 for (; *profile_end && !is_space(*profile_end) && *profile_end != '\n';
489 /* If we run out of buffer space, read a new chunk */
491 unsigned int token_size;
494 /* If we are out of space and we are not on the first or
495 the last byte, then we have run out of things to read */
496 if (profile_end > profile_buf &&
497 profile_end < profile_buf + sizeof (profile_buf) - 1) {
502 /* Move what we have of the token to the start of the buffer,
503 fill the rest of the buffer with new data and start reading
504 from the beginning */
505 token_size = profile_end - token;
506 if (token_size >= sizeof (profile_buf) - 1) {
507 g_warning("Oversize token in profile");
510 memmove(profile_buf, token, token_size);
511 g_io_channel_read_chars(channel, profile_buf + token_size,
512 sizeof (profile_buf) - token_size - 1,
513 &bytes_read, &error);
515 g_warning("Read error: %s", error->message);
518 if (bytes_read < 1) {
522 profile_end = profile_buf;
523 profile_buf[token_size + bytes_read] = 0;
524 goto seek_profile_end;
527 profile_swap = *profile_end;
532 int profile_read_next(void)
533 /* Skip to the next line
534 FIXME should skip multiple blank lines */
541 if (profile_swap == '\n') {
548 int profile_write(const char *str)
549 /* Write a string to the open profile */
551 GError *error = NULL;
554 if (profile_read_only || !str)
558 g_io_channel_write_chars(channel, str, strlen(str), &bytes_written,
561 g_warning("Write error: %s", error->message);
567 int profile_sync_int(int *var)
568 /* Read or write an integer variable depending on the profile mode */
570 if (profile_read_only) {
577 if (n || (s[0] == '0' && !s[1])) {
583 return profile_write(va(" %d", *var));
587 int profile_sync_short(short *var)
588 /* Read or write a short integer variable depending on the profile mode */
592 if (profile_sync_int(&value))
594 if (!profile_read_only)
600 void version_read(void)
604 version = atoi(profile_read());
606 g_warning("Loading a profile with incompatible version %d "
607 "(expected %d)", version, PROFILE_VERSION);
611 Main and signal handling
614 #define NUM_PROFILE_CMDS (sizeof (profile_cmds) / sizeof (*profile_cmds))
616 int profile_line, log_level = 4;
618 static char *log_filename = NULL;
619 static FILE *log_file = NULL;
621 /* Profile commands table */
624 void (*read_func)(void);
625 void (*write_func)(void);
627 { "version", version_read, NULL },
628 { "window", window_sync, window_sync },
629 { "options", options_sync, options_sync },
630 { "recognize", recognize_sync, recognize_sync },
631 { "blocks", blocks_sync, blocks_sync },
632 { "bad_keycodes", bad_keycodes_read, bad_keycodes_write },
633 { "sample", sample_read, samples_write },
636 /* Command line arguments */
637 static GOptionEntry command_line_opts[] = {
638 { "log-level", 0, 0, G_OPTION_ARG_INT, &log_level,
639 "Log threshold (0=silent, 7=debug)", "4" },
640 { "log-file", 0, 0, G_OPTION_ARG_STRING, &log_filename,
641 "Log file to use instead of stdout", PACKAGE ".log" },
642 { "xid", 0, 0, G_OPTION_ARG_NONE, &window_embedded,
643 "Embed the main window (XEMBED)", NULL },
644 { "show-window", 0, 0, G_OPTION_ARG_NONE, &window_force_show,
645 "Show the main window", NULL },
646 { "hide-window", 0, 0, G_OPTION_ARG_NONE, &window_force_hide,
647 "Don't show the main window", NULL },
648 { "window-x", 0, 0, G_OPTION_ARG_INT, &window_force_x,
649 "Horizontal window position", "512" },
650 { "window-y", 0, 0, G_OPTION_ARG_INT, &window_force_y,
651 "Vertical window position", "768" },
652 { "dock-window", 0, 0, G_OPTION_ARG_INT, &window_force_docked,
653 "Docking (0=off, 1=top, 2=bottom)", "0" },
654 { "window-struts", 0, 0, G_OPTION_ARG_NONE, &window_struts,
655 "Reserve space when docking, see manpage", NULL },
656 { "keyboard-only", 0, 0, G_OPTION_ARG_NONE, &keyboard_only,
657 "Show on-screen keyboard only", NULL },
658 { "profile", 0, 0, G_OPTION_ARG_STRING, &force_profile,
659 "Full path to profile file to load", "profile" },
660 { "read-only", 0, 0, G_OPTION_ARG_NONE, &force_read_only,
661 "Do not save changes to the profile", NULL },
662 { "disable-overwrite", 0, 0, G_OPTION_ARG_NONE, &key_disable_overwrite,
663 "Do not modify the keymap", NULL },
666 { NULL, 0, 0, 0, NULL, NULL, NULL }
669 /* If any of these things happen, try to save and exit cleanly -- gdb is not
670 affected by any of these signals being caught */
671 static int catch_signals[] = {
682 if (!window_embedded && profile_open_write()) {
685 profile_write(va("version %d\n", PROFILE_VERSION));
686 for (i = 0; i < NUM_PROFILE_CMDS; i++)
687 if (profile_cmds[i].write_func)
688 profile_cmds[i].write_func();
690 g_debug("Profile saved");
700 g_debug("Cleanup already called");
704 g_message("Cleaning up");
706 /* Explicit cleanup */
707 cell_widget_cleanup();
710 if (!window_embedded)
711 single_instance_cleanup();
721 static void catch_sigterm(int sig)
722 /* Terminated by shutdown */
724 g_warning("Caught signal %d, cleaning up", sig);
729 static void hook_signals(void)
730 /* Setup signal catchers */
735 sigemptyset(&sigset);
738 signal(*ps, catch_sigterm);
739 sigaddset(&sigset, *(ps++));
741 if (sigprocmask(SIG_UNBLOCK, &sigset, NULL) == -1)
742 log_errno("Failed to set signal blocking mask");
745 void log_print(const char *format, ...)
746 /* Print to the log file or stderr */
757 va_start(va, format);
758 vfprintf(file, format, va);
762 void log_func(const gchar *domain, GLogLevelFlags level, const gchar *message)
764 const char *prefix = "", *postfix = "\n", *pmsg;
766 if (level > log_level)
769 /* Do not print empty messages */
770 for (pmsg = message; *pmsg <= ' '; pmsg++)
774 /* Format the message */
775 switch (level & G_LOG_LEVEL_MASK) {
776 case G_LOG_LEVEL_DEBUG:
779 case G_LOG_LEVEL_INFO:
780 case G_LOG_LEVEL_MESSAGE:
781 if (log_level > G_LOG_LEVEL_INFO) {
786 case G_LOG_LEVEL_WARNING:
787 if (log_level > G_LOG_LEVEL_INFO)
789 else if (log_level > G_LOG_LEVEL_WARNING)
790 prefix = "WARNING: ";
792 prefix = PACKAGE ": ";
794 case G_LOG_LEVEL_CRITICAL:
795 case G_LOG_LEVEL_ERROR:
796 if (log_level > G_LOG_LEVEL_INFO)
797 prefix = "* ERROR! ";
798 else if (log_level > G_LOG_LEVEL_WARNING)
801 prefix = PACKAGE ": ERROR! ";
807 log_print("%s[%s] %s%s", prefix, domain, message, postfix);
809 log_print("%s%s%s", prefix, message, postfix);
812 if (level & G_LOG_FLAG_FATAL || level & G_LOG_LEVEL_ERROR)
816 void trace_full(const char *file, const char *func, const char *format, ...)
821 if (LOG_LEVEL_TRACE > log_level)
823 va_start(va, format);
824 vsnprintf(buf, sizeof(buf), format, va);
826 log_print(": %s:%s() %s\n", file, func, buf);
829 void log_errno(const char *string)
831 log_func(NULL, G_LOG_LEVEL_WARNING,
832 va("%s: %s", string, strerror(errno)));
835 static void second_instance(char *str)
837 g_debug("Received '%s' from fifo", str);
838 if (str[0] == '0' || str[0] == 'H' || str[0] == 'h')
840 else if (str[0] == '1' || str[0] == 'S' || str[0] == 's')
842 else if (str[0] == 'T' || str[0] == 't')
848 if (profile_open_read()) {
850 g_message("Parsing profile");
854 token = profile_read();
856 if (profile_read_next())
860 for (i = 0; i < NUM_PROFILE_CMDS; i++)
861 if (!g_ascii_strcasecmp(profile_cmds[i].name,
863 if (profile_cmds[i].read_func)
864 profile_cmds[i].read_func();
867 if (i == NUM_PROFILE_CMDS)
868 g_warning("Unrecognized profile command '%s'",
871 } while (profile_read_next());
873 g_debug("Parsed %d commands", profile_line - 1);
877 int main(int argc, char *argv[])
885 if (!gtk_init_with_args(&argc, &argv,
886 "grid-entry handwriting input panel",
887 command_line_opts, NULL, &error))
891 log_level = 1 << log_level;
892 g_log_set_handler(NULL, -1, (GLogFunc)log_func, NULL);
894 /* Try to open the log-file
896 log_file = fopen(log_filename, "w");
898 g_warning("Failed to open log-file '%s'", log_filename);
901 /* See if the program is already running
902 g_message("Starting " PACKAGE_STRING);
904 if (!window_embedded &&
905 single_instance_init((SingleInstanceFunc)second_instance,
906 window_force_hide ? "0" : "1")) {
907 gdk_notify_startup_complete();
908 g_message(PACKAGE_NAME " already running, quitting");
913 /* Initialize GNOME for the Help button
914 gnome_program_init(PACKAGE, VERSION, LIBGNOME_MODULE,
918 /* Component initilization
919 if (key_event_init(NULL)) {
922 dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL,
925 "Xtest extension not "
927 gtk_window_set_title(GTK_WINDOW(dialog), "Initilization Error");
928 gtk_message_dialog_format_secondary_text(
929 GTK_MESSAGE_DIALOG(dialog),
930 "You
\11r Xserver does not support the Xtest extension. "
931 PACKAGE_NAME " cannot generate keystrokes without it.");
932 gtk_dialog_run(GTK_DIALOG(dialog));
933 gtk_widget_destroy(dialog);
940 /* After loading samples and block enabled/disabled information,
942 update_enabled_samples();
944 /* Ensure that if there is a crash, data is saved properly
948 /* Initialize the interface
949 g_message("Running interface");
952 /* Startup notification is sent when the first window shows but in if
953 the window was closed during last start, it won't show at all so
954 we need to do this manually
955 gdk_notify_startup_complete();
958 if (!samples_loaded() && !keyboard_only)
959 startup_splash_show();
963 /* Session statistics
964 if (characters && inputs && log_level >= G_LOG_LEVEL_DEBUG) {
965 g_message("Session statistics --");
966 g_debug("Average strength: %d%%", strength_sum / inputs);
967 g_debug("Rewrites: %d out of %d inputs (%d%%)",
968 rewrites, inputs, rewrites * 100 / inputs);
969 g_debug("Corrections: %d out of %d characters (%d%%)",
970 corrections, characters,
971 corrections * 100 / characters);
972 g_debug("KeyCodes overwrites: %d out of %d uses (%d%%)",
973 key_overwrites, key_overwrites + key_recycles,
974 key_recycles + key_overwrites ? key_overwrites * 100 /
975 (key_recycles + key_overwrites) : 0);