From 0027d3045ceed84139f45d2039c7fc4ed0fe30038ad4764f9ba5a6c2851d18e3 Mon Sep 17 00:00:00 2001 From: Dominique Leuenberger Date: Fri, 17 May 2024 06:14:05 +0000 Subject: [PATCH] Accepting request 1174713 from home:AZhou:branches:GNOME:Factory - Add mutter-implement-text-input-v1.patch: This allows input method to work in Chromium/Electron-based apps with Wayland Ozone platform, which only has text-input-v1 support (glgo#GNOME/mutter!3751, bsc#1219505). OBS-URL: https://build.opensuse.org/request/show/1174713 OBS-URL: https://build.opensuse.org/package/show/GNOME:Factory/mutter?expand=0&rev=505 --- mutter-implement-text-input-v1.patch | 1138 ++++++++++++++++++++++++++ mutter.changes | 8 + mutter.spec | 3 + 3 files changed, 1149 insertions(+) create mode 100644 mutter-implement-text-input-v1.patch diff --git a/mutter-implement-text-input-v1.patch b/mutter-implement-text-input-v1.patch new file mode 100644 index 0000000..278b226 --- /dev/null +++ b/mutter-implement-text-input-v1.patch @@ -0,0 +1,1138 @@ +From 6b9bbebbdc3a8b35f898a269227f36a36590359e Mon Sep 17 00:00:00 2001 +From: Alynx Zhou +Date: Wed, 15 May 2024 00:07:41 +0800 +Subject: [PATCH] wayland/text-input-v1: Implement basic text-input-v1 support + +This commit makes input methods work in text-input-v1 only clients +(mostly Chromium/Electron based apps with Ozone Wayland), which is +needed by users who needs IME to input their languages, like Chinese, +Japanese or Korean. + +Closes . +--- + clutter/clutter/clutter-enums.h | 3 + + src/core/events.c | 11 +- + src/meson.build | 3 + + src/wayland/meta-wayland-seat.c | 12 +- + src/wayland/meta-wayland-seat.h | 2 + + src/wayland/meta-wayland-text-input-v1.c | 859 +++++++++++++++++++++++ + src/wayland/meta-wayland-text-input-v1.h | 38 + + src/wayland/meta-wayland-versions.h | 1 + + src/wayland/meta-wayland.c | 7 + + src/wayland/meta-wayland.h | 2 + + 10 files changed, 933 insertions(+), 5 deletions(-) + create mode 100644 src/wayland/meta-wayland-text-input-v1.c + create mode 100644 src/wayland/meta-wayland-text-input-v1.h + +diff --git a/clutter/clutter/clutter-enums.h b/clutter/clutter/clutter-enums.h +index 45956bf57..8198e7351 100644 +--- a/clutter/clutter/clutter-enums.h ++++ b/clutter/clutter/clutter-enums.h +@@ -1183,6 +1183,9 @@ typedef enum + CLUTTER_INPUT_CONTENT_HINT_SENSITIVE_DATA = 1 << 7, + CLUTTER_INPUT_CONTENT_HINT_LATIN = 1 << 8, + CLUTTER_INPUT_CONTENT_HINT_MULTILINE = 1 << 9, ++ CLUTTER_INPUT_CONTENT_HINT_DEFAULT = 1 << 10, ++ CLUTTER_INPUT_CONTENT_HINT_PASSWORD = 1 << 11, ++ CLUTTER_INPUT_CONTENT_HINT_AUTO_CORRECTION = 1 << 12, + } ClutterInputContentHintFlags; + + typedef enum +diff --git a/src/core/events.c b/src/core/events.c +index 4261bfb98..35b94e187 100644 +--- a/src/core/events.c ++++ b/src/core/events.c +@@ -238,6 +238,7 @@ meta_display_handle_event (MetaDisplay *display, + #ifdef HAVE_WAYLAND + MetaWaylandCompositor *wayland_compositor; + MetaWaylandTextInput *wayland_text_input = NULL; ++ MetaWaylandTextInputV1 *wayland_text_input_v1 = NULL; + #endif + + #ifdef HAVE_WAYLAND +@@ -246,6 +247,8 @@ meta_display_handle_event (MetaDisplay *display, + { + wayland_text_input = + meta_wayland_compositor_get_text_input (wayland_compositor); ++ wayland_text_input_v1 = ++ meta_wayland_compositor_get_text_input_v1 (wayland_compositor); + } + #endif + +@@ -287,10 +290,12 @@ meta_display_handle_event (MetaDisplay *display, + } + + #ifdef HAVE_WAYLAND +- if (wayland_text_input && +- !has_grab && ++ if (!has_grab && + !meta_compositor_get_current_window_drag (compositor) && +- meta_wayland_text_input_update (wayland_text_input, event)) ++ ((wayland_text_input && ++ meta_wayland_text_input_update (wayland_text_input, event)) || ++ (wayland_text_input_v1 && ++ meta_wayland_text_input_v1_update (wayland_text_input_v1, event)))) + return CLUTTER_EVENT_STOP; + + if (wayland_compositor) +diff --git a/src/meson.build b/src/meson.build +index 3060b2880..12a249c54 100644 +--- a/src/meson.build ++++ b/src/meson.build +@@ -687,6 +687,8 @@ if have_wayland + 'wayland/meta-wayland-tablet-tool.h', + 'wayland/meta-wayland-text-input.c', + 'wayland/meta-wayland-text-input.h', ++ 'wayland/meta-wayland-text-input-v1.c', ++ 'wayland/meta-wayland-text-input-v1.h', + 'wayland/meta-wayland-touch.c', + 'wayland/meta-wayland-touch.h', + 'wayland/meta-wayland-transaction.c', +@@ -1075,6 +1077,7 @@ if have_wayland + ['single-pixel-buffer', 'staging', 'v1', ], + ['tablet', 'unstable', 'v2', ], + ['text-input', 'unstable', 'v3', ], ++ ['text-input', 'unstable', 'v1', ], + ['viewporter', 'stable', ], + ['xdg-activation', 'staging', 'v1', ], + ['xdg-foreign', 'unstable', 'v1', ], +diff --git a/src/wayland/meta-wayland-seat.c b/src/wayland/meta-wayland-seat.c +index f8d58a612..fca0b346d 100644 +--- a/src/wayland/meta-wayland-seat.c ++++ b/src/wayland/meta-wayland-seat.c +@@ -228,6 +228,7 @@ default_focus (MetaWaylandEventHandler *handler, + meta_wayland_data_device_primary_sync_focus (&seat->primary_data_device); + meta_wayland_tablet_seat_set_pad_focus (seat->tablet_seat, surface); + meta_wayland_text_input_set_focus (seat->text_input, surface); ++ /* text-input-v1 will set focused surface on activate. */ + } + + if (caps & CLUTTER_INPUT_CAPABILITY_TABLET_TOOL) +@@ -289,6 +290,8 @@ meta_wayland_seat_new (MetaWaylandCompositor *compositor, + NULL); + + seat->text_input = meta_wayland_text_input_new (seat); ++ /* Chromium/Electron-based apps only support text-input-v1. */ ++ seat->text_input_v1 = meta_wayland_text_input_v1_new (seat); + + meta_wayland_data_device_init (&seat->data_device, seat); + meta_wayland_data_device_primary_init (&seat->primary_data_device, seat); +@@ -337,6 +340,7 @@ meta_wayland_seat_free (MetaWaylandSeat *seat) + g_object_unref (seat->touch); + + meta_wayland_text_input_destroy (seat->text_input); ++ meta_wayland_text_input_v1_destroy (seat->text_input_v1); + + g_free (seat); + } +@@ -477,7 +481,10 @@ meta_wayland_seat_handle_event_internal (MetaWaylandSeat *seat, + if (event_type == CLUTTER_BUTTON_PRESS || + event_type == CLUTTER_TOUCH_BEGIN) + { +- meta_wayland_text_input_handle_event (seat->text_input, event); ++ gboolean handled = FALSE; ++ handled = meta_wayland_text_input_handle_event (seat->text_input, event); ++ if (!handled) ++ handled = meta_wayland_text_input_v1_handle_event (seat->text_input_v1, event); + } + + switch (event_type) +@@ -509,7 +516,8 @@ meta_wayland_seat_handle_event_internal (MetaWaylandSeat *seat, + case CLUTTER_IM_COMMIT: + case CLUTTER_IM_DELETE: + case CLUTTER_IM_PREEDIT: +- if (meta_wayland_text_input_handle_event (seat->text_input, event)) ++ if (meta_wayland_text_input_handle_event (seat->text_input, event) || ++ meta_wayland_text_input_v1_handle_event (seat->text_input_v1, event)) + return TRUE; + + break; +diff --git a/src/wayland/meta-wayland-seat.h b/src/wayland/meta-wayland-seat.h +index 169a92e4e..10633b029 100644 +--- a/src/wayland/meta-wayland-seat.h ++++ b/src/wayland/meta-wayland-seat.h +@@ -30,6 +30,7 @@ + #include "wayland/meta-wayland-pointer.h" + #include "wayland/meta-wayland-tablet-tool.h" + #include "wayland/meta-wayland-text-input.h" ++#include "wayland/meta-wayland-text-input-v1.h" + #include "wayland/meta-wayland-touch.h" + #include "wayland/meta-wayland-types.h" + +@@ -49,6 +50,7 @@ struct _MetaWaylandSeat + MetaWaylandDataDevicePrimary primary_data_device; + + MetaWaylandTextInput *text_input; ++ MetaWaylandTextInputV1 *text_input_v1; + + MetaWaylandInput *input_handler; + MetaWaylandEventHandler *default_handler; +diff --git a/src/wayland/meta-wayland-text-input-v1.c b/src/wayland/meta-wayland-text-input-v1.c +new file mode 100644 +index 000000000..1826a4ff4 +--- /dev/null ++++ b/src/wayland/meta-wayland-text-input-v1.c +@@ -0,0 +1,859 @@ ++/* ++ * Copyright (C) 2024 SUSE LLC ++ * ++ * 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, see . ++ * ++ * Author: Alynx Zhou ++ */ ++ ++#include "config.h" ++#include "wayland/meta-wayland-text-input-v1.h" ++ ++#include ++ ++#include "compositor/meta-surface-actor-wayland.h" ++#include "wayland/meta-wayland-private.h" ++#include "wayland/meta-wayland-seat.h" ++#include "wayland/meta-wayland-versions.h" ++ ++#include "text-input-unstable-v1-server-protocol.h" ++ ++/* ++ * Main difference between text-input-v1 and text-input-v3: ++ * text-input-v1 is not required to be double-buffered, we are expected to send ++ * response immediately after we receive requests, while text-input-v3 requires ++ * us to hold pending state and apply on commit, and all responses are applied ++ * after we send done. ++ * ++ * This implementation is incomplete, but it do make IME work. ++ * ++ * Things won't be implemented (Reminders for myself): ++ * - set_preferred_language (We don't have equivalence in ClutterInputMethod.) ++ * - invoke_action (No description about what button and index are.) ++ * - input_panel_state (We don't set this from ClutterInputFocus to text_input, ++ * we only set this from text_input to ClutterInputFocus.) ++ * - cursor_position (We don't have equivalence in ClutterInputMethod.) ++ * - language (We don't have equivalence in ClutterInputMethod.) ++ * - text_direction (We don't have equivalence in ClutterInputMethod.) ++ * - keysym (This matches keysym request in input-method-v1, but we only have ++ * forward_key in ClutterInputMethod, which is more like key request in ++ * input-method-v1 and will finally become a keyboard key event, we don't have ++ * equivalence for this in ClutterInputMethod.) ++ * - modifiers_map (This is used by keysym and we don't support keysym.) ++ */ ++ ++struct _MetaWaylandTextInputV1 ++{ ++ MetaWaylandSeat *seat; ++ ClutterInputFocus *input_focus; ++ ++ struct wl_list resource_list; ++ struct wl_list focus_resource_list; ++ MetaWaylandSurface *surface; ++ struct wl_listener surface_listener; ++ ++ GHashTable *resource_serials; ++ ++ struct ++ { ++ char *text; ++ uint32_t cursor; ++ uint32_t anchor; ++ } surrounding; ++}; ++ ++#define META_TYPE_WAYLAND_TEXT_INPUT_V1_FOCUS (meta_wayland_text_input_v1_focus_get_type ()) ++G_DECLARE_FINAL_TYPE (MetaWaylandTextInputV1Focus, meta_wayland_text_input_v1_focus, ++ META, WAYLAND_TEXT_INPUT_V1_FOCUS, ClutterInputFocus) ++ ++struct _MetaWaylandTextInputV1Focus ++{ ++ ClutterInputFocus parent_instance; ++ MetaWaylandTextInputV1 *text_input; ++}; ++G_DEFINE_TYPE (MetaWaylandTextInputV1Focus, meta_wayland_text_input_v1_focus, ++ CLUTTER_TYPE_INPUT_FOCUS) ++ ++static MetaBackend * ++backend_from_text_input_v1 (MetaWaylandTextInputV1 *text_input) ++{ ++ MetaWaylandSeat *seat = text_input->seat; ++ MetaWaylandCompositor *compositor = meta_wayland_seat_get_compositor (seat); ++ MetaContext *context = meta_wayland_compositor_get_context (compositor); ++ ++ return meta_context_get_backend (context); ++} ++ ++static uint32_t ++get_serial (MetaWaylandTextInputV1 *text_input, ++ struct wl_resource *resource) ++{ ++ return GPOINTER_TO_UINT (g_hash_table_lookup (text_input->resource_serials, ++ resource)); ++} ++ ++static void ++set_serial (MetaWaylandTextInputV1 *text_input, ++ struct wl_resource *resource, ++ uint32_t serial) ++{ ++ g_hash_table_insert (text_input->resource_serials, resource, ++ GUINT_TO_POINTER (serial)); ++} ++ ++static void ++text_input_v1_send_preedit_string (struct wl_resource *resource, ++ uint32_t serial, ++ const char *text, ++ unsigned int cursor) ++{ ++ gsize pos = 0; ++ ++ /* Chromium does not accept NULL as preedit/commit string... */ ++ text = text ? text : ""; ++ pos = g_utf8_offset_to_pointer (text, cursor) - text; ++ ++ /* We really don't need so much styles... */ ++ zwp_text_input_v1_send_preedit_styling (resource, 0, strlen (text), ++ ZWP_TEXT_INPUT_V1_PREEDIT_STYLE_UNDERLINE); ++ zwp_text_input_v1_send_preedit_cursor (resource, pos); ++ zwp_text_input_v1_send_preedit_string (resource, serial, text, text); ++} ++ ++static void ++meta_wayland_text_input_v1_focus_set_preedit_text (ClutterInputFocus *focus, ++ const gchar *text, ++ unsigned int cursor, ++ unsigned int anchor) ++{ ++ MetaWaylandTextInputV1 *text_input; ++ struct wl_resource *resource; ++ ++ text_input = META_WAYLAND_TEXT_INPUT_V1_FOCUS (focus)->text_input; ++ ++ wl_resource_for_each (resource, &text_input->focus_resource_list) ++ { ++ text_input_v1_send_preedit_string (resource, ++ get_serial (text_input, resource), ++ text, ++ cursor); ++ } ++} ++ ++static void ++meta_wayland_text_input_v1_focus_request_surrounding (ClutterInputFocus *focus) ++{ ++ MetaWaylandTextInputV1 *text_input; ++ long cursor, anchor; ++ ++ /* Clutter uses char offsets but text-input-v1 uses byte offsets. */ ++ text_input = META_WAYLAND_TEXT_INPUT_V1_FOCUS (focus)->text_input; ++ cursor = g_utf8_strlen (text_input->surrounding.text, ++ text_input->surrounding.cursor); ++ anchor = g_utf8_strlen (text_input->surrounding.text, ++ text_input->surrounding.anchor); ++ clutter_input_focus_set_surrounding (focus, ++ text_input->surrounding.text, ++ cursor, ++ anchor); ++} ++ ++static void ++text_input_v1_send_commit_string (struct wl_resource *resource, ++ uint32_t serial, ++ const char *text) ++{ ++ /* Chromium does not accept NULL as preedit/commit string... */ ++ text = text ? text : ""; ++ ++ zwp_text_input_v1_send_commit_string (resource, serial, text); ++} ++ ++static void ++meta_wayland_text_input_v1_focus_delete_surrounding (ClutterInputFocus *focus, ++ int offset, ++ guint len) ++{ ++ MetaWaylandTextInputV1 *text_input; ++ struct wl_resource *resource; ++ const char *start, *end; ++ const char *before, *after; ++ const char *cursor; ++ ++ /* ++ * offset and len are counted by UTF-8 chars, but text-input-v1's lengths are ++ * counted by bytes, so we convert UTF-8 char offsets to pointers here, this ++ * needs the surrounding text ++ */ ++ text_input = META_WAYLAND_TEXT_INPUT_V1_FOCUS (focus)->text_input; ++ offset = MIN (offset, 0); ++ ++ start = text_input->surrounding.text; ++ end = start + strlen (text_input->surrounding.text); ++ cursor = start + text_input->surrounding.cursor; ++ ++ before = g_utf8_offset_to_pointer (cursor, offset); ++ g_assert (before >= start); ++ ++ after = g_utf8_offset_to_pointer (cursor, offset + len); ++ g_assert (after <= end); ++ ++ wl_resource_for_each (resource, &text_input->focus_resource_list) ++ { ++ zwp_text_input_v1_send_delete_surrounding_text (resource, ++ before - cursor, ++ after - before); ++ /* ++ * text-input-v1 says delete_surrounding belongs to next commit, so an ++ * empty commit is required. ++ */ ++ text_input_v1_send_commit_string (resource, ++ get_serial (text_input, resource), ++ NULL); ++ } ++} ++ ++static void ++meta_wayland_text_input_v1_focus_commit_text (ClutterInputFocus *focus, ++ const gchar *text) ++{ ++ MetaWaylandTextInputV1 *text_input; ++ struct wl_resource *resource; ++ ++ text_input = META_WAYLAND_TEXT_INPUT_V1_FOCUS (focus)->text_input; ++ ++ wl_resource_for_each (resource, &text_input->focus_resource_list) ++ { ++ /* ++ * You have to clear preedit string after committing string, otherwise ++ * some apps (I reproduced with Code OSS) will send you empty surrounding ++ * text and breaks delete_surrounding_text. ++ */ ++ text_input_v1_send_commit_string (resource, ++ get_serial (text_input, resource), ++ text); ++ /* Clear preedit string because we already committed. */ ++ text_input_v1_send_preedit_string (resource, ++ get_serial (text_input, resource), ++ NULL, ++ 0); ++ } ++} ++ ++static void ++meta_wayland_text_input_v1_focus_class_init (MetaWaylandTextInputV1FocusClass *klass) ++{ ++ ClutterInputFocusClass *focus_class = CLUTTER_INPUT_FOCUS_CLASS (klass); ++ ++ focus_class->request_surrounding = meta_wayland_text_input_v1_focus_request_surrounding; ++ focus_class->delete_surrounding = meta_wayland_text_input_v1_focus_delete_surrounding; ++ focus_class->commit_text = meta_wayland_text_input_v1_focus_commit_text; ++ focus_class->set_preedit_text = meta_wayland_text_input_v1_focus_set_preedit_text; ++} ++ ++static void ++meta_wayland_text_input_v1_focus_init (MetaWaylandTextInputV1Focus *focus) ++{ ++} ++ ++static ClutterInputFocus * ++meta_wayland_text_input_focus_new (MetaWaylandTextInputV1 *text_input) ++{ ++ MetaWaylandTextInputV1Focus *focus; ++ ++ focus = g_object_new (META_TYPE_WAYLAND_TEXT_INPUT_V1_FOCUS, NULL); ++ focus->text_input = text_input; ++ ++ return CLUTTER_INPUT_FOCUS (focus); ++} ++ ++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)); ++ } ++ } ++} ++ ++static void ++meta_wayland_text_input_v1_set_focus (MetaWaylandTextInputV1 *text_input, ++ MetaWaylandSurface *surface) ++{ ++ if (text_input->surface == surface) ++ return; ++ ++ 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_focus_reset (focus); ++ clutter_input_method_focus_out (input_method); ++ } ++ ++ wl_resource_for_each (resource, &text_input->focus_resource_list) ++ { ++ zwp_text_input_v1_send_leave (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 && surface->resource) ++ { ++ 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_v1_send_enter (resource, surface->resource); ++ } ++ } ++ } ++} ++ ++static void ++text_input_v1_handle_focus_surface_destroy (struct wl_listener *listener, ++ void *data) ++{ ++ MetaWaylandTextInputV1 *text_input = wl_container_of (listener, text_input, surface_listener); ++ ++ meta_wayland_text_input_v1_set_focus (text_input, NULL); ++} ++ ++static void ++text_input_v1_destructor (struct wl_resource *resource) ++{ ++ MetaWaylandTextInputV1 *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 gboolean ++client_matches_focus (MetaWaylandTextInputV1 *text_input, ++ struct wl_client *client) ++{ ++ if (!text_input->surface) ++ return FALSE; ++ ++ return client == wl_resource_get_client (text_input->surface->resource); ++} ++ ++static void ++text_input_v1_activate (struct wl_client *client, ++ struct wl_resource *resource, ++ struct wl_resource *seat_resource, ++ struct wl_resource *surface_resource) ++{ ++ MetaWaylandTextInputV1 *text_input = wl_resource_get_user_data (resource); ++ MetaWaylandSurface *surface; ++ ClutterInputFocus *focus = text_input->input_focus; ++ ClutterInputMethod *input_method; ++ ++ /* ++ * Don't use client_matches_focus() here because we have no focused surface if ++ * not activated in text-input-v1. ++ */ ++ ++ surface = wl_resource_get_user_data (surface_resource); ++ meta_wayland_text_input_v1_set_focus (text_input, surface); ++ ++ input_method = clutter_backend_get_input_method (clutter_get_default_backend ()); ++ ++ if (input_method) ++ { ++ if (!clutter_input_focus_is_focused (focus)) ++ clutter_input_method_focus_in (input_method, focus); ++ ++ clutter_input_focus_set_can_show_preedit (focus, TRUE); ++ } ++} ++ ++static void ++text_input_v1_deactivate (struct wl_client *client, ++ struct wl_resource *resource, ++ struct wl_resource *seat_resource) ++{ ++ MetaWaylandTextInputV1 *text_input = wl_resource_get_user_data (resource); ++ ClutterInputFocus *focus = text_input->input_focus; ++ ClutterInputMethod *input_method; ++ ++ if (!client_matches_focus (text_input, client)) ++ return; ++ ++ meta_wayland_text_input_v1_set_focus (text_input, NULL); ++ ++ input_method = clutter_backend_get_input_method (clutter_get_default_backend ()); ++ if (input_method && clutter_input_focus_is_focused (focus)) ++ { ++ clutter_input_focus_reset (focus); ++ clutter_input_method_focus_out (input_method); ++ } ++} ++ ++static void ++text_input_v1_show_input_panel (struct wl_client *client, ++ struct wl_resource *resource) ++{ ++ MetaWaylandTextInputV1 *text_input = wl_resource_get_user_data (resource); ++ ClutterInputFocus *focus = text_input->input_focus; ++ ++ if (!client_matches_focus (text_input, client)) ++ return; ++ ++ clutter_input_focus_set_input_panel_state (focus, ++ CLUTTER_INPUT_PANEL_STATE_ON); ++} ++ ++static void ++text_input_v1_hide_input_panel (struct wl_client *client, ++ struct wl_resource *resource) ++{ ++ MetaWaylandTextInputV1 *text_input = wl_resource_get_user_data (resource); ++ ClutterInputFocus *focus = text_input->input_focus; ++ ++ if (!client_matches_focus (text_input, client)) ++ return; ++ ++ clutter_input_focus_set_input_panel_state (focus, ++ CLUTTER_INPUT_PANEL_STATE_OFF); ++} ++ ++static void ++text_input_v1_set_surrounding_text (struct wl_client *client, ++ struct wl_resource *resource, ++ const char *text, ++ uint32_t cursor, ++ uint32_t anchor) ++{ ++ MetaWaylandTextInputV1 *text_input = wl_resource_get_user_data (resource); ++ ClutterInputFocus *focus = text_input->input_focus; ++ long char_cursor, char_anchor; ++ ++ if (!client_matches_focus (text_input, client)) ++ return; ++ ++ /* Save the surrounding text for `delete_surrounding_text`. */ ++ g_free (text_input->surrounding.text); ++ text_input->surrounding.text = g_strdup (text); ++ text_input->surrounding.cursor = cursor; ++ text_input->surrounding.anchor = anchor; ++ ++ /* Pass the surrounding text to Clutter to handle it with input method. */ ++ /* Clutter uses char offsets but text-input-v1 uses byte offsets. */ ++ char_cursor = g_utf8_strlen (text_input->surrounding.text, ++ text_input->surrounding.cursor); ++ char_anchor = g_utf8_strlen (text_input->surrounding.text, ++ text_input->surrounding.anchor); ++ clutter_input_focus_set_surrounding (focus, ++ text_input->surrounding.text, ++ char_cursor, ++ char_anchor); ++} ++ ++static void ++text_input_v1_reset (struct wl_client *client, ++ struct wl_resource *resource) ++{ ++ MetaWaylandTextInputV1 *text_input = wl_resource_get_user_data (resource); ++ ClutterInputFocus *focus = text_input->input_focus; ++ ++ if (!client_matches_focus (text_input, client)) ++ return; ++ ++ /* ++ * This means text was changed outside of normal input method flow, but we are ++ * still focusing the same text entry, so we only reset states, but don't ++ * reset focus, cursor position and panel visibility. ++ */ ++ g_clear_pointer (&text_input->surrounding.text, g_free); ++ clutter_input_focus_set_surrounding (focus, NULL, 0, 0); ++ clutter_input_focus_set_content_hints (focus, 0); ++ clutter_input_focus_set_content_purpose (focus, ++ CLUTTER_INPUT_CONTENT_PURPOSE_NORMAL); ++} ++ ++static ClutterInputContentHintFlags ++translate_hints (uint32_t hints) ++{ ++ ClutterInputContentHintFlags clutter_hints = 0; ++ ++ if (hints & ZWP_TEXT_INPUT_V1_CONTENT_HINT_DEFAULT) ++ clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_DEFAULT; ++ if (hints & ZWP_TEXT_INPUT_V1_CONTENT_HINT_PASSWORD) ++ clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_PASSWORD; ++ if (hints & ZWP_TEXT_INPUT_V1_CONTENT_HINT_AUTO_COMPLETION) ++ clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_COMPLETION; ++ if (hints & ZWP_TEXT_INPUT_V1_CONTENT_HINT_AUTO_CORRECTION) ++ clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_AUTO_CORRECTION; ++ if (hints & ZWP_TEXT_INPUT_V1_CONTENT_HINT_AUTO_CAPITALIZATION) ++ clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_AUTO_CAPITALIZATION; ++ if (hints & ZWP_TEXT_INPUT_V1_CONTENT_HINT_LOWERCASE) ++ clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_LOWERCASE; ++ if (hints & ZWP_TEXT_INPUT_V1_CONTENT_HINT_UPPERCASE) ++ clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_UPPERCASE; ++ if (hints & ZWP_TEXT_INPUT_V1_CONTENT_HINT_TITLECASE) ++ clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_TITLECASE; ++ if (hints & ZWP_TEXT_INPUT_V1_CONTENT_HINT_HIDDEN_TEXT) ++ clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_HIDDEN_TEXT; ++ if (hints & ZWP_TEXT_INPUT_V1_CONTENT_HINT_SENSITIVE_DATA) ++ clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_SENSITIVE_DATA; ++ if (hints & ZWP_TEXT_INPUT_V1_CONTENT_HINT_LATIN) ++ clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_LATIN; ++ if (hints & ZWP_TEXT_INPUT_V1_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_V1_CONTENT_PURPOSE_NORMAL: ++ return CLUTTER_INPUT_CONTENT_PURPOSE_NORMAL; ++ case ZWP_TEXT_INPUT_V1_CONTENT_PURPOSE_ALPHA: ++ return CLUTTER_INPUT_CONTENT_PURPOSE_ALPHA; ++ case ZWP_TEXT_INPUT_V1_CONTENT_PURPOSE_DIGITS: ++ return CLUTTER_INPUT_CONTENT_PURPOSE_DIGITS; ++ case ZWP_TEXT_INPUT_V1_CONTENT_PURPOSE_NUMBER: ++ return CLUTTER_INPUT_CONTENT_PURPOSE_NUMBER; ++ case ZWP_TEXT_INPUT_V1_CONTENT_PURPOSE_PHONE: ++ return CLUTTER_INPUT_CONTENT_PURPOSE_PHONE; ++ case ZWP_TEXT_INPUT_V1_CONTENT_PURPOSE_URL: ++ return CLUTTER_INPUT_CONTENT_PURPOSE_URL; ++ case ZWP_TEXT_INPUT_V1_CONTENT_PURPOSE_EMAIL: ++ return CLUTTER_INPUT_CONTENT_PURPOSE_EMAIL; ++ case ZWP_TEXT_INPUT_V1_CONTENT_PURPOSE_NAME: ++ return CLUTTER_INPUT_CONTENT_PURPOSE_NAME; ++ case ZWP_TEXT_INPUT_V1_CONTENT_PURPOSE_PASSWORD: ++ return CLUTTER_INPUT_CONTENT_PURPOSE_PASSWORD; ++ case ZWP_TEXT_INPUT_V1_CONTENT_PURPOSE_DATE: ++ return CLUTTER_INPUT_CONTENT_PURPOSE_DATE; ++ case ZWP_TEXT_INPUT_V1_CONTENT_PURPOSE_TIME: ++ return CLUTTER_INPUT_CONTENT_PURPOSE_TIME; ++ case ZWP_TEXT_INPUT_V1_CONTENT_PURPOSE_DATETIME: ++ return CLUTTER_INPUT_CONTENT_PURPOSE_DATETIME; ++ case ZWP_TEXT_INPUT_V1_CONTENT_PURPOSE_TERMINAL: ++ return CLUTTER_INPUT_CONTENT_PURPOSE_TERMINAL; ++ } ++ ++ g_warn_if_reached (); ++ return CLUTTER_INPUT_CONTENT_PURPOSE_NORMAL; ++} ++ ++static void ++text_input_v1_set_content_type (struct wl_client *client, ++ struct wl_resource *resource, ++ uint32_t hint, ++ uint32_t purpose) ++{ ++ MetaWaylandTextInputV1 *text_input = wl_resource_get_user_data (resource); ++ ClutterInputFocus *focus = text_input->input_focus; ++ ++ if (!client_matches_focus (text_input, client)) ++ return; ++ ++ clutter_input_focus_set_content_hints (focus, translate_hints (hint)); ++ clutter_input_focus_set_content_purpose (focus, translate_purpose (purpose)); ++} ++ ++static void ++text_input_v1_set_cursor_rectangle (struct wl_client *client, ++ struct wl_resource *resource, ++ int32_t x, ++ int32_t y, ++ int32_t width, ++ int32_t height) ++{ ++ MetaWaylandTextInputV1 *text_input = wl_resource_get_user_data (resource); ++ ClutterInputFocus *focus = text_input->input_focus; ++ MtkRectangle rect = (MtkRectangle) { x, y, width, height }; ++ graphene_rect_t cursor_rect; ++ float x1, y1, x2, y2; ++ ++ if (!client_matches_focus (text_input, client)) ++ return; ++ ++ 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 (focus, &cursor_rect); ++} ++ ++static void ++text_input_v1_set_preferred_lanaguage (struct wl_client *client, ++ struct wl_resource *resource, ++ const char *language) ++{ ++ /* ClutterInputMethod does not support this so this is useless. */ ++} ++ ++/* ++ * text-input-v1 is not required to be double-buffered!!!!!!!!!!!!!!!!!!!!!!!!!! ++ * commit_state just means "I am giving you a new serial and you should use ++ * this". It can work without commit_state, chromium does not send this. ++ */ ++static void ++text_input_v1_commit_state (struct wl_client *client, ++ struct wl_resource *resource, ++ uint32_t serial) ++{ ++ MetaWaylandTextInputV1 *text_input = wl_resource_get_user_data (resource); ++ ++ if (!client_matches_focus (text_input, client)) ++ return; ++ ++ set_serial (text_input, resource, serial); ++} ++ ++static void ++text_input_v1_invoke_action (struct wl_client *client, ++ struct wl_resource *resource, ++ uint32_t button, ++ uint32_t index) ++{ ++ /* There is no doc about what button and index are, I am not an invoker. */ ++} ++ ++static struct zwp_text_input_v1_interface meta_text_input_v1_interface = { ++ text_input_v1_activate, ++ text_input_v1_deactivate, ++ text_input_v1_show_input_panel, ++ text_input_v1_hide_input_panel, ++ text_input_v1_reset, ++ text_input_v1_set_surrounding_text, ++ text_input_v1_set_content_type, ++ text_input_v1_set_cursor_rectangle, ++ text_input_v1_set_preferred_lanaguage, ++ text_input_v1_commit_state, ++ text_input_v1_invoke_action ++}; ++ ++void ++meta_wayland_text_input_v1_destroy (MetaWaylandTextInputV1 *text_input) ++{ ++ meta_wayland_text_input_v1_set_focus (text_input, NULL); ++ g_object_unref (text_input->input_focus); ++ g_hash_table_destroy (text_input->resource_serials); ++ g_clear_pointer (&text_input->surrounding.text, g_free); ++ g_free (text_input); ++} ++ ++static void ++meta_wayland_text_input_v1_create_new_resource (MetaWaylandTextInputV1 *text_input, ++ struct wl_client *client, ++ uint32_t id) ++{ ++ struct wl_resource *text_input_resource; ++ ++ text_input_resource = wl_resource_create (client, ++ &zwp_text_input_v1_interface, ++ META_ZWP_TEXT_INPUT_V1_VERSION, ++ id); ++ ++ wl_resource_set_implementation (text_input_resource, ++ &meta_text_input_v1_interface, ++ text_input, text_input_v1_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_v1_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_v1_get_text_input (struct wl_client *client, ++ struct wl_resource *resource, ++ uint32_t id) ++{ ++ MetaWaylandTextInputV1 *text_input = wl_resource_get_user_data (resource); ++ ++ meta_wayland_text_input_v1_create_new_resource (text_input, client, id); ++} ++ ++static struct zwp_text_input_manager_v1_interface meta_text_input_manager_v1_interface = { ++ text_input_manager_v1_get_text_input ++}; ++ ++static void ++bind_text_input_v1 (struct wl_client *client, ++ void *data, ++ uint32_t version, ++ uint32_t id) ++{ ++ MetaWaylandTextInputV1 *text_input = data; ++ struct wl_resource *resource; ++ ++ resource = wl_resource_create (client, ++ &zwp_text_input_manager_v1_interface, ++ META_ZWP_TEXT_INPUT_V1_VERSION, ++ id); ++ wl_resource_set_implementation (resource, ++ &meta_text_input_manager_v1_interface, ++ text_input, NULL); ++} ++ ++gboolean ++meta_wayland_text_input_v1_init (MetaWaylandCompositor *compositor) ++{ ++ return (wl_global_create (compositor->wayland_display, ++ &zwp_text_input_manager_v1_interface, ++ META_ZWP_TEXT_INPUT_V1_VERSION, ++ compositor->seat->text_input_v1, ++ bind_text_input_v1) != NULL); ++} ++ ++MetaWaylandTextInputV1 * ++meta_wayland_text_input_v1_new (MetaWaylandSeat *seat) ++{ ++ MetaWaylandTextInputV1 *text_input; ++ ++ text_input = g_new0 (MetaWaylandTextInputV1, 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_v1_handle_focus_surface_destroy; ++ ++ text_input->resource_serials = g_hash_table_new (NULL, NULL); ++ ++ return text_input; ++} ++ ++/* This function eats key events and will send them to input method. */ ++gboolean ++meta_wayland_text_input_v1_update (MetaWaylandTextInputV1 *text_input, ++ const ClutterEvent *event) ++{ ++ ClutterInputFocus *focus = text_input->input_focus; ++ ClutterEventType event_type; ++ ++ if (!text_input->surface || !clutter_input_focus_is_focused (focus)) ++ return FALSE; ++ ++ event_type = clutter_event_type (event); ++ ++ if (event_type == CLUTTER_KEY_PRESS || ++ event_type == CLUTTER_KEY_RELEASE) ++ { ++ gboolean filtered = FALSE; ++ ++ filtered = clutter_input_focus_filter_event (focus, event); ++ ++ return filtered; ++ } ++ ++ return FALSE; ++} ++ ++gboolean ++meta_wayland_text_input_v1_handle_event (MetaWaylandTextInputV1 *text_input, ++ const ClutterEvent *event) ++{ ++ ClutterInputFocus *focus = text_input->input_focus; ++ ClutterEventType event_type; ++ gboolean retval; ++ ++ if (!text_input->surface || !clutter_input_focus_is_focused (focus)) ++ return FALSE; ++ ++ event_type = clutter_event_type (event); ++ ++ retval = clutter_input_focus_process_event (focus, event); ++ ++ if (event_type == CLUTTER_BUTTON_PRESS || event_type == CLUTTER_TOUCH_BEGIN) ++ { ++ MetaWaylandSurface *surface = NULL; ++ MetaBackend *backend; ++ ClutterStage *stage; ++ ClutterActor *actor; ++ ++ backend = backend_from_text_input_v1 (text_input); ++ stage = CLUTTER_STAGE (meta_backend_get_stage (backend)); ++ ++ actor = clutter_stage_get_device_actor (stage, ++ clutter_event_get_device (event), ++ clutter_event_get_event_sequence (event)); ++ ++ if (META_IS_SURFACE_ACTOR_WAYLAND (actor)) ++ { ++ MetaSurfaceActorWayland *actor_wayland = ++ META_SURFACE_ACTOR_WAYLAND (actor); ++ ++ surface = meta_surface_actor_wayland_get_surface (actor_wayland); ++ ++ if (surface == text_input->surface) ++ clutter_input_focus_reset (focus); ++ } ++ } ++ ++ return retval; ++} +diff --git a/src/wayland/meta-wayland-text-input-v1.h b/src/wayland/meta-wayland-text-input-v1.h +new file mode 100644 +index 000000000..79b1c0a54 +--- /dev/null ++++ b/src/wayland/meta-wayland-text-input-v1.h +@@ -0,0 +1,38 @@ ++/* ++ * Copyright (C) 2024 SUSE LLC ++ * ++ * 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, see . ++ * ++ * Author: Alynx Zhou ++ */ ++ ++#pragma once ++ ++#include ++ ++#include "meta/window.h" ++#include "wayland/meta-wayland-types.h" ++ ++typedef struct _MetaWaylandTextInputV1 MetaWaylandTextInputV1; ++ ++MetaWaylandTextInputV1 * meta_wayland_text_input_v1_new (MetaWaylandSeat *seat); ++void meta_wayland_text_input_v1_destroy (MetaWaylandTextInputV1 *text_input); ++ ++gboolean meta_wayland_text_input_v1_init (MetaWaylandCompositor *compositor); ++ ++gboolean meta_wayland_text_input_v1_update (MetaWaylandTextInputV1 *text_input, ++ const ClutterEvent *event); ++ ++gboolean meta_wayland_text_input_v1_handle_event (MetaWaylandTextInputV1 *text_input, ++ const ClutterEvent *event); +diff --git a/src/wayland/meta-wayland-versions.h b/src/wayland/meta-wayland-versions.h +index 900f30d78..a77b81461 100644 +--- a/src/wayland/meta-wayland-versions.h ++++ b/src/wayland/meta-wayland-versions.h +@@ -49,6 +49,7 @@ + #define META_ZXDG_OUTPUT_V1_VERSION 3 + #define META_ZWP_XWAYLAND_KEYBOARD_GRAB_V1_VERSION 1 + #define META_ZWP_TEXT_INPUT_V3_VERSION 1 ++#define META_ZWP_TEXT_INPUT_V1_VERSION 1 + #define META_WP_VIEWPORTER_VERSION 1 + #define META_ZWP_PRIMARY_SELECTION_V1_VERSION 1 + #define META_WP_PRESENTATION_VERSION 1 +diff --git a/src/wayland/meta-wayland.c b/src/wayland/meta-wayland.c +index 501b69a91..0114823c1 100644 +--- a/src/wayland/meta-wayland.c ++++ b/src/wayland/meta-wayland.c +@@ -865,6 +865,7 @@ meta_wayland_compositor_new (MetaContext *context) + meta_wayland_keyboard_shortcuts_inhibit_init (compositor); + meta_wayland_surface_inhibit_shortcuts_dialog_init (); + meta_wayland_text_input_init (compositor); ++ meta_wayland_text_input_v1_init (compositor); + meta_wayland_init_presentation_time (compositor); + meta_wayland_activation_init (compositor); + meta_wayland_transaction_init (compositor); +@@ -1124,6 +1125,12 @@ meta_wayland_compositor_get_text_input (MetaWaylandCompositor *compositor) + return compositor->seat->text_input; + } + ++MetaWaylandTextInputV1 * ++meta_wayland_compositor_get_text_input_v1 (MetaWaylandCompositor *compositor) ++{ ++ return compositor->seat->text_input_v1; ++} ++ + static void + meta_wayland_compositor_update_focus (MetaWaylandCompositor *compositor, + MetaWindow *window) +diff --git a/src/wayland/meta-wayland.h b/src/wayland/meta-wayland.h +index 0a0476eba..c23e82cdc 100644 +--- a/src/wayland/meta-wayland.h ++++ b/src/wayland/meta-wayland.h +@@ -26,6 +26,7 @@ + #include "meta/types.h" + #include "meta/meta-wayland-compositor.h" + #include "wayland/meta-wayland-text-input.h" ++#include "wayland/meta-wayland-text-input-v1.h" + #include "wayland/meta-wayland-types.h" + + META_EXPORT_TEST +@@ -88,6 +89,7 @@ void meta_wayland_compositor_schedule_surface_association (Me + MetaWindow *window); + + MetaWaylandTextInput * meta_wayland_compositor_get_text_input (MetaWaylandCompositor *compositor); ++MetaWaylandTextInputV1 * meta_wayland_compositor_get_text_input_v1 (MetaWaylandCompositor *compositor); + + #ifdef HAVE_XWAYLAND + void meta_wayland_compositor_notify_surface_id (MetaWaylandCompositor *compositor, +-- +2.45.0 + diff --git a/mutter.changes b/mutter.changes index f773a3c..da6d8a4 100644 --- a/mutter.changes +++ b/mutter.changes @@ -1,3 +1,11 @@ +------------------------------------------------------------------- +Fri May 17 03:26:48 UTC 2024 - Alynx Zhou + +- Add mutter-implement-text-input-v1.patch: This allows input + method to work in Chromium/Electron-based apps with Wayland Ozone + platform, which only has text-input-v1 support + (glgo#GNOME/mutter!3751, bsc#1219505). + ------------------------------------------------------------------- Wed May 1 13:01:02 UTC 2024 - Dominique Leuenberger diff --git a/mutter.spec b/mutter.spec index 1f3ed37..5e19fbd 100644 --- a/mutter.spec +++ b/mutter.spec @@ -38,6 +38,8 @@ Patch2: mutter-window-actor-Special-case-shaped-Java-windows.patch Patch3: mutter-fix-x11-restart.patch # PATCH-FIX-OPENSUSE 0001-Revert-clutter-actor-Cache-stage-relative-instead-of.patch glgo#GNOME/mutter#3302 bsc#1219546 alynx.zhou@suse.com -- Fix partial update on VT switch Patch4: 0001-Revert-clutter-actor-Cache-stage-relative-instead-of.patch +#PATCH-FEATURE-OPENSUSE mutter-implement-text-input-v1.patch glgo#GNOME/mutter!3751 bsc#1219505 alynx.zhou@suse.com -- Allow input method to work in Wayland Chromium +Patch5: mutter-implement-text-input-v1.patch ## SLE-only patches start at 1000 # PATCH-FEATURE-SLE mutter-SLE-bell.patch FATE#316042 bnc#889218 idonmez@suse.com -- make audible bell work out of the box. @@ -149,6 +151,7 @@ applications that want to make use of the mutter library. %patch -P 2 -p1 %patch -P 3 -p1 %patch -P 4 -p1 +%patch -P 5 -p1 %endif # SLE-only patches and translations. %if 0%{?sle_version}