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