Bug #11294
Widget is overlapped by another widget after the parent frame is made visible
90%
History
#1 Updated by Delia Mitric 6 months ago
This testcase:
DEF VAR fill1 AS CHARACTER NO-UNDO.
DEF BUTTON hidden_btn SIZE 4 BY .81.
DEFINE FRAME fmain
fill1 AT ROW 2.71 COL 11.8 COLON-ALIGNED
VIEW-AS FILL-IN
SIZE 13.2 BY 1
hidden_btn AT ROW 2.86 COL 22.6 NO-TAB-STOP
WITH 1 DOWN KEEP-TAB-ORDER
AT COL 1 ROW 1 SCROLLABLE SIZE 50 BY 10.
hidden_btn:HIDDEN = TRUE.
fill1:HIDDEN = TRUE.
fill1:VISIBLE = TRUE.
FRAME fmain:HIDDEN = FALSE.
ENABLE ALL WITH FRAME fmain.
WAIT-FOR CLOSE OF CURRENT-WINDOW.
In OE:
--- In FWD: 
#2 Updated by Delia Mitric 6 months ago
- Status changed from New to WIP
- % Done changed from 0 to 100
- reviewer Hynek Cihlar added
- move the hidden widgets (not frames) on the top of the parent frame when the frame is set to visible (even if it is still visible)
Committed the changes to 11294a branch rev. 16469
Hynek, please review. Thanks!
#4 Updated by Hynek Cihlar 5 months ago
- Status changed from Review to WIP
- % Done changed from 100 to 90
With the changes makeFrameVisible duplicates the logic already in setChildrenVisible. Can these be deduplicated?
Also makeFrameVisible uses widgets for the z-order logic, but should fieldGroup.getWidgets() be used instead? See setChildrenVisible.
#5 Updated by Delia Mitric 5 months ago
- Status changed from WIP to Review
- % Done changed from 90 to 100
Hynek Cihlar wrote:
With the changes
makeFrameVisibleduplicates the logic already insetChildrenVisible. Can these be deduplicated?
I've added a method findHiddenWidgets that returns the list of the hidden widget of the frame to use it in setChildrenVisible and makeFrameVisible in moveToBatchWorker call to "deduplicate" that part of the code, but I think we should let the moveToBatchWorker call at the end of the methods to ensure the moving is made correctly.
Also
makeFrameVisibleuseswidgetsfor the z-order logic, but shouldfieldGroup.getWidgets()be used instead? SeesetChildrenVisible.
Done. findHiddenWidgets method uses fieldGroup.getWidgets().
Please review rev. 16470.
Thank you!
#6 Updated by Hynek Cihlar 5 months ago
- Status changed from Review to WIP
- % Done changed from 100 to 90
- [MAJOR] functional
GenericFrame.setChildrenVisible:findHiddenWidgetsis called unconditionally regardless of thevalueparameter. The old code only populatedmoveToTopWidgetsinside theif (isHidden[i++] && value)block, so hidden widgets were collected only when making children visible. Now when hiding children (value false),moveToBatchWorkerwill still unnecessarily move hidden widgets to the top of z-order. Guard the call withvalue trueor pass an empty list when hiding. - [MAJOR] style
GenericFrame.makeFrameVisible: ThefindHiddenWidgetscall at line 11700 exceeds the 110-character line length limit (116 characters). It should be wrapped, e.g.:List<GenericWidget<?>> moveToTopWidgets = findHiddenWidgets((List<GenericWidget<?>>) fieldGroup.getWidgets()); - [MINOR] performance
GenericFrame.setChildrenVisible: Redundant iteration over the widget list. TheisHidden[]array already caches each widget's_isHidden()state from a prior loop, butfindHiddenWidgetsre-queries_isHidden()on every widget. This adds a third pass over the widget list where the old code only had two. Consider reusing theisHidden[]array to build the move-to-top list. - [MINOR] performance
GenericFrame.makeFrameVisible:findHiddenWidgetscalls_isHidden()on every widget, and the subsequent visibility loop also calls_isHidden()on each widget again. Since_isHidden()withflush=truetriggersflushEnqueuedWidgetAttrs()on every invocation, this doubles the flush cost. Consider caching the hidden state.
#7 Updated by Delia Mitric 5 months ago
- % Done changed from 90 to 100
- Status changed from WIP to Review
Hynek Cihlar wrote:
- [MAJOR] functional
GenericFrame.setChildrenVisible:findHiddenWidgetsis called unconditionally regardless of thevalueparameter. The old code only populatedmoveToTopWidgetsinside theif (isHidden[i++] && value)block, so hidden widgets were collected only when making children visible. Now when hiding children (value false),moveToBatchWorkerwill still unnecessarily move hidden widgets to the top of z-order. Guard the call withvalue trueor pass an empty list when hiding.- [MAJOR] style
GenericFrame.makeFrameVisible: ThefindHiddenWidgetscall at line 11700 exceeds the 110-character line length limit (116 characters). It should be wrapped, e.g.:
[...]- [MINOR] performance
GenericFrame.setChildrenVisible: Redundant iteration over the widget list. TheisHidden[]array already caches each widget's_isHidden()state from a prior loop, butfindHiddenWidgetsre-queries_isHidden()on every widget. This adds a third pass over the widget list where the old code only had two. Consider reusing theisHidden[]array to build the move-to-top list.- [MINOR] performance
GenericFrame.makeFrameVisible:findHiddenWidgetscalls_isHidden()on every widget, and the subsequent visibility loop also calls_isHidden()on each widget again. Since_isHidden()withflush=truetriggersflushEnqueuedWidgetAttrs()on every invocation, this doubles the flush cost. Consider caching the hidden state.
Fixed all of these and committed the changes to 11294a branch rev. 16471 .
Hynek, please review. Thanks!
#8 Updated by Hynek Cihlar 4 months ago
- Status changed from Review to WIP
- % Done changed from 100 to 90
Code review 11294a revisions 16468..16471
- [MAJOR] functional
GenericFrame.makeFrameVisible: ThehiddenIdscache is populated by iteratingthis.widgets[], butfindHiddenWidgetsis later called withfieldGroup.getWidgets(). The two collections are not guaranteed to hold the same widget set -GenericFrame.addDynamicWidgetcan skip thewidgets[]array on a name hit-test (earlyreturn), whileFieldGroup.addWidgetunconditionally receives the widget viaGenericWidget.setFrame. Widgets present only infieldGroup.getWidgets()returnnullfrom the map and are silently treated as "not hidden" by the!= null ? ... : falseguard, so such hidden widgets will never be moved to top - reintroducing the exact z-order bug the patch is meant to fix. Please check whether this can be an issue when the two lists are not identical. - [MAJOR] style
GenericFrame.findHiddenWidgets: The method signature exceeds the project's 110-character line-length limit; wrap the parameter list onto additional lines. - [MINOR] performance
GenericFrame.findHiddenWidgets:hiddenIds.get(widget.getId())is called twice per widget (once for null check, once for the value). Store the result once in a localBooleanor usehiddenIds.getOrDefault(widget.getId(), false). - [MINOR] performance
GenericFrame.setChildrenVisible: The refactor replaced a primitiveboolean[](O(1) indexed access, no boxing) withHashMap<Integer, Boolean>, incurringInteger/Booleanautoboxing, hash computation, andNodeallocation per entry. Since both loops iterate the samewidgetslist in the same order, the original parallelboolean[]suffices and should be restored; onlymakeFrameVisiblegenuinely needs the id-keyed map because its two passes iterate different lists. - [MINOR] style
GenericFrame.setChildrenVisible: Whenvalue == falsethe expressionvalue ? findHiddenWidgets(...) : new ArrayList<>()still allocates a throwaway empty list that is immediately discarded bymoveToBatchWorker(which short-circuits on empty input). Guard just thefindHiddenWidgets/moveToBatchWorkerpair withif (value), or useCollections.emptyList(), to avoid the allocation and make the intent explicit.
#9 Updated by Delia Mitric 4 months ago
- Status changed from WIP to Review
- % Done changed from 90 to 100
Committed the new changes to 11294a rev. 16531.
I've tried to de-duplicate the logic of moving the widget to the top as you said in #11294-4, to use the fieldGroup widget list to be consistent with the client side (as in setChildrenVisible method), to introduce a cache for the hidden state in GenericFrame.makeFrameVisible method and keep the one from GenericFrame.setChildrenVisible as it is.
Hynek, please review.
#10 Updated by Hynek Cihlar 4 months ago
- % Done changed from 100 to 90
- Status changed from Review to WIP
Code review 11294a revisions 16527..16531
- [MAJOR] performance
GenericFrame.makeFrameVisible:hiddenIds.getOrDefault(widget.getId(), widget._isHidden())evaluates the default argument unconditionally (Java is strict), so_isHidden()runs for everyfgWidgetsentry even on cache hits. Since_isHidden()routes throughgetAttr(..., true)which performsflushEnqueuedWidgetAttrs(), this defeats the very cache the diff introduces. UsecontainsKey()/get()(orcomputeIfAbsent) so_isHidden()only runs on genuine cache misses.
- [MAJOR] style
GenericFrame.moveHiddenWidgets: Javadoc declares@param hiddenIdsbut the actual parameter is namedhidden. Update the Javadoc tag to@param hidden(or rename the parameter) so they match.
- [MAJOR] style
GenericFrame.setChildrenVisible: The new callmoveHiddenWidgets(widgets, isHidden);is indented with 8 spaces. The enclosingif (value)block opens at 6 spaces, so its body should be at 9 spaces per the project's 3-space indent convention. Re-indent the block body and closing brace accordingly.
- [MINOR] performance
GenericFrame.makeFrameVisible: After buildinghidden[]via one pass overfgWidgets,moveHiddenWidgetsiteratesfgWidgetsa second time to filter intohiddenWidgets. The two passes can be fused into one at this call site (buildhiddenWidgetsdirectly during the cache-resolution loop), saving a full traversal and theboolean[]bridge allocation. ThemoveHiddenWidgetshelper can still be kept forsetChildrenVisible, which genuinely needs the cache captured before its mutating loop.
- [MINOR] performance
GenericFrame.setChildrenVisible: The move-to-top collection is now computed in a second pass insidemoveHiddenWidgets. Previously it was produced as a side effect of the single pass that flipped visibility. Restoring the single-pass accumulation insetChildrenVisible(while still reusing the helper frommakeFrameVisible) avoids N extra iterations and N redundantinstanceof FrameWidgetchecks on every frame un-hide.
#11 Updated by Razvan-Nicolae Chichirau about 2 months ago
- Status changed from WIP to Review
- Assignee changed from Delia Mitric to Razvan-Nicolae Chichirau
- % Done changed from 90 to 100
The changes from 11294a were overcomplicating things. I rebased the branch and switched to a cleaner approach.
Hynek: Please do a review for 11294a.
#12 Updated by Hynek Cihlar about 1 month ago
- Status changed from Review to WIP
- % Done changed from 100 to 90
Code review 11294a revisions 16653..16658
- [MAJOR] functional
GenericFrame.makeFrameVisible: The new move-to-top logic buildsmoveToTopWidgetsby iterating thewidgets[]array, but the siblingsetChildrenVisibledeliberately iteratesfieldGroup.getWidgets()for this same purpose (see its comment thatwidgetsorder can be "inconsistent with the client containers ... when using MOVE-TO-TOP", introduced by history rev 391, with the move-to-top added in rev 401). AMOVE-TO-TOP— or the batch move itself, or a priorHIDDEN=FALSE/VIEWcycle — reorders only the field group (GenericFrame.toTop→FieldGroup.toTop) and neverwidgets[], so the two orderings diverge. BecausemoveToBatchappliesmoveToTopper z-order class with the last id ending topmost, iteratingwidgets[]can restack two or more simultaneously-hidden, overlapping, same-class widgets in the wrong relative z-order; once they are shown (e.g.ENABLE ALL) the overlap this change is meant to fix renders incorrectly, and the wrong order is also written back to the field group viasyncMoveToState. Trigger: a frame reachingmakeFrameVisible(e.g. viaVIEW) that holds 2+ overlapping hidden same-class widgets after an earlierMOVE-TO-TOPor hide/show cycle diverged the two orderings. This reintroduces the exactwidgets[]-vs-field-group inconsistency that rev 391 removed. Fix: iteratefieldGroup.getWidgets()inmakeFrameVisible, matchingsetChildrenVisible.
#13 Updated by Razvan-Nicolae Chichirau about 1 month ago
- Status changed from WIP to Review
- % Done changed from 90 to 100
I think this went over my head while refactoring Delia's approach. Please check 11294a/rev. 16659.
#14 Updated by Hynek Cihlar 6 days ago
- Status changed from Review to WIP
- % Done changed from 100 to 90
Code review 11294a revisions 16653..16659
There is one group of issues remaining, which share one root cause: that call moveToBatchWorker is unguarded and placed after _setVisible(true), so it fires on every makeFrameVisible() invocation rather than only on the hidden-visible transition that history entry 405 describes.
- [MAJOR] functional
GenericFrame.makeFrameVisible: The newmoveToBatchWorker(true, moveToTopWidgets)is unconditional, so it runs even when the frame is already visible, silently overriding an application-requested z-order for hidden widgets.prepareView's gate!frame._isHidden() || currentStatement != UIStatement.DISPLAY(5855) is satisfied by the first disjunct alone for a visible frame, sodisplayWorker(12055) ->view(data)->prepareView->makeFrameVisible()re-runs the batch on everyDISPLAY ... WITH FRAME. Trigger (GUI; CHUI rejectsMOVE-TO-BOTTOMon non-frame widgets with 4088):hidden_btn:HIDDEN = TRUE./hidden_btn:MOVE-TO-BOTTOM()./DISPLAY fill1 WITH FRAME fmain.— theMOVE-TO-BOTTOMis honored (AbstractWidget._setHiddendoes not detach the widget from its container, soUtils.moveToBottomInClassreturns TRUE and the server state syncs), and the followingDISPLAYsilently undoes it. The batch also rewrites the 4GL handle chain viasyncMoveToState->moveInChain(true)(re-insert at thefirstResourcehead) plusFieldGroup.toTop, soFIRST-CHILD/NEXT-SIBLINGtraversal flips as a side effect of aDISPLAYstatement. Gate the batch on the visible->transition;wasVisibleis already computed at 11839 but currently only pushed, never used.
- [MAJOR] functional
GenericFrame.makeFrameVisible: On the DISPLAY path the batch runs before DISPLAY clears the HIDDEN flag, so it raises exactly the widgets DISPLAY is about to un-hide.prepareViewcallsmakeFrameVisible()at 5857 and only afterwards doesgw.setAttr("hidden", wcfg.hidden, false, ...)for the displayed widgets (5876-5880). The client genuinely performs the move —ThinClient.moveToTop(26461) ->AbstractWidget.moveToTop->Utils.moveToTopInClass(5993) has no visible/hidden/realized filter — andAbstractContainer.draw()(603) paints the list forward, so last == topmost. Trigger: two overlapping fill-insaandbin a frame,DISPLAY a b(b over a), thena:HIDDEN IN FRAME f = TRUE./DISPLAY a WITH FRAME f.—ais raised, then un-hidden, and now paints overb. This contradicts both pre-existing, deliberately gated raise mechanisms:ThinClient14283-14294 requiresw.canMovetoTop()andWINDOW:KEEP-FRAME-Z-ORDER(both defaultfalse, so BUTTON/RECTANGLE/IMAGE were never raised), andAbstractWidget.afterConfigUpdateBase3411-3414 raises only un-realized widgets. The new batch honors neither gate, so trunk and branch disagree for any realized hidden widget in a default window. Note also thatsetHiddenimplicitly hides the side label (1596-1607), so labels are collected and raised too.
- [MAJOR] functional
GenericFrame.makeFrameVisible: The batch is issued before the client realizes and lays out the frame, so auto-placed widgets in a frame containing a HIDDEN field are physically mis-positioned on first display.flushDeferredDefBeforeVisible()force-pushes theScreenDefinitionsynchronously (9084-9086), so the clientFrameand its children exist and are parented to aTopLevelWindow— butconfig().realizedis set only inFrame.realizeFrame(2928), reached fromFrame.setVisible(true), which the subsequentviewRPC triggers.ThinClient.moveToTophas norealizedguard and all three of its checks pass, soUtils.moveToTopInClassphysically reorders the liveAbstractContainer.widgetslist;realizeFramethen clearssavedTabOrder,doLayoutWorkerre-snapshots it from the reorderedgetContentPane().widgets(), andZeroColumnLayout.calcLayout(~700) walks it sequentially, re-applyingsetLocationfor every widget whosebc.row/bc.columnisINV_COORD. Hidden widgets are not skipped in the placement loop, so the hidden widget still consumes its slot. Trigger:DEFINE FRAME f a b c WITH SIDE-LABELS./b:HIDDEN IN FRAME f = TRUE./VIEW FRAME f.— pane order becomes[a, c, b]and the realization layout placescinb's slot. Contrast the pre-existing call sitesetChildrenVisible(5722), where the frame is already realized so the reorder is pure z-order. Fix: defer the move until after theviewRPC, or apply it inrealizeFrameafterdoLayout()— wherenormalizeZOrder()is already called for exactly this reason ("z-order is normalized after layout is done; layout needs to use the original widget order", 2956-2962).
- [MAJOR] performance
GenericFrame.makeFrameVisible: Adds a synchronous server->client round trip to everyVIEW/DISPLAYof any frame holding at least one hidden widget.LogicalTerminal.moveToBatch(19231) returnsBoolean[]throughclientCall()->session.transact, so it blocks; it is not enqueued/deferred likepushWidgetAttr. SincemakeFrameVisibleruns once per DISPLAY (see the first finding), a frame with a conditionally hidden field pays this on every iteration:form v1 v2 with frame f./v2:hidden in frame f = yes./for each customer: display customer.name@ v1 with frame f. end.Precisely, this *doubles* the round trips on the affected path (the following @LogicalTerminal.viewis also blocking), i.e. 1 -> 2 per DISPLAY, and it is a new RTT that did not exist on this path before. Per-widget client amplification:AbstractContainer.moveToTopInClass(1017) callswidgetStateChanged(), which recurses over every child widget, andUtils.moveToTopInClassrunsnormalizeZOrderover the whole container list even whennp == pos; in web mode those mark ids inGuiWebDriver.stateChanged, forcing a fullregisterInteractiveWidgets()walk at the next input point.moveToBatchWorkerdoes early-return on an empty list andWidgetConfig.hiddendefaults tofalse, so frames with no explicitly hidden widgets are unaffected. Caveat on the fix: a bare!wasVisiblegate would skip widgets whose HIDDEN is set after the frame became visible; a dirty-flag or piggy-backing the ids onto the followingviewcall is safer.