Initial check-in master
authoratnnn <atnnn@belvedere.(none)>
Tue, 20 Apr 2010 16:14:08 +0000 (12:14 -0400)
committeratnnn <atnnn@belvedere.(none)>
Tue, 20 Apr 2010 16:14:08 +0000 (12:14 -0400)
29 files changed:
Makefile [new file with mode: 0644]
Makefile.in [new file with mode: 0644]
configure [new file with mode: 0755]
debian/changelog [new file with mode: 0644]
debian/compat [new file with mode: 0644]
debian/control [new file with mode: 0644]
debian/copyright [new file with mode: 0644]
debian/dirs [new file with mode: 0644]
debian/him-cellwriter.postinst [new file with mode: 0644]
debian/rules [new file with mode: 0755]
default_profile [new file with mode: 0644]
src/Makefile [new file with mode: 0644]
src/averages.c [new file with mode: 0644]
src/cellwidget.c [new file with mode: 0644]
src/common.h [new file with mode: 0644]
src/config.h [new file with mode: 0644]
src/him_cellwriter.c [new file with mode: 0644]
src/keyevent.c [new file with mode: 0644]
src/keys.h [new file with mode: 0644]
src/main.c [new file with mode: 0644]
src/options.c [new file with mode: 0644]
src/preprocess.c [new file with mode: 0644]
src/recognize.c [new file with mode: 0644]
src/recognize.h [new file with mode: 0644]
src/singleinstance.c [new file with mode: 0644]
src/stroke.c [new file with mode: 0644]
src/window.c [new file with mode: 0644]
src/wordfreq.c [new file with mode: 0644]
welcome [deleted file]

diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..d0a29db
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,21 @@
+LIBS = gtk+-2.0 glib-2.0 gconf-2.0 hildon-1 hildon-input-method-ui-3.0 hildon-input-method-framework-3.0
+
+build:
+       LIBS='$(LIBS)' make -C src
+
+clean:
+       make -C src clean
+
+install: src/him_cellwriter.so default_profile
+       mkdir -p $(prefix)/lib/hildon-input-method/
+       install src/him_cellwriter.so $(prefix)/lib/hildon-input-method/
+       mkdir -p $(prefix)/share/him_cellwriter
+       install default_profile $(prefix)/share/him_cellwriter/profile
+
+configure:
+       @echo '#!/bin/sh' > configure
+       @echo 'echo Looking for $(LIBS) && echo "Found!" && echo Run make to build' >> configure
+       @echo 'pkg-config --print-errors $(LIBS)' >> configure
+       @chmod +x configure
+
+distclean:
diff --git a/Makefile.in b/Makefile.in
new file mode 100644 (file)
index 0000000..4cfa353
--- /dev/null
@@ -0,0 +1,20 @@
+
+build:
+       LIBS='$(LIBS)' make -C src
+
+clean:
+       make -C src clean
+
+install: src/him_cellwriter.so default_profile
+       mkdir -p $(prefix)/lib/hildon-input-method/
+       install src/him_cellwriter.so $(prefix)/lib/hildon-input-method/
+       mkdir -p $(prefix)/share/him_cellwriter
+       install default_profile $(prefix)/share/him_cellwriter/profile
+
+configure:
+       @echo '#!/bin/sh' > configure
+       @echo 'echo Looking for $(LIBS) && echo "Found!" && echo Run make to build' >> configure
+       @echo 'pkg-config --print-errors $(LIBS)' >> configure
+       @chmod +x configure
+
+distclean:
diff --git a/configure b/configure
new file mode 100755 (executable)
index 0000000..5e65d12
--- /dev/null
+++ b/configure
@@ -0,0 +1,13 @@
+#!/bin/sh
+LIBS="gtk+-2.0 glib-2.0 gconf-2.0 hildon-1 hildon-input-method-ui-3.0 hildon-input-method-framework-3.0"
+echo Looking for $LIBS 
+if pkg-config --print-errors $LIBS; then
+
+       echo LIBS = $LIBS > Makefile
+       cat Makefile.in >> Makefile
+
+       echo "Found: Run make to build"
+else
+       echo Missing libraries
+       exit 1
+fi
diff --git a/debian/changelog b/debian/changelog
new file mode 100644 (file)
index 0000000..9770441
--- /dev/null
@@ -0,0 +1,6 @@
+him-cellwriter (0.1-1) unstable; urgency=low
+
+  * Initial release
+
+ -- Etienne Laurin <etienne@atnnn.com>  Mon, 19 Apr 2010 23:27:51 -0400
+
diff --git a/debian/compat b/debian/compat
new file mode 100644 (file)
index 0000000..7ed6ff8
--- /dev/null
@@ -0,0 +1 @@
+5
diff --git a/debian/control b/debian/control
new file mode 100644 (file)
index 0000000..8540758
--- /dev/null
@@ -0,0 +1,13 @@
+Source: him-cellwriter
+Priority: extra
+Maintainer: Etienne Laurin <etienne@atnnn.com>
+Build-Depends: debhelper (>= 5), gcc, libhildon-im-ui-dev, libhildon1-dev
+Standards-Version: 3.7.2
+Section: user/system
+
+Package: him-cellwriter
+Section: user/system
+Architecture: any
+Depends: ${shlibs:Depends}, ${misc:Depends}
+Description: CellWriter Input Plugin
+       Handwriting recognition
diff --git a/debian/copyright b/debian/copyright
new file mode 100644 (file)
index 0000000..fe3a33a
--- /dev/null
@@ -0,0 +1,18 @@
+This package was debianized by Etienne Laurin <etienne@atnnn.com> on
+Mon, 19 Apr 2010 23:27:51 -0400.
+
+License:
+
+   This package is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License version 2 as published
+   by the Free Software Foundation.
+
+   This package is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this package; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+
diff --git a/debian/dirs b/debian/dirs
new file mode 100644 (file)
index 0000000..37f1452
--- /dev/null
@@ -0,0 +1,2 @@
+usr/share/him_cellwriter
+usr/lib/hildon-input-method
diff --git a/debian/him-cellwriter.postinst b/debian/him-cellwriter.postinst
new file mode 100644 (file)
index 0000000..f64a10e
--- /dev/null
@@ -0,0 +1,5 @@
+#!/bin/sh -e
+
+if [ "$1" = "configure" ]; then
+       hildon-im-recache
+fi
diff --git a/debian/rules b/debian/rules
new file mode 100755 (executable)
index 0000000..9269233
--- /dev/null
@@ -0,0 +1,30 @@
+#!/usr/bin/make -f
+
+config.status: configure
+       ./configure
+
+build:
+       $(MAKE)
+
+clean: 
+       $(MAKE) clean
+
+install: build
+       rm -rf debian/him-cellwriter/* || true
+       $(MAKE) prefix=$(CURDIR)/debian/him-cellwriter/usr install
+
+
+binary: build install
+       dh_testdir
+       dh_testroot
+#      dh_install
+       dh_strip
+       dh_fixperms
+       dh_makeshlibs
+       dh_installdeb
+       dh_shlibdeps
+       dh_gencontrol
+       dh_md5sums
+       dh_builddeb
+
+.PHONY: build clean binary install 
diff --git a/default_profile b/default_profile
new file mode 100644 (file)
index 0000000..6612d42
--- /dev/null
@@ -0,0 +1,55 @@
+version 0
+window 0 0 0 0 1 640 0
+options 66 96 12 4 -256 -256 -256 -11264 -8704 -7680 -13312 0 0 0 0 0 1 1 1 1 0 26368 27392 30720 1 0 1 0
+recognize 55 5 0 100 100 100 33
+blocks 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+sample    51     1   -2  -91   18  -86   49  -59   51  -54   54  -37   53  -28   47  -24   41  -24   27  -20   25  -18   25  -15   32   -9   50   14   60   38   58   46   41   52   13   55  -27   61    ;
+sample    61     1  -52  -41  -51  -43  -51  -48  -27  -50   10  -33   29   -1   30    5   29   10   12   25  -12   35  -26   35  -26   23   -7   19   75   19    ;
+sample    39     1    0  -77    0  -21    2   -4    5    5    ;
+sample    98    45  -22 -100  -21  -68  -21   22  -23   32  -25   32  -26   30  -29  -21  -29  -37  -21  -42   -7  -43   11  -42   21  -32   22    2   17    9    0   21  -12   25  -32   27  -44   27    ;
+sample    57     1   44  -44   40  -48   38  -49   16  -49    0  -51   -9  -49  -12  -46  -14  -40  -15  -12  -11   -2   -7    1    2    2   11    1   12    0   19  -29   19  -32   21  -39   24  -39   25  -37   25   16   27   27   30   30    ;
+sample    97    43  -21  -51    3  -50   21  -47   32  -42   36  -39   39  -35   43    3   43   21   42   25   34   38   21   46    7   50   -1   49   -9   43  -16   31  -17   25  -17  -24  -16  -26    0  -30   18  -31   24  -30   32  -24   36  -18    ;
+sample    69    42   54  -80    0  -79  -12  -77  -13  -75  -13  -28   -7    0   -6   10   -4   16    3   18   43   18    ;   43  -25    3  -24   -7  -25    ;
+sample    46    46    3   32    ;
+sample    45    40  -29  -14   -6   -7   10   -6   29   -3    ;
+sample    46    47    0   29    ;
+sample    45    38  -18  -14    0  -12   10   -9   18   -9   47   -6   73   -6   76   -7    ;
+sample    33     1    3 -113    3  -25    ;    3   69    ;
+sample    34     2  -10  -65   -9  -28  -10  -25    ;   18  -62   18  -18    ;
+sample    35     3  -54   36  -53   18  -47   -6  -40  -14  -35  -28  -34  -42  -28  -61  -28  -64  -27  -66  -21  -69    ;  -10   32    0   14    4  -13    7  -20    7  -31   14  -46   14  -53   18  -64   18  -84    ;  -73  -21  -57  -20  -50  -21  -46  -23  -17  -24   -9  -27   54  -28   58  -29    ;  -58   10   36   10   40    9   43    7    ;
+sample    36     4    3 -113    3   21    ;   18  -54   18  -72   17  -74   14  -75  -24  -75  -29  -73  -32  -64  -34  -61  -35  -50  -34  -46  -31  -43   36  -42   39  -41   40  -39   40  -20   39  -17   36  -14   21   -9  -20   -9  -25  -10    ;
+sample    37     5    3  -76  -13  -79  -17  -79  -24  -76  -30  -71  -31  -68  -31  -50  -30  -48  -24  -46  -13  -46   -6  -47   -3  -50   -2  -53   -2  -72   -3  -76    ;   47  -76   31  -58   30  -56   20  -51   11  -39    9  -32    3  -27    1  -24   -1  -13   -8    0  -12    4  -14   18    ;   32  -21   31   -6   30   -5   29    7   30    9   32   10   40   10   46    9   47    7   47  -17   46  -19   40  -20   37  -19   36  -17   36   -7    ;
+sample    38     7   54  -69   40  -76   29  -85   -6  -86  -13  -85  -16  -83  -20  -72  -20  -42  -17  -35  -12  -29   14  -28   18  -29    9  -26    2  -20    0  -13   -2  -10   -5   -8   -8    0  -12    4  -13   43  -12   45   -6   47   21   47   30   40   33   32   41   22   46   17   48   11   51    7   51  -20   50  -23   40  -25    ;   10    7   18   21   21   24   29   25   33   28   41   31   50   33   54   36   65   39   76   47   84   47    ;
+sample    39    49    0  -84    4  -65    9  -60   10  -57   10  -42   14  -36    ;
+sample    40     9   29  -98   16  -91    9  -80    2  -72   -6  -53   -6    7    0   21    0   25    2   30   10   36   19   47   21   53   32   58   47   58    ;
+sample    41    10  -21  -80   -6  -61   -6  -53   -2  -42   -2  -35    0  -28    0  -17    3   84    2   90   -3   94  -17   98  -25   98    ;
+sample    42    11  -21  -84  -16  -65   -6  -57    1  -48    7  -46   14  -46   17  -47   18  -68   17  -71   14  -71   11  -68    6  -58    0  -53   -5  -42  -12  -34  -14  -28  -20  -21  -24  -20  -31  -20  -34  -21  -35  -24  -35  -46  -34  -49  -31  -50  -13  -50   -5  -52    0  -56   14  -58    ;
+sample    43    12    0  -47    0   51    ;  -29    3    0   -8   10  -13   18  -16   29  -17   36  -20   43  -21    ;
+sample    44    13    0   10    0   53   -7   61   -9   62  -17   63  -18   65    ;
+sample    45    14   -3  -10   54   -9   58  -10    ;
+sample    46    52    0   21    ;
+sample    47    16  -40   47  -32   21  -27    0  -10  -50    6 -112   13 -124   18 -126   36 -126   36 -127    ;
+sample    48    17   36  -62   18  -68   15  -67   11  -61   10  -50    9  -47    3  -39    3   36    4   38   14   43   29   42   39   29   41   23   46   20   47   18   47   10   51    0   51  -57   50  -60   47  -61   36  -61   32  -62    ;
+sample    49    18  -14  -54   -5  -71   -2  -75    1  -86    3  -89   10  -90   13  -89   14  -83   14   51    ;
+sample    50    19  -36  -69  -22  -77  -19  -83   -3  -87    0  -90   14  -94   40  -94   46  -92   47  -90   47  -28   46  -25   40  -17   39  -14   33   -8   31   -7   30   -5   20    0    9    8    1   11   -2   15   -6   17  -18   18   10   18   21   14   29   13   32   11   40   10   45    8   49    4   51    3   54    3    ;
+sample    51    51  -18  -87   10  -85   14  -84   16  -82   18  -75   18  -46   17  -42    9  -32    1  -27    0  -25   -3  -25    3  -24   21  -24   30  -21   32  -17   32    3   31    9   21   14   14   14   -2   18  -36   18    ;
+sample    52    21  -32   -7  -20  -24  -16  -35   -7  -43   -5  -50    3  -64    3  -68    4  -70   11  -74   15  -85   21  -93   28  -92   29  -90   29   32    ;  -40   -3  -20  -12  -13  -16   -6  -18   -2  -20    0  -23    3  -25   21  -31   29  -32   32  -34   51  -35   53  -34   54  -32    ;
+sample    53    22  -14  -87  -13  -35  -12  -33   -6  -31   -2  -31    0  -32    1  -34    3  -35   21  -35   28  -33   30  -31   32  -24   32   18   29   24   21   29   -6   29  -12   27  -13   25  -13  -20  -14  -25    ;   -7  -76   21  -72   47  -72   51  -73    ;
+sample    54    23   14  -73   -2  -75   -5  -74   -7  -69  -11  -63  -13  -57  -13  -46  -14  -42  -16  -39  -17    0  -16    3  -12    8    0   14   14   14   17   13   18   10   18    3   19    1   24   -3   25  -20   24  -22   18  -24  -13  -24  -18  -25    ;
+sample    55    24  -36  -87   -9  -86    0  -90   29  -90   31  -89   32  -83   32  -39   30  -31   26  -24   19  -19   13   -3   10    0    9    9    0   21    0   24   -5   30  -10   32    ;
+sample    56    53   32  -58   31  -72   -9  -72  -17  -71  -23  -67  -27  -61  -29  -54  -34  -49  -35  -46  -35  -28  -34  -25  -28  -20  -21  -16  -17  -13   -9  -12   -6  -10   18   -5   28    1   29    3   29   21   28   25   25   28    7   36  -17   36  -19   35  -23   29  -24    0  -16  -11   -9  -14   -1  -24    0  -27   10  -30   14  -32   19  -37   31  -43   33  -49   36  -53   36  -58    ;
+sample    57    44   25  -47   25  -90   24  -93   14  -94    3  -94   -2  -93   -9  -90  -18  -84  -21  -76  -26  -71  -28  -64  -28  -57  -31  -50  -31  -35  -30  -31  -28  -29  -17  -24    3  -24    6  -25    8  -30   13  -36   21  -58   21   32   20   35   13   42    4   44    0   47   -3   47    ;
+sample    58    27   -3  -58    ;   -3   43    ;
+sample    59    28  -29  -47    ;  -29   25  -28   80  -29   84    ;
+sample    60    29   36  -36   21  -33   14  -28    4  -19    0  -16   -2  -13  -19    1  -20    3  -20    7  -17   11  -13   13    0   14    3   15   11   20   17   22   21   25   29   25   33   28   41   31   65   32    ;
+sample    61    50  -18   -7   10   -6   14   -7    ;  -18   29   10   29   13   28   15   26   28   23   29   21    ;
+sample    62    31  -58  -40  -42  -28  -38  -21  -30  -18  -21  -16  -20  -13  -20    0  -21    3  -34   14  -36   21  -43   31  -51   39  -54   43    ;
+sample    63    32  -25  -54  -11  -70    3  -80   14  -83   21  -83   29  -86   40  -86   46  -85   47  -83   47  -42   37  -30   33  -27   32  -24   32    3    ;   29   51    ;
+sample    64    33   21  -14    4    1    2    2    0    7    0   13   -2   21   -2   32    0   36   10   36   13   35   17   29   20   20   21    7   22    4   28    0   29   -2   29  -13   32  -24   32  -46   31  -49   29  -50    3  -50    1  -49   -1  -42  -14  -24  -18  -17  -20   -9  -21   -2  -23    0  -24    3  -24   40  -23   43  -16   50  -13   52   -6   53    0   58   36   58   39   57   41   49   46   45   47   43   47   40    ;
+sample    65    34  -36   36  -35  -50  -31  -61  -31  -72  -30  -74  -24  -78  -21  -78  -20  -72  -17  -64  -17  -50  -14  -42  -13  -24  -12  -21   -7  -16   -6  -13   -6    7   -3   11   -2   14   -2   32    0   36    ;    0   -3  -24   -2  -32    0    ;
+sample    66    35  -25  -65  -24  -39  -20  -24  -20   14  -17   32  -18   40  -17  -50  -14  -54   -3  -58    0  -61   25  -61   28  -60   29  -57   29  -31   28  -28   25  -25   18  -23    7  -14    0  -10   -1   -8   -5   -7   -5   -8    7  -13   43  -13   46  -12   47   14   45   20   43   22   30   26   28   28   25   29   18   29    7   32    0   32   -3   35   -6   36  -25   36    ;
+sample    67    36   25  -76   12  -69    9  -64    7  -64    1  -60   -8  -39   -9    7   -8    9   -6   10   14   10   32    1   36    0    ;
+sample    68    37  -29  -65  -24  -46  -24  -24  -23  -20  -21  -17  -20  -13  -20   14  -18   25  -18   29    ;  -21  -58   32  -57   35  -56   36   10   32   21   29   25   25   28    7   29    0   32   -9   32  -21   36    ;
+sample    46    54   -8  -22    ;
+sample    46    48  -22   33  -22   30    ;
+sample    56     1   19  -19   18  -20   11  -21  -12  -29  -26  -28  -26  -25  -21  -24  -15  -24    0  -21   13  -12   20    0   22   16   22   22   21   24   13   28    0   31  -15   33  -29   32  -31   30  -30   25  -18   22   13   22    ;
diff --git a/src/Makefile b/src/Makefile
new file mode 100644 (file)
index 0000000..29e7faa
--- /dev/null
@@ -0,0 +1,8 @@
+SOURCES = him_cellwriter.c recognize.c main.c window.c stroke.c options.c wordfreq.c averages.c cellwidget.c preprocess.c singleinstance.c keyevent.c 
+HEADERS = keys.h config.h common.h
+
+him_cellwriter.so: $(SOURCES) $(HEADERS)
+       gcc `pkg-config $(LIBS) --libs --cflags` $(SOURCES) -shared -o $@
+
+clean:
+       rm him_cellwriter.so || true
diff --git a/src/averages.c b/src/averages.c
new file mode 100644 (file)
index 0000000..88859bd
--- /dev/null
@@ -0,0 +1,268 @@
+
+/*
+
+cellwriter -- a character recognition input method
+Copyright (C) 2007 Michael Levin <risujin@risujin.org>
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+*/
+
+#include "config.h"
+#include "common.h"
+#include "recognize.h"
+#include <stdlib.h>
+#include <string.h>
+
+/*
+        Average distance engine
+*/
+
+/* Maximum measures */
+#define MEASURE_DIST  (MAX_DIST)
+#define MEASURE_ANGLE (ANGLE_PI / 4)
+
+int num_disqualified;
+
+float measure_distance(const Stroke *a, int i, const Stroke *b, int j,
+                       const Vec2 *offset)
+/* Measure the offset Euclidean distance between two points */
+{
+        Vec2 v;
+
+        vec2_set(&v, a->points[i].x + offset->x - b->points[j].x,
+                 a->points[i].y + offset->y - b->points[j].y);
+        return vec2_square(&v);
+}
+
+static float measure_angle(const Stroke *a, int i, const Stroke *b, int j)
+/* Measure the lesser angular difference between two segments */
+{
+        float diff;
+
+        diff = (ANGLE)(a->points[i].angle - b->points[j].angle);
+        return diff >= 0 ? diff : -diff;
+}
+
+float measure_strokes(Stroke *a, Stroke *b, MeasureFunc func,
+                      void *extra, int points, int elasticity)
+/* Find optimal match between A points and B points for lowest distance via
+   dynamic programming */
+{
+        int i, j, j_to;
+        float table[(points + 1) * (points + 1) + 1];
+
+        /* Coordinates are counted from 1 because of buffer areas */
+        points++;
+
+        /* Fill out the buffer row */
+        j_to = elasticity + 2;
+        if (points < j_to)
+                j_to = points;
+        for (j = 1; j < j_to; j++)
+                table[j] = G_MAXFLOAT;
+
+        /* The first table entry is given */
+        table[points + 1] = 2 * func(a, 0, b, 0, extra);
+
+        for (i = 1; i < points; i++) {
+                float value;
+
+                /* Starting position */
+                j = i - elasticity;
+                if (j < 1)
+                        j = 1;
+
+                /* Buffer column entry */
+                table[i * points + j - 1] = G_MAXFLOAT;
+
+                /* Start from the 2nd cell on the first row */
+                j += i == 1;
+
+                /* End limit */
+                j_to = i + elasticity + 1;
+                if (j_to > points)
+                        j_to = points;
+
+                /* Start with up-left */
+                value = table[(i - 1) * points + j - 1];
+
+                /* Dynamically program the row segment */
+                for (; j < j_to; j++) {
+                        float low_value, measure;
+
+                        measure = func(a, i - 1, b, j - 1, extra);
+                        low_value = value + measure * 2;
+
+                        /* Check if left is lower */
+                        value = table[i * points + j - 1] + measure;
+                        if (value <= low_value)
+                                low_value = value;
+
+                        /* Check if up is lower */
+                        value = table[(i - 1) * points + j];
+                        if (value + measure <= low_value)
+                                low_value = value + measure;
+
+                        table[i * points + j] = low_value;
+                }
+
+                /* End of the row buffer */
+                table[i * points + j_to] = G_MAXFLOAT;
+        }
+
+        /* Return final lowest progression */
+        return table[points * points - 1] / ((points - 1) * 2);
+}
+
+static void stroke_average(Stroke *a, Stroke *b, float *pdist, float *pangle,
+                           Vec2 *ac_to_bc)
+/* Compute the average measures for A vs B */
+{
+        Stroke *a_sampled, *b_sampled;
+
+        /* Sample strokes to equal lengths */
+        if (a->len < 1 || b->len < 1) {
+                g_warning("Attempted to measure zero-length stroke");
+                return;
+        }
+        sample_strokes(a, b, &a_sampled, &b_sampled);
+
+        /* Average the distance between the corresponding points */
+        *pdist = 0.f;
+        if (engines[ENGINE_AVGDIST].range)
+                *pdist = measure_strokes(a_sampled, b_sampled,
+                                         (MeasureFunc)measure_distance,
+                                         ac_to_bc, a_sampled->len,
+                                         FINE_ELASTICITY);
+
+        /* We cannot run angle averages if one of the two strokes has no
+           segments */
+        *pangle = 0.f;
+        if (a->spread < DOT_SPREAD)
+                goto cleanup;
+        else if (b->spread < DOT_SPREAD) {
+                *pangle = ANGLE_PI;
+                goto cleanup;
+        }
+
+        /* Average the angle differences between the points */
+        if (engines[ENGINE_AVGANGLE].range)
+                *pangle = measure_strokes(a_sampled, b_sampled,
+                                          (MeasureFunc)measure_angle, NULL,
+                                          a_sampled->len - 1, FINE_ELASTICITY);
+
+cleanup:
+        /* Free stroke data */
+        stroke_free(a_sampled);
+        stroke_free(b_sampled);
+}
+
+static void sample_average(Sample *sample)
+/* Take the distance between the input and the sample, enumerating the best
+   match assignment between input and sample strokes
+   TODO scale the measures by stroke distance */
+{
+        Vec2 ic_to_sc;
+        Sample *smaller;
+        float distance, m_dist, m_angle;
+        int i;
+
+        /* Ignore disqualified samples */
+        if ((i = sample_disqualified(sample))) {
+                if (i == 2)
+                        num_disqualified++;
+                return;
+        }
+
+        /* Adjust for the difference between sample centers */
+        center_samples(&ic_to_sc, input, sample);
+
+        /* Run the averages */
+        smaller = input->len < sample->len ? input : sample;
+        for (i = 0, distance = 0.f, m_dist = 0.f, m_angle = 0.f;
+             i < smaller->len; i++) {
+                Stroke *input_stroke, *sample_stroke;
+                float weight, s_dist = MAX_DIST, s_angle = ANGLE_PI;
+
+                /* Transform strokes, mapping the larger sample onto the
+                   smaller one */
+                if (input->len >= sample->len) {
+                        input_stroke = transform_stroke(input,
+                                                        &sample->transform, i);
+                        sample_stroke = sample->strokes[i];
+                } else {
+                        input_stroke = input->strokes[i];
+                        sample_stroke = transform_stroke(sample,
+                                                         &sample->transform, i);
+                }
+
+                weight = smaller->strokes[i]->spread < DOT_SPREAD ?
+                         DOT_SPREAD : smaller->strokes[i]->distance;
+                stroke_average(input_stroke, sample_stroke,
+                               &s_dist, &s_angle, &ic_to_sc);
+                m_dist += s_dist * weight;
+                m_angle += s_angle * weight;
+                distance += weight;
+
+                /* Clear the created stroke */
+                stroke_free(input->len >= sample->len ?
+                            input_stroke : sample_stroke);
+        }
+
+        /* Undo square distortion and account for multiple strokes */
+        m_dist = sqrtf(m_dist) / distance;
+        m_angle /= distance;
+
+        /* Check limits */
+        if (m_dist > MAX_DIST)
+                m_dist = MAX_DIST;
+        if (m_angle > ANGLE_PI)
+                m_angle = ANGLE_PI;
+
+        /* Assign the ratings */
+        sample->ratings[ENGINE_AVGDIST] = RATING_MAX -
+                                          RATING_MAX * m_dist / MEASURE_DIST;
+        sample->ratings[ENGINE_AVGANGLE] = RATING_MAX -
+                                           RATING_MAX * m_angle / MEASURE_ANGLE;
+}
+
+void engine_average(void)
+/* Computes average distance and angle differences */
+{
+        Sample *sample;
+        int i;
+
+        num_disqualified = 0;
+        if (!engines[ENGINE_AVGDIST].range &&
+            !engines[ENGINE_AVGANGLE].range)
+                return;
+
+        /* Average angle engine needs to be discounted when the input
+           contains segments too short to produce meaningful angles */
+        engines[ENGINE_AVGANGLE].scale = 0;
+        for (i = 0; i < input->len; i++)
+                if (input->strokes[i]->spread >= DOT_SPREAD)
+                        engines[ENGINE_AVGANGLE].scale++;
+        engines[ENGINE_AVGANGLE].scale = engines[ENGINE_AVGANGLE].scale *
+                                         ENGINE_SCALE / input->len;
+
+        /* Run the averaging engine on every sample */
+        sampleiter_reset();
+        while ((sample = sampleiter_next()))
+                if (sample->ch)
+                        sample_average(sample);
+}
+
diff --git a/src/cellwidget.c b/src/cellwidget.c
new file mode 100644 (file)
index 0000000..e977d59
--- /dev/null
@@ -0,0 +1,2133 @@
+
+/*
+
+cellwriter -- a character recognition input method
+Copyright (C) 2007 Michael Levin <risujin@risujin.org>
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+*/
+
+#include "config.h"
+#include "common.h"
+#include "recognize.h"
+#include "keys.h"
+#include <string.h>
+#include <malloc.h>
+
+#include "hildon-im-ui.h"
+
+/* stroke.c */
+void smooth_stroke(Stroke *s);
+void simplify_stroke(Stroke *s);
+
+/* cellwidget.c */
+int cell_widget_scrollbar_width(void);
+static void start_timeout(void);
+static void show_context_menu(int button, int time);
+static void stop_drawing(void);
+
+/*
+        Cells
+*/
+
+#define ALTERNATES 5
+#define CELL_BASELINE (cell_height / 3)
+#define CELL_BORDER (cell_height / 12)
+
+/* Msec of no mouse motion before a cell is finished */
+#define MOTION_TIMEOUT 500
+
+/* Cell flags */
+#define CELL_SHOW_INK   0x01
+#define CELL_DIRTY      0x02
+#define CELL_VERIFIED   0x04
+#define CELL_SHIFTED    0x08
+
+struct Cell {
+        Sample sample, *alts[ALTERNATES];
+        gunichar2 ch;
+        int alt_used[ALTERNATES];
+        char flags, alt_ratings[ALTERNATES];
+};
+
+/* Cell preferences */
+int cell_width = 40, cell_height = 70, cell_cols_pref = 12, cell_rows_pref = 4,
+    enable_cairo = TRUE, training = FALSE, train_on_input = TRUE,
+    right_to_left = FALSE, keyboard_enabled = TRUE, xinput_enabled = FALSE;
+
+/* Statistics */
+int corrections = 0, rewrites = 0, characters = 0, inputs = 0;
+
+/* Colors */
+GdkColor custom_active_color = RGB_TO_GDKCOLOR(255, 255, 255),
+         custom_inactive_color = RGB_TO_GDKCOLOR(212, 222, 226),
+         custom_ink_color = RGB_TO_GDKCOLOR(0, 0, 0),
+         custom_select_color = RGB_TO_GDKCOLOR(204, 0, 0);
+static GdkColor color_active, color_inactive, color_ink, color_select;
+
+static Cell *cells = NULL, *cells_saved = NULL;
+static GtkWidget *drawing_area = NULL, *training_menu, *scrollbar;
+static GdkPixmap *pixmap = NULL;
+static GdkGC *pixmap_gc = NULL;
+static GdkColor color_bg, color_bg_dark;
+static cairo_t *cairo = NULL;
+static PangoContext *pango = NULL;
+static PangoFontDescription *pango_font_desc = NULL;
+static gunichar2 *history[HISTORY_MAX];
+static int cell_cols, cell_rows, cell_row_view = 0, current_cell = -1, old_cc,
+           cell_cols_saved, cell_rows_saved, cell_row_view_saved,
+           timeout_source,
+           drawing = FALSE, inserting = FALSE, eraser = FALSE, invalid = FALSE,
+           potential_insert = FALSE, potential_hold = FALSE, cross_out = FALSE,
+           show_keys = TRUE, is_clear = TRUE, keys_dirty = FALSE;
+static double cursor_x, cursor_y;
+
+static void cell_coords(int cell, int *px, int *py)
+/* Get the int position of a cell from its index */
+{
+        int cell_y, cell_x;
+
+        cell -= cell_row_view * cell_cols;
+        cell_y = cell / cell_cols;
+        cell_x = cell - cell_y * cell_cols;
+        *px = (!right_to_left ? cell_x * cell_width :
+                                (cell_cols - cell_x - 1) * cell_width) + 1;
+        *py = cell_y * cell_height + 1;
+}
+
+static void set_pen_color(Sample *sample, int cell)
+/* Selects the pen color depending on if the sample being drawn is the input
+   or the template sample */
+{
+        if (sample == input || sample == &cells[cell].sample)
+                cairo_set_source_gdk_color(cairo, &color_ink, 1.);
+        else
+                cairo_set_source_gdk_color(cairo, &color_select, 1.);
+}
+
+static void render_point(Sample *sample, int cell, int stroke, Vec2 *offset)
+/* Draw a single point stroke */
+{
+        double x, y, radius;
+        int cx, cy;
+
+        if (!pixmap || stroke < 0 || !sample || stroke >= sample->len ||
+            sample->strokes[stroke]->len < 1)
+                return;
+
+        /* Apply offset */
+        x = sample->strokes[stroke]->points[0].x;
+        y = sample->strokes[stroke]->points[0].y;
+        if (offset) {
+                x += offset->x;
+                y += offset->y;
+        }
+
+        /* Unscale coordinates */
+        cell_coords(cell, &cx, &cy);
+        x = cx + cell_width / 2 + x * cell_height / SCALE;
+        y = cy + cell_height / 2 + y * cell_height / SCALE;
+
+        /* Draw a dot with cairo */
+        cairo_new_path(cairo);
+        radius = cell_height / 33.;
+        cairo_arc(cairo, x, y, radius > 1. ? radius : 1., 0., 2 * M_PI);
+        set_pen_color(sample, cell);
+        cairo_fill(cairo);
+
+        gtk_widget_queue_draw_area(drawing_area, x - radius - 0.5,
+                                   y - radius - 0.5, radius * 2 + 0.5,
+                                   radius * 2 + 0.5);
+}
+
+static void render_segment(Sample *sample, int cell, int stroke, int seg,
+                           Vec2 *offset)
+/* Draw a segment of the stroke
+   FIXME since the segments are not properly connected according to Cairo,
+         there is a bit of missing value at the segment connection points */
+{
+        double pen_width, x1, x2, y1, y2;
+        int xmin, xmax, ymin, ymax, cx, cy, pen_range;
+
+        if (!cairo || stroke < 0 || !sample || stroke >= sample->len ||
+            seg < 0 || seg >= sample->strokes[stroke]->len - 1)
+                return;
+
+        x1 = sample->strokes[stroke]->points[seg].x;
+        x2 = sample->strokes[stroke]->points[seg + 1].x;
+        y1 = sample->strokes[stroke]->points[seg].y;
+        y2 = sample->strokes[stroke]->points[seg + 1].y;
+
+        /* Apply offset */
+        if (offset) {
+                x1 += offset->x;
+                y1 += offset->y;
+                x2 += offset->x;
+                y2 += offset->y;
+        }
+
+        /* Unscale coordinates */
+        cell_coords(cell, &cx, &cy);
+        x1 = cx + cell_width / 2 + x1 * cell_height / SCALE;
+        x2 = cx + cell_width / 2 + x2 * cell_height / SCALE;
+        y1 = cy + cell_height / 2 + y1 * cell_height / SCALE;
+        y2 = cy + cell_height / 2 + y2 * cell_height / SCALE;
+
+        /* Find minimum and maximum x and y */
+        if (x1 > x2) {
+                xmax = x1 + 0.9999;
+                xmin = x2;
+        } else {
+                xmin = x1;
+                xmax = x2 + 0.9999;
+        }
+        if (y1 > y2) {
+                ymax = y1 + 0.9999;
+                ymin = y2;
+        } else {
+                ymin = y1;
+                ymax = y2 + 0.9999;
+        }
+
+        /* Draw the new segment using Cairo */
+        cairo_new_path(cairo);
+        cairo_move_to(cairo, x1, y1);
+        cairo_line_to(cairo, x2, y2);
+        set_pen_color(sample, cell);
+        pen_width = cell_height / 33.;
+        if (pen_width < 1.)
+                pen_width = 1.;
+        cairo_set_line_width(cairo, pen_width);
+        cairo_stroke(cairo);
+
+        /* Dirty only the new segment */
+        pen_range = 2 * pen_width + 0.9999;
+        gtk_widget_queue_draw_area(drawing_area, xmin - pen_range,
+                                   ymin - pen_range,
+                                   xmax - xmin + pen_range + 1,
+                                   ymax - ymin + pen_range + 1);
+}
+
+static void render_sample(Sample *sample, int cell)
+/* Render the ink from a sample in a cell */
+{
+        Vec2 sc_to_ic;
+        int i, j;
+
+        if (!sample)
+                return;
+
+        /* Center stored samples on input */
+        if (sample != &cells[cell].sample)
+                center_samples(&sc_to_ic, sample, &cells[cell].sample);
+        else
+                vec2_set(&sc_to_ic, 0., 0.);
+
+        for (i = 0; i < sample->len; i++)
+                if (sample->strokes[i]->len <= 1 ||
+                    sample->strokes[i]->spread < DOT_SPREAD)
+                        render_point(sample, cell, i, &sc_to_ic);
+                else
+                        for (j = 0; j < sample->strokes[i]->len - 1; j++)
+                                render_segment(sample, cell, i, j, &sc_to_ic);
+}
+
+static int cell_offscreen(int cell)
+{
+        int rows, cols;
+
+        cols = cell_cols;
+        rows = cell_rows < cell_rows_pref ? cell_rows : cell_rows_pref;
+        return cell < cell_row_view * cols ||
+               cell >= (cell_row_view + rows) * cols;
+}
+
+static void dirty_cell(int cell)
+{
+        if (!cell_offscreen(cell))
+                cells[cell].flags |= CELL_DIRTY;
+}
+
+static void dirty_all(void)
+{
+        int i, rows;
+
+        rows = cell_row_view + cell_rows_pref > cell_rows ?
+               cell_rows : cell_row_view + cell_rows_pref;
+        for (i = cell_cols * cell_row_view; i < rows * cell_cols; i++)
+                cells[i].flags |= CELL_DIRTY;
+}
+
+static void render_cell(int i)
+{
+        cairo_pattern_t *pattern;
+        GdkColor color, *base_color;
+        Cell *pc;
+        int x, y, active, cols, samples = 0;
+
+        if (!cairo || !pixmap || !pixmap_gc || cell_offscreen(i))
+                return;
+        pc = cells + i;
+        cell_coords(i, &x, &y);
+        if (training) {
+                samples = char_trained(pc->ch);
+                active = pc->ch && (samples > 0 ||
+                                    (current_cell == i && input &&
+                                     !invalid && input->len));
+        } else
+                active = pc->ch || (current_cell == i && !inserting &&
+                                    !invalid && input && input->len);
+        base_color = active ? &color_active : &color_inactive;
+
+        /* Fill above baseline */
+        gdk_gc_set_rgb_fg_color(pixmap_gc, base_color);
+        gdk_draw_rectangle(pixmap, pixmap_gc, TRUE, x, y, cell_width,
+                                cell_height - CELL_BASELINE);
+
+        /* Fill baseline */
+        highlight_gdk_color(base_color, &color, 0.1);
+        gdk_gc_set_rgb_fg_color(pixmap_gc, &color);
+        gdk_draw_rectangle(pixmap, pixmap_gc, TRUE, x, y + cell_height -
+                           CELL_BASELINE, cell_width, CELL_BASELINE);
+
+        /* Cairo clip region */
+        cairo_reset_clip(cairo);
+        cairo_rectangle(cairo, x, y, cell_width, cell_height);
+        cairo_clip(cairo);
+
+        /* Separator line */
+        cols = cell_cols;
+        if ((!right_to_left && i % cell_cols) ||
+            (right_to_left && i % cell_cols != cols - 1)) {
+                highlight_gdk_color(base_color, &color, 0.5);
+                pattern = cairo_pattern_create_linear(x, y, x, y + cell_height);
+                cairo_pattern_add_gdk_color_stop(pattern, 0.0, &color, 0.);
+                cairo_pattern_add_gdk_color_stop(pattern, 0.5, &color, 1.);
+                cairo_pattern_add_gdk_color_stop(pattern, 1.0, &color, 0.);
+                cairo_set_source(cairo, pattern);
+                cairo_set_line_width(cairo, 0.5);
+                cairo_move_to(cairo, x + 0.5, y);
+                cairo_line_to(cairo, x + 0.5, y + cell_height - 1);
+                cairo_stroke(cairo);
+                cairo_pattern_destroy(pattern);
+        }
+
+        /* Draw ink if shown */
+        if ((cells[i].ch && cells[i].flags & CELL_SHOW_INK) ||
+            (current_cell == i && input && input->len)) {
+                int j;
+
+                render_sample(&cells[i].sample, i);
+                if (cells[i].ch)
+                        for (j = 0; j < ALTERNATES && cells[i].alts[j]; j++)
+                                if (sample_valid(cells[i].alts[j],
+                                                 cells[i].alt_used[j]) &&
+                                    cells[i].alts[j]->ch == cells[i].ch) {
+                                        render_sample(cells[i].alts[j], i);
+                                        break;
+                                }
+        }
+
+        /* Draw letter if recognized or training */
+        else if (pc->ch && (current_cell != i || !input || !input->len)) {
+                PangoLayout *layout;
+                PangoRectangle ink_ext, log_ext;
+                char string[6] = { 0, 0, 0, 0, 0, 0 };
+
+                /* Training color is determined by how well a character is
+                   trained */
+                if (training) {
+                        if (samples)
+                                highlight_gdk_color(&color_ink, &color,
+                                                    0.5 - ((double)samples) /
+                                                    samples_max / 2.);
+                        else
+                                highlight_gdk_color(&color_inactive,
+                                                    &color, 0.2);
+                }
+
+                /* Use ink color unless this is a questionable match */
+                else {
+                        color = color_ink;
+                        if (!(pc->flags & CELL_VERIFIED) && pc->alts[0] &&
+                            pc->alts[1] && pc->ch == pc->alts[0]->ch &&
+                            pc->alt_ratings[0] - pc->alt_ratings[1] <= 10)
+                                color = color_select;
+                }
+
+                cairo_set_source_gdk_color(cairo, &color, 1.);
+                layout = pango_layout_new(pango);
+                cairo_move_to(cairo, x, y);
+                g_unichar_to_utf8(pc->ch, string);
+                pango_layout_set_text(layout, string, 6);
+                pango_layout_set_font_description(layout, pango_font_desc);
+                pango_layout_get_pixel_extents(layout, &ink_ext, &log_ext);
+                cairo_rel_move_to(cairo,
+                                  cell_width / 2 - log_ext.width / 2, 2);
+                pango_cairo_show_layout(cairo, layout);
+                g_object_unref(layout);
+        }
+
+        /* Insertion arrows */
+        if (!invalid && inserting &&
+            (current_cell == i || current_cell == i + 1)) {
+                double width, stem, height;
+
+                cairo_set_source_gdk_color(cairo, &color_select, 1.);
+                width = CELL_BORDER;
+                stem = CELL_BORDER / 2;
+                height = CELL_BORDER;
+                if ((!right_to_left && current_cell == i) ||
+                    (right_to_left && current_cell == i + 1)) {
+
+                        /* Top right arrow */
+                        cairo_move_to(cairo, x, y + 1);
+                        cairo_line_to(cairo, x + stem, y + 1);
+                        cairo_line_to(cairo, x + stem, y + height);
+                        cairo_line_to(cairo, x + width, y + height);
+                        cairo_line_to(cairo, x, y + height * 2);
+                        cairo_close_path(cairo);
+                        cairo_fill(cairo);
+
+                        /* Bottom right arrow */
+                        cairo_move_to(cairo, x, y + cell_height - 1);
+                        cairo_line_to(cairo, x + stem, y + cell_height - 1);
+                        cairo_line_to(cairo, x + stem,
+                                      y + cell_height - height);
+                        cairo_line_to(cairo, x + width,
+                                      y + cell_height - height);
+                        cairo_line_to(cairo, x, y + cell_height - height * 2);
+                        cairo_close_path(cairo);
+                        cairo_fill(cairo);
+
+                } else if ((!right_to_left && current_cell == i + 1) ||
+                           (right_to_left && current_cell == i)) {
+                        double ox;
+
+                        ox = i % cell_cols == cell_cols - 1 ? 0. : 1.;
+
+                        /* Top left arrow */
+                        cairo_move_to(cairo, x + cell_width + ox, y + 1);
+                        cairo_line_to(cairo, x + cell_width - stem + ox,
+                                      y + 1);
+                        cairo_line_to(cairo, x + cell_width - stem + ox,
+                                      y + height);
+                        cairo_line_to(cairo, x + cell_width - width + ox,
+                                      y + height);
+                        cairo_line_to(cairo, x + cell_width + ox,
+                                      y + height * 2);
+                        cairo_close_path(cairo);
+                        cairo_fill(cairo);
+
+                        /* Bottom left arrow */
+                        cairo_move_to(cairo, x + cell_width + ox,
+                                      y + cell_height - 1);
+                        cairo_line_to(cairo, x + cell_width - stem + ox,
+                                      y + cell_height - 1);
+                        cairo_line_to(cairo, x + cell_width - stem + ox,
+                                      y + cell_height - height);
+                        cairo_line_to(cairo, x + cell_width - width + ox,
+                                      y + cell_height - height);
+                        cairo_line_to(cairo, x + cell_width + ox,
+                                      y + cell_height - height * 2);
+                        cairo_close_path(cairo);
+                        cairo_fill(cairo);
+
+                }
+        }
+
+        gtk_widget_queue_draw_area(drawing_area, x, y, cell_width, cell_height);
+        pc->flags &= ~CELL_DIRTY;
+
+}
+
+static void render_dirty(void)
+/* Render cells marked dirty */
+{
+        int i;
+
+        for (i = cell_row_view * cell_cols; i < cell_rows * cell_cols; i++)
+                if (cells[i].flags & CELL_DIRTY)
+                        render_cell(i);
+}
+
+void cell_widget_render(void)
+/* Render the cells */
+{
+        int i, cols, rows, width, height;
+
+        if (!cairo || !pixmap || !pixmap_gc)
+                return;
+
+        /* On-screen keyboard eats up some cells on the end */
+        cols = cell_cols;
+
+        /* Render cells */
+        for (i = cell_row_view * cols; i < cell_rows * cols; i++)
+                render_cell(i);
+
+        /* Draw border */
+        rows = cell_rows < cell_rows_pref ? cell_rows : cell_rows_pref;
+        width = cell_width * cols + 1;
+        height = cell_height * rows + 1;
+        gdk_gc_set_rgb_fg_color(pixmap_gc, &color_bg_dark);
+        if (!right_to_left)
+                gdk_draw_rectangle(pixmap, pixmap_gc, FALSE, 0, 0,
+                                   width, height);
+        else
+                gdk_draw_rectangle(pixmap, pixmap_gc, FALSE,
+                                   drawing_area->allocation.width - width - 1,
+                                   0, width, height);
+
+        /* Fill extra space to the right */
+        gdk_gc_set_rgb_fg_color(pixmap_gc, &color_bg);
+        if (!right_to_left)
+                gdk_draw_rectangle(pixmap, pixmap_gc, TRUE, width + 1, 0,
+                                   drawing_area->allocation.width - width,
+                                   height + 1);
+        else
+                gdk_draw_rectangle(pixmap, pixmap_gc, TRUE, 0, 0,
+                                   drawing_area->allocation.width - width - 1,
+                                   height + 1);
+
+        /* Fill extra space below */
+        gdk_draw_rectangle(pixmap, pixmap_gc, TRUE, 0, height + 1,
+                           drawing_area->allocation.width,
+                           drawing_area->allocation.height - height + 1);
+
+        /* Dirty the entire drawing area */
+        gtk_widget_queue_draw(drawing_area);
+}
+
+static void clear_cell(int i)
+{
+        Cell *cell;
+
+        cell = cells + i;
+        cell->flags = 0;
+        if (cell->ch || i == current_cell) {
+                if (i == current_cell)
+                        input = NULL;
+                cell->flags |= CELL_DIRTY;
+        }
+        clear_sample(&cell->sample);
+        cell->ch = 0;
+        cell->alts[0] = NULL;
+}
+
+static void pad_cell(int cell)
+{
+        int i;
+
+        /* Turn any blank cells behind the cell into spaces */
+        for (i = cell - 1; i >= 0 && !cells[i].ch; i--) {
+                cells[i].ch = ' ';
+                cells[i].flags |= CELL_DIRTY;
+        }
+}
+
+static void free_cells(void)
+/* Free sample data */
+{
+        int i;
+
+        if (!cells)
+                return;
+        for (i = 0; i < cell_rows * cell_cols; i++)
+                clear_cell(i);
+        g_free(cells);
+        cells = NULL;
+        input = NULL;
+}
+
+static void wrap_cells(int new_rows, int new_cols)
+/* Word wrap cells */
+{
+        Cell *new_cells;
+        int i, j, size, row, col, break_i = -1, break_j = -1;
+
+        /* Allocate and clear the new grid */
+        if (new_rows < 1)
+                new_rows = 1;
+        size = new_rows * new_cols * sizeof (Cell);
+        new_cells = g_malloc0(size);
+
+        for (i = 0, j = 0, row = 0, col = 0; i < cell_rows * cell_cols; i++) {
+                if (!cells[i].ch)
+                        continue;
+
+                /* Break at non-alphanumeric characters */
+                if (!g_unichar_isalnum(cells[i].ch)) {
+                        break_i = i;
+                        break_j = j;
+                }
+
+                if (col >= new_cols) {
+
+                        /* If we need to, allocate room for the new row */
+                        if (++row >= new_rows) {
+                                size = ++new_rows * new_cols * sizeof (Cell);
+                                new_cells = g_realloc(new_cells, size);
+                                memset(new_cells + (new_rows - 1) * new_cols,
+                                       0, new_cols * sizeof (Cell));
+                        }
+
+                        /* Move any hanging words down to the next row */
+                        size = i - break_i - 1;
+                        if (size >= 0 && size < i - 1) {
+                                memset(new_cells + break_j + 1, 0,
+                                       sizeof (Cell) * size);
+                                i = break_i + 1;
+                                break_i = -1;
+                        }
+                        col = 0;
+                        if (!cells[i].ch)
+                                continue;
+                }
+                new_cells[j++] = cells[i];
+                col++;
+        }
+
+        /* If we have filled the last row, we need to add a new row */
+        if (col >= new_cols && row >= new_rows - 1) {
+                size = ++new_rows * new_cols * sizeof (Cell);
+                new_cells = g_realloc(new_cells, size);
+                memset(new_cells + (new_rows - 1) * new_cols, 0,
+                       new_cols * sizeof (Cell));
+        }
+
+        /* Only free the cell array, NOT the samples as we have copied the
+           Sample data over to the new cell array */
+        g_free(cells);
+        cells = new_cells;
+
+        /* Scroll the grid */
+        if (new_rows > cell_rows && new_rows > cell_rows_pref)
+                cell_row_view += new_rows - cell_rows;
+
+        /* Do not let the row view look too far down */
+        if (cell_row_view + cell_rows_pref > new_rows) {
+                cell_row_view = new_rows - cell_rows_pref;
+                if (cell_row_view < 0)
+                        cell_row_view = 0;
+        }
+
+        cell_rows = new_rows;
+        cell_cols = new_cols;
+}
+
+static int set_size_request(int force)
+/* Resize the drawing area if necessary */
+{
+        int new_w, new_h, rows, resized;
+
+        new_w = cell_cols * cell_width + 2;
+        rows = cell_rows;
+        if (rows > cell_rows_pref)
+                rows = cell_rows_pref;
+        new_h = rows * cell_height + 2;
+        resized = new_w != drawing_area->allocation.width ||
+                  new_h != drawing_area->allocation.height || force;
+        if (!resized)
+                return FALSE;
+        gtk_widget_set_size_request(drawing_area, new_w, new_h);
+        return TRUE;
+}
+
+static int pack_cells(int new_rows, int new_cols)
+/* Pack and position cells, resize widget and window when necessary.
+   Returns TRUE if the widget was resized in the process and can expect a
+   configure event in the near future. */
+{
+        int i, rows, range, new_range;
+
+        /* Must have at least one row */
+        if (new_rows < 1)
+                new_rows = 1;
+
+        /* Word wrapping will perform its own memory allocation */
+        if (!training && cells)
+                wrap_cells(new_rows, new_cols);
+
+        else if (!cells || new_rows != cell_rows || new_cols != cell_cols) {
+
+                /* Find minimum number of rows necessary */
+                if (cells) {
+                        for (i = cell_rows * cell_cols - 1; i > 0; i--)
+                                if (cells[i].ch)
+                                        break;
+                        rows = i / new_cols + 1;
+                        if (new_rows < rows)
+                                new_rows = rows;
+                        new_range = new_rows * new_cols;
+
+                        /* If we have shrunk the grid, clear cells outside */
+                        range = cell_rows * cell_cols;
+                        for (i = new_range; i < range; i++)
+                                clear_cell(i);
+                } else {
+                        range = 0;
+                        new_range = new_rows * new_cols;
+                }
+
+                /* Allocate enough room, clear any new cells */
+                cells = g_realloc(cells, new_rows * new_cols * sizeof (Cell));
+                if (new_range > range)
+                        memset(cells + range, 0,
+                               (new_range - range) * sizeof (Cell));
+
+                cell_rows = new_rows;
+                cell_cols = new_cols;
+        }
+        dirty_all();
+
+        /* Update the scrollbar */
+        if (cell_rows <= cell_rows_pref) {
+                cell_row_view = 0;
+                gtk_widget_hide(scrollbar);
+        } else {
+                GtkObject *adjustment;
+
+                if (cell_row_view > cell_rows - cell_rows_pref)
+                        cell_row_view = cell_rows - cell_rows_pref;
+                if (cell_row_view < 0)
+                        cell_row_view = 0;
+                adjustment = gtk_adjustment_new(cell_row_view, 0, cell_rows, 1,
+                                                cell_rows_pref, cell_rows_pref);
+                gtk_range_set_adjustment(GTK_RANGE(scrollbar),
+                                         GTK_ADJUSTMENT(adjustment));
+                gtk_widget_show(scrollbar);
+        }
+
+        return set_size_request(FALSE);
+}
+
+static void stop_timeout(void)
+{
+        if (!timeout_source)
+                return;
+        g_source_remove(timeout_source);
+        timeout_source = 0;
+}
+
+static void finish_cell(int cell)
+{
+        stop_timeout();
+        if (cell < 0 || cell >= cell_rows * cell_cols ||
+            !input || input->len < 1)
+                return;
+        cells[cell].flags |= CELL_DIRTY;
+
+        /* Train on the input */
+        if (training)
+                train_sample(&cells[cell].sample, TRUE);
+
+        /* Recognize input */
+        else if (input && input->strokes[0] && input->strokes[0]->len) {
+                Cell *pc = cells + cell;
+                int i;
+
+                /* Track stats */
+                if (pc->ch && pc->ch != ' ')
+                        rewrites++;
+                inputs++;
+
+                old_cc = cell;
+                recognize_sample(input, pc->alts, ALTERNATES);
+                pc->ch = input->ch;
+                pc->flags &= ~CELL_VERIFIED;
+                if (pc->ch)
+                        pad_cell(cell);
+
+                /* Copy the alternate ratings and usage stamps before they're
+                   overwritten by another call to recognize_sample() */
+                for (i = 0; i < ALTERNATES && pc->alts[i]; i++) {
+                        pc->alt_ratings[i] = pc->alts[i]->rating;
+                        pc->alt_used[i] = pc->alts[i]->used;
+                }
+
+                /* Add a row if this is the last cell */
+                if (cell == cell_rows * cell_cols - 1)
+                        pack_cells(0, cell_cols);
+        }
+
+        input = NULL;
+        drawing = FALSE;
+}
+
+static gboolean finish_timeout(void)
+/* Motion timeout for finishing drawing a cell */
+{
+        finish_cell(current_cell);
+        render_dirty();
+        timeout_source = 0;
+        start_timeout();
+        return FALSE;
+}
+
+static gboolean row_timeout(void)
+/* Motion timeout for adding a row */
+{
+        pack_cells(cell_rows + 1, cell_cols);
+        cell_widget_render();
+        timeout_source = 0;
+        return FALSE;
+}
+
+static int check_clear(void)
+{
+        int i;
+
+        if (is_clear)
+                return TRUE;
+        if (training || (input && input->len))
+                return FALSE;
+        for (i = 0; i < cell_cols * cell_rows; i++)
+                if (cells[i].ch)
+                        return FALSE;
+        return TRUE;
+}
+
+static gboolean is_clear_timeout(void)
+/* Motion timeout for checking clear state */
+{
+        timeout_source = 0;
+        if (is_clear || !check_clear())
+                return FALSE;
+
+        /* Show the on-screen keyboard */
+        show_keys = keyboard_enabled;
+        is_clear = TRUE;
+
+        pack_cells(1, cell_cols);
+        cell_widget_render();
+        return FALSE;
+}
+
+static gboolean hold_timeout(void)
+/* Motion timeout for popping up a hold-click context menu */
+{
+        if (potential_hold) {
+                potential_hold = FALSE;
+                stop_drawing();
+                show_context_menu(1, gtk_get_current_event_time());
+        }
+        timeout_source = 0;
+        return FALSE;
+}
+
+static void start_timeout(void)
+/* If a timeout action is approriate for the current situation, start a
+   timeout */
+{
+        GSourceFunc func = NULL;
+
+        if (potential_hold)
+                return;
+        stop_timeout();
+        if (cross_out)
+                return;
+
+        /* Events below are not triggered while drawing */
+        if (!drawing) {
+                if (input)
+                        func = (GSourceFunc)finish_timeout;
+                else if (!cells[cell_rows * cell_cols - 1].ch &&
+                         cells[cell_rows * cell_cols - 2].ch && !training)
+                        func = (GSourceFunc)row_timeout;
+                else if (!is_clear && check_clear())
+                        func = (GSourceFunc)is_clear_timeout;
+        }
+
+        if (func)
+                timeout_source = g_timeout_add(MOTION_TIMEOUT, func, NULL);
+}
+
+static void start_hold(void)
+{
+        potential_hold = TRUE;
+        if (timeout_source)
+                g_source_remove(timeout_source);
+        timeout_source = g_timeout_add(MOTION_TIMEOUT,
+                                       (GSourceFunc)hold_timeout, NULL);
+}
+
+void cell_widget_set_cursor(int recreate)
+/* Set the drawing area cursor to a black box pen cursor or to a blank cursor
+   depending on which is appropriate */
+{
+        static char bits[] = { 0xff, 0xff, 0xff };      /* Square cursor */
+                           /*{ 0x02, 0xff, 0x02 };*/    /* Cross cursor */
+        static GdkCursor *square;
+        GdkPixmap *pixmap;
+        GdkCursor *cursor;
+
+        /* Ink color changed, recreate cursor */
+        if (recreate) {
+                if (square)
+                        gdk_cursor_unref(square);
+                pixmap = gdk_bitmap_create_from_data(NULL, bits, 3, 3);
+                square = gdk_cursor_new_from_pixmap(pixmap, pixmap,
+                                                    &color_ink,
+                                                    &color_ink, 1, 1);
+                g_object_unref(pixmap);
+        }
+        cursor = square;
+
+        /* Eraser cursor */
+        if (eraser || cross_out) {
+                GdkDisplay *display;
+
+                display = gtk_widget_get_display(drawing_area);
+                cursor = gdk_cursor_new_for_display(display, GDK_CIRCLE);
+        }
+
+        gdk_window_set_cursor(drawing_area->window,
+                              invalid || inserting ? NULL : cursor);
+}
+
+static void stop_drawing(void)
+/* Ends the current stroke and applies various processing functions */
+{
+        Stroke *stroke;
+
+        if (!drawing) {
+                if (cross_out) {
+                        cross_out = FALSE;
+                        cell_widget_set_cursor(FALSE);
+                }
+                return;
+        }
+        drawing = FALSE;
+        if (!input || input->len >= STROKES_MAX)
+                return;
+        stroke = input->strokes[input->len - 1];
+        smooth_stroke(stroke);
+        simplify_stroke(stroke);
+        process_stroke(stroke);
+        render_cell(current_cell);
+        render_sample(input, current_cell);
+        start_timeout();
+}
+
+static void erase_cell(int cell)
+{
+        if (!training) {
+                clear_cell(cell);
+                render_dirty();
+        } else {
+                untrain_char(cells[cell].ch);
+                render_cell(cell);
+        }
+}
+
+static void check_cell(double x, double y, GdkDevice *device)
+/* Check if we have changed to a different cell */
+{
+        int cell_x, cell_y, cell, rem_x, rem_y,
+            old_inserting, old_invalid, old_eraser, old_cross_out;
+
+        /* Stop drawing first */
+        old_cross_out = cross_out;
+        if (drawing && !cross_out) {
+                int dx, dy;
+
+                /* Check if we have started the cross-out gesture */
+                cell_coords(current_cell, &cell_x, &cell_y);
+                cell_x += cell_width / 2;
+                cell_y += cell_height / 2;
+                dx = cell_x - x;
+                dy = cell_y - y;
+                if (dx < 0)
+                        dx = -dx;
+                if (dy < 0)
+                        dy = -dy;
+                if (dx < cell_width && dy < cell_height)
+                        return;
+
+                cross_out = TRUE;
+                drawing = FALSE;
+                clear_sample(input);
+                input = NULL;
+                erase_cell(current_cell);
+        }
+
+        /* Is this the eraser tip? */
+        old_eraser = eraser;
+        eraser = device && device->source == GDK_SOURCE_ERASER;
+
+        /* Adjust for border */
+        x--;
+        y--;
+
+        /* Right-to-left mode inverts the x-axis */
+        if (right_to_left)
+                x = cell_cols * cell_width - x - 1;
+
+        /* What cell are we hovering over? */
+        cell_y = y / cell_height + cell_row_view;
+        cell_x = x / cell_width;
+        cell = cell_cols * cell_y + cell_x;
+
+        /* Out of bounds or invalid cell */
+        old_invalid = invalid;
+        invalid = cell_x < 0 || cell_y < 0 || cell_x >= cell_cols ||
+                  cell_y >= cell_rows || cell_offscreen(cell) ||
+                  (training && !cells[cell].ch);
+
+        /* Are we in the insertion hotspot? */
+        rem_x = x - cell_x * cell_width;
+        rem_y = y - (cell_y - cell_row_view) * cell_height;
+        old_inserting = inserting;
+        inserting = FALSE;
+        if (!cross_out && !eraser && !invalid && !training && !input &&
+            (rem_y <= CELL_BORDER * 2 ||
+             rem_y > cell_height - CELL_BORDER * 2)) {
+                if (rem_x <= CELL_BORDER + 1)
+                        inserting = TRUE;
+                else if (cell < cell_rows * cell_cols - 1 &&
+                         rem_x > cell_width - CELL_BORDER) {
+                        inserting = TRUE;
+                        cell++;
+                }
+        }
+
+        /* Current cell has changed */
+        old_cc = current_cell;
+        if (current_cell != cell) {
+                current_cell = cell;
+                if (!cross_out)
+                        finish_cell(old_cc);
+        }
+
+        /* We have moved into or out of the insertion hotspot */
+        if (old_inserting != inserting || old_cc != cell) {
+                if (old_inserting) {
+                        dirty_cell(old_cc);
+                        dirty_cell(old_cc - 1);
+                }
+                if (inserting) {
+                        dirty_cell(current_cell);
+                        dirty_cell(current_cell - 1);
+                }
+        }
+
+        /* Update cursor if necessary */
+        if (old_invalid != invalid || old_inserting != inserting ||
+            old_eraser != eraser || old_cross_out != cross_out)
+                cell_widget_set_cursor(FALSE);
+
+        render_dirty();
+}
+
+static void unclear(int render)
+/* Hides the on-screen keyboard and re-renders the cells.
+   FIXME we only need to render dirty cells */
+{
+        is_clear = FALSE;
+        if (!show_keys)
+                return;
+        show_keys = FALSE;
+        if (render)
+                cell_widget_render();
+}
+
+static void draw(double x, double y)
+{
+        Stroke *s;
+        int cx, cy;
+
+        if (current_cell < 0)
+                return;
+
+        /* Hide the on-screen keyboard */
+        unclear(TRUE);
+
+        /* New character */
+        if (!input || !input->len) {
+                clear_sample(&cells[current_cell].sample);
+                cells[current_cell].alts[0] = NULL;
+                input = &cells[current_cell].sample;
+                cells[current_cell].sample.ch = cells[current_cell].ch;
+        }
+
+        /* Allocate a new stroke if we aren't already drawing */
+        s = input->strokes[input->len - 1];
+        if (!drawing) {
+                if (input->len >= STROKES_MAX)
+                        return;
+                s = input->strokes[input->len++]= stroke_new(0);
+                drawing = TRUE;
+                if (input->len == 1)
+                        render_cell(current_cell);
+        }
+
+        /* Check bounds */
+        cell_coords(current_cell, &cx, &cy);
+
+        /* Normalize the input */
+        x = (x - cx - cell_width / 2) * SCALE / cell_height;
+        y = (y - cy - cell_height / 2) * SCALE / cell_height;
+
+        draw_stroke(&input->strokes[input->len - 1], x, y);
+}
+
+static void insert_cell(int cell)
+{
+        int i;
+
+        /* Find a blank to consume */
+        for (i = cell; i < cell_rows * cell_cols; i++)
+                if (!cells[i].ch)
+                        break;
+
+        /* Insert a row if necessary */
+        if (i >= cell_rows * cell_cols - 1) {
+                cells = g_realloc(cells,
+                                  ++cell_rows * cell_cols * sizeof (Cell));
+                memset(cells + (cell_rows - 1) * cell_cols, 0,
+                       cell_cols * sizeof (Cell));
+                if (cell_rows > cell_rows_pref)
+                        cell_row_view++;
+        }
+
+        if (i > cell)
+                memmove(cells + cell + 1, cells + cell,
+                        (i - cell) * sizeof (Cell));
+        cells[cell].ch = ' ';
+        cells[cell].alts[0] = NULL;
+        cells[cell].sample.len = 0;
+        cells[cell].sample.ch = 0;
+        pad_cell(cell);
+        pack_cells(0, cell_cols);
+        unclear(FALSE);
+        cell_widget_render();
+}
+
+static void delete_cell(int cell)
+{
+        int i, rows;
+
+        clear_cell(cell);
+        memmove(cells + cell, cells + cell + 1,
+                (cell_rows * cell_cols - cell - 1) * sizeof (Cell));
+
+        /* Delete a row if necessary */
+        for (i = 0; i < cell_cols &&
+             !cells[(cell_rows - 1) * cell_cols + i].ch; i++);
+        rows = cell_rows;
+        if (i == cell_cols && cell_rows > 1 &&
+            !cells[(cell_rows - 1) * cell_cols - 1].ch)
+                rows--;
+        cells[cell_rows * cell_cols - 1].ch = 0;
+        cells[cell_rows * cell_cols - 1].alts[0] = NULL;
+
+        pack_cells(0, cell_cols);
+        cell_widget_render();
+}
+
+static void send_cell_key(int cell)
+/* Send the key event for the cell */
+{
+        int i;
+
+        if (!cells[cell].ch)
+                return;
+
+        /* Collect stats and train on corrections */
+        if (cells[cell].ch != ' ') {
+                if (cells[cell].ch != cells[cell].sample.ch)
+                        corrections++;
+                if (train_on_input && !(cells[cell].flags & CELL_SHIFTED) &&
+                    cells[cell].sample.len) {
+                        cells[cell].sample.ch = cells[cell].ch;
+                        train_sample(&cells[cell].sample, FALSE);
+                }
+                characters++;
+        }
+
+        /* Update the usage time for the sample that matched this character */
+        for (i = 0; i < ALTERNATES && cells[cell].alts[i]; i++) {
+                if (!sample_valid(cells[cell].alts[i], cells[cell].alt_used[i]))
+                        break;
+                if (cells[cell].alts[i]->ch == cells[cell].ch) {
+                        promote_sample(cells[cell].alts[i]);
+                        break;
+                }
+                demote_sample(cells[cell].alts[i]);
+        }
+
+        key_event_send_char(cells[cell].ch);
+}
+
+/*
+        Events
+*/
+
+/* Hold click area radius */
+#define HOLD_CLICK_WIDTH 3.
+
+/* Mask for possible buttons used by the eraser */
+#define ERASER_BUTTON_MASK (GDK_MOD5_MASK | GDK_BUTTON1_MASK | \
+                            GDK_BUTTON2_MASK | GDK_BUTTON3_MASK | \
+                            GDK_BUTTON4_MASK | GDK_BUTTON5_MASK)
+
+static int menu_cell, alt_menu_alts[ALTERNATES];
+
+static void training_menu_reset(void)
+{
+        untrain_char(cells[menu_cell].ch);
+        render_cell(menu_cell);
+}
+
+static void alt_menu_selection_done(GtkWidget *widget)
+{
+        gtk_widget_destroy(widget);
+}
+
+static void alt_menu_activate(GtkWidget *widget, int *alt)
+{
+        cells[menu_cell].ch = *alt;
+        cells[menu_cell].flags |= CELL_VERIFIED;
+        cells[menu_cell].flags &= ~CELL_SHIFTED;
+        render_cell(menu_cell);
+}
+
+static void alt_menu_delete(void)
+{
+        delete_cell(menu_cell);
+}
+
+static void alt_menu_show_ink(void)
+{
+        cells[menu_cell].flags ^= CELL_SHOW_INK;
+        render_cell(menu_cell);
+}
+
+static void alt_menu_change_case(void)
+{
+        if (g_unichar_isupper(cells[menu_cell].ch)) {
+                cells[menu_cell].ch = g_unichar_tolower(cells[menu_cell].ch);
+                cells[menu_cell].flags |= CELL_SHIFTED;
+                render_cell(menu_cell);
+        } else if (g_unichar_islower(cells[menu_cell].ch)) {
+                cells[menu_cell].ch = g_unichar_toupper(cells[menu_cell].ch);
+                cells[menu_cell].flags |= CELL_SHIFTED;
+                render_cell(menu_cell);
+        } else
+                g_debug("Cannot change case, not an alphabetic character");
+}
+
+static gboolean scrollbar_scroll_event(GtkWidget *widget, GdkEventScroll *event)
+{
+        check_cell(event->x, event->y, event->device);
+        return FALSE;
+}
+
+static gboolean scroll_event(GtkWidget *widget, GdkEventScroll *event)
+{
+        if (scrollbar && GTK_WIDGET_VISIBLE(scrollbar))
+                gtk_widget_event(scrollbar, (GdkEvent*)event);
+        return FALSE;
+}
+
+static void context_menu_position(GtkMenu *menu, gint *x, gint *y,
+                                  gboolean *push_in)
+/* Positions the two-column context menu so that the column divide is at
+   the cursor rather than the upper left hand point */
+{
+        if (cells[menu_cell].alts[0])
+                *x -= GTK_WIDGET(menu)->requisition.width / 2;
+        *push_in = TRUE;
+}
+
+static void show_context_menu(int button, int time)
+/* Popup the cell context menu for the current cell */
+{
+        GtkWidget *menu, *widget;
+        int i, pos;
+
+        /* Training menu is the same for all cells */
+        if (training) {
+                if (!char_trained(cells[current_cell].ch))
+                        return;
+                menu_cell = current_cell;
+                gtk_menu_popup(GTK_MENU(training_menu), 0, 0, 0, 0,
+                               button, time);
+                return;
+        }
+
+        /* Can't delete blanks */
+        if (!cells[current_cell].ch)
+                return;
+
+        /* Construct an alternates menu for the current button */
+        menu = gtk_menu_new();
+        menu_cell = current_cell;
+
+        /* Menu -> Delete */
+        widget = gtk_menu_item_new_with_label("Delete");
+        g_signal_connect(G_OBJECT(widget), "activate",
+                         G_CALLBACK(alt_menu_delete), NULL);
+        gtk_menu_attach(GTK_MENU(menu), widget, 0, 1, 0, 1);
+
+        /* Menu -> Show Ink */
+        if (cells[menu_cell].sample.ch) {
+                const char *label;
+
+                label = cells[menu_cell].flags & CELL_SHOW_INK ?
+                        "Hide ink" : "Show ink";
+                widget = gtk_menu_item_new_with_label(label);
+                g_signal_connect(G_OBJECT(widget), "activate",
+                                 G_CALLBACK(alt_menu_show_ink), NULL);
+                gtk_menu_attach(GTK_MENU(menu), widget, 0, 1, 1, 2);
+        }
+
+        /* Menu -> Change case */
+        if (g_unichar_isupper(cells[menu_cell].ch) ||
+                g_unichar_islower(cells[menu_cell].ch)) {
+                const char *string = "To upper";
+
+                if (g_unichar_isupper(cells[menu_cell].ch))
+                        string = "To lower";
+                widget = gtk_menu_item_new_with_label(string);
+                g_signal_connect(G_OBJECT(widget), "activate",
+                                 G_CALLBACK(alt_menu_change_case), NULL);
+                gtk_menu_attach(GTK_MENU(menu), widget, 0, 1, 2, 3);
+        }
+
+        /* Menu -> Alternates */
+        for (i = 0, pos = 0; i < ALTERNATES &&
+                cells[current_cell].alts[i]; i++) {
+                char *str;
+
+                if (!sample_valid(cells[current_cell].alts[i],
+                                  cells[current_cell].alt_used[i]))
+                        continue;
+                str = va("%C\t%d%%", cells[current_cell].alts[i]->ch,
+                         cells[current_cell].alt_ratings[i]);
+                alt_menu_alts[i] = cells[current_cell].alts[i]->ch;
+                widget = gtk_check_menu_item_new_with_label(str);
+                if (cells[current_cell].ch == cells[current_cell].alts[i]->ch)
+                        gtk_check_menu_item_set_active(
+                                             GTK_CHECK_MENU_ITEM(widget), TRUE);
+                g_signal_connect(G_OBJECT(widget), "activate",
+                                 G_CALLBACK(alt_menu_activate),
+                                 alt_menu_alts + i);
+                gtk_menu_attach(GTK_MENU(menu), widget, 1, 2, pos, pos + 1);
+                pos++;
+        }
+        g_signal_connect(G_OBJECT(menu), "selection-done",
+                         G_CALLBACK(alt_menu_selection_done), NULL);
+        gtk_widget_show_all(menu);
+        gtk_menu_popup(GTK_MENU(menu), 0, 0,
+                       (GtkMenuPositionFunc)context_menu_position,
+                       0, button, time);
+
+}
+
+static gboolean button_press_event(GtkWidget *widget, GdkEventButton *event)
+/* Mouse button is pressed over drawing area */
+{
+        /* Don't process double clicks */
+        if (event->type != GDK_BUTTON_PRESS)
+                return TRUE;
+
+        /* Check validity every time */
+        check_cell(event->x, event->y, event->device);
+        if (invalid)
+                return TRUE;
+
+        /* If we are drawing and we get a button press event it is possible
+           that we never received a button release event for some reason.
+           This is a fix for Zaurus drawing connected lines. */
+        if (drawing)
+                stop_drawing();
+
+        /* If we have pressed with the eraser, erase the cell */
+        if (eraser || event->button == 2) {
+                erase_cell(current_cell);
+                return TRUE;
+        }
+
+        /* Draw/activate insert with left click */
+        if (event->button == 1) {
+                if (inserting)
+                        potential_insert = TRUE;
+                else if (cells[current_cell].ch) {
+                        start_hold();
+                } else
+                        draw(event->x, event->y);
+
+                /* We are now counting on getting valid coordinates here so
+                   save in case we are doing a potential insert/hold and we
+                   don't get a motion event in between */
+                cursor_x = event->x;
+                cursor_y = event->y;
+
+                return TRUE;
+        }
+
+        /* Right-click opens context menu */
+        else if (event->button == 3 && current_cell >= 0 && !inserting &&
+                 (!input || !input->len)) {
+                show_context_menu(event->button, event->time);
+                return TRUE;
+        }
+
+        return FALSE;
+}
+
+static gboolean button_release_event(GtkWidget *widget, GdkEventButton *event)
+/* Mouse button is released over drawing area */
+{
+        /* Only handle left-clicks */
+        if (event->button != 1)
+                return TRUE;
+
+        /* Complete an insertion */
+        if (potential_insert && inserting) {
+                insert_cell(current_cell);
+                potential_insert = FALSE;
+                return TRUE;
+        }
+
+        /* Cancel a hold-click */
+        if (potential_hold) {
+                potential_hold = FALSE;
+                draw(cursor_x, cursor_y);
+        }
+
+        stop_drawing();
+        return TRUE;
+}
+
+static gboolean motion_notify_event(GtkWidget *widget, GdkEventMotion *event)
+/* Mouse is moved over drawing area */
+{
+        GdkModifierType state;
+        double x, y;
+
+        /* Fetch event coordinates */
+        x = event->x;
+        y = event->y;
+        if (xinput_enabled) {
+                gdk_device_get_state(event->device, event->window, NULL,
+                                     &state);
+                gdk_event_get_coords((GdkEvent*)event, &x, &y);
+        }
+
+#if GTK_CHECK_VERSION(2, 12, 0)
+        /* Process a hint event (GTK >= 2.12) */
+        gdk_event_request_motions(event);
+#else
+        /* Process a hint event (GTK <= 2.10) */
+        else if (event->is_hint) {
+                int nx, ny;
+
+                gdk_window_get_pointer(event->window, &nx, &ny, &state);
+                x = nx;
+                y = ny;
+        }
+#endif
+
+        /* If we are getting invalid output from this device with XInput
+           enabled, try disabling it */
+        if ((x < 0 || x > drawing_area->allocation.width ||
+             y < 0 || y > drawing_area->allocation.width) &&
+            event->device->mode != GDK_MODE_DISABLED && xinput_enabled) {
+                g_warning("Extended input device is generating invalid "
+                          "coordinates, disabled");
+                gdk_device_set_mode(event->device, GDK_MODE_DISABLED);
+                return TRUE;
+        }
+
+        /* Check where the pointer is */
+        check_cell(x, y, event->device);
+
+        /* Cancel a potential insert */
+        if (potential_insert) {
+                if (!inserting) {
+                        potential_insert = FALSE;
+                        draw(cursor_x, cursor_y);
+                } else
+                        return TRUE;
+        }
+
+        /* Cancel a potential hold-click */
+        if (potential_hold) {
+                double dx, dy;
+
+                dx = x - cursor_x;
+                dy = y - cursor_y;
+                if (dx < -HOLD_CLICK_WIDTH || dx > HOLD_CLICK_WIDTH ||
+                    dy < -HOLD_CLICK_WIDTH || dy > HOLD_CLICK_WIDTH) {
+                        potential_hold = FALSE;
+                        draw(cursor_x, cursor_y);
+                } else
+                        return TRUE;
+        }
+
+        cursor_x = x;
+        cursor_y = y;
+
+        /* Record and draw new segment */
+        if (drawing) {
+                draw(cursor_x, cursor_y);
+                render_segment(input, current_cell, input->len - 1,
+                               input->strokes[input->len - 1]->len - 2, NULL);
+        }
+
+        /* Erasing with the eraser. We get MOD5 rather than a button for the
+           eraser being pressed on a Tablet PC. */
+        else if (!invalid &&
+                 (cross_out || (eraser && (state & ERASER_BUTTON_MASK))))
+                erase_cell(current_cell);
+
+        /* Plain motion restarts the finish countdown */
+        start_timeout();
+
+        return TRUE;
+}
+
+static void configure_keys(void)
+{
+}
+
+static gboolean configure_event(void)
+/* Create a new backing pixmap of the appropriate size */
+{
+        int new_cols;
+
+        /* Do nothing if we are not visible */
+        if (!drawing_area || !drawing_area->window ||
+            !GTK_WIDGET_VISIBLE(drawing_area))
+                return TRUE;
+
+        /* Backing pixmap */
+        if (pixmap) {
+                int old_width, old_height;
+
+                //return TRUE;
+
+                g_object_unref(pixmap);
+        }
+        pixmap = gdk_pixmap_new(drawing_area->window,
+                                drawing_area->allocation.width,
+                                drawing_area->allocation.height, -1);
+        trace("%dx%d", drawing_area->allocation.width,
+              drawing_area->allocation.height);
+
+        /* GDK graphics context */
+        if (pixmap_gc)
+                g_object_unref(pixmap_gc);
+        pixmap_gc = gdk_gc_new(GDK_DRAWABLE(pixmap));
+
+        /* Cairo context */
+        if (cairo)
+                cairo_destroy(cairo);
+        cairo = gdk_cairo_create(GDK_DRAWABLE(pixmap));
+
+        /* Set font size */
+        pango_font_description_set_absolute_size(pango_font_desc, PANGO_SCALE *
+                                                 (cell_height -
+                                                  CELL_BASELINE - 2));
+
+        /* Get the background color */
+        color_bg = window->style->bg[0];
+        color_bg_dark = window->style->bg[1];
+
+        /* Cursor */
+        cell_widget_set_cursor(TRUE);
+
+        /* If the cell dimensions changed, repack */
+        if (window_embedded) {
+                new_cols = (drawing_area->allocation.width -
+                            cell_widget_scrollbar_width() - 6) / cell_width;
+                if (new_cols != cell_cols)
+                        pack_cells(1, new_cols);
+        }
+
+        /* If we are embedded we won't be able to resize the window so we
+           can't honor the maximum rows preference */
+        if (window_embedded)
+                cell_rows_pref = drawing_area->allocation.height / cell_height;
+
+        /* Update the key widget with new values */
+        configure_keys();
+
+        /* Render the cells */
+        cell_widget_render();
+
+        return TRUE;
+}
+
+static gboolean expose_event(GtkWidget *widget, GdkEventExpose *event)
+/* Redraw the drawing area from the backing pixmap */
+{
+        if (!pixmap)
+                return FALSE;
+        gdk_draw_drawable(widget->window,
+                          widget->style->fg_gc[GTK_WIDGET_STATE(widget)],
+                          pixmap, event->area.x, event->area.y, event->area.x,
+                          event->area.y, event->area.width, event->area.height);
+        return FALSE;
+}
+
+static gboolean enter_notify_event(GtkWidget *widget, GdkEventCrossing *event)
+{
+        check_cell(event->x, event->y, NULL);
+        return FALSE;
+}
+
+static gboolean leave_notify_event(GtkWidget *widget, GdkEventCrossing *event)
+{
+        /* Tablet PC gets grab leave-notify event when starting to draw.
+           Ignore this if we are still drawing. */
+        if (event->mode == GDK_CROSSING_GRAB || drawing || cross_out)
+                return FALSE;
+
+        old_cc = current_cell;
+        current_cell = -1;
+        finish_cell(old_cc);
+        if (inserting) {
+                inserting = FALSE;
+                dirty_cell(old_cc);
+                dirty_cell(old_cc - 1);
+        }
+        invalid = TRUE;
+        cell_widget_set_cursor(FALSE);
+        render_dirty();
+        start_timeout();
+        return FALSE;
+}
+
+static void scrollbar_value_changed(void)
+/* The cell widget has been scrolled */
+{
+        double value;
+
+        value = gtk_range_get_value(GTK_RANGE(scrollbar));
+        if ((int)value == cell_row_view)
+                return;
+        cell_row_view = value;
+        cell_widget_render();
+}
+
+/*
+        Widget
+*/
+
+void cell_widget_enable_xinput(int on)
+/* Enable Xinput devices. We set everything to screen mode despite the fact
+   that we actually want window coordinates. Window mode just seems to break
+   everything and we get window coords with screen mode anyway! */
+{
+        GList *list;
+        GdkDevice *device;
+        int i, mode;
+
+        gtk_widget_set_extension_events(drawing_area,
+                                        on ? GDK_EXTENSION_EVENTS_ALL :
+                                             GDK_EXTENSION_EVENTS_NONE);
+        mode = on ? GDK_MODE_SCREEN : GDK_MODE_DISABLED;
+        list = gdk_devices_list();
+        for (i = 0; (device = (GdkDevice*)g_list_nth_data(list, i)); i++)
+                gdk_device_set_mode(device, mode);
+        xinput_enabled = on;
+        g_debug(on ? "Xinput events enabled" : "Xinput events disabled");
+}
+
+int cell_widget_update_colors(void)
+{
+        GdkColor old_active, old_inactive, old_ink, old_select;
+
+        old_active = color_active;
+        old_inactive = color_inactive;
+        old_ink = color_ink;
+        old_select = color_select;
+        color_active = custom_active_color;
+        color_inactive = custom_inactive_color;
+        color_ink = custom_ink_color;
+        color_select = custom_select_color;
+        if (style_colors) {
+                color_active = window->style->base[0];
+                color_ink = window->style->text[0];
+                color_inactive = window->style->bg[1];
+        }
+        return !gdk_colors_equal(&old_active, &color_active) ||
+               !gdk_colors_equal(&old_inactive, &color_inactive) ||
+               !gdk_colors_equal(&old_ink, &color_ink) ||
+               !gdk_colors_equal(&old_select, &color_select);
+}
+
+const char *cell_widget_word(void)
+/* Return the current word and the current cell's position in that word
+   FIXME this function ignores wide chars */
+{
+        static char buf[64];
+        int i, min, max;
+
+        memset(buf, 0, sizeof (buf));
+        if (cell_offscreen(old_cc))
+                return buf;
+
+        /* Find the start of the word */
+        for (min = old_cc - 1; min >= 0 && cells[min].ch &&
+             g_ascii_isalnum(cells[min].ch) && cells[min].ch < 0x7f; min--);
+
+        /* Find the end of the word */
+        for (max = old_cc + 1; max < cell_rows * cell_cols && cells[max].ch &&
+             g_ascii_isalnum(cells[max].ch) && cells[max].ch < 0x7f; max++);
+
+        /* Copy the word to a buffer */
+        for (++min, i = 0; i < max - min && i < (int)sizeof (buf) - 1; i++)
+                buf[i] = cells[min + i].ch;
+        buf[old_cc - min] = 0;
+        buf[i] = 0;
+
+        return buf;
+}
+
+void cell_widget_clear(void)
+{
+        int resized;
+
+        stop_timeout();
+        free_cells();
+
+        /* Restore cells if we just finished training */
+        if (training) {
+                cells = cells_saved;
+                cell_rows = cell_rows_saved;
+                cell_cols = cell_cols_saved;
+                cell_row_view = cell_row_view_saved;
+                training = FALSE;
+                resized = pack_cells(cell_rows, cell_cols);
+
+                /* Show the on-screen keyboard */
+                if (check_clear()) {
+                        show_keys = keyboard_enabled;
+                        is_clear = TRUE;
+                }
+        }
+
+        /* Clear cells otherwise */
+        else {
+                resized = pack_cells(1, cell_cols);
+
+                /* Show the on-screen keyboard */
+                show_keys = keyboard_enabled;
+                is_clear = TRUE;
+        }
+
+        /* Only re-render when we aren't going to get a configure event */
+        if (!resized)
+                cell_widget_render();
+}
+
+void cell_widget_train(void)
+{
+        UnicodeBlock *block;
+        int i, pos, range;
+
+        stop_timeout();
+
+        /* Save cells */
+        if (!training) {
+                cells_saved = cells;
+                cell_rows_saved = cell_rows;
+                cell_cols_saved = cell_cols;
+                cell_row_view_saved = cell_row_view;
+                cells = NULL;
+                cell_row_view = 0;
+        }
+
+        /* Clear if not training any block */
+        if (training_block < 0) {
+                free_cells();
+                pack_cells(1, cell_cols);
+                cell_widget_render();
+                return;
+        }
+
+        /* Pack the Unicode block's characters into the cell grid */
+        block = unicode_blocks + training_block;
+        range = block->end - block->start + 1;
+        training = TRUE;
+        pack_cells((range + cell_cols - 1) / cell_cols, cell_cols);
+
+        /* Preset all of the characters for training */
+        for (i = 0, pos = 0; i < range; i++) {
+                short ch;
+
+                ch = block->start + i;
+                if (char_disabled(ch))
+                        continue;
+                cells[pos].ch = ch;
+                cells[pos].alts[0] = NULL;
+                cells[pos++].flags = 0;
+        }
+        range = pos;
+        for (; pos < cell_rows * cell_cols; pos++)
+                clear_cell(pos);
+        pack_cells(1, cell_cols);
+
+        unclear(FALSE);
+        cell_widget_render();
+}
+
+void cell_widget_load_string(const gchar *str){
+
+  int range = strlen(str);
+  int i;
+
+        pack_cells((range + cell_cols - 1) / cell_cols, cell_cols);
+
+        /* Preset all of the characters for training */
+        for (i = 0; i < range; i++) {
+          cells[i].ch = str[i]; // todo: support utf-8
+          cells[i].alts[0] = NULL;
+          cells[i].flags = 0;
+        }
+        for (; i < cell_rows * cell_cols; i++)
+                clear_cell(i);
+        pack_cells(1, cell_cols);
+
+        cell_widget_render();
+
+}
+
+void cell_widget_pack(void)
+{
+        int cols;
+
+        if (training) {
+                cell_widget_train();
+                return;
+        }
+        cols = cell_cols_pref;
+        if (window_docked) {
+                GdkScreen *screen;
+
+                screen = gtk_window_get_screen(GTK_WINDOW(window));
+                cols = (gdk_screen_get_width(screen) -
+                        cell_widget_scrollbar_width() - 6) / cell_width;
+        }
+        if (!pack_cells(0, cols))
+                set_size_request(TRUE);
+        if (is_clear)
+                show_keys = keyboard_enabled;
+
+        /* Right-to-left mode may have changed so we need to reconfigure the
+           on-screen keyboard */
+        configure_keys();
+
+        cell_widget_render();
+        trace("%dx%d, scrollbar %d",
+               cell_cols, cell_rows, cell_widget_scrollbar_width());
+}
+
+
+
+int cell_widget_insert(void)
+{
+        gunichar2 *utf16;
+        int i, j, slot, chars;
+
+        if (training)
+                return FALSE;
+        chars = 0;
+
+        /* Prepare for sending key events */
+        //key_event_update_mappings();
+
+        /* Need to send the keys out in reverse order for right_to_left mode
+           because the cells are displayed with columns reversed */
+        if (right_to_left)
+                for (i = cell_cols - 1; i < cell_rows * cell_cols; i--) {
+                        if (cells[i].ch) {
+                                chars++;
+                                send_cell_key(i);
+                        }
+                        if (i % cell_cols == 0)
+                                i += cell_cols * 2;
+                }
+
+        else
+                for (i = 0; i < cell_rows * cell_cols; i++) {
+                        if (!cells[i].ch)
+                                continue;
+                        chars++;
+                        send_cell_key(i);
+                }
+
+        /* If nothing was entered, send Enter key event */
+        if (!chars) {
+                key_event_send_enter();
+                return FALSE;
+        }
+
+        /* Create a UTF-16 string representation */
+        utf16 = g_malloc(sizeof (**history) * (chars + 1));
+        for (i = 0, j = 0; i < cell_rows * cell_cols; i++)
+                if (cells[i].ch)
+                        utf16[j++] = cells[i].ch;
+        utf16[j] = 0;
+
+        /* If this text has been entered before, consume that history slot */
+        slot = HISTORY_MAX - 1;
+        for (i = 0; i < slot && history[i]; i++)
+                for (j = 0; history[i][j] == utf16[j]; j++)
+                        if (!utf16[j]) {
+                                slot = i;
+                                break;
+                        }
+
+        /* Save entered text to history */
+        g_free(history[slot]);
+        memmove(history + 1, history, sizeof (*history) * slot);
+        history[0] = utf16;
+
+        cell_widget_clear();
+        return TRUE;
+}
+
+static void buffer_menu_deactivate(GtkMenuShell *shell, GtkWidget *button)
+{
+        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), FALSE);
+}
+
+static void buffer_menu_item_activate(GtkWidget *widget, gunichar2 *history)
+{
+        int i;
+
+        stop_timeout();
+        free_cells();
+        for (i = 0; history[i]; i++);
+        cell_rows = i / cell_cols + 1;
+        cell_cols = cell_cols;
+        cells = g_malloc0(sizeof (*cells) * cell_cols * cell_rows);
+        for (i = 0; history[i]; i++)
+                cells[i].ch = history[i];
+        pack_cells(cell_rows, cell_cols);
+        unclear(TRUE);
+}
+
+static void buffer_menu_item_destroy(GtkWidget *widget, gchar *string)
+{
+        g_free(string);
+}
+
+static void buffer_menu_position_func(GtkMenu *menu, gint *x, gint *y,
+                                      gboolean *push_in, GtkWidget *button)
+{
+        gdk_window_get_origin(button->window, x, y);
+        *x += button->allocation.x + button->allocation.width -
+              GTK_WIDGET(menu)->requisition.width;
+        *y += button->allocation.y + button->allocation.height;
+        *push_in = TRUE;
+}
+
+void cell_widget_show_buffer(GtkWidget *button)
+/* Show input back buffer menu */
+{
+        static GtkWidget *menu;
+        int i;
+
+        if (menu)
+                gtk_widget_destroy(GTK_WIDGET(menu));
+        menu = gtk_menu_new();
+        g_signal_connect(G_OBJECT(menu), "deactivate",
+                         G_CALLBACK(buffer_menu_deactivate), button);
+        for (i = 0; history[i] && i < HISTORY_MAX; i++) {
+                GtkWidget *item;
+                GError *error = NULL;
+                gchar *string;
+
+                /* Convert string from a UTF-16 array to displayable UTF-8 */
+                string = g_utf16_to_utf8(history[i], -1, NULL, NULL, &error);
+                if (error) {
+                        g_warning("g_utf16_to_utf8(): %s", error->message);
+                        continue;
+                }
+
+                /* Reverse the displayed string for right-to-left mode */
+                if (right_to_left) {
+                        gchar *reversed;
+
+                        reversed = g_utf8_strreverse(string, -1);
+                        g_free(string);
+                        string = reversed;
+                }
+
+                /* Create menu item */
+                item = gtk_menu_item_new_with_label(string);
+                g_signal_connect(G_OBJECT(item), "destroy",
+                                 G_CALLBACK(buffer_menu_item_destroy), string);
+                g_signal_connect(G_OBJECT(item), "activate",
+                                 G_CALLBACK(buffer_menu_item_activate),
+                                 history[i]);
+                gtk_menu_attach(GTK_MENU(menu), item, 0, 1, i, i + 1);
+        }
+
+        /* Show back buffer menu */
+        gtk_widget_show_all(menu);
+        gtk_menu_popup(GTK_MENU(menu), NULL, NULL,
+                       (GtkMenuPositionFunc)buffer_menu_position_func,
+                       button, 0, gtk_get_current_event_time());
+}
+
+int cell_widget_scrollbar_width(void)
+/* Gets the width of the scrollbar even if it is hidden */
+{
+        GtkRequisition requisition;
+
+        if (scrollbar->requisition.width <= 1) {
+                gtk_widget_size_request(scrollbar, &requisition);
+                return requisition.width;
+        }
+        return scrollbar->requisition.width + 4;
+}
+
+int cell_widget_get_height(void)
+{
+        int rows;
+
+        rows = cell_rows > cell_rows_pref ? cell_rows_pref : cell_rows;
+        return rows * cell_height + 2;
+}
+
+GtkWidget *cell_widget_new(void)
+/* Creates the Cell widget. Should only be called once per program run! */
+{
+        PangoCairoFontMap *font_map;
+        GtkWidget *widget, *hbox;
+
+        /* Initial settings */
+        cell_cols = cell_cols_pref;
+
+        /* Create drawing area */
+        drawing_area = gtk_drawing_area_new();
+        g_signal_connect(G_OBJECT(drawing_area), "expose_event",
+                         G_CALLBACK(expose_event), NULL);
+        g_signal_connect(G_OBJECT(drawing_area), "configure_event",
+                         G_CALLBACK(configure_event), NULL);
+        g_signal_connect(G_OBJECT(drawing_area), "show",
+                         G_CALLBACK(configure_event), NULL);
+        g_signal_connect(G_OBJECT(drawing_area), "button_press_event",
+                         G_CALLBACK(button_press_event), NULL);
+        g_signal_connect(G_OBJECT(drawing_area), "button_release_event",
+                         G_CALLBACK(button_release_event), NULL);
+        g_signal_connect(G_OBJECT(drawing_area), "motion_notify_event",
+                         G_CALLBACK(motion_notify_event), NULL);
+        g_signal_connect(G_OBJECT(drawing_area), "enter_notify_event",
+                         G_CALLBACK(enter_notify_event), NULL);
+        g_signal_connect(G_OBJECT(drawing_area), "leave_notify_event",
+                         G_CALLBACK(leave_notify_event), NULL);
+        g_signal_connect(G_OBJECT(drawing_area), "scroll_event",
+                         G_CALLBACK(scroll_event), NULL);
+        g_signal_connect(G_OBJECT(drawing_area), "style-set",
+                         G_CALLBACK(cell_widget_update_colors), NULL);
+        gtk_widget_set_events(drawing_area,
+                              GDK_EXPOSURE_MASK |
+                              GDK_BUTTON_PRESS_MASK |
+                              GDK_BUTTON_RELEASE_MASK |
+                              GDK_POINTER_MOTION_MASK |
+                              GDK_POINTER_MOTION_HINT_MASK |
+                              GDK_ENTER_NOTIFY_MASK |
+                              GDK_LEAVE_NOTIFY_MASK |
+                              GDK_SCROLL_MASK);
+
+        /* Update colors */
+        cell_widget_update_colors();
+
+        /* Create training menu */
+        training_menu = gtk_menu_new();
+        widget = gtk_menu_item_new_with_label("Reset");
+        g_signal_connect(G_OBJECT(widget), "activate",
+                         G_CALLBACK(training_menu_reset), NULL);
+        gtk_menu_attach(GTK_MENU(training_menu), widget, 0, 1, 0, 1);
+        gtk_widget_show_all(training_menu);
+
+        /* Create scroll bar */
+        scrollbar = gtk_vscrollbar_new(NULL);
+        gtk_widget_set_no_show_all(scrollbar, TRUE);
+        g_signal_connect(G_OBJECT(scrollbar), "value-changed",
+                         G_CALLBACK(scrollbar_value_changed), NULL);
+        g_signal_connect(G_OBJECT(scrollbar), "scroll_event",
+                         G_CALLBACK(scrollbar_scroll_event), NULL);
+        gtk_widget_add_events(drawing_area, GDK_SCROLL_MASK);
+
+        /* Box container */
+        hbox = gtk_hbox_new(FALSE, 0);
+        gtk_box_pack_start(GTK_BOX(hbox), drawing_area, TRUE, TRUE, 2);
+        gtk_box_pack_start(GTK_BOX(hbox), scrollbar, FALSE, FALSE, 2);
+
+        /* Create Pango font description
+           FIXME font characteristics, not family */
+        pango_font_desc = pango_font_description_new();
+        pango_font_description_set_family(pango_font_desc, "Monospace");
+
+        /* Pango context */
+        font_map = PANGO_CAIRO_FONT_MAP(pango_cairo_font_map_new());
+        pango = pango_cairo_font_map_create_context(font_map);
+        g_object_unref(font_map);
+
+        /* Clear cells */
+        cell_widget_clear();
+
+        /* Set Xinput state */
+        cell_widget_enable_xinput(xinput_enabled);
+
+        /* Clear history */
+        memset(history, 0, sizeof (history));
+
+        return hbox;
+}
+
+void cell_widget_cleanup(void)
+{
+        /* Freeing memory when closing is important when trying to sort
+           legitimate memory leaks from left-over memory */
+        if (pixmap)
+                g_object_unref(pixmap);
+        if (pixmap_gc)
+                g_object_unref(pixmap_gc);
+        if (cairo)
+                cairo_destroy(cairo);
+        if (pango)
+                g_object_unref(pango);
+}
+
+extern HildonIMUI *ui;
+
+void unicode_to_utf8(unsigned int code, char *out);
+void cell_widget_insert_surrounding_string(){
+  int i;
+
+  gchar *str = malloc(cell_cols * cell_rows * 2 + 1);
+  gchar *s = str;
+  *s = 0;
+  for(i = 0; i < cell_cols * cell_rows; i++){
+    if(!(cells[i].flags & CELL_DIRTY)){
+      unicode_to_utf8(cells[i].ch, s);
+      s += strlen(s);
+    }
+  }
+
+  hildon_im_ui_send_surrounding_content(ui, str);
+}
diff --git a/src/common.h b/src/common.h
new file mode 100644 (file)
index 0000000..ff28a7d
--- /dev/null
@@ -0,0 +1,292 @@
+
+/*
+
+cellwriter -- a character recognition input method
+Copyright (C) 2007 Michael Levin <risujin@risujin.org>
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+*/
+
+#include <gtk/gtk.h>
+#include <math.h>
+
+/*
+        Limits
+*/
+
+#define HISTORY_MAX 8
+#define KEYBOARD_SIZE_MIN 480
+
+/*
+        Single instance protection
+*/
+
+typedef void (*SingleInstanceFunc)(const char *msg);
+
+int single_instance_init(SingleInstanceFunc callback, const char *str);
+void single_instance_cleanup(void);
+
+/*
+        Unicode blocks
+*/
+
+typedef struct {
+        short enabled;
+        const int start, end;
+        const char *name;
+} UnicodeBlock;
+
+extern UnicodeBlock unicode_blocks[];
+
+/*
+        Profile
+*/
+
+extern int profile_line, profile_read_only;
+
+const char *profile_read(void);
+int profile_write(const char *str);
+int profile_sync_int(int *var);
+int profile_sync_short(short *var);
+
+/*
+        Window
+*/
+
+enum {
+        WINDOW_UNDOCKED = 0,
+        WINDOW_DOCKED_TOP,
+        WINDOW_DOCKED_BOTTOM,
+};
+
+extern GtkWidget *window;
+extern GtkTooltips *tooltips;
+extern int window_force_show, window_force_hide, window_force_x, window_force_y,
+           window_force_docked, window_struts,
+           window_embedded, window_button_labels, window_show_info,
+           window_docked, style_colors;
+
+void window_create(GtkWidget *parent);
+void window_sync(void);
+void window_cleanup(void);
+void window_show(void);
+void window_hide(void);
+void window_toggle(void);
+void window_pack(void);
+void window_update_colors(void);
+void window_set_docked(int mode);
+void unicode_block_toggle(int block, int on);
+void blocks_sync(void);
+void startup_splash_show(void);
+
+
+void read_profile();
+
+/*
+        GTK/GDK/Glib specific
+*/
+
+/* Multiply to convert RGB to GDK color */
+#define COLOR_SCALE 256
+
+/* Constants may not have been defined if GLib is not included */
+#ifndef TRUE
+#define TRUE 1
+#endif
+#ifndef FALSE
+#define FALSE 0
+#endif
+#ifndef NULL
+#define NULL ((void*)0)
+#endif
+
+/* A macro used to initialize GdkColor with RGB values */
+#define RGB_TO_GDKCOLOR(r, g, b) {0, (r) * 256, (g) * 256, (b) * 256 }
+
+static inline void cairo_set_source_gdk_color(cairo_t *cairo,
+                                              const GdkColor *color,
+                                              double alpha)
+/* Set the cairo source color from a GdkColor */
+{
+        cairo_set_source_rgba(cairo, color->red / 65535.,
+                                     color->green / 65535.,
+                                     color->blue / 65535., alpha);
+}
+
+static inline void cairo_pattern_add_gdk_color_stop(cairo_pattern_t *pattern,
+                                                    double offset,
+                                                    GdkColor *color,
+                                                    double alpha)
+/* Add a GdkColor color stop to a cairo pattern */
+{
+        cairo_pattern_add_color_stop_rgba(pattern, offset,
+                                          color->red / 65535.,
+                                          color->green / 65535.,
+                                          color->blue / 65535., alpha);
+}
+
+static inline int gdk_colors_equal(GdkColor *a, GdkColor *b)
+/* Check if two GdkColor structures are equal */
+{
+        return a->red == b->red && a->green == b->green && a->blue == b->blue;
+}
+
+void highlight_gdk_color(const GdkColor *base, GdkColor *out, double value);
+void scale_gdk_color(const GdkColor *base, GdkColor *out, double value);
+void shade_gdk_color(const GdkColor *base, GdkColor *out, double value);
+void gdk_color_to_hsl(const GdkColor *src,
+                      double *hue, double *sat, double *lit);
+void hsl_to_gdk_color(GdkColor *src, double hue, double sat, double lit);
+
+/*
+        Error logging and variable argument parsing
+*/
+
+/* Function traces */
+#define LOG_LEVEL_TRACE (G_LOG_LEVEL_DEBUG << 1)
+#define trace(...) trace_full(__FILE__, __FUNCTION__, __VA_ARGS__)
+
+/* Log detail level */
+extern int log_level;
+
+#ifdef _EFISTDARG_H_
+char *nvav(int *plen, const char *format, va_list va);
+#endif
+char *nva(int *length, const char *format, ...);
+char *va(const char *format, ...);
+void log_errno(const char *message);
+void log_print(const char *format, ...);
+void trace_full(const char *file, const char *func, const char *fmt, ...);
+
+/*
+        Angles
+*/
+
+/* Size of the ANGLE data type in bytes */
+#define ANGLE_SIZE 2
+
+#if (ANGLE_SIZE == 4)
+
+/* High-precision angle type */
+typedef int ANGLE;
+#define ANGLE_PI 2147483648
+
+#elif (ANGLE_SIZE == 2)
+
+/* Medium-precision angle type */
+typedef short ANGLE;
+#define ANGLE_PI 32768
+
+#else
+
+/* Low-precision angle type */
+typedef signed char ANGLE;
+#define ANGLE_PI 128
+
+#endif
+
+/*
+        2D Vector
+*/
+
+typedef struct Vec2 {
+       float x, y;
+} Vec2;
+
+static inline void vec2_set(Vec2 *dest, float x, float y)
+{
+       dest->x = x;
+       dest->y = y;
+}
+#define vec2_from_coords vec2_set
+
+static inline void vec2_copy(Vec2 *dest, const Vec2 *src)
+{
+       dest->x = src->x;
+       dest->y = src->y;
+}
+
+static inline void vec2_sub(Vec2 *dest, const Vec2 *a, const Vec2 *b)
+{
+       dest->x = a->x - b->x;
+       dest->y = a->y - b->y;
+}
+
+static inline void vec2_sum(Vec2 *dest, const Vec2 *a, const Vec2 *b)
+{
+       dest->x = a->x + b->x;
+       dest->y = a->y + b->y;
+}
+
+static inline float vec2_dot(const Vec2 *a, const Vec2 *b)
+{
+       return a->x * b->x + a->y * b->y;
+}
+
+static inline float vec2_cross(const Vec2 *a, const Vec2 *b)
+{
+       return a->y * b->x - b->y * a->x;
+}
+
+static inline void vec2_scale(Vec2 *dest, const Vec2 *src, float scale)
+{
+       dest->x = src->x * scale;
+       dest->y = src->y * scale;
+}
+
+static inline void vec2_avg(Vec2 *dest, const Vec2 *a, const Vec2 *b,
+                           float scale)
+{
+       dest->x = a->x + (b->x - a->x) * scale;
+       dest->y = a->y + (b->y - a->y) * scale;
+}
+
+static inline float vec2_square(const Vec2 *src)
+{
+        return src->x * src->x + src->y * src->y;
+}
+
+static inline float vec2_mag(const Vec2 *src)
+{
+       return sqrt(src->x * src->x + src->y * src->y);
+}
+
+static inline ANGLE vec2_angle(const Vec2 *src)
+{
+       return (ANGLE)(atan2f(src->y, src->x) * ANGLE_PI / M_PI + 0.5f);
+}
+
+static inline float vec2_norm(Vec2 *dest, const Vec2 *a)
+{
+       float mag = vec2_mag(a);
+       dest->x = a->x / mag;
+       dest->y = a->y / mag;
+       return mag;
+}
+
+static inline void vec2_proj(Vec2 *dest, const Vec2 *a, const Vec2 *b)
+{
+       float dist = vec2_dot(a, b), mag = vec2_mag(b), mag2 = mag * mag;
+       dest->x = dist * b->x / mag2;
+       dest->y = dist * b->y / mag2;
+}
+
+static inline void vec2_from_angle(Vec2 *dest, ANGLE angle, float mag)
+{
+       dest->y = sinf(angle * M_PI / ANGLE_PI) * mag;
+       dest->x = cosf(angle * M_PI / ANGLE_PI) * mag;
+}
+
diff --git a/src/config.h b/src/config.h
new file mode 100644 (file)
index 0000000..561e7aa
--- /dev/null
@@ -0,0 +1,4 @@
+#define PACKAGE "him_cellwriter"
+#define PACKAGE_NAME "him_cellwriter"
+#define PACKAGE_STRING "him_cellwriter"
+#define PKGDATADIR "/usr/share/cellwriter"
diff --git a/src/him_cellwriter.c b/src/him_cellwriter.c
new file mode 100644 (file)
index 0000000..02a89b8
--- /dev/null
@@ -0,0 +1,491 @@
+#include "hildon-im-plugin.h"
+#include "hildon-im-ui.h"
+
+#include <string.h>
+#include <glib.h>
+#include <gdk/gdk.h>
+#include <gdk/gdkx.h>
+#include <gtk/gtk.h>
+#include <hildon/hildon.h>
+
+#include <stdarg.h>
+#include <stdio.h>
+
+#include "common.h"
+
+#define HILDON_IM_CELLWRITER_TYPE hildon_im_cellwriter_get_type()
+#define HILDON_IM_CELLWRITER(obj) GTK_CHECK_CAST(obj, hildon_im_cellwriter_get_type(), HildonIMCellwriter)
+#define HILDON_IM_CELLWRITER_CLASS(klass) \
+        GTK_CHECK_CLASS_CAST(klass, hildon_im_cellwriter_get_type, \
+                             HildonIMCellwriterClass)
+#define HILDON_IS_IM_CELLWRITER(obj) \
+        GTK_CHECK_TYPE(obj, hildon_im_cellwriter_get_type())
+#define HILDON_IM_CELLWRITER_GET_PRIVATE(obj) \
+        (G_TYPE_INSTANCE_GET_PRIVATE ((obj), HILDON_IM_CELLWRITER_TYPE,\
+                                      HildonIMCellwriterPrivate))
+
+typedef struct
+{
+  GtkContainerClass parent;
+}
+HildonIMCellwriterClass;
+
+typedef struct
+{
+  GtkContainer parent;
+
+}
+HildonIMCellwriter;
+
+typedef struct
+{
+  HildonIMUI *ui;
+  GtkWidget *window;
+
+}
+HildonIMCellwriterPrivate;
+
+HildonIMUI * ui = NULL;
+
+static GType hildon_im_cellwriter_type = 0;
+static GtkWidgetClass *parent_class = NULL;
+
+GType hildon_im_cellwriter_get_type (void);
+GtkWidget *hildon_im_cellwriter_new (HildonIMUI *kbd);
+
+/*
+ * HildonIMPlugin interface
+ */
+
+static void hildon_im_cellwriter_iface_init (HildonIMPluginIface *iface);
+
+static void hildon_im_cellwriter_enable (HildonIMPlugin *plugin, gboolean init);
+static void hildon_im_cellwriter_disable                   (HildonIMPlugin *plugin);
+static void hildon_im_cellwriter_surrounding_received      (HildonIMPlugin *plugin,
+                                                             const gchar *surrounding,
+                                                             gint offset);
+/*
+ * GObject functions
+ */
+static void hildon_im_cellwriter_finalize     (GObject *obj);
+static void hildon_im_cellwriter_get_property (GObject *object,
+                                                guint prop_id,
+                                                GValue *value,
+                                                GParamSpec *pspec);
+static void hildon_im_cellwriter_set_property (GObject *object,
+                                                guint prop_id,
+                                                const GValue *value,
+                                                GParamSpec *pspec);
+
+static void hildon_im_cellwriter_class_init (HildonIMCellwriterClass *klass);
+static void hildon_im_cellwriter_init       (HildonIMCellwriter *self);
+
+/*
+ * Internal functions
+ */
+static void populate_window (HildonIMCellwriter *self);
+static void cellwriter_init (HildonIMCellwriter *self);
+
+/*
+ * Logging functions
+ */
+#define LOG_FILE "/tmp/hildon-im-cellwriter.log"
+
+int log_indent = 0;
+
+void Log(char *format, ...){
+       va_list args;
+       static FILE *fp = NULL;
+
+       va_start(args, format);
+       
+       if(!fp){
+               fp = fopen(LOG_FILE, "a");
+       }
+       
+        int i;
+        for(i = 0; i < log_indent; i++)
+          fprintf(fp, "  ");
+       vfprintf(fp, format, args);
+       fputc('\n', fp);
+       fflush(fp);
+
+       va_end(args);
+}
+
+
+/*
+ * Module functions
+ */
+
+HildonIMPlugin*
+module_create (HildonIMUI *keyboard)
+{
+  Log("module_create(%x)", keyboard); log_indent ++;
+  HildonIMPlugin *plugin = HILDON_IM_PLUGIN (hildon_im_cellwriter_new (keyboard));
+  log_indent --;
+  Log("module_create = %x", plugin);
+  return plugin;
+}
+
+void
+module_exit(void)
+{
+  Log("module_exit()");
+  /* empty */
+}
+
+void
+module_init(GTypeModule *module)
+{
+  Log("module_init(%x)", module); log_indent ++;
+
+  static const GTypeInfo type_info = {
+    sizeof(HildonIMCellwriterClass),
+    NULL, /* base_init */
+    NULL, /* base_finalize */
+    (GClassInitFunc) hildon_im_cellwriter_class_init,
+    NULL, /* class_finalize */
+    NULL, /* class_data */
+    sizeof(HildonIMCellwriter),
+    0, /* n_preallocs */
+    (GInstanceInitFunc) hildon_im_cellwriter_init,
+  };
+
+  static const GInterfaceInfo plugin_info = {
+    (GInterfaceInitFunc) hildon_im_cellwriter_iface_init,
+    NULL, /* interface_finalize */
+    NULL, /* interface_data */
+  };
+
+  hildon_im_cellwriter_type =
+          g_type_module_register_type(module,
+                                      GTK_TYPE_CONTAINER, "HildonIMCellwriter",
+                                      &type_info,
+                                      0);
+
+  g_type_module_add_interface(module,
+                              HILDON_IM_CELLWRITER_TYPE,
+                              HILDON_IM_TYPE_PLUGIN,
+                              &plugin_info);
+  log_indent --;
+}
+
+/*
+ * This is used to know the plugin's information when loading the module
+ */
+const HildonIMPluginInfo *
+hildon_im_plugin_get_info(void)
+{
+  Log("hildon_im_plugin_get_info()"); log_indent ++;
+  static const HildonIMPluginInfo info =
+  {
+    "HIM Cellwriter",                   /* description */
+    "him_cellwriter",                   /* name */
+    NULL,                               /* menu title */
+    NULL,                               /* gettext domain */
+    TRUE,                               /* visible in menu */
+    FALSE,                              /* cached TODO make it TRUE */
+    HILDON_IM_TYPE_FULLSCREEN,          /* UI type */
+    HILDON_IM_GROUP_LATIN,              /* group */
+    HILDON_IM_DEFAULT_PLUGIN_PRIORITY,  /* priority */
+    NULL,                               /* special character plugin */
+    NULL,                               /* help page */
+    TRUE,                               /* disable common UI buttons */
+    0,                                  /* plugin height */
+    HILDON_IM_TRIGGER_FINGER            /* trigger */
+  };
+
+  log_indent --;
+
+  return &info;
+}
+
+/*
+ * This function returns the list of available languages supported
+ * by the plugin.
+ */
+gchar **
+hildon_im_plugin_get_available_languages (gboolean *free)
+{
+  Log("hildon_im_plugin_get_available_langauges()");
+  static gchar *list[] = { "en_GB", NULL };
+  *free = FALSE;
+
+  return list;
+}
+
+GType
+hildon_im_cellwriter_get_type (void)
+{
+  // Log("hildon_im_plugin_fkb_get_type()");
+  return hildon_im_cellwriter_type;
+}
+
+/*
+ * Implement the interface.
+ */
+static void
+hildon_im_cellwriter_iface_init (HildonIMPluginIface *iface)
+{
+  Log("hildon_im_cellwriter_iface_init()");
+  iface->enable = hildon_im_cellwriter_enable;
+  iface->disable = hildon_im_cellwriter_disable;
+  iface->surrounding_received = hildon_im_cellwriter_surrounding_received;
+  // preedit_commited ?
+}
+
+static void
+hildon_im_cellwriter_class_init (HildonIMCellwriterClass *klass)
+{
+  Log("hildon_im_cellwriter_class_init(%x)", klass); log_indent ++;
+  GObjectClass *object_class;
+  GtkObjectClass *gtk_object_class;
+  GtkWidgetClass *widget_class;
+  GtkContainerClass *container_class;
+
+  parent_class = g_type_class_peek_parent(klass);
+  g_type_class_add_private(klass, sizeof(HildonIMCellwriterPrivate));
+
+  object_class = G_OBJECT_CLASS(klass);
+  gtk_object_class = GTK_OBJECT_CLASS(klass);
+  widget_class = GTK_WIDGET_CLASS(klass);
+  container_class = GTK_CONTAINER_CLASS(klass);
+
+  object_class->set_property  = hildon_im_cellwriter_set_property;
+  object_class->get_property  = hildon_im_cellwriter_get_property;
+  object_class->finalize      = hildon_im_cellwriter_finalize;
+
+  /* install properties and signals as needed */
+
+  g_object_class_install_property(object_class, HILDON_IM_PROP_UI,
+                                  g_param_spec_object (HILDON_IM_PROP_UI_DESCRIPTION,
+                                                       HILDON_IM_PROP_UI_DESCRIPTION,
+                                                       "UI that uses plugin",
+                                                       HILDON_IM_TYPE_UI,
+                                                       G_PARAM_READWRITE
+                                                       | G_PARAM_CONSTRUCT_ONLY));
+
+  log_indent --;
+}
+
+static void
+hildon_im_cellwriter_init (HildonIMCellwriter *self)
+{
+  Log("hildon_im_cellwriter_init(%x)", self);
+
+  HildonIMCellwriterPrivate *priv;
+  g_return_if_fail (HILDON_IS_IM_CELLWRITER (self));
+  priv = HILDON_IM_CELLWRITER_GET_PRIVATE(self);
+
+  priv->window = NULL;
+}
+
+static void
+hildon_im_cellwriter_finalize(GObject *obj)
+{
+  Log("hildon_im_cellwriter_finalize(%x)", obj);
+  if (G_OBJECT_CLASS(parent_class)->finalize)
+  {
+    G_OBJECT_CLASS(parent_class)->finalize(obj);
+  }
+}
+
+GtkWidget *
+hildon_im_cellwriter_new (HildonIMUI *kbd)
+{
+  Log("hildon_im_cellwriter_new(%x)", kbd); log_indent ++;
+  GtkWidget *widget =  g_object_new (HILDON_IM_CELLWRITER_TYPE,
+                       HILDON_IM_PROP_UI_DESCRIPTION, kbd, NULL);
+  log_indent --;
+  return widget;
+}
+
+static void
+hildon_im_cellwriter_get_property (GObject *object,
+                                    guint prop_id,
+                                    GValue *value,
+                                    GParamSpec *pspec)
+{
+  Log("hildon_im_cellwriter_get_property(%x, %d, %x, %x)", object, prop_id, value, pspec); log_indent ++;
+  HildonIMCellwriterPrivate *priv;
+
+  g_return_if_fail (HILDON_IS_IM_CELLWRITER(object));
+  priv = HILDON_IM_CELLWRITER_GET_PRIVATE(object);
+
+  switch (prop_id)
+  {
+    case HILDON_IM_PROP_UI:
+      g_value_set_object(value, priv->ui);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+      break;
+  }
+  log_indent ++;
+}
+
+static void
+hildon_im_cellwriter_set_property (GObject *object,
+                                    guint prop_id,
+                                    const GValue *value,
+                                    GParamSpec *pspec)
+{
+  Log("hildon_im_cellwriter_set_property(%x, %d, %x, %x)", object, prop_id, value, pspec); log_indent ++;
+  HildonIMCellwriterPrivate *priv;
+
+  g_return_if_fail (HILDON_IS_IM_CELLWRITER (object));
+  priv = HILDON_IM_CELLWRITER_GET_PRIVATE(object);
+
+  switch (prop_id)
+  {
+    case HILDON_IM_PROP_UI:
+      ui = priv->ui = g_value_get_object(value);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+      break;
+  }
+  log_indent --;
+}
+
+static void
+hildon_im_cellwriter_enable (HildonIMPlugin *plugin, gboolean init)
+{
+  Log("hildon_im_cellwriter_enable(%x, %s)", plugin, init ? "true" : "false" ); log_indent ++;
+
+  HildonIMCellwriter *self;
+  HildonIMCellwriterPrivate *priv;
+  gboolean window_is_new;
+
+  g_return_if_fail (HILDON_IS_IM_CELLWRITER (plugin));
+  self = HILDON_IM_CELLWRITER(plugin);
+  priv = HILDON_IM_CELLWRITER_GET_PRIVATE(self);
+
+  window_is_new = (priv->window == NULL);
+
+  if (window_is_new)
+  {
+    priv->window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+  }
+
+  gtk_window_set_type_hint(GTK_WINDOW(priv->window), GDK_WINDOW_TYPE_HINT_DIALOG);
+  gtk_window_set_decorated(GTK_WINDOW(priv->window), FALSE);
+  // hildon_gtk_window_set_portrait_flags (GTK_WINDOW(priv->window), HILDON_PORTRAIT_MODE_SUPPORT);
+
+  hildon_im_ui_send_communication_message(priv->ui, HILDON_IM_CONTEXT_REQUEST_SURROUNDING);
+
+  gtk_window_fullscreen(GTK_WINDOW(priv->window));
+  gtk_widget_show_all(priv->window);
+
+  gdk_window_set_transient_for(GTK_WIDGET(priv->window)->window, gtk_widget_get_root_window(GTK_WIDGET(priv->window)));
+
+  if (window_is_new)
+  {
+    cellwriter_init(self);
+    populate_window(self);
+    gtk_widget_show_all(priv->window);
+  }
+
+  window_show();
+
+  log_indent --;
+}
+
+static void
+hildon_im_cellwriter_disable (HildonIMPlugin *plugin)
+{
+  Log("hildon_im_cellwriter_disable(%x)", plugin); log_indent ++;
+
+  HildonIMCellwriter *self;
+  HildonIMCellwriterPrivate *priv;
+
+  g_return_if_fail (HILDON_IS_IM_CELLWRITER(plugin));
+  self = HILDON_IM_CELLWRITER(plugin);
+  priv = HILDON_IM_CELLWRITER_GET_PRIVATE(self);
+
+  if (GTK_WIDGET_VISIBLE(GTK_WIDGET(priv->window)))
+  {
+    gtk_widget_hide(GTK_WIDGET(priv->window));
+  }
+
+  log_indent --;
+}
+
+void cell_widget_load_string(const gchar *str);
+
+static void
+hildon_im_cellwriter_surrounding_received(HildonIMPlugin *plugin,
+                                           const gchar *surrounding,
+                                           gint offset)
+{
+  Log("hildon_im_cellwriter_surrounding_received(%x, '%s', %d)", plugin, surrounding, offset); log_indent ++;
+
+  HildonIMCellwriter *self;
+  HildonIMCellwriterPrivate *priv;
+
+  if(hildon_im_ui_get_commit_mode(ui) == HILDON_IM_COMMIT_REDIRECT)
+    cell_widget_load_string(surrounding);
+
+  log_indent --;
+}
+
+static void
+close_fkb (GtkButton *button, gpointer user_data)
+{
+  HildonIMCellwriter *self;
+  HildonIMCellwriterPrivate *priv;
+
+  g_return_if_fail (HILDON_IS_IM_CELLWRITER (user_data));
+  self = HILDON_IM_CELLWRITER(user_data);
+  priv = HILDON_IM_CELLWRITER_GET_PRIVATE(self);
+
+  hildon_im_ui_restore_previous_mode(priv->ui);
+}
+
+void read_profile();
+
+static void cellwriter_init(HildonIMCellwriter *self){
+
+  HildonIMCellwriterPrivate *priv;
+  priv = HILDON_IM_CELLWRITER_GET_PRIVATE(self);
+
+  key_event_init(priv->ui);
+  recognize_init();
+  read_profile();
+  update_enabled_samples();
+  
+}
+
+static void
+populate_window (HildonIMCellwriter *self)
+{
+  Log("populate_window(%x)", self); log_indent ++;
+
+  HildonIMCellwriterPrivate *priv;
+  GtkWidget *parea, *hbox;
+  gint screen_width = gdk_screen_width();
+
+  priv = HILDON_IM_CELLWRITER_GET_PRIVATE(self);
+
+  window_create(priv->window);
+
+  log_indent --;
+}
+
+
+/*
+typedef enum
+{
+  HILDON_IM_COMMIT_DIRECT,
+  HILDON_IM_COMMIT_REDIRECT,
+  HILDON_IM_COMMIT_SURROUNDING,
+  HILDON_IM_COMMIT_BUFFERED,
+  HILDON_IM_COMMIT_PREEDIT
+} HildonIMCommitMode;
+
+hildon_im_ui_Send_event(ui, Window window, XEvent event)
+
+*/
diff --git a/src/keyevent.c b/src/keyevent.c
new file mode 100644 (file)
index 0000000..0f6a92e
--- /dev/null
@@ -0,0 +1,473 @@
+
+/*
+
+cellwriter -- a character recognition input method
+Copyright (C) 2007 Michael Levin <risujin@risujin.org>
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+*/
+
+#include "hildon-im-ui.h"
+
+#include "common.h"
+#include "keys.h"
+#include <string.h>
+#include <stdlib.h>
+#include <X11/X.h>
+#include <X11/Xlib.h>
+#include <X11/keysym.h>
+#include <X11/extensions/XTest.h>
+#include <gdk/gdkx.h>
+
+/* Define this to always overwrite an unused KeyCode to send any KeySym */
+/* #define ALWAYS_OVERWRITE */
+
+/* Define this to print key events without actually generating them */
+/* #define DEBUG_KEY_EVENTS */
+
+/*
+        X11 KeyCodes
+*/
+
+enum {
+        KEY_TAKEN = 0,   /* Has KeySyms, cannot be overwritten */
+        KEY_BAD,         /* Manually marked as unusable */
+        KEY_USABLE,      /* Has no KeySyms, can be overwritten */
+        KEY_ALLOCATED,   /* Normally usable, but currently allocated */
+        /* Values greater than this represent multiple allocations */
+};
+
+extern HildonIMUI *ui;
+
+static char usable[256], pressed[256];
+static int key_min, key_max, key_offset, key_codes;
+//static KeySym *keysyms = NULL;
+//static XModifierKeymap *modmap = NULL;
+
+/* Bad keycodes: Despite having no KeySym entries, certain KeyCodes will
+   generate special KeySyms even if their KeySym entries have been overwritten.
+   For instance, KeyCode 204 attempts to eject the CD-ROM even if there is no
+   CD-ROM device present! KeyCode 229 will launch GNOME file search even if
+   there is no search button on the physical keyboard. There is no programatic
+   way around this but to keep a list of commonly used "bad" KeyCodes. */
+
+void bad_keycodes_read(void)
+{
+  /*
+        int keycode;
+
+        while (!profile_sync_int(&keycode)) {
+                if (keycode < key_min || keycode > key_max) {
+                        g_warning("Cannot block bad keycode %d, out of range",
+                                  keycode);
+                        continue;
+                }
+                usable[keycode] = KEY_BAD;
+        }
+  */
+}
+
+void bad_keycodes_write(void)
+{
+  /*
+        int i;
+
+        profile_write("bad_keycodes");
+        for (i = key_min; i < key_max; i++)
+                if (usable[i] == KEY_BAD)
+                        profile_write(va(" %d", i));
+        profile_write("\n");
+  */
+}
+
+static void press_keycode(KeyCode k)
+/* Called from various places to generate a key-down event */
+{
+  //if (k >= key_min && k <= key_max)
+  //              XTestFakeKeyEvent(GDK_DISPLAY(), k, True, 1);
+}
+
+static void release_keycode(KeyCode k)
+/* Called from various places to generate a key-up event */
+{
+  //        if (k >= key_min && k <= key_max)
+  //            XTestFakeKeyEvent(GDK_DISPLAY(), k, False, 1);
+}
+
+static void type_keycode(KeyCode k)
+/* Key-down + key-up */
+{
+        press_keycode(k);
+        release_keycode(k);
+}
+
+static void setup_usable(void)
+/* Find unused slots in the key mapping */
+{
+  /*
+        int i, found;
+
+        /* Find all free keys 
+        memset(usable, 0, sizeof (usable));
+        for (i = key_min, found = 0; i <= key_max; i++) {
+                int j;
+
+                for (j = 0; j < key_codes &&
+                     keysyms[(i - key_min) * key_codes + j] == NoSymbol; j++);
+                if (j < key_codes) {
+                        usable[i] = KEY_TAKEN;
+                        continue;
+                }
+                usable[i] = KEY_USABLE;
+                found++;
+        }
+        key_offset = 0;
+
+        /* If we haven't found a usable key, it's probably because we have
+           already ran once ad used them all up without setting them back 
+        if (!found) {
+                usable[key_max - key_min - 1] = KEY_USABLE;
+                g_warning("Found no usable KeyCodes, restart the X server!");
+        } else
+                g_debug("Found %d usable KeyCodes", found);
+  */
+}
+
+static void cleanup_usable(void)
+/* Clear all the usable KeyCodes or we won't have any when we run again! */
+{
+  /*
+        int i, bad, unused = 0, freed;
+
+        for (i = 0, freed = 0, bad = 0; i <= key_max; i++)
+                if (usable[i] >= KEY_USABLE) {
+                        int j, kc_used = FALSE;
+
+                        for (j = 0; j < key_codes; j++) {
+                                int index = (i - key_min) * key_codes + j;
+
+                                if (keysyms[index] != NoSymbol)
+                                        kc_used = TRUE;
+                                keysyms[index] = NoSymbol;
+                        }
+                        if (kc_used)
+                                freed++;
+                        else
+                                unused++;
+                } else if (usable[i] == KEY_BAD)
+                        bad++;
+        if (freed) {
+                XChangeKeyboardMapping(GDK_DISPLAY(), key_min, key_codes,
+                                       keysyms, key_max - key_min + 1);
+                XFlush(GDK_DISPLAY());
+        }
+        g_debug("Free'd %d KeyCode(s), %d unused, %d marked bad",
+                freed, unused, bad);
+  */
+}
+
+static void release_held_keys(void)
+/* Release all held keys that were not pressed by us */
+{
+  /*
+        int i;
+        char keys[32];
+
+        XQueryKeymap(GDK_DISPLAY(), keys);
+        for (i = 0; i < 32; i++) {
+                int j;
+
+                for (j = 0; j < 8; j++) {
+                        KeyCode keycode;
+
+                        keycode = i * 8 + j;
+                        if (keys[i] & (1 << j) && !pressed[keycode]) {
+                                g_debug("Released held KeyCode %d", keycode);
+                                release_keycode(keycode);
+                        }
+                }
+        }
+  */
+}
+
+/*
+        Key Events
+*/
+
+int key_overwrites = 0, key_recycles = 0,
+    key_shifted = 0, key_num_locked = FALSE, key_caps_locked = FALSE,
+    key_disable_overwrite = FALSE;
+
+static int alt_mask, num_lock_mask, meta_mask;
+static KeyEvent ke_shift, ke_enter, ke_num_lock, ke_caps_lock;
+
+static void reset_keyboard(void)
+/* In order to reliably predict key event behavior we need to be able to
+   reset the keyboard modifier and pressed state */
+{
+  /*
+  Window root, child;
+        int root_x, root_y, win_x, win_y;
+        unsigned int mask;
+
+        release_held_keys();
+        XQueryPointer(GDK_DISPLAY(), GDK_WINDOW_XWINDOW(GDK_ROOT_PARENT()),
+                      &root, &child, &root_x, &root_y, &win_x, &win_y, &mask);
+        if (mask & LockMask)
+                type_keycode(ke_caps_lock.keycode);
+        if (mask & num_lock_mask)
+                type_keycode(ke_num_lock.keycode);
+  */
+}
+
+static int is_modifier(unsigned int keysym)
+/* Returns TRUE for KeySyms that are tracked internally */
+{
+        switch (keysym) {
+        case XK_Shift_L:
+        case XK_Shift_R:
+        case XK_Num_Lock:
+        case XK_Caps_Lock:
+                return TRUE;
+        default:
+                return FALSE;
+        }
+}
+
+static void key_event_allocate(KeyEvent *key_event, unsigned int keysym)
+/* Either finds the KeyCode associated with the given keysym or overwrites
+   a usable one to generate it */
+{
+  /*
+        int i, start;
+
+        //* Invalid KeySym 
+        if (!keysym) {
+                key_event->keycode = -1;
+                key_event->keysym = 0;
+                return;
+        }
+
+        /* First see if our KeySym is already in the mapping 
+        key_event->shift = FALSE;
+#ifndef ALWAYS_OVERWRITE
+        for (i = 0; i <= key_max - key_min; i++) {
+                if (keysyms[i * key_codes + 1] == keysym)
+                        key_event->shift = TRUE;
+                if (keysyms[i * key_codes] == keysym || key_event->shift) {
+                        key_event->keycode = key_min + i;
+                        key_recycles++;
+
+                        /* Bump the allocation count if this is an
+                           allocateable KeyCode 
+                        if (usable[key_event->keycode] >= KEY_USABLE)
+                                usable[key_event->keycode]++;
+
+                        return;
+                }
+        }
+#endif
+
+        /* Key overwrites may be disabled, in which case we're out of luck 
+        if (key_disable_overwrite) {
+                key_event->keycode = -1;
+                key_event->keysym = 0;
+                g_warning("Not allowed to overwrite KeyCode for %s",
+                          XKeysymToString(keysym));
+                return;
+        }
+
+        /* If not, find a usable KeyCode in the mapping 
+        for (start = key_offset++; ; key_offset++) {
+                if (key_offset > key_max - key_min)
+                        key_offset = 0;
+                if (usable[key_min + key_offset] == KEY_USABLE &&
+                    !pressed[key_min + key_offset])
+                        break;
+
+                /* If we can't find one, invalidate the event 
+                if (key_offset == start) {
+                        key_event->keycode = -1;
+                        key_event->keysym = 0;
+                        g_warning("Failed to allocate KeyCode for %s",
+                                  XKeysymToString(keysym));
+                        return;
+                }
+        }
+        key_overwrites++;
+        key_event->keycode = key_min + key_offset;
+        usable[key_event->keycode] = KEY_ALLOCATED;
+
+        /* Modify the slot to hold our character 
+        keysyms[key_offset * key_codes] = keysym;
+        keysyms[key_offset * key_codes + 1] = keysym;
+        XChangeKeyboardMapping(GDK_DISPLAY(), key_event->keycode, key_codes,
+                               keysyms + key_offset * key_codes, 1);
+        XSync(GDK_DISPLAY(), False);
+
+        g_debug("Overwrote KeyCode %d for %s", key_event->keycode,
+                XKeysymToString(keysym));
+  */
+}
+
+void key_event_new(KeyEvent *key_event, unsigned int keysym)
+/* Filters locks and shifts but allocates other keys normally */
+{
+        key_event->keysym = keysym;
+        /*
+        if (is_modifier(keysym))
+                return;
+        key_event_allocate(key_event, keysym);
+        */
+}
+
+void key_event_free(KeyEvent *key_event)
+/* Release resources associated with and invalidate a key event */
+{
+  /*
+        if (key_event->keycode >= key_min && key_event->keycode <= key_max &&
+            usable[key_event->keycode] == KEY_ALLOCATED)
+                usable[key_event->keycode] = KEY_USABLE;
+        key_event->keycode = -1;
+        key_event->keysym = 0;
+  */
+}
+
+void key_event_press(KeyEvent *key_event)
+/* Press the KeyCode specified in the event */
+{
+  /*
+        /* Track modifiers without actually using them 
+        if (key_event->keysym == XK_Shift_L ||
+            key_event->keysym == XK_Shift_R) {
+                key_shifted++;
+                return;
+        } else if (key_event->keysym == XK_Caps_Lock) {
+                key_caps_locked = !key_caps_locked;
+                return;
+        } else if (key_event->keysym == XK_Num_Lock) {
+                key_num_locked = !key_num_locked;
+                return;
+        }
+
+        /* Invalid event 
+        if (key_event->keycode < key_min || key_event->keycode > key_max)
+                return;
+
+        /* If this KeyCode is already pressed, something is wrong 
+        if (pressed[key_event->keycode]) {
+                g_debug("KeyCode %d is already pressed", key_event->keycode);
+                return;
+        }
+
+        /* Keep track of what KeyCodes we pressed down 
+        pressed[key_event->keycode] = TRUE;
+
+        /* Press our keycode 
+        if (key_event->shift)
+                press_keycode(ke_shift.keycode);
+        press_keycode(key_event->keycode);
+        XSync(GDK_DISPLAY(), False);
+  */
+}
+
+void key_event_release(KeyEvent *key_event)
+{
+  /*
+        /* Track modifiers without actually using them 
+        if (key_event->keysym == XK_Shift_L ||
+            key_event->keysym == XK_Shift_R) {
+                key_shifted--;
+                return;
+        }
+
+        /* Invalid key event 
+        if (key_event->keycode < key_min || key_event->keycode > key_max)
+                return;
+
+        /* Keep track of what KeyCodes are pressed because of us 
+        pressed[key_event->keycode] = FALSE;
+
+        /* Release our keycode 
+        release_keycode(key_event->keycode);
+        if (key_event->shift)
+                release_keycode(ke_shift.keycode);
+        XSync(GDK_DISPLAY(), False);
+  */
+}
+
+void unicode_to_utf8(unsigned int code, char *out){
+  // little endian
+  unsigned char *c = (unsigned char*)&code;
+  if(code < 0x80){
+    out[0] = c[0];
+    out[1] = 0;
+  }else if(code < 0x800){
+    out[0] = 0xC0 | (c[1] << 2) | (c[0] >> 6);
+    out[1] = 0x80 | (0x3F & c[0]);
+    out[2] = 0;
+  }else *out = 0; // todo
+}
+
+void key_event_send_char(int unichar)
+{
+  char string[6];
+
+  unicode_to_utf8((unsigned int)unichar, string);
+  
+  if(ui){
+    if(unichar == '\n')
+      hildon_im_ui_send_communication_message(ui, HILDON_IM_CONTEXT_HANDLE_ENTER);
+    else
+      hildon_im_ui_send_utf8(ui, string);
+  }
+
+  /*
+        KeyEvent key_event;
+        KeySym keysym;
+
+        /* Get the keysym for the unichar (may be unsupported) 
+        keysym = XStringToKeysym(va("U%04X", unichar));
+        if (!keysym) {
+                g_warning("XStringToKeysym failed to get Keysym for '%C'",
+                          unichar);
+                return;
+        }
+
+        key_event_new(&key_event, keysym);
+        key_event_press(&key_event);
+        key_event_release(&key_event);
+        key_event_free(&key_event);
+        */
+}
+
+void key_event_send_enter()
+{
+  //type_keycode(ke_enter.keycode);
+}
+
+int key_event_init()
+{
+
+        /* Clear the array that keeps track of our pressed keys */
+        memset(pressed, 0, sizeof (pressed));
+
+        return 0;
+}
+
+void key_event_cleanup(void)
+{
+}
diff --git a/src/keys.h b/src/keys.h
new file mode 100644 (file)
index 0000000..272e627
--- /dev/null
@@ -0,0 +1,98 @@
+
+/*
+
+cellwriter -- a character recognition input method
+Copyright (C) 2007 Michael Levin <risujin@risujin.org>
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+*/
+
+/*
+        Key events
+*/
+
+typedef struct {
+        unsigned char shift;
+        unsigned int keysym;
+} KeyEvent;
+
+extern int key_shifted, key_num_locked, key_caps_locked;
+
+void key_event_new(KeyEvent *key_event, unsigned int keysym);
+void key_event_free(KeyEvent *key_event);
+void key_event_press(KeyEvent *key_event);
+void key_event_release(KeyEvent *key_event);
+void key_event_send_char(int unichar);
+void key_event_send_enter(void);
+void key_event_update_mappings(void);
+
+/*
+        Key widget
+*/
+
+/* Key flags */
+#define KEY_ARROW               0x0001
+#define KEY_TOGGLE_ON           0x0002
+#define KEY_TOGGLE_OFF          0x0003
+#define KEY_ICON_MASK           0x000f
+#define KEY_STICKY              0x0010
+#define KEY_SHIFT               0x0020
+#define KEY_SHIFTABLE           0x0040
+#define KEY_CAPS_LOCK           0x0080
+#define KEY_ICON_SHIFT          0x0100
+#define KEY_NUM_LOCK            0x0200
+#define KEY_NUM_LOCKABLE        0x0400
+
+typedef struct {
+        char active;
+        short flags;
+        const char *string, *string_shift;
+        unsigned int keysym, keysym_shift;
+        int x, y, width, height, rotate;
+        KeyEvent key_event;
+} Key;
+
+typedef struct {
+        GtkWidget *drawing_area;
+        GdkPixmap *pixmap;
+        GdkGC *pixmap_gc;
+        cairo_t *cairo;
+        PangoContext *pango;
+        PangoFontDescription *pango_font_desc;
+        int slaved, len, max_len, x, y, width, height, active, x_range, y_range,
+            min_height;
+        Key keys[];
+} KeyWidget;
+
+extern int keyboard_size;
+
+/* Create slaved or non-slaved keyboard */
+KeyWidget *key_widget_new_small(GtkWidget *drawing_area);
+KeyWidget *key_widget_new_full(void);
+
+/* Functions for slaved keyboards only */
+gboolean key_widget_button_press(GtkWidget *widget, GdkEventButton *event,
+                                 KeyWidget *key_widget);
+gboolean key_widget_button_release(GtkWidget *widget, GdkEventButton *event,
+                                   KeyWidget *key_widget);
+void key_widget_render(KeyWidget *key_widget);
+void key_widget_configure(KeyWidget *key_widget, int x, int y,
+                          int width, int height);
+
+/* Functions to update keyboards */
+int key_widget_update_colors(void);
+void key_widget_cleanup(KeyWidget *key_widget);
+
diff --git a/src/main.c b/src/main.c
new file mode 100644 (file)
index 0000000..32c2a75
--- /dev/null
@@ -0,0 +1,980 @@
+
+/*
+
+cellwriter -- a character recognition input method
+Copyright (C) 2007 Michael Levin <risujin@risujin.org>
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+*/
+
+#include "config.h"
+#include "common.h"
+#include <stdlib.h>
+#include <string.h>
+#include <signal.h>
+#include <stdio.h>
+#include <errno.h>
+#ifdef HAVE_GNOME
+#include <libgnome/libgnome.h>
+#endif
+
+/* recognize.c */
+extern int strength_sum;
+
+void recognize_init(void);
+void recognize_sync(void);
+void samples_write(void);
+void sample_read(void);
+void update_enabled_samples(void);
+int samples_loaded(void);
+
+/* cellwidget.c */
+extern int training, corrections, rewrites, characters, inputs;
+
+void cell_widget_cleanup(void);
+
+/* options.c */
+void options_sync(void);
+
+/* keyevent.c */
+extern int key_recycles, key_overwrites, key_disable_overwrite;
+
+
+void bad_keycodes_write(void);
+void bad_keycodes_read(void);
+
+/*
+        Variable argument parsing
+*/
+
+char *nvav(int *plen, const char *fmt, va_list va)
+{
+       static char buffer[2][16000];
+       static int which;
+       int len;
+
+       which = !which;
+       len = g_vsnprintf(buffer[which], sizeof(buffer[which]), fmt, va);
+       if (plen)
+               *plen = len;
+       return buffer[which];
+}
+
+char *nva(int *plen, const char *fmt, ...)
+{
+       va_list va;
+       char *string;
+
+       va_start(va, fmt);
+       string = nvav(plen, fmt, va);
+       va_end(va);
+       return string;
+}
+
+char *va(const char *fmt, ...)
+{
+       va_list va;
+       char *string;
+
+       va_start(va, fmt);
+       string = nvav(NULL, fmt, va);
+       va_end(va);
+       return string;
+}
+
+/*
+        GDK colors
+*/
+
+static int check_color_range(int value)
+{
+        if (value < 0)
+                value = 0;
+        if (value > 65535)
+                value = 65535;
+        return value;
+}
+
+void scale_gdk_color(const GdkColor *base, GdkColor *out, double value)
+{
+        out->red = check_color_range(base->red * value);
+        out->green = check_color_range(base->green * value);
+        out->blue = check_color_range(base->blue * value);
+}
+
+void gdk_color_to_hsl(const GdkColor *src,
+                      double *hue, double *sat, double *lit)
+/* Source: http://en.wikipedia.org/wiki/HSV_color_space
+           #Conversion_from_RGB_to_HSL_or_HSV */
+{
+        double max = src->red, min = src->red;
+
+        /* Find largest and smallest channel */
+        if (src->green > max)
+                max = src->green;
+        if (src->green < min)
+                min = src->green;
+        if (src->blue > max)
+                max = src->blue;
+        if (src->blue < min)
+                min = src->blue;
+
+        /* Hue depends on max/min */
+        if (max == min)
+                *hue = 0;
+        else if (max == src->red) {
+                *hue = (src->green - src->blue) / (max - min) / 6.;
+                if (*hue < 0.)
+                        *hue += 1.;
+        } else if (max == src->green)
+                *hue = ((src->blue - src->red) / (max - min) + 2.) / 6.;
+        else if (max == src->blue)
+                *hue = ((src->red - src->green) / (max - min) + 4.) / 6.;
+
+        /* Lightness */
+        *lit = (max + min) / 2 / 65535;
+
+        /* Saturation depends on lightness */
+        if (max == min)
+                *sat = 0.;
+        else if (*lit <= 0.5)
+                *sat = (max - min) / (max + min);
+        else
+                *sat = (max - min) / (65535 * 2 - (max + min));
+}
+
+void hsl_to_gdk_color(GdkColor *src, double hue, double sat, double lit)
+/* Source: http://en.wikipedia.org/wiki/HSV_color_space
+           #Conversion_from_RGB_to_HSL_or_HSV */
+{
+        double q, p, t[3];
+        int i;
+
+        /* Clamp ranges */
+        if (hue < 0)
+                hue -= (int)hue - 1.;
+        if (hue > 1)
+                hue -= (int)hue;
+        if (sat < 0)
+                sat = 0;
+        if (sat > 1)
+                sat = 1;
+        if (lit < 0)
+                lit = 0;
+        if (lit > 1)
+                lit = 1;
+
+        /* Special case for gray */
+        if (sat == 0.) {
+                src->red = lit * 65535;
+                src->green = lit * 65535;
+                src->blue = lit * 65535;
+                return;
+        }
+
+        q = (lit < 0.5) ? lit * (1 + sat) : lit + sat - (lit * sat);
+        p = 2 * lit - q;
+        t[0] = hue + 1 / 3.;
+        t[1] = hue;
+        t[2] = hue - 1 / 3.;
+        for (i = 0; i < 3; i++) {
+                if (t[i] < 0.)
+                        t[i] += 1.;
+                if (t[i] > 1.)
+                        t[i] -= 1.;
+                if (t[i] >= 2 / 3.)
+                        t[i] = p;
+                else if (t[i] >= 0.5)
+                        t[i] = p + ((q - p) * 6 * (2 / 3. - t[i]));
+                else if (t[i] >= 1 / 6.)
+                        t[i] = q;
+                else
+                        t[i] = p + ((q - p) * 6 * t[i]);
+        }
+        src->red = t[0] * 65535;
+        src->green = t[1] * 65535;
+        src->blue = t[2] * 65535;
+}
+
+void shade_gdk_color(const GdkColor *base, GdkColor *out, double value)
+{
+        double hue, sat, lit;
+
+        gdk_color_to_hsl(base, &hue, &sat, &lit);
+        sat *= value;
+        lit += value - 1.;
+        hsl_to_gdk_color(out, hue, sat, lit);
+}
+
+void highlight_gdk_color(const GdkColor *base, GdkColor *out, double value)
+/* Shades brighter or darker depending on the luminance of the base color */
+{
+        double lum = (0.3 * base->red + 0.59 * base->green +
+                      0.11 * base->blue) / 65535;
+
+        value = lum < 0.5 ? 1. + value : 1. - value;
+        shade_gdk_color(base, out, value);
+}
+
+/*
+        Profile
+*/
+
+/* Profile format version */
+#define PROFILE_VERSION 0
+
+int profile_read_only, keyboard_only = FALSE;
+
+static GIOChannel *channel;
+static char profile_buf[4096], *profile_end = NULL, profile_swap,
+            *force_profile = NULL, *profile_tmp = NULL;
+static int force_read_only;
+
+static int is_space(int ch)
+{
+        return ch == ' ' || ch == '\t' || ch == '\r';
+}
+
+static int profile_open_channel(const char *type, const char *path)
+/* Tries to open a profile channel, returns TRUE if it succeeds */
+{
+        GError *error = NULL;
+
+        if (!g_file_test(path, G_FILE_TEST_IS_REGULAR) &&
+            g_file_test(path, G_FILE_TEST_EXISTS)) {
+                g_warning("Failed to open %s profile '%s': Not a regular file",
+                          type, path);
+                return FALSE;
+        }
+        channel = g_io_channel_new_file(path, profile_read_only ? "r" : "w",
+                                        &error);
+        if (!error)
+                return TRUE;
+        g_warning("Failed to open %s profile '%s' for %s: %s",
+                  type, path, profile_read_only ? "reading" : "writing",
+                  error->message);
+        g_error_free(error);
+        return FALSE;
+}
+
+static void create_user_dir(void)
+/* Make sure the user directory exists */
+{
+        char *path;
+
+        path = g_build_filename(g_get_home_dir(), "." PACKAGE, NULL);
+        if (g_mkdir_with_parents(path, 0755))
+                g_warning("Failed to create user directory '%s'", path);
+        g_free(path);
+}
+
+static int profile_open_read(void)
+/* Open the profile file for reading. Returns TRUE if the profile was opened
+   successfully. */
+{
+        char *path;
+
+        profile_read_only = TRUE;
+
+        /* Try opening a command-line specified profile first */
+        if (force_profile &&
+            profile_open_channel("command-line specified", force_profile))
+                return TRUE;
+
+        /* Open user's profile */
+        path = g_build_filename(g_get_home_dir(), "." PACKAGE, "profile", NULL);
+        if (profile_open_channel("user's", path)) {
+                g_free(path);
+                return TRUE;
+        }
+        g_free(path);
+
+        /* Open system profile */
+        path = g_build_filename(PKGDATADIR, "profile", NULL);
+        if (profile_open_channel("system", path)) {
+                g_free(path);
+                return TRUE;
+        }
+        g_free(path);
+
+        return FALSE;
+}
+
+static int profile_open_write(void)
+/* Open a temporary profile file for writing. Returns TRUE if the profile was
+   opened successfully. */
+{
+        GError *error;
+        gint fd;
+
+        if (force_read_only) {
+                g_debug("Not saving profile, opened in read-only mode");
+                return FALSE;
+        }
+        profile_read_only = FALSE;
+
+        /* Open a temporary file as a channel */
+        error = NULL;
+        fd = g_file_open_tmp(PACKAGE ".XXXXXX", &profile_tmp, &error);
+        if (error) {
+                g_warning("Failed to open tmp file while saving "
+                          "profile: %s", error->message);
+                return FALSE;
+        }
+        channel = g_io_channel_unix_new(fd);
+        if (!channel) {
+                g_warning("Failed to create channel from temporary file");
+                return FALSE;
+        }
+
+        return TRUE;
+}
+
+static int move_file(char *from, char *to)
+/* The standard library rename() cannot move across filesystems so we need a
+   function that can emulate that. This function will copy a file, byte-by-byte
+   but is not as safe as rename(). */
+{
+        GError *error = NULL;
+        GIOChannel *src_channel, *dest_channel;
+        gchar buffer[4096];
+
+        /* Open source file for reading */
+        src_channel = g_io_channel_new_file(from, "r", &error);
+        if (error) {
+                g_warning("move_file() failed to open src '%s': %s",
+                          from, error->message);
+                return FALSE;
+        }
+
+        /* Open destination file for writing */
+        dest_channel = g_io_channel_new_file(to, "w", &error);
+        if (error) {
+                g_warning("move_file() failed to open dest '%s': %s",
+                          to, error->message);
+                g_io_channel_unref(src_channel);
+                return FALSE;
+        }
+
+        /* Copy data in blocks */
+        for (;;) {
+                gsize bytes_read, bytes_written;
+
+                /* Read a block in */
+                g_io_channel_read_chars(src_channel, buffer, sizeof (buffer),
+                                        &bytes_read, &error);
+                if (bytes_read < 1 || error)
+                        break;
+
+                /* Write the block out */
+                g_io_channel_write_chars(dest_channel, buffer, bytes_read,
+                                         &bytes_written, &error);
+                if (bytes_written < bytes_read || error) {
+                        g_warning("move_file() error writing to '%s': %s",
+                                  to, error->message);
+                        g_io_channel_unref(src_channel);
+                        g_io_channel_unref(dest_channel);
+                        return FALSE;
+                }
+        }
+
+        /* Close channels */
+        g_io_channel_unref(src_channel);
+        g_io_channel_unref(dest_channel);
+
+        g_debug("move_file() copied '%s' to '%s'", from, to);
+
+        /* Should be safe to delete the old file now */
+        if (remove(from))
+                log_errno("move_file() failed to delete src");
+
+        return TRUE;
+}
+
+static int profile_close(void)
+/* Close the currently open profile and, if we just wrote the profile to a
+   temporary file, move it in place of the old profile */
+{
+        char *path = NULL;
+
+        if (!channel)
+                return FALSE;
+        g_io_channel_unref(channel);
+
+        if (!profile_tmp || profile_read_only)
+                return TRUE;
+
+        /* For some bizarre reason we may not have managed to create the
+           temporary file */
+        if (!g_file_test(profile_tmp, G_FILE_TEST_EXISTS)) {
+                g_warning("Tmp profile '%s' does not exist", profile_tmp);
+                return FALSE;
+        }
+
+        /* Use command-line specified profile path first then the user's
+           home directory profile */
+        path = force_profile;
+        if (!path)
+                path = g_build_filename(g_get_home_dir(),
+                                        "." PACKAGE, "profile", NULL);
+
+        if (g_file_test(path, G_FILE_TEST_EXISTS)) {
+                g_message("Replacing '%s' with '%s'", path, profile_tmp);
+
+                /* Don't write over non-regular files */
+                if (!g_file_test(path, G_FILE_TEST_IS_REGULAR)) {
+                        g_warning("Old profile '%s' is not a regular file",
+                                  path);
+                        goto error_recovery;
+                }
+
+                /* Remove the old profile */
+                if (remove(path)) {
+                        log_errno("Failed to delete old profile");
+                        goto error_recovery;
+                }
+        }
+        else
+                g_message("Creating new profile '%s'", path);
+
+        /* Move the temporary profile file in place of the old one */
+        if (rename(profile_tmp, path)) {
+                log_errno("rename() failed to move tmp profile in place");
+                if (!move_file(profile_tmp, path))
+                        goto error_recovery;
+        }
+
+        if (path != force_profile)
+                g_free(path);
+        return TRUE;
+
+error_recovery:
+        g_warning("Recover tmp profile at '%s'", profile_tmp);
+        return FALSE;
+}
+
+const char *profile_read(void)
+/* Read a token from the open profile */
+{
+        GError *error = NULL;
+        char *token;
+
+        if (!channel)
+                return "";
+        if (!profile_end)
+                profile_end = profile_buf;
+        *profile_end = profile_swap;
+
+seek_profile_end:
+
+        /* Get the next token from the buffer */
+        for (; is_space(*profile_end); profile_end++);
+        token = profile_end;
+        for (; *profile_end && !is_space(*profile_end) && *profile_end != '\n';
+             profile_end++);
+
+        /* If we run out of buffer space, read a new chunk */
+        if (!*profile_end) {
+                unsigned int token_size;
+                gsize bytes_read;
+
+                /* If we are out of space and we are not on the first or
+                   the last byte, then we have run out of things to read */
+                if (profile_end > profile_buf &&
+                    profile_end < profile_buf + sizeof (profile_buf) - 1) {
+                        profile_swap = 0;
+                        return "";
+                }
+
+                /* Move what we have of the token to the start of the buffer,
+                   fill the rest of the buffer with new data and start reading
+                   from the beginning */
+                token_size = profile_end - token;
+                if (token_size >= sizeof (profile_buf) - 1) {
+                        g_warning("Oversize token in profile");
+                        return "";
+                }
+                memmove(profile_buf, token, token_size);
+                g_io_channel_read_chars(channel, profile_buf + token_size,
+                                        sizeof (profile_buf) - token_size - 1,
+                                        &bytes_read, &error);
+                if (error) {
+                        g_warning("Read error: %s", error->message);
+                        return "";
+                }
+                if (bytes_read < 1) {
+                        profile_swap = 0;
+                        return "";
+                }
+                profile_end = profile_buf;
+                profile_buf[token_size + bytes_read] = 0;
+                goto seek_profile_end;
+        }
+
+        profile_swap = *profile_end;
+        *profile_end = 0;
+        return token;
+}
+
+int profile_read_next(void)
+/* Skip to the next line
+   FIXME should skip multiple blank lines */
+{
+        const char *s;
+
+        do {
+                s = profile_read();
+        } while (s[0]);
+        if (profile_swap == '\n') {
+                profile_swap = ' ';
+                return TRUE;
+        }
+        return FALSE;
+}
+
+int profile_write(const char *str)
+/* Write a string to the open profile */
+{
+        GError *error = NULL;
+        gsize bytes_written;
+
+        if (profile_read_only || !str)
+                return 0;
+        if (!channel)
+                return 1;
+        g_io_channel_write_chars(channel, str, strlen(str), &bytes_written,
+                                 &error);
+        if (error) {
+                g_warning("Write error: %s", error->message);
+                return 1;
+        }
+        return 0;
+}
+
+int profile_sync_int(int *var)
+/* Read or write an integer variable depending on the profile mode */
+{
+        if (profile_read_only) {
+                const char *s;
+                int n;
+
+                s = profile_read();
+                if (s[0]) {
+                        n = atoi(s);
+                        if (n || (s[0] == '0' && !s[1])) {
+                                *var = n;
+                                return 0;
+                        }
+                }
+        } else
+                return profile_write(va(" %d", *var));
+        return 1;
+}
+
+int profile_sync_short(short *var)
+/* Read or write a short integer variable depending on the profile mode */
+{
+        int value = *var;
+
+        if (profile_sync_int(&value))
+                return 1;
+        if (!profile_read_only)
+                return 0;
+        *var = (short)value;
+        return 0;
+}
+
+void version_read(void)
+{
+        int version;
+
+        version = atoi(profile_read());
+        if (version != 0)
+                g_warning("Loading a profile with incompatible version %d "
+                          "(expected %d)", version, PROFILE_VERSION);
+}
+
+/*
+        Main and signal handling
+*/
+
+#define NUM_PROFILE_CMDS (sizeof (profile_cmds) / sizeof (*profile_cmds))
+
+int profile_line, log_level = 4;
+
+static char *log_filename = NULL;
+static FILE *log_file = NULL;
+
+/* Profile commands table */
+static struct {
+        const char *name;
+        void (*read_func)(void);
+        void (*write_func)(void);
+} profile_cmds[] = {
+        { "version",      version_read,      NULL               },
+        { "window",       window_sync,       window_sync        },
+        { "options",      options_sync,      options_sync       },
+        { "recognize",    recognize_sync,    recognize_sync     },
+        { "blocks",       blocks_sync,       blocks_sync        },
+        { "bad_keycodes", bad_keycodes_read, bad_keycodes_write },
+        { "sample",       sample_read,       samples_write      },
+};
+
+/* Command line arguments */
+static GOptionEntry command_line_opts[] = {
+        { "log-level", 0, 0, G_OPTION_ARG_INT, &log_level,
+          "Log threshold (0=silent, 7=debug)", "4" },
+        { "log-file", 0, 0, G_OPTION_ARG_STRING, &log_filename,
+          "Log file to use instead of stdout", PACKAGE ".log" },
+        { "xid", 0, 0, G_OPTION_ARG_NONE, &window_embedded,
+          "Embed the main window (XEMBED)", NULL },
+        { "show-window", 0, 0, G_OPTION_ARG_NONE, &window_force_show,
+          "Show the main window", NULL },
+        { "hide-window", 0, 0, G_OPTION_ARG_NONE, &window_force_hide,
+          "Don't show the main window", NULL },
+        { "window-x", 0, 0, G_OPTION_ARG_INT, &window_force_x,
+          "Horizontal window position", "512" },
+        { "window-y", 0, 0, G_OPTION_ARG_INT, &window_force_y,
+          "Vertical window position", "768" },
+        { "dock-window", 0, 0, G_OPTION_ARG_INT, &window_force_docked,
+          "Docking (0=off, 1=top, 2=bottom)", "0" },
+        { "window-struts", 0, 0, G_OPTION_ARG_NONE, &window_struts,
+          "Reserve space when docking, see manpage", NULL },
+        { "keyboard-only", 0, 0, G_OPTION_ARG_NONE, &keyboard_only,
+          "Show on-screen keyboard only", NULL },
+        { "profile", 0, 0, G_OPTION_ARG_STRING, &force_profile,
+          "Full path to profile file to load", "profile" },
+        { "read-only", 0, 0, G_OPTION_ARG_NONE, &force_read_only,
+          "Do not save changes to the profile", NULL },
+        { "disable-overwrite", 0, 0, G_OPTION_ARG_NONE, &key_disable_overwrite,
+          "Do not modify the keymap", NULL },
+
+        /* Sentinel */
+        { NULL, 0, 0, 0, NULL, NULL, NULL }
+};
+
+/* If any of these things happen, try to save and exit cleanly -- gdb is not
+   affected by any of these signals being caught */
+static int catch_signals[] = {
+       SIGSEGV,
+       SIGHUP,
+       SIGINT,
+       SIGTERM,
+       SIGQUIT,
+       SIGALRM,
+        -1
+};
+
+void save_profile(){
+        if (!window_embedded && profile_open_write()) {
+                unsigned int i;
+
+                profile_write(va("version %d\n", PROFILE_VERSION));
+                for (i = 0; i < NUM_PROFILE_CMDS; i++)
+                        if (profile_cmds[i].write_func)
+                                profile_cmds[i].write_func();
+                if (profile_close())
+                        g_debug("Profile saved");
+        }
+}
+
+void cleanup(void)
+{
+        static int finished;
+
+        /* Run once */
+        if (finished) {
+                g_debug("Cleanup already called");
+                return;
+        }
+        finished = TRUE;
+        g_message("Cleaning up");
+
+        /* Explicit cleanup */
+        cell_widget_cleanup();
+        window_cleanup();
+        key_event_cleanup();
+        if (!window_embedded)
+                single_instance_cleanup();
+
+        /* Save profile */
+        save_profile();
+
+        /* Close log file */
+        if (log_file)
+                fclose(log_file);
+}
+
+static void catch_sigterm(int sig)
+/* Terminated by shutdown */
+{
+        g_warning("Caught signal %d, cleaning up", sig);
+        cleanup();
+        exit(1);
+}
+
+static void hook_signals(void)
+/* Setup signal catchers */
+{
+        sigset_t sigset;
+        int *ps;
+
+        sigemptyset(&sigset);
+        ps = catch_signals;
+        while (*ps != -1) {
+                signal(*ps, catch_sigterm);
+                sigaddset(&sigset, *(ps++));
+        }
+        if (sigprocmask(SIG_UNBLOCK, &sigset, NULL) == -1)
+                log_errno("Failed to set signal blocking mask");
+}
+
+void log_print(const char *format, ...)
+/* Print to the log file or stderr */
+{
+        FILE *file;
+        va_list va;
+
+        file = log_file;
+        if (!file) {
+                if (window_embedded)
+                        return;
+                file = stderr;
+        }
+        va_start(va, format);
+        vfprintf(file, format, va);
+        va_end(va);
+}
+
+void log_func(const gchar *domain, GLogLevelFlags level, const gchar *message)
+{
+        const char *prefix = "", *postfix = "\n", *pmsg;
+
+        if (level > log_level)
+                goto skip_print;
+
+        /* Do not print empty messages */
+        for (pmsg = message; *pmsg <= ' '; pmsg++)
+                if (!*pmsg)
+                        goto skip_print;
+
+        /* Format the message */
+        switch (level & G_LOG_LEVEL_MASK) {
+        case G_LOG_LEVEL_DEBUG:
+                prefix = "| ";
+                break;
+        case G_LOG_LEVEL_INFO:
+        case G_LOG_LEVEL_MESSAGE:
+                if (log_level > G_LOG_LEVEL_INFO) {
+                        prefix = "\n";
+                        postfix = ":\n";
+                }
+                break;
+        case G_LOG_LEVEL_WARNING:
+                if (log_level > G_LOG_LEVEL_INFO)
+                        prefix = "* ";
+                else if (log_level > G_LOG_LEVEL_WARNING)
+                        prefix = "WARNING: ";
+                else
+                        prefix = PACKAGE ": ";
+                break;
+        case G_LOG_LEVEL_CRITICAL:
+        case G_LOG_LEVEL_ERROR:
+                if (log_level > G_LOG_LEVEL_INFO)
+                        prefix = "* ERROR! ";
+                else if (log_level > G_LOG_LEVEL_WARNING)
+                        prefix = "ERROR: ";
+                else
+                        prefix = PACKAGE ": ERROR! ";
+                break;
+        default:
+                break;
+        }
+        if (domain)
+                log_print("%s[%s] %s%s", prefix, domain, message, postfix);
+        else
+                log_print("%s%s%s", prefix, message, postfix);
+
+skip_print:
+        if (level & G_LOG_FLAG_FATAL || level & G_LOG_LEVEL_ERROR)
+                abort();
+}
+
+void trace_full(const char *file, const char *func, const char *format, ...)
+{
+        char buf[256];
+        va_list va;
+
+        if (LOG_LEVEL_TRACE > log_level)
+                return;
+        va_start(va, format);
+        vsnprintf(buf, sizeof(buf), format, va);
+        va_end(va);
+        log_print(": %s:%s() %s\n", file, func, buf);
+}
+
+void log_errno(const char *string)
+{
+        log_func(NULL, G_LOG_LEVEL_WARNING,
+                 va("%s: %s", string, strerror(errno)));
+}
+
+static void second_instance(char *str)
+{
+        g_debug("Received '%s' from fifo", str);
+        if (str[0] == '0' || str[0] == 'H' || str[0] == 'h')
+                window_hide();
+        else if (str[0] == '1' || str[0] == 'S' || str[0] == 's')
+                window_show();
+        else if (str[0] == 'T' || str[0] == 't')
+                window_toggle();
+}
+
+void read_profile(){
+  const gchar *token;
+        if (profile_open_read()) {
+                profile_line = 1;
+                g_message("Parsing profile");
+                do {
+                        unsigned int i;
+
+                        token = profile_read();
+                        if (!token[0]) {
+                                if (profile_read_next())
+                                        continue;
+                                break;
+                        }
+                        for (i = 0; i < NUM_PROFILE_CMDS; i++)
+                                if (!g_ascii_strcasecmp(profile_cmds[i].name,
+                                                        token)) {
+                                        if (profile_cmds[i].read_func)
+                                                profile_cmds[i].read_func();
+                                        break;
+                                }
+                        if (i == NUM_PROFILE_CMDS)
+                                g_warning("Unrecognized profile command '%s'",
+                                          token);
+                        profile_line++;
+                } while (profile_read_next());
+                profile_close();
+                g_debug("Parsed %d commands", profile_line - 1);
+        }
+}
+
+int main(int argc, char *argv[])
+{
+  /*
+        GError *error;
+        const char *token;
+
+        /* Initialize GTK+ 
+        error = NULL;
+        if (!gtk_init_with_args(&argc, &argv,
+                                "grid-entry handwriting input panel",
+                                command_line_opts, NULL, &error))
+                return 0;
+
+        /* Setup log handler 
+        log_level = 1 << log_level;
+        g_log_set_handler(NULL, -1, (GLogFunc)log_func, NULL);
+
+        /* Try to open the log-file 
+        if (log_filename) {
+                log_file = fopen(log_filename, "w");
+                if (!log_file)
+                        g_warning("Failed to open log-file '%s'", log_filename);
+        }
+
+        /* See if the program is already running 
+        g_message("Starting " PACKAGE_STRING);
+        create_user_dir();
+        if (!window_embedded &&
+            single_instance_init((SingleInstanceFunc)second_instance,
+                                 window_force_hide ? "0" : "1")) {
+                gdk_notify_startup_complete();
+                g_message(PACKAGE_NAME " already running, quitting");
+                return 0;
+        }
+
+#ifdef HAVE_GNOME
+        /* Initialize GNOME for the Help button 
+        gnome_program_init(PACKAGE, VERSION, LIBGNOME_MODULE,
+                           argc, argv, NULL);
+#endif
+
+        /* Component initilization 
+        if (key_event_init(NULL)) {
+                GtkWidget *dialog;
+
+                dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL,
+                                                GTK_MESSAGE_ERROR,
+                                                GTK_BUTTONS_OK,
+                                                "Xtest extension not "
+                                                "supported");
+                gtk_window_set_title(GTK_WINDOW(dialog), "Initilization Error");
+                gtk_message_dialog_format_secondary_text(
+                        GTK_MESSAGE_DIALOG(dialog),
+                        "You\11r Xserver does not support the Xtest extension. "
+                        PACKAGE_NAME " cannot generate keystrokes without it.");
+                gtk_dialog_run(GTK_DIALOG(dialog));
+                gtk_widget_destroy(dialog);
+        }
+        recognize_init();
+
+        /* Read profile 
+        read_profile();
+
+        /* After loading samples and block enabled/disabled information,
+           update the samples 
+        update_enabled_samples();
+
+        /* Ensure that if there is a crash, data is saved properly 
+        hook_signals();
+        atexit(cleanup);
+
+        /* Initialize the interface 
+        g_message("Running interface");
+        window_create(NULL);
+
+        /* Startup notification is sent when the first window shows but in if
+           the window was closed during last start, it won't show at all so
+           we need to do this manually 
+        gdk_notify_startup_complete();
+
+        /* Run the interface 
+        if (!samples_loaded() && !keyboard_only)
+                startup_splash_show();
+        gtk_main();
+        cleanup();
+
+        /* Session statistics 
+        if (characters && inputs && log_level >= G_LOG_LEVEL_DEBUG) {
+                g_message("Session statistics --");
+                g_debug("Average strength: %d%%", strength_sum / inputs);
+                g_debug("Rewrites: %d out of %d inputs (%d%%)",
+                        rewrites, inputs, rewrites * 100 / inputs);
+                g_debug("Corrections: %d out of %d characters (%d%%)",
+                        corrections, characters,
+                        corrections * 100 / characters);
+                g_debug("KeyCodes overwrites: %d out of %d uses (%d%%)",
+                        key_overwrites, key_overwrites + key_recycles,
+                        key_recycles + key_overwrites ? key_overwrites * 100 /
+                        (key_recycles + key_overwrites) : 0);
+        }
+
+        return 0;
+        */
+}
diff --git a/src/options.c b/src/options.c
new file mode 100644 (file)
index 0000000..e1b41f8
--- /dev/null
@@ -0,0 +1,570 @@
+
+/*
+
+cellwriter -- a character recognition input method
+Copyright (C) 2007 Michael Levin <risujin@risujin.org>
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+*/
+
+#include "config.h"
+#include "common.h"
+#include "recognize.h"
+#include <stdlib.h>
+#include <string.h>
+#ifdef HAVE_GNOME
+#include <libgnome/libgnome.h>
+#endif
+
+/* preprocess.c */
+int ignore_stroke_dir, ignore_stroke_num;
+
+/* cellwidget.c */
+extern int cell_width, cell_height, cell_cols_pref, cell_rows_pref,
+           train_on_input, right_to_left, keyboard_enabled, xinput_enabled;
+extern GdkColor custom_active_color, custom_inactive_color,
+                custom_ink_color, custom_select_color;
+
+void cell_widget_render(void);
+void cell_widget_set_cursor(int recreate);
+void cell_widget_enable_xinput(int on);
+
+/* keywidget.c */
+GdkColor custom_key_color;
+int keyboard_size;
+
+void key_widget_update_colors(void);
+
+int status_menu_left_click;
+
+/*
+        Profile options
+*/
+
+static void color_sync(GdkColor *color)
+{
+        profile_sync_short((short*)&color->red);
+        profile_sync_short((short*)&color->green);
+        profile_sync_short((short*)&color->blue);
+}
+
+void options_sync(void)
+/* Read or write options. Order here is important for compatibility. */
+{
+        profile_write("options");
+        profile_sync_int(&cell_width);
+        profile_sync_int(&cell_height);
+        profile_sync_int(&cell_cols_pref);
+        profile_sync_int(&cell_rows_pref);
+        color_sync(&custom_active_color);
+        color_sync(&custom_inactive_color);
+        color_sync(&custom_select_color);
+        color_sync(&custom_ink_color);
+        profile_sync_int(&train_on_input);
+        profile_sync_int(&ignore_stroke_dir);
+        profile_sync_int(&ignore_stroke_num);
+        profile_sync_int(&wordfreq_enable);
+        profile_sync_int(&right_to_left);
+        color_sync(&custom_key_color);
+        profile_sync_int(&keyboard_enabled);
+        profile_sync_int(&xinput_enabled);
+        profile_sync_int(&style_colors);
+        profile_sync_int(&status_menu_left_click);
+        profile_write("\n");
+}
+
+/*
+        Unicode blocks list
+*/
+
+static void unicode_block_toggled(GtkCellRendererToggle *renderer, gchar *path,
+                                  GtkListStore *blocks_store)
+{
+        UnicodeBlock *block;
+        GtkTreePath *tree_path;
+        GtkTreeIter iter;
+        GValue value;
+        gboolean enabled;
+        int index;
+
+        /* Get the block this checkbox references */
+        tree_path = gtk_tree_path_new_from_string(path);
+        gtk_tree_model_get_iter(GTK_TREE_MODEL(blocks_store), &iter, tree_path);
+        index = gtk_tree_path_get_indices(tree_path)[0];
+        gtk_tree_path_free(tree_path);
+        block = unicode_blocks + index;
+
+        /* Toggle its value */
+        memset(&value, 0, sizeof (value));
+        gtk_tree_model_get_value(GTK_TREE_MODEL(blocks_store), &iter, 0,
+                                 &value);
+        enabled = !g_value_get_boolean(&value);
+        gtk_list_store_set(blocks_store, &iter, 0, enabled, -1);
+        unicode_block_toggle(index, enabled);
+}
+
+static GtkWidget *create_blocks_list(void)
+{
+        GtkWidget *view, *scrolled;
+        GtkTreeIter iter;
+        GtkTreeViewColumn *column;
+        GtkListStore *blocks_store;
+        GtkCellRenderer *renderer;
+        UnicodeBlock *block;
+
+        /* Tree view */
+        blocks_store = gtk_list_store_new(2, G_TYPE_BOOLEAN, G_TYPE_STRING);
+        view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(blocks_store));
+        gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(view), FALSE);
+        gtk_tooltips_set_tip(tooltips, view,
+                             "Controls which blocks are enabled for "
+                             "recognition and appear in the training mode "
+                             "combo box.", NULL);
+
+        /* Column */
+        column = gtk_tree_view_column_new();
+        gtk_tree_view_insert_column(GTK_TREE_VIEW(view), column, 0);
+        renderer = gtk_cell_renderer_toggle_new();
+        g_signal_connect(G_OBJECT(renderer), "toggled",
+                         G_CALLBACK(unicode_block_toggled), blocks_store);
+        gtk_tree_view_column_pack_start(column, renderer, FALSE);
+        gtk_tree_view_column_add_attribute(column, renderer, "active", 0);
+        renderer = gtk_cell_renderer_text_new();
+        gtk_tree_view_column_pack_start(column, renderer, TRUE);
+        gtk_tree_view_column_add_attribute(column, renderer, "text", 1);
+
+        /* Fill blocks list */
+        block = unicode_blocks;
+        while (block->name) {
+                gtk_list_store_append(blocks_store, &iter);
+                gtk_list_store_set(blocks_store, &iter, 0, block->enabled,
+                                   1, block->name, -1);
+                block++;
+        }
+
+        /* Scrolled window */
+        scrolled = gtk_scrolled_window_new(NULL, NULL);
+        gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled),
+                                            GTK_SHADOW_ETCHED_IN);
+        gtk_container_add(GTK_CONTAINER(scrolled), view);
+
+        return scrolled;
+}
+
+/*
+        Options dialog
+*/
+
+#define CELL_WIDTH_MIN  24
+#define CELL_HEIGHT_MIN 48
+#define CELL_HEIGHT_MAX 96
+
+static GtkWidget *options_dialog = NULL, *cell_width_spin, *cell_height_spin,
+                 *color_table;
+
+static void close_dialog(void)
+{
+  save_profile();
+        gtk_widget_hide(options_dialog);
+}
+
+static void color_set(GtkColorButton *button, GdkColor *color)
+{
+        gtk_color_button_get_color(button, color);
+        window_update_colors();
+}
+
+static void ink_color_set(void)
+{
+        cell_widget_set_cursor(TRUE);
+}
+
+static void xinput_enabled_toggled(void)
+{
+        cell_widget_enable_xinput(xinput_enabled);
+}
+
+static void spin_value_changed_int(GtkSpinButton *button, int *value)
+{
+        *value = (int)gtk_spin_button_get_value(button);
+}
+
+static void spin_value_changed_int_repack(GtkSpinButton *button, int *value)
+{
+        spin_value_changed_int(button, value);
+        window_pack();
+}
+
+static void check_button_toggled(GtkToggleButton *button, int *value)
+{
+        *value = gtk_toggle_button_get_active(button);
+}
+
+static void check_button_toggled_repack(GtkToggleButton *button, int *value)
+{
+        check_button_toggled(button, value);
+        window_pack();
+}
+
+static GtkWidget *label_new_markup(const char *s)
+{
+        GtkWidget *w;
+
+        w = gtk_label_new(NULL);
+        gtk_label_set_markup(GTK_LABEL(w), s);
+        gtk_misc_set_alignment(GTK_MISC(w), 0, 0.5);
+        return w;
+}
+
+static GtkWidget *spacer_new(int width, int height)
+{
+        GtkWidget *w;
+
+        w = gtk_hbox_new(FALSE, 0);
+        gtk_widget_set_size_request(w, width, height);
+        return w;
+}
+
+static GtkWidget *spin_button_new_int(int min, int max, int *variable,
+                                      int repack)
+{
+        GtkWidget *w;
+
+        w = gtk_spin_button_new_with_range(min, max, 1.);
+        gtk_spin_button_set_value(GTK_SPIN_BUTTON(w), *variable);
+        g_signal_connect(G_OBJECT(w), "value-changed",
+                         repack ? G_CALLBACK(spin_value_changed_int_repack) :
+                                  G_CALLBACK(spin_value_changed_int), variable);
+        return w;
+}
+
+static GtkWidget *check_button_new(const char *label, int *variable, int repack)
+{
+        GtkWidget *w;
+
+        w = gtk_check_button_new_with_label(label);
+        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w), *variable);
+        g_signal_connect(G_OBJECT(w), "toggled",
+                         repack ? G_CALLBACK(check_button_toggled_repack) :
+                                  G_CALLBACK(check_button_toggled), variable);
+        return w;
+}
+
+static void cell_height_value_changed(void)
+{
+        gtk_spin_button_set_range(GTK_SPIN_BUTTON(cell_width_spin),
+                                  CELL_WIDTH_MIN, cell_height);
+}
+
+static void cell_width_value_changed(void)
+{
+        int min;
+
+        min = CELL_HEIGHT_MIN > cell_width ? CELL_HEIGHT_MIN : cell_width;
+        gtk_spin_button_set_range(GTK_SPIN_BUTTON(cell_height_spin),
+                                  min, CELL_HEIGHT_MAX);
+}
+
+static void style_colors_changed(void)
+{
+#if GTK_CHECK_VERSION(2, 10, 0)
+        gtk_widget_set_sensitive(color_table, !style_colors);
+        window_update_colors();
+#endif
+}
+
+#ifdef HAVE_GNOME
+
+static void help_clicked(void)
+{
+        GError *error = NULL;
+
+        gnome_url_show(CELLWRITER_URL, &error);
+        if (error)
+                g_warning("Failed to launch help: %s", error->message);
+}
+
+#endif
+
+static GtkWidget *create_color_table(void)
+{
+        GtkWidget *table;
+        int i, entries;
+
+        struct {
+                const char *string;
+                GdkColor *color;
+                int reset_cursor;
+        } colors[] = {
+                { "<b>Custom colors:</b>", NULL, FALSE },
+                { "Used cell:", &custom_active_color, FALSE },
+                { "Blank cell:", &custom_inactive_color, FALSE },
+                { "Highlight:", &custom_select_color, FALSE },
+                { "Text and ink:", &custom_ink_color, TRUE  },
+                { "Key face:", &custom_key_color, FALSE },
+        };
+
+        entries = (int)(sizeof (colors) / sizeof (*colors));
+        table = gtk_table_new(entries, 2, TRUE);
+        for (i = 0; i < entries; i++) {
+                GtkWidget *w, *hbox;
+
+                /* Headers */
+                if (!colors[i].color)
+                        w = label_new_markup(colors[i].string);
+
+                /* Color label */
+                else {
+                        hbox = gtk_hbox_new(FALSE, 0);
+                        gtk_box_pack_start(GTK_BOX(hbox), spacer_new(16, -1),
+                                           FALSE, FALSE, 0);
+                        w = label_new_markup(colors[i].string);
+                        gtk_box_pack_start(GTK_BOX(hbox), w, FALSE, FALSE, 0);
+                        gtk_misc_set_alignment(GTK_MISC(w), 0, 0.5);
+                        w = hbox;
+                }
+
+                gtk_table_attach(GTK_TABLE(table), w, 0, 1, i, i + 1,
+                                 GTK_EXPAND | GTK_FILL, GTK_SHRINK, 0, 0);
+                if (!colors[i].color)
+                        continue;
+
+                /* Attach color selection button */
+                w = gtk_color_button_new_with_color(colors[i].color);
+                g_signal_connect(G_OBJECT(w), "color-set",
+                                 G_CALLBACK(color_set), colors[i].color);
+                gtk_table_attach(GTK_TABLE(table), w, 1, 2, i, i + 1,
+                                 GTK_EXPAND | GTK_FILL, GTK_SHRINK, 0, 0);
+
+                /* Some colors reset the cursor */
+                if (colors[i].reset_cursor)
+                        g_signal_connect(G_OBJECT(w), "color-set",
+                                         G_CALLBACK(ink_color_set), NULL);
+        }
+        return table;
+}
+
+static void window_docking_changed(GtkComboBox *combo)
+{
+        int mode;
+
+        mode = gtk_combo_box_get_active(combo);
+        window_set_docked(mode);
+}
+
+static void create_dialog(void)
+{
+        GtkWidget *vbox, *hbox, *vbox2, *notebook, *w;
+
+        if (options_dialog)
+                return;
+        vbox = gtk_vbox_new(FALSE, 0);
+
+        /* Buttons box */
+        hbox = gtk_hbutton_box_new();
+        gtk_box_pack_end(GTK_BOX(vbox), hbox, FALSE, TRUE, 0);
+        gtk_button_box_set_layout(GTK_BUTTON_BOX(hbox), GTK_BUTTONBOX_END);
+
+        /* Close button */
+        w = gtk_button_new_with_label("Close");
+        gtk_button_set_image(GTK_BUTTON(w),
+                             gtk_image_new_from_stock(GTK_STOCK_CLOSE,
+                                                      GTK_ICON_SIZE_BUTTON));
+        gtk_box_pack_start(GTK_BOX(hbox), w, FALSE, FALSE, 0);
+        g_signal_connect(G_OBJECT(w), "clicked",
+                         G_CALLBACK(close_dialog), NULL);
+
+        gtk_box_pack_end(GTK_BOX(vbox), spacer_new(-1, 8), FALSE, TRUE, 0);
+
+        /* Create notebook */
+        notebook = gtk_notebook_new();
+        gtk_box_pack_start(GTK_BOX(vbox), notebook, TRUE, TRUE, 0);
+
+        /* Colors page */
+        vbox2 = gtk_vbox_new(FALSE, 0);
+        w = gtk_label_new("Colors");
+        gtk_notebook_append_page(GTK_NOTEBOOK(notebook), vbox2, w);
+        gtk_container_set_border_width(GTK_CONTAINER(vbox2), 8);
+
+        /* Colors -> Use style */
+        w = check_button_new("Use default theme colors", &style_colors, FALSE);
+        g_signal_connect(G_OBJECT(w), "toggled",
+                         G_CALLBACK(style_colors_changed), NULL);
+        gtk_box_pack_start(GTK_BOX(vbox2), w, FALSE, FALSE, 0);
+
+        /* Colors -> Custom colors */
+        gtk_box_pack_start(GTK_BOX(vbox2), spacer_new(-1, 8), FALSE, FALSE, 0);
+        color_table = create_color_table();
+        gtk_box_pack_start(GTK_BOX(vbox2), color_table, FALSE, FALSE, 0);
+        style_colors_changed();
+
+        /* Unicode page */
+        vbox2 = gtk_vbox_new(FALSE, 0);
+        w = gtk_label_new("Languages");
+        gtk_notebook_append_page(GTK_NOTEBOOK(notebook), vbox2, w);
+        gtk_container_set_border_width(GTK_CONTAINER(vbox2), 8);
+
+        /* Unicode -> Displayed blocks */
+        w = label_new_markup("<b>Enabled Unicode blocks</b>");
+        gtk_box_pack_start(GTK_BOX(vbox2), w, FALSE, FALSE, 0);
+        hbox = gtk_hbox_new(FALSE, 0);
+        gtk_box_pack_start(GTK_BOX(hbox), spacer_new(-1, 4), FALSE, FALSE, 0);
+        gtk_box_pack_start(GTK_BOX(vbox2), hbox, FALSE, FALSE, 0);
+
+        /* Unicode -> Blocks list */
+        hbox = gtk_hbox_new(FALSE, 0);
+        gtk_box_pack_start(GTK_BOX(hbox), spacer_new(16, -1), FALSE, FALSE, 0);
+        w = create_blocks_list();
+        gtk_box_pack_start(GTK_BOX(hbox), w, TRUE, TRUE, 0);
+        gtk_box_pack_start(GTK_BOX(vbox2), hbox, TRUE, TRUE, 0);
+
+        /* Recognition -> Duplicate glyphs */
+        gtk_box_pack_start(GTK_BOX(vbox2), spacer_new(-1, 8), FALSE, FALSE, 0);
+        w = label_new_markup("<b>Language options</b>");
+        gtk_box_pack_start(GTK_BOX(vbox2), w, FALSE, FALSE, 0);
+
+        /* Unicode -> Disable Latin letters */
+        hbox = gtk_hbox_new(FALSE, 0);
+        gtk_box_pack_start(GTK_BOX(hbox), spacer_new(16, -1), FALSE, FALSE, 0);
+        w = check_button_new("Disable Basic Latin letters",
+                             &no_latin_alpha, TRUE);
+        gtk_box_pack_start(GTK_BOX(hbox), w, TRUE, TRUE, 0);
+        gtk_box_pack_start(GTK_BOX(vbox2), hbox, FALSE, FALSE, 0);
+        gtk_tooltips_set_tip(tooltips, w,
+                             "If you have trained both the Basic Latin block "
+                             "and a block with characters similar to Latin "
+                             "letters (for instance, Cyrillic) you can disable "
+                             "the Basic Latin letters in order to use only "
+                             "numbers and symbols from Basic Latin.", NULL);
+
+        /* Unicode -> Right-to-left */
+        hbox = gtk_hbox_new(FALSE, 0);
+        gtk_box_pack_start(GTK_BOX(hbox), spacer_new(16, -1), FALSE, FALSE, 0);
+        w = check_button_new("Enable right-to-left mode",
+                             &right_to_left, TRUE);
+        gtk_box_pack_start(GTK_BOX(hbox), w, TRUE, TRUE, 0);
+        gtk_box_pack_start(GTK_BOX(vbox2), hbox, FALSE, FALSE, 0);
+        gtk_tooltips_set_tip(tooltips, w,
+                             PACKAGE_NAME " will expect you to write from "
+                             "the rightmost cell to the left and will pad "
+                             "cells and create new lines accordingly.", NULL);
+
+        /* Recognition page */
+        vbox2 = gtk_vbox_new(FALSE, 0);
+        gtk_container_set_border_width(GTK_CONTAINER(vbox2), 8);
+        w = gtk_label_new("Recognition");
+        gtk_notebook_append_page(GTK_NOTEBOOK(notebook), vbox2, w);
+
+        /* Recognition -> Samples */
+        w = label_new_markup("<b>Training samples</b>");
+        gtk_box_pack_start(GTK_BOX(vbox2), w, FALSE, FALSE, 0);
+
+        /* Recognition -> Samples -> Train on input */
+        hbox = gtk_hbox_new(FALSE, 0);
+        gtk_box_pack_start(GTK_BOX(hbox), spacer_new(16, -1), FALSE, FALSE, 0);
+        w = check_button_new("Train on input when entering",
+                             &train_on_input, FALSE);
+        gtk_box_pack_start(GTK_BOX(hbox), w, TRUE, TRUE, 0);
+        gtk_box_pack_start(GTK_BOX(vbox2), hbox, FALSE, FALSE, 0);
+        gtk_tooltips_set_tip(tooltips, w,
+                             "When enabled, input characters will be used as "
+                             "training samples when 'Enter' is pressed. This "
+                             "is a good way to quickly build up many samples, "
+                             "but can generate poor samples if your writing "
+                             "gets sloppy.", NULL);
+
+        /* Recognition -> Samples -> Maximum */
+        hbox = gtk_hbox_new(FALSE, 0);
+        gtk_box_pack_start(GTK_BOX(hbox), spacer_new(16, -1), FALSE, FALSE, 0);
+        w = label_new_markup("Samples per character: ");
+        gtk_box_pack_start(GTK_BOX(hbox), w, FALSE, FALSE, 0);
+        w = spin_button_new_int(2, SAMPLES_MAX, &samples_max, FALSE);
+        gtk_box_pack_start(GTK_BOX(hbox), w, FALSE, FALSE, 0);
+        gtk_box_pack_start(GTK_BOX(vbox2), hbox, FALSE, FALSE, 0);
+        gtk_tooltips_set_tip(tooltips, w,
+                             "The maximum number of training samples kept per "
+                             "character. Lower this value if recognition is "
+                             "too slow or the program uses too much memory.",
+                             NULL);
+
+        /* Recognition -> Word context */
+        gtk_box_pack_start(GTK_BOX(vbox2), spacer_new(-1, 8), FALSE, FALSE, 0);
+        w = label_new_markup("<b>Word context</b>");
+        gtk_box_pack_start(GTK_BOX(vbox2), w, FALSE, FALSE, 0);
+
+        /* Recognition -> Word context -> English */
+        hbox = gtk_hbox_new(FALSE, 0);
+        gtk_box_pack_start(GTK_BOX(hbox), spacer_new(16, -1), FALSE, FALSE, 0);
+        w = check_button_new("Enable English word context",
+                             &wordfreq_enable, FALSE);
+        gtk_box_pack_start(GTK_BOX(hbox), w, TRUE, TRUE, 0);
+        gtk_box_pack_start(GTK_BOX(vbox2), hbox, FALSE, FALSE, 0);
+        gtk_tooltips_set_tip(tooltips, w,
+                             "Use a dictionary of the most frequent English "
+                             "words to assist recognition. Also aids in "
+                             "consistent recognition of numbers and "
+                             "capitalization.", NULL);
+
+        /* Recognition -> Preprocessor */
+        gtk_box_pack_start(GTK_BOX(vbox2), spacer_new(-1, 8), FALSE, FALSE, 0);
+        w = label_new_markup("<b>Preprocessor</b>");
+        gtk_box_pack_start(GTK_BOX(vbox2), w, FALSE, FALSE, 0);
+
+        /* Recognition -> Preprocessor -> Ignore stroke direction */
+        hbox = gtk_hbox_new(FALSE, 0);
+        gtk_box_pack_start(GTK_BOX(hbox), spacer_new(16, -1), FALSE, FALSE, 0);
+        w = check_button_new("Ignore stroke direction",
+                             &ignore_stroke_dir, FALSE);
+        gtk_box_pack_start(GTK_BOX(hbox), w, TRUE, TRUE, 0);
+        gtk_box_pack_start(GTK_BOX(vbox2), hbox, FALSE, FALSE, 0);
+        gtk_tooltips_set_tip(tooltips, w,
+                             "Match input strokes with training sample strokes "
+                             "that were drawn in the opposite direction. "
+                             "Disabling this can boost recognition speed.",
+                             NULL);
+
+        /* Recognition -> Preprocessor -> Ignore stroke number */
+        hbox = gtk_hbox_new(FALSE, 0);
+        gtk_box_pack_start(GTK_BOX(hbox), spacer_new(16, -1), FALSE, FALSE, 0);
+        w = check_button_new("Match differing stroke numbers",
+                             &ignore_stroke_num, FALSE);
+        gtk_box_pack_start(GTK_BOX(hbox), w, TRUE, TRUE, 0);
+        gtk_box_pack_start(GTK_BOX(vbox2), hbox, FALSE, FALSE, 0);
+        gtk_tooltips_set_tip(tooltips, w,
+                             "Match inputs to training samples that do not "
+                             "have the same number of strokes. Disabling this "
+                             "can boost recognition speed.", NULL);
+
+        /* Create dialog window */
+        options_dialog = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+        g_signal_connect(G_OBJECT(options_dialog), "delete_event",
+                         G_CALLBACK(gtk_widget_hide_on_delete), NULL);
+        gtk_window_set_destroy_with_parent(GTK_WINDOW(options_dialog), TRUE);
+        gtk_window_set_resizable(GTK_WINDOW(options_dialog), TRUE);
+        gtk_window_set_title(GTK_WINDOW(options_dialog), "CellWriter Setup");
+        gtk_container_set_border_width(GTK_CONTAINER(options_dialog), 8);
+        gtk_container_add(GTK_CONTAINER(options_dialog), vbox);
+        if (!window_embedded)
+                gtk_window_set_transient_for(GTK_WINDOW(options_dialog),
+                                             GTK_WINDOW(window));
+}
+
+void options_dialog_open(void)
+{
+        create_dialog();
+        gtk_widget_show_all(options_dialog);
+}
+
diff --git a/src/preprocess.c b/src/preprocess.c
new file mode 100644 (file)
index 0000000..accaa21
--- /dev/null
@@ -0,0 +1,300 @@
+
+/*
+
+cellwriter -- a character recognition input method
+Copyright (C) 2007 Michael Levin <risujin@risujin.org>
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+*/
+
+#include "config.h"
+#include "common.h"
+#include "recognize.h"
+#include <string.h>
+
+/*
+        Preprocessing engine
+*/
+
+/* Maximum and variable versions of the number of samples to prepare for
+   thorough examination */
+#define PREP_MAX     (SAMPLES_MAX * 4)
+#define PREP_SAMPLES (samples_max * 4)
+
+/* Greedy mapping */
+#define VALUE_MAX 2048.f
+#define VALUE_MIN 1024.f
+
+/* Penalties (proportion of final score deducted) */
+#define VERTICAL_PENALTY 16.00f
+#define GLUABLE_PENALTY   0.08f
+#define GLUE_PENALTY      0.02f
+
+int ignore_stroke_dir = TRUE, ignore_stroke_num = TRUE, prep_examined;
+
+static float measure_partial(Stroke *as, Stroke *b, Vec2 *offset, float scale_b)
+{
+        Stroke *bs;
+        float value;
+        int b_len, min_len;
+
+        b_len = b->distance * scale_b / ROUGH_RESOLUTION + 0.5;
+        if (b_len < 4)
+                b_len = 4;
+        min_len = as->len >= b_len ? b_len : as->len;
+        bs = sample_stroke(NULL, b, b_len, min_len);
+        value = measure_strokes(as, bs, (MeasureFunc)measure_distance, offset,
+                                min_len, ROUGH_ELASTICITY);
+        stroke_free(bs);
+        return value;
+}
+
+static float greedy_map(Sample *larger, Sample *smaller, Transform *ptfm,
+                        Vec2 *offset)
+{
+        Transform tfm;
+        int i, unmapped_len;
+        float total;
+
+        unmapped_len = larger->len;
+
+        /* Prepare transform structure */
+        memset(&tfm, 0, sizeof (tfm));
+        *ptfm = tfm;
+        tfm.valid = TRUE;
+
+        for (i = 0, total = 0.f; i < smaller->len; i++) {
+                float best, best_reach = G_MAXFLOAT, best_value = G_MAXFLOAT,
+                      value, penalty = G_MAXFLOAT, seg_dist = 0.f;
+                int j, last_j = 0, best_j = 0, glue = 0;
+
+        glue_more:
+                for (j = 0, best = G_MAXFLOAT; j < larger->len; j++) {
+                        Stroke *stroke;
+                        float reach, scale;
+                        unsigned char gluable;
+
+                        if (tfm.order[j])
+                                continue;
+                        tfm.reverse[j] = FALSE;
+
+                        /* Do not glue on oversize segments */
+                        if (seg_dist +
+                            larger->strokes[j]->distance / 2 >
+                            smaller->strokes[i]->distance &&
+                            (larger->strokes[j]->spread > DOT_SPREAD ||
+                             smaller->strokes[i]->spread > DOT_SPREAD))
+                                continue;
+
+                        tfm.order[j] = i + 1;
+                        tfm.glue[j] = glue;
+
+                measure:
+                        reach = 0.f;
+                        gluable = 0;
+                        if (glue) {
+                                Vec2 v;
+                                Point *p1, *p2;
+                                unsigned char gluable2;
+
+                                /* Can we glue these strokes together? */
+                                if (!tfm.reverse[j]) {
+                                        gluable = larger->strokes[j]->
+                                                  gluable_start[last_j];
+                                        gluable2 = larger->strokes[last_j]->
+                                                   gluable_end[j];
+                                        if (gluable2 < gluable)
+                                                gluable = gluable2;
+                                        if (gluable >= GLUABLE_MAX) {
+                                                if (!ignore_stroke_dir)
+                                                        continue;
+                                                tfm.reverse[j] = TRUE;
+                                        }
+                                }
+                                if (tfm.reverse[j]) {
+                                        gluable = larger->strokes[j]->
+                                                  gluable_end[last_j];
+                                        gluable2 = larger->strokes[last_j]->
+                                                   gluable_start[j];
+                                        if (gluable2 < gluable)
+                                                gluable = gluable2;
+                                        if (gluable >= GLUABLE_MAX)
+                                                continue;
+                                }
+
+                                /* Get the inter-stroke (reach) distance */
+                                p1 = larger->strokes[last_j]->points +
+                                     (tfm.reverse[last_j] ? 0 :
+                                      larger->strokes[last_j]->len - 1);
+                                p2 = larger->strokes[j]->points +
+                                     (!tfm.reverse[j] ? 0 :
+                                      larger->strokes[j]->len - 1);
+                                vec2_set(&v, p2->x - p1->x,
+                                         p2->y - p1->y);
+                                reach = vec2_mag(&v);
+                        }
+
+                        /* Transform and measure the distance */
+                        stroke = transform_stroke(larger, &tfm, i);
+                        scale = smaller->distance /
+                                (reach + ptfm->reach + larger->distance);
+                        value = measure_partial(smaller->roughs[i], stroke,
+                                                offset, scale);
+                        stroke_free(stroke);
+
+                        /* Keep track of the best result */
+                        if (value < best && value < VALUE_MAX) {
+                                best = value;
+                                best_j = j;
+                                best_reach = reach;
+                                *ptfm = tfm;
+
+                                /* Penalize glue and reach distance */
+                                penalty = glue * GLUE_PENALTY +
+                                          gluable * GLUABLE_PENALTY /
+                                                    GLUABLE_MAX;
+                        }
+
+                        /* Bail if we have a really good match */
+                        if (value < VALUE_MIN)
+                                break;
+
+                        /* Glue on with reversed direction */
+                        if (ignore_stroke_dir && !tfm.reverse[j] &&
+                            larger->strokes[j]->spread > DOT_SPREAD) {
+                                tfm.reverse[j] = TRUE;
+                                goto measure;
+                        }
+
+                        tfm.reverse[j] = FALSE;
+                        tfm.order[j] = 0;
+                }
+                if (best < G_MAXFLOAT) {
+                        best_value = best;
+                        larger->penalty += penalty;
+                        smaller->penalty += penalty;
+                        seg_dist += best_reach +
+                                    larger->strokes[best_j]->distance;
+                        ptfm->reach += best_reach;
+                        tfm = *ptfm;
+
+                        /* If we still have strokes and we didn't just add on
+                           a dot, try gluing them on */
+                        unmapped_len--;
+                        if (unmapped_len >= smaller->len - i &&
+                            larger->strokes[best_j]->spread >
+                            DOT_SPREAD) {
+                                last_j = best_j;
+                                glue++;
+                                goto glue_more;
+                        }
+                }
+
+                /* Didn't map a target stroke? */
+                else if (!glue) {
+                        ptfm->valid = FALSE;
+                        return G_MAXFLOAT;
+                }
+
+                total += best_value;
+        }
+
+        /* Didn't assign all of the strokes? */
+        if (unmapped_len) {
+                ptfm->valid = FALSE;
+                return G_MAXFLOAT;
+        }
+
+        return total / smaller->len;
+}
+
+static int prep_sample(Sample *sample)
+{
+        Vec2 offset;
+        float dist;
+
+        /* Structural disqualification */
+        if (!sample->used || !sample->enabled ||
+            (!ignore_stroke_num && sample->len != input->len))
+                return FALSE;
+
+        prep_examined++;
+        sample->penalty = 0.f;
+
+        /* Account for displacement */
+        center_samples(&offset, sample, input);
+
+        /* Compare each input stroke to every stroke in the sample and
+           generate the stroke order information which will be used by other
+           engines */
+        if (input->len >= sample->len)
+                dist = greedy_map(input, sample, &sample->transform, &offset);
+        else {
+                vec2_set(&offset, -offset.x, -offset.y);
+                dist = greedy_map(sample, input, &sample->transform, &offset);
+        }
+        if (!sample->transform.valid)
+                return FALSE;
+
+        /* Undo square distortion */
+        dist = sqrtf(dist);
+        if (dist > MAX_DIST)
+                return FALSE;
+
+        /* Penalize vertical displacement */
+        sample->penalty += VERTICAL_PENALTY *
+                           offset.y * offset.y / SCALE / SCALE;
+
+        sample->ratings[ENGINE_PREP] = RATING_MAX -
+                                       RATING_MAX * dist / MAX_DIST;
+        return TRUE;
+}
+
+void engine_prep(void)
+{
+        Sample *sample, *list[PREP_MAX];
+        int i;
+
+        /* Rate every sample in every possible configuration */
+        list[0] = NULL;
+        prep_examined = 0;
+        sampleiter_reset();
+        while ((sample = sampleiter_next())) {
+                sample->disqualified = TRUE;
+                if (!sample->used || !sample->ch || !prep_sample(sample))
+                        continue;
+
+                /* Bubble-sort sample into the list */
+                for (i = 0; i < PREP_SAMPLES; i++)
+                        if (!list[i]) {
+                                list[i] = sample;
+                                if (i < PREP_MAX - 1)
+                                        list[i + 1] = NULL;
+                                break;
+                        } else if (list[i]->ratings[ENGINE_PREP] <
+                                   sample->ratings[ENGINE_PREP]) {
+                                memmove(list + i + 1, list + i,
+                                        (PREP_MAX - i - 1) * sizeof (*list));
+                                list[i] = sample;
+                                break;
+                        }
+        }
+
+        /* Qualify the best samples */
+        for (i = 0; i < PREP_SAMPLES && list[i]; i++)
+                list[i]->disqualified = FALSE;
+}
+
diff --git a/src/recognize.c b/src/recognize.c
new file mode 100644 (file)
index 0000000..0e68129
--- /dev/null
@@ -0,0 +1,737 @@
+
+/*
+
+cellwriter -- a character recognition input method
+Copyright (C) 2007 Michael Levin <risujin@risujin.org>
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+*/
+
+#include "config.h"
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <gtk/gtk.h>
+#include "common.h"
+#include "recognize.h"
+
+/* preprocess.c */
+int prep_examined;
+
+void engine_prep(void);
+
+/*
+        Engines
+*/
+
+Engine engines[] = {
+
+        /* Preprocessor engine must run first */
+        { "Key-point distance", engine_prep, MAX_RANGE, TRUE, -1, 0, 0 },
+
+        /* Averaging engines */
+        { "Average distance", engine_average, MAX_RANGE, TRUE, -1, 0, 0 },
+        { "Average angle", NULL, MAX_RANGE, TRUE, 0, 0, 0 },
+
+#ifndef DISABLE_WORDFREQ
+        /* Word frequency engine */
+        { "Word context", engine_wordfreq, MAX_RANGE / 3, FALSE, -1, 0, 0 },
+#endif
+};
+
+static int engine_rating(const Sample *sample, int j)
+/* Get the processed rating for engine j on a sample */
+{
+        int value;
+
+        if (!engines[j].range || engines[j].max < 1)
+                return 0;
+        value = ((int)sample->ratings[j] - engines[j].average) *
+                engines[j].range / engines[j].max;
+        if (engines[j].scale >= 0)
+                value = value * engines[j].scale / ENGINE_SCALE;
+        return value;
+}
+
+/*
+        Sample chain wrapper
+*/
+
+typedef struct SampleLink {
+        Sample sample;
+        struct SampleLink *prev, *next;
+} SampleLink;
+
+static SampleLink *samplelink_root = NULL, *samplelink_iter = NULL;
+static int current = 1;
+
+static Sample *sample_new(void)
+/* Allocate a link in the sample linked list */
+{
+        SampleLink *link;
+
+        link = g_malloc0(sizeof (*link));
+        link->next = samplelink_root;
+        if (samplelink_root)
+                samplelink_root->prev = link;
+        samplelink_root = link;
+        return &link->sample;
+}
+
+void sampleiter_reset(void)
+/* Reset the sample linked list iterator */
+{
+        samplelink_iter = samplelink_root;
+}
+
+Sample *sampleiter_next(void)
+/* Get the next sample link from the sample linked list iterator */
+{
+        SampleLink *link;
+
+        if (!samplelink_iter)
+                return NULL;
+        link = samplelink_iter;
+        samplelink_iter = samplelink_iter->next;
+        return &link->sample;
+}
+
+int samples_loaded(void)
+{
+        return samplelink_root != NULL;
+}
+
+/*
+        Samples
+*/
+
+int samples_max = 5, no_latin_alpha = FALSE;
+
+void clear_sample(Sample *sample)
+/* Free stroke data associated with a sample and reset its parameters */
+{
+        int i;
+
+        for (i = 0; i < sample->len; i++) {
+                stroke_free(sample->strokes[i]);
+                stroke_free(sample->roughs[i]);
+        }
+        memset(sample, 0, sizeof (*sample));
+}
+
+void copy_sample(Sample *dest, const Sample *src)
+/* Copy a sample, cloing its strokes, overwriting dest */
+{
+        int i;
+
+        *dest = *src;
+        for (i = 0; i < src->len; i++) {
+                dest->strokes[i] = stroke_clone(src->strokes[i], FALSE);
+                dest->roughs[i] = stroke_clone(src->roughs[i], FALSE);
+        }
+}
+
+static void process_gluable(const Sample *sample, int stroke_num)
+/* Calculates the lowest distance between the start or end of one stroke and any
+   other point on each other stroke in the sample */
+{
+        Point point;
+        Stroke *s1;
+        int i, start;
+
+        /* Dots cannot be glued */
+        s1 = sample->strokes[stroke_num];
+        memset(s1->gluable_start, -1, sizeof (s1->gluable_start));
+        memset(s1->gluable_end, -1, sizeof (s1->gluable_end));
+        if (s1->spread < DOT_SPREAD)
+                return;
+
+        start = TRUE;
+scan:
+        point = start ? s1->points[0] : s1->points[s1->len - 1];
+        for (i = 0; i < sample->len; i++) {
+                Vec2 v;
+                Stroke *s2;
+                float dist, min = GLUE_DIST;
+                int j;
+                char gluable;
+
+                s2 = sample->strokes[i];
+                if (i == stroke_num || s2->spread < DOT_SPREAD)
+                        continue;
+
+                /* Check the distance to the first point */
+                vec2_set(&v, s2->points[0].x - point.x,
+                         s2->points[0].y - point.y);
+                dist = vec2_mag(&v);
+                if (dist < min)
+                        min = dist;
+
+                /* Find the lowest distance from the glue point to any other
+                   point on the other stroke */
+                for (j = 0; j < s2->len - 1; j++) {
+                       Vec2 l, w;
+                       double dist, mag, dot;
+
+                        /* Vector l is a unit vector from point j to j + 1 */
+                       vec2_set(&l, s2->points[j].x - s2->points[j + 1].x,
+                                s2->points[j].y - s2->points[j + 1].y);
+                        mag = vec2_norm(&l, &l);
+
+                       /* Vector w is a vector from point j to our point */
+                       vec2_set(&w, s2->points[j].x - point.x,
+                                s2->points[j].y - point.y);
+
+                       /* For points that are not in between a segment,
+                          get the distance from the points themselves,
+                          otherwise get the distance from the segment line */
+                       dot = vec2_dot(&l, &w);
+                       if (dot < 0. || dot > mag) {
+                               vec2_set(&v, s2->points[j + 1].x - point.x,
+                                        s2->points[j + 1].y - point.y);
+                               dist = vec2_mag(&v);
+                       } else {
+                               dist = vec2_cross(&w, &l);
+                               if (dist < 0)
+                                       dist = -dist;
+                       }
+                        if (dist < min)
+                                min = dist;
+                }
+                gluable = min * GLUABLE_MAX / GLUE_DIST;
+                if (start)
+                        s1->gluable_start[i] = gluable;
+                else
+                        s1->gluable_end[i] = gluable;
+        }
+        if (start) {
+                start = FALSE;
+                goto scan;
+        }
+}
+
+void process_sample(Sample *sample)
+/* Generate cached properties of a sample */
+{
+        int i;
+        float distance;
+
+        if (sample->processed)
+                return;
+        sample->processed = TRUE;
+
+        /* Make sure all strokes have been processed first */
+        for (i = 0; i < sample->len; i++)
+                process_stroke(sample->strokes[i]);
+
+        /* Compute properties for each stroke */
+        vec2_set(&sample->center, 0., 0.);
+        for (i = 0, distance = 0.; i < sample->len; i++) {
+                Vec2 v;
+                Stroke *stroke;
+                float weight;
+                int points;
+
+                stroke = sample->strokes[i];
+
+                /* Add the stroke center to the center vector, weighted by
+                   length */
+                vec2_copy(&v, &stroke->center);
+                weight = stroke->spread < DOT_SPREAD ?
+                         DOT_SPREAD : stroke->distance;
+                vec2_scale(&v, &v, weight);
+                vec2_sum(&sample->center, &sample->center, &v);
+                distance += weight;
+
+                /* Get gluing distances */
+                process_gluable(sample, i);
+
+                /* Create a rough-sampled version */
+                points = stroke->distance / ROUGH_RESOLUTION + 0.5;
+                if (points < 4)
+                        points = 4;
+                sample->roughs[i] = sample_stroke(NULL, stroke, points, points);
+        }
+        vec2_scale(&sample->center, &sample->center, 1.f / distance);
+        sample->distance = distance;
+}
+
+void center_samples(Vec2 *ac_to_bc, Sample *a, Sample *b)
+/* Adjust for the difference between two sample centers */
+{
+        vec2_sub(ac_to_bc, &b->center, &a->center);
+}
+
+int char_disabled(int ch)
+/* Returns TRUE if a character is not renderable or is explicity disabled by
+   a setting (not counting disabled Unicode blocks) */
+{
+        return (no_latin_alpha && ch >= unicode_blocks[0].start &&
+                ch <= unicode_blocks[0].end && g_ascii_isalpha(ch)) ||
+               !g_unichar_isgraph(ch);
+}
+
+int sample_disqualified(const Sample *sample)
+/* Check disqualification conditions for a sample during recognition.
+   The preprocessor engine must run before any calls to this or
+   disqualification will not work. */
+{
+        if ((!ignore_stroke_num && sample->len != input->len) ||
+            !sample->enabled)
+                return 1;
+        if (sample->disqualified)
+                return 2;
+        if (char_disabled(sample->ch))
+                return 3;
+        return 0;
+}
+
+int sample_valid(const Sample *sample, int used)
+/* Check if this sample has changed since it was last referenced */
+{
+        if (!sample || !used)
+                return FALSE;
+        return sample->used == used;
+}
+
+static void sample_rating(Sample *sample)
+/* Get the composite processed rating on a sample */
+{
+        int i, rating;
+
+        if (!sample->ch || sample_disqualified(sample) ||
+            sample->penalty >= 1.f) {
+                sample->rating = RATING_MIN;
+                return;
+        }
+        for (i = 0, rating = 0; i < ENGINES; i++)
+                rating += engine_rating(sample, i);
+        rating *= 1.f - sample->penalty;
+        if (rating > RATING_MAX)
+                rating = RATING_MAX;
+        if (rating < RATING_MIN)
+                rating = RATING_MIN;
+        sample->rating = rating;
+}
+
+void update_enabled_samples(void)
+/* Run through the samples list and enable samples in enabled blocks */
+{
+        Sample *sample;
+
+        sampleiter_reset();
+        while ((sample = sampleiter_next())) {
+                UnicodeBlock *block;
+
+                sample->enabled = FALSE;
+                if (!sample->ch)
+                        continue;
+                block = unicode_blocks;
+                while (block->name) {
+                        if (sample->ch >= block->start &&
+                            sample->ch <= block->end) {
+                                sample->enabled = block->enabled;
+                                break;
+                        }
+                        block++;
+                }
+        }
+}
+
+void promote_sample(Sample *sample)
+/* Update usage counter for a sample */
+{
+        sample->used = current++;
+}
+
+void demote_sample(Sample *sample)
+/* Remove the sample from our set if we can */
+{
+        if (char_trained(sample->ch) > 1)
+                clear_sample(sample);
+        else
+                sample->used = 1;
+}
+
+Stroke *transform_stroke(Sample *src, Transform *tfm, int i)
+/* Create a new stroke by applying the transformation to the source */
+{
+        Stroke *stroke;
+        int k, j;
+
+        stroke = stroke_new(0);
+        for (k = 0, j = 0; k < STROKES_MAX && j < src->len; k++)
+                for (j = 0; j < src->len; j++)
+                        if (tfm->order[j] - 1 == i && tfm->glue[j] == k) {
+                                glue_stroke(&stroke, src->strokes[j],
+                                            tfm->reverse[j]);
+                                break;
+                        }
+        process_stroke(stroke);
+        return stroke;
+}
+
+/*
+        Recognition and training
+*/
+
+Sample *input = NULL;
+int strength_sum = 0;
+
+static GTimer *timer;
+
+void recognize_init(void)
+{
+#ifndef DISABLE_WORDFREQ
+        load_wordfreq();
+#endif
+        timer = g_timer_new();
+}
+
+void recognize_sample(Sample *sample, Sample **alts, int num_alts)
+{
+        gulong microsec;
+        int i, range, strength, msec;
+
+        g_timer_start(timer);
+        input = sample;
+        process_sample(input);
+
+        /* Clear ratings */
+        sampleiter_reset();
+        while ((sample = sampleiter_next())) {
+                memset(sample->ratings, 0, sizeof (sample->ratings));
+                sample->rating = 0;
+        }
+
+        /* Run engines */
+        for (i = 0, range = 0; i < ENGINES; i++) {
+                int rated = 0;
+
+                if (engines[i].func)
+                        engines[i].func();
+
+                /* Compute average and maximum value */
+                engines[i].max = 0;
+                engines[i].average = 0;
+                sampleiter_reset();
+                while ((sample = sampleiter_next())) {
+                        int value = 0;
+
+                        if (!sample->ch)
+                                continue;
+                        if (sample->ratings[i] > value)
+                                value = sample->ratings[i];
+                        if (!value && engines[i].ignore_zeros)
+                                continue;
+                        if (value > engines[i].max)
+                                engines[i].max = value;
+                        engines[i].average += value;
+                        rated++;
+                }
+                if (!rated)
+                        continue;
+                engines[i].average /= rated;
+                if (engines[i].max > 0)
+                        range += engines[i].range;
+                if (engines[i].max == engines[i].average) {
+                        engines[i].average = 0;
+                        continue;
+                }
+                engines[i].max -= engines[i].average;
+        }
+        if (!range) {
+                g_timer_elapsed(timer, &microsec);
+                msec = microsec / 100;
+                g_message("Recognized -- No ratings, %dms", msec);
+                input->ch = 0;
+                return;
+        }
+
+        /* Rank the top samples */
+        alts[0] = NULL;
+        sampleiter_reset();
+        while ((sample = sampleiter_next())) {
+                int j;
+
+                sample_rating(sample);
+                if (sample->rating < 1)
+                        continue;
+
+                /* Bubble-sort the new rating in */
+                for (j = 0; j < num_alts; j++)
+                        if (!alts[j]) {
+                                if (j < num_alts - 1)
+                                        alts[j + 1] = NULL;
+                                break;
+                        } else if (alts[j]->ch == sample->ch) {
+                                if (alts[j]->rating >= sample->rating)
+                                        j = num_alts;
+                                break;
+                        } else if (alts[j]->rating < sample->rating) {
+                                int k;
+
+                                if (j == num_alts - 1)
+                                        break;
+
+                                /* See if the character is in the list */
+                                for (k = j + 1; k < num_alts - 1 && alts[k] &&
+                                     alts[k]->ch != sample->ch; k++);
+
+                                /* Do not swallow zeroes */
+                                if (!alts[k] && k < num_alts - 1)
+                                        alts[k + 1] = NULL;
+
+                                memmove(alts + j + 1, alts + j,
+                                        sizeof (*alts) * (k - j));
+                                break;
+                        }
+                if (j >= num_alts)
+                        continue;
+                alts[j] = sample;
+        }
+
+        /* Normalize the alternates' accuracies to 100 */
+        if (range)
+                for (i = 0; i < num_alts && alts[i]; i++)
+                        alts[i]->rating = alts[i]->rating * 100 / range;
+
+        /* Keep track of strength stat */
+        strength = 0;
+        if (alts[0]) {
+                strength = alts[1] ? alts[0]->rating - alts[1]->rating :
+                                        100;
+                strength_sum += strength;
+        }
+
+        g_timer_elapsed(timer, &microsec);
+        msec = microsec / 100;
+        g_message("Recognized -- %d/%d (%d%%) disqualified, "
+                  "%dms (%dms/symbol), %d%% strong",
+                  num_disqualified, prep_examined,
+                  num_disqualified * 100 / prep_examined, msec,
+                  prep_examined - num_disqualified ?
+                  msec / (prep_examined - num_disqualified) : -1,
+                  strength);
+
+        /*  Print out the top candidate scores in detail */
+        if (log_level >= G_LOG_LEVEL_DEBUG)
+                for (i = 0; i < num_alts && alts[i]; i++) {
+                        int j, len;
+
+                        len = input->len >= alts[i]->len ? input->len :
+                                                           alts[i]->len;
+                        log_print("| '%C' (", alts[i]->ch);
+                        for (j = 0; j < ENGINES; j++)
+                                log_print("%4d [%5d]%s",
+                                        engine_rating(alts[i], j),
+                                        alts[i]->ratings[j],
+                                        j < ENGINES - 1 ? "," : "");
+                        log_print(") %3d%% [", alts[i]->rating);
+                        for (j = 0; j < len; j++)
+                                log_print("%d",
+                                          alts[i]->transform.order[j] - 1);
+                        for (j = 0; j < len; j++)
+                                log_print("%c", alts[i]->transform.reverse[j] ?
+                                                'R' : '-');
+                        for (j = 0; j < len; j++)
+                                log_print("%d", alts[i]->transform.glue[j]);
+                        log_print("]\n");
+                }
+
+        /* Select the top result */
+        input->ch = alts[0] ? alts[0]->ch : 0;
+}
+
+static void insert_sample(const Sample *new_sample, int force_overwrite)
+/* Insert a sample into the sample chain, possibly overwriting an older
+   sample */
+{
+        int last_used, count = 0;
+        Sample *sample, *overwrite = NULL, *create = NULL;
+
+        last_used = force_overwrite ? current + 1 : new_sample->used;
+        sampleiter_reset();
+        while ((sample = sampleiter_next())) {
+                if (!sample->used) {
+                        create = sample;
+                        continue;
+                }
+                if (sample->ch != new_sample->ch)
+                        continue;
+                if (sample->used < last_used) {
+                        overwrite = sample;
+                        last_used = sample->used;
+                }
+                count++;
+        }
+        if (overwrite && count >= samples_max) {
+                sample = overwrite;
+                clear_sample(sample);
+        } else if (create)
+                sample = create;
+        else
+                sample = sample_new();
+        *sample = *new_sample;
+        process_sample(sample);
+}
+
+void train_sample(const Sample *sample, int trusted)
+/* Overwrite a blank or least-recently-used slot in the samples set */
+{
+        Sample new_sample;
+
+        /* Do not allow zero-length samples */
+        if (sample->len < 1) {
+                g_warning("Attempted to train zero length sample for '%C'",
+                          sample->ch);
+                return;
+        }
+
+        copy_sample(&new_sample, sample);
+        new_sample.used = trusted ? current++ : 1;
+        new_sample.enabled = TRUE;
+        insert_sample(&new_sample, TRUE);
+}
+
+int char_trained(int ch)
+/* Count the number of samples for this character */
+{
+        Sample *sample;
+        int count = 0;
+
+        sampleiter_reset();
+        while ((sample = sampleiter_next())) {
+                if (sample->ch != ch)
+                        continue;
+                count++;
+        }
+        return count;
+}
+
+void untrain_char(int ch)
+/* Delete all samples for a character */
+{
+        Sample *sample;
+
+        sampleiter_reset();
+        while ((sample = sampleiter_next()))
+                if (sample->ch == ch)
+                        clear_sample(sample);
+}
+
+/*
+        Profile
+*/
+
+void recognize_sync(void)
+/* Sync params with the profile */
+{
+        int i;
+
+        profile_write("recognize");
+        profile_sync_int(&current);
+        profile_sync_int(&samples_max);
+        if (samples_max < 1)
+                samples_max = 1;
+        profile_sync_int(&no_latin_alpha);
+        for (i = 0; i < ENGINES; i++)
+                profile_sync_int(&engines[i].range);
+        profile_write("\n");
+}
+
+void sample_read(void)
+/* Read a sample from the profile */
+{
+        Sample sample;
+        Stroke *stroke;
+
+        memset(&sample, 0, sizeof (sample));
+        sample.ch = atoi(profile_read());
+        if (!sample.ch) {
+                g_warning("Sample on line %d has NULL symbol", profile_line);
+                return;
+        }
+        sample.used = atoi(profile_read());
+        stroke = sample.strokes[0];
+        for (;;) {
+                const char *str;
+                int x, y;
+
+                str = profile_read();
+                if (!str[0]) {
+                        if (!sample.strokes[0]) {
+                                g_warning("Sample on line %d ('%C') with no "
+                                          "point data", profile_line,
+                                          sample.ch);
+                                break;
+                        }
+                        insert_sample(&sample, FALSE);
+                        break;
+                }
+                if (str[0] == ';') {
+                        stroke = sample.strokes[sample.len];
+                        continue;
+                }
+                if (sample.len >= STROKES_MAX) {
+                        g_warning("Sample on line %d ('%C') is oversize",
+                                  profile_line, sample.ch);
+                        clear_sample(&sample);
+                        break;
+                }
+                if (!stroke) {
+                        stroke = stroke_new(0);
+                        sample.strokes[sample.len++] = stroke;
+                }
+                if (stroke->len >= POINTS_MAX) {
+                        g_warning("Symbol '%C' stroke %d is oversize",
+                                  sample.ch, sample.len);
+                        clear_sample(&sample);
+                        break;
+                }
+                x = atoi(str);
+                y = atoi(profile_read());
+                draw_stroke(&stroke, x, y);
+        }
+}
+
+static void sample_write(Sample *sample)
+/* Write a sample link to the profile */
+{
+        int k, l;
+
+        profile_write(va("sample %5d %5d", sample->ch, sample->used));
+        for (k = 0; k < sample->len; k++) {
+                for (l = 0; l < sample->strokes[k]->len; l++)
+                        profile_write(va(" %4d %4d",
+                                         sample->strokes[k]->points[l].x,
+                                         sample->strokes[k]->points[l].y));
+                profile_write("    ;");
+        }
+        profile_write("\n");
+}
+
+void samples_write(void)
+/* Write all of the samples to the profile */
+{
+        Sample *sample;
+
+        sampleiter_reset();
+        while ((sample = sampleiter_next()))
+                if (sample->ch && sample->used)
+                        sample_write(sample);
+}
+
diff --git a/src/recognize.h b/src/recognize.h
new file mode 100644 (file)
index 0000000..e970d23
--- /dev/null
@@ -0,0 +1,184 @@
+
+/*
+
+cellwriter -- a character recognition input method
+Copyright (C) 2007 Michael Levin <risujin@risujin.org>
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+*/
+
+/*
+        Stroke data
+*/
+
+/* Maximum number of points a stroke can have */
+#define POINTS_MAX 256
+
+/* Scale of the point coordinates */
+#define SCALE    256
+#define MAX_DIST 362 /* sqrt(2) * SCALE */
+
+/* Maximum number of strokes a sample can have */
+#define STROKES_MAX 32
+
+/* Largest value the gluable matrix entries can take */
+#define GLUABLE_MAX 255
+
+typedef struct {
+        signed char x, y;
+        ANGLE angle;
+} Point;
+
+typedef struct {
+        Vec2 center;
+        float distance;
+        int len, size, spread;
+        unsigned char processed,
+                      gluable_start[STROKES_MAX], gluable_end[STROKES_MAX];
+        signed char min_x, max_x, min_y, max_y;
+        Point points[];
+} Stroke;
+
+/* Stroke allocation */
+Stroke *stroke_new(int size);
+Stroke *stroke_clone(const Stroke *src, int reverse);
+void stroke_free(Stroke *stroke);
+void clear_stroke(Stroke *stroke);
+
+/* Stroke manipulation */
+void process_stroke(Stroke *stroke);
+void draw_stroke(Stroke **stroke, int x, int y);
+void smooth_stroke(Stroke *s);
+void simplify_stroke(Stroke *s);
+Stroke *sample_stroke(Stroke *out, Stroke *in, int points, int size);
+void sample_strokes(Stroke *a, Stroke *b, Stroke **as, Stroke **bs);
+void glue_stroke(Stroke **a, const Stroke *b, int reverse);
+void dump_stroke(Stroke *stroke);
+
+/*
+        Recognition engines
+*/
+
+/* This will prevent the word frequency table from loading */
+/* #define DISABLE_WORDFREQ */
+
+/* Largest allowed engine weight */
+#define MAX_RANGE 100
+
+/* Range of the scale value for engines */
+#define ENGINE_SCALE STROKES_MAX
+
+/* Minimum stroke spread distance for angle measurements */
+#define DOT_SPREAD (SCALE / 10)
+
+/* Maximum distance between glue points */
+#define GLUE_DIST (SCALE / 6)
+
+enum {
+        ENGINE_PREP,
+        ENGINE_AVGDIST,
+        ENGINE_AVGANGLE,
+#ifndef DISABLE_WORDFREQ
+        ENGINE_WORDFREQ,
+#endif
+        ENGINES
+};
+
+typedef struct {
+        const char *name;
+        void (*func)(void);
+        int range, ignore_zeros, scale, average, max;
+} Engine;
+
+typedef struct Cell Cell;
+
+/* Generalized measure function */
+typedef float (*MeasureFunc)(Stroke *a, int i, Stroke *b, int j, void *extra);
+
+extern int ignore_stroke_order, ignore_stroke_dir, ignore_stroke_num,
+           elasticity, no_latin_alpha, wordfreq_enable;
+extern Engine engines[ENGINES];
+
+void engine_average(void);
+void engine_wordfreq(void);
+void load_wordfreq(void);
+float measure_distance(const Stroke *a, int i, const Stroke *b, int j,
+                       const Vec2 *offset);
+float measure_strokes(Stroke *a, Stroke *b, MeasureFunc func,
+                      void *extra, int points, int elasticity);
+
+/*
+        Samples and characters
+*/
+
+/* Highest range a rating can have */
+#define RATING_MAX 32767
+#define RATING_MIN -32767
+
+/* Maximum number of samples we can have per character */
+#define SAMPLES_MAX 16
+
+/* Fine sampling parameters */
+#define FINE_RESOLUTION 8.f
+#define FINE_ELASTICITY 2
+
+/* Rough sampling parameters */
+#define ROUGH_RESOLUTION 24.f
+#define ROUGH_ELASTICITY 0
+
+typedef struct {
+        unsigned char valid, order[STROKES_MAX], reverse[STROKES_MAX],
+                      glue[STROKES_MAX];
+        float reach;
+} Transform;
+
+typedef struct {
+        int used;
+        gunichar2 ch;
+        unsigned short len;
+        short rating, ratings[ENGINES];
+        unsigned char enabled, disqualified, processed;
+        Transform transform;
+        Vec2 center;
+        float distance, penalty;
+        Stroke *strokes[STROKES_MAX], *roughs[STROKES_MAX];
+} Sample;
+
+extern Sample *input;
+extern int num_disqualified, training_block, samples_max;
+
+/* Sample list iteration */
+void sampleiter_reset(void);
+Sample *sampleiter_next(void);
+
+/* Properties */
+void process_sample(Sample *sample);
+void center_samples(Vec2 *ac_to_bc, Sample *a, Sample *b);
+int sample_disqualified(const Sample *sample);
+int sample_valid(const Sample *sample, int used);
+int char_trained(int ch);
+int char_disabled(int ch);
+
+/* Processing */
+void clear_sample(Sample *sample);
+void recognize_sample(Sample *cell, Sample **alts, int num_alts);
+void train_sample(const Sample *cell, int trusted);
+void untrain_char(int ch);
+void update_enabled_samples(void);
+void promote_sample(Sample *sample);
+void demote_sample(Sample *sample);
+Stroke *transform_stroke(Sample *src, Transform *tfm, int i);
+
diff --git a/src/singleinstance.c b/src/singleinstance.c
new file mode 100644 (file)
index 0000000..6dfdb89
--- /dev/null
@@ -0,0 +1,98 @@
+
+/*
+
+cellwriter -- a character recognition input method
+Copyright (C) 2007 Michael Levin <risujin@risujin.org>
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+*/
+
+#include "config.h"
+#include "common.h"
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+/*
+        Single-instance checks
+*/
+
+static SingleInstanceFunc on_dupe;
+static int fifo;
+static char *path;
+
+static gboolean check_dupe(void)
+{
+        ssize_t len;
+        char buf[2];
+
+        if (fifo <= 0 || !on_dupe)
+                return FALSE;
+        len = read(fifo, buf, 1);
+        buf[1] = 0;
+        if (len > 0)
+                on_dupe(buf);
+        return TRUE;
+}
+
+void single_instance_cleanup(void)
+{
+        if (fifo > 0)
+                close(fifo);
+        if (path && unlink(path) == -1)
+                log_errno("Failed to unlink program FIFO");
+}
+
+int single_instance_init(SingleInstanceFunc func, const char *str)
+{
+        on_dupe = func;
+        path = g_build_filename(g_get_home_dir(), "." PACKAGE, "fifo", NULL);
+
+        /* If we can open the program FIFO in write-only mode then we must
+           have a reader process already running. We send it a one-byte junk
+           message to wake it up and quit. */
+        if ((fifo = open(path, O_WRONLY | O_NONBLOCK)) > 0) {
+                write(fifo, str, 1);
+                close(fifo);
+                return TRUE;
+        }
+
+        /* The FIFO can be left over from a previous instance if the program
+           crashes or is killed */
+        if (g_file_test(path, G_FILE_TEST_EXISTS)) {
+                g_debug("Program FIFO exists but is not opened on "
+                        "read-only side, deleting\n");
+                single_instance_cleanup();
+        }
+
+        /* Otherwise, create a read-only FIFO and poll for input */
+        fifo = 0;
+        if (mkfifo(path, S_IRUSR | S_IWUSR)) {
+                log_errno("Failed to create program FIFO");
+                return FALSE;
+        }
+        if ((fifo = open(path, O_RDONLY | O_NONBLOCK)) == -1) {
+                log_errno("Failed to open FIFO for reading");
+                return FALSE;
+        }
+
+        /* Setup the polling function */
+        g_timeout_add_full(G_PRIORITY_DEFAULT_IDLE, 1000,
+                           (GSourceFunc)check_dupe, NULL, NULL);
+
+        return FALSE;
+}
+
diff --git a/src/stroke.c b/src/stroke.c
new file mode 100644 (file)
index 0000000..1bc7b70
--- /dev/null
@@ -0,0 +1,446 @@
+
+/*
+
+cellwriter -- a character recognition input method
+Copyright (C) 2007 Michael Levin <risujin@risujin.org>
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+*/
+
+#include "config.h"
+#include <string.h>
+#include <math.h>
+#include <gtk/gtk.h>
+#include "common.h"
+#include "recognize.h"
+
+/*
+        Stroke functions
+*/
+
+/* Distance from the line formed by the two neighbors of a point, which, if
+   not exceeded, will cause the point to be culled during simplification */
+#define SIMPLIFY_THRESHOLD 0.5
+
+/* Granularity of stroke point array in points */
+#define POINTS_GRAN 64
+
+/* Size of a stroke structure */
+#define STROKE_SIZE(size) (sizeof (Stroke) + (size) * sizeof (Point))
+
+void process_stroke(Stroke *stroke)
+/* Generate cached parameters of a stroke */
+{
+        int i;
+        float distance;
+
+        if (stroke->processed)
+                return;
+        stroke->processed = TRUE;
+
+        /* Dot strokes */
+        if (stroke->len == 1) {
+                vec2_set(&stroke->center, stroke->points[0].x,
+                         stroke->points[0].y);
+                stroke->spread = 0.f;
+                return;
+        }
+
+        stroke->min_x = stroke->max_x = stroke->points[0].x;
+        stroke->min_y = stroke->max_y = stroke->points[0].y;
+        for (i = 0, distance = 0.; i < stroke->len - 1; i++) {
+                Vec2 v;
+                float weight;
+
+                /* Angle */
+                vec2_set(&v, stroke->points[i + 1].x - stroke->points[i].x,
+                         stroke->points[i + 1].y - stroke->points[i].y);
+                stroke->points[i].angle = vec2_angle(&v);
+
+                /* Point contribution to spread */
+                if (stroke->points[i + 1].x < stroke->min_x)
+                        stroke->min_x = stroke->points[i + 1].x;
+                if (stroke->points[i + 1].y < stroke->min_y)
+                        stroke->min_y = stroke->points[i + 1].y;
+                if (stroke->points[i + 1].x > stroke->max_x)
+                        stroke->max_x = stroke->points[i + 1].x;
+                if (stroke->points[i + 1].y > stroke->max_y)
+                        stroke->max_y = stroke->points[i + 1].y;
+
+                /* Segment contribution to center */
+                vec2_set(&v, stroke->points[i + 1].x - stroke->points[i].x,
+                         stroke->points[i + 1].y - stroke->points[i].y);
+                distance += weight = vec2_mag(&v);
+                vec2_set(&v, stroke->points[i + 1].x + stroke->points[i].x,
+                         stroke->points[i + 1].y + stroke->points[i].y);
+                vec2_scale(&v, &v, weight / 2.);
+                vec2_sum(&stroke->center, &stroke->center, &v);
+        }
+        vec2_scale(&stroke->center, &stroke->center, 1. / distance);
+        stroke->points[i].angle = stroke->points[i - 1].angle;
+        stroke->distance = distance;
+
+        /* Stroke spread */
+        stroke->spread = stroke->max_x - stroke->min_x;
+        if (stroke->max_y - stroke->min_y > stroke->spread)
+                stroke->spread = stroke->max_y - stroke->min_y;
+}
+
+void clear_stroke(Stroke *stroke)
+/* Clear cached parameters */
+{
+        int size;
+
+        size = stroke->size;
+        memset(stroke, 0, sizeof (*stroke));
+        stroke->size = size;
+}
+
+Stroke *stroke_new(int size)
+/* Allocate memory for a new stroke */
+{
+        Stroke *stroke;
+
+        if (size < POINTS_GRAN)
+                size = POINTS_GRAN;
+        stroke = g_malloc(STROKE_SIZE(size));
+        stroke->size = size;
+        clear_stroke(stroke);
+        return stroke;
+}
+
+static void reverse_copy_points(Point *dest, const Point *src, int len)
+{
+        int i;
+
+        for (i = 0; i < len; i++) {
+                ANGLE angle = 0;
+
+                if (i < len - 1)
+                        angle = src[len - i - 2].angle + ANGLE_PI;
+                dest[i] = src[len - i - 1];
+                dest[i].angle = angle;
+        }
+}
+
+Stroke *stroke_clone(const Stroke *src, int reverse)
+{
+        Stroke *stroke;
+
+        if (!src)
+                return NULL;
+        stroke = stroke_new(src->size);
+        if (!reverse)
+                memcpy(stroke, src, STROKE_SIZE(src->size));
+        else {
+                memcpy(stroke, src, sizeof (Stroke));
+                reverse_copy_points(stroke->points, src->points, src->len);
+        }
+        return stroke;
+}
+
+void stroke_free(Stroke *stroke)
+{
+        g_free(stroke);
+}
+
+void glue_stroke(Stroke **pa, const Stroke *b, int reverse)
+/* Glue B onto the end of A preserving processed properties */
+{
+        Vec2 glue_seg, glue_center, b_center;
+        Point start;
+        Stroke *a;
+        float glue_mag;
+
+        a = *pa;
+
+        /* If there is no stroke to glue to, just copy */
+        if (!a || a->len < 1) {
+                if (a->len < 1)
+                        stroke_free(a);
+                *pa = stroke_clone(b, reverse);
+                return;
+        }
+
+        /* Allocate memory */
+        if (a->size < a->len + b->len) {
+                a->size = a->len + b->len;
+                a = g_realloc(a, STROKE_SIZE(a->size));
+        }
+
+        /* Gluing two strokes creates a new segment between them */
+        start = reverse ? b->points[b->len - 1] : b->points[0];
+        vec2_set(&glue_seg, start.x - a->points[a->len - 1].x,
+                 start.y - a->points[a->len - 1].y);
+        vec2_set(&glue_center, (start.x + a->points[a->len - 1].x) / 2,
+                 (start.y + a->points[a->len - 1].y) / 2);
+        glue_mag = vec2_mag(&glue_seg);
+
+        /* Compute new spread */
+        if (b->min_x < a->min_x)
+                a->min_x = b->min_x;
+        if (b->max_x > a->max_x)
+                a->max_x = b->max_x;
+        if (b->min_y < a->min_y)
+                a->min_y = b->min_y;
+        if (b->max_y > a->max_y)
+                a->max_y = b->max_y;
+        a->spread = a->max_x - a->min_x;
+        if (a->max_y - a->min_y > a->spread)
+                a->spread = a->max_y - a->min_y;
+
+        /* Compute new center point */
+        vec2_scale(&a->center, &a->center, a->distance);
+        vec2_scale(&b_center, &b->center, b->distance);
+        vec2_scale(&glue_center, &glue_center, glue_mag);
+        vec2_set(&a->center, a->center.x + b_center.x + glue_center.x,
+                 a->center.y + b_center.y + glue_center.y);
+        vec2_scale(&a->center, &a->center,
+                   1.f / (a->distance + b->distance + glue_mag));
+
+        /* Copy points */
+        if (!reverse || b->len < 2)
+                memcpy(a->points + a->len, b->points, b->len * sizeof (Point));
+        else
+                reverse_copy_points(a->points + a->len, b->points, b->len);
+
+        a->points[a->len - 1].angle = vec2_angle(&glue_seg);
+        a->distance += glue_mag + b->distance;
+        a->len += b->len;
+        *pa = a;
+}
+
+void draw_stroke(Stroke **ps, int x, int y)
+/* Add a point in scaled coordinates to a stroke */
+{
+        /* Create a new stroke if necessary */
+        if (!(*ps))
+                *ps = stroke_new(0);
+
+        /* If we run out of room, resample the stroke to fit */
+        if ((*ps)->len >= POINTS_MAX) {
+                Stroke *new_stroke;
+
+                new_stroke = sample_stroke(NULL, *ps, POINTS_MAX - POINTS_GRAN,
+                                           POINTS_MAX);
+                stroke_free(*ps);
+                *ps = new_stroke;
+        }
+
+        /* Range limits */
+        if (x <= -SCALE / 2)
+                x = -SCALE / 2 + 1;
+        if (x >= SCALE / 2)
+                x = SCALE / 2 - 1;
+        if (y <= -SCALE / 2)
+                y = -SCALE / 2 + 1;
+        if (y >= SCALE / 2)
+                y = SCALE / 2 - 1;
+
+        /* Do we need more memory? */
+        if ((*ps)->len >= (*ps)->size) {
+                (*ps)->size += POINTS_GRAN;
+                *ps = g_realloc(*ps, STROKE_SIZE((*ps)->size));
+        }
+
+        (*ps)->points[(*ps)->len].x = x;
+        (*ps)->points[(*ps)->len++].y = y;
+}
+
+void smooth_stroke(Stroke *s)
+/* Smooth stroke points by moving each point halfway toward the line between
+   its two neighbors */
+{
+        int i, last_x, last_y;
+
+        last_x = s->points[0].x;
+        last_y = s->points[0].y;
+        for (i = 1; i < s->len - 1; i++) {
+                Vec2 a, b, c, m, ab, ac, am;
+
+                if (last_x == s->points[i + 1].x &&
+                    last_y == s->points[i + 1].y) {
+                        last_x = s->points[i].x;
+                        last_y = s->points[i].y;
+                        continue;
+                }
+                vec2_set(&a, last_x, last_y);
+                vec2_set(&b, s->points[i].x, s->points[i].y);
+                vec2_set(&c, s->points[i + 1].x, s->points[i + 1].y);
+                vec2_sub(&ac, &c, &a);
+                vec2_sub(&ab, &b, &a);
+                vec2_proj(&am, &ab, &ac);
+                vec2_sum(&m, &a, &am);
+                vec2_avg(&b, &b, &m, 0.5);
+                last_x = s->points[i].x;
+                last_y = s->points[i].y;
+                s->points[i].x = b.x + 0.5;
+                s->points[i].y = b.y + 0.5;
+        }
+}
+
+void simplify_stroke(Stroke *s)
+/* Remove excess points between neighbors */
+{
+       int i;
+
+       for (i = 1; i < s->len - 1; i++) {
+               Vec2 l, w;
+               double dist, mag, dot;
+
+                /* Vector l is a unit vector from point i - 1 to point i + 1 */
+               vec2_set(&l, s->points[i - 1].x - s->points[i + 1].x,
+                        s->points[i - 1].y - s->points[i + 1].y);
+               mag = vec2_norm(&l, &l);
+
+               /* Vector w is a vector from point i - 1 to point i */
+               vec2_set(&w, s->points[i - 1].x - s->points[i].x,
+                        s->points[i - 1].y - s->points[i].y);
+
+               /* Do not touch mid points that are not in between their
+                  neighbors */
+               dot = vec2_dot(&l, &w);
+               if (dot < 0. || dot > mag)
+                       continue;
+
+               /* Remove any points that are less than some threshold away
+                  from their neighbor points */
+               dist = vec2_cross(&w, &l);
+               if (dist < SIMPLIFY_THRESHOLD && dist > -SIMPLIFY_THRESHOLD) {
+                       memmove(s->points + i, s->points + i + 1,
+                               (--s->len - i) * sizeof (*s->points));
+                       i--;
+               }
+       }
+}
+
+void dump_stroke(Stroke *stroke)
+{
+        int i;
+
+        /* Print stats */
+        g_message("Stroke data --");
+        g_debug("Distance: %g", stroke->distance);
+        g_debug("  Center: (%g, %g)", stroke->center.x, stroke->center.y);
+        g_debug("  Spread: %d", stroke->spread);
+        g_message("%d points --", stroke->len);
+
+        /* Print point data */
+        for (i = 0; i < stroke->len; i++)
+                g_debug("%3d: (%4d,%4d)\n",
+                        i, stroke->points[i].x, stroke->points[i].y);
+}
+
+Stroke *sample_stroke(Stroke *out, Stroke *in, int points, int size)
+/* Recreate the stroke by sampling at regular distance intervals.
+   Sampled strokes always have angle data. */
+{
+        Vec2 v;
+        double dist_i, dist_j, dist_per;
+        int i, j, len;
+
+        if (!in || in->len < 1) {
+                g_warning("Attempted to sample an invalid stroke");
+                return NULL;
+        }
+
+        /* Check ranges */
+        if (size >= POINTS_MAX) {
+                g_warning("Stroke sized to maximum length possible");
+                size = POINTS_MAX;
+        }
+        if (points >= POINTS_MAX) {
+                g_warning("Stroke sampled to maximum length possible");
+                points = POINTS_MAX;
+        }
+        if (size < 1)
+                size = 1;
+        if (points < 1)
+                points = 1;
+
+        /* Allocate memory and copy cached data */
+        if (!out)
+                out = g_malloc(STROKE_SIZE(size));
+        out->size = size;
+        len = out->size < points ? out->size - 1 : points - 1;
+        out->len = len + 1;
+        out->spread = in->spread;
+        out->center = in->center;
+
+        /* Special case for sampling a single point */
+        if (in->len <= 1 || points <= 1) {
+                for (i = 0; i < len + 1; i++)
+                        out->points[i] = in->points[0];
+                out->distance = 0.;
+                return out;
+        }
+
+        dist_per = in->distance / (points - 1);
+        out->distance = in->distance;
+        vec2_set(&v, in->points[1].x - in->points[0].x,
+                 in->points[1].y - in->points[0].y);
+        dist_j = vec2_mag(&v);
+        dist_i = dist_per;
+        out->points[0] = in->points[0];
+        for (i = 1, j = 0; i < len; i++) {
+
+                /* Advance our position */
+                while (dist_i >= dist_j) {
+                        if (j >= in->len - 2)
+                                goto finish;
+                        dist_i -= dist_j;
+                        j++;
+                        vec2_set(&v, in->points[j + 1].x - in->points[j].x,
+                                 in->points[j + 1].y - in->points[j].y);
+                        dist_j = vec2_mag(&v);
+                }
+
+                /* Interpolate points */
+                out->points[i].x = in->points[j].x +
+                                   (in->points[j + 1].x - in->points[j].x) *
+                                   dist_i / dist_j;
+                out->points[i].y = in->points[j].y +
+                                   (in->points[j + 1].y - in->points[j].y) *
+                                   dist_i / dist_j;
+                out->points[i].angle = in->points[j].angle;
+
+                dist_i += dist_per;
+        }
+finish:
+        for (; i < len + 1; i++)
+                out->points[i] = in->points[j + 1];
+
+        return out;
+}
+
+void sample_strokes(Stroke *a, Stroke *b, Stroke **as, Stroke **bs)
+/* Sample multiple strokes to equal lengths */
+{
+        double dist;
+        int points;
+
+        /* Find the sample length */
+        dist = a->distance;
+        if (b->distance > dist)
+                dist = b->distance;
+        points = 1 + dist / FINE_RESOLUTION;
+        if (points > POINTS_MAX)
+                points = POINTS_MAX;
+
+        *as = sample_stroke(NULL, a, points, points);
+        *bs = sample_stroke(NULL, b, points, points);
+}
+
diff --git a/src/window.c b/src/window.c
new file mode 100644 (file)
index 0000000..65db090
--- /dev/null
@@ -0,0 +1,1027 @@
+
+/*
+
+cellwriter -- a character recognition input method
+Copyright (C) 2007 Michael Levin <risujin@risujin.org>
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+*/
+
+#include "config.h"
+#include "common.h"
+#include "keys.h"
+#include <memory.h>
+#include <X11/Xatom.h>
+#include <X11/Xlib.h>
+#include <gdk/gdkx.h>
+
+#include "hildon-im-ui.h"
+
+/* options.c */
+void options_dialog_open(void);
+
+/* recognize.c */
+void update_enabled_samples(void);
+
+/* cellwidget.c */
+extern int training, cell_width, cell_height, cell_cols_pref;
+
+GtkWidget *cell_widget_new(void);
+void cell_widget_clear(void);
+void cell_widget_render(void);
+int cell_widget_insert(void);
+void cell_widget_train(void);
+void cell_widget_pack(void);
+int cell_widget_update_colors(void);
+void cell_widget_show_buffer(GtkWidget *button);
+int cell_widget_scrollbar_width(void);
+int cell_widget_get_height(void);
+
+extern HildonIMUI * ui;
+
+/* main.c */
+extern int keyboard_only;
+
+/*
+        Main window
+*/
+
+GtkWidget *window;
+GtkTooltips *tooltips;
+int window_force_x = -1, window_force_y = -1, training_block = 0,
+    window_docked = WINDOW_UNDOCKED, window_force_docked = -1,
+    window_button_labels = TRUE, window_force_show = FALSE,
+    window_force_hide = FALSE, style_colors = TRUE, window_embedded = FALSE,
+    window_struts = FALSE;
+
+/* Tab XPM image */
+static char *tab_xpm[] =
+{
+        "7 4 2 1",
+        "      c None",
+        ".     c #000000",
+        ".......",
+        " ..... ",
+        "  ...  ",
+        "   .   "
+};
+
+static GtkWidget *train_label_box, *train_label_frame, *train_label = NULL,
+                 *bottom_box, *blocks_combo, *cell_widget,
+  *setup_button, *keys_button, *insert_button, *enter_button, *cancel_button,
+                 *clear_button, *train_button, *buffer_button;
+static GdkRectangle window_frame = {-1, -1, 0, 0}, window_frame_saved;
+static int screen_width = -1, screen_height = -1,
+           window_shown = TRUE, history_valid = FALSE, keys_on = FALSE;
+
+static void toggle_button_labels(int on)
+{
+        static int labels_off;
+
+        if (labels_off && on) {
+                gtk_button_set_label(GTK_BUTTON(train_button), "Train");
+                gtk_button_set_label(GTK_BUTTON(setup_button), "Setup");
+                gtk_button_set_label(GTK_BUTTON(clear_button), "Clear");
+                gtk_button_set_label(GTK_BUTTON(insert_button), "Insert");
+                gtk_button_set_label(GTK_BUTTON(keys_button), "Keys");
+        } else if (!labels_off && !on) {
+                gtk_button_set_label(GTK_BUTTON(train_button), "");
+                gtk_button_set_label(GTK_BUTTON(setup_button), "");
+                gtk_button_set_label(GTK_BUTTON(keys_button), "");
+                gtk_button_set_label(GTK_BUTTON(clear_button), "");
+                gtk_button_set_label(GTK_BUTTON(insert_button), "");
+                gtk_button_set_label(GTK_BUTTON(keys_button), "");
+        }
+        labels_off = !on;
+}
+
+void window_pack(void)
+{
+        cell_widget_pack();
+        toggle_button_labels(window_button_labels);
+        if (training)
+                gtk_widget_show(train_label_frame);
+}
+
+void window_update_colors(void)
+{
+        int keys_changed;
+
+        if (cell_widget_update_colors() || keys_changed)
+                cell_widget_render();
+}
+
+static void update_struts(void)
+/* Reserves screen space for the docked window.
+   FIXME In Metacity it causes the window to be shoved outside of its own
+         struts, which is especially devastating for top docking because this
+         causes an infinite loop of events causing the struts to repeatedly
+         scan down from the top of the screen. GOK and other applications
+         somehow get around this but I can't figure out how. */
+{
+       static guint32 struts[12];
+       guint32 new2 = 0, new3 = 0, new9 = 0, new11 = 0;
+       GdkAtom atom_strut, atom_strut_partial, cardinal;
+
+       if (!window || !window->window || !window_struts)
+               return;
+       if (window_docked == WINDOW_DOCKED_TOP) {
+               new2 = window_frame.y + window_frame.height;
+               new9 = window_frame.width;
+        } else if (window_docked == WINDOW_DOCKED_BOTTOM) {
+                new3 = window_frame.height;
+                new11 = window_frame.width;
+        }
+        if (new2 == struts[2] && new3 == struts[3] &&
+            new9 == struts[9] && new11 == struts[11])
+                return;
+        trace("top=%d (%d) bottom=%d (%d)", new2, new9, new3, new11);
+        struts[2] = new2;
+        struts[3] = new3;
+        struts[9] = new9;
+        struts[11] = new11;
+       atom_strut = gdk_atom_intern("_NET_WM_STRUT", FALSE),
+       atom_strut_partial = gdk_atom_intern("_NET_WM_STRUT_PARTIAL", FALSE);
+       cardinal = gdk_atom_intern("CARDINAL", FALSE);
+       gdk_property_change(GDK_WINDOW(window->window), atom_strut, cardinal,
+                           32, GDK_PROP_MODE_REPLACE, (guchar*)&struts, 4);
+       gdk_property_change(GDK_WINDOW(window->window), atom_strut_partial,
+                           cardinal, 32, GDK_PROP_MODE_REPLACE,
+                           (guchar*)&struts, 12);
+}
+
+static void set_geometry_hints(void)
+{
+        GdkGeometry geometry;
+
+        geometry.min_width = -1;
+        geometry.min_height = -1;
+        geometry.max_width = -1;
+        geometry.max_height = -1;
+
+        /* Use window geometry to force the window to be as large as the
+           screen */
+        if (window_docked)
+                geometry.max_width = geometry.min_width = screen_width;
+
+        gtk_window_set_geometry_hints(GTK_WINDOW(window), window, &geometry,
+                                      GDK_HINT_MIN_SIZE | GDK_HINT_MAX_SIZE);
+        trace("%dx%d", geometry.min_width, geometry.min_height);
+
+        /* In some bright and sunny alternate universe when specifications are
+           actually implemented as inteded, this function alone would cause the
+           window frame to expand upwards without having to perform the ugly
+           hack in window_configure(). XFWM4 does not respect this hint and
+           setting this hint will further mess up the window_configure()
+           movement code. */
+        /*geometry.win_gravity = window_docked == WINDOW_DOCKED_TOP ?
+                                 GDK_GRAVITY_NORTH_WEST :
+                                 GDK_GRAVITY_SOUTH_WEST;
+        gtk_window_set_geometry_hints(GTK_WINDOW(window), window, &geometry,
+                                      GDK_HINT_WIN_GRAVITY);*/
+}
+
+static void docked_move_resize(void)
+{
+        GdkScreen *screen;
+        int y = 0;
+
+        if (!window_docked)
+                return;
+        screen = gtk_window_get_screen(GTK_WINDOW(window));
+        if (window_docked == WINDOW_DOCKED_BOTTOM)
+                y = gdk_screen_get_height(screen) - window_frame.height;
+        set_geometry_hints();
+        gtk_window_move(GTK_WINDOW(window), 0, y);
+        cell_widget_pack();
+        trace("y=%d", y);
+}
+
+static gboolean window_configure(GtkWidget *widget, GdkEventConfigure *event)
+/* Intelligently grow the window up and/or left if we are in the bottom or
+   right corners of the screen respectively */
+{
+        GdkRectangle new_frame = {0, 0, 0, 0};
+        GdkScreen *screen;
+        GdkDisplay *display;
+        int screen_w, screen_h, height_change, label_w;
+
+        if (!window || !window->window)
+                return FALSE;
+        display = gtk_widget_get_display(window);
+
+        /* Get screen and window information */
+        screen = gtk_window_get_screen(GTK_WINDOW(window));
+        screen_w = gdk_screen_get_width(screen);
+        screen_h = gdk_screen_get_height(screen);
+        gdk_window_get_frame_extents(window->window, &new_frame);
+
+        /* We need to resize wrapped labels manually */
+        label_w = window->allocation.width - 16;
+        if (train_label && train_label->requisition.width != label_w)
+                gtk_widget_set_size_request(train_label, label_w, -1);
+
+        /* Docked windows have special placing requirements */
+        height_change = new_frame.height - window_frame.height;
+        if (window_docked) {
+                window_frame = new_frame;
+                if (screen_w != screen_width || screen_h != screen_height ||
+                    (height_change && window_docked == WINDOW_DOCKED_BOTTOM)) {
+                        screen_width = screen_w;
+                        screen_height = screen_h;
+                        trace("move-sizing bottom-docked window");
+                        docked_move_resize();
+                }
+                update_struts();
+                return FALSE;
+        }
+        screen_width = screen_w;
+        screen_height = screen_h;
+
+        /* Do nothing on the first configure */
+        if (window_frame.height <= 1) {
+                window_frame = new_frame;
+                return FALSE;
+        }
+
+        /* Keep the window aligned to the bottom border */
+        if (height_change && window_frame.y + window_frame.height / 2 >
+                             gdk_screen_get_height(screen) / 2)
+                window_frame.y -= height_change;
+        else
+                height_change = 0;
+
+        /* Do not allow the window to go off-screen */
+        if (window_frame.x + new_frame.width > screen_w)
+                window_frame.x = screen_w - new_frame.width;
+        if (window_frame.y + new_frame.height > screen_h)
+                window_frame.y = screen_h - new_frame.height;
+        if (window_frame.x < 0)
+                window_frame.x = 0;
+        if (window_frame.y < 0)
+                window_frame.y = 0;
+
+        /* Some window managers (Metacity) do not allow windows to resize
+           larger than the screen and will move the window back within the
+           screen bounds when this happens. We don't like this because it
+           screws with our own correcting offset. Fortunately, both the move
+           and the resize are bundled in one configure event so we can work
+           around this by using our old x/y coordinates when the dimensions
+           change. */
+        if (height_change && (new_frame.x != window_frame.x ||
+                              new_frame.y != window_frame.y)) {
+                gtk_window_move(GTK_WINDOW(window),
+                                window_frame.x, window_frame.y);
+                window_frame.width = new_frame.width;
+                window_frame.height = new_frame.height;
+                trace("moving to (%d, %d)", window_frame.x, window_frame.y);
+        } else
+                window_frame = new_frame;
+
+        return FALSE;
+}
+
+void window_set_docked(int mode)
+{
+        if (mode < WINDOW_UNDOCKED)
+                mode = WINDOW_UNDOCKED;
+        if (mode >= WINDOW_DOCKED_BOTTOM)
+                mode = WINDOW_DOCKED_BOTTOM;
+        if (mode && !window_docked)
+                window_frame_saved = window_frame;
+        window_docked = mode;
+        gtk_window_set_decorated(GTK_WINDOW(window), !mode);
+        set_geometry_hints();
+        cell_widget_pack();
+
+        /* Restore the old window position */
+        if (!mode) {
+                update_struts();
+                window_frame = window_frame_saved;
+                gtk_window_move(GTK_WINDOW(window), window_frame.x,
+                                window_frame.y);
+                trace("moving to (%d, %d)", window_frame.x, window_frame.y);
+        }
+
+        /* Move the window into docked position */
+        else
+                docked_move_resize();
+}
+
+void train_button_toggled(void)
+{
+        if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(train_button))) {
+                cell_widget_train();
+                gtk_widget_hide(clear_button);
+                gtk_widget_hide(keys_button);
+                gtk_widget_hide(insert_button);
+                gtk_widget_hide(enter_button);
+                gtk_widget_hide(buffer_button);
+                gtk_widget_show(blocks_combo);
+                gtk_widget_show(train_label_frame);
+        } else {
+          save_profile();
+                cell_widget_clear();
+                gtk_widget_hide(blocks_combo);
+                gtk_widget_hide(train_label_frame);
+                gtk_widget_show(clear_button);
+                gtk_widget_show(keys_button);
+                gtk_widget_show(insert_button);
+                gtk_widget_show(enter_button);
+                gtk_widget_show(buffer_button);
+        }
+}
+
+static int block_combo_to_unicode(int block)
+/* Find the Combo Box index's unicode block */
+{
+        int i, pos;
+
+        for (i = 0, pos = 0; unicode_blocks[i].name; i++)
+                if (unicode_blocks[i].enabled && ++pos > block)
+                        break;
+        return i;
+}
+
+static int block_unicode_to_combo(int block)
+/* Find the Unicode block's combo box position */
+{
+        int i, pos;
+
+        for (i = 0, pos = 0; i < block && unicode_blocks[i].name; i++)
+                if (unicode_blocks[i].enabled)
+                        pos++;
+        return pos;
+}
+
+static void blocks_combo_changed(void)
+{
+        int pos;
+
+        pos = gtk_combo_box_get_active(GTK_COMBO_BOX(blocks_combo));
+        training_block = block_combo_to_unicode(pos);
+        if (training)
+                cell_widget_train();
+}
+
+static GtkWidget *create_blocks_combo(void)
+{
+        GtkWidget *event_box;
+        UnicodeBlock *block;
+
+        if (blocks_combo)
+                gtk_widget_destroy(blocks_combo);
+        blocks_combo = gtk_combo_box_new_text();
+        block = unicode_blocks;
+        while (block->name) {
+                if (block->enabled)
+                        gtk_combo_box_append_text(GTK_COMBO_BOX(blocks_combo),
+                                                  block->name);
+                block++;
+        }
+        gtk_combo_box_set_active(GTK_COMBO_BOX(blocks_combo),
+                                 block_unicode_to_combo(training_block));
+        gtk_combo_box_set_focus_on_click(GTK_COMBO_BOX(blocks_combo), FALSE);
+        g_signal_connect(G_OBJECT(blocks_combo), "changed",
+                         G_CALLBACK(blocks_combo_changed), NULL);
+
+        /* Wrap ComboBox in an EventBox for tooltips */
+        event_box = gtk_event_box_new();
+        gtk_tooltips_set_tip(tooltips, event_box,
+                             "Select Unicode block to train", NULL);
+        gtk_container_add(GTK_CONTAINER(event_box), blocks_combo);
+
+        return event_box;
+}
+
+void window_toggle(void)
+{
+        if (GTK_WIDGET_VISIBLE(window)) {
+                gtk_widget_hide(window);
+                window_shown = FALSE;
+
+                /* User may have rendered themselves unable to interact with
+                   the program widgets by pressing one of the modifier keys
+                   that, for instance, puts the WM in move-window mode, so
+                   if the window is closed we need to reset the held keys */
+        } else {
+                gtk_widget_show(window);
+                window_shown = TRUE;
+        }
+}
+
+void window_show(void)
+{
+        if (!(GTK_WIDGET_VISIBLE(window)))
+                window_toggle();
+}
+
+void window_hide(void)
+{
+        if (GTK_WIDGET_VISIBLE(window))
+                window_toggle();
+}
+
+gboolean window_close(void)
+{
+  window_hide();
+        return FALSE;
+}
+
+static void window_style_set(GtkWidget *w)
+{
+        GdkColor train_label_bg = RGB_TO_GDKCOLOR(255, 255, 200),
+                 train_label_fg = RGB_TO_GDKCOLOR(0, 0, 0);
+
+        /* The training label color is taken from tooltips */
+        if (!train_label)
+                return;
+#if GTK_CHECK_VERSION(2, 10, 0)
+        gtk_style_lookup_color(w->style, "tooltip_bg_color", &train_label_bg);
+        gtk_style_lookup_color(w->style, "tooltip_fg_color", &train_label_fg);
+#endif
+        gtk_widget_modify_bg(train_label_frame, GTK_STATE_NORMAL,
+                             &train_label_bg);
+        gtk_widget_modify_bg(train_label_box, GTK_STATE_NORMAL,
+                             &train_label_bg);
+        gtk_widget_modify_fg(train_label, GTK_STATE_NORMAL,
+                             &train_label_fg);
+        gtk_widget_modify_fg(blocks_combo, GTK_STATE_NORMAL,
+                             &train_label_fg);
+}
+
+static void button_set_image_xpm(GtkWidget *button, char **xpm)
+/* Creates a button with an XPM icon */
+{
+        GdkPixmap *pixmap;
+        GdkBitmap *mask;
+        GtkWidget *image;
+
+        pixmap = gdk_pixmap_colormap_create_from_xpm_d
+                 (NULL, gdk_colormap_get_system(), &mask, NULL, xpm);
+        image = gtk_image_new_from_pixmap(pixmap, mask);
+        g_object_unref(pixmap);
+        gtk_button_set_image(GTK_BUTTON(button), image);
+}
+
+void cell_widget_insert_surrounding_string();
+
+static void insert_button_clicked(void)
+{
+  if(FALSE){
+    if (cell_widget_insert()) {
+      history_valid = TRUE;
+      gtk_widget_set_sensitive(buffer_button, TRUE);
+    }
+  }else{
+    cell_widget_insert_surrounding_string();
+    cell_widget_clear();
+  }
+
+        if(ui){
+          hildon_im_ui_restore_previous_mode(ui);
+
+          window_hide();
+        }
+}
+
+
+static void return_button_clicked(void)
+{
+  if(FALSE){
+    if (cell_widget_insert()) {
+      history_valid = TRUE;
+      gtk_widget_set_sensitive(buffer_button, TRUE);
+      //hildon_im_ui_send_communication_message(ui, HILDON_IM_CONTEXT_HANDLE_ENTER);
+      hildon_im_ui_send_utf8(ui, "\n");
+    }
+  }else{
+    cell_widget_insert_surrounding_string();
+    cell_widget_clear();
+    //hildon_im_ui_send_communication_message(ui, HILDON_IM_CONTEXT_HANDLE_ENTER);
+    //hildon_im_ui_send_communication_message(ui, HILDON_IM_CONTEXT_ENTER_ON_FOCUS);
+    hildon_im_ui_send_utf8(ui, "\n");
+  }
+
+
+  if(ui){
+    hildon_im_ui_restore_previous_mode(ui);
+    
+    window_hide();
+  }
+}
+
+static void cancel_button_clicked(void){
+  cell_widget_clear();
+  if(ui){
+    hildon_im_ui_restore_previous_mode(ui);
+    window_hide();
+  }  
+}
+
+static void buffer_button_pressed(void)
+{
+        if (!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(buffer_button))) {
+                cell_widget_show_buffer(buffer_button);
+                gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(buffer_button),
+                                             TRUE);
+        }
+}
+
+static void print_window_xid(GtkWidget *widget)
+{
+        g_print("%d\n", (unsigned int)GDK_WINDOW_XID(widget->window));
+}
+
+void window_create(GtkWidget *parent)
+/* Create the main window and child widgets */
+{
+        GtkWidget *widget, *window_vbox, *image;
+        GdkScreen *screen;
+
+        /* Create the window or plug */
+        if (!parent)
+          window = !window_embedded ? gtk_window_new(GTK_WINDOW_TOPLEVEL) :
+                                      gtk_plug_new(0);
+        else
+          window = parent;
+
+        g_signal_connect(G_OBJECT(window), "delete-event",
+                         G_CALLBACK(window_close), NULL);
+        g_signal_connect(G_OBJECT(window), "destroy",
+                         G_CALLBACK(gtk_main_quit), NULL);
+        g_signal_connect(G_OBJECT(window), "style-set",
+                         G_CALLBACK(window_style_set), NULL);
+        g_signal_connect(G_OBJECT(window), "configure-event",
+                         G_CALLBACK(window_configure), NULL);
+        gtk_window_set_accept_focus(GTK_WINDOW(window), FALSE);
+        gtk_window_set_resizable(GTK_WINDOW(window), FALSE);
+
+        /* This hint was alleged to fix the strut problems with metacity but
+           doesn't and only causes the window to overlap the docked panels */
+        /*gtk_window_set_type_hint(GTK_WINDOW(window),
+                                 GDK_WINDOW_TYPE_HINT_DOCK);*/
+
+        /* Tooltips */
+        tooltips = gtk_tooltips_new();
+        gtk_tooltips_enable(tooltips);
+
+        /* Root box */
+        window_vbox = gtk_vbox_new(FALSE, 0);
+        gtk_widget_show(window_vbox);
+
+        /* Training info label frame */
+        train_label_frame = gtk_frame_new(NULL);
+        gtk_widget_set_no_show_all(train_label_frame, TRUE);
+        gtk_frame_set_shadow_type(GTK_FRAME(train_label_frame), GTK_SHADOW_IN);
+        gtk_container_set_border_width(GTK_CONTAINER(train_label_frame), 2);
+
+        /* Training info label */
+        train_label = gtk_label_new(NULL);
+        gtk_label_set_line_wrap(GTK_LABEL(train_label), TRUE);
+        gtk_label_set_justify(GTK_LABEL(train_label), GTK_JUSTIFY_FILL);
+        gtk_label_set_markup(GTK_LABEL(train_label),
+                             "<b>Training Mode:</b> Carefully draw each "
+                             "character in its cell.");
+        gtk_widget_show(train_label);
+
+        /* Training info label colored box */
+        train_label_box = gtk_event_box_new();
+        gtk_widget_show(train_label_box);
+        gtk_container_add(GTK_CONTAINER(train_label_box), train_label);
+        gtk_container_add(GTK_CONTAINER(train_label_frame), train_label_box);
+        gtk_widget_show_all(train_label_frame);
+        gtk_box_pack_start(GTK_BOX(window_vbox), train_label_frame,
+                           FALSE, FALSE, 0);
+
+        /* Cell widget */
+        cell_widget = cell_widget_new();
+        gtk_box_pack_start(GTK_BOX(window_vbox), cell_widget, TRUE, TRUE, 2);
+        if (!keyboard_only)
+                gtk_widget_show_all(cell_widget);
+
+        /* Bottom box */
+        bottom_box = gtk_hbox_new(FALSE, 0);
+
+        /* Train button */
+        train_button = gtk_toggle_button_new_with_label("Train");
+        gtk_button_set_focus_on_click(GTK_BUTTON(train_button), FALSE);
+        gtk_button_set_image(GTK_BUTTON(train_button),
+                             gtk_image_new_from_stock(GTK_STOCK_MEDIA_RECORD,
+                                                      GTK_ICON_SIZE_BUTTON));
+        gtk_button_set_relief(GTK_BUTTON(train_button), GTK_RELIEF_NONE);
+        gtk_box_pack_start(GTK_BOX(bottom_box), train_button, FALSE, FALSE, 0);
+        g_signal_connect(G_OBJECT(train_button), "toggled",
+                         G_CALLBACK(train_button_toggled), 0);
+        gtk_tooltips_set_tip(tooltips, train_button, "Toggle training mode",
+                             NULL);
+
+        /* Setup button */
+        setup_button = gtk_button_new_with_label("Setup");
+        gtk_button_set_focus_on_click(GTK_BUTTON(setup_button), FALSE);
+        gtk_button_set_image(GTK_BUTTON(setup_button),
+                             gtk_image_new_from_stock(GTK_STOCK_PREFERENCES,
+                                                      GTK_ICON_SIZE_BUTTON));
+        gtk_button_set_relief(GTK_BUTTON(setup_button), GTK_RELIEF_NONE);
+        gtk_box_pack_start(GTK_BOX(bottom_box), setup_button, FALSE, FALSE, 0);
+        g_signal_connect(G_OBJECT(setup_button), "clicked",
+                         G_CALLBACK(options_dialog_open), 0);
+        gtk_tooltips_set_tip(tooltips, setup_button, "Edit program options",
+                             NULL);
+
+        /* Expanding box to keep things tidy */
+        widget = gtk_vbox_new(FALSE, 0);
+        gtk_box_pack_start(GTK_BOX(bottom_box), widget, TRUE, FALSE, 0);
+
+        /* Training Unicode Block selector */
+        widget = create_blocks_combo();
+        gtk_box_pack_start(GTK_BOX(bottom_box), widget, FALSE, FALSE, 0);
+        gtk_widget_set_no_show_all(blocks_combo, TRUE);
+
+        /* Clear button */
+        clear_button = gtk_button_new_with_label("Clear");
+        gtk_button_set_focus_on_click(GTK_BUTTON(clear_button), FALSE);
+        image = gtk_image_new_from_stock(GTK_STOCK_CLEAR, GTK_ICON_SIZE_BUTTON);
+        gtk_button_set_image(GTK_BUTTON(clear_button), image);
+        gtk_button_set_relief(GTK_BUTTON(clear_button), GTK_RELIEF_NONE);
+        gtk_box_pack_start(GTK_BOX(bottom_box), clear_button, FALSE, FALSE, 0);
+        g_signal_connect(G_OBJECT(clear_button), "clicked",
+                         G_CALLBACK(cell_widget_clear), 0);
+        gtk_tooltips_set_tip(tooltips, clear_button, "Clear current input",
+                             NULL);
+
+        /* Cancel button */
+        cancel_button = gtk_button_new_with_label("Cancel");
+        gtk_button_set_focus_on_click(GTK_BUTTON(cancel_button), FALSE);
+        gtk_button_set_image(GTK_BUTTON(cancel_button),
+                             gtk_image_new_from_stock(GTK_STOCK_OK,
+                                                      GTK_ICON_SIZE_BUTTON));
+        gtk_button_set_relief(GTK_BUTTON(cancel_button), GTK_RELIEF_NONE);
+        gtk_box_pack_start(GTK_BOX(bottom_box), cancel_button, FALSE, FALSE, 0);
+        g_signal_connect(G_OBJECT(cancel_button), "clicked",
+                         G_CALLBACK(cancel_button_clicked), 0);
+        gtk_tooltips_set_tip(tooltips, cancel_button,
+                             "Insert input and press Enter key", NULL);
+        /* Enter button */
+        enter_button = gtk_button_new_with_label("Enter");
+        gtk_button_set_focus_on_click(GTK_BUTTON(enter_button), FALSE);
+        gtk_button_set_image(GTK_BUTTON(enter_button),
+                             gtk_image_new_from_stock(GTK_STOCK_OK,
+                                                      GTK_ICON_SIZE_BUTTON));
+        gtk_button_set_relief(GTK_BUTTON(enter_button), GTK_RELIEF_NONE);
+        gtk_box_pack_start(GTK_BOX(bottom_box), enter_button, FALSE, FALSE, 0);
+        g_signal_connect(G_OBJECT(enter_button), "clicked",
+                         G_CALLBACK(return_button_clicked), 0);
+        gtk_tooltips_set_tip(tooltips, enter_button,
+                             "Insert input and press Enter key", NULL);
+
+        /* Insert button */
+        insert_button = gtk_button_new_with_label("Insert");
+        gtk_button_set_focus_on_click(GTK_BUTTON(insert_button), FALSE);
+        gtk_button_set_image(GTK_BUTTON(insert_button),
+                             gtk_image_new_from_stock(GTK_STOCK_OK,
+                                                      GTK_ICON_SIZE_BUTTON));
+        gtk_button_set_relief(GTK_BUTTON(insert_button), GTK_RELIEF_NONE);
+        gtk_box_pack_start(GTK_BOX(bottom_box), insert_button, FALSE, FALSE, 0);
+        g_signal_connect(G_OBJECT(insert_button), "clicked",
+                         G_CALLBACK(insert_button_clicked), 0);
+        gtk_tooltips_set_tip(tooltips, insert_button,
+                             "Insert input", NULL);
+
+        /* Back buffer button */
+        buffer_button = gtk_toggle_button_new();
+        gtk_button_set_focus_on_click(GTK_BUTTON(buffer_button), FALSE);
+        button_set_image_xpm(buffer_button, tab_xpm);
+        gtk_button_set_relief(GTK_BUTTON(buffer_button), GTK_RELIEF_NONE);
+        gtk_box_pack_start(GTK_BOX(bottom_box), buffer_button, FALSE, FALSE, 0);
+        g_signal_connect(G_OBJECT(buffer_button), "pressed",
+                         G_CALLBACK(buffer_button_pressed), NULL);
+        gtk_tooltips_set_tip(tooltips, buffer_button,
+                             "Recall previously entered input", NULL);
+        gtk_widget_set_sensitive(buffer_button, FALSE);
+
+        /* Pack the regular bottom box */
+        gtk_box_pack_start(GTK_BOX(window_vbox), bottom_box, FALSE, FALSE, 0);
+        if (!keyboard_only)
+                gtk_widget_show_all(bottom_box);
+
+        /* Update button labels */
+        toggle_button_labels(window_button_labels);
+
+        /* Set window style */
+        window_style_set(window);
+
+        if (window_embedded) {
+
+                /* Embedding in a screensaver won't let us popup new windows */
+                gtk_widget_hide(buffer_button);
+                gtk_widget_hide(train_button);
+                gtk_widget_hide(setup_button);
+
+                /* If we are embedded we need to print the plug's window XID */
+                g_signal_connect_after(G_OBJECT(window), "realize",
+                                       G_CALLBACK(print_window_xid), NULL);
+
+                gtk_container_add(GTK_CONTAINER(window), window_vbox);
+                gtk_widget_show(window);
+                return;
+        }
+
+        /* Non-embedded window configuration */
+        gtk_container_add(GTK_CONTAINER(window), window_vbox);
+
+        if(!parent){
+          gtk_window_set_keep_above(GTK_WINDOW(window), TRUE);
+          gtk_window_set_type_hint(GTK_WINDOW(window),
+                                   GDK_WINDOW_TYPE_HINT_UTILITY);
+          gtk_window_set_title(GTK_WINDOW(window), PACKAGE_NAME);
+          gtk_window_set_skip_pager_hint(GTK_WINDOW(window), TRUE);
+          gtk_window_set_skip_taskbar_hint(GTK_WINDOW(window), TRUE);
+          gtk_window_set_decorated(GTK_WINDOW(window), TRUE);
+          gtk_window_stick(GTK_WINDOW(window));
+
+          /* Coordinates passed on the command-line */
+          if (window_force_x >= 0)
+            window_frame.x = window_force_x;
+          if (window_force_y >= 0)
+            window_frame.y = window_force_y;
+          
+          /* Center window on initial startup */
+          screen = gtk_window_get_screen(GTK_WINDOW(window));
+          if (window_frame.x < 0)
+            window_frame.x = gdk_screen_get_width(screen) / 2;
+          if (window_frame.y < 0)
+            window_frame.y = gdk_screen_get_height(screen) * 3 / 4;
+          gtk_window_move(GTK_WINDOW(window), window_frame.x,
+                          window_frame.y);
+
+          /* Set the window size */
+          if (window_force_docked >= WINDOW_UNDOCKED)
+            window_docked = window_force_docked;
+          if (window_docked) {
+            int mode;
+            
+            mode = window_docked;
+            window_docked = WINDOW_UNDOCKED;
+            window_set_docked(mode);
+          }
+          
+          /* Show window */
+          if (window_force_hide)
+            window_shown = FALSE;
+          else if (window_force_show)
+            window_shown = TRUE;
+          if (window_shown)
+            gtk_widget_show(window);
+        }
+}
+        
+void window_sync(void)
+/* Sync data with profile, do not change item order! */
+{
+        profile_write("window");
+
+        /* Docking the window will mess up the desired natural frame */
+        if (!profile_read_only && window_docked) {
+                profile_sync_int(&window_frame_saved.x);
+                profile_sync_int(&window_frame_saved.y);
+        } else {
+                profile_sync_int(&window_frame.x);
+                profile_sync_int(&window_frame.y);
+        }
+
+        profile_sync_int(&training_block);
+        profile_sync_int(&window_shown);
+        profile_sync_int(&window_button_labels);
+        profile_sync_int(&keyboard_size);
+        profile_sync_int(&window_docked);
+        profile_write("\n");
+}
+
+void window_cleanup(void)
+{
+}
+
+/*
+        Unicode blocks
+*/
+
+/* This table is based on unicode-blocks.h from the gucharmap project */
+UnicodeBlock unicode_blocks[] =
+{
+        { TRUE,  0x0000, 0x007F, "Basic Latin" },
+        { TRUE,  0x0080, 0x00FF, "Latin-1 Supplement" },
+        { FALSE, 0x0100, 0x017F, "Latin Extended-A" },
+        { FALSE, 0x0180, 0x024F, "Latin Extended-B" },
+        { FALSE, 0x0250, 0x02AF, "IPA Extensions" },
+        { FALSE, 0x02B0, 0x02FF, "Spacing Modifier Letters" },
+        { FALSE, 0x0300, 0x036F, "Combining Diacritical Marks" },
+        { FALSE, 0x0370, 0x03FF, "Greek and Coptic" },
+        { FALSE, 0x0400, 0x04FF, "Cyrillic" },
+        { FALSE, 0x0500, 0x052F, "Cyrillic Supplement" },
+        { FALSE, 0x0530, 0x058F, "Armenian" },
+        { FALSE, 0x0590, 0x05FF, "Hebrew" },
+        { FALSE, 0x0600, 0x06FF, "Arabic" },
+        { FALSE, 0x0700, 0x074F, "Syriac" },
+        { FALSE, 0x0750, 0x077F, "Arabic Supplement" },
+        { FALSE, 0x0780, 0x07BF, "Thaana" },
+        { FALSE, 0x07C0, 0x07FF, "N'Ko" },
+        { FALSE, 0x0900, 0x097F, "Devanagari" },
+        { FALSE, 0x0980, 0x09FF, "Bengali" },
+        { FALSE, 0x0A00, 0x0A7F, "Gurmukhi" },
+        { FALSE, 0x0A80, 0x0AFF, "Gujarati" },
+        { FALSE, 0x0B00, 0x0B7F, "Oriya" },
+        { FALSE, 0x0B80, 0x0BFF, "Tamil" },
+        { FALSE, 0x0C00, 0x0C7F, "Telugu" },
+        { FALSE, 0x0C80, 0x0CFF, "Kannada" },
+        { FALSE, 0x0D00, 0x0D7F, "Malayalam" },
+        { FALSE, 0x0D80, 0x0DFF, "Sinhala" },
+        { FALSE, 0x0E00, 0x0E7F, "Thai" },
+        { FALSE, 0x0E80, 0x0EFF, "Lao" },
+        { FALSE, 0x0F00, 0x0FFF, "Tibetan" },
+        { FALSE, 0x1000, 0x109F, "Myanmar" },
+        { FALSE, 0x10A0, 0x10FF, "Georgian" },
+        { FALSE, 0x1100, 0x11FF, "Hangul Jamo" },
+        { FALSE, 0x1200, 0x137F, "Ethiopic" },
+        { FALSE, 0x1380, 0x139F, "Ethiopic Supplement" },
+        { FALSE, 0x13A0, 0x13FF, "Cherokee" },
+        { FALSE, 0x1400, 0x167F, "Unified Canadian Aboriginal Syllabics" },
+        { FALSE, 0x1680, 0x169F, "Ogham" },
+        { FALSE, 0x16A0, 0x16FF, "Runic" },
+        { FALSE, 0x1700, 0x171F, "Tagalog" },
+        { FALSE, 0x1720, 0x173F, "Hanunoo" },
+        { FALSE, 0x1740, 0x175F, "Buhid" },
+        { FALSE, 0x1760, 0x177F, "Tagbanwa" },
+        { FALSE, 0x1780, 0x17FF, "Khmer" },
+        { FALSE, 0x1800, 0x18AF, "Mongolian" },
+        { FALSE, 0x1900, 0x194F, "Limbu" },
+        { FALSE, 0x1950, 0x197F, "Tai Le" },
+        { FALSE, 0x1980, 0x19DF, "New Tai Lue" },
+        { FALSE, 0x19E0, 0x19FF, "Khmer Symbols" },
+        { FALSE, 0x1A00, 0x1A1F, "Buginese" },
+        { FALSE, 0x1B00, 0x1B7F, "Balinese" },
+        { FALSE, 0x1D00, 0x1D7F, "Phonetic Extensions" },
+        { FALSE, 0x1D80, 0x1DBF, "Phonetic Extensions Supplement" },
+        { FALSE, 0x1DC0, 0x1DFF, "Combining Diacritical Marks Supplement" },
+        { FALSE, 0x1E00, 0x1EFF, "Latin Extended Additional" },
+        { FALSE, 0x1F00, 0x1FFF, "Greek Extended" },
+        { FALSE, 0x2000, 0x206F, "General Punctuation" },
+        { FALSE, 0x2070, 0x209F, "Superscripts and Subscripts" },
+        { FALSE, 0x20A0, 0x20CF, "Currency Symbols" },
+        { FALSE, 0x20D0, 0x20FF, "Combining Diacritical Marks for Symbols" },
+        { FALSE, 0x2100, 0x214F, "Letterlike Symbols" },
+        { FALSE, 0x2150, 0x218F, "Number Forms" },
+        { FALSE, 0x2190, 0x21FF, "Arrows" },
+        { FALSE, 0x2200, 0x22FF, "Mathematical Operators" },
+        { FALSE, 0x2300, 0x23FF, "Miscellaneous Technical" },
+        { FALSE, 0x2400, 0x243F, "Control Pictures" },
+        { FALSE, 0x2440, 0x245F, "Optical Character Recognition" },
+        { FALSE, 0x2460, 0x24FF, "Enclosed Alphanumerics" },
+        { FALSE, 0x2500, 0x257F, "Box Drawing" },
+        { FALSE, 0x2580, 0x259F, "Block Elements" },
+        { FALSE, 0x25A0, 0x25FF, "Geometric Shapes" },
+        { FALSE, 0x2600, 0x26FF, "Miscellaneous Symbols" },
+        { FALSE, 0x2700, 0x27BF, "Dingbats" },
+        { FALSE, 0x27C0, 0x27EF, "Miscellaneous Mathematical Symbols-A" },
+        { FALSE, 0x27F0, 0x27FF, "Supplemental Arrows-A" },
+        { FALSE, 0x2800, 0x28FF, "Braille Patterns" },
+        { FALSE, 0x2900, 0x297F, "Supplemental Arrows-B" },
+        { FALSE, 0x2980, 0x29FF, "Miscellaneous Mathematical Symbols-B" },
+        { FALSE, 0x2A00, 0x2AFF, "Supplemental Mathematical Operators" },
+        { FALSE, 0x2B00, 0x2BFF, "Miscellaneous Symbols and Arrows" },
+        { FALSE, 0x2C00, 0x2C5F, "Glagolitic" },
+        { FALSE, 0x2C60, 0x2C7F, "Latin Extended-C" },
+        { FALSE, 0x2C80, 0x2CFF, "Coptic" },
+        { FALSE, 0x2D00, 0x2D2F, "Georgian Supplement" },
+        { FALSE, 0x2D30, 0x2D7F, "Tifinagh" },
+        { FALSE, 0x2D80, 0x2DDF, "Ethiopic Extended" },
+        { FALSE, 0x2E00, 0x2E7F, "Supplemental Punctuation" },
+        { FALSE, 0x2E80, 0x2EFF, "CJK Radicals Supplement" },
+        { FALSE, 0x2F00, 0x2FDF, "Kangxi Radicals" },
+        { FALSE, 0x2FF0, 0x2FFF, "Ideographic Description Characters" },
+        { FALSE, 0x3000, 0x303F, "CJK Symbols and Punctuation" },
+        { FALSE, 0x3040, 0x309F, "Hiragana" },
+        { FALSE, 0x30A0, 0x30FF, "Katakana" },
+        { FALSE, 0x3100, 0x312F, "Bopomofo" },
+        { FALSE, 0x3130, 0x318F, "Hangul Compatibility Jamo" },
+        { FALSE, 0x3190, 0x319F, "Kanbun" },
+        { FALSE, 0x31A0, 0x31BF, "Bopomofo Extended" },
+        { FALSE, 0x31C0, 0x31EF, "CJK Strokes" },
+        { FALSE, 0x31F0, 0x31FF, "Katakana Phonetic Extensions" },
+        { FALSE, 0x3200, 0x32FF, "Enclosed CJK Letters and Months" },
+        { FALSE, 0x3300, 0x33FF, "CJK Compatibility" },
+        { FALSE, 0x3400, 0x4DBF, "CJK Unified Ideographs Extension A" },
+        { FALSE, 0x4DC0, 0x4DFF, "Yijing Hexagram Symbols" },
+        { FALSE, 0x4E00, 0x9FFF, "CJK Unified Ideographs" },
+        { FALSE, 0xA000, 0xA48F, "Yi Syllables" },
+        { FALSE, 0xA490, 0xA4CF, "Yi Radicals" },
+        { FALSE, 0xA700, 0xA71F, "Modifier Tone Letters" },
+        { FALSE, 0xA720, 0xA7FF, "Latin Extended-D" },
+        { FALSE, 0xA800, 0xA82F, "Syloti Nagri" },
+        { FALSE, 0xA840, 0xA87F, "Phags-pa" },
+        { FALSE, 0xAC00, 0xD7AF, "Hangul Syllables" },
+        { FALSE, 0xD800, 0xDB7F, "High Surrogates" },
+        { FALSE, 0xDB80, 0xDBFF, "High Private Use Surrogates" },
+        { FALSE, 0xDC00, 0xDFFF, "Low Surrogates" },
+        { FALSE, 0xE000, 0xF8FF, "Private Use Area" },
+        { FALSE, 0xF900, 0xFAFF, "CJK Compatibility Ideographs" },
+        { FALSE, 0xFB00, 0xFB4F, "Alphabetic Presentation Forms" },
+        { FALSE, 0xFB50, 0xFDFF, "Arabic Presentation Forms-A" },
+        { FALSE, 0xFE00, 0xFE0F, "Variation Selectors" },
+        { FALSE, 0xFE10, 0xFE1F, "Vertical Forms" },
+        { FALSE, 0xFE20, 0xFE2F, "Combining Half Marks" },
+        { FALSE, 0xFE30, 0xFE4F, "CJK Compatibility Forms" },
+        { FALSE, 0xFE50, 0xFE6F, "Small Form Variants" },
+        { FALSE, 0xFE70, 0xFEFF, "Arabic Presentation Forms-B" },