diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServicesReflectionTest.kt b/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServicesReflectionTest.kt new file mode 100644 index 000000000000..0b8c904ea6b4 --- /dev/null +++ b/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServicesReflectionTest.kt @@ -0,0 +1,71 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.jewel.intui.standalone.window.macos + +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import org.junit.jupiter.api.Test + +/** + * Regression test for JEWEL-1387: the reflective lookups used to resolve the native window must search the whole class + * hierarchy, not just the runtime class. + * + * On JBR 25 the AWT window peer is `sun.lwawt.macosx.LWCWindowPeer`, a subclass of `sun.lwawt.LWWindowPeer` that + * inherits (but does not declare) `getPlatformWindow()`. A `getDeclaredMethod` call on the runtime class alone throws + * [NoSuchMethodException] and silently breaks native window chrome updates on macOS. + */ +internal class MacPlatformServicesReflectionTest { + @Suppress("UnusedPrivateMember", "FunctionOnlyReturningConstant") + private open class BasePeer { + @JvmField internal val ptr: Long = 42L + + fun getPlatformWindow(): Any = this + } + + // Mimics JBR 25's LWCWindowPeer: inherits getPlatformWindow() and ptr without declaring them + private class SubclassPeer : BasePeer() + + @Test + fun `findMethodInHierarchy finds method declared on the class itself`() { + val method = MacPlatformServicesDefaultImpl.findMethodInHierarchy(BasePeer::class.java, "getPlatformWindow") + + assertNotNull(method, "Should find a method declared directly on the class") + } + + @Test + fun `findMethodInHierarchy finds method inherited from a superclass`() { + val method = MacPlatformServicesDefaultImpl.findMethodInHierarchy(SubclassPeer::class.java, "getPlatformWindow") + + assertNotNull(method, "Should find a method declared on a superclass (JBR 25 LWCWindowPeer scenario)") + assertEquals(BasePeer::class.java, method.declaringClass, "Method should be resolved from the superclass") + } + + @Test + fun `findMethodInHierarchy returns null for a missing method`() { + val method = MacPlatformServicesDefaultImpl.findMethodInHierarchy(SubclassPeer::class.java, "doesNotExist") + + assertNull(method, "Should return null instead of throwing for missing methods") + } + + @Test + fun `findFieldInHierarchy finds field declared on the class itself`() { + val field = MacPlatformServicesDefaultImpl.findFieldInHierarchy(BasePeer::class.java, "ptr") + + assertNotNull(field, "Should find a field declared directly on the class") + } + + @Test + fun `findFieldInHierarchy finds field inherited from a superclass`() { + val field = MacPlatformServicesDefaultImpl.findFieldInHierarchy(SubclassPeer::class.java, "ptr") + + assertNotNull(field, "Should find a field declared on a superclass") + assertEquals(BasePeer::class.java, field.declaringClass, "Field should be resolved from the superclass") + } + + @Test + fun `findFieldInHierarchy returns null for a missing field`() { + val field = MacPlatformServicesDefaultImpl.findFieldInHierarchy(SubclassPeer::class.java, "doesNotExist") + + assertNull(field, "Should return null instead of throwing for missing fields") + } +} diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServices.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServices.kt index 75e6abcb1a10..86fca4ad7b59 100644 --- a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServices.kt +++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServices.kt @@ -7,9 +7,12 @@ import com.sun.jna.Callback import com.sun.jna.Pointer import java.awt.Component import java.awt.Window +import java.lang.reflect.Field import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method import javax.swing.SwingUtilities import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.VisibleForTesting import org.jetbrains.jewel.foundation.InternalJewelApi import org.jetbrains.jewel.foundation.util.JewelLogger import org.jetbrains.jewel.intui.standalone.styling.default @@ -60,14 +63,16 @@ public object MacPlatformServicesDefaultImpl : MacPlatformServices { try { val cPlatformWindow = getPlatformWindow(w) if (cPlatformWindow != null) { - val ptr = cPlatformWindow.javaClass.superclass.getDeclaredField("ptr") + val ptr = findFieldInHierarchy(cPlatformWindow.javaClass, "ptr") + if (ptr == null) { + logger.warn("Fail to get cPlatformWindow from awt window: no 'ptr' field found.") + return ID.NIL + } ptr.setAccessible(true) return ID(ptr.getLong(cPlatformWindow)) } } catch (e: IllegalAccessException) { logger.warn("Fail to get cPlatformWindow from awt window.", e) - } catch (e: NoSuchFieldException) { - logger.warn("Fail to get cPlatformWindow from awt window.", e) } return ID.NIL } @@ -79,15 +84,19 @@ public object MacPlatformServicesDefaultImpl : MacPlatformServices { val getPeer = componentAccessor.javaClass.getMethod("getPeer", Component::class.java).accessible() val peer = getPeer.invoke(componentAccessor, w) if (peer != null) { - val cWindowPeerClass: Class<*> = peer.javaClass - val getPlatformWindowMethod = cWindowPeerClass.getDeclaredMethod("getPlatformWindow") + // The method may be declared on a superclass of the runtime peer class (e.g., on JBR 25 the peer + // is sun.lwawt.macosx.LWCWindowPeer, which inherits getPlatformWindow() from sun.lwawt.LWWindowPeer), + // so we must search the whole class hierarchy rather than just the runtime class. + val getPlatformWindowMethod = findMethodInHierarchy(peer.javaClass, "getPlatformWindow") + if (getPlatformWindowMethod == null) { + logger.warn("Fail to get cPlatformWindow from awt window: no getPlatformWindow() method found.") + return null + } val cPlatformWindow = getPlatformWindowMethod.invoke(peer) if (cPlatformWindow != null) { return cPlatformWindow } } - } catch (e: NoSuchMethodException) { - logger.warn("Fail to get cPlatformWindow from awt window.", e) } catch (e: IllegalAccessException) { logger.warn("Fail to get cPlatformWindow from awt window.", e) } catch (e: InvocationTargetException) { @@ -98,6 +107,44 @@ public object MacPlatformServicesDefaultImpl : MacPlatformServices { return null } + /** + * Finds a method with the given [name] and no parameters declared on [clazz] or any of its superclasses, or `null` + * if no such method exists. + */ + @VisibleForTesting + @ApiStatus.Internal + @InternalJewelApi + public fun findMethodInHierarchy(clazz: Class<*>, name: String): Method? { + var current: Class<*>? = clazz + while (current != null) { + try { + return current.getDeclaredMethod(name) + } catch (_: NoSuchMethodException) { + current = current.superclass + } + } + return null + } + + /** + * Finds a field with the given [name] declared on [clazz] or any of its superclasses, or `null` if no such field + * exists. + */ + @VisibleForTesting + @ApiStatus.Internal + @InternalJewelApi + public fun findFieldInHierarchy(clazz: Class<*>, name: String): Field? { + var current: Class<*>? = clazz + while (current != null) { + try { + return current.getDeclaredField(name) + } catch (_: NoSuchFieldException) { + current = current.superclass + } + } + return null + } + public override fun updateColors(w: Window) { SwingUtilities.invokeLater { val window = getWindowFromJavaWindow(w)