Initial check-in
[him-cellwriter] / src / cellwidget.c
diff --git a/src/cellwidget.c b/src/cellwidget.c
new file mode 100644 (file)
index 0000000..e977d59
--- /dev/null
@@ -0,0 +1,2133 @@
+
+/*
+
+cellwriter -- a character recognition input method
+Copyright (C) 2007 Michael Levin <risujin@risujin.org>
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+*/
+
+#include "config.h"
+#include "common.h"
+#include "recognize.h"
+#include "keys.h"
+#include <string.h>
+#include <malloc.h>
+
+#include "hildon-im-ui.h"
+
+/* stroke.c */
+void smooth_stroke(Stroke *s);
+void simplify_stroke(Stroke *s);
+
+/* cellwidget.c */
+int cell_widget_scrollbar_width(void);
+static void start_timeout(void);
+static void show_context_menu(int button, int time);
+static void stop_drawing(void);
+
+/*
+        Cells
+*/
+
+#define ALTERNATES 5
+#define CELL_BASELINE (cell_height / 3)
+#define CELL_BORDER (cell_height / 12)
+
+/* Msec of no mouse motion before a cell is finished */
+#define MOTION_TIMEOUT 500
+
+/* Cell flags */
+#define CELL_SHOW_INK   0x01
+#define CELL_DIRTY      0x02
+#define CELL_VERIFIED   0x04
+#define CELL_SHIFTED    0x08
+
+struct Cell {
+        Sample sample, *alts[ALTERNATES];
+        gunichar2 ch;
+        int alt_used[ALTERNATES];
+        char flags, alt_ratings[ALTERNATES];
+};
+
+/* Cell preferences */
+int cell_width = 40, cell_height = 70, cell_cols_pref = 12, cell_rows_pref = 4,
+    enable_cairo = TRUE, training = FALSE, train_on_input = TRUE,
+    right_to_left = FALSE, keyboard_enabled = TRUE, xinput_enabled = FALSE;
+
+/* Statistics */
+int corrections = 0, rewrites = 0, characters = 0, inputs = 0;
+
+/* Colors */
+GdkColor custom_active_color = RGB_TO_GDKCOLOR(255, 255, 255),
+         custom_inactive_color = RGB_TO_GDKCOLOR(212, 222, 226),
+         custom_ink_color = RGB_TO_GDKCOLOR(0, 0, 0),
+         custom_select_color = RGB_TO_GDKCOLOR(204, 0, 0);
+static GdkColor color_active, color_inactive, color_ink, color_select;
+
+static Cell *cells = NULL, *cells_saved = NULL;
+static GtkWidget *drawing_area = NULL, *training_menu, *scrollbar;
+static GdkPixmap *pixmap = NULL;
+static GdkGC *pixmap_gc = NULL;
+static GdkColor color_bg, color_bg_dark;
+static cairo_t *cairo = NULL;
+static PangoContext *pango = NULL;
+static PangoFontDescription *pango_font_desc = NULL;
+static gunichar2 *history[HISTORY_MAX];
+static int cell_cols, cell_rows, cell_row_view = 0, current_cell = -1, old_cc,
+           cell_cols_saved, cell_rows_saved, cell_row_view_saved,
+           timeout_source,
+           drawing = FALSE, inserting = FALSE, eraser = FALSE, invalid = FALSE,
+           potential_insert = FALSE, potential_hold = FALSE, cross_out = FALSE,
+           show_keys = TRUE, is_clear = TRUE, keys_dirty = FALSE;
+static double cursor_x, cursor_y;
+
+static void cell_coords(int cell, int *px, int *py)
+/* Get the int position of a cell from its index */
+{
+        int cell_y, cell_x;
+
+        cell -= cell_row_view * cell_cols;
+        cell_y = cell / cell_cols;
+        cell_x = cell - cell_y * cell_cols;
+        *px = (!right_to_left ? cell_x * cell_width :
+                                (cell_cols - cell_x - 1) * cell_width) + 1;
+        *py = cell_y * cell_height + 1;
+}
+
+static void set_pen_color(Sample *sample, int cell)
+/* Selects the pen color depending on if the sample being drawn is the input
+   or the template sample */
+{
+        if (sample == input || sample == &cells[cell].sample)
+                cairo_set_source_gdk_color(cairo, &color_ink, 1.);
+        else
+                cairo_set_source_gdk_color(cairo, &color_select, 1.);
+}
+
+static void render_point(Sample *sample, int cell, int stroke, Vec2 *offset)
+/* Draw a single point stroke */
+{
+        double x, y, radius;
+        int cx, cy;
+
+        if (!pixmap || stroke < 0 || !sample || stroke >= sample->len ||
+            sample->strokes[stroke]->len < 1)
+                return;
+
+        /* Apply offset */
+        x = sample->strokes[stroke]->points[0].x;
+        y = sample->strokes[stroke]->points[0].y;
+        if (offset) {
+                x += offset->x;
+                y += offset->y;
+        }
+
+        /* Unscale coordinates */
+        cell_coords(cell, &cx, &cy);
+        x = cx + cell_width / 2 + x * cell_height / SCALE;
+        y = cy + cell_height / 2 + y * cell_height / SCALE;
+
+        /* Draw a dot with cairo */
+        cairo_new_path(cairo);
+        radius = cell_height / 33.;
+        cairo_arc(cairo, x, y, radius > 1. ? radius : 1., 0., 2 * M_PI);
+        set_pen_color(sample, cell);
+        cairo_fill(cairo);
+
+        gtk_widget_queue_draw_area(drawing_area, x - radius - 0.5,
+                                   y - radius - 0.5, radius * 2 + 0.5,
+                                   radius * 2 + 0.5);
+}
+
+static void render_segment(Sample *sample, int cell, int stroke, int seg,
+                           Vec2 *offset)
+/* Draw a segment of the stroke
+   FIXME since the segments are not properly connected according to Cairo,
+         there is a bit of missing value at the segment connection points */
+{
+        double pen_width, x1, x2, y1, y2;
+        int xmin, xmax, ymin, ymax, cx, cy, pen_range;
+
+        if (!cairo || stroke < 0 || !sample || stroke >= sample->len ||
+            seg < 0 || seg >= sample->strokes[stroke]->len - 1)
+                return;
+
+        x1 = sample->strokes[stroke]->points[seg].x;
+        x2 = sample->strokes[stroke]->points[seg + 1].x;
+        y1 = sample->strokes[stroke]->points[seg].y;
+        y2 = sample->strokes[stroke]->points[seg + 1].y;
+
+        /* Apply offset */
+        if (offset) {
+                x1 += offset->x;
+                y1 += offset->y;
+                x2 += offset->x;
+                y2 += offset->y;
+        }
+
+        /* Unscale coordinates */
+        cell_coords(cell, &cx, &cy);
+        x1 = cx + cell_width / 2 + x1 * cell_height / SCALE;
+        x2 = cx + cell_width / 2 + x2 * cell_height / SCALE;
+        y1 = cy + cell_height / 2 + y1 * cell_height / SCALE;
+        y2 = cy + cell_height / 2 + y2 * cell_height / SCALE;
+
+        /* Find minimum and maximum x and y */
+        if (x1 > x2) {
+                xmax = x1 + 0.9999;
+                xmin = x2;
+        } else {
+                xmin = x1;
+                xmax = x2 + 0.9999;
+        }
+        if (y1 > y2) {
+                ymax = y1 + 0.9999;
+                ymin = y2;
+        } else {
+                ymin = y1;
+                ymax = y2 + 0.9999;
+        }
+
+        /* Draw the new segment using Cairo */
+        cairo_new_path(cairo);
+        cairo_move_to(cairo, x1, y1);
+        cairo_line_to(cairo, x2, y2);
+        set_pen_color(sample, cell);
+        pen_width = cell_height / 33.;
+        if (pen_width < 1.)
+                pen_width = 1.;
+        cairo_set_line_width(cairo, pen_width);
+        cairo_stroke(cairo);
+
+        /* Dirty only the new segment */
+        pen_range = 2 * pen_width + 0.9999;
+        gtk_widget_queue_draw_area(drawing_area, xmin - pen_range,
+                                   ymin - pen_range,
+                                   xmax - xmin + pen_range + 1,
+                                   ymax - ymin + pen_range + 1);
+}
+
+static void render_sample(Sample *sample, int cell)
+/* Render the ink from a sample in a cell */
+{
+        Vec2 sc_to_ic;
+        int i, j;
+
+        if (!sample)
+                return;
+
+        /* Center stored samples on input */
+        if (sample != &cells[cell].sample)
+                center_samples(&sc_to_ic, sample, &cells[cell].sample);
+        else
+                vec2_set(&sc_to_ic, 0., 0.);
+
+        for (i = 0; i < sample->len; i++)
+                if (sample->strokes[i]->len <= 1 ||
+                    sample->strokes[i]->spread < DOT_SPREAD)
+                        render_point(sample, cell, i, &sc_to_ic);
+                else
+                        for (j = 0; j < sample->strokes[i]->len - 1; j++)
+                                render_segment(sample, cell, i, j, &sc_to_ic);
+}
+
+static int cell_offscreen(int cell)
+{
+        int rows, cols;
+
+        cols = cell_cols;
+        rows = cell_rows < cell_rows_pref ? cell_rows : cell_rows_pref;
+        return cell < cell_row_view * cols ||
+               cell >= (cell_row_view + rows) * cols;
+}
+
+static void dirty_cell(int cell)
+{
+        if (!cell_offscreen(cell))
+                cells[cell].flags |= CELL_DIRTY;
+}
+
+static void dirty_all(void)
+{
+        int i, rows;
+
+        rows = cell_row_view + cell_rows_pref > cell_rows ?
+               cell_rows : cell_row_view + cell_rows_pref;
+        for (i = cell_cols * cell_row_view; i < rows * cell_cols; i++)
+                cells[i].flags |= CELL_DIRTY;
+}
+
+static void render_cell(int i)
+{
+        cairo_pattern_t *pattern;
+        GdkColor color, *base_color;
+        Cell *pc;
+        int x, y, active, cols, samples = 0;
+
+        if (!cairo || !pixmap || !pixmap_gc || cell_offscreen(i))
+                return;
+        pc = cells + i;
+        cell_coords(i, &x, &y);
+        if (training) {
+                samples = char_trained(pc->ch);
+                active = pc->ch && (samples > 0 ||
+                                    (current_cell == i && input &&
+                                     !invalid && input->len));
+        } else
+                active = pc->ch || (current_cell == i && !inserting &&
+                                    !invalid && input && input->len);
+        base_color = active ? &color_active : &color_inactive;
+
+        /* Fill above baseline */
+        gdk_gc_set_rgb_fg_color(pixmap_gc, base_color);
+        gdk_draw_rectangle(pixmap, pixmap_gc, TRUE, x, y, cell_width,
+                                cell_height - CELL_BASELINE);
+
+        /* Fill baseline */
+        highlight_gdk_color(base_color, &color, 0.1);
+        gdk_gc_set_rgb_fg_color(pixmap_gc, &color);
+        gdk_draw_rectangle(pixmap, pixmap_gc, TRUE, x, y + cell_height -
+                           CELL_BASELINE, cell_width, CELL_BASELINE);
+
+        /* Cairo clip region */
+        cairo_reset_clip(cairo);
+        cairo_rectangle(cairo, x, y, cell_width, cell_height);
+        cairo_clip(cairo);
+
+        /* Separator line */
+        cols = cell_cols;
+        if ((!right_to_left && i % cell_cols) ||
+            (right_to_left && i % cell_cols != cols - 1)) {
+                highlight_gdk_color(base_color, &color, 0.5);
+                pattern = cairo_pattern_create_linear(x, y, x, y + cell_height);
+                cairo_pattern_add_gdk_color_stop(pattern, 0.0, &color, 0.);
+                cairo_pattern_add_gdk_color_stop(pattern, 0.5, &color, 1.);
+                cairo_pattern_add_gdk_color_stop(pattern, 1.0, &color, 0.);
+                cairo_set_source(cairo, pattern);
+                cairo_set_line_width(cairo, 0.5);
+                cairo_move_to(cairo, x + 0.5, y);
+                cairo_line_to(cairo, x + 0.5, y + cell_height - 1);
+                cairo_stroke(cairo);
+                cairo_pattern_destroy(pattern);
+        }
+
+        /* Draw ink if shown */
+        if ((cells[i].ch && cells[i].flags & CELL_SHOW_INK) ||
+            (current_cell == i && input && input->len)) {
+                int j;
+
+                render_sample(&cells[i].sample, i);
+                if (cells[i].ch)
+                        for (j = 0; j < ALTERNATES && cells[i].alts[j]; j++)
+                                if (sample_valid(cells[i].alts[j],
+                                                 cells[i].alt_used[j]) &&
+                                    cells[i].alts[j]->ch == cells[i].ch) {
+                                        render_sample(cells[i].alts[j], i);
+                                        break;
+                                }
+        }
+
+        /* Draw letter if recognized or training */
+        else if (pc->ch && (current_cell != i || !input || !input->len)) {
+                PangoLayout *layout;
+                PangoRectangle ink_ext, log_ext;
+                char string[6] = { 0, 0, 0, 0, 0, 0 };
+
+                /* Training color is determined by how well a character is
+                   trained */
+                if (training) {
+                        if (samples)
+                                highlight_gdk_color(&color_ink, &color,
+                                                    0.5 - ((double)samples) /
+                                                    samples_max / 2.);
+                        else
+                                highlight_gdk_color(&color_inactive,
+                                                    &color, 0.2);
+                }
+
+                /* Use ink color unless this is a questionable match */
+                else {
+                        color = color_ink;
+                        if (!(pc->flags & CELL_VERIFIED) && pc->alts[0] &&
+                            pc->alts[1] && pc->ch == pc->alts[0]->ch &&
+                            pc->alt_ratings[0] - pc->alt_ratings[1] <= 10)
+                                color = color_select;
+                }
+
+                cairo_set_source_gdk_color(cairo, &color, 1.);
+                layout = pango_layout_new(pango);
+                cairo_move_to(cairo, x, y);
+                g_unichar_to_utf8(pc->ch, string);
+                pango_layout_set_text(layout, string, 6);
+                pango_layout_set_font_description(layout, pango_font_desc);
+                pango_layout_get_pixel_extents(layout, &ink_ext, &log_ext);
+                cairo_rel_move_to(cairo,
+                                  cell_width / 2 - log_ext.width / 2, 2);
+                pango_cairo_show_layout(cairo, layout);
+                g_object_unref(layout);
+        }
+
+        /* Insertion arrows */
+        if (!invalid && inserting &&
+            (current_cell == i || current_cell == i + 1)) {
+                double width, stem, height;
+
+                cairo_set_source_gdk_color(cairo, &color_select, 1.);
+                width = CELL_BORDER;
+                stem = CELL_BORDER / 2;
+                height = CELL_BORDER;
+                if ((!right_to_left && current_cell == i) ||
+                    (right_to_left && current_cell == i + 1)) {
+
+                        /* Top right arrow */
+                        cairo_move_to(cairo, x, y + 1);
+                        cairo_line_to(cairo, x + stem, y + 1);
+                        cairo_line_to(cairo, x + stem, y + height);
+                        cairo_line_to(cairo, x + width, y + height);
+                        cairo_line_to(cairo, x, y + height * 2);
+                        cairo_close_path(cairo);
+                        cairo_fill(cairo);
+
+                        /* Bottom right arrow */
+                        cairo_move_to(cairo, x, y + cell_height - 1);
+                        cairo_line_to(cairo, x + stem, y + cell_height - 1);
+                        cairo_line_to(cairo, x + stem,
+                                      y + cell_height - height);
+                        cairo_line_to(cairo, x + width,
+                                      y + cell_height - height);
+                        cairo_line_to(cairo, x, y + cell_height - height * 2);
+                        cairo_close_path(cairo);
+                        cairo_fill(cairo);
+
+                } else if ((!right_to_left && current_cell == i + 1) ||
+                           (right_to_left && current_cell == i)) {
+                        double ox;
+
+                        ox = i % cell_cols == cell_cols - 1 ? 0. : 1.;
+
+                        /* Top left arrow */
+                        cairo_move_to(cairo, x + cell_width + ox, y + 1);
+                        cairo_line_to(cairo, x + cell_width - stem + ox,
+                                      y + 1);
+                        cairo_line_to(cairo, x + cell_width - stem + ox,
+                                      y + height);
+                        cairo_line_to(cairo, x + cell_width - width + ox,
+                                      y + height);
+                        cairo_line_to(cairo, x + cell_width + ox,
+                                      y + height * 2);
+                        cairo_close_path(cairo);
+                        cairo_fill(cairo);
+
+                        /* Bottom left arrow */
+                        cairo_move_to(cairo, x + cell_width + ox,
+                                      y + cell_height - 1);
+                        cairo_line_to(cairo, x + cell_width - stem + ox,
+                                      y + cell_height - 1);
+                        cairo_line_to(cairo, x + cell_width - stem + ox,
+                                      y + cell_height - height);
+                        cairo_line_to(cairo, x + cell_width - width + ox,
+                                      y + cell_height - height);
+                        cairo_line_to(cairo, x + cell_width + ox,
+                                      y + cell_height - height * 2);
+                        cairo_close_path(cairo);
+                        cairo_fill(cairo);
+
+                }
+        }
+
+        gtk_widget_queue_draw_area(drawing_area, x, y, cell_width, cell_height);
+        pc->flags &= ~CELL_DIRTY;
+
+}
+
+static void render_dirty(void)
+/* Render cells marked dirty */
+{
+        int i;
+
+        for (i = cell_row_view * cell_cols; i < cell_rows * cell_cols; i++)
+                if (cells[i].flags & CELL_DIRTY)
+                        render_cell(i);
+}
+
+void cell_widget_render(void)
+/* Render the cells */
+{
+        int i, cols, rows, width, height;
+
+        if (!cairo || !pixmap || !pixmap_gc)
+                return;
+
+        /* On-screen keyboard eats up some cells on the end */
+        cols = cell_cols;
+
+        /* Render cells */
+        for (i = cell_row_view * cols; i < cell_rows * cols; i++)
+                render_cell(i);
+
+        /* Draw border */
+        rows = cell_rows < cell_rows_pref ? cell_rows : cell_rows_pref;
+        width = cell_width * cols + 1;
+        height = cell_height * rows + 1;
+        gdk_gc_set_rgb_fg_color(pixmap_gc, &color_bg_dark);
+        if (!right_to_left)
+                gdk_draw_rectangle(pixmap, pixmap_gc, FALSE, 0, 0,
+                                   width, height);
+        else
+                gdk_draw_rectangle(pixmap, pixmap_gc, FALSE,
+                                   drawing_area->allocation.width - width - 1,
+                                   0, width, height);
+
+        /* Fill extra space to the right */
+        gdk_gc_set_rgb_fg_color(pixmap_gc, &color_bg);
+        if (!right_to_left)
+                gdk_draw_rectangle(pixmap, pixmap_gc, TRUE, width + 1, 0,
+                                   drawing_area->allocation.width - width,
+                                   height + 1);
+        else
+                gdk_draw_rectangle(pixmap, pixmap_gc, TRUE, 0, 0,
+                                   drawing_area->allocation.width - width - 1,
+                                   height + 1);
+
+        /* Fill extra space below */
+        gdk_draw_rectangle(pixmap, pixmap_gc, TRUE, 0, height + 1,
+                           drawing_area->allocation.width,
+                           drawing_area->allocation.height - height + 1);
+
+        /* Dirty the entire drawing area */
+        gtk_widget_queue_draw(drawing_area);
+}
+
+static void clear_cell(int i)
+{
+        Cell *cell;
+
+        cell = cells + i;
+        cell->flags = 0;
+        if (cell->ch || i == current_cell) {
+                if (i == current_cell)
+                        input = NULL;
+                cell->flags |= CELL_DIRTY;
+        }
+        clear_sample(&cell->sample);
+        cell->ch = 0;
+        cell->alts[0] = NULL;
+}
+
+static void pad_cell(int cell)
+{
+        int i;
+
+        /* Turn any blank cells behind the cell into spaces */
+        for (i = cell - 1; i >= 0 && !cells[i].ch; i--) {
+                cells[i].ch = ' ';
+                cells[i].flags |= CELL_DIRTY;
+        }
+}
+
+static void free_cells(void)
+/* Free sample data */
+{
+        int i;
+
+        if (!cells)
+                return;
+        for (i = 0; i < cell_rows * cell_cols; i++)
+                clear_cell(i);
+        g_free(cells);
+        cells = NULL;
+        input = NULL;
+}
+
+static void wrap_cells(int new_rows, int new_cols)
+/* Word wrap cells */
+{
+        Cell *new_cells;
+        int i, j, size, row, col, break_i = -1, break_j = -1;
+
+        /* Allocate and clear the new grid */
+        if (new_rows < 1)
+                new_rows = 1;
+        size = new_rows * new_cols * sizeof (Cell);
+        new_cells = g_malloc0(size);
+
+        for (i = 0, j = 0, row = 0, col = 0; i < cell_rows * cell_cols; i++) {
+                if (!cells[i].ch)
+                        continue;
+
+                /* Break at non-alphanumeric characters */
+                if (!g_unichar_isalnum(cells[i].ch)) {
+                        break_i = i;
+                        break_j = j;
+                }
+
+                if (col >= new_cols) {
+
+                        /* If we need to, allocate room for the new row */
+                        if (++row >= new_rows) {
+                                size = ++new_rows * new_cols * sizeof (Cell);
+                                new_cells = g_realloc(new_cells, size);
+                                memset(new_cells + (new_rows - 1) * new_cols,
+                                       0, new_cols * sizeof (Cell));
+                        }
+
+                        /* Move any hanging words down to the next row */
+                        size = i - break_i - 1;
+                        if (size >= 0 && size < i - 1) {
+                                memset(new_cells + break_j + 1, 0,
+                                       sizeof (Cell) * size);
+                                i = break_i + 1;
+                                break_i = -1;
+                        }
+                        col = 0;
+                        if (!cells[i].ch)
+                                continue;
+                }
+                new_cells[j++] = cells[i];
+                col++;
+        }
+
+        /* If we have filled the last row, we need to add a new row */
+        if (col >= new_cols && row >= new_rows - 1) {
+                size = ++new_rows * new_cols * sizeof (Cell);
+                new_cells = g_realloc(new_cells, size);
+                memset(new_cells + (new_rows - 1) * new_cols, 0,
+                       new_cols * sizeof (Cell));
+        }
+
+        /* Only free the cell array, NOT the samples as we have copied the
+           Sample data over to the new cell array */
+        g_free(cells);
+        cells = new_cells;
+
+        /* Scroll the grid */
+        if (new_rows > cell_rows && new_rows > cell_rows_pref)
+                cell_row_view += new_rows - cell_rows;
+
+        /* Do not let the row view look too far down */
+        if (cell_row_view + cell_rows_pref > new_rows) {
+                cell_row_view = new_rows - cell_rows_pref;
+                if (cell_row_view < 0)
+                        cell_row_view = 0;
+        }
+
+        cell_rows = new_rows;
+        cell_cols = new_cols;
+}
+
+static int set_size_request(int force)
+/* Resize the drawing area if necessary */
+{
+        int new_w, new_h, rows, resized;
+
+        new_w = cell_cols * cell_width + 2;
+        rows = cell_rows;
+        if (rows > cell_rows_pref)
+                rows = cell_rows_pref;
+        new_h = rows * cell_height + 2;
+        resized = new_w != drawing_area->allocation.width ||
+                  new_h != drawing_area->allocation.height || force;
+        if (!resized)
+                return FALSE;
+        gtk_widget_set_size_request(drawing_area, new_w, new_h);
+        return TRUE;
+}
+
+static int pack_cells(int new_rows, int new_cols)
+/* Pack and position cells, resize widget and window when necessary.
+   Returns TRUE if the widget was resized in the process and can expect a
+   configure event in the near future. */
+{
+        int i, rows, range, new_range;
+
+        /* Must have at least one row */
+        if (new_rows < 1)
+                new_rows = 1;
+
+        /* Word wrapping will perform its own memory allocation */
+        if (!training && cells)
+                wrap_cells(new_rows, new_cols);
+
+        else if (!cells || new_rows != cell_rows || new_cols != cell_cols) {
+
+                /* Find minimum number of rows necessary */
+                if (cells) {
+                        for (i = cell_rows * cell_cols - 1; i > 0; i--)
+                                if (cells[i].ch)
+                                        break;
+                        rows = i / new_cols + 1;
+                        if (new_rows < rows)
+                                new_rows = rows;
+                        new_range = new_rows * new_cols;
+
+                        /* If we have shrunk the grid, clear cells outside */
+                        range = cell_rows * cell_cols;
+                        for (i = new_range; i < range; i++)
+                                clear_cell(i);
+                } else {
+                        range = 0;
+                        new_range = new_rows * new_cols;
+                }
+
+                /* Allocate enough room, clear any new cells */
+                cells = g_realloc(cells, new_rows * new_cols * sizeof (Cell));
+                if (new_range > range)
+                        memset(cells + range, 0,
+                               (new_range - range) * sizeof (Cell));
+
+                cell_rows = new_rows;
+                cell_cols = new_cols;
+        }
+        dirty_all();
+
+        /* Update the scrollbar */
+        if (cell_rows <= cell_rows_pref) {
+                cell_row_view = 0;
+                gtk_widget_hide(scrollbar);
+        } else {
+                GtkObject *adjustment;
+
+                if (cell_row_view > cell_rows - cell_rows_pref)
+                        cell_row_view = cell_rows - cell_rows_pref;
+                if (cell_row_view < 0)
+                        cell_row_view = 0;
+                adjustment = gtk_adjustment_new(cell_row_view, 0, cell_rows, 1,
+                                                cell_rows_pref, cell_rows_pref);
+                gtk_range_set_adjustment(GTK_RANGE(scrollbar),
+                                         GTK_ADJUSTMENT(adjustment));
+                gtk_widget_show(scrollbar);
+        }
+
+        return set_size_request(FALSE);
+}
+
+static void stop_timeout(void)
+{
+        if (!timeout_source)
+                return;
+        g_source_remove(timeout_source);
+        timeout_source = 0;
+}
+
+static void finish_cell(int cell)
+{
+        stop_timeout();
+        if (cell < 0 || cell >= cell_rows * cell_cols ||
+            !input || input->len < 1)
+                return;
+        cells[cell].flags |= CELL_DIRTY;
+
+        /* Train on the input */
+        if (training)
+                train_sample(&cells[cell].sample, TRUE);
+
+        /* Recognize input */
+        else if (input && input->strokes[0] && input->strokes[0]->len) {
+                Cell *pc = cells + cell;
+                int i;
+
+                /* Track stats */
+                if (pc->ch && pc->ch != ' ')
+                        rewrites++;
+                inputs++;
+
+                old_cc = cell;
+                recognize_sample(input, pc->alts, ALTERNATES);
+                pc->ch = input->ch;
+                pc->flags &= ~CELL_VERIFIED;
+                if (pc->ch)
+                        pad_cell(cell);
+
+                /* Copy the alternate ratings and usage stamps before they're
+                   overwritten by another call to recognize_sample() */
+                for (i = 0; i < ALTERNATES && pc->alts[i]; i++) {
+                        pc->alt_ratings[i] = pc->alts[i]->rating;
+                        pc->alt_used[i] = pc->alts[i]->used;
+                }
+
+                /* Add a row if this is the last cell */
+                if (cell == cell_rows * cell_cols - 1)
+                        pack_cells(0, cell_cols);
+        }
+
+        input = NULL;
+        drawing = FALSE;
+}
+
+static gboolean finish_timeout(void)
+/* Motion timeout for finishing drawing a cell */
+{
+        finish_cell(current_cell);
+        render_dirty();
+        timeout_source = 0;
+        start_timeout();
+        return FALSE;
+}
+
+static gboolean row_timeout(void)
+/* Motion timeout for adding a row */
+{
+        pack_cells(cell_rows + 1, cell_cols);
+        cell_widget_render();
+        timeout_source = 0;
+        return FALSE;
+}
+
+static int check_clear(void)
+{
+        int i;
+
+        if (is_clear)
+                return TRUE;
+        if (training || (input && input->len))
+                return FALSE;
+        for (i = 0; i < cell_cols * cell_rows; i++)
+                if (cells[i].ch)
+                        return FALSE;
+        return TRUE;
+}
+
+static gboolean is_clear_timeout(void)
+/* Motion timeout for checking clear state */
+{
+        timeout_source = 0;
+        if (is_clear || !check_clear())
+                return FALSE;
+
+        /* Show the on-screen keyboard */
+        show_keys = keyboard_enabled;
+        is_clear = TRUE;
+
+        pack_cells(1, cell_cols);
+        cell_widget_render();
+        return FALSE;
+}
+
+static gboolean hold_timeout(void)
+/* Motion timeout for popping up a hold-click context menu */
+{
+        if (potential_hold) {
+                potential_hold = FALSE;
+                stop_drawing();
+                show_context_menu(1, gtk_get_current_event_time());
+        }
+        timeout_source = 0;
+        return FALSE;
+}
+
+static void start_timeout(void)
+/* If a timeout action is approriate for the current situation, start a
+   timeout */
+{
+        GSourceFunc func = NULL;
+
+        if (potential_hold)
+                return;
+        stop_timeout();
+        if (cross_out)
+                return;
+
+        /* Events below are not triggered while drawing */
+        if (!drawing) {
+                if (input)
+                        func = (GSourceFunc)finish_timeout;
+                else if (!cells[cell_rows * cell_cols - 1].ch &&
+                         cells[cell_rows * cell_cols - 2].ch && !training)
+                        func = (GSourceFunc)row_timeout;
+                else if (!is_clear && check_clear())
+                        func = (GSourceFunc)is_clear_timeout;
+        }
+
+        if (func)
+                timeout_source = g_timeout_add(MOTION_TIMEOUT, func, NULL);
+}
+
+static void start_hold(void)
+{
+        potential_hold = TRUE;
+        if (timeout_source)
+                g_source_remove(timeout_source);
+        timeout_source = g_timeout_add(MOTION_TIMEOUT,
+                                       (GSourceFunc)hold_timeout, NULL);
+}
+
+void cell_widget_set_cursor(int recreate)
+/* Set the drawing area cursor to a black box pen cursor or to a blank cursor
+   depending on which is appropriate */
+{
+        static char bits[] = { 0xff, 0xff, 0xff };      /* Square cursor */
+                           /*{ 0x02, 0xff, 0x02 };*/    /* Cross cursor */
+        static GdkCursor *square;
+        GdkPixmap *pixmap;
+        GdkCursor *cursor;
+
+        /* Ink color changed, recreate cursor */
+        if (recreate) {
+                if (square)
+                        gdk_cursor_unref(square);
+                pixmap = gdk_bitmap_create_from_data(NULL, bits, 3, 3);
+                square = gdk_cursor_new_from_pixmap(pixmap, pixmap,
+                                                    &color_ink,
+                                                    &color_ink, 1, 1);
+                g_object_unref(pixmap);
+        }
+        cursor = square;
+
+        /* Eraser cursor */
+        if (eraser || cross_out) {
+                GdkDisplay *display;
+
+                display = gtk_widget_get_display(drawing_area);
+                cursor = gdk_cursor_new_for_display(display, GDK_CIRCLE);
+        }
+
+        gdk_window_set_cursor(drawing_area->window,
+                              invalid || inserting ? NULL : cursor);
+}
+
+static void stop_drawing(void)
+/* Ends the current stroke and applies various processing functions */
+{
+        Stroke *stroke;
+
+        if (!drawing) {
+                if (cross_out) {
+                        cross_out = FALSE;
+                        cell_widget_set_cursor(FALSE);
+                }
+                return;
+        }
+        drawing = FALSE;
+        if (!input || input->len >= STROKES_MAX)
+                return;
+        stroke = input->strokes[input->len - 1];
+        smooth_stroke(stroke);
+        simplify_stroke(stroke);
+        process_stroke(stroke);
+        render_cell(current_cell);
+        render_sample(input, current_cell);
+        start_timeout();
+}
+
+static void erase_cell(int cell)
+{
+        if (!training) {
+                clear_cell(cell);
+                render_dirty();
+        } else {
+                untrain_char(cells[cell].ch);
+                render_cell(cell);
+        }
+}
+
+static void check_cell(double x, double y, GdkDevice *device)
+/* Check if we have changed to a different cell */
+{
+        int cell_x, cell_y, cell, rem_x, rem_y,
+            old_inserting, old_invalid, old_eraser, old_cross_out;
+
+        /* Stop drawing first */
+        old_cross_out = cross_out;
+        if (drawing && !cross_out) {
+                int dx, dy;
+
+                /* Check if we have started the cross-out gesture */
+                cell_coords(current_cell, &cell_x, &cell_y);
+                cell_x += cell_width / 2;
+                cell_y += cell_height / 2;
+                dx = cell_x - x;
+                dy = cell_y - y;
+                if (dx < 0)
+                        dx = -dx;
+                if (dy < 0)
+                        dy = -dy;
+                if (dx < cell_width && dy < cell_height)
+                        return;
+
+                cross_out = TRUE;
+                drawing = FALSE;
+                clear_sample(input);
+                input = NULL;
+                erase_cell(current_cell);
+        }
+
+        /* Is this the eraser tip? */
+        old_eraser = eraser;
+        eraser = device && device->source == GDK_SOURCE_ERASER;
+
+        /* Adjust for border */
+        x--;
+        y--;
+
+        /* Right-to-left mode inverts the x-axis */
+        if (right_to_left)
+                x = cell_cols * cell_width - x - 1;
+
+        /* What cell are we hovering over? */
+        cell_y = y / cell_height + cell_row_view;
+        cell_x = x / cell_width;
+        cell = cell_cols * cell_y + cell_x;
+
+        /* Out of bounds or invalid cell */
+        old_invalid = invalid;
+        invalid = cell_x < 0 || cell_y < 0 || cell_x >= cell_cols ||
+                  cell_y >= cell_rows || cell_offscreen(cell) ||
+                  (training && !cells[cell].ch);
+
+        /* Are we in the insertion hotspot? */
+        rem_x = x - cell_x * cell_width;
+        rem_y = y - (cell_y - cell_row_view) * cell_height;
+        old_inserting = inserting;
+        inserting = FALSE;
+        if (!cross_out && !eraser && !invalid && !training && !input &&
+            (rem_y <= CELL_BORDER * 2 ||
+             rem_y > cell_height - CELL_BORDER * 2)) {
+                if (rem_x <= CELL_BORDER + 1)
+                        inserting = TRUE;
+                else if (cell < cell_rows * cell_cols - 1 &&
+                         rem_x > cell_width - CELL_BORDER) {
+                        inserting = TRUE;
+                        cell++;
+                }
+        }
+
+        /* Current cell has changed */
+        old_cc = current_cell;
+        if (current_cell != cell) {
+                current_cell = cell;
+                if (!cross_out)
+                        finish_cell(old_cc);
+        }
+
+        /* We have moved into or out of the insertion hotspot */
+        if (old_inserting != inserting || old_cc != cell) {
+                if (old_inserting) {
+                        dirty_cell(old_cc);
+                        dirty_cell(old_cc - 1);
+                }
+                if (inserting) {
+                        dirty_cell(current_cell);
+                        dirty_cell(current_cell - 1);
+                }
+        }
+
+        /* Update cursor if necessary */
+        if (old_invalid != invalid || old_inserting != inserting ||
+            old_eraser != eraser || old_cross_out != cross_out)
+                cell_widget_set_cursor(FALSE);
+
+        render_dirty();
+}
+
+static void unclear(int render)
+/* Hides the on-screen keyboard and re-renders the cells.
+   FIXME we only need to render dirty cells */
+{
+        is_clear = FALSE;
+        if (!show_keys)
+                return;
+        show_keys = FALSE;
+        if (render)
+                cell_widget_render();
+}
+
+static void draw(double x, double y)
+{
+        Stroke *s;
+        int cx, cy;
+
+        if (current_cell < 0)
+                return;
+
+        /* Hide the on-screen keyboard */
+        unclear(TRUE);
+
+        /* New character */
+        if (!input || !input->len) {
+                clear_sample(&cells[current_cell].sample);
+                cells[current_cell].alts[0] = NULL;
+                input = &cells[current_cell].sample;
+                cells[current_cell].sample.ch = cells[current_cell].ch;
+        }
+
+        /* Allocate a new stroke if we aren't already drawing */
+        s = input->strokes[input->len - 1];
+        if (!drawing) {
+                if (input->len >= STROKES_MAX)
+                        return;
+                s = input->strokes[input->len++]= stroke_new(0);
+                drawing = TRUE;
+                if (input->len == 1)
+                        render_cell(current_cell);
+        }
+
+        /* Check bounds */
+        cell_coords(current_cell, &cx, &cy);
+
+        /* Normalize the input */
+        x = (x - cx - cell_width / 2) * SCALE / cell_height;
+        y = (y - cy - cell_height / 2) * SCALE / cell_height;
+
+        draw_stroke(&input->strokes[input->len - 1], x, y);
+}
+
+static void insert_cell(int cell)
+{
+        int i;
+
+        /* Find a blank to consume */
+        for (i = cell; i < cell_rows * cell_cols; i++)
+                if (!cells[i].ch)
+                        break;
+
+        /* Insert a row if necessary */
+        if (i >= cell_rows * cell_cols - 1) {
+                cells = g_realloc(cells,
+                                  ++cell_rows * cell_cols * sizeof (Cell));
+                memset(cells + (cell_rows - 1) * cell_cols, 0,
+                       cell_cols * sizeof (Cell));
+                if (cell_rows > cell_rows_pref)
+                        cell_row_view++;
+        }
+
+        if (i > cell)
+                memmove(cells + cell + 1, cells + cell,
+                        (i - cell) * sizeof (Cell));
+        cells[cell].ch = ' ';
+        cells[cell].alts[0] = NULL;
+        cells[cell].sample.len = 0;
+        cells[cell].sample.ch = 0;
+        pad_cell(cell);
+        pack_cells(0, cell_cols);
+        unclear(FALSE);
+        cell_widget_render();
+}
+
+static void delete_cell(int cell)
+{
+        int i, rows;
+
+        clear_cell(cell);
+        memmove(cells + cell, cells + cell + 1,
+                (cell_rows * cell_cols - cell - 1) * sizeof (Cell));
+
+        /* Delete a row if necessary */
+        for (i = 0; i < cell_cols &&
+             !cells[(cell_rows - 1) * cell_cols + i].ch; i++);
+        rows = cell_rows;
+        if (i == cell_cols && cell_rows > 1 &&
+            !cells[(cell_rows - 1) * cell_cols - 1].ch)
+                rows--;
+        cells[cell_rows * cell_cols - 1].ch = 0;
+        cells[cell_rows * cell_cols - 1].alts[0] = NULL;
+
+        pack_cells(0, cell_cols);
+        cell_widget_render();
+}
+
+static void send_cell_key(int cell)
+/* Send the key event for the cell */
+{
+        int i;
+
+        if (!cells[cell].ch)
+                return;
+
+        /* Collect stats and train on corrections */
+        if (cells[cell].ch != ' ') {
+                if (cells[cell].ch != cells[cell].sample.ch)
+                        corrections++;
+                if (train_on_input && !(cells[cell].flags & CELL_SHIFTED) &&
+                    cells[cell].sample.len) {
+                        cells[cell].sample.ch = cells[cell].ch;
+                        train_sample(&cells[cell].sample, FALSE);
+                }
+                characters++;
+        }
+
+        /* Update the usage time for the sample that matched this character */
+        for (i = 0; i < ALTERNATES && cells[cell].alts[i]; i++) {
+                if (!sample_valid(cells[cell].alts[i], cells[cell].alt_used[i]))
+                        break;
+                if (cells[cell].alts[i]->ch == cells[cell].ch) {
+                        promote_sample(cells[cell].alts[i]);
+                        break;
+                }
+                demote_sample(cells[cell].alts[i]);
+        }
+
+        key_event_send_char(cells[cell].ch);
+}
+
+/*
+        Events
+*/
+
+/* Hold click area radius */
+#define HOLD_CLICK_WIDTH 3.
+
+/* Mask for possible buttons used by the eraser */
+#define ERASER_BUTTON_MASK (GDK_MOD5_MASK | GDK_BUTTON1_MASK | \
+                            GDK_BUTTON2_MASK | GDK_BUTTON3_MASK | \
+                            GDK_BUTTON4_MASK | GDK_BUTTON5_MASK)
+
+static int menu_cell, alt_menu_alts[ALTERNATES];
+
+static void training_menu_reset(void)
+{
+        untrain_char(cells[menu_cell].ch);
+        render_cell(menu_cell);
+}
+
+static void alt_menu_selection_done(GtkWidget *widget)
+{
+        gtk_widget_destroy(widget);
+}
+
+static void alt_menu_activate(GtkWidget *widget, int *alt)
+{
+        cells[menu_cell].ch = *alt;
+        cells[menu_cell].flags |= CELL_VERIFIED;
+        cells[menu_cell].flags &= ~CELL_SHIFTED;
+        render_cell(menu_cell);
+}
+
+static void alt_menu_delete(void)
+{
+        delete_cell(menu_cell);
+}
+
+static void alt_menu_show_ink(void)
+{
+        cells[menu_cell].flags ^= CELL_SHOW_INK;
+        render_cell(menu_cell);
+}
+
+static void alt_menu_change_case(void)
+{
+        if (g_unichar_isupper(cells[menu_cell].ch)) {
+                cells[menu_cell].ch = g_unichar_tolower(cells[menu_cell].ch);
+                cells[menu_cell].flags |= CELL_SHIFTED;
+                render_cell(menu_cell);
+        } else if (g_unichar_islower(cells[menu_cell].ch)) {
+                cells[menu_cell].ch = g_unichar_toupper(cells[menu_cell].ch);
+                cells[menu_cell].flags |= CELL_SHIFTED;
+                render_cell(menu_cell);
+        } else
+                g_debug("Cannot change case, not an alphabetic character");
+}
+
+static gboolean scrollbar_scroll_event(GtkWidget *widget, GdkEventScroll *event)
+{
+        check_cell(event->x, event->y, event->device);
+        return FALSE;
+}
+
+static gboolean scroll_event(GtkWidget *widget, GdkEventScroll *event)
+{
+        if (scrollbar && GTK_WIDGET_VISIBLE(scrollbar))
+                gtk_widget_event(scrollbar, (GdkEvent*)event);
+        return FALSE;
+}
+
+static void context_menu_position(GtkMenu *menu, gint *x, gint *y,
+                                  gboolean *push_in)
+/* Positions the two-column context menu so that the column divide is at
+   the cursor rather than the upper left hand point */
+{
+        if (cells[menu_cell].alts[0])
+                *x -= GTK_WIDGET(menu)->requisition.width / 2;
+        *push_in = TRUE;
+}
+
+static void show_context_menu(int button, int time)
+/* Popup the cell context menu for the current cell */
+{
+        GtkWidget *menu, *widget;
+        int i, pos;
+
+        /* Training menu is the same for all cells */
+        if (training) {
+                if (!char_trained(cells[current_cell].ch))
+                        return;
+                menu_cell = current_cell;
+                gtk_menu_popup(GTK_MENU(training_menu), 0, 0, 0, 0,
+                               button, time);
+                return;
+        }
+
+        /* Can't delete blanks */
+        if (!cells[current_cell].ch)
+                return;
+
+        /* Construct an alternates menu for the current button */
+        menu = gtk_menu_new();
+        menu_cell = current_cell;
+
+        /* Menu -> Delete */
+        widget = gtk_menu_item_new_with_label("Delete");
+        g_signal_connect(G_OBJECT(widget), "activate",
+                         G_CALLBACK(alt_menu_delete), NULL);
+        gtk_menu_attach(GTK_MENU(menu), widget, 0, 1, 0, 1);
+
+        /* Menu -> Show Ink */
+        if (cells[menu_cell].sample.ch) {
+                const char *label;
+
+                label = cells[menu_cell].flags & CELL_SHOW_INK ?
+                        "Hide ink" : "Show ink";
+                widget = gtk_menu_item_new_with_label(label);
+                g_signal_connect(G_OBJECT(widget), "activate",
+                                 G_CALLBACK(alt_menu_show_ink), NULL);
+                gtk_menu_attach(GTK_MENU(menu), widget, 0, 1, 1, 2);
+        }
+
+        /* Menu -> Change case */
+        if (g_unichar_isupper(cells[menu_cell].ch) ||
+                g_unichar_islower(cells[menu_cell].ch)) {
+                const char *string = "To upper";
+
+                if (g_unichar_isupper(cells[menu_cell].ch))
+                        string = "To lower";
+                widget = gtk_menu_item_new_with_label(string);
+                g_signal_connect(G_OBJECT(widget), "activate",
+                                 G_CALLBACK(alt_menu_change_case), NULL);
+                gtk_menu_attach(GTK_MENU(menu), widget, 0, 1, 2, 3);
+        }
+
+        /* Menu -> Alternates */
+        for (i = 0, pos = 0; i < ALTERNATES &&
+                cells[current_cell].alts[i]; i++) {
+                char *str;
+
+                if (!sample_valid(cells[current_cell].alts[i],
+                                  cells[current_cell].alt_used[i]))
+                        continue;
+                str = va("%C\t%d%%", cells[current_cell].alts[i]->ch,
+                         cells[current_cell].alt_ratings[i]);
+                alt_menu_alts[i] = cells[current_cell].alts[i]->ch;
+                widget = gtk_check_menu_item_new_with_label(str);
+                if (cells[current_cell].ch == cells[current_cell].alts[i]->ch)
+                        gtk_check_menu_item_set_active(
+                                             GTK_CHECK_MENU_ITEM(widget), TRUE);
+                g_signal_connect(G_OBJECT(widget), "activate",
+                                 G_CALLBACK(alt_menu_activate),
+                                 alt_menu_alts + i);
+                gtk_menu_attach(GTK_MENU(menu), widget, 1, 2, pos, pos + 1);
+                pos++;
+        }
+        g_signal_connect(G_OBJECT(menu), "selection-done",
+                         G_CALLBACK(alt_menu_selection_done), NULL);
+        gtk_widget_show_all(menu);
+        gtk_menu_popup(GTK_MENU(menu), 0, 0,
+                       (GtkMenuPositionFunc)context_menu_position,
+                       0, button, time);
+
+}
+
+static gboolean button_press_event(GtkWidget *widget, GdkEventButton *event)
+/* Mouse button is pressed over drawing area */
+{
+        /* Don't process double clicks */
+        if (event->type != GDK_BUTTON_PRESS)
+                return TRUE;
+
+        /* Check validity every time */
+        check_cell(event->x, event->y, event->device);
+        if (invalid)
+                return TRUE;
+
+        /* If we are drawing and we get a button press event it is possible
+           that we never received a button release event for some reason.
+           This is a fix for Zaurus drawing connected lines. */
+        if (drawing)
+                stop_drawing();
+
+        /* If we have pressed with the eraser, erase the cell */
+        if (eraser || event->button == 2) {
+                erase_cell(current_cell);
+                return TRUE;
+        }
+
+        /* Draw/activate insert with left click */
+        if (event->button == 1) {
+                if (inserting)
+                        potential_insert = TRUE;
+                else if (cells[current_cell].ch) {
+                        start_hold();
+                } else
+                        draw(event->x, event->y);
+
+                /* We are now counting on getting valid coordinates here so
+                   save in case we are doing a potential insert/hold and we
+                   don't get a motion event in between */
+                cursor_x = event->x;
+                cursor_y = event->y;
+
+                return TRUE;
+        }
+
+        /* Right-click opens context menu */
+        else if (event->button == 3 && current_cell >= 0 && !inserting &&
+                 (!input || !input->len)) {
+                show_context_menu(event->button, event->time);
+                return TRUE;
+        }
+
+        return FALSE;
+}
+
+static gboolean button_release_event(GtkWidget *widget, GdkEventButton *event)
+/* Mouse button is released over drawing area */
+{
+        /* Only handle left-clicks */
+        if (event->button != 1)
+                return TRUE;
+
+        /* Complete an insertion */
+        if (potential_insert && inserting) {
+                insert_cell(current_cell);
+                potential_insert = FALSE;
+                return TRUE;
+        }
+
+        /* Cancel a hold-click */
+        if (potential_hold) {
+                potential_hold = FALSE;
+                draw(cursor_x, cursor_y);
+        }
+
+        stop_drawing();
+        return TRUE;
+}
+
+static gboolean motion_notify_event(GtkWidget *widget, GdkEventMotion *event)
+/* Mouse is moved over drawing area */
+{
+        GdkModifierType state;
+        double x, y;
+
+        /* Fetch event coordinates */
+        x = event->x;
+        y = event->y;
+        if (xinput_enabled) {
+                gdk_device_get_state(event->device, event->window, NULL,
+                                     &state);
+                gdk_event_get_coords((GdkEvent*)event, &x, &y);
+        }
+
+#if GTK_CHECK_VERSION(2, 12, 0)
+        /* Process a hint event (GTK >= 2.12) */
+        gdk_event_request_motions(event);
+#else
+        /* Process a hint event (GTK <= 2.10) */
+        else if (event->is_hint) {
+                int nx, ny;
+
+                gdk_window_get_pointer(event->window, &nx, &ny, &state);
+                x = nx;
+                y = ny;
+        }
+#endif
+
+        /* If we are getting invalid output from this device with XInput
+           enabled, try disabling it */
+        if ((x < 0 || x > drawing_area->allocation.width ||
+             y < 0 || y > drawing_area->allocation.width) &&
+            event->device->mode != GDK_MODE_DISABLED && xinput_enabled) {
+                g_warning("Extended input device is generating invalid "
+                          "coordinates, disabled");
+                gdk_device_set_mode(event->device, GDK_MODE_DISABLED);
+                return TRUE;
+        }
+
+        /* Check where the pointer is */
+        check_cell(x, y, event->device);
+
+        /* Cancel a potential insert */
+        if (potential_insert) {
+                if (!inserting) {
+                        potential_insert = FALSE;
+                        draw(cursor_x, cursor_y);
+                } else
+                        return TRUE;
+        }
+
+        /* Cancel a potential hold-click */
+        if (potential_hold) {
+                double dx, dy;
+
+                dx = x - cursor_x;
+                dy = y - cursor_y;
+                if (dx < -HOLD_CLICK_WIDTH || dx > HOLD_CLICK_WIDTH ||
+                    dy < -HOLD_CLICK_WIDTH || dy > HOLD_CLICK_WIDTH) {
+                        potential_hold = FALSE;
+                        draw(cursor_x, cursor_y);
+                } else
+                        return TRUE;
+        }
+
+        cursor_x = x;
+        cursor_y = y;
+
+        /* Record and draw new segment */
+        if (drawing) {
+                draw(cursor_x, cursor_y);
+                render_segment(input, current_cell, input->len - 1,
+                               input->strokes[input->len - 1]->len - 2, NULL);
+        }
+
+        /* Erasing with the eraser. We get MOD5 rather than a button for the
+           eraser being pressed on a Tablet PC. */
+        else if (!invalid &&
+                 (cross_out || (eraser && (state & ERASER_BUTTON_MASK))))
+                erase_cell(current_cell);
+
+        /* Plain motion restarts the finish countdown */
+        start_timeout();
+
+        return TRUE;
+}
+
+static void configure_keys(void)
+{
+}
+
+static gboolean configure_event(void)
+/* Create a new backing pixmap of the appropriate size */
+{
+        int new_cols;
+
+        /* Do nothing if we are not visible */
+        if (!drawing_area || !drawing_area->window ||
+            !GTK_WIDGET_VISIBLE(drawing_area))
+                return TRUE;
+
+        /* Backing pixmap */
+        if (pixmap) {
+                int old_width, old_height;
+
+                //return TRUE;
+
+                g_object_unref(pixmap);
+        }
+        pixmap = gdk_pixmap_new(drawing_area->window,
+                                drawing_area->allocation.width,
+                                drawing_area->allocation.height, -1);
+        trace("%dx%d", drawing_area->allocation.width,
+              drawing_area->allocation.height);
+
+        /* GDK graphics context */
+        if (pixmap_gc)
+                g_object_unref(pixmap_gc);
+        pixmap_gc = gdk_gc_new(GDK_DRAWABLE(pixmap));
+
+        /* Cairo context */
+        if (cairo)
+                cairo_destroy(cairo);
+        cairo = gdk_cairo_create(GDK_DRAWABLE(pixmap));
+
+        /* Set font size */
+        pango_font_description_set_absolute_size(pango_font_desc, PANGO_SCALE *
+                                                 (cell_height -
+                                                  CELL_BASELINE - 2));
+
+        /* Get the background color */
+        color_bg = window->style->bg[0];
+        color_bg_dark = window->style->bg[1];
+
+        /* Cursor */
+        cell_widget_set_cursor(TRUE);
+
+        /* If the cell dimensions changed, repack */
+        if (window_embedded) {
+                new_cols = (drawing_area->allocation.width -
+                            cell_widget_scrollbar_width() - 6) / cell_width;
+                if (new_cols != cell_cols)
+                        pack_cells(1, new_cols);
+        }
+
+        /* If we are embedded we won't be able to resize the window so we
+           can't honor the maximum rows preference */
+        if (window_embedded)
+                cell_rows_pref = drawing_area->allocation.height / cell_height;
+
+        /* Update the key widget with new values */
+        configure_keys();
+
+        /* Render the cells */
+        cell_widget_render();
+
+        return TRUE;
+}
+
+static gboolean expose_event(GtkWidget *widget, GdkEventExpose *event)
+/* Redraw the drawing area from the backing pixmap */
+{
+        if (!pixmap)
+                return FALSE;
+        gdk_draw_drawable(widget->window,
+                          widget->style->fg_gc[GTK_WIDGET_STATE(widget)],
+                          pixmap, event->area.x, event->area.y, event->area.x,
+                          event->area.y, event->area.width, event->area.height);
+        return FALSE;
+}
+
+static gboolean enter_notify_event(GtkWidget *widget, GdkEventCrossing *event)
+{
+        check_cell(event->x, event->y, NULL);
+        return FALSE;
+}
+
+static gboolean leave_notify_event(GtkWidget *widget, GdkEventCrossing *event)
+{
+        /* Tablet PC gets grab leave-notify event when starting to draw.
+           Ignore this if we are still drawing. */
+        if (event->mode == GDK_CROSSING_GRAB || drawing || cross_out)
+                return FALSE;
+
+        old_cc = current_cell;
+        current_cell = -1;
+        finish_cell(old_cc);
+        if (inserting) {
+                inserting = FALSE;
+                dirty_cell(old_cc);
+                dirty_cell(old_cc - 1);
+        }
+        invalid = TRUE;
+        cell_widget_set_cursor(FALSE);
+        render_dirty();
+        start_timeout();
+        return FALSE;
+}
+
+static void scrollbar_value_changed(void)
+/* The cell widget has been scrolled */
+{
+        double value;
+
+        value = gtk_range_get_value(GTK_RANGE(scrollbar));
+        if ((int)value == cell_row_view)
+                return;
+        cell_row_view = value;
+        cell_widget_render();
+}
+
+/*
+        Widget
+*/
+
+void cell_widget_enable_xinput(int on)
+/* Enable Xinput devices. We set everything to screen mode despite the fact
+   that we actually want window coordinates. Window mode just seems to break
+   everything and we get window coords with screen mode anyway! */
+{
+        GList *list;
+        GdkDevice *device;
+        int i, mode;
+
+        gtk_widget_set_extension_events(drawing_area,
+                                        on ? GDK_EXTENSION_EVENTS_ALL :
+                                             GDK_EXTENSION_EVENTS_NONE);
+        mode = on ? GDK_MODE_SCREEN : GDK_MODE_DISABLED;
+        list = gdk_devices_list();
+        for (i = 0; (device = (GdkDevice*)g_list_nth_data(list, i)); i++)
+                gdk_device_set_mode(device, mode);
+        xinput_enabled = on;
+        g_debug(on ? "Xinput events enabled" : "Xinput events disabled");
+}
+
+int cell_widget_update_colors(void)
+{
+        GdkColor old_active, old_inactive, old_ink, old_select;
+
+        old_active = color_active;
+        old_inactive = color_inactive;
+        old_ink = color_ink;
+        old_select = color_select;
+        color_active = custom_active_color;
+        color_inactive = custom_inactive_color;
+        color_ink = custom_ink_color;
+        color_select = custom_select_color;
+        if (style_colors) {
+                color_active = window->style->base[0];
+                color_ink = window->style->text[0];
+                color_inactive = window->style->bg[1];
+        }
+        return !gdk_colors_equal(&old_active, &color_active) ||
+               !gdk_colors_equal(&old_inactive, &color_inactive) ||
+               !gdk_colors_equal(&old_ink, &color_ink) ||
+               !gdk_colors_equal(&old_select, &color_select);
+}
+
+const char *cell_widget_word(void)
+/* Return the current word and the current cell's position in that word
+   FIXME this function ignores wide chars */
+{
+        static char buf[64];
+        int i, min, max;
+
+        memset(buf, 0, sizeof (buf));
+        if (cell_offscreen(old_cc))
+                return buf;
+
+        /* Find the start of the word */
+        for (min = old_cc - 1; min >= 0 && cells[min].ch &&
+             g_ascii_isalnum(cells[min].ch) && cells[min].ch < 0x7f; min--);
+
+        /* Find the end of the word */
+        for (max = old_cc + 1; max < cell_rows * cell_cols && cells[max].ch &&
+             g_ascii_isalnum(cells[max].ch) && cells[max].ch < 0x7f; max++);
+
+        /* Copy the word to a buffer */
+        for (++min, i = 0; i < max - min && i < (int)sizeof (buf) - 1; i++)
+                buf[i] = cells[min + i].ch;
+        buf[old_cc - min] = 0;
+        buf[i] = 0;
+
+        return buf;
+}
+
+void cell_widget_clear(void)
+{
+        int resized;
+
+        stop_timeout();
+        free_cells();
+
+        /* Restore cells if we just finished training */
+        if (training) {
+                cells = cells_saved;
+                cell_rows = cell_rows_saved;
+                cell_cols = cell_cols_saved;
+                cell_row_view = cell_row_view_saved;
+                training = FALSE;
+                resized = pack_cells(cell_rows, cell_cols);
+
+                /* Show the on-screen keyboard */
+                if (check_clear()) {
+                        show_keys = keyboard_enabled;
+                        is_clear = TRUE;
+                }
+        }
+
+        /* Clear cells otherwise */
+        else {
+                resized = pack_cells(1, cell_cols);
+
+                /* Show the on-screen keyboard */
+                show_keys = keyboard_enabled;
+                is_clear = TRUE;
+        }
+
+        /* Only re-render when we aren't going to get a configure event */
+        if (!resized)
+                cell_widget_render();
+}
+
+void cell_widget_train(void)
+{
+        UnicodeBlock *block;
+        int i, pos, range;
+
+        stop_timeout();
+
+        /* Save cells */
+        if (!training) {
+                cells_saved = cells;
+                cell_rows_saved = cell_rows;
+                cell_cols_saved = cell_cols;
+                cell_row_view_saved = cell_row_view;
+                cells = NULL;
+                cell_row_view = 0;
+        }
+
+        /* Clear if not training any block */
+        if (training_block < 0) {
+                free_cells();
+                pack_cells(1, cell_cols);
+                cell_widget_render();
+                return;
+        }
+
+        /* Pack the Unicode block's characters into the cell grid */
+        block = unicode_blocks + training_block;
+        range = block->end - block->start + 1;
+        training = TRUE;
+        pack_cells((range + cell_cols - 1) / cell_cols, cell_cols);
+
+        /* Preset all of the characters for training */
+        for (i = 0, pos = 0; i < range; i++) {
+                short ch;
+
+                ch = block->start + i;
+                if (char_disabled(ch))
+                        continue;
+                cells[pos].ch = ch;
+                cells[pos].alts[0] = NULL;
+                cells[pos++].flags = 0;
+        }
+        range = pos;
+        for (; pos < cell_rows * cell_cols; pos++)
+                clear_cell(pos);
+        pack_cells(1, cell_cols);
+
+        unclear(FALSE);
+        cell_widget_render();
+}
+
+void cell_widget_load_string(const gchar *str){
+
+  int range = strlen(str);
+  int i;
+
+        pack_cells((range + cell_cols - 1) / cell_cols, cell_cols);
+
+        /* Preset all of the characters for training */
+        for (i = 0; i < range; i++) {
+          cells[i].ch = str[i]; // todo: support utf-8
+          cells[i].alts[0] = NULL;
+          cells[i].flags = 0;
+        }
+        for (; i < cell_rows * cell_cols; i++)
+                clear_cell(i);
+        pack_cells(1, cell_cols);
+
+        cell_widget_render();
+
+}
+
+void cell_widget_pack(void)
+{
+        int cols;
+
+        if (training) {
+                cell_widget_train();
+                return;
+        }
+        cols = cell_cols_pref;
+        if (window_docked) {
+                GdkScreen *screen;
+
+                screen = gtk_window_get_screen(GTK_WINDOW(window));
+                cols = (gdk_screen_get_width(screen) -
+                        cell_widget_scrollbar_width() - 6) / cell_width;
+        }
+        if (!pack_cells(0, cols))
+                set_size_request(TRUE);
+        if (is_clear)
+                show_keys = keyboard_enabled;
+
+        /* Right-to-left mode may have changed so we need to reconfigure the
+           on-screen keyboard */
+        configure_keys();
+
+        cell_widget_render();
+        trace("%dx%d, scrollbar %d",
+               cell_cols, cell_rows, cell_widget_scrollbar_width());
+}
+
+
+
+int cell_widget_insert(void)
+{
+        gunichar2 *utf16;
+        int i, j, slot, chars;
+
+        if (training)
+                return FALSE;
+        chars = 0;
+
+        /* Prepare for sending key events */
+        //key_event_update_mappings();
+
+        /* Need to send the keys out in reverse order for right_to_left mode
+           because the cells are displayed with columns reversed */
+        if (right_to_left)
+                for (i = cell_cols - 1; i < cell_rows * cell_cols; i--) {
+                        if (cells[i].ch) {
+                                chars++;
+                                send_cell_key(i);
+                        }
+                        if (i % cell_cols == 0)
+                                i += cell_cols * 2;
+                }
+
+        else
+                for (i = 0; i < cell_rows * cell_cols; i++) {
+                        if (!cells[i].ch)
+                                continue;
+                        chars++;
+                        send_cell_key(i);
+                }
+
+        /* If nothing was entered, send Enter key event */
+        if (!chars) {
+                key_event_send_enter();
+                return FALSE;
+        }
+
+        /* Create a UTF-16 string representation */
+        utf16 = g_malloc(sizeof (**history) * (chars + 1));
+        for (i = 0, j = 0; i < cell_rows * cell_cols; i++)
+                if (cells[i].ch)
+                        utf16[j++] = cells[i].ch;
+        utf16[j] = 0;
+
+        /* If this text has been entered before, consume that history slot */
+        slot = HISTORY_MAX - 1;
+        for (i = 0; i < slot && history[i]; i++)
+                for (j = 0; history[i][j] == utf16[j]; j++)
+                        if (!utf16[j]) {
+                                slot = i;
+                                break;
+                        }
+
+        /* Save entered text to history */
+        g_free(history[slot]);
+        memmove(history + 1, history, sizeof (*history) * slot);
+        history[0] = utf16;
+
+        cell_widget_clear();
+        return TRUE;
+}
+
+static void buffer_menu_deactivate(GtkMenuShell *shell, GtkWidget *button)
+{
+        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), FALSE);
+}
+
+static void buffer_menu_item_activate(GtkWidget *widget, gunichar2 *history)
+{
+        int i;
+
+        stop_timeout();
+        free_cells();
+        for (i = 0; history[i]; i++);
+        cell_rows = i / cell_cols + 1;
+        cell_cols = cell_cols;
+        cells = g_malloc0(sizeof (*cells) * cell_cols * cell_rows);
+        for (i = 0; history[i]; i++)
+                cells[i].ch = history[i];
+        pack_cells(cell_rows, cell_cols);
+        unclear(TRUE);
+}
+
+static void buffer_menu_item_destroy(GtkWidget *widget, gchar *string)
+{
+        g_free(string);
+}
+
+static void buffer_menu_position_func(GtkMenu *menu, gint *x, gint *y,
+                                      gboolean *push_in, GtkWidget *button)
+{
+        gdk_window_get_origin(button->window, x, y);
+        *x += button->allocation.x + button->allocation.width -
+              GTK_WIDGET(menu)->requisition.width;
+        *y += button->allocation.y + button->allocation.height;
+        *push_in = TRUE;
+}
+
+void cell_widget_show_buffer(GtkWidget *button)
+/* Show input back buffer menu */
+{
+        static GtkWidget *menu;
+        int i;
+
+        if (menu)
+                gtk_widget_destroy(GTK_WIDGET(menu));
+        menu = gtk_menu_new();
+        g_signal_connect(G_OBJECT(menu), "deactivate",
+                         G_CALLBACK(buffer_menu_deactivate), button);
+        for (i = 0; history[i] && i < HISTORY_MAX; i++) {
+                GtkWidget *item;
+                GError *error = NULL;
+                gchar *string;
+
+                /* Convert string from a UTF-16 array to displayable UTF-8 */
+                string = g_utf16_to_utf8(history[i], -1, NULL, NULL, &error);
+                if (error) {
+                        g_warning("g_utf16_to_utf8(): %s", error->message);
+                        continue;
+                }
+
+                /* Reverse the displayed string for right-to-left mode */
+                if (right_to_left) {
+                        gchar *reversed;
+
+                        reversed = g_utf8_strreverse(string, -1);
+                        g_free(string);
+                        string = reversed;
+                }
+
+                /* Create menu item */
+                item = gtk_menu_item_new_with_label(string);
+                g_signal_connect(G_OBJECT(item), "destroy",
+                                 G_CALLBACK(buffer_menu_item_destroy), string);
+                g_signal_connect(G_OBJECT(item), "activate",
+                                 G_CALLBACK(buffer_menu_item_activate),
+                                 history[i]);
+                gtk_menu_attach(GTK_MENU(menu), item, 0, 1, i, i + 1);
+        }
+
+        /* Show back buffer menu */
+        gtk_widget_show_all(menu);
+        gtk_menu_popup(GTK_MENU(menu), NULL, NULL,
+                       (GtkMenuPositionFunc)buffer_menu_position_func,
+                       button, 0, gtk_get_current_event_time());
+}
+
+int cell_widget_scrollbar_width(void)
+/* Gets the width of the scrollbar even if it is hidden */
+{
+        GtkRequisition requisition;
+
+        if (scrollbar->requisition.width <= 1) {
+                gtk_widget_size_request(scrollbar, &requisition);
+                return requisition.width;
+        }
+        return scrollbar->requisition.width + 4;
+}
+
+int cell_widget_get_height(void)
+{
+        int rows;
+
+        rows = cell_rows > cell_rows_pref ? cell_rows_pref : cell_rows;
+        return rows * cell_height + 2;
+}
+
+GtkWidget *cell_widget_new(void)
+/* Creates the Cell widget. Should only be called once per program run! */
+{
+        PangoCairoFontMap *font_map;
+        GtkWidget *widget, *hbox;
+
+        /* Initial settings */
+        cell_cols = cell_cols_pref;
+
+        /* Create drawing area */
+        drawing_area = gtk_drawing_area_new();
+        g_signal_connect(G_OBJECT(drawing_area), "expose_event",
+                         G_CALLBACK(expose_event), NULL);
+        g_signal_connect(G_OBJECT(drawing_area), "configure_event",
+                         G_CALLBACK(configure_event), NULL);
+        g_signal_connect(G_OBJECT(drawing_area), "show",
+                         G_CALLBACK(configure_event), NULL);
+        g_signal_connect(G_OBJECT(drawing_area), "button_press_event",
+                         G_CALLBACK(button_press_event), NULL);
+        g_signal_connect(G_OBJECT(drawing_area), "button_release_event",
+                         G_CALLBACK(button_release_event), NULL);
+        g_signal_connect(G_OBJECT(drawing_area), "motion_notify_event",
+                         G_CALLBACK(motion_notify_event), NULL);
+        g_signal_connect(G_OBJECT(drawing_area), "enter_notify_event",
+                         G_CALLBACK(enter_notify_event), NULL);
+        g_signal_connect(G_OBJECT(drawing_area), "leave_notify_event",
+                         G_CALLBACK(leave_notify_event), NULL);
+        g_signal_connect(G_OBJECT(drawing_area), "scroll_event",
+                         G_CALLBACK(scroll_event), NULL);
+        g_signal_connect(G_OBJECT(drawing_area), "style-set",
+                         G_CALLBACK(cell_widget_update_colors), NULL);
+        gtk_widget_set_events(drawing_area,
+                              GDK_EXPOSURE_MASK |
+                              GDK_BUTTON_PRESS_MASK |
+                              GDK_BUTTON_RELEASE_MASK |
+                              GDK_POINTER_MOTION_MASK |
+                              GDK_POINTER_MOTION_HINT_MASK |
+                              GDK_ENTER_NOTIFY_MASK |
+                              GDK_LEAVE_NOTIFY_MASK |
+                              GDK_SCROLL_MASK);
+
+        /* Update colors */
+        cell_widget_update_colors();
+
+        /* Create training menu */
+        training_menu = gtk_menu_new();
+        widget = gtk_menu_item_new_with_label("Reset");
+        g_signal_connect(G_OBJECT(widget), "activate",
+                         G_CALLBACK(training_menu_reset), NULL);
+        gtk_menu_attach(GTK_MENU(training_menu), widget, 0, 1, 0, 1);
+        gtk_widget_show_all(training_menu);
+
+        /* Create scroll bar */
+        scrollbar = gtk_vscrollbar_new(NULL);
+        gtk_widget_set_no_show_all(scrollbar, TRUE);
+        g_signal_connect(G_OBJECT(scrollbar), "value-changed",
+                         G_CALLBACK(scrollbar_value_changed), NULL);
+        g_signal_connect(G_OBJECT(scrollbar), "scroll_event",
+                         G_CALLBACK(scrollbar_scroll_event), NULL);
+        gtk_widget_add_events(drawing_area, GDK_SCROLL_MASK);
+
+        /* Box container */
+        hbox = gtk_hbox_new(FALSE, 0);
+        gtk_box_pack_start(GTK_BOX(hbox), drawing_area, TRUE, TRUE, 2);
+        gtk_box_pack_start(GTK_BOX(hbox), scrollbar, FALSE, FALSE, 2);
+
+        /* Create Pango font description
+           FIXME font characteristics, not family */
+        pango_font_desc = pango_font_description_new();
+        pango_font_description_set_family(pango_font_desc, "Monospace");
+
+        /* Pango context */
+        font_map = PANGO_CAIRO_FONT_MAP(pango_cairo_font_map_new());
+        pango = pango_cairo_font_map_create_context(font_map);
+        g_object_unref(font_map);
+
+        /* Clear cells */
+        cell_widget_clear();
+
+        /* Set Xinput state */
+        cell_widget_enable_xinput(xinput_enabled);
+
+        /* Clear history */
+        memset(history, 0, sizeof (history));
+
+        return hbox;
+}
+
+void cell_widget_cleanup(void)
+{
+        /* Freeing memory when closing is important when trying to sort
+           legitimate memory leaks from left-over memory */
+        if (pixmap)
+                g_object_unref(pixmap);
+        if (pixmap_gc)
+                g_object_unref(pixmap_gc);
+        if (cairo)
+                cairo_destroy(cairo);
+        if (pango)
+                g_object_unref(pango);
+}
+
+extern HildonIMUI *ui;
+
+void unicode_to_utf8(unsigned int code, char *out);
+void cell_widget_insert_surrounding_string(){
+  int i;
+
+  gchar *str = malloc(cell_cols * cell_rows * 2 + 1);
+  gchar *s = str;
+  *s = 0;
+  for(i = 0; i < cell_cols * cell_rows; i++){
+    if(!(cells[i].flags & CELL_DIRTY)){
+      unicode_to_utf8(cells[i].ch, s);
+      s += strlen(s);
+    }
+  }
+
+  hildon_im_ui_send_surrounding_content(ui, str);
+}