aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMattias Andrée <m@maandree.se>2026-10-18 05:55:18 +0200
committerMattias Andrée <m@maandree.se>2026-10-18 05:55:18 +0200
commite578c50ffbd02f2f7b9116b6b9eb096bb1fcee60 (patch)
treea70534fc3c758dd23c494a086e2af46a7d0e602d
parentMisc (diff)
downloadgcmap-e578c50ffbd02f2f7b9116b6b9eb096bb1fcee60.tar.gz
gcmap-e578c50ffbd02f2f7b9116b6b9eb096bb1fcee60.tar.bz2
gcmap-e578c50ffbd02f2f7b9116b6b9eb096bb1fcee60.tar.xz
Misc
Signed-off-by: Mattias Andrée <m@maandree.se>
-rw-r--r--Makefile7
-rw-r--r--TODO35
-rw-r--r--char-table.c325
-rw-r--r--char-table.h14
-rw-r--r--characters.c80
-rw-r--r--common.h26
-rw-r--r--config.mk2
-rw-r--r--filtered-combo.c263
-rw-r--r--filtered-combo.h32
-rw-r--r--gcmap.c557
10 files changed, 1183 insertions, 158 deletions
diff --git a/Makefile b/Makefile
index 25034c9..6f61267 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/TODO b/TODO
index f9782e0..3232a63 100644
--- a/TODO
+++ b/TODO
@@ -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));
+}
diff --git a/common.h b/common.h
index f54e8ed..943d4b8 100644
--- a/common.h
+++ b/common.h
@@ -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);
diff --git a/config.mk b/config.mk
index 578c9b5..04fe02e 100644
--- a/config.mk
+++ b/config.mk
@@ -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);
diff --git a/gcmap.c b/gcmap.c
index 25a9ef6..a474b56 100644
--- a/gcmap.c
+++ b/gcmap.c
@@ -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));