Bug #11211
Improve source detection when dispatching mouse wheel events
100%
Related issues
History
#1 Updated by Razvan-Nicolae Chichirau 5 months ago
Currently FWD is dispatching the mouse wheel events on the widget that is directly under the mouse cursor according to the Z-Order. However, 4GL extends this concept and re-routes the mouse wheel event on the focused widget, even if the mouse pointer is not on that widget.
Initial investigation was done in #10152-19.
#2 Updated by Razvan-Nicolae Chichirau 5 months ago
- Related to Bug #10152: COMBO-BOX: mouse wheel does not scroll the list added
#3 Updated by Razvan-Nicolae Chichirau 5 months ago
Also, when the mouse wheel with SHIFT/CTRL are used simultaneously, the widget frame seems to be scrolled rather than the widget.
#4 Updated by Razvan-Nicolae Chichirau 5 months ago
- respond to mouse wheel events: combo-box, browse, browse column, selection-list, slider, editor
- do not respond to mouse wheel events: fill-in, button, radio-set, toggle-box, images, rectangles, text, menu, sub-menus, sub-menu options, OCX
If there are widgets that I've missed please mention them. This list is important as the widgets that do not respond to mouse wheel events will redirect the event to the scroll pane of its parent frame. Widgets that can respond to the mouse-wheel can have the same behavior only when pressing CTRL or SHIFT.
#5 Updated by Razvan-Nicolae Chichirau 5 months ago
UiUtils method I made for finding the mouse source:public static Widget<?> findMouseWheelSource(MouseWheelEvent event)
{
Widget<?> target = getCurrentFocus(true);
if (target == null || !target.isEnabled())
{
// TODO: In 4GL the legacy FOCUS can be unknown or even point to a disabled widget. In this case,
// it seems that the last widget which received the ENTRY event is scrolled, including frames.
// This may not be the correct behavior every time, so further testing needs to be done.
ThinClient tc = ThinClient.getInstance();
target = tc.getLastEntryWidget();
}
if (target == null)
{
return null;
}
boolean hasCTRL = event.isControlDown();
if (target instanceof BrowseColumnGuiImpl)
{
BrowseGuiImpl browse = ((BrowseColumnGuiImpl) target).getBrowse();
Widget<?> editor = browse.getActiveEditor();
if (editor == null)
{
return null;
}
// The browse column can either be a COMBO-BOX, FILL-IN or TOGGLE-BOX. Only COMBO-BOX can handle
// the mouse wheel so re-route the event directly to this widget only if is enabled and CTRL is
// not pressed. For the other, the browse itself should be scrolled.
target = !hasCTRL && editor.canHandleMouseWheel() && editor.isEnabled() ? editor : browse;
}
if (hasCTRL && target instanceof BrowseGuiImpl)
{
// CTRL + mouse-wheel does not scroll the browse itself, the browse column or the parent frame
return null;
}
// The mouse wheel event should be routed to its frame in the following cases:
// 1. The target can not handle the mouse wheel.
boolean routeToFrame = !target.canHandleMouseWheel();
// 2. Or the target can handle the mouse wheel, but either SHIFT was pressed or CTRL was pressed
// and the target is not a frame
routeToFrame |= !(target instanceof FrameGuiImpl) && (event.isShiftDown() || hasCTRL);
if (routeToFrame)
{
Frame<?> frame = locateFrame(target);
if (frame == null)
{
return null;
}
target = frame.getScrollPane();
}
return target;
}
target.canHandleMouseWheel()is overriden totruein the widgets from #11211-4 that respond to mouse events.tc.getLastEntryWidget()returns the last widget that processed an ENTRY event.nullreturn value should indicate that no suitable source was found for the mouse wheel event, thus is should be aborted. This can actually happen in 4GL, such as when the window is attempted to be scrolled and nothing happens.
I'm currently working on integrating this with Swing + Web, but it's not as straight forward as it looks. For example Web searches for the source on JavaScript and does some checks. We can not do this anymore, as we compute the actual source on the client, so we need to "copy" some logic.
#6 Updated by Razvan-Nicolae Chichirau 5 months ago
- Status changed from WIP to Review
- % Done changed from 0 to 100
- reviewer Hynek Cihlar, Vladimir Tsichevski added
Hynek/Vladimir: I'm going to need both of you to carefully review 11211a/rev. 16438 as these are some "risky" changes (i.e., the whole mouse wheel source detection is refactored).
After you review, I have some questions regarding the implementation:- Web: In
p2j.screen.js -> raiseMouseEvent()there is this code:if (me.hasOsEvent(evt, wThis, mouseOp, wid)) { // high-level event, do not process here return; }
Do we need to also check this on the client? If so, is it enough to verify if the actions of the widget have at least 1 OS event, such as COPY, CUT, PASTE, etc... ? - Swing: In
MouseHandler.mouseWheelMoved()the window of the mouse target can actually be different from the window returned byews(i.e., when the focus is on the window and we are scrolling with the mouse pointer being on another window). Should we post the event on the window of the target or the window of the mouse pointer? I guess changing theewsdoes not make sense as scrolling on another window does not have any effect on it in 4GL.
- Non-legacy widgets can be scrolled, such as the window message area
- A mouse wheel event does have effect on the widgets from a non-sensitive (blocked) window
#7 Updated by Radu Apetrii 5 months ago
- reviewer Alexandru Lungu, Șerban Bursuc added
Aaaand, another one. If it's alright, I would like both Alex and Serban to look at the changes. Thank you!
#8 Updated by Hynek Cihlar 3 months ago
- Status changed from Review to WIP
- % Done changed from 100 to 90
Code review 11211a revisions 16437..16438
- [CRITICAL] functional
EntryFieldGuiImpl.mouseActions: The override returns only{ MouseEvent.MOUSE_WHEEL }, completely replacing the parentFillInGuiImpl.mouseActions(). The fix should appendMOUSE_WHEELto the parent's array rather than replacing it. - [MINOR] style
UiUtils(imports): Import changed from wildcardimport com.goldencode.p2j.ui.client.event.listener.*to explicitimport ...ActionListener. Per GoldenCode convention, wildcard imports should be used unless conflicts require explicit class imports. - [MINOR] style
UiUtils.findMouseWheelSource: The Javadoc for thehasShiftparameter says{@code false}but should say{@code true}, matching thehasCTRLparameter pattern. Copy-paste error. - [MINOR] style
MouseEvt(imports): Added importcom.goldencode.p2j.ui.Keyboarddoes not appear to be used in the changed code withinMouseEvt.java. - [MINOR] style
p2j.screen.js.raiseMouseEvent: Variablewidis declared withvartwice - once in the outer scope and again inside theifblock. The innervarkeyword is redundant and should be removed. - [MINOR] functional
ScrollPaneGuiImpl.mouseActions: In the first branch (when child isScrollableSelectionListGuiImpl),MouseEvent.MOUSE_WHEELwas already present in the array before this change. The diff appends a second duplicate entry. The other two branches correctly addMOUSE_WHEELfor the first time. Harmless at runtime because the array is consumed viaRoaringBitmapwhich deduplicates, but the duplicate is clearly unintentional and should be removed.
In the following chunk window() is likely not correct. It may return a different top-level window from the one source is located in. Use topLevelWindow instead.
+ int widgetId = ((Widget<?>) source).getId().asInt(); + int windowId = ((Widget<?>) source).window().getId().asInt();
#9 Updated by Razvan-Nicolae Chichirau 3 months ago
- % Done changed from 90 to 100
- Status changed from WIP to Review
Rebased 11211a to trunk rev. 16514.
Please check rev. 16516.
#10 Updated by Hynek Cihlar 3 months ago
- Status changed from Review to Internal Test
- % Done changed from 100 to 90
Code review 11211a revisions 16514..16516
- [CRITICAL] functional
TreeBodyGuiImpl.canHandleMouseWheel:TreeBodyGuiImplimplementsmouseWheelMoved()that scrolls the tree (viatree.hModel/tree.vModel.increment) and registersMOUSE_WHEELin itsmouseActions(), yet does not overridecanHandleMouseWheel()to returntrue. After the newUiUtils.findMouseWheelSource()routing, when a tree body is focused the returned target inherits the defaultfalsefromAbstractWidget, causing wheel events to be rerouted to the parent frame's scroll pane rather than reaching the tree. This breaks TREEVIEW/TREELIST wheel scrolling. Please check this case.
- [MAJOR] functional
FrameGuiImpl.canHandleMouseWheel:FrameGuiImploverridescanHandleMouseWheel()to returntruebut neitherFrameGuiImplnorFrameimplementsmouseWheelMoved(). InfindMouseWheelSource()the branchrouteToFrame |= !(target instanceof FrameGuiImpl) && (hasShift || hasCTRL)is specifically designed so that when the target is already a frame the scroll pane is NOT substituted. Consequently a wheel event whose resolved target is the frame itself is delivered to the frame and dropped byAbstractWidget.mouseWheelMoved()(no-op). Either route throughframeScrollor implementmouseWheelMoved()inFrameGuiImpldelegating toframeScroll. Is this case an issue? For example when a frame itself s focused?
- [MAJOR] functional
EntryFieldGuiImpl.canHandleMouseWheel: Returningtrueunconditionally is inconsistent with actual wheel handling:EntryFieldGuiImpl.mouseWheelMoved()only forwards tocomboBoxRefwhen non-null and otherwise does nothing. For a standalone fill-in (not a combo-box editor),findMouseWheelSource()returns the fill-in as target (since it "handles" the wheel), so the mouse wheel is consumed without scrolling the parent frame. Consider returningcomboBoxRef != nullhere, or letting routing fall through to the parent frame for plain fill-ins. Is this an issue? Does scrolling work correctly whencomboBoxRef == null?
- [MAJOR] functional
ScrollBarGuiImpl.canHandleMouseWheel:ScrollBarGuiImplimplements a substantivemouseWheelMoved()(computing scroll step and callingonButtonClick) andAbstractScrollBarlistsMOUSE_WHEELin itsMOUSE_ACTIONS, but neither class overridescanHandleMouseWheel(). Is this expected? At least at the source code level this looks suspicious.
- [MAJOR] functional
MouseHandler.mouseWheelMoved:((Widget>) source).topLevelWindow().getId().asInt()is unguarded.AbstractWidget.topLevelWindow()is documented to return null when the widget is detached during creation/destruction lifecycle transitions. A race wherefindMouseWheelSourcereturns a widget that is just detached will NPE before the event is posted.
- [MAJOR] functional
GuiWebDriver.raiseMouseEvent: Same NPE risk asMouseHandler.mouseWheelMoved:source.topLevelWindow().getId().asInt()can throw NPE whentopLevelWindow()returns null for a widget that was just detached from its window hierarchy.
- [MAJOR] functional
SwingMouseHandler.mouseWheelMoved: Behavioral change from the previous implementation. Previously, when no source was found (processActionreturning false with null source), control fell through toSwingMouseHandler.super.mouseWheelMoved(e), which still posted the event viatc.postMouseEvent(e, ews.getWindowId()). The new code returns silently and posts nothing, dropping wheel events that cannot be attributed to a specific widget (e.g., wheel scrolls over a region with no focused non-legacy widget). Any window/frame-level wheel handling that relied on receiving events without a resolved source will break. Please check this case.
- [MAJOR] functional
Screen.canProcessOsEvent(p2j.screen.js): AllowingWheelEventto bypasswin.processOsEventsis a real semantics change.processOsEvents = falseis set byWindowManager.modalizeWindow()to silence non-active windows while a modal dialog is displayed. The existing!p2j.isDialogDisplayed()guard only detects internal JS dialogs, not 4GL modal windows. Consequently, wheel events will now be processed on silenced windows behind a modal 4GL window, which is unwanted. Please check this case. I think it is enough to test mouse wheels work the same indialog-boxas non-modal top-level windows, also that mouse wheels are not sent to the non-modal windows while modal is active.
- [MAJOR] style
p2j.screen.js(file header): Erroneous duplicate text in the history table between entries 098 and 099 (** Added a new method updateWindowState to update the window state. Refs #9973a.); this text belongs to the earlier 096 IMS entry and must not be repeated.
- [MINOR] functional
UiUtils.findMouseWheelSource: The browse-edit-mode branch returns null ifbrowse.getActiveEditor()returns null. If editor was dismissed between focus read and this call. I'm not sure this case is possible, but a fallback to scroll some of the parent containers wouldn't hurt.
- [MINOR] functional
UiUtils.findMouseWheelSource: Does not check whether the focused widget is disabled to trigger the fallback path (as per Redmine summary). The onlyisEnabled()check is inside the browse-editor branch; a disabled non-browse focused widget is still used normally. Please check.
- [MINOR] functional
UiUtils.findMouseWheelSource: The initial loopwhile (target != null && target.getId() == null) target = target.parent()escalates focus to a non-focus ancestor when the focused widget is a purely client-side (id-less) sub-widget (dropdown item, calendar cell, tree editor subcomponent). This can mis-route the wheel event to an ancestor rather than letting the real sub-widget's own wheel handling run. This can be confirmed by wheel scrolling a virtual widget.
- [MINOR] functional
UiUtils.findMouseWheelSource:locateFrame(target)returns the innermost ancestor frame. For nested frames where the inner frame's scroll pane has no visible scrollbar but the outer one does,ScrollPaneGuiImpl.mouseWheelMoved()returns early and the event never bubbles. If scroll-bubbling across nested frames is a supported scenario, try the parent frame when the inner one cannot scroll. Please check this case.
- [MINOR] functional
Frame.getScrollPane: Declared return type isAbstractContainer<O>, but the fieldframeScrollis typedScrollPane<O>. Returning the broader type loses information and forces callers to useWidget-level API only. PreferScrollPane<O>.
- [MINOR] functional
GuiWebDriver.raiseMouseEvent: After the new wheel block remapswindowIdto the focused widget's top-level window, subsequent code usesdirect.getWindowEmulatorSafe(windowId).getWindowLocation()for screen-coordinate computation whilemouseX/mouseYstill come from the original cursor window. The resultingMouseWheelEvent'sXOnScreen/YOnScreenare inconsistent (computed relative to a different window than the one producing the raw coordinates). Wheel handlers relying on screen coordinates see incorrect values.
- [MINOR] functional
SwingMouseHandler.mouseWheelMoved:e.setSource(mouseSource)is placed inside the!processAction(...)branch. However,processAction(mouseSource, e)invokesaction.mouseWheelMoved((MouseWheelEvent) e)whilee.getSource()still points to the placeholder. If any registeredMouseWidgetActioninspectse.getSource(), it will see the placeholder rather than the resolved widget. Setting the source beforeprocessActionyields more consistent semantics.
- [MINOR] functional
MouseEvt.BOGUS_AWT_COMPONENT: Duplicates the privateBOGUS_AWT_COMPONENTalready defined inGuiWebDriver(line 420). Consolidate to a single public definition to eliminate duplication.
- [MINOR] performance
MouseEvt(static init):BOGUS_AWT_COMPONENT = new Component() { }forces eager AWT/Toolkit initialization (native peer infrastructure) in environments that otherwise would not need it. Using a lighter stand-in or reusing the existing one inGuiWebDriverwould avoid this.
- [MINOR] style
UiUtils.findMouseWheelSource: Inconsistent parameter naming -hasCTRLuses all-caps acronym whilehasShiftdoes not. Choose one style consistently.
Wheel events are now sent to Java driver uncoditionally. Do we still need to declare MOUSE_WHEEL in mouseActions?
#11 Updated by Razvan-Nicolae Chichirau 3 months ago
- % Done changed from 90 to 100
- Status changed from Internal Test to Review
- reviewer deleted (
Alexandru Lungu, Vladimir Tsichevski, Șerban Bursuc)
Hynek Cihlar wrote:
- [MAJOR] functional
SwingMouseHandler.mouseWheelMoved: Behavioral change from the previous implementation. Previously, when no source was found (processActionreturning false with null source), control fell through toSwingMouseHandler.super.mouseWheelMoved(e), which still posted the event viatc.postMouseEvent(e, ews.getWindowId()). The new code returns silently and posts nothing, dropping wheel events that cannot be attributed to a specific widget (e.g., wheel scrolls over a region with no focused non-legacy widget). Any window/frame-level wheel handling that relied on receiving events without a resolved source will break. Please check this case.
I could not identify any places where the logic relied on receiving events without a resolved source.
- [MAJOR] functional
Screen.canProcessOsEvent(p2j.screen.js): AllowingWheelEventto bypasswin.processOsEventsis a real semantics change.processOsEvents = falseis set byWindowManager.modalizeWindow()to silence non-active windows while a modal dialog is displayed. The existing!p2j.isDialogDisplayed()guard only detects internal JS dialogs, not 4GL modal windows. Consequently, wheel events will now be processed on silenced windows behind a modal 4GL window, which is unwanted. Please check this case. I think it is enough to test mouse wheels work the same indialog-boxas non-modal top-level windows, also that mouse wheels are not sent to the non-modal windows while modal is active.
A modal window is guaranteed to hold the focus until it is closed, so the final target can never really be a widget from a silenced window.
The modification was made for the situation where you have a non-modal and a modal window visible and you scroll with the mouse pointer being on the silenced (non-modal) window. We need to propagate this event to the client to be redirected to a widget from the modal window (since it's guaranteed it has focus).
The widget to which the mouse wheel event is posted need to have a non-null ID. For example in
- [MINOR] functional
UiUtils.findMouseWheelSource: The initial loopwhile (target != null && target.getId() == null) target = target.parent()escalates focus to a non-focus ancestor when the focused widget is a purely client-side (id-less) sub-widget (dropdown item, calendar cell, tree editor subcomponent). This can mis-route the wheel event to an ancestor rather than letting the real sub-widget's own wheel handling run. This can be confirmed by wheel scrolling a virtual widget.
GuiWebDriver.raiseMouseEvent() there are the following method invocations:
public static void handleCursorRestore(int widgetId)fromMouseHandlerpublic void raiseMouseEvent(MouseEvent event, int windowId, int widgetId)fromWebMouseHandler
- [MINOR] functional
UiUtils.findMouseWheelSource:locateFrame(target)returns the innermost ancestor frame. For nested frames where the inner frame's scroll pane has no visible scrollbar but the outer one does,ScrollPaneGuiImpl.mouseWheelMoved()returns early and the event never bubbles. If scroll-bubbling across nested frames is a supported scenario, try the parent frame when the inner one cannot scroll. Please check this case.
In the new revision, locateFrame can not be called for frames. In addition, the maximum level of mouse wheel propagation should be only to widget's frame and not further up the stack.
Yes, because:Wheel events are now sent to Java driver uncoditionally. Do we still need to declare
MOUSE_WHEELinmouseActions?
- Some JS logic may depend on a widget having the
MOUSE_WHEELaction - This action is still checked in
GuiWebDriver.raiseMouseEvent():RoaringBitmap actions = getWebActions(source); // Check if the widget can process MOUSE_WHEEL on Web. if (!actions.contains(mop.code)) { return; }
Refactored the whole UiUtils.findMouseWheelSource() to take a more clean approach. Renamed canHandleMouseWheel() to getMouseWheelTarget(). The widgets that implement this method will need to return the delegated widget to which the mouse wheel event should be forwarded.
Please check 11211a/rev. 16517.
#12 Updated by Hynek Cihlar about 2 months ago
- % Done changed from 100 to 90
- Status changed from Review to WIP
Code review 11211a revisions 16514..16517
Two minor points, otherwise the changes look good. The functional point is worth checking. ChUI regression testing will be needed - while the changes are related only to GUI there are also common code changes.
- [MINOR] functional
GuiWebDriver.raiseMouseEvent: For wheel events,widgetIdis overwritten with the wheel-source widget id (source.getId().asInt()) beforeMouseHandler.handleCursorRestore(widgetId)runs.handleCursorRestoreresolves the cursor from the hovered widget'sgetEffectiveMousePointer(), but for wheel events the source resolved viaUiUtils.findMouseWheelSourceis focus-derived and may differ from the hovered widget. Trigger: a 4GLSET-POINTERhas set a global User32 cursor, focus is on widget A while the mouse hovers a different widget B, and the user wheel-scrolls —handleCursorRestorethen uses A's effective pointer instead of B's. The mismatch self-corrects on the nextMOUSE_MOVED. Consider preserving the original JS-suppliedwidgetIdfor thehandleCursorRestorecall, or skipping the cursor-restore step for the wheel branch.
- [MINOR] style
ScrollBarGuiImpl: History entry 039 contains a typo: "Overidden" should be "Overridden".
#13 Updated by Razvan-Nicolae Chichirau about 2 months ago
- Status changed from WIP to Review
- % Done changed from 90 to 100
Please check 11211a/rev. 16518.
#14 Updated by Hynek Cihlar about 2 months ago
Razvan-Nicolae Chichirau wrote:
Please check 11211a/rev. 16518.
The changes look good. Please go ahead with regression testing.
Btw., use more descriptive commit messages. Addressed review from #1234-12 requires additional step to go and hunt in Redmine what was the change about.
#15 Updated by Hynek Cihlar about 2 months ago
- Status changed from Review to Internal Test
#16 Updated by Razvan-Nicolae Chichirau about 2 months ago
Hynek Cihlar wrote:
Btw., use more descriptive commit messages.
Addressed review from #1234-12requires additional step to go and hunt in Redmine what was the change about.
The idea behind Addressed review from ... was that, since reviews have become much more detailed nowadays, it is easier to look at both my changes and the Redmine note at the same time to understand why I made those changes, rather than trying to summarize everything in a single sentence.
#17 Updated by Hynek Cihlar about 2 months ago
Razvan-Nicolae Chichirau wrote:
The idea behind
Addressed review from ...was that, since reviews have become much more detailed nowadays, it is easier to look at both my changes and the Redmine note at the same time to understand why I made those changes, rather than trying to summarize everything in a single sentence.
It is easier when the changes are summarized in the commit message, btw. see Source_Code_Management. Feel free to provide refs.
#18 Updated by Razvan-Nicolae Chichirau 25 days ago
- ChUI regression tests
- Whole UI suite from testcases
- Hotel GUI + 2 customer app smoke-tests (tested the mouse wheel on all sort of widgets)
- Unit-tests from a customer app
If this is enough, 11211a should be merged.
#19 Updated by Radu Apetrii 25 days ago
Alex: is the trunk still frozen for the delivery?
#20 Updated by Alexandru Lungu 25 days ago
Yes, I will let you know when ready.
#21 Updated by Alexandru Lungu 20 days ago
- Status changed from Internal Test to Merge Pending
Please merge 11211a to trunk now.
#22 Updated by Razvan-Nicolae Chichirau 20 days ago
Branch 11211a was merged into trunk as rev. 16631 and archived.
#23 Updated by Razvan-Nicolae Chichirau 20 days ago
- Status changed from Merge Pending to Test