Display pre-edit information from X input methods

This also repurposes the `pgtk-preedit-text' event to be
meaningful on X, renames it `preedit-text', and documents it.

* doc/lispref/commands.texi (Misc Events): Document
`preedit-text'.
* lisp/term/pgtk-win.el (pgtk-preedit-text): Bind to
`preedit-text' instead.
* lisp/term/x-win.el (x-preedit-overlay): New variable.
(x-preedit-text): New command, bound as a special event to
`preedit-text'.

* src/keyboard.c (kbd_buffer_get_event):
(make_lispy_event): Rename PGTK_PREEDIT_TEXT_EVENT
PREEDIT_TEXT_EVENT.
(syms_of_keyboard): New defsym `preedit-text'.
* src/pgtkterm.c (pgtk_enqueue_preedit): Use PREEDIT_TEXT_EVENT
instead.
* src/termhooks.h (enum event_kind): Rename
`PGTK_PREEDIT_TEXT_EVENT' `PREEDIT_TEXT_EVENT'.

* src/xfns.c (Xxic_preedit_draw_callback):
(Xxic_preedit_caret_callback):
(Xxic_preedit_done_callback):
(Xxic_preedit_start_callback): New callback variables.

(STYLE_OFFTHESPOT, STYLE_OVERTHESPOT):
(STYLE_ROOT, STYLE_CALLBACK, STYLE_NONE): New macros.
(supported_xim_styles): Use reasonable values.  This also serves
as a better fix for bug#10867.
(best_xim_style): Restore code deleted as part of the original
fix for bug#10867.
(create_frame_xic): Add preedit callbacks.
(xic_set_preeditarea): Add preedit callbacks.
(x_xic_to_frame):
(xic_preedit_start_callback):
(xic_preedit_caret_callback):
(xic_preedit_done_callback):
(x_xim_text_to_utf8_unix):
(xic_preedit_draw_callback): New functions.

* src/xterm.c (x_detect_focus_change): Fix type of XI event.
(x_free_frame_resources): Free preedit text buffer if still
present.
* src/xterm.h (struct x_output): New fields `preedit_size',
`preedit_chars' and `preedit_active'.
This commit is contained in:
Po Lu 2022-01-07 14:35:29 +08:00
parent 48038cb2b2
commit 751789471c
9 changed files with 378 additions and 23 deletions

View file

@ -2129,6 +2129,19 @@ which @code{1.0} is the width and height of the touchpad
respectively. They are usually interpreted as being relative to the
size of the object beneath the gesture: image, window, etc.
@cindex @code{preedit-text} event
@item (preedit-text @var{arg})
This kind of event is sent when a system input method tells Emacs to
display some text to indicate to the user what will be inserted. The
contents of @var{arg} are dependent on the window system being used.
On X, @var{arg} is a string describing some text to place behind the
cursor. It can be @code{nil}, which means to remove any text
previously displayed. @c FIXME: what is the value of ARG on PGTK?
It is a special event (@xref{Special Events}), which should normally
not be bound by the user.
@cindex @code{drag-n-drop} event
@item (drag-n-drop @var{position} @var{files})
This kind of event is generated when a group of files is

View file

@ -325,8 +325,7 @@ See the documentation of `create-fontset-from-fontset-spec' for the format.")
(defun pgtk-preedit-text (event)
"An internal function to display preedit text from input method.
EVENT is an event of PGTK_PREEDIT_TEXT_EVENT.
It contains colors and texts."
EVENT is a `preedit-text-event'."
(interactive "e")
(when pgtk-preedit-overlay
(delete-overlay pgtk-preedit-overlay))
@ -356,6 +355,7 @@ It contains colors and texts."
(overlay-put ov 'before-string ovstr)
(setq pgtk-preedit-overlay ov)))
(define-key special-event-map [preedit-text] 'pgtk-preedit-text)
(add-hook 'after-init-hook
(function

View file

@ -1517,6 +1517,24 @@ This uses `icon-map-list' to map icon file names to stock icon names."
(global-set-key [XF86WakeUp] 'ignore)
(defvar x-preedit-overlay nil
"The overlay currently used to display preedit text from a compose sequence.")
(defun x-preedit-text (event)
"Display preedit text from a compose sequence in EVENT.
EVENT is a preedit-text event."
(interactive "e")
(when x-preedit-overlay
(delete-overlay x-preedit-overlay)
(setq x-preedit-overlay nil))
(when (nth 1 event)
(setq x-preedit-overlay (make-overlay (point) (point)))
(overlay-put x-preedit-overlay 'before-string
(propertize (nth 1 event) 'face '(:underline t)))))
(define-key special-event-map [preedit-text] 'x-preedit-text)
(provide 'x-win)
(provide 'term/x-win)

View file

@ -3973,9 +3973,7 @@ kbd_buffer_get_event (KBOARD **kbp,
*used_mouse_menu = true;
FALLTHROUGH;
#endif
#ifdef HAVE_PGTK
case PGTK_PREEDIT_TEXT_EVENT:
#endif
case PREEDIT_TEXT_EVENT:
#ifdef HAVE_NTGUI
case END_SESSION_EVENT:
case LANGUAGE_CHANGE_EVENT:
@ -6289,10 +6287,8 @@ make_lispy_event (struct input_event *event)
return list3 (Qconfig_changed_event,
event->arg, event->frame_or_window);
#ifdef HAVE_PGTK
case PGTK_PREEDIT_TEXT_EVENT:
return list2 (intern ("pgtk-preedit-text"), event->arg);
#endif
case PREEDIT_TEXT_EVENT:
return list2 (Qpreedit_text, event->arg);
/* The 'kind' field of the event is something we don't recognize. */
default:
@ -12003,6 +11999,8 @@ syms_of_keyboard (void)
DEFSYM (Qno_record, "no-record");
DEFSYM (Qencoded, "encoded");
DEFSYM (Qpreedit_text, "preedit-text");
button_down_location = make_nil_vector (5);
staticpro (&button_down_location);
staticpro (&frame_relative_event_pos);
@ -12771,8 +12769,6 @@ keys_of_keyboard (void)
"ns-put-working-text");
initial_define_lispy_key (Vspecial_event_map, "ns-unput-working-text",
"ns-unput-working-text");
initial_define_lispy_key (Vspecial_event_map, "pgtk-preedit-text",
"pgtk-preedit-text");
/* Here we used to use `ignore-event' which would simple set prefix-arg to
current-prefix-arg, as is done in `handle-switch-frame'.
But `handle-switch-frame is not run from the special-map.

View file

@ -5259,7 +5259,7 @@ pgtk_enqueue_preedit (struct frame *f, Lisp_Object preedit)
{
union buffered_input_event inev;
EVENT_INIT (inev.ie);
inev.ie.kind = PGTK_PREEDIT_TEXT_EVENT;
inev.ie.kind = PREEDIT_TEXT_EVENT;
inev.ie.arg = preedit;
inev.ie.code = 0;
XSETFRAME (inev.ie.frame_or_window, f);

View file

@ -269,10 +269,8 @@ enum event_kind
, FILE_NOTIFY_EVENT
#endif
#ifdef HAVE_PGTK
/* Pre-edit text was changed. */
, PGTK_PREEDIT_TEXT_EVENT
#endif
, PREEDIT_TEXT_EVENT
/* Either the mouse wheel has been released without it being
clicked, or the user has lifted his finger from a touchpad.

View file

@ -24,6 +24,7 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
#include <unistd.h>
#include "lisp.h"
#include "character.h"
#include "xterm.h"
#include "frame.h"
#include "window.h"
@ -2330,8 +2331,19 @@ hack_wm_protocols (struct frame *f, Widget widget)
#ifdef HAVE_X_I18N
static XFontSet xic_create_xfontset (struct frame *);
static XIMStyle best_xim_style (XIMStyles *);
static void xic_preedit_draw_callback (XIC, XPointer, XIMPreeditDrawCallbackStruct *);
static void xic_preedit_caret_callback (XIC, XPointer, XIMPreeditCaretCallbackStruct *);
static void xic_preedit_done_callback (XIC, XPointer, XPointer);
static int xic_preedit_start_callback (XIC, XPointer, XPointer);
static XIMCallback Xxic_preedit_draw_callback = { NULL,
(XIMProc) xic_preedit_draw_callback };
static XIMCallback Xxic_preedit_caret_callback = { NULL,
(XIMProc) xic_preedit_caret_callback };
static XIMCallback Xxic_preedit_done_callback = { NULL,
(XIMProc) xic_preedit_done_callback };
static XIMCallback Xxic_preedit_start_callback = { NULL,
(void *) xic_preedit_start_callback };
#if defined HAVE_X_WINDOWS && defined USE_X_TOOLKIT
/* Create an X fontset on frame F with base font name BASE_FONTNAME. */
@ -2608,6 +2620,23 @@ xic_free_xfontset (struct frame *f)
FRAME_XIC_FONTSET (f) = NULL;
}
/* Create XIC for frame F. */
#define STYLE_OFFTHESPOT (XIMPreeditArea | XIMStatusArea)
#define STYLE_OVERTHESPOT (XIMPreeditPosition | XIMStatusNothing)
#define STYLE_ROOT (XIMPreeditNothing | XIMStatusNothing)
#define STYLE_CALLBACK (XIMPreeditCallbacks | XIMStatusNothing)
#define STYLE_NONE (XIMPreeditNothing | XIMStatusNothing)
static const XIMStyle supported_xim_styles[] =
{
STYLE_CALLBACK,
STYLE_NONE,
STYLE_OVERTHESPOT,
STYLE_OFFTHESPOT,
STYLE_ROOT
};
/* Value is the best input style, given user preferences USER (already
checked to be supported by Emacs), and styles supported by the
@ -2616,8 +2645,15 @@ xic_free_xfontset (struct frame *f)
static XIMStyle
best_xim_style (XIMStyles *xim)
{
/* Return the default style. This is what GTK3 uses and
should work fine with all modern input methods. */
int i, j;
int nr_supported = ARRAYELTS (supported_xim_styles);
for (i = 0; i < nr_supported; ++i)
for (j = 0; j < xim->count_styles; ++j)
if (supported_xim_styles[i] == xim->supported_styles[j])
return supported_xim_styles[i];
/* Return the default style. */
return XIMPreeditNothing | XIMStatusNothing;
}
@ -2692,6 +2728,22 @@ create_frame_xic (struct frame *f)
goto out;
}
if (xic_style & XIMPreeditCallbacks)
{
spot.x = 0;
spot.y = 0;
preedit_attr = XVaCreateNestedList (0,
XNSpotLocation, &spot,
XNPreeditStartCallback, &Xxic_preedit_start_callback,
XNPreeditDoneCallback, &Xxic_preedit_done_callback,
XNPreeditDrawCallback, &Xxic_preedit_draw_callback,
XNPreeditCaretCallback, &Xxic_preedit_caret_callback,
NULL);
if (!preedit_attr)
goto out;
}
if (preedit_attr && status_attr)
xic = XCreateIC (xim,
XNInputStyle, xic_style,
@ -2768,7 +2820,12 @@ xic_set_preeditarea (struct window *w, int x, int y)
spot.x = WINDOW_TO_FRAME_PIXEL_X (w, x) + WINDOW_LEFT_FRINGE_WIDTH (w) + WINDOW_LEFT_MARGIN_WIDTH(w);
spot.y = WINDOW_TO_FRAME_PIXEL_Y (w, y) + FONT_BASE (FRAME_FONT (f));
attr = XVaCreateNestedList (0, XNSpotLocation, &spot, NULL);
attr = XVaCreateNestedList (0, XNSpotLocation, &spot,
XNPreeditStartCallback, &Xxic_preedit_start_callback,
XNPreeditDoneCallback, &Xxic_preedit_done_callback,
XNPreeditDrawCallback, &Xxic_preedit_draw_callback,
XNPreeditCaretCallback, &Xxic_preedit_caret_callback,
NULL);
XSetICValues (FRAME_XIC (f), XNPreeditAttributes, attr, NULL);
XFree (attr);
}
@ -2816,9 +2873,273 @@ xic_set_statusarea (struct frame *f)
XFree (attr);
}
static struct frame *
x_xic_to_frame (XIC xic)
{
Lisp_Object tail, tem;
struct frame *f;
/* Set X fontset for XIC of frame F, using base font name
BASE_FONTNAME. Called when a new Emacs fontset is chosen. */
FOR_EACH_FRAME (tail, tem)
{
f = XFRAME (tem);
if (FRAME_X_P (f) && FRAME_XIC (f) == xic)
return f;
}
return NULL;
}
static int
xic_preedit_start_callback (XIC xic, XPointer client_data,
XPointer call_data)
{
struct frame *f = x_xic_to_frame (xic);
struct x_output *output;
if (f)
{
output = FRAME_X_OUTPUT (f);
output->preedit_size = 0;
output->preedit_active = true;
if (output->preedit_chars)
xfree (output->preedit_chars);
output->preedit_chars = NULL;
}
return -1;
}
static void
xic_preedit_caret_callback (XIC xic, XPointer client_data,
XIMPreeditCaretCallbackStruct *call_data)
{
}
static void
xic_preedit_done_callback (XIC xic, XPointer client_data,
XPointer call_data)
{
struct frame *f = x_xic_to_frame (xic);
struct x_output *output;
struct input_event ie;
if (f)
{
ie.kind = PREEDIT_TEXT_EVENT;
ie.arg = Qnil;
XSETFRAME (ie.frame_or_window, f);
XSETINT (ie.x, 0);
XSETINT (ie.y, 0);
kbd_buffer_store_event (&ie);
output = FRAME_X_OUTPUT (f);
if (output->preedit_chars)
xfree (output->preedit_chars);
output->preedit_size = 0;
output->preedit_active = false;
output->preedit_chars = NULL;
}
}
/* The string returned is not null-terminated. */
static char *
x_xim_text_to_utf8_unix (XIMText *text, ptrdiff_t *length)
{
unsigned char *wchar_buf;
ptrdiff_t wchar_actual_length, i;
ptrdiff_t nbytes;
struct coding_system coding;
if (text->encoding_is_wchar)
{
wchar_buf = xmalloc ((text->length + 1) * MAX_MULTIBYTE_LENGTH);
wchar_actual_length = 0;
for (i = 0; i < text->length; ++i)
wchar_actual_length += CHAR_STRING (text->string.wide_char[i],
wchar_buf + wchar_actual_length);
*length = wchar_actual_length;
return (char *) wchar_buf;
}
nbytes = strlen (text->string.multi_byte);
setup_coding_system (Qutf_8_unix, &coding);
coding.mode |= (CODING_MODE_LAST_BLOCK
| CODING_MODE_SAFE_ENCODING);
coding.source = (const unsigned char *) text->string.multi_byte;
coding.dst_bytes = 2048;
coding.destination = xmalloc (2048);
decode_coding_object (&coding, Qnil, 0, 0, nbytes, nbytes, Qnil);
/* coding.destination has either been allocated by us, or
reallocated by decode_coding_object. */
*length = coding.produced;
return (char *) coding.destination;
}
static void
xic_preedit_draw_callback (XIC xic, XPointer client_data,
XIMPreeditDrawCallbackStruct *call_data)
{
struct frame *f = x_xic_to_frame (xic);
struct x_output *output;
ptrdiff_t text_length;
ptrdiff_t charpos;
ptrdiff_t original_size;
char *text;
char *chg_start, *chg_end;
struct input_event ie;
if (f)
{
output = FRAME_X_OUTPUT (f);
if (!output->preedit_active)
return;
if (call_data->text)
text = x_xim_text_to_utf8_unix (call_data->text, &text_length);
else
text = NULL;
original_size = output->preedit_size;
/* This is an ordinary insertion: reallocate the buffer to hold
enough for TEXT. */
if (!call_data->chg_length)
{
if (!text)
goto im_abort;
if (output->preedit_chars)
output->preedit_chars = xrealloc (output->preedit_chars,
output->preedit_size += text_length);
else
output->preedit_chars = xmalloc (output->preedit_size += text_length);
}
chg_start = output->preedit_chars;
/* The IM sent bad data: the buffer is empty, but the change
position is more than 0. */
if (!output->preedit_chars && call_data->chg_first)
goto im_abort;
/* Find the byte position for the character position where the
first change is to be made. */
if (call_data->chg_first)
{
charpos = 0;
while (charpos < call_data->chg_first)
{
chg_start += BYTES_BY_CHAR_HEAD (*chg_start);
if ((chg_start - output->preedit_chars) > output->preedit_size)
/* The IM sent bad data: chg_start is larger than the
current buffer. */
goto im_abort;
++charpos;
}
}
if (!call_data->chg_length)
{
if (!text)
goto im_abort;
memmove (chg_start + text_length, chg_start,
original_size - (chg_start - output->preedit_chars));
memcpy (chg_start, text, text_length);
}
else
{
if (call_data->chg_length < 1)
goto im_abort;
charpos = 0;
chg_end = chg_start;
while (charpos < call_data->chg_length)
{
chg_end += BYTES_BY_CHAR_HEAD (*chg_end);
if ((chg_end - output->preedit_chars) > output->preedit_size)
/* The IM sent bad data: chg_end ends someplace outside
the current buffer. */
goto im_abort;
++charpos;
}
memmove (chg_start, chg_end, ((output->preedit_chars
+ output->preedit_size) - chg_end));
output->preedit_size -= (chg_end - chg_start);
if (text)
{
original_size = output->preedit_size;
output->preedit_chars = xrealloc (output->preedit_chars,
output->preedit_size += text_length);
/* Find chg_start again, since preedit_chars was reallocated. */
chg_start = output->preedit_chars;
charpos = 0;
while (charpos < call_data->chg_first)
{
chg_start += BYTES_BY_CHAR_HEAD (*chg_start);
if ((chg_start - output->preedit_chars) > output->preedit_size)
/* The IM sent bad data: chg_start is larger than the
current buffer. */
goto im_abort;
++charpos;
}
memmove (chg_start + text_length, chg_start,
original_size - (chg_start - output->preedit_chars));
memcpy (chg_start, text, text_length);
}
}
if (text)
xfree (text);
/* This is okay because this callback is called from the big XIM
event filter, which runs inside XTread_socket. */
ie.kind = PREEDIT_TEXT_EVENT;
XSETFRAME (ie.frame_or_window, f);
ie.arg = make_string_from_utf8 (output->preedit_chars,
output->preedit_size);
XSETINT (ie.x, 0);
XSETINT (ie.y, 0);
kbd_buffer_store_event (&ie);
}
return;
im_abort:
if (text)
xfree (text);
if (output->preedit_chars)
xfree (output->preedit_chars);
output->preedit_chars = NULL;
output->preedit_size = 0;
output->preedit_active = false;
}
void
xic_set_xfontset (struct frame *f, const char *base_fontname)

View file

@ -5198,7 +5198,7 @@ x_detect_focus_change (struct x_display_info *dpyinfo, struct frame *frame,
#ifdef HAVE_XINPUT2
case GenericEvent:
{
XIEvent *xi_event = (XIEvent *) event;
XIEvent *xi_event = (XIEvent *) event->xcookie.data;
struct frame *focus_frame = dpyinfo->x_focus_event_frame;
int focus_state
@ -14046,6 +14046,9 @@ x_free_frame_resources (struct frame *f)
#ifdef HAVE_X_I18N
if (FRAME_XIC (f))
free_frame_xic (f);
if (f->output_data.x->preedit_chars)
xfree (f->output_data.x->preedit_chars);
#endif
#ifdef USE_CAIRO

View file

@ -788,6 +788,12 @@ struct x_output
They are used when creating the cairo surface next time. */
int cr_surface_desired_width, cr_surface_desired_height;
#endif
#ifdef HAVE_X_I18N
ptrdiff_t preedit_size;
char *preedit_chars;
bool preedit_active;
#endif
};
enum