mutter/src/wayland/meta-wayland-text-input.c

743 lines
24 KiB
C
Raw Normal View History

/*
* Copyright (C) 2017, 2018 Red Hat
*
* This program 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 2 of the
* License, or (at your option) any later version.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
* 02111-1307, USA.
*
* Author: Carlos Garnacho <carlosg@gnome.org>
*/
#include "config.h"
#include "wayland/meta-wayland-text-input.h"
#include <wayland-server.h>
#include "wayland/meta-wayland-private.h"
#include "wayland/meta-wayland-seat.h"
#include "wayland/meta-wayland-versions.h"
#include "text-input-unstable-v3-server-protocol.h"
#define META_TYPE_WAYLAND_TEXT_INPUT_FOCUS (meta_wayland_text_input_focus_get_type ())
typedef enum
{
META_WAYLAND_PENDING_STATE_NONE = 0,
META_WAYLAND_PENDING_STATE_INPUT_RECT = 1 << 0,
META_WAYLAND_PENDING_STATE_CONTENT_TYPE = 1 << 1,
META_WAYLAND_PENDING_STATE_SURROUNDING_TEXT = 1 << 2,
META_WAYLAND_PENDING_STATE_CHANGE_CAUSE = 1 << 3,
META_WAYLAND_PENDING_STATE_ENABLED = 1 << 4,
} MetaWaylandTextInputPendingState;
typedef struct _MetaWaylandTextInput MetaWaylandTextInput;
struct _MetaWaylandTextInput
{
MetaWaylandSeat *seat;
ClutterInputFocus *input_focus;
struct wl_list resource_list;
struct wl_list focus_resource_list;
MetaWaylandSurface *surface;
struct wl_listener surface_listener;
MetaWaylandTextInputPendingState pending_state;
GHashTable *resource_serials;
struct
{
char *text;
uint32_t cursor;
uint32_t anchor;
} surrounding;
cairo_rectangle_int_t cursor_rect;
uint32_t content_type_hint;
uint32_t content_type_purpose;
uint32_t text_change_cause;
gboolean enabled;
guint done_idle_id;
};
struct _MetaWaylandTextInputFocus
{
ClutterInputFocus parent_instance;
MetaWaylandTextInput *text_input;
};
G_DECLARE_FINAL_TYPE (MetaWaylandTextInputFocus, meta_wayland_text_input_focus,
META, WAYLAND_TEXT_INPUT_FOCUS, ClutterInputFocus)
G_DEFINE_TYPE (MetaWaylandTextInputFocus, meta_wayland_text_input_focus,
CLUTTER_TYPE_INPUT_FOCUS)
static void
meta_wayland_text_input_focus_request_surrounding (ClutterInputFocus *focus)
{
MetaWaylandTextInput *text_input;
text_input = META_WAYLAND_TEXT_INPUT_FOCUS (focus)->text_input;
clutter_input_focus_set_surrounding (focus,
text_input->surrounding.text,
text_input->surrounding.cursor,
text_input->surrounding.anchor);
}
static uint32_t
lookup_serial (MetaWaylandTextInput *text_input,
struct wl_resource *resource)
{
return GPOINTER_TO_UINT (g_hash_table_lookup (text_input->resource_serials,
resource));
}
static void
increment_serial (MetaWaylandTextInput *text_input,
struct wl_resource *resource)
{
uint32_t serial;
serial = lookup_serial (text_input, resource);
g_hash_table_insert (text_input->resource_serials, resource,
GUINT_TO_POINTER (serial + 1));
}
static gboolean
done_idle_cb (gpointer user_data)
{
ClutterInputFocus *focus = user_data;
MetaWaylandTextInput *text_input;
struct wl_resource *resource;
text_input = META_WAYLAND_TEXT_INPUT_FOCUS (focus)->text_input;
wl_resource_for_each (resource, &text_input->focus_resource_list)
{
zwp_text_input_v3_send_done (resource,
lookup_serial (text_input, resource));
}
text_input->done_idle_id = 0;
return G_SOURCE_REMOVE;
}
static void
meta_wayland_text_input_focus_defer_done (ClutterInputFocus *focus)
{
MetaWaylandTextInput *text_input;
text_input = META_WAYLAND_TEXT_INPUT_FOCUS (focus)->text_input;
if (text_input->done_idle_id != 0)
return;
/* This operates on 3 principles:
* - GDBus uses G_PRIORITY_DEFAULT to put messages in the thread default main
* context.
* - All relevant ClutterInputFocus methods are ultimately backed by
* DBus methods inside IBus.
* - We want to run .done after them all. The slightly lower
* G_PRIORITY_DEFAULT + 1 priority should ensure we at least group
* all messages seen so far.
*
* FIXME: .done may be delayed indefinitely if there's a high enough
* priority idle source in the main loop. It's unlikely that
* recurring idles run at this high priority though.
*/
text_input->done_idle_id = g_idle_add_full (G_PRIORITY_DEFAULT + 1,
done_idle_cb, focus, NULL);
}
static void
meta_wayland_text_input_focus_delete_surrounding (ClutterInputFocus *focus,
int offset,
guint len)
{
MetaWaylandTextInput *text_input;
uint32_t before_length;
uint32_t after_length;
struct wl_resource *resource;
text_input = META_WAYLAND_TEXT_INPUT_FOCUS (focus)->text_input;
before_length = ABS (MIN (offset, 0));
after_length = MAX (0, offset + len);
g_warn_if_fail (ABS (offset) <= len);
wl_resource_for_each (resource, &text_input->focus_resource_list)
{
zwp_text_input_v3_send_delete_surrounding_text (resource,
before_length,
after_length);
}
meta_wayland_text_input_focus_defer_done (focus);
}
static void
meta_wayland_text_input_focus_commit_text (ClutterInputFocus *focus,
const gchar *text)
{
MetaWaylandTextInput *text_input;
struct wl_resource *resource;
text_input = META_WAYLAND_TEXT_INPUT_FOCUS (focus)->text_input;
wl_resource_for_each (resource, &text_input->focus_resource_list)
{
zwp_text_input_v3_send_preedit_string (resource, NULL, 0, 0);
zwp_text_input_v3_send_commit_string (resource, text);
}
meta_wayland_text_input_focus_defer_done (focus);
}
static void
meta_wayland_text_input_focus_set_preedit_text (ClutterInputFocus *focus,
const gchar *text,
guint cursor)
{
MetaWaylandTextInput *text_input;
struct wl_resource *resource;
gsize pos = 0;
text_input = META_WAYLAND_TEXT_INPUT_FOCUS (focus)->text_input;
if (text)
pos = g_utf8_offset_to_pointer (text, cursor) - text;
wl_resource_for_each (resource, &text_input->focus_resource_list)
{
zwp_text_input_v3_send_preedit_string (resource, text, pos, pos);
}
meta_wayland_text_input_focus_defer_done (focus);
}
static void
meta_wayland_text_input_focus_class_init (MetaWaylandTextInputFocusClass *klass)
{
ClutterInputFocusClass *focus_class = CLUTTER_INPUT_FOCUS_CLASS (klass);
focus_class->request_surrounding = meta_wayland_text_input_focus_request_surrounding;
focus_class->delete_surrounding = meta_wayland_text_input_focus_delete_surrounding;
focus_class->commit_text = meta_wayland_text_input_focus_commit_text;
focus_class->set_preedit_text = meta_wayland_text_input_focus_set_preedit_text;
}
static void
meta_wayland_text_input_focus_init (MetaWaylandTextInputFocus *focus)
{
}
static ClutterInputFocus *
meta_wayland_text_input_focus_new (MetaWaylandTextInput *text_input)
{
MetaWaylandTextInputFocus *focus;
focus = g_object_new (META_TYPE_WAYLAND_TEXT_INPUT_FOCUS, NULL);
focus->text_input = text_input;
return CLUTTER_INPUT_FOCUS (focus);
}
static void
text_input_handle_focus_surface_destroy (struct wl_listener *listener,
void *data)
{
MetaWaylandTextInput *text_input = wl_container_of (listener, text_input,
surface_listener);
meta_wayland_text_input_set_focus (text_input, NULL);
}
static void
move_resources (struct wl_list *destination, struct wl_list *source)
{
wl_list_insert_list (destination, source);
wl_list_init (source);
}
static void
move_resources_for_client (struct wl_list *destination,
struct wl_list *source,
struct wl_client *client)
{
struct wl_resource *resource, *tmp;
wl_resource_for_each_safe (resource, tmp, source)
{
if (wl_resource_get_client (resource) == client)
{
wl_list_remove (wl_resource_get_link (resource));
wl_list_insert (destination, wl_resource_get_link (resource));
}
}
}
void
meta_wayland_text_input_set_focus (MetaWaylandTextInput *text_input,
MetaWaylandSurface *surface)
{
if (text_input->surface == surface)
return;
text_input->pending_state = META_WAYLAND_PENDING_STATE_NONE;
if (text_input->surface)
{
if (!wl_list_empty (&text_input->focus_resource_list))
{
ClutterInputFocus *focus = text_input->input_focus;
ClutterInputMethod *input_method;
struct wl_resource *resource;
if (clutter_input_focus_is_focused (focus))
{
input_method = clutter_backend_get_input_method (clutter_get_default_backend ());
clutter_input_method_focus_out (input_method);
}
wl_resource_for_each (resource, &text_input->focus_resource_list)
{
zwp_text_input_v3_send_leave (resource,
text_input->surface->resource);
}
move_resources (&text_input->resource_list,
&text_input->focus_resource_list);
}
wl_list_remove (&text_input->surface_listener.link);
text_input->surface = NULL;
}
if (surface)
{
struct wl_resource *focus_surface_resource;
text_input->surface = surface;
focus_surface_resource = text_input->surface->resource;
wl_resource_add_destroy_listener (focus_surface_resource,
&text_input->surface_listener);
move_resources_for_client (&text_input->focus_resource_list,
&text_input->resource_list,
wl_resource_get_client (focus_surface_resource));
if (!wl_list_empty (&text_input->focus_resource_list))
{
struct wl_resource *resource;
wl_resource_for_each (resource, &text_input->focus_resource_list)
{
zwp_text_input_v3_send_enter (resource, surface->resource);
}
}
}
}
static void
text_input_destructor (struct wl_resource *resource)
{
MetaWaylandTextInput *text_input = wl_resource_get_user_data (resource);
g_hash_table_remove (text_input->resource_serials, resource);
wl_list_remove (wl_resource_get_link (resource));
}
static void
text_input_destroy (struct wl_client *client,
struct wl_resource *resource)
{
wl_resource_destroy (resource);
}
static void
text_input_enable (struct wl_client *client,
struct wl_resource *resource)
{
MetaWaylandTextInput *text_input = wl_resource_get_user_data (resource);
text_input->enabled = TRUE;
text_input->pending_state |= META_WAYLAND_PENDING_STATE_ENABLED;
}
static void
text_input_disable (struct wl_client *client,
struct wl_resource *resource)
{
MetaWaylandTextInput *text_input = wl_resource_get_user_data (resource);
text_input->enabled = FALSE;
text_input->pending_state |= META_WAYLAND_PENDING_STATE_ENABLED;
}
static void
text_input_set_surrounding_text (struct wl_client *client,
struct wl_resource *resource,
const char *text,
int32_t cursor,
int32_t anchor)
{
MetaWaylandTextInput *text_input = wl_resource_get_user_data (resource);
g_free (text_input->surrounding.text);
text_input->surrounding.text = g_strdup (text);
text_input->surrounding.cursor = cursor;
text_input->surrounding.anchor = anchor;
text_input->pending_state |= META_WAYLAND_PENDING_STATE_SURROUNDING_TEXT;
}
static void
text_input_set_text_change_cause (struct wl_client *client,
struct wl_resource *resource,
uint32_t cause)
{
MetaWaylandTextInput *text_input = wl_resource_get_user_data (resource);
text_input->text_change_cause = cause;
text_input->pending_state |= META_WAYLAND_PENDING_STATE_CHANGE_CAUSE;
}
static ClutterInputContentHintFlags
translate_hints (uint32_t hints)
{
ClutterInputContentHintFlags clutter_hints = 0;
if (hints & ZWP_TEXT_INPUT_V3_CONTENT_HINT_COMPLETION)
clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_COMPLETION;
if (hints & ZWP_TEXT_INPUT_V3_CONTENT_HINT_SPELLCHECK)
clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_SPELLCHECK;
if (hints & ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION)
clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_AUTO_CAPITALIZATION;
if (hints & ZWP_TEXT_INPUT_V3_CONTENT_HINT_LOWERCASE)
clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_LOWERCASE;
if (hints & ZWP_TEXT_INPUT_V3_CONTENT_HINT_UPPERCASE)
clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_UPPERCASE;
if (hints & ZWP_TEXT_INPUT_V3_CONTENT_HINT_TITLECASE)
clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_TITLECASE;
if (hints & ZWP_TEXT_INPUT_V3_CONTENT_HINT_HIDDEN_TEXT)
clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_HIDDEN_TEXT;
if (hints & ZWP_TEXT_INPUT_V3_CONTENT_HINT_SENSITIVE_DATA)
clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_SENSITIVE_DATA;
if (hints & ZWP_TEXT_INPUT_V3_CONTENT_HINT_LATIN)
clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_LATIN;
if (hints & ZWP_TEXT_INPUT_V3_CONTENT_HINT_MULTILINE)
clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_MULTILINE;
return clutter_hints;
}
static ClutterInputContentPurpose
translate_purpose (uint32_t purpose)
{
switch (purpose)
{
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL:
return CLUTTER_INPUT_CONTENT_PURPOSE_NORMAL;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ALPHA:
return CLUTTER_INPUT_CONTENT_PURPOSE_ALPHA;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DIGITS:
return CLUTTER_INPUT_CONTENT_PURPOSE_DIGITS;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NUMBER:
return CLUTTER_INPUT_CONTENT_PURPOSE_NUMBER;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PHONE:
return CLUTTER_INPUT_CONTENT_PURPOSE_PHONE;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_URL:
return CLUTTER_INPUT_CONTENT_PURPOSE_URL;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_EMAIL:
return CLUTTER_INPUT_CONTENT_PURPOSE_EMAIL;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NAME:
return CLUTTER_INPUT_CONTENT_PURPOSE_NAME;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PASSWORD:
return CLUTTER_INPUT_CONTENT_PURPOSE_PASSWORD;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATE:
return CLUTTER_INPUT_CONTENT_PURPOSE_DATE;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TIME:
return CLUTTER_INPUT_CONTENT_PURPOSE_TIME;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATETIME:
return CLUTTER_INPUT_CONTENT_PURPOSE_DATETIME;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL:
return CLUTTER_INPUT_CONTENT_PURPOSE_TERMINAL;
}
g_warn_if_reached ();
return CLUTTER_INPUT_CONTENT_PURPOSE_NORMAL;
}
static void
text_input_set_content_type (struct wl_client *client,
struct wl_resource *resource,
uint32_t hint,
uint32_t purpose)
{
MetaWaylandTextInput *text_input = wl_resource_get_user_data (resource);
if (!text_input->surface)
return;
text_input->content_type_hint = hint;
text_input->content_type_purpose = purpose;
text_input->pending_state |= META_WAYLAND_PENDING_STATE_CONTENT_TYPE;
}
static void
text_input_set_cursor_rectangle (struct wl_client *client,
struct wl_resource *resource,
int32_t x,
int32_t y,
int32_t width,
int32_t height)
{
MetaWaylandTextInput *text_input = wl_resource_get_user_data (resource);
if (!text_input->surface)
return;
text_input->cursor_rect = (cairo_rectangle_int_t) { x, y, width, height };
text_input->pending_state |= META_WAYLAND_PENDING_STATE_INPUT_RECT;
}
static void
meta_wayland_text_input_reset (MetaWaylandTextInput *text_input)
{
g_clear_pointer (&text_input->surrounding.text, g_free);
text_input->content_type_hint = ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE;
text_input->content_type_purpose = ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL;
text_input->text_change_cause = ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_INPUT_METHOD;
text_input->cursor_rect = (cairo_rectangle_int_t) { 0, 0, 0, 0 };
text_input->pending_state = META_WAYLAND_PENDING_STATE_NONE;
}
static void
text_input_commit_state (struct wl_client *client,
struct wl_resource *resource)
{
MetaWaylandTextInput *text_input = wl_resource_get_user_data (resource);
ClutterInputFocus *focus = text_input->input_focus;
gboolean enable_panel = FALSE;
ClutterInputMethod *input_method;
increment_serial (text_input, resource);
if (text_input->surface == NULL)
return;
input_method = clutter_backend_get_input_method (clutter_get_default_backend ());
if (input_method &&
text_input->pending_state & META_WAYLAND_PENDING_STATE_ENABLED)
{
if (text_input->enabled)
{
if (!clutter_input_focus_is_focused (focus))
clutter_input_method_focus_in (input_method, focus);
else
enable_panel = TRUE;
clutter_input_focus_set_can_show_preedit (focus, TRUE);
}
else if (clutter_input_focus_is_focused (focus))
{
text_input->pending_state = META_WAYLAND_PENDING_STATE_NONE;
clutter_input_focus_reset (text_input->input_focus);
clutter_input_method_focus_out (input_method);
}
}
if (!clutter_input_focus_is_focused (focus))
{
meta_wayland_text_input_reset (text_input);
return;
}
if (text_input->pending_state & META_WAYLAND_PENDING_STATE_CONTENT_TYPE)
{
clutter_input_focus_set_content_hints (text_input->input_focus,
translate_hints (text_input->content_type_hint));
clutter_input_focus_set_content_purpose (text_input->input_focus,
translate_purpose (text_input->content_type_purpose));
}
if (text_input->pending_state & META_WAYLAND_PENDING_STATE_SURROUNDING_TEXT)
{
clutter_input_focus_set_surrounding (text_input->input_focus,
text_input->surrounding.text,
text_input->surrounding.cursor,
text_input->surrounding.anchor);
}
if (text_input->pending_state & META_WAYLAND_PENDING_STATE_INPUT_RECT)
{
graphene_rect_t cursor_rect;
float x1, y1, x2, y2;
cairo_rectangle_int_t rect;
rect = text_input->cursor_rect;
meta_wayland_surface_get_absolute_coordinates (text_input->surface,
rect.x, rect.y, &x1, &y1);
meta_wayland_surface_get_absolute_coordinates (text_input->surface,
rect.x + rect.width,
rect.y + rect.height,
&x2, &y2);
graphene_rect_init (&cursor_rect, x1, y1, x2 - x1, y2 - y1);
clutter_input_focus_set_cursor_location (text_input->input_focus,
&cursor_rect);
}
meta_wayland_text_input_reset (text_input);
if (enable_panel)
clutter_input_focus_set_input_panel_state (focus, CLUTTER_INPUT_PANEL_STATE_ON);
}
static struct zwp_text_input_v3_interface meta_text_input_interface = {
text_input_destroy,
text_input_enable,
text_input_disable,
text_input_set_surrounding_text,
text_input_set_text_change_cause,
text_input_set_content_type,
text_input_set_cursor_rectangle,
text_input_commit_state,
};
MetaWaylandTextInput *
meta_wayland_text_input_new (MetaWaylandSeat *seat)
{
MetaWaylandTextInput *text_input;
text_input = g_new0 (MetaWaylandTextInput, 1);
text_input->input_focus = meta_wayland_text_input_focus_new (text_input);
text_input->seat = seat;
wl_list_init (&text_input->resource_list);
wl_list_init (&text_input->focus_resource_list);
text_input->surface_listener.notify = text_input_handle_focus_surface_destroy;
text_input->resource_serials = g_hash_table_new (NULL, NULL);
return text_input;
}
void
meta_wayland_text_input_destroy (MetaWaylandTextInput *text_input)
{
meta_wayland_text_input_set_focus (text_input, NULL);
g_object_unref (text_input->input_focus);
g_hash_table_destroy (text_input->resource_serials);
g_free (text_input);
}
static void
meta_wayland_text_input_create_new_resource (MetaWaylandTextInput *text_input,
struct wl_client *client,
struct wl_resource *seat_resource,
uint32_t id)
{
struct wl_resource *text_input_resource;
text_input_resource = wl_resource_create (client,
&zwp_text_input_v3_interface,
META_ZWP_TEXT_INPUT_V3_VERSION,
id);
wl_resource_set_implementation (text_input_resource,
&meta_text_input_interface,
text_input, text_input_destructor);
if (text_input->surface &&
wl_resource_get_client (text_input->surface->resource) == client)
{
wl_list_insert (&text_input->focus_resource_list,
wl_resource_get_link (text_input_resource));
zwp_text_input_v3_send_enter (text_input_resource,
text_input->surface->resource);
}
else
{
wl_list_insert (&text_input->resource_list,
wl_resource_get_link (text_input_resource));
}
}
static void
text_input_manager_destroy (struct wl_client *client,
struct wl_resource *resource)
{
wl_resource_destroy (resource);
}
static void
text_input_manager_get_text_input (struct wl_client *client,
struct wl_resource *resource,
uint32_t id,
struct wl_resource *seat_resource)
{
MetaWaylandSeat *seat = wl_resource_get_user_data (seat_resource);
meta_wayland_text_input_create_new_resource (seat->text_input, client,
seat_resource, id);
}
static struct zwp_text_input_manager_v3_interface meta_text_input_manager_interface = {
text_input_manager_destroy,
text_input_manager_get_text_input,
};
static void
bind_text_input (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,
META_ZWP_TEXT_INPUT_V3_VERSION,
id);
wl_resource_set_implementation (resource,
&meta_text_input_manager_interface,
NULL, NULL);
}
gboolean
meta_wayland_text_input_init (MetaWaylandCompositor *compositor)
{
return (wl_global_create (compositor->wayland_display,
&zwp_text_input_manager_v3_interface,
META_ZWP_TEXT_INPUT_V3_VERSION,
compositor->seat->text_input,
bind_text_input) != NULL);
}
gboolean
meta_wayland_text_input_handle_event (MetaWaylandTextInput *text_input,
const ClutterEvent *event)
{
if (!text_input->surface ||
!clutter_input_focus_is_focused (text_input->input_focus))
return FALSE;
return clutter_input_focus_filter_key_event (text_input->input_focus,
(const ClutterKeyEvent *) event);
}