From dfce3225729abe9fa3cbb9841b71aa3afa256a0a Mon Sep 17 00:00:00 2001 From: Sergei Tachenov Date: Thu, 15 Jan 2026 13:37:35 +0200 Subject: [PATCH] [UI, Wayland] IJPL-222880 Fix ActionMenu UsabilityHelper 1. Because screen coordinates on Wayland are not reliable, use coordinates relative to the parent menu window instead. 2. Because the absolute mouse position is not reliable either, wait for the first mouse event to capture the position relative to whatever component the mouse in. GitOrigin-RevId: 131542f2815b13fc935e3549a27a9ce0179c18e2 --- .../openapi/actionSystem/impl/ActionMenu.kt | 83 +++++++++++++------ 1 file changed, 56 insertions(+), 27 deletions(-) diff --git a/platform/platform-impl/src/com/intellij/openapi/actionSystem/impl/ActionMenu.kt b/platform/platform-impl/src/com/intellij/openapi/actionSystem/impl/ActionMenu.kt index 5d95e65dab75..a8b253179187 100644 --- a/platform/platform-impl/src/com/intellij/openapi/actionSystem/impl/ActionMenu.kt +++ b/platform/platform-impl/src/com/intellij/openapi/actionSystem/impl/ActionMenu.kt @@ -444,10 +444,20 @@ class ActionMenu constructor( @OptIn(FlowPreview::class) private class UsabilityHelper(component: Component) : IdeEventQueue.NonLockedEventDispatcher, AWTEventListener, Disposable { - private var component: Component? - private var startMousePoint: Point? + /** + * The parent popup menu window. + * + * To make this whole thing Wayland-agnostic, all computations are done in this window's coordinate system. + * This makes it possible to avoid relying on screen coordinates. + * + * This window is never null (until dispose) because the helper is only created when the menu is showing. + */ + private var window: Component? = ComponentUtil.getWindow(component) + private var component: Component? = component + private var startMousePoint: Point? = null private var xClosestToTargetSoFar = 0 private var closestHorizontalDistanceSoFar = 0 + private var targetBounds: Rectangle? = null private var upperTargetPoint: Point? = null private var lowerTargetPoint: Point? = null private var eventToRedispatch: MouseEvent? = null @@ -468,45 +478,52 @@ private class UsabilityHelper(component: Component) : IdeEventQueue.NonLockedEve } } }.cancelOnDispose(this) - this.component = component - val info = MouseInfo.getPointerInfo() - startMousePoint = info?.location - if (startMousePoint != null) { - xClosestToTargetSoFar = startMousePoint!!.x - Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.COMPONENT_EVENT_MASK) - IdeEventQueue.getInstance().addDispatcher(this, this) - } + Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.COMPONENT_EVENT_MASK) + IdeEventQueue.getInstance().addDispatcher(this, this) } override fun eventDispatched(event: AWTEvent) { - if (event !is ComponentEvent) { + if (event !is ComponentEvent || window == null || component == null || done) { return } val component = event.component val popup = ComponentUtil.getParentOfType(JPopupMenu::class.java, component) - if (popup != null && popup.invoker === this.component && popup.isShowing()) { + val popupParent = popup?.parent + if (popup != null && popupParent != null && popup.invoker === this.component && popup.isShowing()) { val bounds = popup.bounds if (bounds.isEmpty) { return } - bounds.location = popup.locationOnScreen - if (startMousePoint!!.x < bounds.x) { - upperTargetPoint = Point(bounds.x, bounds.y) - lowerTargetPoint = Point(bounds.x, bounds.y + bounds.height) - closestHorizontalDistanceSoFar = abs(upperTargetPoint!!.x - xClosestToTargetSoFar) - } - if (startMousePoint!!.x > bounds.x + bounds.width) { - upperTargetPoint = Point(bounds.x + bounds.width, bounds.y) - lowerTargetPoint = Point(bounds.x + bounds.width, bounds.y + bounds.height) - closestHorizontalDistanceSoFar = abs(upperTargetPoint!!.x - xClosestToTargetSoFar) - } + bounds.location = SwingUtilities.convertPoint(popupParent, popup.location, window) + this.targetBounds = bounds + computeTargetPointsIfPossible() + } + } + + private fun computeTargetPointsIfPossible() { + if (upperTargetPoint != null && lowerTargetPoint != null) return // already computed + val bounds = targetBounds ?: return // the child popup position is not known yet + val startMousePoint = this.startMousePoint ?: return // the mouse position is not known yet + if (startMousePoint.x < bounds.x) { + upperTargetPoint = Point(bounds.x, bounds.y) + lowerTargetPoint = Point(bounds.x, bounds.y + bounds.height) + closestHorizontalDistanceSoFar = abs(upperTargetPoint!!.x - xClosestToTargetSoFar) + } + if (startMousePoint.x > bounds.x + bounds.width) { + upperTargetPoint = Point(bounds.x + bounds.width, bounds.y) + lowerTargetPoint = Point(bounds.x + bounds.width, bounds.y + bounds.height) + closestHorizontalDistanceSoFar = abs(upperTargetPoint!!.x - xClosestToTargetSoFar) } } override fun dispatch(e: AWTEvent): Boolean { - if (e !is MouseEvent || upperTargetPoint == null || lowerTargetPoint == null || done) { + val component = this.component + val window = this.window + val parent = component?.parent + + if (e !is MouseEvent || window == null || component == null || parent == null || done) { return false } @@ -514,9 +531,20 @@ private class UsabilityHelper(component: Component) : IdeEventQueue.NonLockedEve return false } - val point = e.locationOnScreen - val bounds = component!!.bounds - bounds.location = component!!.locationOnScreen + if (startMousePoint == null) { + startMousePoint = SwingUtilities.convertPoint(e.component, e.point, window) + xClosestToTargetSoFar = startMousePoint!!.x + } + + computeTargetPointsIfPossible() + + if (upperTargetPoint == null || lowerTargetPoint == null) { // the child popup is not shown yet + return false + } + + val point = SwingUtilities.convertPoint(e.component, e.point, window) + val bounds = component.bounds + bounds.location = SwingUtilities.convertPoint(component.parent, component.location, window) val insideTarget = bounds.contains(point) val horizontalDistance = abs(upperTargetPoint!!.x - point.x) if (!insideTarget && horizontalDistance < closestHorizontalDistanceSoFar) { @@ -536,6 +564,7 @@ private class UsabilityHelper(component: Component) : IdeEventQueue.NonLockedEve override fun dispose() { done = true + window = null component = null eventToRedispatch = null lowerTargetPoint = null