Bug #11071
COMBO-BOX (SIMPLE): Incorrect Mouse Wheel Scrolling Step
90%
History
#1 Updated by Vladimir Tsichevski 10 months ago
Run the following 4GL example:
DEFINE VARIABLE cb AS CHARACTER VIEW-AS COMBO-BOX SIMPLE LIST-ITEMS "item1", "item2", "item3", "item4", "item5", "item6", "item7", "item8", "item9" SIZE 20 BY 3. DEFINE FRAME fr cb WITH CENTERED SIZE 80 BY 20 SIDE-LABELS. ENABLE ALL WITH FRAME fr. WAIT-FOR GO OF FRAME fr.
This creates and enables a SIMPLE combo-box.
- Open the drop-down list.
- Rotate the mouse wheel while the mouse pointer is over the drop-down list area.
- OE: The list scrolls by one line per wheel notch.
- FWD: The list scrolls by three lines per wheel notch.
Note: The scrolling step is independent of the combo-box vertical size.
Note: Tested only with Swing client.
#2 Updated by Razvan-Nicolae Chichirau about 2 months ago
- Status changed from New to WIP
- Assignee set to Razvan-Nicolae Chichirau
#3 Updated by Razvan-Nicolae Chichirau about 2 months ago
- Status changed from WIP to Review
- % Done changed from 0 to 100
A simple combo-box should scroll 1 item per notch. Tested on multiple OE + Windows versions.
Hynek: Please check 11071a/rev. 16726.
#4 Updated by Hynek Cihlar about 2 months ago
Razvan-Nicolae Chichirau wrote:
A simple combo-box should scroll 1 item per notch. Tested on multiple OE + Windows versions.
Razvan, isn't this dependent on OS settings. In the system you can set how many lines/pages to (wheel) scroll.
#5 Updated by Razvan-Nicolae Chichirau about 2 months ago
Only the drop-downs of a DROP-DOWN / DROP-DOWN-LIST combo-box are affected by this Windows setting. For a SIMPLE combo-box, it will always be 1 per each notch regardless of this setting.
#6 Updated by Hynek Cihlar about 1 month ago
- Status changed from Review to WIP
- % Done changed from 100 to 90
Code review 11071a revisions 16725..16726
The points below look legit.
- [MAJOR] functional
ScrollPaneGuiImpl.scrollVerticallyTo: The newif (newPos vBar.getPosition()) return;guard permanently swallows wheel-up in the SIMPLE combo-box whenevervBar's cached position has drifted below the list's real top row, and there is a live user path that creates exactly that drift. Trigger, with the issue's own reproducer (9 items,SIZE 20 BY 3⇒visibleRows 2): typeitem2into the combo's entry field.EntryFieldGuiImpl.processKeyEvent(SE_VALUE_CHANGED) →ComboBoxGuiImpl.entryFieldChanged(:1481) →ScrollableSelectionListGuiImpl.setTopRow(1)(:548-552), which first callsensureRowVisible(1, true)→DefaultList.setCurrentRow(1, true): therecalculateScrollYPosition()returnsrowTop 0both before and after, sopos newPosandpostScrollEventis not called — no listener is notified andvBar.positionstays 0.setTopRowthen assignsrowTop = fixTopRow(1) = 1directly, again with no notification. Derived state:rowTop 1,vBar.getPosition() 0. Nothing resyncs the bar upward from the widget —adjustScrollLayoutImpl(:1016-1158) snapshotsvBar.getPosition()andadjustScrollBar(:1263-1266) only clamps down (if (bar.getPosition() > max);0 > 7is false), after which :1141-1149 nullsnewYPositionbecause the bar did not move, sogetScrollWidget().scroll(...)is never called even thoughdraw()runs it on every repaint;ScrollableSelectionListGuiImpl.draw(:266-270) only self-corrects whenrowTop + rowsVisible > modelSize(1 + 2 > 9is false). One wheel notch up then computesnewPos = -1 * 1 + 0 = -1→ clamped to 0 → equalsvBar.getPosition()→ return. NoScrollEventis posted,item1can never be reached by wheeling up, and every further wheel-up is swallowed identically. Before this change the unconditionalScrollEvent(w, null, 0)reachedScrollableSelectionListGuiImpl.scroll(null, 0), which setrowTop = 0and repainted — i.e. the old post also acted as the de-facto resync. Fix by comparing against the scrolled widget's actual offset (getTopRow()) rather thanvBar.getPosition(), or by makingScrollableSelectionListGuiImpl.setTopRownotify its scroll listeners after assigningrowTop. Note the mirror-image drift (bar above the widget, e.g. typingitem9) does self-heal via the clamp-down inadjustScrollBar, so only the bar-below-widget direction is affected; no drift source was found for the other users of the shared helper (frames, editors, drop-downs).
- [MINOR] functional
ComboBoxGuiImpl.mouseWheelMoved: On the web driver the new step is one item per wheel message, not per physical notch, so high-resolution input over-scrolls a SIMPLE combo relative to the pre-change behavior.e.getWheelRotation()is±1by construction on the web driver —GuiWebDriversplits the JSrotationintodirection = (wheelParameters[0] > 0) ? 1 : -1andscrollAmount = Math.abs(wheelParameters[0])(:1806-1807) and passesdirectionas the AWTwheelRotation(:1841-1843) — so the gesture magnitude survives only ingetScrollAmount(), whichscrollBySteps()discards. Trigger: web GUI client, focused SIMPLE combo-box, macOS trackpad / Windows precision touchpad / high-resolution wheel, where Chrome sendsdeltaMode 0with many smalldeltaYvalues per gesture; each event now scrolls a full list item, whereas the oldscrollStepsFromPixels(e.getScrollAmount())path (CoordinatesConversion:738-748,(int) Math.round(heightFromNative(pixels))) returned 0 steps for deltas below about half a text row (~7px at a typicalpixelsPerRowof 15-21) and so ignored them.MouseEvt.mergeWith()sums rotations as well as scroll amounts (:632-633), so queue coalescing provides no damping. Kept at MINOR rather than MAJOR because the effect is limited to viewport over-scroll —scrollBySteps()only posts aScrollEvent, the selection is untouched, and the excursion is clamped tovBar.getMax()and trivially reversible — and because it is not a new anomaly:SliderGuiImpl(:470),ScrollableListGuiImpl(:399),TreeBodyGuiImpl(:578),EditorGuiImpl(:1441),SelectionListBodyGuiImpl(:552) andComboBoxGuiImpl's own closed-drop-down branch (:656-687) all already move one unit per web wheel event, so this diff extends an existing codebase-wide web convention to the SIMPLE combo rather than regressing it in isolation. Optional, non-blocking follow-up: reuse the accepted mechanism fromBrowseGuiImpl.mouseWheelMoved(:1523-1537), which accumulatesMath.abs(e.getScrollAmount()) / 100.0againstThinClient.getMouseScrollThreshold()behind agd.isWeb()guard — ideally hoisted into shared code so all rotation-only web widgets benefit at once. A classic discrete notch (deltaY 100, or FirefoxdeltaMode 1) is unaffected and in fact improved by this diff (was ~6 rows / 3 rows, now 1 item), which is exactly what the issue asks for.
- [MINOR] functional
ScrollPaneGuiImpl.mouseWheelMoved: ThehBar null && hBar.isVisible()→hBar != null && hBar.isVisible()correction is right and removes a real crash —&&evaluates its right operand only when the left is true, i.e. exactly whenhBaris null, so the old guard was an unconditional NPE forBars.VERTICAL-only panes (DropDownGuiImpl:222,ComboBoxGuiImpl:2148,MessageAreaGuiImpl:157) and the body was genuinely dead. But it brings that previously-dead statement to life, and the statement mixes units.scrollStepisw.getVisibleDimension().height, which is native pixels for every pane that can actually have a visiblehBar—NativeScrollContainer(:110-120,dimensionToNative(pane.calcInitialViewportSize())), used by the frame scroll container andWindowWorkSpace— whereashBar.height()returns character rows as adouble(ScrollBarGuiImpl.initsetssetHeight(cc.heightFromNative(theme.getScrollBarGaugeWidth())), andScrollBarGuiImpl.height():269 only subtracts the 3D offset). The decisive corroboration isadjustScrollLayoutImpl(:1066-1070), which performs the analogous correction asmaxV += screen().coordinates().heightToNative(barSize(Orientation.HORIZONTAL))— it must convert to pixels;mouseWheelMoveddoes not. Per JLS 15.26.2 the compoundscrollStep -= hBar.height()is(int)(scrollStep - hBar.height()), so a ~0.8-0.9 row value truncates the correction to ~1px: a 200px viewport becomes 199 instead of the intended 184, and the page scroll skips roughly one text row hidden behind the horizontal bar. Trigger: Windows wheel setting "One screen at a time" (SwingWHEEL_BLOCK_SCROLL) or a browser sendingdeltaMode == 2(GuiWebDriver:1808-1811), wheeling with focus on an ordinary widget inside a SCROLLABLE frame whose content overflows both ways so both bars are visible —UiUtils.findMouseWheelSourcefalls through toFrameGuiImpl.getMouseWheelTarget()→frameScroll(created withBars.BOTH,FrameGuiImpl:2127). UsebarSizeNative(Orientation.HORIZONTAL)(:987-1011, returnshBar.physicalDimension().heightin pixels) orscreen().coordinates().heightToNative(hBar.height()).
#7 Updated by Razvan-Nicolae Chichirau 23 days ago
- Status changed from WIP to Review
- % Done changed from 90 to 100
Hynek: Please check 11071a/rev. 16727.
All items were valid. For the second one, I removed the cumulative mouse wheel scrolling from browse and moved it at the driver-level for WEB clients. Tested on a laptop trackpad and it seems OK.
#8 Updated by Hynek Cihlar 7 days ago
- % Done changed from 100 to 90
- Status changed from Review to WIP
Code review 11071a revisions 16725..16727
Please check the major points below, they look legit.
- [MAJOR] functional
GuiWebDriver.MOUSE_SCROLL_THRESOLD: Thestatic finalconstant is initialized fromThinClient.getMouseScrollThreshold()whenGuiWebDriveris first loaded. That happens atnew GuiWebDriver(config)(WebClientSpawnerfor the spawned web client,OutputManager.initotherwise), which is beforeThinClient.initializePostassignsmouseScrollThreshold = server.getMouseScrollThreshold(0.8).ThinClient.mouseScrollThresholdhas no static default, so the constant is always 0.0. As a result, theheadStartPixelsbranch inwheelNotchesnever runs and the threshold is ignored, including the 0.8 default. The removedBrowseGuiImplconstant was loaded only when the first browse was created, afterinitializePost, so it did get 0.8. Trigger: a pixel-mode device reporting less than 100 px per notch, e.g. the 85 px case from the code comment, or Chrome/Edge at 110% page zoom (about 90 px). The first notch of the session and the first notch after every change of direction does nothing over a browse, SIMPLE combo, editor or selection list. Read the threshold lazily, at the first wheel event, instead of in a static initializer.
- [MAJOR] functional
GuiWebDriver.wheelNotches: The driver assumes a fixed 100 px per notch (WHEEL_NOTCH_PIXELS). Chromium-based browsers on Windows report pixeldeltaY= Windows "lines per notch" x 100/3 px, divided by the page zoom, andwriteInt32BinaryMessagethen truncates it. So the rotation produced here does not count physical notches. This contradicts the agreed #11071 target (1 item per notch regardless of the Windows setting) forComboBoxGuiImpl.mouseWheelMovedin SIMPLE mode (scrollBySteps(e.getWheelRotation())). Compared to trunk, it also changes the consumers that use only the rotation; these used to get +/-1 per message, which matches the Swing semantics where the lines setting lives ingetScrollAmount().- 6 lines per notch (
deltaY= 200, rotation 2 per notch): the SIMPLE combo moves 2 items per notch, the closed DROP-DOWN-LIST (oldIdx + scroll) skips every other value, and the browse, tree andScrollableListGuiImpldouble their step. - 1 line per notch (33 px): the SIMPLE combo and the other rotation consumers move once every 3 to 4 notches.
- Page zoom 90% (111 px): about every 9th notch moves 2 items. Zoom 110% (90 px): about every 10th notch does nothing. Zoom 125% (80 px): every 5th notch does nothing.
- Rev. 16726 (+/-1 per message) met the target for a classic wheel at any lines setting; 16727 meets it only at 3 lines and 100% zoom. Firefox is unaffected because it stays in line mode.
- The rotation should not be derived from a fixed pixel distance per notch. The browser does not expose a notch count in pixel mode, so this needs a different approach, e.g. counting each discrete classic-wheel event as one notch and accumulating only for smooth-scrolling devices.
- 6 lines per notch (
- [MAJOR] functional
ComboBoxGuiImpl.createSimpleList:simpleList.set(idx)/simpleList.setTopRow(idx)run beforesimpleListPane.setScrollBars(Bars.VERTICAL). The newpostScrollEvent(null, rowTop)insetTopRowtherefore reachesScrollPaneGuiImpl.handleScrollEventwhilevBaris still null, so only the list is scrolled. The bar is then created at position 0, and neithercreateScrollBarnoradjustScrollBar/adjustScrollLayoutImplsyncs it withrowTop(adjustScrollBaronly clamps whenposition > max). No later value change callssetTopRowagain, so the drift persists until the user wheels down once. Trigger: the #11071 repro withINITIAL "item7"(idx= 6,fixTopRow(6)= 6). The list showsitem7at the top whilevBar.positionis 0. The first wheel-up is swallowed by the newnewPos == vBar.getPosition()guard inscrollVerticallyTo, and wheel-down jumps back toitem2. Before the branch, the stale wheel-up at least jumped toitem1. Create the scroll bars beforeset/setTopRow, or (more robust, it also covers theshiftCurrentRowfinding) basescrollByStepsand the no-op guard on the list's real top row (getTopRow()) instead ofvBar.getPosition().
- [MINOR] style
GuiWebDriver.MOUSE_SCROLL_THRESOLD: Typo in the identifier ("THRESOLD"); since the constant is moved into new code, fix the name as part of the move.
- [MINOR] style
GuiWebDriver.wheelNotches: The localdirectionshadows thedirectionvariable the caller (raiseMouseEvent) assigns from the result, which hurts readability. The comment also mixes the "threshold" and "head start" terms.