Skip to content

Commit 24c633d

Browse files
committed
JBR-9817 Wayland: text-input-unstable-v3: Japanese input text partially highlighted in search
* Completely reimplementing JavaPreeditString#fromWaylandPreeditString and JavaCommitString#fromWaylandCommitString to make sure they don't ignore UTF-8 encoding errors and that JavaPreeditString#fromWaylandPreeditString sensibly handles invalid cursor indices. * Covering the new JavaPreeditString#fromWaylandPreeditString with tests. * Working around https://gitlab.gnome.org/GNOME/mutter/-/issues/3547. (cherry picked from commit e621663)
1 parent d82b2fe commit 24c633d

File tree

7 files changed

+1604
-61
lines changed

7 files changed

+1604
-61
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright 2026 JetBrains s.r.o.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation. Oracle designates this
8+
* particular file as subject to the "Classpath" exception as provided
9+
* by Oracle in the LICENSE file that accompanied this code.
10+
*
11+
* This code is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14+
* version 2 for more details (a copy is included in the LICENSE file that
15+
* accompanied this code).
16+
*
17+
* You should have received a copy of the GNU General Public License version
18+
* 2 along with this work; if not, write to the Free Software Foundation,
19+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20+
*
21+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22+
* or visit www.oracle.com if you need additional information or have any
23+
* questions.
24+
*/
25+
26+
package sun.awt.wl.im.text_input_unstable_v3;
27+
28+
import sun.awt.UNIXToolkit;
29+
30+
import java.awt.Toolkit;
31+
import java.security.AccessController;
32+
import java.security.PrivilegedAction;
33+
import java.util.Objects;
34+
import java.util.concurrent.atomic.AtomicReference;
35+
36+
final class CurrentDesktopInfo {
37+
38+
private CurrentDesktopInfo() {}
39+
40+
41+
public static boolean isGnome() {
42+
Boolean result = isGnome.get();
43+
44+
if (result == null) {
45+
synchronized (CurrentDesktopInfo.class) {
46+
if (Toolkit.getDefaultToolkit() instanceof UNIXToolkit unixToolkit) {
47+
result = "gnome".equals(unixToolkit.getDesktop());
48+
} else {
49+
result = false;
50+
}
51+
52+
isGnome.set(result);
53+
}
54+
55+
return result;
56+
}
57+
58+
return result;
59+
}
60+
// {@code null} if not initialized yet
61+
private static final AtomicReference<Boolean> isGnome = new AtomicReference<>(null);
62+
63+
/** negative if couldn't obtain or the desktop is not GNOME */
64+
public static int getGnomeShellMajorVersion() {
65+
Integer result = gnomeVersion.get();
66+
67+
if (result == null) {
68+
synchronized (CurrentDesktopInfo.class) {
69+
if (!isGnome()) {
70+
result = -1;
71+
} else if (Toolkit.getDefaultToolkit() instanceof UNIXToolkit unixToolkit) {
72+
try {
73+
@SuppressWarnings("removal")
74+
final Integer version = AccessController.doPrivileged(
75+
(PrivilegedAction<Integer>)unixToolkit::getGnomeShellMajorVersion
76+
);
77+
78+
result = Objects.requireNonNullElse(version, -1);
79+
} catch (Exception ignored) {
80+
result = -1;
81+
}
82+
} else {
83+
result = -1;
84+
}
85+
86+
gnomeVersion.set(result);
87+
}
88+
89+
return result;
90+
}
91+
92+
return result;
93+
}
94+
// {@code null} if not initialized yet, negative if couldn't obtain or the desktop is not GNOME
95+
private static final AtomicReference<Integer> gnomeVersion = new AtomicReference<>(null);
96+
}

src/java.desktop/unix/classes/sun/awt/wl/im/text_input_unstable_v3/IncomingChanges.java

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2025 JetBrains s.r.o.
2+
* Copyright 2025-2026 JetBrains s.r.o.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -25,6 +25,7 @@
2525

2626
package sun.awt.wl.im.text_input_unstable_v3;
2727

28+
import java.nio.charset.CharacterCodingException;
2829
import java.util.Arrays;
2930
import java.util.Objects;
3031

@@ -36,6 +37,25 @@
3637
*/
3738
final class IncomingChanges
3839
{
40+
public static class ConversionException extends java.io.IOException {
41+
@java.io.Serial
42+
private static final long serialVersionUID = 7010594789107134519L;
43+
44+
public ConversionException(String message) {
45+
super(message);
46+
}
47+
public ConversionException(Throwable cause) {
48+
super(cause);
49+
}
50+
public ConversionException(String message, Throwable cause) {
51+
super(message, cause);
52+
}
53+
public ConversionException(Throwable cause, String format, Object... args) {
54+
super(String.format(format, args), cause);
55+
}
56+
}
57+
58+
3959
public IncomingChanges updatePreeditString(byte[] newPreeditStringUtf8, int newPreeditStringCursorBeginUtf8Byte, int newPreeditStringCursorEndUtf8Byte) {
4060
this.doUpdatePreeditString = true;
4161
this.newPreeditStringUtf8 = newPreeditStringUtf8;
@@ -50,18 +70,30 @@ public IncomingChanges updatePreeditString(byte[] newPreeditStringUtf8, int newP
5070
* @return {@code null} if there are no changes in the preedit string
5171
* (i.e. {@link #updatePreeditString(byte[], int, int)} hasn't been called);
5272
* an instance of JavaPreeditString otherwise.
73+
* @throws ConversionException if failed to convert the data provided in {@link #updatePreeditString(byte[], int, int)}
74+
* to an instance of JavaPreeditString.
5375
* @see JavaPreeditString
5476
*/
55-
public JavaPreeditString getPreeditString() {
77+
public JavaPreeditString getPreeditString() throws ConversionException {
5678
if (cachedResultPreeditString != null) {
5779
return cachedResultPreeditString;
5880
}
5981

60-
cachedResultPreeditString = doUpdatePreeditString
61-
? JavaPreeditString.fromWaylandPreeditString(newPreeditStringUtf8, newPreeditStringCursorBeginUtf8Byte, newPreeditStringCursorEndUtf8Byte)
62-
: null;
82+
try {
83+
cachedResultPreeditString = doUpdatePreeditString
84+
? JavaPreeditString.fromWaylandPreeditString(newPreeditStringUtf8, newPreeditStringCursorBeginUtf8Byte, newPreeditStringCursorEndUtf8Byte)
85+
: null;
6386

64-
return cachedResultPreeditString;
87+
return cachedResultPreeditString;
88+
} catch (CharacterCodingException err) {
89+
throw new ConversionException(
90+
err,
91+
"Failed to convert zwp_text_input_v3::preedit_string(%s, %d, %d) to JavaPreeditString",
92+
byteArrayToHexArrayString(newPreeditStringUtf8),
93+
newPreeditStringCursorBeginUtf8Byte,
94+
newPreeditStringCursorEndUtf8Byte
95+
);
96+
}
6597
}
6698

6799

@@ -77,18 +109,28 @@ public IncomingChanges updateCommitString(byte[] newCommitStringUtf8) {
77109
* @return {@code null} if there are no changes in the commit string
78110
* (i.e. {@link #updateCommitString(byte[])} hasn't been called);
79111
* an instance of JavaCommitString otherwise.
112+
* @throws ConversionException if failed to convert the data provided in {@link #updateCommitString(byte[])}
113+
* to an instance of JavaCommitString.
80114
* @see JavaCommitString
81115
*/
82-
public JavaCommitString getCommitString() {
116+
public JavaCommitString getCommitString() throws ConversionException {
83117
if (cachedResultCommitString != null) {
84118
return cachedResultCommitString;
85119
}
86120

87-
cachedResultCommitString = doUpdateCommitString
88-
? JavaCommitString.fromWaylandCommitString(newCommitStringUtf8)
89-
: null;
121+
try {
122+
cachedResultCommitString = doUpdateCommitString
123+
? JavaCommitString.fromWaylandCommitString(newCommitStringUtf8)
124+
: null;
90125

91-
return cachedResultCommitString;
126+
return cachedResultCommitString;
127+
} catch (CharacterCodingException err) {
128+
throw new ConversionException(
129+
err,
130+
"Failed to convert zwp_text_input_v3::commit_string(%s) to JavaCommitString",
131+
byteArrayToHexArrayString(newCommitStringUtf8)
132+
);
133+
}
92134
}
93135

94136

@@ -128,4 +170,27 @@ public int hashCode() {
128170
private boolean doUpdateCommitString = false;
129171
private byte[] newCommitStringUtf8 = null;
130172
private JavaCommitString cachedResultCommitString = null;
173+
174+
175+
private static String byteArrayToHexArrayString(byte[] arr) {
176+
if (arr == null)
177+
return "null";
178+
179+
final int iMax = Math.min(arr.length - 1, 15);
180+
if (iMax == -1)
181+
return "[]";
182+
183+
final StringBuilder b = new StringBuilder();
184+
b.append('[');
185+
for (int i = 0; ; ++i) {
186+
b.append("0x").append(Integer.toHexString(Byte.toUnsignedInt(arr[i])));
187+
if (i == iMax) {
188+
if (iMax < arr.length - 1) {
189+
b.append(", ...");
190+
}
191+
return b.append(']').toString();
192+
}
193+
b.append(", ");
194+
}
195+
}
131196
}

src/java.desktop/unix/classes/sun/awt/wl/im/text_input_unstable_v3/JavaCommitString.java

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2025 JetBrains s.r.o.
2+
* Copyright 2025-2026 JetBrains s.r.o.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -25,6 +25,13 @@
2525

2626
package sun.awt.wl.im.text_input_unstable_v3;
2727

28+
import java.nio.ByteBuffer;
29+
import java.nio.CharBuffer;
30+
import java.nio.charset.CharacterCodingException;
31+
import java.nio.charset.CharsetDecoder;
32+
import java.nio.charset.CoderResult;
33+
import java.nio.charset.CodingErrorAction;
34+
import java.nio.charset.StandardCharsets;
2835
import java.util.Objects;
2936

3037
record JavaCommitString(String text) {
@@ -34,8 +41,39 @@ record JavaCommitString(String text) {
3441

3542
public static final JavaCommitString EMPTY = new JavaCommitString("");
3643

37-
/** Never returns {@code null}. */
38-
public static JavaCommitString fromWaylandCommitString(byte[] utf8Bytes) {
39-
return new JavaCommitString(Utilities.utf8BytesToJavaString(utf8Bytes));
44+
/**
45+
* Converts a UTF-8 string received in a {@code zwp_text_input_v3::commit_string} event to its UTF-16 equivalent.
46+
*
47+
* @return an instance of {@code JavaCommitString}. Never returns {@code null}.
48+
* @throws CharacterCodingException if {@code utf8Bytes} doesn't represent a valid UTF-8 string.
49+
*/
50+
public static JavaCommitString fromWaylandCommitString(byte[] utf8Bytes) throws CharacterCodingException {
51+
// The only Unicode code point that can contain zero byte(s) is U+000000.
52+
// It hardly makes sense to have it at the end of preedit/commit strings, so let's trim it.
53+
final int utf8BytesCorrectedLength = Utilities.getLengthOfUtf8BytesWithoutTrailingNULs(utf8Bytes);
54+
55+
if (utf8BytesCorrectedLength < 1) {
56+
return JavaCommitString.EMPTY;
57+
}
58+
59+
final CharsetDecoder utf8Decoder = StandardCharsets.UTF_8.newDecoder()
60+
.onMalformedInput(CodingErrorAction.REPORT)
61+
// there can't be unmappable characters for this
62+
// kind of conversion, so REPLACE is just in case
63+
.onUnmappableCharacter(CodingErrorAction.REPLACE);
64+
final CharBuffer decodingBuffer = CharBuffer.allocate(utf8BytesCorrectedLength + 1);
65+
final ByteBuffer utf8BytesBuffer = ByteBuffer.wrap(utf8Bytes, 0, utf8BytesCorrectedLength);
66+
67+
CoderResult decodingResult = utf8Decoder.decode(utf8BytesBuffer, decodingBuffer, true);
68+
if (decodingResult.isError() || decodingResult.isOverflow()) {
69+
decodingResult.throwException();
70+
}
71+
72+
decodingResult = utf8Decoder.flush(decodingBuffer);
73+
if (decodingResult.isError() || decodingResult.isOverflow()) {
74+
decodingResult.throwException();
75+
}
76+
77+
return new JavaCommitString(decodingBuffer.flip().toString());
4078
}
4179
}

0 commit comments

Comments
 (0)