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