diff options
Diffstat (limited to '')
| -rw-r--r-- | Makefile | 7 | ||||
| -rw-r--r-- | TODO | 35 | ||||
| -rw-r--r-- | char-table.c | 325 | ||||
| -rw-r--r-- | char-table.h | 14 | ||||
| -rw-r--r-- | characters.c | 80 | ||||
| -rw-r--r-- | common.h | 26 | ||||
| -rw-r--r-- | config.mk | 2 | ||||
| -rw-r--r-- | filtered-combo.c | 263 | ||||
| -rw-r--r-- | filtered-combo.h | 32 | ||||
| -rw-r--r-- | gcmap.c | 557 |
10 files changed, 1183 insertions, 158 deletions
@@ -5,13 +5,16 @@ include $(CONFIGFILE) OBJ =\ gcmap.o\ + characters.o\ char-table.o\ - logical-scroll-pane.o\ - colour.o + colour.o\ + filtered-combo.o\ + logical-scroll-pane.o HDR =\ common.h\ char-table.h\ + filtered-combo.h\ logical-scroll-pane.h all: gcmap @@ -1,9 +1,7 @@ -Middle-clicking should find the character stored in PRIMARY (SECONDARY if Mod1 is held down) - -Add drag-and-drop support, both in to find a character and out to copy a character - Add bookmarks expander under the tabbed pane +Display in magnification popup what font the glyph is rendered with + Mark whenever a fallback font is used for a glyph Mark reserved, non-printable, and unassigned characters @@ -19,15 +17,6 @@ Add font blacklist Add support for command line operands: select listed fonts by path -Add editor to font drop-down to make it easier to select a font - -Scrolling inside a GtkScrolledWindow or LogicalScrollPane shall steal focus (gtk_widget_grab_focus) - -Scrolling with Control held down shall change zoom. The hovered cell shall -remain visible, preferably at the same Y position as the pointer. The pointer -shall jump to the new position of the cell so account for it being impossible -to ensure that cell remains at the same X position as the pointer. - Under "Go" add option to go to the same character in: - All - Block @@ -38,3 +27,23 @@ Under "Go" add option to go to the same character in: Add command line options for opening into a specific character or codepoint Fix XXX's + +There are several minor problems with the font combo + +Certain categories of specials characters are not rendered, +such as those in the block Tags, even due it may be helpful + +Dragging above or below the char table with the right button shall scroll it; +the rate shall depend on the distance from the edge compared to the distance +to the edge of the monitor + +Add search feature + +When a grapheme composition is displayed in the status bar, +it should be possible to click on the codepoints to inspect +individual codepoints combined in the grapheme. Hovering +one of them ought also bring up a magnification that instead +of displaying the used front, displays the character name. + +Along with "Scripts" and "Unicode blocks" there should be +"Bookmarked scripts" and "Bookmarked blocks" diff --git a/char-table.c b/char-table.c index 2d877d9..ab3f73d 100644 --- a/char-table.c +++ b/char-table.c @@ -8,7 +8,12 @@ #define CELL_Y_CLEARANCE 2 #define MAGNIFICATION_WIDTH 200 #define MAGNIFICATION_HEIGHT 200 +#define DRAG_WIDTH 100 +#define DRAG_HEIGHT 100 +#define DRAG_X -16 +#define DRAG_Y -16 +#define TEXT_LIMIT 32u #define IS_SHIFT_KEY(KEYVAL) ((KEYVAL) == GDK_KEY_Shift_L || (KEYVAL) == GDK_KEY_Shift_R) #define SHIFT_KEY(KEYVAL) (1u << ((KEYVAL) == GDK_KEY_Shift_R)) @@ -42,26 +47,30 @@ G_DEFINE_TYPE(CharTable, gcmap_char_table, GTK_TYPE_DRAWING_AREA) static int -is_noncharacter(gunichar ch) +format_character(uint32_t cp, char *buf) { - return (0xFDD0u <= ch && ch <= 0xFDEFu) || (ch & 0xFFFEu) == 0xFFFEu; -} + int len = 0; + switch (classify_codepoint(cp)) { + case CP_COMBINING_MARK: +#if MARK_BASE + len += g_unichar_to_utf8(MARK_BASE, &buf[len]); +#endif + /* fall through */ -static int -is_printable_character(gunichar ch) -{ - return g_unichar_validate(ch) && - !g_unichar_iscntrl(ch) && - !g_unichar_iszerowidth(ch) && - !is_noncharacter(ch); -} + case CP_REGULAR: + case CP_RESERVED: + len += g_unichar_to_utf8((gunichar)cp, &buf[len]); + break; + case CP_CONTROL: + case CP_SURROGATE: + case CP_NONCHARACTER: + case CP_OUT_OF_RANGE: + default: + break; + } -static int -format_character(gunichar ch, char *buf) -{ - int len = is_printable_character(ch) ? g_unichar_to_utf8(ch, buf) : 0; buf[len] = '\0'; return len; } @@ -109,6 +118,22 @@ find_codepoint_index(CharTable *this, uint32_t codepoint, uint32_t *index_out) static void +execute_selection_callback(CharTable *this, uint32_t cell, int meta, int copy_to_text_field) +{ + uint32_t codepoint; + if (!this->selection_callback) + return; + codepoint = get_codepoint(this, cell); + if (copy_to_text_field) + (*this->selection_callback)(cell, codepoint, COPY_TO_TEXTFIELD); + else if (meta) + (*this->selection_callback)(cell, codepoint, COPY_TO_SECONDARY); + else + (*this->selection_callback)(cell, codepoint, COPY_TO_PRIMARY); +} + + +static void fit_text(CharTable *this, const char *text, int *text_w_out, int *text_h_out) { double size, ok_size = 1.0; @@ -187,7 +212,7 @@ gcmap_char_table_expose(GtkWidget *widget, GdkEventExpose *event) GdkRectangle area = event->area; GtkStyle *style = gtk_widget_get_style(widget); cairo_t *g; - char text[16]; + char text[TEXT_LIMIT]; int r, c, x0, x1, y0, y1, text_w, text_h; size_t skip, range, row_skip, max_jump, jump; uint32_t cp, index; @@ -274,7 +299,7 @@ gcmap_char_table_expose(GtkWidget *widget, GdkEventExpose *event) set_source_colour(g, &style->text[selectedState]); } - format_character((gunichar)cp, text); + format_character(cp, text); fit_text(this, text, &text_w, &text_h); cairo_move_to(g, (x0 + x1 - text_w) / 2, (y0 + y1 - text_h) / 2); pango_cairo_show_layout(g, this->layout); @@ -318,10 +343,10 @@ get_selected_cell_screen_rectangle(CharTable *this, int *x_out, int *y_out, int static int -fit_magnification(CharTable *this, const char *text, int text_len, struct glyph_layout *layout_out) +fit_magnification(CharTable *this, const char *text, int text_len, int width, int height, struct glyph_layout *layout_out) { - const int max_w = MAGNIFICATION_WIDTH - CELL_X_CLEARANCE * 2; - const int max_h = MAGNIFICATION_HEIGHT - CELL_Y_CLEARANCE * 2; + const int max_w = width - CELL_X_CLEARANCE * 2; + const int max_h = height - CELL_Y_CLEARANCE * 2; int font_size = this->font_size * PANGO_SCALE; PangoFontDescription *font; double xscale, yscale; @@ -377,35 +402,24 @@ fit_magnification(CharTable *this, const char *text, int text_len, struct glyph_ } -static gboolean -gcmap_char_table_magnification_expose(GtkWidget *widget, GdkEventExpose *event, void *user_data) +static void +draw_magnification(CharTable *this, cairo_t *g, int width, int height) { - CharTable *this = user_data; GtkStyle *style = gtk_widget_get_style(GTK_WIDGET(this)); - cairo_t *g; - char text[16]; + char text[TEXT_LIMIT]; int text_len; struct glyph_layout layout; - const double w = (double)MAGNIFICATION_WIDTH; - const double h = (double)MAGNIFICATION_HEIGHT; - - g = gdk_cairo_create(widget->window); - if (!g) { - weprintf("gdk_cairo_create:"); - return FALSE; - } - - text_len = format_character((gunichar)get_codepoint(this, this->selected), text); - if (fit_magnification(this, text, text_len, &layout) < 0) - return FALSE; + const double w = (double)width; + const double h = (double)height; - gdk_cairo_rectangle(g, &event->area); - cairo_clip(g); set_source_colour(g, &style->base[GTK_STATE_NORMAL]); cairo_paint(g); - if (!this->nchars) - goto out; + return; + + text_len = format_character(get_codepoint(this, this->selected), text); + if (fit_magnification(this, text, text_len, width, height, &layout) < 0) + return; set_source_colour_blend(g, &style->base[GTK_STATE_NORMAL], 0.05, &style->text[GTK_STATE_NORMAL]); cairo_set_line_width(g, 1.0); @@ -424,13 +438,26 @@ gcmap_char_table_magnification_expose(GtkWidget *widget, GdkEventExpose *event, set_source_colour(g, &style->text[GTK_STATE_NORMAL]); cairo_save(g); - cairo_translate(g, 0.5 * ((double)MAGNIFICATION_WIDTH - layout.width) + layout.x, - 0.5 * ((double)MAGNIFICATION_HEIGHT - layout.height) + layout.y); + cairo_translate(g, 0.5 * (w - layout.width) + layout.x, + 0.5 * (h - layout.height) + layout.y); cairo_scale(g, layout.scale, layout.scale); pango_cairo_show_layout(g, this->magnification_layout); cairo_restore(g); +} -out: + +static gboolean +gcmap_char_table_magnification_expose(GtkWidget *widget, GdkEventExpose *event, void *user_data) +{ + CharTable *this = user_data; + cairo_t *g = gdk_cairo_create(widget->window); + if (!g) { + weprintf("gdk_cairo_create:"); + return FALSE; + } + gdk_cairo_rectangle(g, &event->area); + cairo_clip(g); + draw_magnification(this, g, MAGNIFICATION_WIDTH, MAGNIFICATION_HEIGHT); cairo_destroy(g); return FALSE; } @@ -593,6 +620,44 @@ gcmap_char_table_size_request(GtkWidget *widget, GtkRequisition *requisition) static void +gcmap_char_table_drag_begin(GtkWidget *widget, GdkDragContext *context, void *user_data) +{ + CharTable *this = GCMAP_CHAR_TABLE(widget); + GdkPixmap *pixmap; + cairo_t *g; + uint32_t codepoint; + + (void) user_data; + + if (!this->nchars) + return; + + codepoint = get_codepoint(this, this->selected); + if (!is_valid_codepoint(codepoint)) + return; + + pixmap = gdk_pixmap_new(widget->window, DRAG_WIDTH, DRAG_HEIGHT, -1); + if (!pixmap) + return; + + g = gdk_cairo_create(pixmap); + if (!g) { + g_object_unref(pixmap); + return; + } + draw_magnification(this, g, DRAG_WIDTH, DRAG_HEIGHT); + cairo_destroy(g); + + gtk_drag_set_icon_pixmap(context, gtk_widget_get_colormap(widget), pixmap, NULL, DRAG_X, DRAG_Y); + g_object_unref(pixmap); + + this->shift_keys = 0u; + this->right_button_down = 0; + gcmap_char_table_update_magnification(this); +} + + +static int select_clicked_character(GtkWidget *widget, int meta, unsigned click_count, double x, double y) { CharTable *this = GCMAP_CHAR_TABLE(widget); @@ -606,12 +671,12 @@ select_clicked_character(GtkWidget *widget, int meta, unsigned click_count, doub selected = ((uint32_t)r + (uint32_t)r0) * (uint32_t)this->ncols + (uint32_t)c; if (selected >= this->nchars) { this->multiclick_count = 0u; + return 0; } else if (selected != this->selected) { click_count = 1u; clicked: this->selected = selected; - if (this->selection_callback) - (*this->selection_callback)(selected, get_codepoint(this, selected), meta, click_count >= 2u); + execute_selection_callback(this, selected, meta, click_count >= 2u); this->multiclick_count = click_count == 1u ? 1u : 0u; gcmap_char_table_update_magnification(this); gtk_widget_queue_draw(GTK_WIDGET(this)); /* XXX just redraw the affected cells */ @@ -620,6 +685,7 @@ select_clicked_character(GtkWidget *widget, int meta, unsigned click_count, doub } else { this->multiclick_count = 0u; } + return 1; } @@ -628,23 +694,33 @@ gcmap_char_table_button_press(GtkWidget *widget, GdkEventButton *event, void *us { CharTable *this = GCMAP_CHAR_TABLE(widget); unsigned click_count; + int meta, is_a_cell; (void) user_data; - gtk_widget_grab_focus(widget); - if (event->button == 1 || event->button == 3) { + switch (event->button) { + case 3: + this->right_button_down = 1; + /* fall through */ + case 1: + gtk_widget_grab_focus(widget); switch ((unsigned)event->type) { default: case GDK_BUTTON_PRESS: click_count = 1u; break; case GDK_2BUTTON_PRESS: click_count = 2u; break; case GDK_3BUTTON_PRESS: click_count = 3u; break; } - if (event->button == 3) - this->right_button_down = 1; - select_clicked_character(widget, !!(event->state & GDK_MOD1_MASK), click_count, event->x, event->y); + meta = !!(event->state & GDK_MOD1_MASK); + is_a_cell = select_clicked_character(widget, meta, click_count, event->x, event->y); gcmap_char_table_update_magnification(this); - } else { + if (is_a_cell && event->button == 1 && event->type == GDK_BUTTON_PRESS) { + this->drag_x = (int)event->x; + this->drag_y = (int)event->y; + this->drag_stage = 1; + } + return TRUE; + default: this->multiclick_count = 0u; + return event->button != 2; } - return TRUE; } @@ -653,6 +729,8 @@ gcmap_char_table_button_release(GtkWidget *widget, GdkEventButton *event, void * { CharTable *this = GCMAP_CHAR_TABLE(widget); (void) user_data; + if (event->button == 1) + this->drag_stage = 0; if (event->button != 3) return FALSE; this->right_button_down = 0; @@ -665,7 +743,37 @@ static gboolean gcmap_char_table_motion_notify(GtkWidget *widget, GdkEventMotion *event, void *user_data) { CharTable *this = GCMAP_CHAR_TABLE(widget); - (void) user_data; + uint32_t codepoint; + GdkDragContext *drag_ctx; + + this->pointer_cell = UINT32_MAX; + + if (this->drag_stage == 1 && (event->state & GDK_BUTTON1_MASK)) { + if (!gtk_drag_check_threshold(widget, this->drag_x, this->drag_y, (int)event->x, (int)event->y)) + goto not_starting_drag; + this->drag_stage = 2; + + if (!this->nchars) + goto cannot_drag; + + codepoint = get_codepoint(this, this->selected); + if (this->selection_callback) + (*this->selection_callback)(this->selected, codepoint, DRAG_ANNOUNCEMENT); + if (!is_valid_codepoint(codepoint)) + goto cannot_drag; + + drag_ctx = gtk_drag_begin(widget, this->drag_targets, GDK_ACTION_COPY, 1, (GdkEvent *)event); + if (!drag_ctx) + goto cannot_drag; + + gcmap_char_table_drag_begin(widget, drag_ctx, user_data); + return TRUE; + + cannot_drag: + this->drag_stage = 0; + } + +not_starting_drag: if (!this->right_button_down) return FALSE; if (!(event->state & GDK_BUTTON3_MASK)) { @@ -707,8 +815,7 @@ gcmap_char_table_key_press(GtkWidget *widget, GdkEventKey *event, void *user_dat case GDK_KEY_Return: case GDK_KEY_KP_Enter: case GDK_KEY_Begin: case GDK_KEY_KP_Begin: case GDK_KEY_space: - if (this->selection_callback) - (*this->selection_callback)(this->selected, get_codepoint(this, this->selected), meta, 1); + execute_selection_callback(this, this->selected, meta, 1); break; default: return FALSE; @@ -819,9 +926,34 @@ gcmap_char_table_parent_set(GtkWidget *widget, GtkWidget *old_parent, void *user static void +gcmap_char_table_drag_data_get(GtkWidget *widget, GdkDragContext *context, GtkSelectionData *selection_data, + unsigned int info, unsigned int time, void *user_data) +{ + CharTable *this = GCMAP_CHAR_TABLE(widget); + char text[TEXT_LIMIT]; + uint32_t codepoint; + int text_len; + (void) context; + (void) info; + (void) time; + (void) user_data; + if (!this->nchars) + return; + codepoint = get_codepoint(this, this->selected); + if (!is_valid_codepoint(codepoint)) + return; + text_len = g_unichar_to_utf8((gunichar)codepoint, text); + gtk_selection_data_set_text(selection_data, text, text_len); +} + + + +static void gcmap_char_table_init(CharTable *this) { + static char utf8_string[] = "UTF8_STRING"; GtkWidget *widget = GTK_WIDGET(this); + GtkTargetEntry drag_target = {utf8_string, 0, 0}; this->nrows = 1; this->ncols = 1; @@ -839,10 +971,14 @@ gcmap_char_table_init(CharTable *this) this->vadjustment_value_changed_id = 0u; this->shift_keys = 0u; this->right_button_down = 0; + this->pointer_cell = UINT32_MAX; this->viewport_w = 1; this->viewport_h = 1; this->snap_cols_to_2pow = 0; this->snap_rows_to_2pow = 0; + this->drag_x = 0; + this->drag_y = 0; + this->drag_stage = 0; this->multiclick_count = 0u; this->selection_callback = NULL; @@ -852,11 +988,17 @@ gcmap_char_table_init(CharTable *this) gtk_widget_add_events(widget, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | GDK_BUTTON3_MOTION_MASK | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_FOCUS_CHANGE_MASK); + this->drag_targets = gtk_target_list_new(&drag_target, 1u); + if (!this->drag_targets) + eprintf("gtk_target_list_new:"); + + g_signal_connect(widget, "drag-data-get", G_CALLBACK(&gcmap_char_table_drag_data_get), NULL); g_signal_connect(widget, "button-press-event", G_CALLBACK(&gcmap_char_table_button_press), NULL); g_signal_connect(widget, "button-release-event", G_CALLBACK(&gcmap_char_table_button_release), NULL); g_signal_connect(widget, "motion-notify-event", G_CALLBACK(&gcmap_char_table_motion_notify), NULL); @@ -879,6 +1021,8 @@ gcmap_char_table_dispose(GObject *object) } g_clear_object(&this->magnification_layout); g_clear_object(&this->layout); + gtk_target_list_unref(this->drag_targets); + this->drag_targets = NULL; G_OBJECT_CLASS(gcmap_char_table_parent_class)->dispose(object); } @@ -964,6 +1108,69 @@ gcmap_char_table_set_font_size(CharTable *this, int size) void +gcmap_char_table_set_font_size_at_pointer(CharTable *this, int size, double x, double y) +{ + int row, col, row0, px, py, prow, crow; + double value, upper, yadj; + uint32_t cell; + + if (!this->nchars || !this->vadjustment || this->font_size == MAX(size, 1)) { + gcmap_char_table_set_font_size(this, size); + return; + } + + px = (int)x; + py = (int)y; + + if (this->pointer_cell < this->nchars) { + cell = this->pointer_cell; + } else { + yadj = round(MAX(this->vadjustment->value, 0.0)); + row0 = (int)yadj; + + row = py * this->nrows / (this->viewport_h + 1); + col = px * this->ncols / (this->viewport_w + 1); + row = MAX(row, 0); + col = MAX(col, 0); + row = MIN(row, this->nrows - 1); + col = MIN(col, this->ncols - 1); + + cell = ((uint32_t)row + (uint32_t)row0) * (uint32_t)this->ncols + (uint32_t)col; + cell = MIN(cell, this->nchars - 1u); + this->pointer_cell = cell; + } + + gcmap_char_table_set_font_size(this, size); + + yadj = round(MAX(this->vadjustment->value, 0.0)); + row0 = (int)yadj; + crow = (int)(cell / (uint32_t)this->ncols) - row0; + crow = MAX(crow, 0); + prow = py * this->nrows / (this->viewport_h + 1); + prow = MAX(prow, 0); + prow = MIN(prow, this->nrows - 1); + row0 = crow - prow + row0; + + row = (int)(this->selected / (uint32_t)this->ncols); + if (row < row0) { + this->selected = (uint32_t)row0 * (uint32_t)this->ncols; + } else if (row >= row0 + this->nrows) { + this->selected = (uint32_t)row0 + (uint32_t)this->nrows; + this->selected *= (uint32_t)this->ncols; + if (this->selected) + this->selected -= 1u; + } + this->selected = MIN(this->selected, this->nchars); + + value = (double)row0; + upper = this->vadjustment->upper - this->vadjustment->page_size; + value = MAX(value, this->vadjustment->lower); + value = MIN(value, upper); + gtk_adjustment_set_value(this->vadjustment, value); +} + + +void gcmap_char_table_set_snap_cols_to_2pow(CharTable *this, int active) { GtkWidget *widget = GTK_WIDGET(this); @@ -1004,8 +1211,7 @@ gcmap_char_table_set_ranges(CharTable *this, const struct libcmap_range *ranges, widget = GTK_WIDGET(this); update_cell_size(widget, 1); update_content_size(this); - if (this->selection_callback) - (*this->selection_callback)(this->selected, get_codepoint(this, this->selected), 0, 0); + execute_selection_callback(this, this->selected, 0, 0); gcmap_char_table_update_magnification(this); gtk_widget_queue_draw(widget); } @@ -1103,8 +1309,7 @@ gcmap_char_table_select_cell(CharTable *this, int32_t selected, int clamp, int m } this->selected = (uint32_t)selected; - if (this->selection_callback) - (*this->selection_callback)(this->selected, get_codepoint(this, this->selected), meta, 0); + execute_selection_callback(this, this->selected, meta, 0); gcmap_char_table_update_magnification(this); if (r == r0) { diff --git a/char-table.h b/char-table.h index 0d41d17..e0c828f 100644 --- a/char-table.h +++ b/char-table.h @@ -10,6 +10,14 @@ #define GCMAP_CHAR_TABLE_GET_CLASS(OBJ) (G_TYPE_INSTANCE_GET_CLASS((OBJ), GCMAP_TYPE_CHAR_TABLE, CharTableClass)) +enum char_table_selection_type { + COPY_TO_TEXTFIELD, + COPY_TO_PRIMARY, + COPY_TO_SECONDARY, + DRAG_ANNOUNCEMENT +}; + + typedef struct _CharTable { GtkDrawingArea parent_instance; int nrows, ncols; @@ -26,15 +34,18 @@ typedef struct _CharTable { unsigned long int vadjustment_value_changed_id; unsigned shift_keys; int right_button_down; + uint32_t pointer_cell; int font_size; int snap_cols_to_2pow; int snap_rows_to_2pow; + int drag_x, drag_y, drag_stage; + GtkTargetList *drag_targets; unsigned multiclick_count; PangoWeight font_weight; PangoStyle font_style; const struct libcmap_range *ranges; size_t nranges; - void (*selection_callback)(uint32_t index, uint32_t codepoint, int meta, int copy_to_text_field); + void (*selection_callback)(uint32_t index, uint32_t codepoint, enum char_table_selection_type selection_type); } CharTable; typedef struct _CharTableClass { @@ -49,6 +60,7 @@ void gcmap_char_table_set_font_family(CharTable *this, const char *family); void gcmap_char_table_set_font_weight(CharTable *this, PangoWeight weight); void gcmap_char_table_set_font_style(CharTable *this, PangoStyle style); void gcmap_char_table_set_font_size(CharTable *this, int size); +void gcmap_char_table_set_font_size_at_pointer(CharTable *this, int size, double x, double y); void gcmap_char_table_set_snap_cols_to_2pow(CharTable *this, int active); void gcmap_char_table_set_snap_rows_to_2pow(CharTable *this, int active); void gcmap_char_table_set_ranges(CharTable *this, const struct libcmap_range *ranges, size_t nranges); diff --git a/characters.c b/characters.c new file mode 100644 index 0000000..5679918 --- /dev/null +++ b/characters.c @@ -0,0 +1,80 @@ +/* See LICENSE file for copyright and license details. */ +#include "common.h" + + +int +is_noncharacter(uint32_t cp) +{ + return (0xFDD0u <= cp && cp <= 0xFDEFu) || (cp & 0xFFFEu) == 0xFFFEu; +} + + +int +is_surrogate(uint32_t cp) +{ + return 0xD800u <= cp && cp <= 0xDFFFu; +} + + +int +is_valid_codepoint(uint32_t cp) +{ + return cp <= LIBCMAP_ULTIMATE_CODEPOINT && !is_noncharacter(cp) && !is_surrogate(cp); +} + + +struct libcmap_codepoint_data * +get_codepoint_data(uint32_t cp) +{ + static char *databuf = NULL; + static size_t databufsize = 0u; + static struct libcmap_codepoint_data data; + + int r; + +again: + r = libcmap_get_codepoint_data(cp, &data, databuf, &databufsize); + if (r <= 0) { + if (!r || errno != ERANGE || !databufsize) + return NULL; + databuf = erealloc(databuf, databufsize); + goto again; + } + + return &data; +} + + +enum codepoint_class +classify_from_codepoint_data(const struct libcmap_codepoint_data *data) +{ + if (!data) + return CP_OUT_OF_RANGE; + + switch ((int)(data->general_category & LIBCMAP_GENERAL_SUPERCATEGORY_MASK)) { + case LIBCMAP_GENERAL_SUPERCATEGORY_MARK: + return CP_COMBINING_MARK; + default: + break; + } + + switch ((int)data->general_category) { + case LIBCMAP_GENERAL_CATEGORY_OTHER_CONTROL: + return CP_CONTROL; + case LIBCMAP_GENERAL_CATEGORY_OTHER_SURROGATE: + return CP_SURROGATE; + case LIBCMAP_GENERAL_CATEGORY_OTHER_NOT_ASSIGNED: + return data->noncharacter ? CP_NONCHARACTER : CP_RESERVED; + default: + break; + } + + return CP_REGULAR; +} + + +enum codepoint_class +classify_codepoint(uint32_t cp) +{ + return classify_from_codepoint_data(get_codepoint_data(cp)); +} @@ -9,6 +9,7 @@ #include <libsimple.h> #include <libsimple-arg.h> #include <libcmap.h> +#include <grapheme.h> #if defined(__GNUC__) # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wdeprecated-declarations" @@ -27,15 +28,40 @@ #if defined(__GNUC__) # define PURE __attribute__((__pure__)) +# define CONST __attribute__((__const__)) #else # define PURE +# define CONST #endif #define COMMA , #include "char-table.h" +#include "filtered-combo.h" #include "logical-scroll-pane.h" + +#define MARK_BASE 0x20u /* 0x25CCu DOTTED CIRCLE || XXX make the a setting */ + +enum codepoint_class { + CP_OUT_OF_RANGE, + CP_CONTROL, + CP_SURROGATE, + CP_NONCHARACTER, + CP_RESERVED, /* includes not yet assigned */ + CP_COMBINING_MARK, + CP_REGULAR, +}; + + +/* characters.c */ +CONST int is_noncharacter(uint32_t cp); +CONST int is_surrogate(uint32_t cp); +CONST int is_valid_codepoint(uint32_t cp); +struct libcmap_codepoint_data *get_codepoint_data(uint32_t cp); +PURE enum codepoint_class classify_from_codepoint_data(const struct libcmap_codepoint_data *data); +enum codepoint_class classify_codepoint(uint32_t cp); + /* colour.c */ double srgb_encode_unsigned(double t); double srgb_decode_unsigned(double t); @@ -5,7 +5,7 @@ CC = c99 CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_GNU_SOURCE CFLAGS = $$(pkg-config --cflags pango) -LDFLAGS = -lm -lsimple -lcmap $$(pkg-config --libs pango) +LDFLAGS = -lm -lsimple -lcmap -lgrapheme $$(pkg-config --libs pango) CFLAGS_GTK2 = $$(pkg-config --cflags gtk+-2.0) LDFLAGS_GTK2 = $$(pkg-config --libs gtk+-2.0) diff --git a/filtered-combo.c b/filtered-combo.c new file mode 100644 index 0000000..a15d939 --- /dev/null +++ b/filtered-combo.c @@ -0,0 +1,263 @@ +#include "common.h" + +G_DEFINE_TYPE(FilteredCombo, filtered_combo, GTK_TYPE_HBOX) + + +enum { + FILTERED_COMBO_CHANGED, + LAST_SIGNAL +}; + + +static unsigned int signals[LAST_SIGNAL]; + + +static int +filtered_combo_matches(const char *value, const char *query) +{ + return !!libsimple_strcasestr(value, query); +} + + +static void +filtered_combo_set_selected(FilteredCombo *this, const char *value) +{ + free(this->selected); + this->selected = estrdup(value); +} + + +static void +filtered_combo_notify_changed(FilteredCombo *this) +{ + if (this->selected) + g_signal_emit(this, signals[FILTERED_COMBO_CHANGED], 0, this->selected); +} + + +static gboolean +filtered_combo_completion_match(GtkEntryCompletion *completion, const char *query, GtkTreeIter *iter, void *user_data) +{ + GtkTreeModel *model = gtk_entry_completion_get_model(completion); + char *value; + int matches; + (void) user_data; + gtk_tree_model_get(model, iter, 0, &value, -1); + if (!value) + return FALSE; + matches = filtered_combo_matches(value, query); + g_free(value); + return matches; +} + + +static int +filtered_combo_select_first(FilteredCombo *this, const char *query) +{ + GtkTreeIter iter; + char *value; + if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(this->store), &iter)) + return 0; + do { + gtk_tree_model_get(GTK_TREE_MODEL(this->store), &iter, 0, &value, -1); + if (value && filtered_combo_matches(value, query)) { + filtered_combo_set_selected(this, value); + g_free(value); + return 1; + } + g_free(value); + } while (gtk_tree_model_iter_next(GTK_TREE_MODEL(this->store), &iter)); + free(this->selected); + this->selected = NULL; + return 0; +} + + +static void +filtered_combo_changed(GtkComboBox *combo, FilteredCombo *this) +{ + GtkTreeIter iter; + char *value; + if (!gtk_combo_box_get_active_iter(combo, &iter)) + return; + gtk_tree_model_get(GTK_TREE_MODEL(this->store), &iter, 0, &value, -1); + if (!value) + return; + filtered_combo_set_selected(this, value); + filtered_combo_notify_changed(this); + g_free(value); +} + + +static void +filtered_combo_entry_changed(GtkEditable *editable, FilteredCombo *this) +{ + const char *query = gtk_entry_get_text(GTK_ENTRY(editable)); + int has_match = filtered_combo_select_first(this, query); + gtk_entry_completion_set_popup_completion(this->completion, has_match); + gtk_entry_completion_complete(this->completion); +} + + +static gboolean +filtered_combo_entry_key_press(GtkWidget *widget, GdkEventKey *event, FilteredCombo *this) +{ + (void) widget; + if (event->keyval == GDK_Up || event->keyval == GDK_Down) { + gtk_entry_completion_complete(this->completion); + return FALSE; + } + return FALSE; +} + + +static gboolean +filtered_combo_completion_cursor_on_match(GtkEntryCompletion *completion, GtkTreeModel *model, + GtkTreeIter *iter, FilteredCombo *this) +{ + char *value; + (void) completion; + gtk_tree_model_get(model, iter, 0, &value, -1); + if (value) { + filtered_combo_set_selected(this, value); + g_free(value); + } + return FALSE; +} + + +static gboolean +filtered_combo_completion_match_selected(GtkEntryCompletion *completion, GtkTreeModel *model, + GtkTreeIter *iter, FilteredCombo *this) +{ + char *value; + (void) completion; + gtk_tree_model_get(model, iter, 0, &value, -1); + if (value) { + filtered_combo_set_selected(this, value); + filtered_combo_notify_changed(this); + g_free(value); + } + return FALSE; +} + + +static void +filtered_combo_entry_accept(GtkEntry *entry, FilteredCombo *this) +{ + (void) entry; + filtered_combo_notify_changed(this); +} + + +static gboolean +filtered_combo_entry_focus_out(GtkWidget *widget, GdkEventFocus *event, FilteredCombo *this) +{ + (void) event; + filtered_combo_entry_accept(GTK_ENTRY(widget), this); + return FALSE; +} + + +static gboolean +filtered_combo_entry_focus_in(GtkWidget *widget, GdkEventFocus *event, FilteredCombo *this) +{ + (void) event; + (void) this; + gtk_editable_set_position(GTK_EDITABLE(widget), -1); + return FALSE; +} + + +static void +filtered_combo_destroy(GtkObject *this_) +{ + FilteredCombo *this = FILTERED_COMBO(this_); + if (this->reposition_source) + g_source_remove(this->reposition_source); + this->reposition_source = 0; + free(this->selected); + this->selected = NULL; + GTK_OBJECT_CLASS(filtered_combo_parent_class)->destroy(this_); +} + + +static void +filtered_combo_class_init(FilteredComboClass *class) +{ + GtkObjectClass *object_class = GTK_OBJECT_CLASS(class); + object_class->destroy = filtered_combo_destroy; + signals[FILTERED_COMBO_CHANGED] = g_signal_new("changed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, + 0, NULL, NULL, g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, G_TYPE_STRING); +} + + +static void +filtered_combo_init(FilteredCombo *this) +{ + GtkCellRenderer *width_renderer; + + this->store = gtk_list_store_new(1, G_TYPE_STRING); + this->combo = gtk_combo_box_entry_new_with_model(GTK_TREE_MODEL(this->store), 0); + this->entry = gtk_bin_get_child(GTK_BIN(this->combo)); + this->completion = gtk_entry_completion_new(); + + gtk_entry_completion_set_model(this->completion, GTK_TREE_MODEL(this->store)); + gtk_entry_completion_set_match_func(this->completion, &filtered_combo_completion_match, this, NULL); + gtk_entry_completion_set_minimum_key_length(this->completion, 0); + gtk_entry_completion_set_text_column(this->completion, 0); + width_renderer = gtk_cell_renderer_text_new(); + gtk_cell_renderer_set_padding(width_renderer, 0, 0); + gtk_cell_renderer_set_fixed_size(width_renderer, 48, -1); + gtk_cell_layout_pack_end(GTK_CELL_LAYOUT(this->completion), width_renderer, FALSE); + gtk_entry_completion_set_inline_completion(this->completion, FALSE); + gtk_entry_completion_set_inline_selection(this->completion, FALSE); + gtk_entry_completion_set_popup_completion(this->completion, TRUE); + gtk_entry_completion_set_popup_set_width(this->completion, FALSE); + gtk_entry_completion_set_popup_single_match(this->completion, TRUE); + + gtk_entry_set_completion(GTK_ENTRY(this->entry), this->completion); + gtk_combo_box_set_focus_on_click(GTK_COMBO_BOX(this->combo), TRUE); + gtk_box_pack_start(GTK_BOX(this), this->combo, FALSE, FALSE, 0); + + g_signal_connect(this->combo, "changed", G_CALLBACK(&filtered_combo_changed), this); + g_signal_connect(this->entry, "changed", G_CALLBACK(&filtered_combo_entry_changed), this); + g_signal_connect(this->entry, "key-press-event", G_CALLBACK(&filtered_combo_entry_key_press), this); + g_signal_connect(this->entry, "activate", G_CALLBACK(&filtered_combo_entry_accept), this); + g_signal_connect(this->entry, "focus-out-event", G_CALLBACK(&filtered_combo_entry_focus_out), this); + g_signal_connect_after(this->entry, "focus-in-event", G_CALLBACK(&filtered_combo_entry_focus_in), this); + g_signal_connect(this->completion, "cursor-on-match", G_CALLBACK(&filtered_combo_completion_cursor_on_match), this); + g_signal_connect(this->completion, "match-selected", G_CALLBACK(&filtered_combo_completion_match_selected), this); +} + + +GtkWidget * +filtered_combo_new(void) +{ + return g_object_new(FILTERED_TYPE_COMBO, NULL); +} + + +void +filtered_combo_append(FilteredCombo *this, const char *value) +{ + GtkTreeIter iter; + gtk_list_store_append(this->store, &iter); + gtk_list_store_set(this->store, &iter, 0, value, -1); +} + + +void +filtered_combo_set_active(FilteredCombo *this, int index) +{ + gtk_combo_box_set_active(GTK_COMBO_BOX(this->combo), index); + gtk_editable_set_position(GTK_EDITABLE(this->entry), -1); +} + + +const char * +filtered_combo_get_selected(FilteredCombo *this) +{ + return this->selected; +} diff --git a/filtered-combo.h b/filtered-combo.h new file mode 100644 index 0000000..7e604c7 --- /dev/null +++ b/filtered-combo.h @@ -0,0 +1,32 @@ +#include <gtk/gtk.h> + +#define FILTERED_TYPE_COMBO (filtered_combo_get_type()) +#define FILTERED_COMBO(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), FILTERED_TYPE_COMBO, FilteredCombo)) +#define FILTERED_IS_COMBO(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), FILTERED_TYPE_COMBO)) + +typedef struct _FilteredCombo FilteredCombo; +typedef struct _FilteredComboClass FilteredComboClass; + +struct _FilteredCombo { + GtkHBox parent_instance; + GtkWidget *combo; + GtkWidget *entry; + GtkListStore *store; + GtkEntryCompletion *completion; + char *selected; + GtkWidget *popup; + unsigned int reposition_source; + int popup_x; + int popup_y; + int popup_width; +}; + +struct _FilteredComboClass { + GtkHBoxClass parent_class; +}; + +GType filtered_combo_get_type(void); +GtkWidget *filtered_combo_new(void); +void filtered_combo_append(FilteredCombo *this, const char *value); +void filtered_combo_set_active(FilteredCombo *this, int index); +PURE const char *filtered_combo_get_selected(FilteredCombo *this); @@ -47,9 +47,13 @@ static GtkWidget *vtabtext; static GtkWidget *snap2_cols; static GtkWidget *snap2_rows; static GtkWidget *chartable; -static GtkListStore *fontcombo_store; +static GtkWidget *details; static GtkAdjustment *fontsize_adjustment; static GtkAccelGroup *accelgroup; +static GtkTextBuffer *details_buffer; +static GtkTextTag *tag_display; +static GtkTextTag *tag_name; +static GtkTextTag *tag_header; static uint32_t selected_codepoint = 0u; static int switching_groups = 0; @@ -66,7 +70,7 @@ static size_t ngroups_tooltips; static size_t format_statusbar_message(char *buffer, size_t size) { - const struct libcmap_codepoint_data *data; + struct libcmap_codepoint_data *data; int r; size_t len = 0u; @@ -77,7 +81,7 @@ format_statusbar_message(char *buffer, size_t size) buffer = &buffer[MIN(size, (size_t)r)]; size -= MIN(size, (size_t)r); - data = libcmap_find_codepoint_data(selected_codepoint); + data = get_codepoint_data(selected_codepoint); if (!data) return len; @@ -104,8 +108,186 @@ update_statusbar(void) } +static const char * +format_utf8_hex(char *buf, uint32_t cp, const char *prefix, const char *suffix) +{ + char utf8[8]; + int r, i, len = g_unichar_to_utf8((gunichar)cp, utf8); + char *p = stpcpy(buf, prefix); + for (i = 0; i < len; i++) { + if (i) + *p++ = ' '; + r = sprintf(p, "%02X", (unsigned char)utf8[i] & 0xFFu); + if (r < 0) + eprintf("sprintf:"); + p += r; + } + stpcpy(p, suffix); + return buf; +} + + +static const char * +format_utf8_hexesc(char *buf, uint32_t cp, const char *prefix, const char *suffix) +{ + char utf8[8]; + int r, i, len = g_unichar_to_utf8((gunichar)cp, utf8); + char *p = stpcpy(buf, prefix); + for (i = 0; i < len; i++) { + r = sprintf(p, "\\x%02X", (unsigned char)utf8[i] & 0xFFu); + if (r < 0) + eprintf("sprintf:"); + p += r; + } + stpcpy(p, suffix); + return buf; +} + + +static const char * +format_utf8_octesc(char *buf, uint32_t cp, const char *prefix, const char *suffix) +{ + char utf8[8]; + int r, i, len = g_unichar_to_utf8((gunichar)cp, utf8); + char *p = stpcpy(buf, prefix); + for (i = 0; i < len; i++) { + r = sprintf(p, "\\%03o", (unsigned char)utf8[i] & 0xFFu); + if (r < 0) + eprintf("sprintf:"); + p += r; + } + stpcpy(p, suffix); + return buf; +} + + +static const char * +format_utf16_hex(char *buf, uint32_t cp, const char *prefix, const char *suffix) +{ + char *p = stpcpy(buf, prefix); + int r; + if (cp <= UINT32_C(0xFFFF)) { + r = sprintf(p, "%04"PRIX32, cp); + } else { + cp -= UINT32_C(0x10000); + r = sprintf(p, "%04"PRIX32" %04"PRIX32, + (uint32_t)((cp >> 10) + 0xD800u), + (uint32_t)((cp & 0x3FFu) + 0xDC00u)); + } + if (r < 0) + eprintf("sprintf:"); + p += r; + stpcpy(p, suffix); + return buf; +} + + +static const char * +format_xml_dec(char *buf, uint32_t cp, const char *prefix, const char *suffix) +{ + char *p = stpcpy(stpcpy(buf, prefix), "&#"); + int r = sprintf(p, "%"PRIu32, cp); + if (r < 0) + eprintf("sprintf:"); + p += r; + stpcpy(stpcpy(p, ";"), suffix); + return buf; +} + + static void -character_cell_selected(uint32_t index, uint32_t codepoint, int meta, int copy_to_text_field) +update_details(void) +{ + struct libcmap_codepoint_data *data; + GtkTextIter itr, itr_end, itr0; + GtkTextMark *mark; + char buf[128u]; + int len; + + /* XXX this should be defered to when the tab is open (being opened if it is not already opened) */ + + gtk_text_buffer_get_start_iter(details_buffer, &itr); + gtk_text_buffer_get_end_iter(details_buffer, &itr_end); + + gtk_text_buffer_delete(details_buffer, &itr, &itr_end); + + data = get_codepoint_data(selected_codepoint); + if (!data) + return; + + len = 0; + len = (int)(stpcpy(&buf[len], "\xe2\x80\xad" /* LEFT-TO-RIGHT OVERRIDE */ ) - buf); + switch (classify_from_codepoint_data(data)) { + case CP_COMBINING_MARK: +#if MARK_BASE + len += g_unichar_to_utf8(MARK_BASE, &buf[len]); +#endif + /* fall through */ + + case CP_REGULAR: + case CP_RESERVED: + len += g_unichar_to_utf8((gunichar)selected_codepoint, &buf[len]); + len = (int)(stpcpy(&buf[len], "\xe2\x80\xac" /* POP DIRECTIONAL FORMATTING */) - buf); + buf[len++] = '\n'; + buf[len] = '\0'; + gtk_text_buffer_insert_with_tags(details_buffer, &itr, buf, -1, tag_display, NULL); + break; + + case CP_CONTROL: + case CP_SURROGATE: + case CP_NONCHARACTER: + case CP_OUT_OF_RANGE: + default: + gtk_text_buffer_insert(details_buffer, &itr, "[non-printable character]\n", -1); + break; + } + gtk_text_buffer_insert(details_buffer, &itr, "\n", -1); + + mark = gtk_text_buffer_create_mark(details_buffer, NULL, &itr, TRUE); + len = 0; + buf[len++] = 'U'; + buf[len++] = '+'; + if (selected_codepoint >> 20) buf[len++] = "0123456789ABCDEF"[(selected_codepoint >> 20) & 15u]; + if (selected_codepoint >> 16) buf[len++] = "0123456789ABCDEF"[(selected_codepoint >> 16) & 15u]; + buf[len++] = "0123456789ABCDEF"[(selected_codepoint >> 12) & 15u]; + buf[len++] = "0123456789ABCDEF"[(selected_codepoint >> 8) & 15u]; + buf[len++] = "0123456789ABCDEF"[(selected_codepoint >> 4) & 15u]; + buf[len++] = "0123456789ABCDEF"[(selected_codepoint >> 0) & 15u]; + buf[len++] = ' '; + gtk_text_buffer_insert(details_buffer, &itr, buf, len); + gtk_text_buffer_insert(details_buffer, &itr, data->name, -1); + gtk_text_buffer_insert(details_buffer, &itr, "\n", -1); + gtk_text_buffer_get_iter_at_mark(details_buffer, &itr0, mark); + gtk_text_buffer_apply_tag(details_buffer, tag_name, &itr0, &itr); + gtk_text_buffer_delete_mark(details_buffer, mark); + + gtk_text_buffer_insert(details_buffer, &itr, "\n", -1); + + gtk_text_buffer_insert_with_tags_by_name(details_buffer, &itr, "General character properties\n", -1, "header", NULL); + gtk_text_buffer_insert(details_buffer, &itr, "\n", -1); + gtk_text_buffer_insert(details_buffer, &itr, "TODO\n", -1); + + /* TODO list block and scripts */ + + if (is_surrogate(selected_codepoint)) + return; + + gtk_text_buffer_insert(details_buffer, &itr, "\n", -1); + gtk_text_buffer_insert_with_tags_by_name(details_buffer, &itr, "Various encodings\n", -1, "header", NULL); + gtk_text_buffer_insert(details_buffer, &itr, "\n", -1); + gtk_text_buffer_insert(details_buffer, &itr, "UTF-8:\n", -1); + gtk_text_buffer_insert(details_buffer, &itr, format_utf8_hex(buf, selected_codepoint, "\t", "\n"), -1); + gtk_text_buffer_insert(details_buffer, &itr, format_utf8_hexesc(buf, selected_codepoint, "\t", "\n"), -1); + gtk_text_buffer_insert(details_buffer, &itr, format_utf8_octesc(buf, selected_codepoint, "\t", "\n"), -1); + gtk_text_buffer_insert(details_buffer, &itr, "\nUTF-16:\n", -1); + gtk_text_buffer_insert(details_buffer, &itr, format_utf16_hex(buf, selected_codepoint, "\t", "\n"), -1); + gtk_text_buffer_insert(details_buffer, &itr, "\nXML:\n", -1); + gtk_text_buffer_insert(details_buffer, &itr, format_xml_dec(buf, selected_codepoint, "\t", "\n"), -1); +} + + +static void +character_cell_selected(uint32_t index, uint32_t codepoint, enum char_table_selection_type selection_type) { GtkEditable *editable; char text[16]; @@ -113,38 +295,247 @@ character_cell_selected(uint32_t index, uint32_t codepoint, int meta, int copy_t (void) index; - text[len = g_unichar_to_utf8(codepoint, text)] = '\0'; selected_codepoint = codepoint; - if (copy_to_text_field) { + if (!is_valid_codepoint(codepoint)) { + switch (selection_type) { + case COPY_TO_TEXTFIELD: + case COPY_TO_SECONDARY: + case DRAG_ANNOUNCEMENT: + gtk_statusbar_pop(GTK_STATUSBAR(statusbar), 0u); + gtk_statusbar_push(GTK_STATUSBAR(statusbar), 0u, "Cannot copy non-character codepoint"); + return; + default: + case COPY_TO_PRIMARY: + goto out; + } + } + + text[len = g_unichar_to_utf8(codepoint, text)] = '\0'; + + switch (selection_type) { + case COPY_TO_TEXTFIELD: editable = GTK_EDITABLE(copytext); pos = gtk_editable_get_position(editable); gtk_editable_insert_text(editable, text, len, &pos); gtk_editable_set_position(editable, pos); - } else if (meta) { - gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_SECONDARY), text, len); - /* XXX add setting to swap SECONDARY and PRIMARY */ - } else { + break; + case COPY_TO_PRIMARY: gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_PRIMARY), text, len); /* XXX add setting to disable */ + break; + case COPY_TO_SECONDARY: + gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_SECONDARY), text, len); + /* XXX add setting to swap SECONDARY and PRIMARY */ + break; + case DRAG_ANNOUNCEMENT: + return; } +out: update_statusbar(); + update_details(); +} + + +static void +select_group(int index) +{ + GtkTreeView *treeview = GTK_TREE_VIEW(grouplist); + GtkTreePath *path = gtk_tree_path_new_from_indices(index, -1); + GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview); + gtk_tree_selection_select_path(selection, path); + gtk_tree_view_set_cursor(treeview, path, NULL, FALSE); + gtk_tree_path_free(path); +} + + +static void +select_group_for_codepoint(enum listing listing, uint32_t codepoint, int fallback_to_all) +{ + const struct libcmap_block *block; + const struct libcmap_script *script; + switch (listing) { + case BY_BLOCK: + block = libcmap_find_block(codepoint, NULL); + select_group(block ? (int)(block - libcmap_block_list) + 1 : + fallback_to_all ? 0 : (int)libcmap_block_list_size + 1); + break; + case BY_SCRIPT: + script = libcmap_find_script(codepoint, NULL, NULL); + select_group(script ? (int)(script - libcmap_script_list) + 1 : + fallback_to_all ? 0 : (int)libcmap_script_list_size + 1); + break; + default: + abort(); + } } static int -populate_font_families(GtkWidget *widget, GtkListStore *store, const char **selectedp) +get_codepoint(const char **textp, uint32_t *codepoint_out) +{ + static const uint32_t limits[] = { + (UINT32_C(1) << 0) - 1u, + (UINT32_C(1) << 7) - 1u, + (UINT32_C(1) << 11) - 1u, + (UINT32_C(1) << 16) - 1u, + UINT32_C(0x10FFFF) + }; + + const unsigned char *text = (const unsigned char *)*textp; + uint32_t lower_limit, upper_limit; + unsigned char c; + size_t n; + + c = *text++; + + n = 0u; + while (c & 0x80u) { + c <<= 1; + n += 1u; + } + + if (n == 0u) { + *codepoint_out = (uint32_t)c; + goto out; + } + if (n == 1u || n >= ELEMSOF(limits)) + return 0; + + lower_limit = limits[n - 1u] + 1u; + upper_limit = limits[n]; + + *codepoint_out = (uint32_t)((c & 0xFFu) >> n); + + while (--n) { + c = *text++; + if ((c & 0xC0u) != 0x80u) + return 0; + *codepoint_out <<= 6; + *codepoint_out |= (uint32_t)(c & 0x7Fu); + } + + if (*codepoint_out < lower_limit || *codepoint_out > upper_limit) + return 0; + +out: + *textp = (const char *)text; + return 1; +} + + +static int +jump_to_character(const char *text) +{ +#define TEXT_HEAD "Grapheme composition:" +#define CP_HEAD " U+" +#define ELLIPSIS " …" + + uint32_t codepoints[32]; + char *p, composition[sizeof(TEXT_HEAD ELLIPSIS) + ELEMSOF(codepoints) * (sizeof(CP_HEAD"10FFFF") - 1u)]; + size_t i, n = 0u; + int r, listing, ellipsis; + + if (!text || !*text) + return 0; + + if (text[grapheme_next_character_break_utf8(text, SIZE_MAX)]) + return 0; + + for (; *text && n < ELEMSOF(codepoints); n++) + if (!get_codepoint(&text, &codepoints[n])) + return 0; + if (!n) + return 0; + ellipsis = !!*text; + + if (gcmap_char_table_select_codepoint(GCMAP_CHAR_TABLE(chartable), codepoints[0])) + goto out; + listing = gtk_combo_box_get_active(GTK_COMBO_BOX(listcombo)); + select_group_for_codepoint((enum listing)listing, codepoints[0], 0); + gcmap_char_table_select_codepoint(GCMAP_CHAR_TABLE(chartable), codepoints[0]); + +out: + if (n > 1u) { + p = stpcpy(composition, TEXT_HEAD); + for (i = 0u; i < n; i++) { + r = sprintf(p, "%s%04"PRIX32, CP_HEAD, codepoints[i]); + if (r < 0) { + weprintf("sprintf:"); + return 1; + } + p = &p[r]; + } + if (ellipsis) + stpcpy(composition, ELLIPSIS); + gtk_statusbar_pop(GTK_STATUSBAR(statusbar), 0u); + gtk_statusbar_push(GTK_STATUSBAR(statusbar), 0u, composition); + } + + return 1; + +#undef TEXT_HEAD +#undef CP_HEAD +#undef ELLIPSIS +} + + +static void +chartable_drag_data_received(GtkWidget *widget, GdkDragContext *context, int x, int y, + GtkSelectionData *selection_data, unsigned int info, unsigned int time, void *user_data) +{ + unsigned char *text = gtk_selection_data_get_text(selection_data); + int success = text != NULL; + (void) widget; + (void) x; + (void) y; + (void) info; + (void) user_data; + if (text) { + success = jump_to_character((char *)text); + g_free(text); + } + gtk_drag_finish(context, success, FALSE, time); +} + + +static void +chartable_clipboard_text_received(GtkClipboard *clipboard, const char *text, void *user_data) +{ + (void) clipboard; + (void) user_data; + if (text) + jump_to_character(text); +} + + +static gboolean +chartable_button_press(GtkWidget *widget, GdkEventButton *event, void *user_data) +{ + int meta = !!(event->state & GDK_MOD1_MASK); + GdkAtom selection = meta ? GDK_SELECTION_SECONDARY : GDK_SELECTION_PRIMARY; + (void) user_data; + if (event->button == 2 && event->type == GDK_BUTTON_PRESS) { + gtk_widget_grab_focus(widget); + gtk_clipboard_request_text(gtk_clipboard_get(selection), &chartable_clipboard_text_received, widget); + return TRUE; + } + return FALSE; +} + + +static int +populate_font_families(FilteredCombo *combo, const char **selectedp) { const char **names; PangoFontFamily **families; int nfamilies_int; size_t i, nfamilies; - GtkTreeIter iter; size_t selected_index = SIZE_MAX; size_t default_index = SIZE_MAX; - pango_context_list_families(gtk_widget_get_pango_context(GTK_WIDGET(widget)), &families, &nfamilies_int); + pango_context_list_families(gtk_widget_get_pango_context(GTK_WIDGET(combo)), &families, &nfamilies_int); nfamilies = (size_t)nfamilies_int; if (nfamilies == 0u) eprintf("no fonts available"); @@ -153,9 +544,8 @@ populate_font_families(GtkWidget *widget, GtkListStore *store, const char **sele for (i = 0u; i < nfamilies; i++) names[i] = pango_font_family_get_name(families[i]); libsimple_qsort_str(names, nfamilies); - for (i = 0; i < nfamilies; i++) { - gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, 0, names[i], -1); + for (i = 0u; i < nfamilies; i++) { + filtered_combo_append(combo, names[i]); if (selected_index == SIZE_MAX && !strcasecmp(names[i], *selectedp)) selected_index = i; if (default_index == SIZE_MAX && !strcasecmp(names[i], DEFAULT_FONT_FAMILY)) @@ -175,18 +565,6 @@ populate_font_families(GtkWidget *widget, GtkListStore *store, const char **sele } -static void -select_group(int index) -{ - GtkTreeView *treeview = GTK_TREE_VIEW(grouplist); - GtkTreePath *path = gtk_tree_path_new_from_indices(index, -1); - GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview); - gtk_tree_selection_select_path(selection, path); - gtk_tree_view_set_cursor(treeview, path, NULL, FALSE); - gtk_tree_path_free(path); -} - - #ifdef HAVE_ITEM_TOOLTIPS @@ -431,16 +809,10 @@ copybutton_clicked(GtkWidget *button, GtkEditable *editable) static void -fontcombo_changed(GtkComboBox *combo, void *user_data) +fontcombo_changed(FilteredCombo *combo, const char *family, void *user_data) { - GtkTreeIter iter; - char *family; + (void) combo; (void) user_data; - if (!gtk_combo_box_get_active_iter(combo, &iter)) - return; - gtk_tree_model_get(GTK_TREE_MODEL(fontcombo_store), &iter, 0, &family, -1); - if (!family) - return; gcmap_char_table_set_font_family(GCMAP_CHAR_TABLE(chartable), family); } @@ -474,6 +846,48 @@ fontsize_changed(GtkAdjustment *adjustment, void *user_data) } +static gboolean +scroll_grab_focus(GtkWidget *widget, GdkEventScroll *event, void *user_data) +{ + (void) event; + (void) user_data; + gtk_widget_grab_focus(widget); + return FALSE; +} + + +static gboolean +chartable_scroll(GtkWidget *widget, GdkEventScroll *event, void *user_data) +{ + double size; + + (void) user_data; + + if (!(event->state & GDK_CONTROL_MASK)) + return FALSE; + + if (event->state & (GDK_SHIFT_MASK | GDK_MOD1_MASK | GDK_BUTTON3_MASK)) + return FALSE; + + size = gtk_adjustment_get_value(fontsize_adjustment); + switch (event->direction) { + case GDK_SCROLL_UP: size += fontsize_adjustment->step_increment; break; + case GDK_SCROLL_DOWN: size -= fontsize_adjustment->step_increment; break; + case GDK_SCROLL_LEFT: + case GDK_SCROLL_RIGHT: + default: + return FALSE; + } + + if (size < fontsize_adjustment->lower) return TRUE; + if (size > fontsize_adjustment->upper) return TRUE; + + gcmap_char_table_set_font_size_at_pointer(GCMAP_CHAR_TABLE(widget), (int)size, event->x, event->y); + gtk_adjustment_set_value(fontsize_adjustment, size); + return TRUE; +} + + static void listcombo_changed(GtkComboBox *combo, GtkListStore *store) { @@ -486,8 +900,6 @@ listcombo_changed(GtkComboBox *combo, GtkListStore *store) GtkTreeModel *model; GtkTreePath *path; GtkTreeIter iter; - const struct libcmap_block *block; - const struct libcmap_script *script; uint32_t codepoint = selected_codepoint; if (recursion_guard) @@ -526,22 +938,10 @@ listcombo_changed(GtkComboBox *combo, GtkListStore *store) switching_groups--; - if (all_selected) { + if (all_selected) select_group(0); - } else { - switch ((enum listing)listing) { - case BY_BLOCK: - block = libcmap_find_block(codepoint, NULL); - select_group((block ? (int)(block - libcmap_block_list) : (int)libcmap_block_list_size) + 1); - break; - case BY_SCRIPT: - script = libcmap_find_script(codepoint, NULL, NULL); - select_group((script ? (int)(script - libcmap_script_list) : (int)libcmap_script_list_size) + 1); - break; - default: - abort(); - } - } + else + select_group_for_codepoint((enum listing)listing, codepoint, 0); recursion_guard--; } @@ -962,17 +1362,11 @@ create_window(const char *default_font, PangoWeight default_weight, PangoStyle d accessible = gtk_widget_get_accessible(GTK_WIDGET(hbox)); atk_object_set_name(accessible, "Font"); - fontcombo = gtk_combo_box_new(); - renderer = gtk_cell_renderer_text_new(); - gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(fontcombo), renderer, TRUE); - gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(fontcombo), renderer, "text", 0, NULL); - fontcombo_store = gtk_list_store_new(1, G_TYPE_STRING); - gtk_combo_box_set_model(GTK_COMBO_BOX(fontcombo), GTK_TREE_MODEL(fontcombo_store)); - default_font_index = populate_font_families(fontcombo, fontcombo_store, &default_font); - gtk_combo_box_set_focus_on_click(GTK_COMBO_BOX(fontcombo), FALSE); + fontcombo = filtered_combo_new(); + default_font_index = populate_font_families(FILTERED_COMBO(fontcombo), &default_font); + filtered_combo_set_active(FILTERED_COMBO(fontcombo), default_font_index); + default_font = filtered_combo_get_selected(FILTERED_COMBO(fontcombo)); gtk_box_pack_start(GTK_BOX(hbox), fontcombo, FALSE, FALSE, 0); - gtk_combo_box_set_active(GTK_COMBO_BOX(fontcombo), default_font_index); - g_signal_connect(fontcombo, "changed", G_CALLBACK(&fontcombo_changed), NULL); accessible = gtk_widget_get_accessible(fontcombo); atk_object_set_name(accessible, "Font family"); @@ -1015,6 +1409,7 @@ create_window(const char *default_font, PangoWeight default_weight, PangoStyle d column = gtk_tree_view_column_new_with_attributes(NULL, renderer, "text", 0, NULL); gtk_tree_view_append_column(GTK_TREE_VIEW(grouplist), column); gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(grouplist), FALSE); + g_signal_connect(grouplist, "scroll-event", G_CALLBACK(&scroll_grab_focus), NULL); g_signal_connect(grouplist, "query-tooltip", G_CALLBACK(&grouplist_query_tooltip), NULL); gtk_widget_set_has_tooltip(GTK_WIDGET(grouplist), TRUE); g_signal_connect(gtk_tree_view_get_selection(GTK_TREE_VIEW(grouplist)), "changed", @@ -1031,17 +1426,29 @@ create_window(const char *default_font, PangoWeight default_weight, PangoStyle d scrolled = logical_scroll_pane_new(); logical_scroll_pane_set_hscrollbar_visible(LOGICAL_SCROLL_PANE(scrolled), FALSE); chartable = gcmap_char_table_new(default_font, (int)default_font_size, default_weight, default_style); + gtk_drag_dest_set(chartable, GTK_DEST_DEFAULT_ALL, NULL, 0, GDK_ACTION_COPY); + gtk_drag_dest_add_text_targets(chartable); + g_signal_connect(chartable, "drag-data-received", G_CALLBACK(&chartable_drag_data_received), NULL); + g_signal_connect(chartable, "button-press-event", G_CALLBACK(&chartable_button_press), NULL); + g_signal_connect(fontcombo, "changed", G_CALLBACK(&fontcombo_changed), NULL); + g_signal_connect(chartable, "scroll-event", G_CALLBACK(&scroll_grab_focus), NULL); logical_scroll_pane_set_child(LOGICAL_SCROLL_PANE(scrolled), chartable); + g_signal_connect(chartable, "scroll-event", G_CALLBACK(&chartable_scroll), NULL); g_signal_connect(chartable, "size-allocate", G_CALLBACK(&chartable_size_allocate), NULL); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), scrolled, gtk_label_new_with_mnemonic("Character _table")); -#ifdef TODO scrolled = gtk_scrolled_window_new(NULL, NULL); - gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_NONE); - gtk_container_add(GTK_CONTAINER(scrolled), gtk_label_new("todo")); + details = gtk_text_view_new(); + gtk_text_view_set_editable(GTK_TEXT_VIEW(details), FALSE); + gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(details), FALSE); + details_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(details)); + tag_display = gtk_text_buffer_create_tag(details_buffer, "display", "scale", 8.0, NULL); + tag_name = gtk_text_buffer_create_tag(details_buffer, "name", "scale", 1.5, "weight", PANGO_WEIGHT_BOLD, NULL); + tag_header = gtk_text_buffer_create_tag(details_buffer, "header", "weight", PANGO_WEIGHT_BOLD, NULL); + gtk_container_add(GTK_CONTAINER(scrolled), details); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), scrolled, gtk_label_new_with_mnemonic("Character _details")); -#endif #ifdef TODO scrolled = gtk_scrolled_window_new(NULL, NULL); @@ -1164,8 +1571,6 @@ main(int argc, char *argv[]) int min_font_size_set = 0; int max_font_size_set = 0; const char *font_family = DEFAULT_FONT_FAMILY; - const struct libcmap_block *block; - const struct libcmap_script *script; char *message; ARGBEGIN { @@ -1250,18 +1655,7 @@ main(int argc, char *argv[]) groups_tooltips = group_tooltips[listing]; ngroups_tooltips = ngroup_tooltips[listing]; - switch (listing) { - case BY_BLOCK: - block = libcmap_find_block(selected_codepoint, NULL); - select_group(block ? (int)(block - libcmap_block_list) + 1 : 0); - break; - case BY_SCRIPT: - script = libcmap_find_script(selected_codepoint, NULL, NULL); - select_group(script ? (int)(script - libcmap_script_list) + 1 : 0); - break; - default: - abort(); - } + select_group_for_codepoint(listing, selected_codepoint, 1); GCMAP_CHAR_TABLE(chartable)->selection_callback = &character_cell_selected; gcmap_char_table_select_codepoint(GCMAP_CHAR_TABLE(chartable), selected_codepoint); @@ -1272,6 +1666,7 @@ main(int argc, char *argv[]) } else { update_statusbar(); } + update_details(); gtk_window_present(GTK_WINDOW(window)); gtk_widget_grab_focus(GTK_WIDGET(chartable)); |
