4 cellwriter -- a character recognition input method
5 Copyright (C) 2007 Michael Levin <risujin@risujin.org>
7 This program is free software; you can redistribute it and/or
8 modify it under the terms of the GNU General Public License
9 as published by the Free Software Foundation; either version 2
10 of the License, or (at your option) any later version.
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
17 You should have received a copy of the GNU General Public License
18 along with this program; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
25 #include "recognize.h"
30 #include "hildon-im-ui.h"
33 void smooth_stroke(Stroke *s);
34 void simplify_stroke(Stroke *s);
37 int cell_widget_scrollbar_width(void);
38 static void start_timeout(void);
39 static void show_context_menu(int button, int time);
40 static void stop_drawing(void);
47 #define CELL_BASELINE (cell_height / 3)
48 #define CELL_BORDER (cell_height / 12)
50 /* Msec of no mouse motion before a cell is finished */
51 #define MOTION_TIMEOUT 500
54 #define CELL_SHOW_INK 0x01
55 #define CELL_DIRTY 0x02
56 #define CELL_VERIFIED 0x04
57 #define CELL_SHIFTED 0x08
60 Sample sample, *alts[ALTERNATES];
62 int alt_used[ALTERNATES];
63 char flags, alt_ratings[ALTERNATES];
66 /* Cell preferences */
67 int cell_width = 40, cell_height = 70, cell_cols_pref = 12, cell_rows_pref = 4,
68 enable_cairo = TRUE, training = FALSE, train_on_input = TRUE,
69 right_to_left = FALSE, keyboard_enabled = TRUE, xinput_enabled = FALSE;
72 int corrections = 0, rewrites = 0, characters = 0, inputs = 0;
75 GdkColor custom_active_color = RGB_TO_GDKCOLOR(255, 255, 255),
76 custom_inactive_color = RGB_TO_GDKCOLOR(212, 222, 226),
77 custom_ink_color = RGB_TO_GDKCOLOR(0, 0, 0),
78 custom_select_color = RGB_TO_GDKCOLOR(204, 0, 0);
79 static GdkColor color_active, color_inactive, color_ink, color_select;
81 static Cell *cells = NULL, *cells_saved = NULL;
82 static GtkWidget *drawing_area = NULL, *training_menu, *scrollbar;
83 static GdkPixmap *pixmap = NULL;
84 static GdkGC *pixmap_gc = NULL;
85 static GdkColor color_bg, color_bg_dark;
86 static cairo_t *cairo = NULL;
87 static PangoContext *pango = NULL;
88 static PangoFontDescription *pango_font_desc = NULL;
89 static gunichar2 *history[HISTORY_MAX];
90 static int cell_cols, cell_rows, cell_row_view = 0, current_cell = -1, old_cc,
91 cell_cols_saved, cell_rows_saved, cell_row_view_saved,
93 drawing = FALSE, inserting = FALSE, eraser = FALSE, invalid = FALSE,
94 potential_insert = FALSE, potential_hold = FALSE, cross_out = FALSE,
95 show_keys = TRUE, is_clear = TRUE, keys_dirty = FALSE;
96 static double cursor_x, cursor_y;
98 static void cell_coords(int cell, int *px, int *py)
99 /* Get the int position of a cell from its index */
103 cell -= cell_row_view * cell_cols;
104 cell_y = cell / cell_cols;
105 cell_x = cell - cell_y * cell_cols;
106 *px = (!right_to_left ? cell_x * cell_width :
107 (cell_cols - cell_x - 1) * cell_width) + 1;
108 *py = cell_y * cell_height + 1;
111 static void set_pen_color(Sample *sample, int cell)
112 /* Selects the pen color depending on if the sample being drawn is the input
113 or the template sample */
115 if (sample == input || sample == &cells[cell].sample)
116 cairo_set_source_gdk_color(cairo, &color_ink, 1.);
118 cairo_set_source_gdk_color(cairo, &color_select, 1.);
121 static void render_point(Sample *sample, int cell, int stroke, Vec2 *offset)
122 /* Draw a single point stroke */
127 if (!pixmap || stroke < 0 || !sample || stroke >= sample->len ||
128 sample->strokes[stroke]->len < 1)
132 x = sample->strokes[stroke]->points[0].x;
133 y = sample->strokes[stroke]->points[0].y;
139 /* Unscale coordinates */
140 cell_coords(cell, &cx, &cy);
141 x = cx + cell_width / 2 + x * cell_height / SCALE;
142 y = cy + cell_height / 2 + y * cell_height / SCALE;
144 /* Draw a dot with cairo */
145 cairo_new_path(cairo);
146 radius = cell_height / 33.;
147 cairo_arc(cairo, x, y, radius > 1. ? radius : 1., 0., 2 * M_PI);
148 set_pen_color(sample, cell);
151 gtk_widget_queue_draw_area(drawing_area, x - radius - 0.5,
152 y - radius - 0.5, radius * 2 + 0.5,
156 static void render_segment(Sample *sample, int cell, int stroke, int seg,
158 /* Draw a segment of the stroke
159 FIXME since the segments are not properly connected according to Cairo,
160 there is a bit of missing value at the segment connection points */
162 double pen_width, x1, x2, y1, y2;
163 int xmin, xmax, ymin, ymax, cx, cy, pen_range;
165 if (!cairo || stroke < 0 || !sample || stroke >= sample->len ||
166 seg < 0 || seg >= sample->strokes[stroke]->len - 1)
169 x1 = sample->strokes[stroke]->points[seg].x;
170 x2 = sample->strokes[stroke]->points[seg + 1].x;
171 y1 = sample->strokes[stroke]->points[seg].y;
172 y2 = sample->strokes[stroke]->points[seg + 1].y;
182 /* Unscale coordinates */
183 cell_coords(cell, &cx, &cy);
184 x1 = cx + cell_width / 2 + x1 * cell_height / SCALE;
185 x2 = cx + cell_width / 2 + x2 * cell_height / SCALE;
186 y1 = cy + cell_height / 2 + y1 * cell_height / SCALE;
187 y2 = cy + cell_height / 2 + y2 * cell_height / SCALE;
189 /* Find minimum and maximum x and y */
205 /* Draw the new segment using Cairo */
206 cairo_new_path(cairo);
207 cairo_move_to(cairo, x1, y1);
208 cairo_line_to(cairo, x2, y2);
209 set_pen_color(sample, cell);
210 pen_width = cell_height / 33.;
213 cairo_set_line_width(cairo, pen_width);
216 /* Dirty only the new segment */
217 pen_range = 2 * pen_width + 0.9999;
218 gtk_widget_queue_draw_area(drawing_area, xmin - pen_range,
220 xmax - xmin + pen_range + 1,
221 ymax - ymin + pen_range + 1);
224 static void render_sample(Sample *sample, int cell)
225 /* Render the ink from a sample in a cell */
233 /* Center stored samples on input */
234 if (sample != &cells[cell].sample)
235 center_samples(&sc_to_ic, sample, &cells[cell].sample);
237 vec2_set(&sc_to_ic, 0., 0.);
239 for (i = 0; i < sample->len; i++)
240 if (sample->strokes[i]->len <= 1 ||
241 sample->strokes[i]->spread < DOT_SPREAD)
242 render_point(sample, cell, i, &sc_to_ic);
244 for (j = 0; j < sample->strokes[i]->len - 1; j++)
245 render_segment(sample, cell, i, j, &sc_to_ic);
248 static int cell_offscreen(int cell)
253 rows = cell_rows < cell_rows_pref ? cell_rows : cell_rows_pref;
254 return cell < cell_row_view * cols ||
255 cell >= (cell_row_view + rows) * cols;
258 static void dirty_cell(int cell)
260 if (!cell_offscreen(cell))
261 cells[cell].flags |= CELL_DIRTY;
264 static void dirty_all(void)
268 rows = cell_row_view + cell_rows_pref > cell_rows ?
269 cell_rows : cell_row_view + cell_rows_pref;
270 for (i = cell_cols * cell_row_view; i < rows * cell_cols; i++)
271 cells[i].flags |= CELL_DIRTY;
274 static void render_cell(int i)
276 cairo_pattern_t *pattern;
277 GdkColor color, *base_color;
279 int x, y, active, cols, samples = 0;
281 if (!cairo || !pixmap || !pixmap_gc || cell_offscreen(i))
284 cell_coords(i, &x, &y);
286 samples = char_trained(pc->ch);
287 active = pc->ch && (samples > 0 ||
288 (current_cell == i && input &&
289 !invalid && input->len));
291 active = pc->ch || (current_cell == i && !inserting &&
292 !invalid && input && input->len);
293 base_color = active ? &color_active : &color_inactive;
295 /* Fill above baseline */
296 gdk_gc_set_rgb_fg_color(pixmap_gc, base_color);
297 gdk_draw_rectangle(pixmap, pixmap_gc, TRUE, x, y, cell_width,
298 cell_height - CELL_BASELINE);
301 highlight_gdk_color(base_color, &color, 0.1);
302 gdk_gc_set_rgb_fg_color(pixmap_gc, &color);
303 gdk_draw_rectangle(pixmap, pixmap_gc, TRUE, x, y + cell_height -
304 CELL_BASELINE, cell_width, CELL_BASELINE);
306 /* Cairo clip region */
307 cairo_reset_clip(cairo);
308 cairo_rectangle(cairo, x, y, cell_width, cell_height);
313 if ((!right_to_left && i % cell_cols) ||
314 (right_to_left && i % cell_cols != cols - 1)) {
315 highlight_gdk_color(base_color, &color, 0.5);
316 pattern = cairo_pattern_create_linear(x, y, x, y + cell_height);
317 cairo_pattern_add_gdk_color_stop(pattern, 0.0, &color, 0.);
318 cairo_pattern_add_gdk_color_stop(pattern, 0.5, &color, 1.);
319 cairo_pattern_add_gdk_color_stop(pattern, 1.0, &color, 0.);
320 cairo_set_source(cairo, pattern);
321 cairo_set_line_width(cairo, 0.5);
322 cairo_move_to(cairo, x + 0.5, y);
323 cairo_line_to(cairo, x + 0.5, y + cell_height - 1);
325 cairo_pattern_destroy(pattern);
328 /* Draw ink if shown */
329 if ((cells[i].ch && cells[i].flags & CELL_SHOW_INK) ||
330 (current_cell == i && input && input->len)) {
333 render_sample(&cells[i].sample, i);
335 for (j = 0; j < ALTERNATES && cells[i].alts[j]; j++)
336 if (sample_valid(cells[i].alts[j],
337 cells[i].alt_used[j]) &&
338 cells[i].alts[j]->ch == cells[i].ch) {
339 render_sample(cells[i].alts[j], i);
344 /* Draw letter if recognized or training */
345 else if (pc->ch && (current_cell != i || !input || !input->len)) {
347 PangoRectangle ink_ext, log_ext;
348 char string[6] = { 0, 0, 0, 0, 0, 0 };
350 /* Training color is determined by how well a character is
354 highlight_gdk_color(&color_ink, &color,
355 0.5 - ((double)samples) /
358 highlight_gdk_color(&color_inactive,
362 /* Use ink color unless this is a questionable match */
365 if (!(pc->flags & CELL_VERIFIED) && pc->alts[0] &&
366 pc->alts[1] && pc->ch == pc->alts[0]->ch &&
367 pc->alt_ratings[0] - pc->alt_ratings[1] <= 10)
368 color = color_select;
371 cairo_set_source_gdk_color(cairo, &color, 1.);
372 layout = pango_layout_new(pango);
373 cairo_move_to(cairo, x, y);
374 g_unichar_to_utf8(pc->ch, string);
375 pango_layout_set_text(layout, string, 6);
376 pango_layout_set_font_description(layout, pango_font_desc);
377 pango_layout_get_pixel_extents(layout, &ink_ext, &log_ext);
378 cairo_rel_move_to(cairo,
379 cell_width / 2 - log_ext.width / 2, 2);
380 pango_cairo_show_layout(cairo, layout);
381 g_object_unref(layout);
384 /* Insertion arrows */
385 if (!invalid && inserting &&
386 (current_cell == i || current_cell == i + 1)) {
387 double width, stem, height;
389 cairo_set_source_gdk_color(cairo, &color_select, 1.);
391 stem = CELL_BORDER / 2;
392 height = CELL_BORDER;
393 if ((!right_to_left && current_cell == i) ||
394 (right_to_left && current_cell == i + 1)) {
396 /* Top right arrow */
397 cairo_move_to(cairo, x, y + 1);
398 cairo_line_to(cairo, x + stem, y + 1);
399 cairo_line_to(cairo, x + stem, y + height);
400 cairo_line_to(cairo, x + width, y + height);
401 cairo_line_to(cairo, x, y + height * 2);
402 cairo_close_path(cairo);
405 /* Bottom right arrow */
406 cairo_move_to(cairo, x, y + cell_height - 1);
407 cairo_line_to(cairo, x + stem, y + cell_height - 1);
408 cairo_line_to(cairo, x + stem,
409 y + cell_height - height);
410 cairo_line_to(cairo, x + width,
411 y + cell_height - height);
412 cairo_line_to(cairo, x, y + cell_height - height * 2);
413 cairo_close_path(cairo);
416 } else if ((!right_to_left && current_cell == i + 1) ||
417 (right_to_left && current_cell == i)) {
420 ox = i % cell_cols == cell_cols - 1 ? 0. : 1.;
423 cairo_move_to(cairo, x + cell_width + ox, y + 1);
424 cairo_line_to(cairo, x + cell_width - stem + ox,
426 cairo_line_to(cairo, x + cell_width - stem + ox,
428 cairo_line_to(cairo, x + cell_width - width + ox,
430 cairo_line_to(cairo, x + cell_width + ox,
432 cairo_close_path(cairo);
435 /* Bottom left arrow */
436 cairo_move_to(cairo, x + cell_width + ox,
437 y + cell_height - 1);
438 cairo_line_to(cairo, x + cell_width - stem + ox,
439 y + cell_height - 1);
440 cairo_line_to(cairo, x + cell_width - stem + ox,
441 y + cell_height - height);
442 cairo_line_to(cairo, x + cell_width - width + ox,
443 y + cell_height - height);
444 cairo_line_to(cairo, x + cell_width + ox,
445 y + cell_height - height * 2);
446 cairo_close_path(cairo);
452 gtk_widget_queue_draw_area(drawing_area, x, y, cell_width, cell_height);
453 pc->flags &= ~CELL_DIRTY;
457 static void render_dirty(void)
458 /* Render cells marked dirty */
462 for (i = cell_row_view * cell_cols; i < cell_rows * cell_cols; i++)
463 if (cells[i].flags & CELL_DIRTY)
467 void cell_widget_render(void)
468 /* Render the cells */
470 int i, cols, rows, width, height;
472 if (!cairo || !pixmap || !pixmap_gc)
475 /* On-screen keyboard eats up some cells on the end */
479 for (i = cell_row_view * cols; i < cell_rows * cols; i++)
483 rows = cell_rows < cell_rows_pref ? cell_rows : cell_rows_pref;
484 width = cell_width * cols + 1;
485 height = cell_height * rows + 1;
486 gdk_gc_set_rgb_fg_color(pixmap_gc, &color_bg_dark);
488 gdk_draw_rectangle(pixmap, pixmap_gc, FALSE, 0, 0,
491 gdk_draw_rectangle(pixmap, pixmap_gc, FALSE,
492 drawing_area->allocation.width - width - 1,
495 /* Fill extra space to the right */
496 gdk_gc_set_rgb_fg_color(pixmap_gc, &color_bg);
498 gdk_draw_rectangle(pixmap, pixmap_gc, TRUE, width + 1, 0,
499 drawing_area->allocation.width - width,
502 gdk_draw_rectangle(pixmap, pixmap_gc, TRUE, 0, 0,
503 drawing_area->allocation.width - width - 1,
506 /* Fill extra space below */
507 gdk_draw_rectangle(pixmap, pixmap_gc, TRUE, 0, height + 1,
508 drawing_area->allocation.width,
509 drawing_area->allocation.height - height + 1);
511 /* Dirty the entire drawing area */
512 gtk_widget_queue_draw(drawing_area);
515 static void clear_cell(int i)
521 if (cell->ch || i == current_cell) {
522 if (i == current_cell)
524 cell->flags |= CELL_DIRTY;
526 clear_sample(&cell->sample);
528 cell->alts[0] = NULL;
531 static void pad_cell(int cell)
535 /* Turn any blank cells behind the cell into spaces */
536 for (i = cell - 1; i >= 0 && !cells[i].ch; i--) {
538 cells[i].flags |= CELL_DIRTY;
542 static void free_cells(void)
543 /* Free sample data */
549 for (i = 0; i < cell_rows * cell_cols; i++)
556 static void wrap_cells(int new_rows, int new_cols)
557 /* Word wrap cells */
560 int i, j, size, row, col, break_i = -1, break_j = -1;
562 /* Allocate and clear the new grid */
565 size = new_rows * new_cols * sizeof (Cell);
566 new_cells = g_malloc0(size);
568 for (i = 0, j = 0, row = 0, col = 0; i < cell_rows * cell_cols; i++) {
572 /* Break at non-alphanumeric characters */
573 if (!g_unichar_isalnum(cells[i].ch)) {
578 if (col >= new_cols) {
580 /* If we need to, allocate room for the new row */
581 if (++row >= new_rows) {
582 size = ++new_rows * new_cols * sizeof (Cell);
583 new_cells = g_realloc(new_cells, size);
584 memset(new_cells + (new_rows - 1) * new_cols,
585 0, new_cols * sizeof (Cell));
588 /* Move any hanging words down to the next row */
589 size = i - break_i - 1;
590 if (size >= 0 && size < i - 1) {
591 memset(new_cells + break_j + 1, 0,
592 sizeof (Cell) * size);
600 new_cells[j++] = cells[i];
604 /* If we have filled the last row, we need to add a new row */
605 if (col >= new_cols && row >= new_rows - 1) {
606 size = ++new_rows * new_cols * sizeof (Cell);
607 new_cells = g_realloc(new_cells, size);
608 memset(new_cells + (new_rows - 1) * new_cols, 0,
609 new_cols * sizeof (Cell));
612 /* Only free the cell array, NOT the samples as we have copied the
613 Sample data over to the new cell array */
617 /* Scroll the grid */
618 if (new_rows > cell_rows && new_rows > cell_rows_pref)
619 cell_row_view += new_rows - cell_rows;
621 /* Do not let the row view look too far down */
622 if (cell_row_view + cell_rows_pref > new_rows) {
623 cell_row_view = new_rows - cell_rows_pref;
624 if (cell_row_view < 0)
628 cell_rows = new_rows;
629 cell_cols = new_cols;
632 static int set_size_request(int force)
633 /* Resize the drawing area if necessary */
635 int new_w, new_h, rows, resized;
637 new_w = cell_cols * cell_width + 2;
639 if (rows > cell_rows_pref)
640 rows = cell_rows_pref;
641 new_h = rows * cell_height + 2;
642 resized = new_w != drawing_area->allocation.width ||
643 new_h != drawing_area->allocation.height || force;
646 gtk_widget_set_size_request(drawing_area, new_w, new_h);
650 static int pack_cells(int new_rows, int new_cols)
651 /* Pack and position cells, resize widget and window when necessary.
652 Returns TRUE if the widget was resized in the process and can expect a
653 configure event in the near future. */
655 int i, rows, range, new_range;
657 /* Must have at least one row */
661 /* Word wrapping will perform its own memory allocation */
662 if (!training && cells)
663 wrap_cells(new_rows, new_cols);
665 else if (!cells || new_rows != cell_rows || new_cols != cell_cols) {
667 /* Find minimum number of rows necessary */
669 for (i = cell_rows * cell_cols - 1; i > 0; i--)
672 rows = i / new_cols + 1;
675 new_range = new_rows * new_cols;
677 /* If we have shrunk the grid, clear cells outside */
678 range = cell_rows * cell_cols;
679 for (i = new_range; i < range; i++)
683 new_range = new_rows * new_cols;
686 /* Allocate enough room, clear any new cells */
687 cells = g_realloc(cells, new_rows * new_cols * sizeof (Cell));
688 if (new_range > range)
689 memset(cells + range, 0,
690 (new_range - range) * sizeof (Cell));
692 cell_rows = new_rows;
693 cell_cols = new_cols;
697 /* Update the scrollbar */
698 if (cell_rows <= cell_rows_pref) {
700 gtk_widget_hide(scrollbar);
702 GtkObject *adjustment;
704 if (cell_row_view > cell_rows - cell_rows_pref)
705 cell_row_view = cell_rows - cell_rows_pref;
706 if (cell_row_view < 0)
708 adjustment = gtk_adjustment_new(cell_row_view, 0, cell_rows, 1,
709 cell_rows_pref, cell_rows_pref);
710 gtk_range_set_adjustment(GTK_RANGE(scrollbar),
711 GTK_ADJUSTMENT(adjustment));
712 gtk_widget_show(scrollbar);
715 return set_size_request(FALSE);
718 static void stop_timeout(void)
722 g_source_remove(timeout_source);
726 static void finish_cell(int cell)
729 if (cell < 0 || cell >= cell_rows * cell_cols ||
730 !input || input->len < 1)
732 cells[cell].flags |= CELL_DIRTY;
734 /* Train on the input */
736 train_sample(&cells[cell].sample, TRUE);
738 /* Recognize input */
739 else if (input && input->strokes[0] && input->strokes[0]->len) {
740 Cell *pc = cells + cell;
744 if (pc->ch && pc->ch != ' ')
749 recognize_sample(input, pc->alts, ALTERNATES);
751 pc->flags &= ~CELL_VERIFIED;
755 /* Copy the alternate ratings and usage stamps before they're
756 overwritten by another call to recognize_sample() */
757 for (i = 0; i < ALTERNATES && pc->alts[i]; i++) {
758 pc->alt_ratings[i] = pc->alts[i]->rating;
759 pc->alt_used[i] = pc->alts[i]->used;
762 /* Add a row if this is the last cell */
763 if (cell == cell_rows * cell_cols - 1)
764 pack_cells(0, cell_cols);
771 static gboolean finish_timeout(void)
772 /* Motion timeout for finishing drawing a cell */
774 finish_cell(current_cell);
781 static gboolean row_timeout(void)
782 /* Motion timeout for adding a row */
784 pack_cells(cell_rows + 1, cell_cols);
785 cell_widget_render();
790 static int check_clear(void)
796 if (training || (input && input->len))
798 for (i = 0; i < cell_cols * cell_rows; i++)
804 static gboolean is_clear_timeout(void)
805 /* Motion timeout for checking clear state */
808 if (is_clear || !check_clear())
811 /* Show the on-screen keyboard */
812 show_keys = keyboard_enabled;
815 pack_cells(1, cell_cols);
816 cell_widget_render();
820 static gboolean hold_timeout(void)
821 /* Motion timeout for popping up a hold-click context menu */
823 if (potential_hold) {
824 potential_hold = FALSE;
826 show_context_menu(1, gtk_get_current_event_time());
832 static void start_timeout(void)
833 /* If a timeout action is approriate for the current situation, start a
836 GSourceFunc func = NULL;
844 /* Events below are not triggered while drawing */
847 func = (GSourceFunc)finish_timeout;
848 else if (!cells[cell_rows * cell_cols - 1].ch &&
849 cells[cell_rows * cell_cols - 2].ch && !training)
850 func = (GSourceFunc)row_timeout;
851 else if (!is_clear && check_clear())
852 func = (GSourceFunc)is_clear_timeout;
856 timeout_source = g_timeout_add(MOTION_TIMEOUT, func, NULL);
859 static void start_hold(void)
861 potential_hold = TRUE;
863 g_source_remove(timeout_source);
864 timeout_source = g_timeout_add(MOTION_TIMEOUT,
865 (GSourceFunc)hold_timeout, NULL);
868 void cell_widget_set_cursor(int recreate)
869 /* Set the drawing area cursor to a black box pen cursor or to a blank cursor
870 depending on which is appropriate */
872 static char bits[] = { 0xff, 0xff, 0xff }; /* Square cursor */
873 /*{ 0x02, 0xff, 0x02 };*/ /* Cross cursor */
874 static GdkCursor *square;
878 /* Ink color changed, recreate cursor */
881 gdk_cursor_unref(square);
882 pixmap = gdk_bitmap_create_from_data(NULL, bits, 3, 3);
883 square = gdk_cursor_new_from_pixmap(pixmap, pixmap,
886 g_object_unref(pixmap);
891 if (eraser || cross_out) {
894 display = gtk_widget_get_display(drawing_area);
895 cursor = gdk_cursor_new_for_display(display, GDK_CIRCLE);
898 gdk_window_set_cursor(drawing_area->window,
899 invalid || inserting ? NULL : cursor);
902 static void stop_drawing(void)
903 /* Ends the current stroke and applies various processing functions */
910 cell_widget_set_cursor(FALSE);
915 if (!input || input->len >= STROKES_MAX)
917 stroke = input->strokes[input->len - 1];
918 smooth_stroke(stroke);
919 simplify_stroke(stroke);
920 process_stroke(stroke);
921 render_cell(current_cell);
922 render_sample(input, current_cell);
926 static void erase_cell(int cell)
932 untrain_char(cells[cell].ch);
937 static void check_cell(double x, double y, GdkDevice *device)
938 /* Check if we have changed to a different cell */
940 int cell_x, cell_y, cell, rem_x, rem_y,
941 old_inserting, old_invalid, old_eraser, old_cross_out;
943 /* Stop drawing first */
944 old_cross_out = cross_out;
945 if (drawing && !cross_out) {
948 /* Check if we have started the cross-out gesture */
949 cell_coords(current_cell, &cell_x, &cell_y);
950 cell_x += cell_width / 2;
951 cell_y += cell_height / 2;
958 if (dx < cell_width && dy < cell_height)
965 erase_cell(current_cell);
968 /* Is this the eraser tip? */
970 eraser = device && device->source == GDK_SOURCE_ERASER;
972 /* Adjust for border */
976 /* Right-to-left mode inverts the x-axis */
978 x = cell_cols * cell_width - x - 1;
980 /* What cell are we hovering over? */
981 cell_y = y / cell_height + cell_row_view;
982 cell_x = x / cell_width;
983 cell = cell_cols * cell_y + cell_x;
985 /* Out of bounds or invalid cell */
986 old_invalid = invalid;
987 invalid = cell_x < 0 || cell_y < 0 || cell_x >= cell_cols ||
988 cell_y >= cell_rows || cell_offscreen(cell) ||
989 (training && !cells[cell].ch);
991 /* Are we in the insertion hotspot? */
992 rem_x = x - cell_x * cell_width;
993 rem_y = y - (cell_y - cell_row_view) * cell_height;
994 old_inserting = inserting;
996 if (!cross_out && !eraser && !invalid && !training && !input &&
997 (rem_y <= CELL_BORDER * 2 ||
998 rem_y > cell_height - CELL_BORDER * 2)) {
999 if (rem_x <= CELL_BORDER + 1)
1001 else if (cell < cell_rows * cell_cols - 1 &&
1002 rem_x > cell_width - CELL_BORDER) {
1008 /* Current cell has changed */
1009 old_cc = current_cell;
1010 if (current_cell != cell) {
1011 current_cell = cell;
1013 finish_cell(old_cc);
1016 /* We have moved into or out of the insertion hotspot */
1017 if (old_inserting != inserting || old_cc != cell) {
1018 if (old_inserting) {
1020 dirty_cell(old_cc - 1);
1023 dirty_cell(current_cell);
1024 dirty_cell(current_cell - 1);
1028 /* Update cursor if necessary */
1029 if (old_invalid != invalid || old_inserting != inserting ||
1030 old_eraser != eraser || old_cross_out != cross_out)
1031 cell_widget_set_cursor(FALSE);
1036 static void unclear(int render)
1037 /* Hides the on-screen keyboard and re-renders the cells.
1038 FIXME we only need to render dirty cells */
1045 cell_widget_render();
1048 static void draw(double x, double y)
1053 if (current_cell < 0)
1056 /* Hide the on-screen keyboard */
1060 if (!input || !input->len) {
1061 clear_sample(&cells[current_cell].sample);
1062 cells[current_cell].alts[0] = NULL;
1063 input = &cells[current_cell].sample;
1064 cells[current_cell].sample.ch = cells[current_cell].ch;
1067 /* Allocate a new stroke if we aren't already drawing */
1068 s = input->strokes[input->len - 1];
1070 if (input->len >= STROKES_MAX)
1072 s = input->strokes[input->len++]= stroke_new(0);
1074 if (input->len == 1)
1075 render_cell(current_cell);
1079 cell_coords(current_cell, &cx, &cy);
1081 /* Normalize the input */
1082 x = (x - cx - cell_width / 2) * SCALE / cell_height;
1083 y = (y - cy - cell_height / 2) * SCALE / cell_height;
1085 draw_stroke(&input->strokes[input->len - 1], x, y);
1088 static void insert_cell(int cell)
1092 /* Find a blank to consume */
1093 for (i = cell; i < cell_rows * cell_cols; i++)
1097 /* Insert a row if necessary */
1098 if (i >= cell_rows * cell_cols - 1) {
1099 cells = g_realloc(cells,
1100 ++cell_rows * cell_cols * sizeof (Cell));
1101 memset(cells + (cell_rows - 1) * cell_cols, 0,
1102 cell_cols * sizeof (Cell));
1103 if (cell_rows > cell_rows_pref)
1108 memmove(cells + cell + 1, cells + cell,
1109 (i - cell) * sizeof (Cell));
1110 cells[cell].ch = ' ';
1111 cells[cell].alts[0] = NULL;
1112 cells[cell].sample.len = 0;
1113 cells[cell].sample.ch = 0;
1115 pack_cells(0, cell_cols);
1117 cell_widget_render();
1120 static void delete_cell(int cell)
1125 memmove(cells + cell, cells + cell + 1,
1126 (cell_rows * cell_cols - cell - 1) * sizeof (Cell));
1128 /* Delete a row if necessary */
1129 for (i = 0; i < cell_cols &&
1130 !cells[(cell_rows - 1) * cell_cols + i].ch; i++);
1132 if (i == cell_cols && cell_rows > 1 &&
1133 !cells[(cell_rows - 1) * cell_cols - 1].ch)
1135 cells[cell_rows * cell_cols - 1].ch = 0;
1136 cells[cell_rows * cell_cols - 1].alts[0] = NULL;
1138 pack_cells(0, cell_cols);
1139 cell_widget_render();
1142 static void send_cell_key(int cell)
1143 /* Send the key event for the cell */
1147 if (!cells[cell].ch)
1150 /* Collect stats and train on corrections */
1151 if (cells[cell].ch != ' ') {
1152 if (cells[cell].ch != cells[cell].sample.ch)
1154 if (train_on_input && !(cells[cell].flags & CELL_SHIFTED) &&
1155 cells[cell].sample.len) {
1156 cells[cell].sample.ch = cells[cell].ch;
1157 train_sample(&cells[cell].sample, FALSE);
1162 /* Update the usage time for the sample that matched this character */
1163 for (i = 0; i < ALTERNATES && cells[cell].alts[i]; i++) {
1164 if (!sample_valid(cells[cell].alts[i], cells[cell].alt_used[i]))
1166 if (cells[cell].alts[i]->ch == cells[cell].ch) {
1167 promote_sample(cells[cell].alts[i]);
1170 demote_sample(cells[cell].alts[i]);
1173 key_event_send_char(cells[cell].ch);
1180 /* Hold click area radius */
1181 #define HOLD_CLICK_WIDTH 3.
1183 /* Mask for possible buttons used by the eraser */
1184 #define ERASER_BUTTON_MASK (GDK_MOD5_MASK | GDK_BUTTON1_MASK | \
1185 GDK_BUTTON2_MASK | GDK_BUTTON3_MASK | \
1186 GDK_BUTTON4_MASK | GDK_BUTTON5_MASK)
1188 static int menu_cell, alt_menu_alts[ALTERNATES];
1190 static void training_menu_reset(void)
1192 untrain_char(cells[menu_cell].ch);
1193 render_cell(menu_cell);
1196 static void alt_menu_selection_done(GtkWidget *widget)
1198 gtk_widget_destroy(widget);
1201 static void alt_menu_activate(GtkWidget *widget, int *alt)
1203 cells[menu_cell].ch = *alt;
1204 cells[menu_cell].flags |= CELL_VERIFIED;
1205 cells[menu_cell].flags &= ~CELL_SHIFTED;
1206 render_cell(menu_cell);
1209 static void alt_menu_delete(void)
1211 delete_cell(menu_cell);
1214 static void alt_menu_show_ink(void)
1216 cells[menu_cell].flags ^= CELL_SHOW_INK;
1217 render_cell(menu_cell);
1220 static void alt_menu_change_case(void)
1222 if (g_unichar_isupper(cells[menu_cell].ch)) {
1223 cells[menu_cell].ch = g_unichar_tolower(cells[menu_cell].ch);
1224 cells[menu_cell].flags |= CELL_SHIFTED;
1225 render_cell(menu_cell);
1226 } else if (g_unichar_islower(cells[menu_cell].ch)) {
1227 cells[menu_cell].ch = g_unichar_toupper(cells[menu_cell].ch);
1228 cells[menu_cell].flags |= CELL_SHIFTED;
1229 render_cell(menu_cell);
1231 g_debug("Cannot change case, not an alphabetic character");
1234 static gboolean scrollbar_scroll_event(GtkWidget *widget, GdkEventScroll *event)
1236 check_cell(event->x, event->y, event->device);
1240 static gboolean scroll_event(GtkWidget *widget, GdkEventScroll *event)
1242 if (scrollbar && GTK_WIDGET_VISIBLE(scrollbar))
1243 gtk_widget_event(scrollbar, (GdkEvent*)event);
1247 static void context_menu_position(GtkMenu *menu, gint *x, gint *y,
1249 /* Positions the two-column context menu so that the column divide is at
1250 the cursor rather than the upper left hand point */
1252 if (cells[menu_cell].alts[0])
1253 *x -= GTK_WIDGET(menu)->requisition.width / 2;
1257 static void show_context_menu(int button, int time)
1258 /* Popup the cell context menu for the current cell */
1260 GtkWidget *menu, *widget;
1263 /* Training menu is the same for all cells */
1265 if (!char_trained(cells[current_cell].ch))
1267 menu_cell = current_cell;
1268 gtk_menu_popup(GTK_MENU(training_menu), 0, 0, 0, 0,
1273 /* Can't delete blanks */
1274 if (!cells[current_cell].ch)
1277 /* Construct an alternates menu for the current button */
1278 menu = gtk_menu_new();
1279 menu_cell = current_cell;
1281 /* Menu -> Delete */
1282 widget = gtk_menu_item_new_with_label("Delete");
1283 g_signal_connect(G_OBJECT(widget), "activate",
1284 G_CALLBACK(alt_menu_delete), NULL);
1285 gtk_menu_attach(GTK_MENU(menu), widget, 0, 1, 0, 1);
1287 /* Menu -> Show Ink */
1288 if (cells[menu_cell].sample.ch) {
1291 label = cells[menu_cell].flags & CELL_SHOW_INK ?
1292 "Hide ink" : "Show ink";
1293 widget = gtk_menu_item_new_with_label(label);
1294 g_signal_connect(G_OBJECT(widget), "activate",
1295 G_CALLBACK(alt_menu_show_ink), NULL);
1296 gtk_menu_attach(GTK_MENU(menu), widget, 0, 1, 1, 2);
1299 /* Menu -> Change case */
1300 if (g_unichar_isupper(cells[menu_cell].ch) ||
1301 g_unichar_islower(cells[menu_cell].ch)) {
1302 const char *string = "To upper";
1304 if (g_unichar_isupper(cells[menu_cell].ch))
1305 string = "To lower";
1306 widget = gtk_menu_item_new_with_label(string);
1307 g_signal_connect(G_OBJECT(widget), "activate",
1308 G_CALLBACK(alt_menu_change_case), NULL);
1309 gtk_menu_attach(GTK_MENU(menu), widget, 0, 1, 2, 3);
1312 /* Menu -> Alternates */
1313 for (i = 0, pos = 0; i < ALTERNATES &&
1314 cells[current_cell].alts[i]; i++) {
1317 if (!sample_valid(cells[current_cell].alts[i],
1318 cells[current_cell].alt_used[i]))
1320 str = va("%C\t%d%%", cells[current_cell].alts[i]->ch,
1321 cells[current_cell].alt_ratings[i]);
1322 alt_menu_alts[i] = cells[current_cell].alts[i]->ch;
1323 widget = gtk_check_menu_item_new_with_label(str);
1324 if (cells[current_cell].ch == cells[current_cell].alts[i]->ch)
1325 gtk_check_menu_item_set_active(
1326 GTK_CHECK_MENU_ITEM(widget), TRUE);
1327 g_signal_connect(G_OBJECT(widget), "activate",
1328 G_CALLBACK(alt_menu_activate),
1330 gtk_menu_attach(GTK_MENU(menu), widget, 1, 2, pos, pos + 1);
1333 g_signal_connect(G_OBJECT(menu), "selection-done",
1334 G_CALLBACK(alt_menu_selection_done), NULL);
1335 gtk_widget_show_all(menu);
1336 gtk_menu_popup(GTK_MENU(menu), 0, 0,
1337 (GtkMenuPositionFunc)context_menu_position,
1342 static gboolean button_press_event(GtkWidget *widget, GdkEventButton *event)
1343 /* Mouse button is pressed over drawing area */
1345 /* Don't process double clicks */
1346 if (event->type != GDK_BUTTON_PRESS)
1349 /* Check validity every time */
1350 check_cell(event->x, event->y, event->device);
1354 /* If we are drawing and we get a button press event it is possible
1355 that we never received a button release event for some reason.
1356 This is a fix for Zaurus drawing connected lines. */
1360 /* If we have pressed with the eraser, erase the cell */
1361 if (eraser || event->button == 2) {
1362 erase_cell(current_cell);
1366 /* Draw/activate insert with left click */
1367 if (event->button == 1) {
1369 potential_insert = TRUE;
1370 else if (cells[current_cell].ch) {
1373 draw(event->x, event->y);
1375 /* We are now counting on getting valid coordinates here so
1376 save in case we are doing a potential insert/hold and we
1377 don't get a motion event in between */
1378 cursor_x = event->x;
1379 cursor_y = event->y;
1384 /* Right-click opens context menu */
1385 else if (event->button == 3 && current_cell >= 0 && !inserting &&
1386 (!input || !input->len)) {
1387 show_context_menu(event->button, event->time);
1394 static gboolean button_release_event(GtkWidget *widget, GdkEventButton *event)
1395 /* Mouse button is released over drawing area */
1397 /* Only handle left-clicks */
1398 if (event->button != 1)
1401 /* Complete an insertion */
1402 if (potential_insert && inserting) {
1403 insert_cell(current_cell);
1404 potential_insert = FALSE;
1408 /* Cancel a hold-click */
1409 if (potential_hold) {
1410 potential_hold = FALSE;
1411 draw(cursor_x, cursor_y);
1418 static gboolean motion_notify_event(GtkWidget *widget, GdkEventMotion *event)
1419 /* Mouse is moved over drawing area */
1421 GdkModifierType state;
1424 /* Fetch event coordinates */
1427 if (xinput_enabled) {
1428 gdk_device_get_state(event->device, event->window, NULL,
1430 gdk_event_get_coords((GdkEvent*)event, &x, &y);
1433 #if GTK_CHECK_VERSION(2, 12, 0)
1434 /* Process a hint event (GTK >= 2.12) */
1435 gdk_event_request_motions(event);
1437 /* Process a hint event (GTK <= 2.10) */
1438 else if (event->is_hint) {
1441 gdk_window_get_pointer(event->window, &nx, &ny, &state);
1447 /* If we are getting invalid output from this device with XInput
1448 enabled, try disabling it */
1449 if ((x < 0 || x > drawing_area->allocation.width ||
1450 y < 0 || y > drawing_area->allocation.width) &&
1451 event->device->mode != GDK_MODE_DISABLED && xinput_enabled) {
1452 g_warning("Extended input device is generating invalid "
1453 "coordinates, disabled");
1454 gdk_device_set_mode(event->device, GDK_MODE_DISABLED);
1458 /* Check where the pointer is */
1459 check_cell(x, y, event->device);
1461 /* Cancel a potential insert */
1462 if (potential_insert) {
1464 potential_insert = FALSE;
1465 draw(cursor_x, cursor_y);
1470 /* Cancel a potential hold-click */
1471 if (potential_hold) {
1476 if (dx < -HOLD_CLICK_WIDTH || dx > HOLD_CLICK_WIDTH ||
1477 dy < -HOLD_CLICK_WIDTH || dy > HOLD_CLICK_WIDTH) {
1478 potential_hold = FALSE;
1479 draw(cursor_x, cursor_y);
1487 /* Record and draw new segment */
1489 draw(cursor_x, cursor_y);
1490 render_segment(input, current_cell, input->len - 1,
1491 input->strokes[input->len - 1]->len - 2, NULL);
1494 /* Erasing with the eraser. We get MOD5 rather than a button for the
1495 eraser being pressed on a Tablet PC. */
1496 else if (!invalid &&
1497 (cross_out || (eraser && (state & ERASER_BUTTON_MASK))))
1498 erase_cell(current_cell);
1500 /* Plain motion restarts the finish countdown */
1506 static void configure_keys(void)
1510 static gboolean configure_event(void)
1511 /* Create a new backing pixmap of the appropriate size */
1515 /* Do nothing if we are not visible */
1516 if (!drawing_area || !drawing_area->window ||
1517 !GTK_WIDGET_VISIBLE(drawing_area))
1520 /* Backing pixmap */
1522 int old_width, old_height;
1526 g_object_unref(pixmap);
1528 pixmap = gdk_pixmap_new(drawing_area->window,
1529 drawing_area->allocation.width,
1530 drawing_area->allocation.height, -1);
1531 trace("%dx%d", drawing_area->allocation.width,
1532 drawing_area->allocation.height);
1534 /* GDK graphics context */
1536 g_object_unref(pixmap_gc);
1537 pixmap_gc = gdk_gc_new(GDK_DRAWABLE(pixmap));
1541 cairo_destroy(cairo);
1542 cairo = gdk_cairo_create(GDK_DRAWABLE(pixmap));
1545 pango_font_description_set_absolute_size(pango_font_desc, PANGO_SCALE *
1547 CELL_BASELINE - 2));
1549 /* Get the background color */
1550 color_bg = window->style->bg[0];
1551 color_bg_dark = window->style->bg[1];
1554 cell_widget_set_cursor(TRUE);
1556 /* If the cell dimensions changed, repack */
1557 if (window_embedded) {
1558 new_cols = (drawing_area->allocation.width -
1559 cell_widget_scrollbar_width() - 6) / cell_width;
1560 if (new_cols != cell_cols)
1561 pack_cells(1, new_cols);
1564 /* If we are embedded we won't be able to resize the window so we
1565 can't honor the maximum rows preference */
1566 if (window_embedded)
1567 cell_rows_pref = drawing_area->allocation.height / cell_height;
1569 /* Update the key widget with new values */
1572 /* Render the cells */
1573 cell_widget_render();
1578 static gboolean expose_event(GtkWidget *widget, GdkEventExpose *event)
1579 /* Redraw the drawing area from the backing pixmap */
1583 gdk_draw_drawable(widget->window,
1584 widget->style->fg_gc[GTK_WIDGET_STATE(widget)],
1585 pixmap, event->area.x, event->area.y, event->area.x,
1586 event->area.y, event->area.width, event->area.height);
1590 static gboolean enter_notify_event(GtkWidget *widget, GdkEventCrossing *event)
1592 check_cell(event->x, event->y, NULL);
1596 static gboolean leave_notify_event(GtkWidget *widget, GdkEventCrossing *event)
1598 /* Tablet PC gets grab leave-notify event when starting to draw.
1599 Ignore this if we are still drawing. */
1600 if (event->mode == GDK_CROSSING_GRAB || drawing || cross_out)
1603 old_cc = current_cell;
1605 finish_cell(old_cc);
1609 dirty_cell(old_cc - 1);
1612 cell_widget_set_cursor(FALSE);
1618 static void scrollbar_value_changed(void)
1619 /* The cell widget has been scrolled */
1623 value = gtk_range_get_value(GTK_RANGE(scrollbar));
1624 if ((int)value == cell_row_view)
1626 cell_row_view = value;
1627 cell_widget_render();
1634 void cell_widget_enable_xinput(int on)
1635 /* Enable Xinput devices. We set everything to screen mode despite the fact
1636 that we actually want window coordinates. Window mode just seems to break
1637 everything and we get window coords with screen mode anyway! */
1643 gtk_widget_set_extension_events(drawing_area,
1644 on ? GDK_EXTENSION_EVENTS_ALL :
1645 GDK_EXTENSION_EVENTS_NONE);
1646 mode = on ? GDK_MODE_SCREEN : GDK_MODE_DISABLED;
1647 list = gdk_devices_list();
1648 for (i = 0; (device = (GdkDevice*)g_list_nth_data(list, i)); i++)
1649 gdk_device_set_mode(device, mode);
1650 xinput_enabled = on;
1651 g_debug(on ? "Xinput events enabled" : "Xinput events disabled");
1654 int cell_widget_update_colors(void)
1656 GdkColor old_active, old_inactive, old_ink, old_select;
1658 old_active = color_active;
1659 old_inactive = color_inactive;
1660 old_ink = color_ink;
1661 old_select = color_select;
1662 color_active = custom_active_color;
1663 color_inactive = custom_inactive_color;
1664 color_ink = custom_ink_color;
1665 color_select = custom_select_color;
1667 color_active = window->style->base[0];
1668 color_ink = window->style->text[0];
1669 color_inactive = window->style->bg[1];
1671 return !gdk_colors_equal(&old_active, &color_active) ||
1672 !gdk_colors_equal(&old_inactive, &color_inactive) ||
1673 !gdk_colors_equal(&old_ink, &color_ink) ||
1674 !gdk_colors_equal(&old_select, &color_select);
1677 const char *cell_widget_word(void)
1678 /* Return the current word and the current cell's position in that word
1679 FIXME this function ignores wide chars */
1681 static char buf[64];
1684 memset(buf, 0, sizeof (buf));
1685 if (cell_offscreen(old_cc))
1688 /* Find the start of the word */
1689 for (min = old_cc - 1; min >= 0 && cells[min].ch &&
1690 g_ascii_isalnum(cells[min].ch) && cells[min].ch < 0x7f; min--);
1692 /* Find the end of the word */
1693 for (max = old_cc + 1; max < cell_rows * cell_cols && cells[max].ch &&
1694 g_ascii_isalnum(cells[max].ch) && cells[max].ch < 0x7f; max++);
1696 /* Copy the word to a buffer */
1697 for (++min, i = 0; i < max - min && i < (int)sizeof (buf) - 1; i++)
1698 buf[i] = cells[min + i].ch;
1699 buf[old_cc - min] = 0;
1705 void cell_widget_clear(void)
1712 /* Restore cells if we just finished training */
1714 cells = cells_saved;
1715 cell_rows = cell_rows_saved;
1716 cell_cols = cell_cols_saved;
1717 cell_row_view = cell_row_view_saved;
1719 resized = pack_cells(cell_rows, cell_cols);
1721 /* Show the on-screen keyboard */
1722 if (check_clear()) {
1723 show_keys = keyboard_enabled;
1728 /* Clear cells otherwise */
1730 resized = pack_cells(1, cell_cols);
1732 /* Show the on-screen keyboard */
1733 show_keys = keyboard_enabled;
1737 /* Only re-render when we aren't going to get a configure event */
1739 cell_widget_render();
1742 void cell_widget_train(void)
1744 UnicodeBlock *block;
1751 cells_saved = cells;
1752 cell_rows_saved = cell_rows;
1753 cell_cols_saved = cell_cols;
1754 cell_row_view_saved = cell_row_view;
1759 /* Clear if not training any block */
1760 if (training_block < 0) {
1762 pack_cells(1, cell_cols);
1763 cell_widget_render();
1767 /* Pack the Unicode block's characters into the cell grid */
1768 block = unicode_blocks + training_block;
1769 range = block->end - block->start + 1;
1771 pack_cells((range + cell_cols - 1) / cell_cols, cell_cols);
1773 /* Preset all of the characters for training */
1774 for (i = 0, pos = 0; i < range; i++) {
1777 ch = block->start + i;
1778 if (char_disabled(ch))
1781 cells[pos].alts[0] = NULL;
1782 cells[pos++].flags = 0;
1785 for (; pos < cell_rows * cell_cols; pos++)
1787 pack_cells(1, cell_cols);
1790 cell_widget_render();
1793 void cell_widget_load_string(const gchar *str){
1795 int range = strlen(str);
1798 pack_cells((range + cell_cols - 1) / cell_cols, cell_cols);
1800 /* Preset all of the characters for training */
1801 for (i = 0; i < range; i++) {
1802 cells[i].ch = str[i]; // todo: support utf-8
1803 cells[i].alts[0] = NULL;
1806 for (; i < cell_rows * cell_cols; i++)
1808 pack_cells(1, cell_cols);
1810 cell_widget_render();
1814 void cell_widget_pack(void)
1819 cell_widget_train();
1822 cols = cell_cols_pref;
1823 if (window_docked) {
1826 screen = gtk_window_get_screen(GTK_WINDOW(window));
1827 cols = (gdk_screen_get_width(screen) -
1828 cell_widget_scrollbar_width() - 6) / cell_width;
1830 if (!pack_cells(0, cols))
1831 set_size_request(TRUE);
1833 show_keys = keyboard_enabled;
1835 /* Right-to-left mode may have changed so we need to reconfigure the
1836 on-screen keyboard */
1839 cell_widget_render();
1840 trace("%dx%d, scrollbar %d",
1841 cell_cols, cell_rows, cell_widget_scrollbar_width());
1846 int cell_widget_insert(void)
1849 int i, j, slot, chars;
1855 /* Prepare for sending key events */
1856 //key_event_update_mappings();
1858 /* Need to send the keys out in reverse order for right_to_left mode
1859 because the cells are displayed with columns reversed */
1861 for (i = cell_cols - 1; i < cell_rows * cell_cols; i--) {
1866 if (i % cell_cols == 0)
1871 for (i = 0; i < cell_rows * cell_cols; i++) {
1878 /* If nothing was entered, send Enter key event */
1880 key_event_send_enter();
1884 /* Create a UTF-16 string representation */
1885 utf16 = g_malloc(sizeof (**history) * (chars + 1));
1886 for (i = 0, j = 0; i < cell_rows * cell_cols; i++)
1888 utf16[j++] = cells[i].ch;
1891 /* If this text has been entered before, consume that history slot */
1892 slot = HISTORY_MAX - 1;
1893 for (i = 0; i < slot && history[i]; i++)
1894 for (j = 0; history[i][j] == utf16[j]; j++)
1900 /* Save entered text to history */
1901 g_free(history[slot]);
1902 memmove(history + 1, history, sizeof (*history) * slot);
1905 cell_widget_clear();
1909 static void buffer_menu_deactivate(GtkMenuShell *shell, GtkWidget *button)
1911 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), FALSE);
1914 static void buffer_menu_item_activate(GtkWidget *widget, gunichar2 *history)
1920 for (i = 0; history[i]; i++);
1921 cell_rows = i / cell_cols + 1;
1922 cell_cols = cell_cols;
1923 cells = g_malloc0(sizeof (*cells) * cell_cols * cell_rows);
1924 for (i = 0; history[i]; i++)
1925 cells[i].ch = history[i];
1926 pack_cells(cell_rows, cell_cols);
1930 static void buffer_menu_item_destroy(GtkWidget *widget, gchar *string)
1935 static void buffer_menu_position_func(GtkMenu *menu, gint *x, gint *y,
1936 gboolean *push_in, GtkWidget *button)
1938 gdk_window_get_origin(button->window, x, y);
1939 *x += button->allocation.x + button->allocation.width -
1940 GTK_WIDGET(menu)->requisition.width;
1941 *y += button->allocation.y + button->allocation.height;
1945 void cell_widget_show_buffer(GtkWidget *button)
1946 /* Show input back buffer menu */
1948 static GtkWidget *menu;
1952 gtk_widget_destroy(GTK_WIDGET(menu));
1953 menu = gtk_menu_new();
1954 g_signal_connect(G_OBJECT(menu), "deactivate",
1955 G_CALLBACK(buffer_menu_deactivate), button);
1956 for (i = 0; history[i] && i < HISTORY_MAX; i++) {
1958 GError *error = NULL;
1961 /* Convert string from a UTF-16 array to displayable UTF-8 */
1962 string = g_utf16_to_utf8(history[i], -1, NULL, NULL, &error);
1964 g_warning("g_utf16_to_utf8(): %s", error->message);
1968 /* Reverse the displayed string for right-to-left mode */
1969 if (right_to_left) {
1972 reversed = g_utf8_strreverse(string, -1);
1977 /* Create menu item */
1978 item = gtk_menu_item_new_with_label(string);
1979 g_signal_connect(G_OBJECT(item), "destroy",
1980 G_CALLBACK(buffer_menu_item_destroy), string);
1981 g_signal_connect(G_OBJECT(item), "activate",
1982 G_CALLBACK(buffer_menu_item_activate),
1984 gtk_menu_attach(GTK_MENU(menu), item, 0, 1, i, i + 1);
1987 /* Show back buffer menu */
1988 gtk_widget_show_all(menu);
1989 gtk_menu_popup(GTK_MENU(menu), NULL, NULL,
1990 (GtkMenuPositionFunc)buffer_menu_position_func,
1991 button, 0, gtk_get_current_event_time());
1994 int cell_widget_scrollbar_width(void)
1995 /* Gets the width of the scrollbar even if it is hidden */
1997 GtkRequisition requisition;
1999 if (scrollbar->requisition.width <= 1) {
2000 gtk_widget_size_request(scrollbar, &requisition);
2001 return requisition.width;
2003 return scrollbar->requisition.width + 4;
2006 int cell_widget_get_height(void)
2010 rows = cell_rows > cell_rows_pref ? cell_rows_pref : cell_rows;
2011 return rows * cell_height + 2;
2014 GtkWidget *cell_widget_new(void)
2015 /* Creates the Cell widget. Should only be called once per program run! */
2017 PangoCairoFontMap *font_map;
2018 GtkWidget *widget, *hbox;
2020 /* Initial settings */
2021 cell_cols = cell_cols_pref;
2023 /* Create drawing area */
2024 drawing_area = gtk_drawing_area_new();
2025 g_signal_connect(G_OBJECT(drawing_area), "expose_event",
2026 G_CALLBACK(expose_event), NULL);
2027 g_signal_connect(G_OBJECT(drawing_area), "configure_event",
2028 G_CALLBACK(configure_event), NULL);
2029 g_signal_connect(G_OBJECT(drawing_area), "show",
2030 G_CALLBACK(configure_event), NULL);
2031 g_signal_connect(G_OBJECT(drawing_area), "button_press_event",
2032 G_CALLBACK(button_press_event), NULL);
2033 g_signal_connect(G_OBJECT(drawing_area), "button_release_event",
2034 G_CALLBACK(button_release_event), NULL);
2035 g_signal_connect(G_OBJECT(drawing_area), "motion_notify_event",
2036 G_CALLBACK(motion_notify_event), NULL);
2037 g_signal_connect(G_OBJECT(drawing_area), "enter_notify_event",
2038 G_CALLBACK(enter_notify_event), NULL);
2039 g_signal_connect(G_OBJECT(drawing_area), "leave_notify_event",
2040 G_CALLBACK(leave_notify_event), NULL);
2041 g_signal_connect(G_OBJECT(drawing_area), "scroll_event",
2042 G_CALLBACK(scroll_event), NULL);
2043 g_signal_connect(G_OBJECT(drawing_area), "style-set",
2044 G_CALLBACK(cell_widget_update_colors), NULL);
2045 gtk_widget_set_events(drawing_area,
2047 GDK_BUTTON_PRESS_MASK |
2048 GDK_BUTTON_RELEASE_MASK |
2049 GDK_POINTER_MOTION_MASK |
2050 GDK_POINTER_MOTION_HINT_MASK |
2051 GDK_ENTER_NOTIFY_MASK |
2052 GDK_LEAVE_NOTIFY_MASK |
2056 cell_widget_update_colors();
2058 /* Create training menu */
2059 training_menu = gtk_menu_new();
2060 widget = gtk_menu_item_new_with_label("Reset");
2061 g_signal_connect(G_OBJECT(widget), "activate",
2062 G_CALLBACK(training_menu_reset), NULL);
2063 gtk_menu_attach(GTK_MENU(training_menu), widget, 0, 1, 0, 1);
2064 gtk_widget_show_all(training_menu);
2066 /* Create scroll bar */
2067 scrollbar = gtk_vscrollbar_new(NULL);
2068 gtk_widget_set_no_show_all(scrollbar, TRUE);
2069 g_signal_connect(G_OBJECT(scrollbar), "value-changed",
2070 G_CALLBACK(scrollbar_value_changed), NULL);
2071 g_signal_connect(G_OBJECT(scrollbar), "scroll_event",
2072 G_CALLBACK(scrollbar_scroll_event), NULL);
2073 gtk_widget_add_events(drawing_area, GDK_SCROLL_MASK);
2076 hbox = gtk_hbox_new(FALSE, 0);
2077 gtk_box_pack_start(GTK_BOX(hbox), drawing_area, TRUE, TRUE, 2);
2078 gtk_box_pack_start(GTK_BOX(hbox), scrollbar, FALSE, FALSE, 2);
2080 /* Create Pango font description
2081 FIXME font characteristics, not family */
2082 pango_font_desc = pango_font_description_new();
2083 pango_font_description_set_family(pango_font_desc, "Monospace");
2086 font_map = PANGO_CAIRO_FONT_MAP(pango_cairo_font_map_new());
2087 pango = pango_cairo_font_map_create_context(font_map);
2088 g_object_unref(font_map);
2091 cell_widget_clear();
2093 /* Set Xinput state */
2094 cell_widget_enable_xinput(xinput_enabled);
2097 memset(history, 0, sizeof (history));
2102 void cell_widget_cleanup(void)
2104 /* Freeing memory when closing is important when trying to sort
2105 legitimate memory leaks from left-over memory */
2107 g_object_unref(pixmap);
2109 g_object_unref(pixmap_gc);
2111 cairo_destroy(cairo);
2113 g_object_unref(pango);
2116 extern HildonIMUI *ui;
2118 void unicode_to_utf8(unsigned int code, char *out);
2119 void cell_widget_insert_surrounding_string(){
2122 gchar *str = malloc(cell_cols * cell_rows * 2 + 1);
2125 for(i = 0; i < cell_cols * cell_rows; i++){
2126 if(!(cells[i].flags & CELL_DIRTY)){
2127 unicode_to_utf8(cells[i].ch, s);
2132 hildon_im_ui_send_surrounding_content(ui, str);