12to11/text_input.c
hujianwei d2abef20c4 Pacify more compiler warnings
* text_input.c (EncodeIMString):
* xdata.c (GetConversionCallback, PostReceiveConversion): Pacify
GCC suspecting memory leaks after iconv_open returns -1.
2022-11-17 02:45:33 +00:00

3762 lines
94 KiB
C
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Wayland compositor running on top of an X server.
Copyright (C) 2022 to various contributors.
This file is part of 12to11.
12to11 is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
12to11 is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
along with 12to11. If not, see <https://www.gnu.org/licenses/>. */
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/errno.h>
#include <iconv.h>
#include <locale.h>
#include <langinfo.h>
#include <ctype.h>
#include "compositor.h"
#include "text-input-unstable-v3.h"
#include <X11/extensions/XInput2.h>
#include <X11/XKBlib.h>
/* X Input Method (XIM) support.
The X input method has a client-server architecture; the connection
between the client and server is abstracted away by Xlib, and
always results in an XIM object being produced. The connection
itself can take many forms: the IM server could be linked into the
X library, another X client on the same server, running on another
machine over a TCP/IP connection, or even a DECnet node.
The XIM object will be assigned an arbitrary seat (usually the
virtual core keyboard), which will be the only seat that can
utilize input methods.
Each text input will have a corresponding input context (XIC) for
every focused window. The XIC handles state logically associated
with a single text entry area, such as currently composed text, the
currently focused surface, the position of the cursor, and text
surrounding the cursor.
When the previously assigned seat's focus moves to a surface with
an associated XIC, and the text input is enabled, focus is given to
the XIC. Subsequent extension key events are translated into core
ones, then sent to the input context; then, should the input
context chose to discard the event, the event is simply discarded.
Otherwise, XmbLookupString is called on the event, and any keysym
or string returned is looked up and committed or sent to the
surface.
The X library synthesizes fake client-side events to represent
XIM_COMMIT events, and saves the committed text for XmbLookupString
to return. These events do not contain the information necessary
to determine the XIC that sent the event which caused it to be
generated. Similarly, events sent with XIM_FORWARD_EVENT do not
contain enough information to attribute them to the XIC that sent
them.
That means it is impossible to attribute forwarded events or
committed text to the correct XIC, and thus it is impossible to
look up which seat's TextInput resource an event is actually bound
for. If one day we move to our own implementation of the XIM
protocol, then it will become possible to properly support
multi-seat setups, with one XIC per-client and per-seat.
Further more, the XIM has its own locale, which is not guaranteed
to have the same coded character set as the character set used in
the Wayland protocol, namely UTF-8. During XIM creation, its
locale's coded character set is obtained and used to create a
character conversion context. All text obtained from the XIM
callbacks is then converted with that context, and character
indices provided by the XIM are converted to byte indices into the
converted string before being sent to the client.
This code has many inherent race conditions, just like the
zwp_text_input_v3 protocol itself. And as described above, it only
supports one seat due to limitations of the Xlib XIM wrapper. */
typedef struct _TextInputClientInfo TextInputClientInfo;
typedef struct _TextInput TextInput;
typedef struct _TextInputState TextInputState;
typedef struct _TextPosition TextPosition;
typedef struct _PreeditBuffer PreeditBuffer;
typedef struct _KeycodeMap KeycodeMap;
typedef enum _XimStyleKind XimStyleKind;
enum _XimStyleKind
{
XimStyleNone,
XimOverTheSpot,
XimOffTheSpot,
XimRootWindow,
XimOnTheSpot,
};
enum
{
PendingEnabled = 1,
PendingCursorRectangle = (1 << 1),
PendingSurroundingText = (1 << 2),
};
struct _KeycodeMap
{
/* Packed map between keycodes specified in KeyPress events and
keycodes that were actually sent to applications. */
KeyCode *keycodes;
/* The keycodes that were computed from keysyms and actually sent to
applications. */
KeyCode *keysyms;
/* The number of keycodes and used keycodes in this map. */
int key_count;
};
struct _PreeditBuffer
{
/* Buffer data. */
char *buffer;
/* The locale. */
char *locale;
/* Buffer size in bytes. */
size_t size;
/* Buffer size in characters. */
int total_characters;
};
struct _TextPosition
{
/* Byte position. */
ptrdiff_t bytepos;
/* Character position. */
int charpos;
};
struct _TextInputState
{
/* What is defined; alternatively, what is pending. */
int pending;
/* Whether or not this text input is enabled. */
Bool enabled;
/* Cursor rectangle. */
int cursor_x, cursor_y, cursor_width, cursor_height;
/* Surrounding text. This is allocated with XLMalloc and is made
NULL upon commit in the pending state. */
char *surrounding_text;
/* Character and byte positions of the cursor in the surrounding
text. */
TextPosition cursor;
};
struct _TextInput
{
/* The TextInputClientInfo associated with this text input. */
TextInputClientInfo *client_info;
/* The wl_resource associated with this text input. */
struct wl_resource *resource;
/* The next and last TextInputs. */
TextInput *next, *last;
/* The XIC associated with this text input. */
XIC xic;
/* The current pre-edit buffer, or NULL. */
PreeditBuffer *buffer;
/* The position of the preedit caret in characters. */
int caret;
/* The style of the caret. */
XIMCaretStyle caret_style;
/* The pending state. */
TextInputState pending_state;
/* The current state. */
TextInputState current_state;
/* Number of commit requests performed. */
uint32_t serial;
/* Map between keys currently held down and keysyms they looked up
to. */
KeycodeMap keysym_map;
};
/* Structure describing a list of TextInput resources associated with
a given client. */
struct _TextInputClientInfo
{
/* The next and last objects in this list. */
TextInputClientInfo *next, *last;
/* The associated seat. */
Seat *seat;
/* The key associated with the seat destruction callback. */
void *seat_key;
/* The associated Wayland client. */
struct wl_client *client;
/* The list of Wayland client info objects. */
TextInput inputs;
/* The current focused surface. */
Surface *focus_surface;
};
/* List of all TextInputClientInfos. */
static TextInputClientInfo all_client_infos;
/* The text input manager global. */
static struct wl_global *text_input_manager_global;
/* The IM fontset. */
static XFontSet im_fontset;
#if defined DEBUG
#define DebugPrint(format, args...) \
fprintf (stderr, "%s: " format "\n", __FUNCTION__, ## args)
#else
#define DebugPrint(fmt, ...) ((void) 0)
#endif
/* The XIM currently in use, or NULL. */
static XIM current_xim;
/* The conversion context for that XIM. */
static iconv_t current_cd;
/* The preferred XIM style. */
static XIMStyle xim_style;
/* The order in which XIM input styles will be searched for. */
static XimStyleKind xim_style_order[5];
static void
ClearKeycodeMap (KeycodeMap *map)
{
XLFree (map->keycodes);
XLFree (map->keysyms);
map->keycodes = NULL;
map->keysyms = NULL;
map->key_count = 0;
}
static void
InsertKeycode (KeycodeMap *map, KeyCode keycode, KeyCode keycode_used)
{
int i;
/* Insert keycode_used into map, under keycode. See if map already
contains the given keycode. */
for (i = 0; i < map->key_count; ++i)
{
if (map->keycodes[i] == keycode)
{
map->keysyms[i] = keycode_used;
return;
}
}
/* Otherwise, add it to the end. */
map->key_count++;
map->keycodes
= XLRealloc (map->keycodes,
map->key_count * sizeof *map->keycodes);
map->keysyms
= XLRealloc (map->keysyms,
map->key_count * sizeof *map->keysyms);
map->keycodes[map->key_count - 1] = keycode;
map->keysyms[map->key_count - 1] = keycode_used;
}
static void
RemoveKeysym (KeycodeMap *map, KeyCode keycode)
{
int i;
/* Find where the keycode is located in the map. */
for (i = 0; i < map->key_count; ++i)
{
if (map->keycodes[i] == keycode)
{
/* Remove the keycode from the list and shrink it. */
memcpy (&map->keycodes[i], &map->keycodes[i + 1],
(map->key_count - (i + 1)) * sizeof *map->keycodes);
memcpy (&map->keysyms[i], &map->keysyms[i + 1],
(map->key_count - (i + 1)) * sizeof *map->keysyms);
map->key_count -= 1;
map->keycodes
= XLRealloc (map->keycodes,
map->key_count * sizeof *map->keycodes);
map->keysyms
= XLRealloc (map->keysyms,
map->key_count * sizeof *map->keysyms);
return;
}
}
}
static KeyCode
GetKeycode (KeycodeMap *map, KeyCode keycode)
{
int i;
for (i = 0; i < map->key_count; ++i)
{
if (map->keycodes[i] == keycode)
return map->keysyms[i];
}
return 0;
}
static int
CurrentCursorX (TextInput *input)
{
int x, y;
XLAssert (input->client_info->focus_surface != NULL);
/* Scale these coordinates into window coordinates. */
TruncateSurfaceToWindow (input->client_info->focus_surface,
input->current_state.cursor_x,
input->current_state.cursor_y,
&x, &y);
return x;
}
static int
CurrentCursorY (TextInput *input)
{
int x, y;
XLAssert (input->client_info->focus_surface != NULL);
/* Scale these coordinates into window coordinates. */
TruncateSurfaceToWindow (input->client_info->focus_surface,
input->current_state.cursor_x,
input->current_state.cursor_y,
&x, &y);
return y;
}
static int
CurrentCursorWidth (TextInput *input)
{
int width, height;
XLAssert (input->client_info->focus_surface != NULL);
/* Scale these coordinates into window coordinates. */
TruncateScaleToWindow (input->client_info->focus_surface,
input->current_state.cursor_width,
input->current_state.cursor_height,
&width, &height);
return width;
}
static int
CurrentCursorHeight (TextInput *input)
{
int width, height;
XLAssert (input->client_info->focus_surface != NULL);
/* Scale these coordinates into window coordinates. */
TruncateScaleToWindow (input->client_info->focus_surface,
input->current_state.cursor_width,
input->current_state.cursor_height,
&width, &height);
return height;
}
/* Byte-text position conversion. */
static int
CountOctets (char byte)
{
/* Given the start of a UTF-8 sequence, return how many following
bytes are in the current sequence. */
return (!(byte & 0x80) ? 1
: !(byte & 0x20) ? 2
: !(byte & 0x10) ? 3
: !(byte & 0x08) ? 4
: 5);
}
static TextPosition
TextPositionFromBytePosition (const char *string, size_t length,
ptrdiff_t byte_position)
{
const char *start;
TextPosition position;
start = string;
position.charpos = 0;
position.bytepos = byte_position;
if (!byte_position)
return position;
if (byte_position > length)
goto invalid;
while (start < string + length)
{
if (start + CountOctets (*start) > string + length)
goto invalid;
start += CountOctets (*start);
position.charpos++;
position.bytepos = start - string;
if (position.bytepos == byte_position)
return position;
else if (position.bytepos > byte_position)
goto invalid;
}
invalid:
/* Return the invalid text position. */
position.bytepos = -1;
position.charpos = -1;
return position;
}
static TextPosition
TextPositionFromCharPosition (const char *string, size_t length,
int char_position)
{
const char *start;
TextPosition position;
start = string;
position.charpos = 0;
position.bytepos = 0;
if (!char_position)
return position;
while (position.charpos < char_position)
{
if (start + CountOctets (*start) > string + length)
goto invalid;
start += CountOctets (*start);
position.charpos++;
position.bytepos = start - string;
}
/* Return the resulting text position. */
return position;
invalid:
/* Return the invalid text position. */
position.bytepos = -1;
position.charpos = -1;
return position;
}
/* Forward declaration. */
static void CreateIC (TextInput *);
static void
DestroyTextInput (struct wl_client *client, struct wl_resource *resource)
{
wl_resource_destroy (resource);
}
static void
Enable (struct wl_client *client, struct wl_resource *resource)
{
TextInput *input;
input = wl_resource_get_user_data (resource);
/* If there is already a string, free it, as the pending flag will
be clobbered below. */
if (input->pending_state.surrounding_text)
XLFree (input->pending_state.surrounding_text);
input->pending_state.surrounding_text = NULL;
/* Set the pending state. */
input->pending_state.pending = PendingEnabled;
input->pending_state.enabled = True;
}
static void
Disable (struct wl_client *client, struct wl_resource *resource)
{
TextInput *input;
input = wl_resource_get_user_data (resource);
/* If there is already a string, free it, as the pending flag will
be clobbered below. */
if (input->pending_state.surrounding_text)
XLFree (input->pending_state.surrounding_text);
input->pending_state.surrounding_text = NULL;
/* Set the pending state. */
input->pending_state.pending = PendingEnabled;
input->pending_state.enabled = False;
}
static void
SetSurroundingText (struct wl_client *client, struct wl_resource *resource,
const char *text, int cursor, int anchor)
{
TextInput *input;
input = wl_resource_get_user_data (resource);
/* If there is already a string, free it. */
if (input->pending_state.surrounding_text)
XLFree (input->pending_state.surrounding_text);
/* Set the surrounding text and cursor position. */
input->pending_state.surrounding_text = XLStrdup (text);
input->pending_state.cursor
= TextPositionFromBytePosition (text, strlen (text),
cursor);
input->pending_state.pending |= PendingSurroundingText;
}
static void
SetTextChangeCause (struct wl_client *client, struct wl_resource *resource,
uint32_t cause)
{
/* Not supported. */
}
static void
SetContentType (struct wl_client *client, struct wl_resource *resource,
uint32_t hint, uint32_t purpose)
{
/* Not supported. */
}
static void
SetCursorRectangle (struct wl_client *client, struct wl_resource *resource,
int32_t x, int32_t y, int32_t width, int32_t height)
{
TextInput *input;
input = wl_resource_get_user_data (resource);
if ((input->current_state.pending & PendingCursorRectangle
/* PendingEnabled will clear the current state's cursor
rectangle. */
&& !(input->pending_state.pending & PendingEnabled))
&& x == input->current_state.cursor_x
&& y == input->current_state.cursor_y
&& width == input->current_state.cursor_width
&& height == input->current_state.cursor_height)
/* Nothing changed, return. */
return;
input->pending_state.pending |= PendingCursorRectangle;
input->pending_state.cursor_x = x;
input->pending_state.cursor_y = y;
input->pending_state.cursor_width = width;
input->pending_state.cursor_height = height;
}
static TextInput *
FindEnabledTextInput (TextInputClientInfo *info)
{
TextInput *input;
input = info->inputs.next;
while (input != &info->inputs)
{
if (input->current_state.enabled)
return input;
input = input->next;
}
return NULL;
}
static void
FitRect (XRectangle *input, int view_width, int view_height,
int caret_x, int caret_y, int caret_width, int caret_height)
{
XRectangle r1, r2, copy;
copy = *input;
/* Try to fit the given dimensions into the view. First,
start with the width:
right edge of view
^ |------suggested size-------|
^ caret X, Y + HEIGHT ^ left edge of view
If the suggested size does not fit, like so:
^ |------suggested size------^--|
^ caret X, Y + HEIGHT
Move the caret to X 0 so it does, if the suggested size is wider
than half the view:
^|------suggested size---------|^
^ start X, Y + HEIGHT
Otherwise, move the rectangle leftwards until it fits.
If it still does not fit, limit the width to that of the
the view.
Next, fit the height. Start by placing the rectangle
below the caret. If that is too small, try placing the
rectangle so that its bottom touches the caret. If that
still does not fit, then use the tallest of the following
two rectangles:
CARET_Y + CARET_HEIGHT by (BOTTOM_X - CARET_X + CARET_HEIGHT + 1)
0 by CARET_Y. */
/* Do the width. Input should already be placed at the bottom right
corner of the caret. */
if (input->x + input->width >= view_width)
{
if (input->width > view_width / 2)
/* Flip x to 0. */
input->x = 0;
else
/* Move the rect left until it fits. */
input->x -= (input->x + input->width - 1
- view_width);
/* If it still doesn't fit, set x to 0 and width to
view_width. */
if (input->x + input->width >= view_width)
{
input->x = 0;
input->width = view_width;
}
}
/* Do the height. */
if (input->y + input->height >= view_height)
{
/* Flip the caret upwards, so the last scanline of the preedit
area is immediately above the first scanline of the
caret. */
input->y = caret_y - input->height;
/* If the rectangle is still too small, use the rectangle formed
between the top of the view and the top of the caret, or that
between the bottom of the view and the bottom of the caret,
whichever is larger. */
if (input->y < 0 || input->y + input->height >= view_height)
{
r1.y = 0;
r1.height = caret_y;
r2.y = caret_y + caret_height;
r2.height = view_height - r2.y;
if (r1.height > r2.height)
{
input->y = r1.y;
input->height = r1.height;
}
else
{
input->y = r2.y;
input->height = r2.height;
}
}
}
/* If the rectangle is still invalid, just fall back to the old
one. */
if (input->width <= 0 || input->height <= 0)
*input = copy;
}
static void
DoGeometryAllocation (TextInput *input)
{
XPoint spot;
XRectangle area, *needed;
XVaNestedList attr;
View *view;
char *rc;
DebugPrint ("doing geometry allocation for %p", input);
if (!input->xic)
return;
XLAssert (input->client_info->focus_surface != NULL);
view = input->client_info->focus_surface->view;
if (xim_style & XIMPreeditPosition)
{
DebugPrint ("IM wants spot values for preedit window");
if (input->current_state.pending & PendingCursorRectangle)
{
spot.x = CurrentCursorX (input);
spot.y = (CurrentCursorY (input)
+ CurrentCursorHeight (input));
}
else
{
spot.x = 0;
spot.y = 1;
}
DebugPrint ("using spot: %d, %d", spot.x, spot.y);
attr = XVaCreateNestedList (0, XNSpotLocation, &spot, NULL);
XSetICValues (input->xic, XNPreeditAttributes, attr, NULL);
XFree (attr);
}
else if (xim_style & XIMPreeditArea)
{
DebugPrint ("IM wants geometry negotiation");
/* Suggest no size to the input method. */
area.x = area.y = area.width = area.height = 0;
attr = XVaCreateNestedList (0, XNAreaNeeded, &area, NULL);
XSetICValues (input->xic, XNPreeditAttributes, attr, NULL);
XFree (attr);
/* Get the size from the input method. */
attr = XVaCreateNestedList (0, XNAreaNeeded, &needed, NULL);
rc = XGetICValues (input->xic, XNPreeditAttributes, attr, NULL);
XFree (attr);
if (!rc)
{
DebugPrint ("IM suggested the given size: %d %d",
needed->width, needed->height);
/* Place the rectangle below and to the right of the
caret. */
if (input->current_state.pending & PendingCursorRectangle)
{
needed->x = (CurrentCursorX (input)
+ CurrentCursorWidth (input));
needed->y = (CurrentCursorY (input)
+ CurrentCursorHeight (input));
FitRect (needed, ViewWidth (view), ViewHeight (view),
CurrentCursorX (input), CurrentCursorY (input),
CurrentCursorWidth (input),
CurrentCursorHeight (input));
DebugPrint ("filled rectangle: %d %d %d %d",
needed->x, needed->y, needed->width,
needed->height);
}
else
{
/* No caret was specified... Place the preedit window on
the bottom left corner of the view. */
needed->x = 0;
needed->y = ViewHeight (view) - needed->height;
DebugPrint ("placed rectangle: %d %d %d %d",
needed->x, needed->y, needed->width,
needed->height);
}
/* Set the geometry. */
attr = XVaCreateNestedList (0, XNArea, needed, NULL);
XSetICValues (input->xic, XNPreeditAttributes, attr, NULL);
XFree (attr);
/* Free the rectangle returned. */
XFree (needed);
}
}
if (xim_style & XIMStatusArea)
{
DebugPrint ("IM wants geometry negotiation for status area");
/* Suggest no size to the input method. */
area.x = area.y = area.width = area.height = 0;
attr = XVaCreateNestedList (0, XNAreaNeeded, &area, NULL);
XSetICValues (input->xic, XNStatusAttributes, attr, NULL);
XFree (attr);
/* Get the size from the input method. */
attr = XVaCreateNestedList (0, XNAreaNeeded, &needed, NULL);
rc = XGetICValues (input->xic, XNStatusAttributes, attr, NULL);
XFree (attr);
if (!rc)
{
DebugPrint ("IM suggested the given size: %d %d",
needed->width, needed->height);
/* Place the rectangle at the bottom of the window. */
needed->x = ViewWidth (view) - needed->width;
needed->y = ViewHeight (view) - needed->height;
DebugPrint ("placed rectangle at bottom right: %d %d %d %d",
needed->x, needed->y, needed->width,
needed->height);
/* Set the geometry. */
attr = XVaCreateNestedList (0, XNArea, needed, NULL);
XSetICValues (input->xic, XNStatusAttributes, attr, NULL);
XFree (attr);
/* Free the needed rectangle. */
XFree (needed);
}
}
}
static void
Commit (struct wl_client *client, struct wl_resource *resource)
{
TextInput *input, *enabled;
input = wl_resource_get_user_data (resource);
input->serial++;
if (!input->client_info)
/* The text input has no more associated seat. */
return;
if (!input->client_info->focus_surface)
/* The text input has no more associated surface. */
return;
if (input->pending_state.pending & PendingEnabled)
{
if (input->pending_state.enabled)
{
/* Check if there is another enabled text input in the same
client info structure. */
enabled = FindEnabledTextInput (input->client_info);
if (enabled && enabled != input)
/* Return, as the spec says we should ignore requests to
enable a text input. */
return;
}
/* Free any surrounding text in the current state. */
if (input->current_state.surrounding_text)
XLFree (input->current_state.surrounding_text);
/* Copy the pending state wholesale. */
input->current_state = input->pending_state;
/* Clear the surrounding text. */
input->pending_state.surrounding_text = NULL;
input->pending_state.pending = 0;
if (input->current_state.surrounding_text)
DebugPrint ("surrounding text early change: %s[%d]",
input->current_state.surrounding_text,
input->current_state.cursor.charpos);
if (input->current_state.enabled)
{
DebugPrint ("text input %p enabled, state: %2b", input,
(unsigned int) input->current_state.pending);
/* Maybe create or reset and then focus the IC. */
if (!input->xic)
CreateIC (input);
else
XFree (XmbResetIC (input->xic));
/* Perform geometry/position allocation on the IC. */
DoGeometryAllocation (input);
if (input->xic)
XSetICFocus (input->xic);
}
else
{
DebugPrint ("text input %p disabled", input);
if (input->xic)
XUnsetICFocus (input->xic);
}
}
else
{
/* Apply the pending state piecemeal. */
if (input->pending_state.pending & PendingCursorRectangle)
{
DebugPrint ("cursor rectangle changed to: %d %d %d %d",
input->pending_state.cursor_x,
input->pending_state.cursor_y,
input->pending_state.cursor_width,
input->pending_state.cursor_height);
input->current_state.cursor_x
= input->pending_state.cursor_x;
input->current_state.cursor_y
= input->pending_state.cursor_y;
input->current_state.cursor_width
= input->pending_state.cursor_width;
input->current_state.cursor_height
= input->pending_state.cursor_height;
input->current_state.pending |= PendingCursorRectangle;
if (input->current_state.enabled && input->xic)
/* Perform geometry/position allocation on the IC. */
DoGeometryAllocation (input);
}
if (input->pending_state.pending & PendingSurroundingText)
{
DebugPrint ("surrounding text changed to: %s[%d]",
input->pending_state.surrounding_text,
input->pending_state.cursor.charpos);
if (input->current_state.surrounding_text)
XLFree (input->current_state.surrounding_text);
/* Surrounding text changed. Move the surrounding text and
cursor position over. */
input->current_state.surrounding_text
= input->pending_state.surrounding_text;
input->current_state.cursor = input->pending_state.cursor;
/* Clear the surrounding text on the pending state. */
input->pending_state.surrounding_text = NULL;
/* And add the flag to the current state. */
input->current_state.pending |= PendingSurroundingText;
}
/* Clear the pending state mask. */
input->pending_state.pending = 0;
}
}
static const struct zwp_text_input_v3_interface input_impl =
{
.destroy = DestroyTextInput,
.enable = Enable,
.disable = Disable,
.set_surrounding_text = SetSurroundingText,
.set_text_change_cause = SetTextChangeCause,
.set_content_type = SetContentType,
.set_cursor_rectangle = SetCursorRectangle,
.commit = Commit,
};
/* Forward declarations. */
static void FreePreeditBuffer (PreeditBuffer *);
static void UpdatePreedit (TextInput *);
static void
HandleICDestroyed (TextInput *input)
{
/* Destroy the preedit buffer and update the preedit state. */
if (input->buffer)
{
FreePreeditBuffer (input->buffer);
input->buffer = NULL;
/* Send changes to the client. */
UpdatePreedit (input);
}
}
static void
InputDoLeave (TextInput *input, Surface *old_surface)
{
/* Destroy any XIC that was created. */
if (input->xic)
{
XDestroyIC (input->xic);
input->xic = NULL;
HandleICDestroyed (input);
}
/* Clear the input state. */
if (input->current_state.surrounding_text)
XLFree (input->current_state.surrounding_text);
/* Clear the keycode-keycode table. Correlating key release with
key press events is no longer important, as a leave event has
been sent to the seat. */
ClearKeycodeMap (&input->keysym_map);
memset (&input->current_state, 0, sizeof input->current_state);
}
static void
InputDoEnter (TextInput *input, Surface *new_surface)
{
/* If there is still a preedit buffer, destroy it. */
if (input->buffer)
{
FreePreeditBuffer (input->buffer);
/* Set it to NULL. */
input->buffer = NULL;
UpdatePreedit (input);
}
}
static void
HandleResourceDestroy (struct wl_resource *resource)
{
TextInput *input;
input = wl_resource_get_user_data (resource);
/* Now, if the client info is attached, unlink the text input. */
if (input->client_info)
{
input->last->next = input->next;
input->next->last = input->last;
/* If the client info is now empty, destroy the client info as
well. */
if (input->client_info->inputs.next
== &input->client_info->inputs)
{
XLSeatCancelDestroyListener (input->client_info->seat_key);
input->client_info->last->next = input->client_info->next;
input->client_info->next->last = input->client_info->last;
XLFree (input->client_info);
}
}
/* If an XIC still exists, destroy it. */
if (input->xic)
XDestroyIC (input->xic);
/* If there is a surrounding text string, free it. */
if (input->pending_state.surrounding_text)
XLFree (input->pending_state.surrounding_text);
if (input->current_state.surrounding_text)
XLFree (input->current_state.surrounding_text);
/* If there is still a preedit buffer, destroy it. */
if (input->buffer)
FreePreeditBuffer (input->buffer);
/* Destroy the map of pressed keycodes to keycodes. */
ClearKeycodeMap (&input->keysym_map);
/* Free the text input itself. */
XLFree (input);
}
static void
HandleSeatDestroyed (void *data)
{
TextInputClientInfo *info;
TextInput *input;
/* The seat associated with the given TextInputClientInfo was
destroyed. Detach every TextInput object. */
info = data;
input = info->inputs.next;
while (input != &info->inputs)
{
input->client_info = NULL;
/* client_info is now NULL, meaning this text input is inert.
So destroy the XIC, as it's not being destroyed later. */
if (input->xic)
{
XDestroyIC (input->xic);
input->xic = NULL;
HandleICDestroyed (input);
}
input = input->next;
}
/* Next, unlink and free the client info. */
info->last->next = info->next;
info->next->last = info->last;
XLFree (info);
}
static void
NoticeEnter (TextInputClientInfo *info, Surface *surface)
{
TextInput *input;
DebugPrint ("client info: %p, surface: %p",
info, surface);
if (info->focus_surface == surface)
/* The focus surface did not change. */
return;
input = info->inputs.next;
while (input != &info->inputs)
{
/* If a previous surface exists, also send a leave event. */
if (info->focus_surface)
{
DebugPrint ("sending leave to text input %p", input);
XLAssert (info->focus_surface->resource != NULL);
zwp_text_input_v3_send_leave (input->resource,
info->focus_surface->resource);
InputDoLeave (input, info->focus_surface);
}
DebugPrint ("sending enter to text input %p", input);
/* Send the enter event to each text input. */
zwp_text_input_v3_send_enter (input->resource,
surface->resource);
InputDoEnter (input, surface);
input = input->next;
}
/* Record the focus surface. Note that this surface should always
be removed by ClearFocusSurface in the seat upon destruction, so
there is no need for a callback to be registered here as well!
If that invariant is broken, strange bugs will follow. */
info->focus_surface = surface;
}
static void
NoticeLeave (TextInputClientInfo *info)
{
TextInput *input;
/* If there is already no focus surface, return. */
if (!info->focus_surface)
return;
DebugPrint ("client info: %p", info);
input = info->inputs.next;
while (input != &info->inputs)
{
DebugPrint ("sending leave to text input %p", input);
/* Otherwise, if info->focus_surface->resource is still
there, send the leave event to each text input. */
if (info->focus_surface->resource)
/* Send the enter event to each text input. */
zwp_text_input_v3_send_leave (input->resource,
info->focus_surface->resource);
InputDoLeave (input, info->focus_surface);
input = input->next;
}
/* And clear the focus surface. */
info->focus_surface = NULL;
}
static TextInputClientInfo *
GetClientInfo (struct wl_client *client, Seat *seat, Bool create)
{
TextInputClientInfo *info;
/* First, look through the list of client infos. */
info = all_client_infos.next;
while (info != &all_client_infos)
{
if (info->seat == seat && info->client == client)
return info;
info = info->next;
}
if (!create)
return NULL;
/* If none was found, create one and link it onto the list. */
info = XLCalloc (1, sizeof *info);
info->seat = seat;
info->client = client;
info->next = all_client_infos.next;
info->last = &all_client_infos;
all_client_infos.next->last = info;
all_client_infos.next = info;
/* Then, attach the seat destruction listener and initialize the
list of text input objects. */
info->seat_key
= XLSeatRunOnDestroy (seat, HandleSeatDestroyed, info);
info->inputs.next = &info->inputs;
/* And return info. */
return info;
}
static void
Destroy (struct wl_client *client, struct wl_resource *resource)
{
wl_resource_destroy (resource);
}
/* Forward declaration. */
static void FocusInCallback (Seat *, Surface *);
static void
GetTextInput (struct wl_client *client, struct wl_resource *resource,
uint32_t id, struct wl_resource *seat_resource)
{
Seat *seat;
struct wl_resource *dummy;
TextInput *input;
TextInputClientInfo *info;
seat = wl_resource_get_user_data (seat_resource);
/* If the seat is inert, we cannot rely on destroy callbacks being
run. In that case, we make a dummy text input resource with no
data attached. */
if (XLSeatIsInert (seat))
{
dummy = wl_resource_create (client, &zwp_text_input_v3_interface,
wl_resource_get_version (resource), id);
if (!dummy)
wl_resource_post_no_memory (resource);
else
wl_resource_set_implementation (dummy, &input_impl, NULL, NULL);
return;
}
/* Create the text input. */
input = XLSafeMalloc (sizeof *input);
if (!input)
{
wl_resource_post_no_memory (resource);
return;
}
memset (input, 0, sizeof *input);
input->resource
= wl_resource_create (client, &zwp_text_input_v3_interface,
wl_resource_get_version (resource), id);
if (!input->resource)
{
XLFree (input);
wl_resource_post_no_memory (resource);
return;
}
/* Obtain the client info. */
info = GetClientInfo (client, seat, True);
/* Set the implementation. N.B. that HandleResourceDestroy will
free the client info structure once all references are gone. */
wl_resource_set_implementation (input->resource, &input_impl,
input, HandleResourceDestroy);
/* Initialize and link the text input. */
input->client_info = info;
input->next = info->inputs.next;
input->last = &info->inputs;
info->inputs.next->last = input;
info->inputs.next = input;
/* If there is already a focused surface on the seat belonging to
the client, focus it now. */
if (info->focus_surface)
{
DebugPrint ("focusing newly created text input %p", input);
/* The info already existed and already has a focus surface
set. */
zwp_text_input_v3_send_enter (input->resource,
info->focus_surface->resource);
InputDoEnter (input, info->focus_surface);
}
else if (XLSeatIsClientFocused (seat, client))
{
DebugPrint ("focusing newly created text input with info %p", input);
/* The info did not previously exist, but the client created a
surface that is the seat's input focus. */
FocusInCallback (seat, XLSeatGetFocus (seat));
}
}
static const struct zwp_text_input_manager_v3_interface manager_impl =
{
.destroy = Destroy,
.get_text_input = GetTextInput,
};
static void
HandleBind (struct wl_client *client, void *data, uint32_t version,
uint32_t id)
{
struct wl_resource *resource;
resource = wl_resource_create (client,
&zwp_text_input_manager_v3_interface,
version, id);
if (!resource)
{
wl_client_post_no_memory (client);
return;
}
wl_resource_set_implementation (resource, &manager_impl,
NULL, NULL);
}
static PreeditBuffer *
MakePreeditBuffer (const char *locale)
{
PreeditBuffer *buffer;
buffer = XLCalloc (1, sizeof *buffer);
buffer->locale = XLStrdup (locale);
return buffer;
}
static void
FreePreeditBuffer (PreeditBuffer *buffer)
{
XLFree (buffer->buffer);
XLFree (buffer->locale);
XLFree (buffer);
}
static Bool
PreeditDeleteChars (PreeditBuffer *buffer, int start_char,
int length)
{
wchar_t wc;
char *oldlocale, *start, *end;
int rc, chars, old_chars;
/* Note that preedit buffers operate on text encoded with the IM
locale's charset. Record the old locale. */
oldlocale = XLStrdup (setlocale (LC_CTYPE, NULL));
/* Switch to the new locale. */
if (!setlocale (LC_CTYPE, buffer->locale))
{
XLFree (oldlocale);
return False;
}
start = buffer->buffer;
chars = 0;
/* Increase start until we reach start_char. */
while (chars < start_char)
{
if (start >= buffer->buffer + buffer->size)
{
DebugPrint ("start %p out of bounds %p",
start, buffer->buffer + buffer->size);
/* start is out of bounds. */
goto failure;
}
/* After this, rc should either be -1 (meaning failure) or the
number of bytes read. */
rc = mbtowc (&wc, start, buffer->buffer + buffer->size - start);
chars++;
DebugPrint ("mbtowc gave (calculating start) %d", rc);
/* If rc is not -1, move start forward by that much. */
if (rc != -1)
start += rc;
else
goto failure;
}
DebugPrint ("chars: %d, start, %p", chars, start);
/* Now, start is the first byte of the area we want to delete.
Count forward by length. */
end = start;
old_chars = chars;
while (chars < old_chars + length)
{
if (end >= buffer->buffer + buffer->size)
{
DebugPrint ("end %p out of bounds %p",
end, buffer->buffer + buffer->size);
/* end is out of bounds. */
goto failure;
}
/* After this, rc should either be -1 (meaning failure) or the
number of bytes read. */
rc = mbtowc (&wc, end, buffer->buffer + buffer->size - end);
chars++;
DebugPrint ("mbtowc gave (calculating end) %d", rc);
/* If rc is not -1, move start forward by that much. */
if (rc != -1)
end += rc;
else
goto failure;
}
DebugPrint ("chars: %d, end, %p", chars, end);
/* Now, delete the area between start and end, by moving the bytes
between end and the end of the buffer to start. */
memmove (start, end, buffer->buffer + buffer->size - end);
/* Resize the buffer. */
buffer->size -= end - start;
buffer->total_characters -= length;
XLAssert (buffer->size >= 0);
buffer->buffer = XLRealloc (buffer->buffer,
buffer->size);
/* Restore the locale and return success. */
setlocale (LC_CTYPE, oldlocale);
XLFree (oldlocale);
/* Reset the shift state. */
mbtowc (NULL, NULL, 0);
return True;
failure:
setlocale (LC_CTYPE, oldlocale);
XLFree (oldlocale);
/* Reset the shift state. */
mbtowc (NULL, NULL, 0);
return False;
}
static Bool
PreeditInsertChars (PreeditBuffer *buffer, int start_char,
const char *string, size_t length,
int char_length)
{
wchar_t wc;
char *oldlocale, *start;
int rc, chars;
/* Note that preedit buffers operate on text encoded with the IM
locale's charset. Record the old locale. */
oldlocale = XLStrdup (setlocale (LC_CTYPE, NULL));
/* Switch to the new locale. */
if (!setlocale (LC_CTYPE, buffer->locale))
{
XLFree (oldlocale);
return False;
}
/* Resize the buffer accordingly. */
buffer->buffer = XLRealloc (buffer->buffer,
buffer->size + length);
start = buffer->buffer;
chars = 0;
/* Increase start until we reach start_char. */
while (chars < start_char)
{
if (start >= buffer->buffer + buffer->size)
/* start is out of bounds. */
goto failure;
/* After this, rc should either be -1 (meaning failure) or the
number of bytes read. */
rc = mbtowc (&wc, start, buffer->buffer + buffer->size - start);
chars++;
/* If rc is not -1, move start forward by that much. */
if (rc != -1)
start += rc;
else
goto failure;
}
/* Move everything past start length away. */
memmove (start + length, start,
buffer->buffer + buffer->size - start);
buffer->size += length;
buffer->total_characters += char_length;
/* Copy the text onto start. */
memcpy (start, string, length);
setlocale (LC_CTYPE, oldlocale);
XLFree (oldlocale);
/* Reset the shift state. */
mbtowc (NULL, NULL, 0);
return True;
failure:
setlocale (LC_CTYPE, oldlocale);
XLFree (oldlocale);
/* Reset the shift state. */
mbtowc (NULL, NULL, 0);
return False;
}
/* Forward declarations. */
static char *ConvertString (char *, size_t, size_t *);
static void PreeditString (TextInput *, const char *, size_t, ptrdiff_t);
static void
UpdatePreedit (TextInput *input)
{
char *buffer;
size_t new_text_size;
TextPosition caret;
if (input->buffer)
{
/* Convert the preedit text. */
buffer = ConvertString (input->buffer->buffer,
input->buffer->size,
&new_text_size);
DebugPrint ("updated buffer %p", buffer);
if (!buffer)
goto no_buffer;
/* Obtain the caret position. */
if (input->caret_style != XIMIsInvisible)
caret = TextPositionFromCharPosition (buffer, new_text_size,
input->caret);
else
/* The caret is hidden, so don't send any caret position. */
caret.bytepos = -1, caret.charpos = -1;
DebugPrint ("caret position is: char %d, byte: %td",
caret.charpos, caret.bytepos);
PreeditString (input, buffer, new_text_size,
/* caret.bytepos will be -1 if obtaining the
position failed or the caret is hidden. */
caret.bytepos);
XLFree (buffer);
}
else
{
no_buffer:
DebugPrint ("no buffer");
/* Clear the preedit string. */
zwp_text_input_v3_send_preedit_string (input->resource, NULL,
-1, -1);
zwp_text_input_v3_send_done (input->resource, input->serial);
}
}
static int
PreeditStartCallback (XIC ic, XPointer client_data, XPointer call_data)
{
TextInput *input;
const char *locale;
XLAssert (current_xim != NULL);
input = (TextInput *) client_data;
locale = XLocaleOfIM (current_xim);
DebugPrint ("text input: %p; locale: %s", input, locale);
if (input->buffer)
FreePreeditBuffer (input->buffer);
/* Create the preedit buffer. */
input->buffer = MakePreeditBuffer (locale);
/* Set the default caret style. */
input->caret_style = XIMIsPrimary;
/* There should be no limit on the number of bytes in a preedit
string. We make the string fit in 4000 bytes ourselves. */
return -1;
}
static void
PreeditDoneCallback (XIC ic, XPointer client_data, XPointer call_data)
{
TextInput *input;
input = (TextInput *) client_data;
DebugPrint ("text input: %p", input);
if (input->buffer)
FreePreeditBuffer (input->buffer);
input->buffer = NULL;
/* Send change to the client. */
UpdatePreedit (input);
}
static char *
ConvertWcharString (PreeditBuffer *buffer, const wchar_t *input,
size_t input_size, size_t *string_size)
{
char *output, *oldlocale;
int rc;
size_t bytes;
/* Since the text is intended for BUFFER, switch to BUFFER's
locale. */
oldlocale = XLStrdup (setlocale (LC_CTYPE, NULL));
/* Switch to the new locale. */
if (!setlocale (LC_CTYPE, buffer->locale))
{
/* Setting the locale failed. Return an empty string. */
XLFree (oldlocale);
*string_size = 0;
return NULL;
}
output = XLCalloc (input_size + 1, MB_CUR_MAX);
bytes = 0;
while (input_size)
{
input_size--;
rc = wctomb (output + bytes, *input++);
if (rc == -1)
/* Invalid wide character code. */
continue;
/* Otherwise, move the string forward this much. */
bytes += rc;
}
/* Return the string and the number of bytes put in it. */
*string_size = bytes;
/* Clear shift state. */
wctomb (NULL, L'\0');
/* Restore the old locale. */
setlocale (LC_CTYPE, oldlocale);
XLFree (oldlocale);
return output;
}
static void
PreeditDrawCallback (XIC ic, XPointer client_data,
XIMPreeditDrawCallbackStruct *call_data)
{
TextInput *input;
size_t string_size;
char *multi_byte_string;
input = (TextInput *) client_data;
DebugPrint ("text input: %p", input);
if (!input->buffer)
return;
DebugPrint ("chg_first: %d, chg_length: %d",
call_data->chg_first,
call_data->chg_length);
/* Delete text between chg_first and chg_first + chg_length. */
if (call_data->chg_length
&& !PreeditDeleteChars (input->buffer, call_data->chg_first,
call_data->chg_length))
{
DebugPrint ("text deletion failed");
return;
}
if (call_data->text)
{
if (call_data->text->encoding_is_wchar)
{
DebugPrint ("converting wide character string");
multi_byte_string
= ConvertWcharString (input->buffer,
call_data->text->string.wide_char,
call_data->text->length,
&string_size);
}
else
{
/* The multibyte string should be NULL terminated. */
string_size = strlen (call_data->text->string.multi_byte);
multi_byte_string = call_data->text->string.multi_byte;
}
DebugPrint ("inserting text of size %d, %zu",
call_data->text->length, string_size);
/* Now, insert whatever text was specified at chg_first. */
if (!PreeditInsertChars (input->buffer, call_data->chg_first,
multi_byte_string, string_size,
call_data->text->length))
DebugPrint ("insertion failed");
if (call_data->text->encoding_is_wchar)
/* We must free the conversion results. */
XLFree (multi_byte_string);
}
/* Now set the caret position. */
input->caret = call_data->caret;
DebugPrint ("buffer text is now: %.*s, with the caret at %d",
(int) input->buffer->size, input->buffer->buffer,
input->caret);
/* Send change to the client. */
UpdatePreedit (input);
}
static void
PreeditCaretCallback (XIC ic, XPointer client_data,
XIMPreeditCaretCallbackStruct *call_data)
{
TextInput *input;
input = (TextInput *) client_data;
if (!input->buffer)
return;
DebugPrint ("text input: %p; direction: %u", input,
call_data->direction);
switch (call_data->direction)
{
case XIMAbsolutePosition:
input->caret = call_data->position;
break;
case XIMForwardChar:
input->caret = MIN (input->caret + 1,
input->buffer->total_characters);
break;
case XIMBackwardChar:
input->caret = MAX (input->caret - 1, 0);
break;
/* The rest cannot be implemented under Wayland as the text
input protocol is too limited. */
default:
DebugPrint ("unsupported movement direction");
}
/* Return the caret position. */
call_data->position = input->caret;
/* Set the caret style. */
input->caret_style = call_data->style;
/* Send change to the client. */
UpdatePreedit (input);
}
static TextPosition
ScanForwardWord (const char *string, size_t string_size,
TextPosition caret, int factor)
{
const char *start;
Bool punct_found;
TextPosition caret_before;
start = string + caret.bytepos;
/* Skip initial whitespace. */
while (start < string + string_size
/* Make sure the character has 0 trailing bytes. */
&& *start < 127 && *start >= 0
&& (isspace (*start) || ispunct (*start)))
{
start++;
caret.charpos++;
caret.bytepos++;
}
while (start < string + string_size)
{
punct_found = False;
caret_before = caret;
start += CountOctets (*start);
if (start >= string + string_size)
{
/* The string is too big. */
caret.bytepos = -1;
caret.charpos = -1;
return caret;
}
else
caret.bytepos = start - string;
caret.charpos++;
/* Eat all punctuation. */
while (isspace (*start) || ispunct (*start))
{
punct_found = True;
if (++start >= string + string_size)
/* We are now at the end of the string, so just return the
position of caret_before, which should be before this
extraneous punctuation. */
return caret_before;
/* Move the caret forward. */
caret.charpos++;
caret.bytepos++;
}
if (punct_found && !(--factor))
{
/* Punctuation was seen and factor is now 0. Return the
caret before the punctuation. */
DebugPrint ("returning caret_before: char: %d byte: %td",
caret_before.charpos, caret_before.bytepos);
return caret_before;
}
/* Simply return the current position at the end of the
string. */
if (start == string + string_size - 1)
{
DebugPrint ("returning caret_before at end of string: char: %d byte: %td",
caret_before.charpos, caret_before.bytepos);
return caret_before;
}
}
return caret;
}
static Bool
IsLeading (char c)
{
return (((unsigned char) c) & 0b11000000
|| !(c >> 7));
}
static TextPosition
ScanBackwardWord (const char *string, size_t string_size,
TextPosition caret, int factor)
{
TextPosition original, caret_before;
const char *start;
Bool punct_found;
/* Record the original caret position. */
original = caret;
if (!string_size)
{
/* The string is empty, so simply return the start of the
string. */
caret.charpos = 0;
caret.bytepos = 0;
return caret;
}
/* First, skip all whitespace. */
start = string + caret.bytepos;
while (start >= string
/* Make sure the character has 0 trailing bytes. */
&& *start < 127 && *start >= 0
&& (isspace (*start) || ispunct (*start)))
{
start--;
caret.charpos--;
caret.bytepos--;
if (caret.charpos <= 0 || caret.bytepos <= 0)
return original;
}
/* Next, look backwards. Every time whitespace is encountered,
gobble it up, and decrease factor. Once factor is 0, return the
caret position before the first whitespace character. Otherwise,
repeat. */
while (start >= string)
{
caret_before = caret;
do
{
if (--start < string)
{
/* Invalid UTF-8 data was found in STRING. Just return
the start of the string in this case. */
caret.charpos = 0;
caret.bytepos = 0;
return caret;
}
caret.bytepos--;
}
while (!IsLeading (*start));
caret.charpos--;
DebugPrint ("caret_before: char: %d byte: %td, new char: %c",
caret_before.charpos, caret_before.bytepos,
*start);
/* We are now at the start of the last character. If it is
whitespace, eat the whitespace and decrease factor. */
punct_found = False;
while (isspace (*start) || ispunct (*start))
{
do
{
if (--start < string)
{
/* Invalid UTF-8 data was found in STRING. Just return
the start of the string in this case. */
caret.charpos = 0;
caret.bytepos = 0;
return caret;
}
caret.bytepos--;
}
while (!IsLeading (*start));
caret.charpos--;
punct_found = True;
}
if (punct_found && !(--factor))
{
/* Punctuation was seen and the factor is now 0. Return the
caret before the punctuation. */
DebugPrint ("returning caret_before: char: %d byte: %td",
caret_before.charpos, caret_before.bytepos);
return caret_before;
}
}
return caret;
}
static void
FindTextSections (const char *string, size_t string_size,
TextPosition caret, XIMCaretDirection direction,
int factor, TextPosition *start_return,
TextPosition *end_return)
{
TextPosition end;
const char *found;
switch (direction)
{
case XIMForwardChar:
/* Move forward by factor. */
end = TextPositionFromCharPosition (string, string_size,
caret.charpos + factor);
break;
case XIMBackwardChar:
/* Move backwards by factor. */
end = TextPositionFromCharPosition (string, string_size,
MAX (0, caret.charpos - factor));
break;
case XIMForwardWord:
/* Move forwards by factor words. */
end = ScanForwardWord (string, string_size, caret, factor);
break;
case XIMBackwardWord:
/* Move backwards by factor words. */
end = ScanBackwardWord (string, string_size, caret, factor);
break;
case XIMLineStart:
/* Scan backwards for factor newline characters. */
found = string + caret.bytepos;
DebugPrint ("start: found %p, found-string %td",
found, found - string);
while (factor)
{
found = memrchr (string, '\n', found - string);
DebugPrint ("LineStart processing found %p %td", found,
found ? found - string : 0);
if (!found)
{
/* Use the beginning of the string. */
found = string - 1;
/* Exit the loop too. */
goto end_line_start;
}
factor--;
}
end_line_start:
DebugPrint ("found %p string %p found+1-string %td",
found, string, found + 1 - string);
end = TextPositionFromBytePosition (string, string_size,
found + 1 - string);
break;
case XIMLineEnd:
/* Scan forwards for factor newline characters. */
found = string + caret.bytepos;
while (factor)
{
found = memchr (found + 1, '\n',
(string + string_size - 1) - found + 1);
if (!found)
{
/* Use the end of the string. */
found = string + string_size - 1;
goto end_line_end;
}
factor--;
}
end_line_end:
end = TextPositionFromBytePosition (string, string_size,
found - 1 - string);
break;
default:
DebugPrint ("unsuported string conversion direction: %u",
direction);
end.bytepos = 0;
end.charpos = 0;
}
DebugPrint ("end: char: %d byte: %td", end.charpos,
end.bytepos);
if (caret.charpos > end.charpos)
*start_return = end, *end_return = caret;
else
*start_return = caret, *end_return = end;
}
static Bool
MoveCaret (TextPosition *caret, const char *buffer, size_t buffer_size,
int by)
{
const char *end, *start;
int octets;
XLAssert (caret->bytepos <= buffer_size);
if (by > 0)
{
end = buffer + buffer_size;
buffer += caret->bytepos;
while (by && buffer < end)
{
octets = CountOctets (*buffer);
/* Move the buffer and text position forwards. */
buffer += octets;
caret->bytepos += octets;
caret->charpos++;
by--;
}
/* If caret->bytepos is too large, return failure. */
if (buffer > end)
return False;
}
else if (by < 0)
{
/* Move the buffer and text position backwards. */
start = buffer + caret->bytepos;
while (by && start >= buffer)
{
do
{
start--;
caret->bytepos -= 1;
if (start < buffer)
return False;
}
while (!IsLeading (*start));
caret->charpos--;
by--;
}
}
return True;
}
static char *
EncodeIMString (const char *input, size_t input_size, int *chars)
{
iconv_t cd;
char *oldlocale, *locale;
size_t rc;
ptrdiff_t size;
char *outbuf, *outptr, *end;
size_t outsize, outbytesleft;
int nchars, num_chars_read;
wchar_t wc;
char *inbuf;
/* Encode the given input string in the IM coding system, and then
return a NULL terminated buffer and the number of characters, or
NULL if the conversion failed. */
DebugPrint ("encoding string %.*s", (int) input_size, input);
/* Switch to the input method locale. */
locale = XLocaleOfIM (current_xim);
oldlocale = XLStrdup (setlocale (LC_CTYPE, NULL));
if (!setlocale (LC_CTYPE, locale))
{
/* Switching to the new locale failed. */
XLFree (oldlocale);
return NULL;
}
/* First, try creating a conversion descriptor. */
cd = iconv_open (nl_langinfo (CODESET), "UTF-8");
/* If creating the cd failed, bail out. */
if (cd == (iconv_t) -1)
{
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wanalyzer-malloc-leak"
#endif
/* Restore the old locale. */
if (!setlocale (LC_CTYPE, oldlocale))
abort ();
XLFree (oldlocale);
return NULL;
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
}
/* Otherwise, start converting. */
outbuf = XLMalloc (BUFSIZ + 1);
outptr = outbuf;
outsize = BUFSIZ;
outbytesleft = outsize;
inbuf = (char *) input;
while (input_size > 0)
{
rc = iconv (cd, &inbuf, &input_size, &outptr,
&outbytesleft);
DebugPrint ("iconv gave: %zu", rc);
if (rc == (size_t) -1)
{
/* See what went wrong. */
if (errno == E2BIG)
{
/* Reallocate the output buffer. */
outbuf = XLRealloc (outbuf, outsize + BUFSIZ + 1);
/* Move the outptr to the right location in the new
outbuf. */
outptr = outbuf + outsize - outbytesleft;
/* Expand outsize and outbytesleft. */
outsize += BUFSIZ;
outbytesleft += BUFSIZ;
DebugPrint ("expanding outsize to %zu, outbytesleft now %zu",
outsize, outbytesleft);
}
else
{
/* An error occured while encoding the string.
Normally, this is not such a big deal, but the number
of characters in the string is later counted with
mbtowc. So, simply bail out. */
DebugPrint ("iconv failed: %s", strerror (errno));
XLFree (outbuf);
iconv_close (cd);
/* Restore the old locale. */
if (!setlocale (LC_CTYPE, oldlocale))
abort ();
XLFree (oldlocale);
return NULL;
}
}
}
/* The conversion finished. */
DebugPrint ("conversion finished, size_out %zu",
outsize - outbytesleft);
/* Now, count the number of multibyte characters. */
nchars = 0;
end = outbuf;
size = outsize - outbytesleft;
while (end < outbuf + size)
{
num_chars_read = mbtowc (&wc, end, outbuf + size - end);
nchars++;
if (num_chars_read != -1)
end += num_chars_read;
else
{
DebugPrint ("mbtowc failed");
XLFree (outbuf);
iconv_close (cd);
/* Restore the old locale. */
if (!setlocale (LC_CTYPE, oldlocale))
abort ();
XLFree (oldlocale);
return NULL;
}
}
/* Reset the shift state and return the number of characters. */
mbtowc (NULL, NULL, 0);
*chars = nchars;
/* Close the cd. */
iconv_close (cd);
/* Restore the old locale. */
if (!setlocale (LC_CTYPE, oldlocale))
abort ();
XLFree (oldlocale);
/* Return the output buffer. */
return outbuf;
}
static void
StringConversionCallback (XIC ic, XPointer client_data,
XIMStringConversionCallbackStruct *call_data)
{
TextInput *input;
TextPosition start, end, caret;
short position;
size_t length;
char *buffer;
int num_characters;
int bytes_before, bytes_after;
input = (TextInput *) client_data;
/* Clear some members of the returned text structure. */
call_data->text->feedback = NULL;
call_data->text->encoding_is_wchar = False;
if (!(input->current_state.pending & PendingSurroundingText))
return;
DebugPrint ("string conversion; position: %d, factor: %d"
" operation: %u", (short) call_data->position,
call_data->factor, call_data->operation);
/* Obtain the actual caret position. */
caret = input->current_state.cursor;
DebugPrint ("current caret position: char: %d, byte: %td",
caret.charpos, caret.bytepos);
if (caret.charpos < 0 || caret.bytepos < 0)
goto failure;
/* This is unsigned short in Xlib.h but the spec says it should be
signed. */
position = (short) call_data->position;
/* Move the caret by position. */
length = strlen (input->current_state.surrounding_text);
/* If the string is too small, just fail. */
if (!length)
goto failure;
if (!MoveCaret (&caret, input->current_state.surrounding_text,
length, position))
{
DebugPrint ("failed to move caret position");
goto failure;
}
if (call_data->factor < 1)
goto failure;
DebugPrint ("new caret position: char %d, byte: %td",
caret.charpos, caret.bytepos);
/* Now, obtain the start and end of the text to return. */
FindTextSections (input->current_state.surrounding_text,
length, caret, call_data->direction,
call_data->factor, &start, &end);
DebugPrint ("start: %d, %td, end: %d, %td",
start.charpos, start.bytepos, end.charpos,
end.bytepos);
/* If either of those positions are invalid, signal failure. */
if (start.charpos < 0 || start.bytepos < 0
|| end.charpos < 0 || end.bytepos < 0)
goto failure;
/* Verify that some assumptions hold. */
XLAssert (start.bytepos <= end.bytepos && end.bytepos < length);
/* Extract and encode the contents of the string. */
buffer = EncodeIMString ((input->current_state.surrounding_text
+ start.bytepos),
end.bytepos - start.bytepos + 1,
&num_characters);
/* Return those characters. */
if (buffer)
{
call_data->text->length = MIN (USHRT_MAX, num_characters);
call_data->text->string.mbs = buffer;
}
else
goto failure;
DebugPrint ("returned text: %s", buffer);
if (call_data->operation == XIMStringConversionSubstitution)
{
/* Also tell the client to delete the extracted part of the
buffer. First, calculate how much start extends behind the
cursor. This is an approximation; it assumes that the
portion of text to change always contains the caret, which is
not guaranteed to be the case if the IM specified an offset,
direction, and factor that resulted in none of the text
between start and end containing the caret. */
caret = input->current_state.cursor;
if (start.bytepos < caret.bytepos)
bytes_before = caret.bytepos - start.bytepos;
else
bytes_before = 0;
if (end.bytepos > caret.bytepos)
bytes_after = end.bytepos - caret.bytepos;
else
bytes_after = 0;
DebugPrint ("deleting: %d %d", bytes_before, bytes_after);
zwp_text_input_v3_send_delete_surrounding_text (input->resource,
bytes_before,
bytes_after);
zwp_text_input_v3_send_done (input->resource, input->serial);
}
return;
failure:
/* Return a string of length 0. This assumes XFree is able to free
data allocated with our malloc wrapper. */
call_data->text->length = 0;
call_data->text->string.mbs = XLMalloc (0);
}
static void
CreateIC (TextInput *input)
{
XVaNestedList status_attr, preedit_attr;
XPoint spot;
XRectangle rect;
Window window;
XIMCallback preedit_start_callback;
XIMCallback preedit_draw_callback;
XIMCallback preedit_done_callback;
XIMCallback preedit_caret_callback;
XIMCallback string_conversion_callback;
unsigned long additional_events;
if (!current_xim)
return;
if (!input->client_info)
return;
if (!input->client_info->focus_surface)
return;
window = XLWindowFromSurface (input->client_info->focus_surface);
if (!window)
return;
XLAssert (!input->xic);
DebugPrint ("creating XIC for text input %p and window 0x%lx", input,
window);
status_attr = NULL;
preedit_attr = NULL;
/* Create an XIC for the given text input. */
if (xim_style & XIMPreeditPosition)
{
DebugPrint ("IM wants spot values for preedit window");
if (input->current_state.pending & PendingCursorRectangle)
{
spot.x = CurrentCursorX (input);
spot.y = (CurrentCursorY (input)
+ CurrentCursorHeight (input));
}
else
{
spot.x = 0;
spot.y = 1;
}
DebugPrint ("using spot: %d, %d", spot.x, spot.y);
preedit_attr = XVaCreateNestedList (0, XNSpotLocation, &spot,
XNFontSet, im_fontset, NULL);
}
else if (xim_style & XIMPreeditArea)
{
DebugPrint ("IM wants geometry negotiation");
/* Use some dummy values, and then negotiate geometry after the
XIC is created. */
rect.x = 0;
rect.y = 0;
rect.height = 1;
rect.width = 1;
preedit_attr = XVaCreateNestedList (0, XNArea, &rect, XNFontSet,
im_fontset, NULL);
}
else if (xim_style & XIMPreeditCallbacks)
{
DebugPrint ("IM wants preedit callbacks");
preedit_start_callback.client_data = (XPointer) input;
preedit_done_callback.client_data = (XPointer) input;
preedit_draw_callback.client_data = (XPointer) input;
preedit_caret_callback.client_data = (XPointer) input;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-function-type"
preedit_start_callback.callback = (XIMProc) PreeditStartCallback;
preedit_done_callback.callback = (XIMProc) PreeditDoneCallback;
preedit_draw_callback.callback = (XIMProc) PreeditDrawCallback;
preedit_caret_callback.callback = (XIMProc) PreeditCaretCallback;
#pragma GCC diagnostic pop
preedit_attr = XVaCreateNestedList (0, XNPreeditStartCallback,
&preedit_start_callback,
XNPreeditDoneCallback,
&preedit_done_callback,
XNPreeditDrawCallback,
&preedit_draw_callback,
XNPreeditCaretCallback,
&preedit_caret_callback,
NULL);
}
if (xim_style & XIMStatusArea)
{
DebugPrint ("IM wants geometry negotiation for status area");
/* Use some dummy values, and then negotiate geometry after the
XIC is created. */
rect.x = 0;
rect.y = 0;
rect.height = 1;
rect.width = 1;
status_attr = XVaCreateNestedList (0, XNArea, &rect, XNFontSet,
im_fontset, NULL);
}
DebugPrint ("preedit attr: %p, status attr: %p",
preedit_attr, status_attr);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-function-type"
string_conversion_callback.client_data = (XPointer) input;
string_conversion_callback.callback = (XIMProc) StringConversionCallback;
#pragma GCC diagnostic pop
if (preedit_attr && status_attr)
input->xic = XCreateIC (current_xim, XNInputStyle, xim_style,
XNClientWindow, window, XNFocusWindow,
window, XNStatusAttributes, status_attr,
XNPreeditAttributes, preedit_attr,
XNStringConversionCallback,
&string_conversion_callback,
NULL);
else if (preedit_attr)
input->xic = XCreateIC (current_xim, XNInputStyle, xim_style,
XNClientWindow, window, XNFocusWindow,
window, XNPreeditAttributes, preedit_attr,
XNStringConversionCallback,
&string_conversion_callback,
NULL);
else if (status_attr)
input->xic = XCreateIC (current_xim, XNInputStyle, xim_style,
XNClientWindow, window, XNFocusWindow,
window, XNStatusAttributes, status_attr,
XNStringConversionCallback,
&string_conversion_callback,
NULL);
else
input->xic = XCreateIC (current_xim, XNInputStyle, xim_style,
XNClientWindow, window, XNFocusWindow,
window, XNStringConversionCallback,
&string_conversion_callback,
NULL);
/* Select for additional events should the IC have been successfully
created. Note that we do not deselect for the extra event mask
anywhere; the events an input method makes us select for should
be benign enough. */
if (input->xic)
{
additional_events = NoEventMask;
if (!XGetICValues (input->xic, XNFilterEvents,
&additional_events, NULL)
&& additional_events)
{
DebugPrint ("selecting for additional event mask: %lx",
additional_events);
XLSurfaceSelectExtraEvents (input->client_info->focus_surface,
additional_events);
}
}
/* Free the nested lists. */
if (status_attr)
XFree (status_attr);
if (preedit_attr)
XFree (preedit_attr);
DebugPrint ("created IC %p", input->xic);
}
static void
IMDestroyCallback (XIM im, XPointer client_data, XPointer call_data)
{
TextInputClientInfo *info;
TextInput *input;
DebugPrint ("XIM %p destroyed", im);
if (im != current_xim)
/* Is this even possible? */
return;
/* The XIM was destroyed, and all XICs have been freed. Clear all
fields still referencing XICs or XIMs. */
current_xim = NULL;
/* Close the cd. */
if (current_cd != (iconv_t) -1)
iconv_close (current_cd);
current_cd = (iconv_t) -1;
/* Clear the XIC field of each input. */
info = all_client_infos.next;
while (info != &all_client_infos)
{
input = info->inputs.next;
while (input != &info->inputs)
{
/* Destroy the XIC of this one input. */
if (input->xic)
{
input->xic = NULL;
/* Handle IC destruction. */
HandleICDestroyed (input);
}
/* Move to the next input. */
input = input->next;
}
/* Move to the next client info. */
info = info->next;
}
DebugPrint ("finished XIM destruction");
}
static XIMStyle
CheckStyle (XIMStyles *styles, XIMStyle preedit_style,
XIMStyle status_style)
{
int i;
/* Is this preedit & status style combination supported? */
for (i = 0; i < styles->count_styles; i++)
{
if ((styles->supported_styles[i] & preedit_style)
&& (styles->supported_styles[i] & status_style))
return styles->supported_styles[i];
}
return 0;
}
static void
CheckStyles (XIM xim)
{
XIMStyles *styles;
XIMStyle style;
int i;
/* Pick a supported XIM style from the current input method. The
following input styles are supported:
over-the-spot, where the preedit is displayed in a window at a
given position.
off-the-spot, where the preedit is displayed in a window
somewhere inside the application window.
root-window, where the preedit is displayed is displayed in a
window that is a child of the root window.
on-the-spot, where the preedit is displayed inside the
application window. */
if (XGetIMValues (xim, XNQueryInputStyle, &styles, NULL))
{
/* An error occured; default to none. */
xim_style = XIMPreeditNone | XIMStatusNone;
return;
}
/* Otherwise, find the best style in our order of preference. */
for (i = 0; xim_style_order[i] != XimStyleNone; ++i)
{
DebugPrint ("considering style: %d", (int) xim_style_order[i]);
switch (xim_style_order[i])
{
case XimOverTheSpot:
DebugPrint ("checking for over-the-spot");
style = CheckStyle (styles, XIMPreeditPosition,
XIMStatusArea | XIMStatusNothing | XIMStatusNone);
if (style)
goto done;
break;
case XimOffTheSpot:
DebugPrint ("checking for off-the-spot");
style = CheckStyle (styles, XIMPreeditArea,
XIMStatusArea | XIMStatusNothing | XIMStatusNone);
if (style)
goto done;
break;
case XimRootWindow:
DebugPrint ("checking for root-window");
style = CheckStyle (styles, XIMPreeditNothing,
XIMStatusNothing | XIMStatusNone);
if (style)
goto done;
break;
case XimOnTheSpot:
DebugPrint ("checking for on-the-spot");
style = CheckStyle (styles, XIMPreeditCallbacks,
XIMStatusArea | XIMStatusNothing | XIMStatusNone);
if (style)
goto done;
break;
case XimStyleNone:
/* This shouldn't happen. */
abort ();
}
}
DebugPrint ("checking for input method styles failed");
/* No style could be found, so fall back to XIMPreeditNone and
XIMStatusNone. */
style = XIMPreeditNone | XIMStatusNone;
done:
DebugPrint ("set styles to: %lu", (unsigned long) style);
XFree (styles);
xim_style = style;
}
static void
HandleNewIM (XIM xim)
{
TextInputClientInfo *info;
TextInput *input;
const char *locale;
char *oldlocale, *coding;
iconv_t cd;
XIMCallback destroy_callback;
/* A new input method is available; destroy the XIC of every text
input. */
info = all_client_infos.next;
while (info != &all_client_infos)
{
input = info->inputs.next;
while (input != &info->inputs)
{
/* Destroy the XIC of this one input. */
if (input->xic)
{
XDestroyIC (input->xic);
input->xic = NULL;
/* Handle IC destruction. */
HandleICDestroyed (input);
}
/* Move to the next input. */
input = input->next;
}
/* Move to the next client info. */
info = info->next;
}
/* Now, it is okay to delete the current XIM. */
if (current_xim)
XCloseIM (current_xim);
current_xim = NULL;
/* And its cd. */
if (current_cd != (iconv_t) -1)
iconv_close (current_cd);
current_cd = (iconv_t) -1;
/* Obtain the locale of the new input method. */
locale = XLocaleOfIM (xim);
/* Temporarily switch to the new locale to determine its coded
character set. */
oldlocale = XLStrdup (setlocale (LC_ALL, NULL));
if (!setlocale (LC_ALL, locale))
{
/* The locale specified by the input method couldn't be set. */
XLFree (oldlocale);
goto bad_locale;
}
/* Now we are in the input method locale. Obtain the codeset. */
coding = XLStrdup (nl_langinfo (CODESET));
/* Switch back to the new locale. */
if (!setlocale (LC_ALL, oldlocale))
abort ();
DebugPrint ("input method coding system is %s", coding);
/* Create a character conversion context for input data. */
cd = iconv_open ("UTF-8", coding);
/* Free the new data. */
XLFree (oldlocale);
XLFree (coding);
/* If cd creation failed, assume it isn't supported. */
if (cd == (iconv_t) -1)
goto bad_locale;
DebugPrint ("conversion descriptor created to UTF-8");
/* Now enable the input method and create XICs for all text inputs.
Then, restore previous state. */
current_xim = xim;
current_cd = cd;
/* Attach the destroy callback to the XIM. */
destroy_callback.client_data = NULL;
destroy_callback.callback = IMDestroyCallback;
XSetIMValues (xim, XNDestroyCallback, &destroy_callback,
NULL);
/* Initialize the styles supported by this input method. */
CheckStyles (xim);
/* A new input method is available; destroy the XIC of every text
input. */
info = all_client_infos.next;
while (info != &all_client_infos)
{
input = info->inputs.next;
while (input != &info->inputs)
{
/* Try to create the IC for this one input. */
if (input->current_state.enabled
/* If this is NULL, then the IC will only be created
upon the next commit after the focus is actually
transferred to the text input. */
&& input->client_info->focus_surface)
{
CreateIC (input);
/* Focus the IC and do geometry allocation. */
if (input->xic)
XSetICFocus (input->xic);
DoGeometryAllocation (input);
}
/* Move to the next input. */
input = input->next;
}
/* Move to the next client info. */
info = info->next;
}
return;
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wanalyzer-malloc-leak"
#endif
bad_locale:
XCloseIM (xim);
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
}
static void
IMInstantiateCallback (Display *display, XPointer client_data,
XPointer call_data)
{
XIM newim;
DebugPrint ("input method instantiated");
/* Open the input method. */
newim = XOpenIM (compositor.display,
XrmGetDatabase (compositor.display),
(char *) compositor.resource_name,
(char *) compositor.app_name);
/* Obtain its locale. */
if (newim)
{
DebugPrint ("created input method with locale: %s",
XLocaleOfIM (newim));
HandleNewIM (newim);
}
else
DebugPrint ("input method creation failed");
}
static void
FocusInCallback (Seat *seat, Surface *surface)
{
TextInputClientInfo *info, *start;
DebugPrint ("seat %p, surface %p", seat, surface);
info = GetClientInfo (wl_resource_get_client (surface->resource),
seat, False);
if (info)
{
DebugPrint ("found seat client info; sending events");
NoticeEnter (info, surface);
}
start = info ? info : &all_client_infos;
info = start->next;
/* Now, leave all of the other infos on the same seat. */
while (info != start)
{
if (info->seat == seat)
NoticeLeave (info);
/* Note that info->seat will be NULL for the sentinel node, so
the above comparison can never be true. */
info = info->next;
}
}
static void
FocusOutCallback (Seat *seat)
{
TextInputClientInfo *info;
DebugPrint ("seat %p", seat);
info = all_client_infos.next;
while (info != &all_client_infos)
{
/* Leave the info if this is the same seat. */
if (info->seat == seat)
NoticeLeave (info);
info = info->next;
}
}
static void
ConvertKeyEvent (XIDeviceEvent *xev, XEvent *event)
{
/* Input methods cannot understand extension events, so filter an
equivalent core event instead. */
memset (event, 0, sizeof *event);
if (xev->evtype == XI_KeyPress)
event->xkey.type = KeyPress;
else
event->xkey.type = KeyRelease;
event->xkey.serial = xev->serial;
event->xkey.send_event = xev->send_event;
event->xkey.display = compositor.display;
event->xkey.window = xev->event;
event->xkey.root = xev->root;
event->xkey.subwindow = xev->child;
event->xkey.time = xev->time;
event->xkey.state = ((xev->mods.effective & ~(1 << 13 | 1 << 14))
| (xev->group.effective << 13));
event->xkey.keycode = xev->detail;
event->xkey.x = xev->event_x;
event->xkey.y = xev->event_y;
event->xkey.x_root = xev->root_x;
event->xkey.y_root = xev->root_y;
if (xev->root == DefaultRootWindow (compositor.display))
event->xkey.same_screen = True;
/* Wayland clients don't expect to receive repeated key events,
while input methods do. However, there is no way to stuff the
XIKeyRepeat flag into a core event. Our saving graces are that:
- the high two bits of a valid XID are not set.
- event->xkey.subwindow is unused by all input methods.
- it cannot be valid to actually query information from the
subwindow, since it may no longer exist by the time the event
is forwarded to the input method.
As a result, it becomes possible to record that information by
setting the high bit of the event subwindow for repeated key
events. */
if (xev->flags & XIKeyRepeat)
event->xkey.subwindow |= (1U << 31);
}
static char *
ConvertString (char *buffer, size_t nbytes, size_t *size_out)
{
char *outbuf, *outptr;
size_t outsize, outbytesleft, rc;
outbuf = XLMalloc (BUFSIZ + 1);
outptr = outbuf;
outsize = BUFSIZ;
outbytesleft = outsize;
DebugPrint ("converting string of size %zu", nbytes);
/* Reset the cd state. */
iconv (current_cd, NULL, NULL, &outptr, &outbytesleft);
/* Start converting. */
while (nbytes > 0)
{
rc = iconv (current_cd, &buffer, &nbytes,
&outptr, &outbytesleft);
DebugPrint ("iconv gave: %zu", rc);
if (rc == (size_t) -1)
{
/* See what went wrong. */
if (errno == E2BIG)
{
/* Reallocate the output buffer. */
outbuf = XLRealloc (outbuf, outsize + BUFSIZ + 1);
/* Move the outptr to the right location in the new
outbuf. */
outptr = outbuf + outsize - outbytesleft;
/* Expand outsize and outbytesleft. */
outsize += BUFSIZ;
outbytesleft += BUFSIZ;
DebugPrint ("expanding outsize to %zu, outbytesleft now %zu",
outsize, outbytesleft);
}
else
goto finish;
}
}
finish:
DebugPrint ("conversion finished, size_out %zu",
outsize - outbytesleft);
/* Return outbuf and the number of bytes put in it. */
if (size_out)
*size_out = outsize - outbytesleft;
/* NULL-terminate the string. */
outbuf[outsize - outbytesleft] = '\0';
return outbuf;
}
static void
PreeditString (TextInput *input, const char *buffer,
size_t buffer_size, ptrdiff_t cursor)
{
char chunk[4000];
const char *start, *end;
int skip;
const char *buffer_end;
int cursor_pos;
start = buffer;
buffer_end = buffer + buffer_size;
/* The Wayland protocol limits strings to 4000 bytes (including the
terminating NULL). Send the text as valid substrings consisting
of less than 4000 bytes each. */
while (start < buffer_end)
{
end = start;
while (true)
{
skip = CountOctets (*end);
DebugPrint ("skip %d (%p+%d)", skip, end, skip);
if (end + skip - start >= 3998)
break;
if (end >= buffer_end)
break;
end += skip;
}
DebugPrint ("end-start (%p-%p): %td", end, start,
end - start);
/* Now, start to end contain a UTF-8 sequence less than 4000
bytes in length. */
XLAssert (end - start < 3998);
memcpy (chunk, start, end - start);
/* NULL-terminate the buffer. */
chunk[end - start] = '\0';
DebugPrint ("sending buffered string %s", chunk);
/* Calculate the cursor position and whether or not it is in
this chunk. */
if (cursor == -1)
cursor_pos = -1;
else
cursor_pos = cursor - (start - buffer);
if (cursor_pos < 0)
cursor_pos = -1;
/* Send the sequence. */
zwp_text_input_v3_send_preedit_string (input->resource, chunk,
cursor_pos, cursor_pos);
start = end;
}
/* Finish sending it. */
zwp_text_input_v3_send_done (input->resource, input->serial);
}
static void
CommitString (TextInput *input, const char *buffer,
size_t buffer_size)
{
char chunk[4000];
const char *start, *end;
int skip;
const char *buffer_end;
start = buffer;
buffer_end = buffer + buffer_size;
/* The Wayland protocol limits strings to 4000 bytes (including the
terminating NULL). Send the text as valid substrings consisting
of less than 4000 bytes each. */
while (start < buffer_end)
{
end = start;
while (true)
{
skip = CountOctets (*end);
DebugPrint ("skip %d (%p+%d)", skip, end, skip);
if (end + skip - start >= 3998)
break;
if (end >= buffer_end)
break;
end += skip;
}
DebugPrint ("end-start (%p-%p): %td", end, start,
end - start);
/* Now, start to end contain a UTF-8 sequence less than 4000
bytes in length. */
XLAssert (end - start < 3998);
memcpy (chunk, start, end - start);
/* NULL-terminate the buffer. */
chunk[end - start] = '\0';
DebugPrint ("sending buffered string %s", chunk);
/* Send the sequence. */
zwp_text_input_v3_send_commit_string (input->resource, chunk);
start = end;
}
/* Finish sending it. */
zwp_text_input_v3_send_done (input->resource, input->serial);
}
static Bool
LookupString (TextInput *input, XEvent *event, KeySym *keysym_return)
{
char *buffer;
size_t nbytes, buffer_size;
Status status;
KeySym keysym;
if (event->xkey.type != KeyPress)
{
DebugPrint ("ignoring key release event");
return False;
}
/* First, do XmbLookupString with the default buffer size. */
buffer = alloca (256);
nbytes = XmbLookupString (input->xic, &event->xkey,
buffer, 256, &keysym, &status);
DebugPrint ("looked up %zu", nbytes);
if (status == XBufferOverflow)
{
DebugPrint ("overflow to %zu", nbytes);
/* Handle overflow by growing the buffer. */
buffer = alloca (nbytes + 1);
nbytes = XmbLookupString (input->xic, &event->xkey,
buffer, nbytes + 1,
&keysym, &status);
}
DebugPrint ("status is: %d", (int) status);
/* If no string was returned, return False. Otherwise, convert the
string to UTF-8 and commit it. However, use the keysym if both a
keysym and string were looked up, as a keysym allows modifiers to
be correctly dispatched to the seat and avoids doing potentially
expensive character conversion. */
if (status != XLookupChars)
{
if ((status == XLookupKeySym
|| status == XLookupBoth) && keysym_return)
/* Return the keysym if it was looked up. */
*keysym_return = keysym;
return False;
}
DebugPrint ("converting buffer of %zu", nbytes);
/* current_xim should not be NULL. */
XLAssert (current_xim != NULL);
/* Convert the string. */
buffer = ConvertString (buffer, nbytes, &buffer_size);
if (buffer)
CommitString (input, buffer, buffer_size);
XFree (buffer);
return True;
}
static KeyCode
CalculateKeycodeForEvent (XEvent *event, KeySym keysym)
{
KeySym sym_return;
unsigned int mods_return;
/* Calculate the keycode to actually send clients along with an
event, given the keysym specified by the input method. Return 0
if no special treatment is required. */
if (!keysym)
return 0;
/* If looking up the event keycode also results in the keysym,
then just use the keycode specified in the event. This is
because French keyboard layouts have multiple keycodes that
decode to the same keysym, which causes problems later on
when Wayland clients keep repeating the "a" key, as a keysym
was looked up for the key press but not for the corresponding
key release. */
if (XkbLookupKeySym (compositor.display, event->xkey.keycode,
event->xkey.state, &mods_return, &sym_return)
&& keysym == sym_return)
return 0;
/* Otherwise, convert the keysym to a keycode and use that. */
return XLKeysymToKeycode (keysym, event);
}
static Bool
FilterInputCallback (Seat *seat, Surface *surface, void *event,
KeyCode *keycode)
{
XIDeviceEvent *xev;
XEvent xkey;
TextInputClientInfo *info;
TextInput *input;
KeySym keysym;
xev = event;
keysym = 0;
DebugPrint ("seat %p, surface %p, detail: %d, event: %lx",
seat, surface, xev->detail, xev->event);
/* Find the client info. */
info = GetClientInfo (wl_resource_get_client (surface->resource),
seat, False);
/* Find the enabled text input. */
if (info)
{
input = FindEnabledTextInput (info);
/* If there is an enabled text input, start filtering the
event. */
if (input && input->xic)
{
DebugPrint ("found enabled text input %p on client-seat info %p",
input, info);
/* Convert the extension event into a fake core event that
the input method can understand. */
ConvertKeyEvent (xev, &xkey);
/* And return the result of filtering the event. */
if (XFilterEvent (&xkey, XLWindowFromSurface (surface)))
return True;
/* Otherwise, call XmbLookupString. If a keysym is
returned, return False. Otherwise, commit the string
looked up and return True. */
if (LookupString (input, &xkey, &keysym))
return True;
/* Look up the right keycode for the event. */
if (!keysym && xev->type == XI_KeyRelease)
*keycode = GetKeycode (&input->keysym_map, xev->detail);
else
*keycode = CalculateKeycodeForEvent (&xkey, keysym);
if (xev->type == XI_KeyRelease)
/* Remove the keycode from the keycode-keysym map. */
RemoveKeysym (&input->keysym_map, xev->detail);
}
}
/* Otherwise, do nothing. */
return False;
}
/* Seat input callbacks. */
static TextInputFuncs input_funcs =
{
.focus_in = FocusInCallback,
.focus_out = FocusOutCallback,
.filter_input = FilterInputCallback,
};
void
XLTextInputDispatchCoreEvent (Surface *surface, XEvent *event)
{
Seat *im_seat;
TextInputClientInfo *info;
TextInput *input;
KeySym keysym;
KeyCode effective_keycode;
DebugPrint ("dispatching core event to surface %p:\n"
"\ttype: %d\n"
"\tserial: %lu\n"
"\tsend_event: %d\n"
"\twindow: %lx\n"
"\troot: %lx\n"
"\tsubwindow: %lx\n"
"\ttime: %lu\n"
"\tstate: %x\n"
"\tkeycode: %x", surface,
event->xkey.type,
event->xkey.serial, event->xkey.send_event,
event->xkey.window, event->xkey.subwindow,
event->xkey.subwindow, event->xkey.time,
event->xkey.state, event->xkey.keycode);
keysym = 0;
/* Some of the events we want here are rather special. They are put
back on the event queue by the X internationalization library in
response to an XIM_COMMIT event being received. Other events are
put back on the event queue in response to XIM_FORWARD_EVENT.
First, find out which seat is the input method seat. */
im_seat = XLSeatGetInputMethodSeat ();
if (!im_seat)
return;
/* Next, find the client info associated with the surface for that
seat. */
info = GetClientInfo (wl_resource_get_client (surface->resource),
im_seat, False);
if (!info)
return;
if (info->focus_surface != surface)
{
/* The surface is no longer focused. */
DebugPrint ("incorrect focus surface (focus surface is %p, "
"surface is %p)", info->focus_surface, surface);
return;
}
if (info)
{
/* And look for an enabled text input. */
input = FindEnabledTextInput (info);
if (input)
{
DebugPrint ("found enabled input %p on info %p", input, info);
/* Now, try to dispatch the core event. First, look up the
text string. */
if (!input->xic || LookupString (input, event, &keysym))
return;
if (event->xkey.subwindow & (1U << 31))
DebugPrint ("lookup failed; not dispatching event because"
" this is a key repeat");
else
{
/* Since that failed, dispatch the event to the seat. */
DebugPrint ("lookup failed; dispatching event to seat; "
"keysym is: %lu", keysym);
/* First, clear effective_keycode. */
effective_keycode = 0;
/* If the event is a KeyPress event and a keysym was
looked up, calculate a keycode for the keysym, and
record in the text input's keycode-keycode table, so
the correct keycode can be looked up upon the next
KeyRelease event. X input methods tend not to filter
the KeyRelease events, so the KeyRelease event for an
event that changed the keysym will be sent with the
wrong keycode, which does not matter much with X
programs, but leads to Wayland programs constantly
autorepeating the keycode for which a KeyPress event
was sent. */
if (event->xkey.type == KeyPress && keysym)
{
/* Compute the keycode. */
effective_keycode
= CalculateKeycodeForEvent (event, keysym);
DebugPrint ("inserting keycode %lu into map under %u",
effective_keycode, event->xkey.keycode);
InsertKeycode (&input->keysym_map, event->xkey.keycode,
effective_keycode);
}
else if (event->xkey.type == KeyRelease)
{
/* If no keysym was committed by the input method,
use the keysym provided in the last KeyPress
event for the keycode. Otherwise, the input
method probably knows better than us, so use the
keysym provided by the input method. */
if (!keysym)
{
effective_keycode
= GetKeycode (&input->keysym_map,
event->xkey.keycode);
DebugPrint ("obtained keycode %lu for keycode %u"
" while processing KeyRelease event",
keysym, event->xkey.keycode);
}
else
effective_keycode
= CalculateKeycodeForEvent (event, keysym);
DebugPrint ("removing keycode %u from map",
event->xkey.keycode);
/* Remove the keycode from the keycode-keysym
map. */
RemoveKeysym (&input->keysym_map, event->xkey.keycode);
}
/* Finally, if an effective keycode was calculated,
replace the keycode in the event with it. */
if (effective_keycode)
event->xkey.keycode = effective_keycode;
XLSeatDispatchCoreKeyEvent (im_seat, surface, event);
}
}
}
}
static Bool
InitFontset (void)
{
XrmDatabase rdb;
XrmName namelist[3];
XrmClass classlist[3];
XrmValue value;
XrmRepresentation type;
char **missing_charset_list, *def_string;
int missing_charset_count;
rdb = XrmGetDatabase (compositor.display);
if (!rdb)
return False;
DebugPrint ("initializing fontset");
namelist[1] = XrmStringToQuark ("ximFont");
namelist[0] = app_quark;
namelist[2] = NULLQUARK;
classlist[1] = XrmStringToQuark ("XimFont");
classlist[0] = resource_quark;
classlist[2] = NULLQUARK;
if (XrmQGetResource (rdb, namelist, classlist,
&type, &value)
&& type == QString)
{
DebugPrint ("XIM fontset: %s", value.addr);
im_fontset = XCreateFontSet (compositor.display,
(char *) value.addr,
&missing_charset_list,
&missing_charset_count,
&def_string);
if (missing_charset_count)
XFreeStringList (missing_charset_list);
return True;
}
return False;
}
static void
InitInputStyles (void)
{
XrmDatabase rdb;
XrmName namelist[3];
XrmClass classlist[3];
XrmValue value;
XrmRepresentation type;
int i;
char *string, *end, *sep, *buffer;
rdb = XrmGetDatabase (compositor.display);
if (!rdb)
return;
DebugPrint ("initializing input styles");
namelist[1] = XrmStringToQuark ("ximStyles");
namelist[0] = app_quark;
namelist[2] = NULLQUARK;
classlist[1] = XrmStringToQuark ("XimStyles");
classlist[0] = resource_quark;
classlist[2] = NULLQUARK;
if (XrmQGetResource (rdb, namelist, classlist,
&type, &value)
&& type == QString)
{
DebugPrint ("XIM styles: %s", value.addr);
string = value.addr;
end = string + strlen (string);
i = 0;
while (string < end)
{
/* Find the next comma. */
sep = strchr (string, ',');
if (!sep)
sep = end;
/* Copy the text between string and sep into buffer. */
buffer = alloca (sep - string + 1);
memcpy (buffer, string, sep - string);
buffer[sep - string] = '\0';
/* If the comparison is successful, populate the list. */
DebugPrint ("considering: %s", buffer);
if (!strcmp (buffer, "overTheSpot"))
xim_style_order[i++] = XimOverTheSpot;
else if (!strcmp (buffer, "offTheSpot"))
xim_style_order[i++] = XimOffTheSpot;
else if (!strcmp (buffer, "rootWindow"))
xim_style_order[i++] = XimRootWindow;
else if (!strcmp (buffer, "onTheSpot"))
xim_style_order[i++] = XimOnTheSpot;
else
{
/* Invalid value encountered; stop parsing. */
DebugPrint ("invalid value: %s", buffer);
return;
}
/* Return if i is now 4. */
if (i == 4)
return;
string = sep + 1;
}
return;
}
else
{
/* Set up default values. */
xim_style_order[0] = XimOverTheSpot;
xim_style_order[1] = XimOffTheSpot;
xim_style_order[2] = XimRootWindow;
xim_style_order[3] = XimOnTheSpot;
DebugPrint ("set up default values for XIM style order");
}
return;
}
void
XLInitTextInput (void)
{
const char *modifiers;
char **missing_charset_list, *def_string;
int missing_charset_count;
current_cd = (iconv_t) -1;
if (!XSupportsLocale ())
{
DebugPrint ("not initializing text input because the"
" locale is not supported by the X library");
return;
}
/* Append the contents of XMODIFIERS to the locale modifiers
list. */
modifiers = XSetLocaleModifiers ("");
DebugPrint ("locale modifiers are: %s", modifiers);
/* Prevent -Wunused-but-set-variable when not debug. */
((void) modifiers);
all_client_infos.next = &all_client_infos;
all_client_infos.last = &all_client_infos;
text_input_manager_global
= wl_global_create (compositor.wl_display,
&zwp_text_input_manager_v3_interface,
1, NULL, HandleBind);
/* Initialize the IM fontset. */
if (!InitFontset ())
{
im_fontset = XCreateFontSet (compositor.display,
"-*-*-*-R-*-*-*-120-*-*-*-*",
&missing_charset_list,
&missing_charset_count,
&def_string);
if (missing_charset_count)
XFreeStringList (missing_charset_list);
}
/* Initialize input styles. */
InitInputStyles ();
if (im_fontset == NULL)
fprintf (stderr, "Unable to load any usable fontset for input methods\n");
/* Register the IM callback. */
XRegisterIMInstantiateCallback (compositor.display,
XrmGetDatabase (compositor.display),
(char *) compositor.resource_name,
(char *) compositor.app_name,
IMInstantiateCallback, NULL);
/* Register the text input functions. */
XLSeatSetTextInputFuncs (&input_funcs);
}