From b9bf8c6f84402447eaa3a06079e51f825b50da5d Mon Sep 17 00:00:00 2001 From: atnnn Date: Tue, 20 Apr 2010 12:14:08 -0400 Subject: [PATCH] Initial check-in --- Makefile | 21 + Makefile.in | 20 + configure | 13 + debian/changelog | 6 + debian/compat | 1 + debian/control | 13 + debian/copyright | 18 + debian/dirs | 2 + debian/him-cellwriter.postinst | 5 + debian/rules | 30 + default_profile | 55 ++ src/Makefile | 8 + src/averages.c | 268 +++++ src/cellwidget.c | 2133 ++++++++++++++++++++++++++++++++++++++++ src/common.h | 292 ++++++ src/config.h | 4 + src/him_cellwriter.c | 491 +++++++++ src/keyevent.c | 473 +++++++++ src/keys.h | 98 ++ src/main.c | 980 ++++++++++++++++++ src/options.c | 570 +++++++++++ src/preprocess.c | 300 ++++++ src/recognize.c | 737 ++++++++++++++ src/recognize.h | 184 ++++ src/singleinstance.c | 98 ++ src/stroke.c | 446 +++++++++ src/window.c | 1027 +++++++++++++++++++ src/wordfreq.c | 202 ++++ 28 files changed, 8495 insertions(+) create mode 100644 Makefile create mode 100644 Makefile.in create mode 100755 configure create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/dirs create mode 100644 debian/him-cellwriter.postinst create mode 100755 debian/rules create mode 100644 default_profile create mode 100644 src/Makefile create mode 100644 src/averages.c create mode 100644 src/cellwidget.c create mode 100644 src/common.h create mode 100644 src/config.h create mode 100644 src/him_cellwriter.c create mode 100644 src/keyevent.c create mode 100644 src/keys.h create mode 100644 src/main.c create mode 100644 src/options.c create mode 100644 src/preprocess.c create mode 100644 src/recognize.c create mode 100644 src/recognize.h create mode 100644 src/singleinstance.c create mode 100644 src/stroke.c create mode 100644 src/window.c create mode 100644 src/wordfreq.c delete mode 100644 welcome diff --git a/Makefile b/Makefile new file mode 100644 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 index 0000000..4cfa353 --- /dev/null +++ b/Makefile.in @@ -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 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 index 0000000..9770441 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,6 @@ +him-cellwriter (0.1-1) unstable; urgency=low + + * Initial release + + -- Etienne Laurin Mon, 19 Apr 2010 23:27:51 -0400 + diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..7ed6ff8 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +5 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..8540758 --- /dev/null +++ b/debian/control @@ -0,0 +1,13 @@ +Source: him-cellwriter +Priority: extra +Maintainer: Etienne Laurin +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 index 0000000..fe3a33a --- /dev/null +++ b/debian/copyright @@ -0,0 +1,18 @@ +This package was debianized by Etienne Laurin 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 index 0000000..37f1452 --- /dev/null +++ b/debian/dirs @@ -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 index 0000000..f64a10e --- /dev/null +++ b/debian/him-cellwriter.postinst @@ -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 index 0000000..9269233 --- /dev/null +++ b/debian/rules @@ -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 index 0000000..6612d42 --- /dev/null +++ b/default_profile @@ -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 index 0000000..29e7faa --- /dev/null +++ b/src/Makefile @@ -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 index 0000000..88859bd --- /dev/null +++ b/src/averages.c @@ -0,0 +1,268 @@ + +/* + +cellwriter -- a character recognition input method +Copyright (C) 2007 Michael Levin + +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 +#include + +/* + 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 index 0000000..e977d59 --- /dev/null +++ b/src/cellwidget.c @@ -0,0 +1,2133 @@ + +/* + +cellwriter -- a character recognition input method +Copyright (C) 2007 Michael Levin + +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 +#include + +#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 index 0000000..ff28a7d --- /dev/null +++ b/src/common.h @@ -0,0 +1,292 @@ + +/* + +cellwriter -- a character recognition input method +Copyright (C) 2007 Michael Levin + +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 +#include + +/* + 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 index 0000000..561e7aa --- /dev/null +++ b/src/config.h @@ -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 index 0000000..02a89b8 --- /dev/null +++ b/src/him_cellwriter.c @@ -0,0 +1,491 @@ +#include "hildon-im-plugin.h" +#include "hildon-im-ui.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#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 index 0000000..0f6a92e --- /dev/null +++ b/src/keyevent.c @@ -0,0 +1,473 @@ + +/* + +cellwriter -- a character recognition input method +Copyright (C) 2007 Michael Levin + +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 +#include +#include +#include +#include +#include +#include + +/* 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 index 0000000..272e627 --- /dev/null +++ b/src/keys.h @@ -0,0 +1,98 @@ + +/* + +cellwriter -- a character recognition input method +Copyright (C) 2007 Michael Levin + +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 index 0000000..32c2a75 --- /dev/null +++ b/src/main.c @@ -0,0 +1,980 @@ + +/* + +cellwriter -- a character recognition input method +Copyright (C) 2007 Michael Levin + +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 +#include +#include +#include +#include +#ifdef HAVE_GNOME +#include +#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), + "Your 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 index 0000000..e1b41f8 --- /dev/null +++ b/src/options.c @@ -0,0 +1,570 @@ + +/* + +cellwriter -- a character recognition input method +Copyright (C) 2007 Michael Levin + +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 +#include +#ifdef HAVE_GNOME +#include +#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[] = { + { "Custom colors:", 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("Enabled Unicode blocks"); + 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("Language options"); + 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("Training samples"); + 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("Word context"); + 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("Preprocessor"); + 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 index 0000000..accaa21 --- /dev/null +++ b/src/preprocess.c @@ -0,0 +1,300 @@ + +/* + +cellwriter -- a character recognition input method +Copyright (C) 2007 Michael Levin + +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 + +/* + 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 index 0000000..0e68129 --- /dev/null +++ b/src/recognize.c @@ -0,0 +1,737 @@ + +/* + +cellwriter -- a character recognition input method +Copyright (C) 2007 Michael Levin + +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 +#include +#include +#include +#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, µsec); + 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, µsec); + 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(¤t); + 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 index 0000000..e970d23 --- /dev/null +++ b/src/recognize.h @@ -0,0 +1,184 @@ + +/* + +cellwriter -- a character recognition input method +Copyright (C) 2007 Michael Levin + +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 index 0000000..6dfdb89 --- /dev/null +++ b/src/singleinstance.c @@ -0,0 +1,98 @@ + +/* + +cellwriter -- a character recognition input method +Copyright (C) 2007 Michael Levin + +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 +#include +#include + +/* + 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 index 0000000..1bc7b70 --- /dev/null +++ b/src/stroke.c @@ -0,0 +1,446 @@ + +/* + +cellwriter -- a character recognition input method +Copyright (C) 2007 Michael Levin + +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 +#include +#include +#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 index 0000000..65db090 --- /dev/null +++ b/src/window.c @@ -0,0 +1,1027 @@ + +/* + +cellwriter -- a character recognition input method +Copyright (C) 2007 Michael Levin + +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 +#include +#include +#include + +#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), + "Training Mode: 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" }, + { FALSE, 0xFF00, 0xFFEF, "Halfwidth and Fullwidth Forms" }, + { FALSE, 0xFFF0, 0xFFFF, "Specials" }, + + /* Cut the table here because we only support 4-byte characters */ + { FALSE, 0, 0, NULL }, +}; + +void blocks_sync(void) +{ + UnicodeBlock *block; + + profile_write("blocks"); + block = unicode_blocks; + while (block->name) { + profile_sync_short(&block->enabled); + block++; + } + profile_write("\n"); +} + +void unicode_block_toggle(int block, int on) +{ + int pos, active, training_block_saved; + + if (block < 0 || unicode_blocks[block].enabled == on) + return; + unicode_blocks[block].enabled = on; + active = gtk_combo_box_get_active(GTK_COMBO_BOX(blocks_combo)); + pos = block_unicode_to_combo(block); + training_block_saved = training_block; + if (!on) + gtk_combo_box_remove_text(GTK_COMBO_BOX(blocks_combo), pos); + else + gtk_combo_box_insert_text(GTK_COMBO_BOX(blocks_combo), pos, + unicode_blocks[block].name); + update_enabled_samples(); + if ((!on && block <= training_block_saved) || active < 0) + gtk_combo_box_set_active(GTK_COMBO_BOX(blocks_combo), + active > 0 ? active - 1 : 0); + + /* Are we out of blocks? */ + if (gtk_combo_box_get_active(GTK_COMBO_BOX(blocks_combo)) < 0) { + training_block = -1; + cell_widget_train(); + } +} + +/* + Start-up message dialog +*/ + +#define WELCOME_MSG "You are either starting " PACKAGE_NAME " for the first " \ + "time or have not yet created any training samples.\n\n" \ + PACKAGE_NAME " requires accurate training samples of " \ + "your characters before it can work.\n\n" \ + PACKAGE_NAME " will now enter training mode. " \ + "Carefully draw each character in its cell and then " \ + "press the 'Train' button." + +void startup_splash_show(void) +{ + GtkWidget *dialog; + + dialog = gtk_message_dialog_new(GTK_WINDOW(window), + GTK_DIALOG_DESTROY_WITH_PARENT | + GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, + GTK_BUTTONS_OK, + "Welcome to " PACKAGE_STRING "!"); + gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog), + WELCOME_MSG); + gtk_window_set_title(GTK_WINDOW(dialog), + "Welcome to " PACKAGE_NAME "!"); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + + /* Press in the training button for the user */ + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(train_button), TRUE); +} + diff --git a/src/wordfreq.c b/src/wordfreq.c new file mode 100644 index 0000000..93231cd --- /dev/null +++ b/src/wordfreq.c @@ -0,0 +1,202 @@ + +/* + +cellwriter -- a character recognition input method +Copyright (C) 2007 Michael Levin + +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 +#include + +/* cellwidget.c */ +const char *cell_widget_word(void); + +/* + Word frequency engine +*/ + +#ifndef DISABLE_WORDFREQ + +/* TODO needs to be internationalized (wide char) + TODO user-made words list + TODO choose a list via GUI + FIXME the frequency list contains "n't" etc as separate endings, this + needs to be taken into consideration */ + +/* The number of word frequency entries to load */ +#define WORDFREQS 15000 + +typedef struct { + char string[24]; + int count; +} WordFreq; + +int wordfreq_enable = TRUE; + +static WordFreq wordfreqs[WORDFREQS + 1]; +static int wordfreqs_len, wordfreqs_count; + +void load_wordfreq(void) +/* Read in the word frequency file. The file format is: word\tcount\n */ +{ + GIOChannel *channel; + GError *error = NULL; + char buf[64], *path; + gsize bytes_read = 1; + int i; + + wordfreqs[0].string[0] = 0; + + /* Try to open the user's word frequency file */ + path = g_build_filename(g_get_home_dir(), "." PACKAGE, "wordfreq", + NULL); + channel = g_io_channel_new_file(path, "r", &error); + if (error) { + g_debug("User does not have a word frequency file, " + "loading system file"); + channel = NULL; + } + error = NULL; + g_free(path); + + /* Open the word frequency file */ + if (!channel) { + path = g_build_filename(PKGDATADIR, "wordfreq", NULL); + channel = g_io_channel_new_file(path, "r", &error); + if (error) { + g_warning("Failed to open system word frequency file " + "'%s' for reading: %s", path, error->message); + g_free(path); + return; + } + g_free(path); + } + + /* Read in every entry */ + g_debug("Parsing word frequency list"); + wordfreqs_count = 0; + for (i = 0; bytes_read > 0 && i < WORDFREQS; i++) { + char *pbuf; + int swap, len; + + /* Read a line */ + pbuf = buf - 1; + do { + g_io_channel_read_chars(channel, ++pbuf, 1, + &bytes_read, &error); + } while (bytes_read > 0 && *pbuf != '\n' && + pbuf < buf + sizeof (buf)); + *pbuf = 0; + + /* Parse the word */ + pbuf = buf; + while (*pbuf && *pbuf != '\t' && *pbuf != ' ') + pbuf++; + if (buf == pbuf) { + i--; + continue; + } + swap = *pbuf; + *pbuf = 0; + len = pbuf - buf; + if (len >= (int)sizeof (wordfreqs[i].string)) + len = sizeof (wordfreqs[i].string) - 1; + memcpy(wordfreqs[i].string, buf, len); + wordfreqs[i].string[len] = 0; + + /* Parse the count */ + *pbuf = swap; + while (*pbuf == ' ' || *pbuf == '\t') + pbuf++; + wordfreqs_count += wordfreqs[i].count = log(atoi(pbuf)); + } + wordfreqs[i].string[0] = 0; + wordfreqs_len = i; + g_io_channel_unref(channel); + g_debug("%d words parsed", i); + + return; +} + +void engine_wordfreq(void) +{ + Sample *sample; + const char *pre, *post; + int i, pre_len, post_len, chars[128]; + + if (!wordfreq_enable) + return; + pre = cell_widget_word(); + pre_len = strlen(pre); + post = pre + pre_len + 1; + post_len = strlen(post); + if (!pre_len && !post_len) + return; + memset(chars, 0, sizeof (chars)); + + /* Numbers follow numbers */ + if (g_ascii_isdigit(pre[pre_len - 1])) { + for (i = 0; i <= 9; i++) + chars['0' + i] = 1; + goto apply_table; + } + + /* Search the databases for matches (FIXME sort/index) */ + for (i = 0; i < wordfreqs_len; i++) + if ((!pre_len || + !g_ascii_strncasecmp(pre, wordfreqs[i].string, pre_len)) && + (!post_len || + !g_ascii_strncasecmp(post, wordfreqs[i].string + pre_len + + 1, post_len))) { + int ch = wordfreqs[i].string[pre_len], + ch_lower = ch, ch_upper = 0; + + if (ch < 32 || ch >= 127) + continue; + + /* Suggest proper case */ + if (g_ascii_isalpha(ch)) { + ch_lower = g_ascii_tolower(ch); + ch_upper = g_ascii_toupper(ch); + if (pre_len > 1) { + if (g_ascii_islower(pre[pre_len - 1])) + ch_upper = 0; + else + if (g_ascii_isupper(pre[pre_len - 1]) && + g_ascii_isupper(pre[pre_len - 2])) + ch_lower = 0; + } + } + + chars[ch_lower] += wordfreqs[i].count; + chars[ch_upper] += wordfreqs[i].count; + } + +apply_table: + /* Apply characters table */ + sampleiter_reset(); + while ((sample = sampleiter_next())) + if (sample->ch >= 32 && sample->ch < 127) + sample->ratings[ENGINE_WORDFREQ] = chars[sample->ch]; +} + +#endif /* DISABLE_WORDFREQ */ + diff --git a/welcome b/welcome deleted file mode 100644 index e69de29..0000000 -- 1.7.9.5