From bf80402faefbf06943fb4e0cd42148ce34877abc Mon Sep 17 00:00:00 2001 From: Nikita Provotorov Date: Tue, 12 Mar 2024 04:39:33 +0100 Subject: [PATCH] JBR-3112 Linux: Last character issue with Korean. - Ignores the IM text returned from XmbLookupString/XwcLookupString if the KeyPress event which XmbResetIC was called with was synthetic and the first after a call of XmbResetIC/XwcResetIC. - Only for the new mode introduced in JBR-2460 (-Djb.awt.newXimClient.preferBelowTheSpot=true): cancel text composing on each mouse press, so that preedit text stops following the caret if it's moving in response to mouse clicks. (cherry picked from commit 43a9a3a17a2cfbe8078737e6e64f0cce6eb3cf8c) (cherry picked from commit 156e5d9b65bbd996fe3e454dfce942a7f09c3249) (cherry picked from commit 59f0ca804f383a11c072946bf7a919958603e3c7) --- .../native/libawt_xawt/awt/awt_InputMethod.c | 2 +- .../classes/sun/awt/X11/XInputMethod.java | 57 ++++++++++++++++ .../unix/classes/sun/awt/X11/XToolkit.java | 65 +++++++++++++++++++ .../unix/classes/sun/awt/X11/XWindow.java | 10 ++- .../classes/sun/awt/X11InputMethodBase.java | 40 ++++++++---- .../native/libawt_xawt/awt/awt_InputMethod.c | 27 ++++---- .../unix/native/libawt_xawt/xawt/XWindow.c | 10 ++- 7 files changed, 183 insertions(+), 28 deletions(-) diff --git a/src/java.desktop/aix/native/libawt_xawt/awt/awt_InputMethod.c b/src/java.desktop/aix/native/libawt_xawt/awt/awt_InputMethod.c index 88eb3cbec2d6..1038da7b9524 100644 --- a/src/java.desktop/aix/native/libawt_xawt/awt/awt_InputMethod.c +++ b/src/java.desktop/aix/native/libawt_xawt/awt/awt_InputMethod.c @@ -374,7 +374,7 @@ setXICWindowFocus(XIC ic, Window w) #define INITIAL_LOOKUP_BUF_SIZE 512 Boolean -awt_x11inputmethod_lookupString(XKeyPressedEvent *event, KeySym *keysymp) +awt_x11inputmethod_lookupString(XKeyPressedEvent *event, KeySym *keysymp, Boolean /* not used */) { JNIEnv *env = GetJNIEnv(); X11InputMethodData *pX11IMData = NULL; diff --git a/src/java.desktop/unix/classes/sun/awt/X11/XInputMethod.java b/src/java.desktop/unix/classes/sun/awt/X11/XInputMethod.java index 8d16aeff6cfe..3fc9f51a4b51 100644 --- a/src/java.desktop/unix/classes/sun/awt/X11/XInputMethod.java +++ b/src/java.desktop/unix/classes/sun/awt/X11/XInputMethod.java @@ -99,11 +99,68 @@ public final class XInputMethod extends X11InputMethod { @Override public void dispatchEvent(AWTEvent e) { if (doesSupportMovingCandidatesNativeWindow) { + if (e.getID() == MouseEvent.MOUSE_PRESSED) { + /* doesSupportMovingCandidatesNativeWindow == true means that natively the IM uses XIMPreeditPosition + * input mode (the term "input style" is used in XOrg docs). + * The main flaw of this mode is that AWT doesn't receive any information about changes in the + * currently composed preedit text. In other words, Java applications don't know whether + * composing is happening now or not and don't have a way to get the current preedit text. Therefore, + * when the caret position changes in response to mouse clicks, Swing doesn't understand that + * there's a need to discard the preedit text (because it thinks there is no any), see + * javax.swing.text.JTextComponent.ComposedTextCaret#positionCaret. + * To prevent the preedit text from following the caret when it's moved in response to mouse clicks, + * let's manually discard the preedit text here. + */ + endComposition(); + } clientComponentCaretPositionTracker.onDispatchEvent(e); } super.dispatchEvent(e); } + @Override + public void endComposition() { + if (!doesSupportMovingCandidatesNativeWindow) { + // Use the old implementation if + // the IM isn't using the new mode introduced in JBR-2460 (XIMPreeditPosition) + super.endComposition(); + return; + } + + if (disposed) { + return; + } + + String preeditText = invokeResetXIC(); + needResetXIC = false; + + awtLock(); + try { + if (composedText != null) { + composedText = null; + // Remove any existing composed text by posting an InputMethodEvent with null composed text + postInputMethodEvent( + InputMethodEvent.INPUT_METHOD_TEXT_CHANGED, + null, + 0, + null, + null, + EventQueue.getMostRecentEventTime() + ); + } + + if (committedText != null) { + preeditText = preeditText == null ? committedText : committedText + preeditText; + committedText = null; + } + + if (preeditText != null && !preeditText.isEmpty()) { + dispatchCommittedText(preeditText); + } + } finally { + awtUnlock(); + } + } // Is called from native private static boolean isJbNewXimClientEnabled() { diff --git a/src/java.desktop/unix/classes/sun/awt/X11/XToolkit.java b/src/java.desktop/unix/classes/sun/awt/X11/XToolkit.java index bd416905deac..52886085b423 100644 --- a/src/java.desktop/unix/classes/sun/awt/X11/XToolkit.java +++ b/src/java.desktop/unix/classes/sun/awt/X11/XToolkit.java @@ -955,6 +955,14 @@ public final class XToolkit extends UNIXToolkit implements Runnable { processXkbChanges(ev); } + if (ev.get_type() == XConstants.KeyPress) { + doesCurrentlyDispatchedKeyPressContainThePreeditTextOfLastXResetIC = + mayXResetICReturnThePreeditTextViaNextKeyPressEvent && + isKeyPressSyntetic(ev.get_xkey()); + + mayXResetICReturnThePreeditTextViaNextKeyPressEvent = false; + } + if (XDropTargetEventProcessor.processEvent(ev) || XDragSourceContextPeer.processEvent(ev)) { continue; @@ -1003,6 +1011,7 @@ public final class XToolkit extends UNIXToolkit implements Runnable { XBaseWindow.ungrabInput(); processException(thr); } finally { + doesCurrentlyDispatchedKeyPressContainThePreeditTextOfLastXResetIC = false; // free event data if XGetEventData was called XlibWrapper.XFreeEventData(getDisplay(), ev.pData); awtUnlock(); @@ -1010,6 +1019,62 @@ public final class XToolkit extends UNIXToolkit implements Runnable { } } + + // ================================================================================================================ + // JBR-3112 Linux: Last character issue with Korean. + // XmbResetIC/XwcResetIC are called (by sun.awt.X11InputMethodBase#endComposition) when + // the keyboard focus goes to another Java component. + // By the X11 specification, these functions must return the current preedit text. However, some + // input methods (e.g., iBus and fcitx4) don't return the preedit text, but instead send it later + // (asynchronously) via a combination of a synthetic KeyPress event + XmbLookupString applied to it. + // Not only does this behavior breaks the X11 specification, + // but it also causes the preedit text to wrongly go to the newly focused Java component rather than + // its intended target, the previously focused component for which the preedit text was originally composed. + // Thus, in order to prevent the "outdated" preedit text from going to the newly focused component, let's + // at least discard it at all. + // *How* it's done: the toolkit gets notified whenever XmbResetIC/XwcResetIC gets called and then + // discards the preedit text returned from XmbLookupString/XwcLookupString, applied to the next + // KeyPress event being dispatched. + // ================================================================================================================ + + private volatile boolean mayXResetICReturnThePreeditTextViaNextKeyPressEvent = false; + private boolean doesCurrentlyDispatchedKeyPressContainThePreeditTextOfLastXResetIC = false; + + /** + * Notifies the toolkit that XmbResetIC/XwcResetIC has recently returned null + * (likely meaning that the preedit text will be sent later via a synthetic KeyPress event, + * although the focus may have already moved to another component) + */ + public void xResetICMayReturnThePreeditTextViaNextKeyPressEvent() { + mayXResetICReturnThePreeditTextViaNextKeyPressEvent = true; + } + + /** + * @return true if the composed text returned from XmbLookupString/XwcLookupString + * (see function awt_x11inputmethod_lookupString in awt_InputMethod.c), applied to the currently + * dispatched KeyEvent, must be discarded (instead of being dispatched to the focused component) ; + * false otherwise + * @see XWindow#handleKeyPress(XKeyEvent) + */ + public boolean doesCurrentlyDispatchedKeyPressContainThePreeditTextOfLastXResetIC() { + assert isToolkitThread(); + return doesCurrentlyDispatchedKeyPressContainThePreeditTextOfLastXResetIC; + } + + private static boolean isKeyPressSyntetic(XKeyEvent ev) { + assert (ev.get_type() == XConstants.KeyPress); + + return ( (ev.get_root() == 0) && + (ev.get_subwindow() == 0) && + (ev.get_time() == 0) && + (ev.get_x() == 0) && + (ev.get_y() == 0) && + (ev.get_x_root() == 0) && + (ev.get_y_root() == 0) && + (ev.get_state() == 0) ); + } + + /** * Listener installed to detect display changes. */ diff --git a/src/java.desktop/unix/classes/sun/awt/X11/XWindow.java b/src/java.desktop/unix/classes/sun/awt/X11/XWindow.java index c14c4ef11275..544507325400 100644 --- a/src/java.desktop/unix/classes/sun/awt/X11/XWindow.java +++ b/src/java.desktop/unix/classes/sun/awt/X11/XWindow.java @@ -1026,7 +1026,8 @@ class XWindow extends XBaseWindow implements X11ComponentPeer { // REMIND: need to implement looking for disabled events private native boolean x11inputMethodLookupString(long event, - long[] keysymArray); + long[] keysymArray, + boolean keyPressContainsThePreeditTextOfLastXResetIC); private native boolean haveCurrentX11InputMethodInstance(); @@ -1288,7 +1289,12 @@ class XWindow extends XBaseWindow implements X11ComponentPeer { if ( //TODO check if there's an active input method instance // without calling a native method. Is it necessary though? haveCurrentX11InputMethodInstance()) { - if (x11inputMethodLookupString(ev.pData, keysym)) { + + final boolean keyPressContainsThePreeditTextOfLastXResetIC = + Toolkit.getDefaultToolkit() instanceof XToolkit xToolkit && + xToolkit.doesCurrentlyDispatchedKeyPressContainThePreeditTextOfLastXResetIC(); + + if (x11inputMethodLookupString(ev.pData, keysym, keyPressContainsThePreeditTextOfLastXResetIC)) { if (keyEventLog.isLoggable(PlatformLogger.Level.FINE)) { keyEventLog.fine("--XWindow.java XIM did process event; return; dec keysym processed:"+(keysym[0])+ "; hex keysym processed:"+Long.toHexString(keysym[0]) diff --git a/src/java.desktop/unix/classes/sun/awt/X11InputMethodBase.java b/src/java.desktop/unix/classes/sun/awt/X11InputMethodBase.java index 7a82384e1187..c038d0947638 100644 --- a/src/java.desktop/unix/classes/sun/awt/X11InputMethodBase.java +++ b/src/java.desktop/unix/classes/sun/awt/X11InputMethodBase.java @@ -30,6 +30,7 @@ import java.awt.AWTException; import java.awt.Component; import java.awt.Container; import java.awt.EventQueue; +import java.awt.Toolkit; import java.awt.Window; import java.awt.event.InputMethodEvent; import java.awt.font.TextAttribute; @@ -37,10 +38,6 @@ import java.awt.font.TextHitInfo; import java.awt.im.InputMethodHighlight; import java.awt.im.spi.InputMethodContext; import java.awt.peer.ComponentPeer; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; import java.lang.Character.Subset; import java.lang.ref.WeakReference; import java.text.AttributedCharacterIterator; @@ -49,9 +46,8 @@ import java.util.Collections; import java.util.HashMap; import java.util.Locale; import java.util.Map; -import java.util.StringTokenizer; -import java.util.regex.Pattern; +import sun.awt.X11.XToolkit; import sun.awt.im.InputMethodAdapter; import sun.util.logging.PlatformLogger; @@ -255,7 +251,7 @@ public abstract class X11InputMethodBase extends InputMethodAdapter { */ if (needResetXIC && haveActiveClient() && getClientComponent() != needResetXICClient.get()){ - resetXIC(); + invokeResetXIC(); // needs to reset the last xic focused component. lastXICFocussedComponent = null; @@ -313,7 +309,7 @@ public abstract class X11InputMethodBase extends InputMethodAdapter { lastXICFocussedComponent = null; isLastXICActive = false; - resetXIC(); + invokeResetXIC(); needResetXICClient.clear(); needResetXIC = false; } @@ -375,7 +371,7 @@ public abstract class X11InputMethodBase extends InputMethodAdapter { // method could get the input focus. disableInputMethod(); if (needResetXIC) { - resetXIC(); + invokeResetXIC(); needResetXICClient.clear(); needResetXIC = false; } @@ -477,7 +473,7 @@ public abstract class X11InputMethodBase extends InputMethodAdapter { } } - private void dispatchCommittedText(String str) { + protected final void dispatchCommittedText(String str) { dispatchCommittedText(str, EventQueue.getMostRecentEventTime()); } @@ -625,7 +621,7 @@ public abstract class X11InputMethodBase extends InputMethodAdapter { return; } - String text = resetXIC(); + String text = invokeResetXIC(); /* needResetXIC is only set to true for active client. So passive client should not reset the flag to false. */ if (active) { @@ -812,6 +808,27 @@ public abstract class X11InputMethodBase extends InputMethodAdapter { } } + protected final String invokeResetXIC() { + if (Toolkit.getDefaultToolkit() instanceof XToolkit xToolkit) { + awtLock(); + try { + final String resetResult = resetXIC(); + if (resetResult == null) { + // If XmbResetIC/XwcResetIC returns null, it means one of the following: + // * There was no preedit text + // * In case of iBus/fcitx4, the preedit text is sent later to the toolkit via + // a synthetic KeyPress event + XmbLookupString/XwcLookupString applied to it + xToolkit.xResetICMayReturnThePreeditTextViaNextKeyPressEvent(); + } + return resetResult; + } finally { + awtUnlock(); + } + } else { + return resetXIC(); + } + } + /* * Native methods */ @@ -826,6 +843,7 @@ public abstract class X11InputMethodBase extends InputMethodAdapter { protected native void disposeXIC(); + /** Don't use it directly, use {@link #invokeResetXIC} instead */ private native String resetXIC(); protected native boolean setCompositionEnabledNative(boolean enable); diff --git a/src/java.desktop/unix/native/libawt_xawt/awt/awt_InputMethod.c b/src/java.desktop/unix/native/libawt_xawt/awt/awt_InputMethod.c index a7473da4663e..8af449e848c3 100644 --- a/src/java.desktop/unix/native/libawt_xawt/awt/awt_InputMethod.c +++ b/src/java.desktop/unix/native/libawt_xawt/awt/awt_InputMethod.c @@ -541,7 +541,7 @@ setXICWindowFocus(XIC ic, Window w) #define INITIAL_LOOKUP_BUF_SIZE 512 Boolean -awt_x11inputmethod_lookupString(XKeyPressedEvent *event, KeySym *keysymp) +awt_x11inputmethod_lookupString(XKeyPressedEvent *event, KeySym *keysymp, const Boolean keyPressContainsThePreeditTextOfLastXResetIC) { JNIEnv *env = GetJNIEnv(); X11InputMethodData *pX11IMData = NULL; @@ -627,17 +627,22 @@ awt_x11inputmethod_lookupString(XKeyPressedEvent *event, KeySym *keysymp) /*FALLTHRU*/ case XLookupChars: /* - printf("lookupString: status=XLookupChars, type=%d, state=%x, keycode=%x, keysym=%x\n", - event->type, event->state, event->keycode, keysym); + printf("lookupString: status=XLookupChars, type=%d, state=%x, keycode=%x, keysym=%x, keyPressContainsThePreeditTextOfLastXResetIC=%d\n", + event->type, event->state, event->keycode, keysym, (int)keyPressContainsThePreeditTextOfLastXResetIC); */ - javastr = JNU_NewStringPlatform(env, (const char *)pX11IMData->lookup_buf); - if (javastr != NULL) { - JNU_CallMethodByName(env, NULL, - currentX11InputMethodInstance, - "dispatchCommittedText", - "(Ljava/lang/String;J)V", - javastr, - event->time); + + // JBR-3112 + // See sun.awt.X11.XToolkit#doesCurrentlyDispatchedKeyPressContainThePreeditTextOfLastXResetIC + if (!keyPressContainsThePreeditTextOfLastXResetIC) { + javastr = JNU_NewStringPlatform(env, (const char *)pX11IMData->lookup_buf); + if (javastr != NULL) { + JNU_CallMethodByName(env, NULL, + currentX11InputMethodInstance, + "dispatchCommittedText", + "(Ljava/lang/String;J)V", + javastr, + event->time); + } } break; diff --git a/src/java.desktop/unix/native/libawt_xawt/xawt/XWindow.c b/src/java.desktop/unix/native/libawt_xawt/xawt/XWindow.c index 2b824d15f49e..d97671392a7d 100644 --- a/src/java.desktop/unix/native/libawt_xawt/xawt/XWindow.c +++ b/src/java.desktop/unix/native/libawt_xawt/xawt/XWindow.c @@ -86,7 +86,7 @@ jfieldID targetID; jfieldID graphicsConfigID; extern jobject currentX11InputMethodInstance; -extern Boolean awt_x11inputmethod_lookupString(XKeyPressedEvent *, KeySym *); +extern Boolean awt_x11inputmethod_lookupString(XKeyPressedEvent *, KeySym *, Boolean keyPressContainsThePreeditTextOfLastXResetIC); Boolean awt_UseType4Patch = False; Boolean awt_ServerDetected = False; Boolean awt_XKBDetected = False; @@ -1103,7 +1103,7 @@ JNIEXPORT jboolean JNICALL Java_sun_awt_X11_XWindow_haveCurrentX11InputMethodIns } JNIEXPORT jboolean JNICALL Java_sun_awt_X11_XWindow_x11inputMethodLookupString -(JNIEnv *env, jobject object, jlong event, jlongArray keysymArray) { +(JNIEnv *env, jobject object, jlong event, jlongArray keysymArray, jboolean keyPressContainsThePreeditTextOfLastXResetIC) { KeySym keysym = NoSymbol; Boolean boo; /* keysymArray (and testbuf[]) have dimension 2 because we put there two @@ -1117,7 +1117,11 @@ JNIEXPORT jboolean JNICALL Java_sun_awt_X11_XWindow_x11inputMethodLookupString testbuf[1]=0; - boo = awt_x11inputmethod_lookupString((XKeyPressedEvent*)jlong_to_ptr(event), &keysym); + boo = awt_x11inputmethod_lookupString( + (XKeyPressedEvent*)jlong_to_ptr(event), + &keysym, + (keyPressContainsThePreeditTextOfLastXResetIC == JNI_TRUE) ? True : False + ); testbuf[0] = keysym; (*env)->SetLongArrayRegion(env, keysymArray, 0, 2, (jlong *)(testbuf));