Bug #11294
Widget is overlapped by another widget after the parent frame is made visible
90%
History
#1 Updated by Delia Mitric 7 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 7 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 7 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 6 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 6 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 6 months ago
- Status changed from WIP to Review
- % Done changed from 90 to 100
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 6 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 6 months ago
- % Done changed from 90 to 100
- Status changed from WIP to Review
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 6 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 3 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 3 months ago
- % Done changed from 100 to 90
- Status changed from Review to WIP
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 3 months 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 about 2 months 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.
#15 Updated by Razvan-Nicolae Chichirau about 1 month ago
After addressing the review from #11294-14, another z-order regression was unveiled. The client moves to top any not-yet-realized widget on its own the moment it turns visible in AbstractWidget.afterConfigUpdateBase:
// if widget was not yet realized and we change from hidden to visible ensure it is on the top
// of paint sequence
if (!c.realized && c.visible && c.visible != beforeUpdate.visible)
{
moveToTop();
}
The raise lands after the server has moved the frame's hidden widgets on top, undoing the correct order. Before the wasVisible gate, an ungated second makeFrameVisible() run (triggered by the frame:HIDDEN = FALSE that followed) happened to move the button on top again, which masked the conflict. Restricting the move to the visible transition removed that accidental repair and unveiled a defect that was already there. To see this, I modified the testcase from #11294-1 to hold 3 overlaping widgets:
DEF VAR fill1 AS CHARACTER NO-UNDO.
DEF BUTTON hidden_btn SIZE 4 BY 1.2.
def button btn2 size 5 by 1.
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
btn2 at row 3.2 col 19.4
WITH 1 DOWN KEEP-TAB-ORDER
AT COL 1 ROW 1 SCROLLABLE SIZE 50 BY 10 title "frame".
btn2:hidden = true.
fill1:HIDDEN = TRUE.
hidden_btn:HIDDEN = TRUE.
fill1:VISIBLE = TRUE. // The widget which will gain visiblity here will be at the bottom of the Z-Order,
// meaning the other 2 widgets will overlap it.
ENABLE ALL WITH FRAME fmain.
WAIT-FOR CLOSE OF CURRENT-WINDOW.#16 Updated by Razvan-Nicolae Chichirau about 1 month ago
I think there is one thing both #11294 and #11179 got wrong. The whole "move to top when frame becomes unhidden" logic is being executed every time the frame becomes visible. 4GL does not do that, it only alters the Z-Order when the frame is realized, and subsequent visibility modifications do not change the z-order.
Run this testcase with trunk:
def button btn1. def button btn2. def button btn3. def frame root with size 100 by 15. def frame child btn1 at row 3 col 2 btn2 at row 3.7 col 3 btn3 at row 3.1 col 5.5 with size 50 by 10 at row 2 col 2. frame child:frame = frame root:handle. btn2:hidden = true. // this should be shown on top of other buttons when hitting pause. frame child:hidden = false. enable all with frame child. enable all with frame root. pause. // reset context hide frame child. hide frame root. frame child:visible = false. frame root:visible = false. btn1:hidden = false. btn2:hidden = false. btn3:hidden = false. // Following current trunk, btn1 will be back on top. 4GL does not change the Z-Order again, so btn2 stays on top. btn1:hidden = true. frame child:hidden = false. enable all with frame child. enable all with frame root. wait-for window-close of current-window.
When the code hits the pause statement, btn2 will be on top because it was hidden when the frame was realized. Happens both in 4GL and trunk. After the context is reset and this time btn1 is hidden, trunk will raise btn1 back on top, meanwhile 4GL does not alter the Z-Order anymore.
So basically #11179 does not attack the root of the problem, but treats only a part of it. As such, I think the server logic added in #11179 should disappear and moved to the client, where we can reliably determine the exact moment when the frame gets realized.
#17 Updated by Razvan-Nicolae Chichirau about 1 month ago
- Status changed from WIP to Review
- % Done changed from 90 to 100
Hynek Cihlar wrote:
- [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.
Solved.
- [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.
The testcase as described:
def var a as char view-as fill-in. def var b as char view-as fill-in. def frame f a at row 3 col 2 b at row 3 col 6 with side-labels. a:screen-value = "a". b:screen-value = "b". display a b with frame f. a:hidden in frame f = true. display a with frame f. enable all with frame f. pause.
11294a and 4GL both paint fill-in "b" over "a".
- [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).
Testcase:
def button btn1. def button btn2. def button btn3. def frame f btn1 at row 3 col 3 btn2 at row 3 col 6 btn3 at row 2.3 col 4. btn2:hidden in frame f = true. view frame f. btn2:hidden = false. pause.
The Z-Order and layout is the same with 4GL and 11294a.
- [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.
Solved.
Commited 11294a/rev. 16740. Please first review the idea from AbstractWidget, and then review the changes. That moveToTop() call overrides the z-order imposed by moveToBatchWorker(). It does not matter if the move to batch call is made before or after a frame view. If the core idea is not right, please give some suggestions on how to tackle this problem.
#18 Updated by Hynek Cihlar 8 days ago
- % Done changed from 100 to 90
- Status changed from Review to WIP
Code review 11294a revisions 16733..16740
Razvan, there are still some issues with the changes. I'm attaching my attempt to fix the core issue so that I also offer a constructive advice. I didn't regression test the changes so I'm not confident these are the final code changes. Please take it as an inspiration, perhaps it will help you in finding the proper solution. I'm attaching in the next note.
- [MAJOR] functional
GenericFrame.makeFrameVisible: The!wasVisiblegate is not a realization gate.hideWorkercallsframe._setVisible(false)andgetAttr(FrameConfig::getVisible, ...)reads the server config, so afterHIDE FRAME fthe nextVIEW/DISPLAYseeswasVisible == falseand the batch raises whatever is hidden at that moment on an already realized frame (HIDE does not unrealize the client frame). Trigger: overlappingbtn1/btn2,VIEW FRAME f.,btn1:HIDDEN = TRUE.,HIDE FRAME f.,VIEW FRAME f.,btn1:HIDDEN = FALSE.- FWD putsbtn1on top, 4GL keepsbtn2on top (the #11294-16 rule). It also undoes an applicationMOVE-TO-BOTTOM()on a hidden widget, and costs a synchronousmoveToBatchround trip on every hide/re-view cycle of a frame with at least one hidden widget (e.g.REPEAT: DISPLAY ... WITH FRAME f. HIDE FRAME f. END.). Gate on the frame's realized state (frame._isRealized(), read beforeLogicalTerminal.view) instead. - [MAJOR] functional
GenericFrame.makeFrameVisible: The batch reorders the client content pane before the frame is realized. The frame is already attached (Frame.initialize->attachToWindow), soThinClient.moveToTop->Utils.moveToTopInClassreorders the live list;Frame.realizeFramethen runsdoLayout(), wheresaveFrameDetailscopies the reorderedwidgets()andZeroColumnLayout.calcLayoutplaces non-positioned widgets in that order.normalizeZOrder()runs only after layout and does not restore it. Trigger:FORM a b c WITH FRAME f SIDE-LABELS.,b:HIDDEN IN FRAME f = TRUE.,VIEW FRAME f.,b:HIDDEN = FALSE.-bis laid out aftercinstead of the compile-timea b c(also DOWN frame columns). Before, this only affected child frames viasetChildrenVisible; now it hits every first VIEW/DISPLAY of a root frame. The #11294-18 check (btn1..3withATpositions) cannot show this, since explicitly positioned widgets ignore the order. - [MAJOR] functional
GenericFrame.makeFrameVisible: The batch goes throughsyncMoveToState->GenericFrame.toTop->FieldGroup.toTop, which rewrites the listFieldGroup.firstChild()/getNextChild()walk, so realizing the frame reorders theFIRST-CHILD/NEXT-SIBLINGchain. Trigger:DEFINE FRAME f b1 b2 b3.,b1:HIDDEN = TRUE.,VIEW FRAME f., walkFRAME f:FIRST-CHILD:FIRST-CHILD/NEXT-SIBLING- FWD givesb2 b3 b1(and keeps it afterb1:HIDDEN = FALSE); OE 11.6 givesb1 b2 b3. OE reorders the chain only on explicitMOVE-TO-TOP()/MOVE-TO-BOTTOM(). The realization raise should touch the client z-order only, without the server-sidesyncMoveToState. - [MAJOR] functional
AbstractWidget.afterConfigUpdateBase: Removing the client raise for frame-parented widgets breaks frames realized throughENABLEalone.GenericFrame.enable(list, ...)->LogicalTerminal.enablerealizes the frame on the client (enableWorker->viewWorker) without reachingmakeFrameVisible, so no server batch compensates, and the widget-levelrealizedflag of the hidden widget stays false. Trigger:DEFINE FRAME f b1 AT ROW 2 COL 2 b2 AT ROW 2.5 COL 8 WITH SIZE 40 BY 5.,b1:HIDDEN = TRUE.,ENABLE b2 WITH FRAME f.,b1:HIDDEN = FALSE.- before,b1ended on top, which matches OE 11.6; nowb2stays on top. This affectsenable_UI-style code that realizes the frame withENABLEand no precedingDISPLAY/VIEW. - [MINOR] functional
GenericFrame.setChildrenVisible: The #11179 raise is still unconditional. It runs on everyFRAME fc:HIDDEN = FALSEof a child frame whose parent is not visible, including after the child was realized, which violates the same realization rule. Trigger:VIEW FRAME fp(childcrealizes, overlappingb1/b2,b2on top),HIDE FRAME fp.,b1:HIDDEN = TRUE.,FRAME c:HIDDEN = FALSE.-b1is raised aboveb2. Both sites should share one realization gate. - [MINOR] functional
AbstractWidget.afterConfigUpdateBase: Dynamic widgets created hidden in an already realized frame lose the client raise added in EVL 20210803. Trigger: framefviewed,CREATE BUTTON h1 ASSIGN FRAME = FRAME f:HANDLE HIDDEN = TRUE ...,CREATE BUTTON h2overlapping it,h1:HIDDEN = FALSE.- beforeh1was raised aboveh2, nowh2stays on top and no server path raisesh1. The OE baseline for this case was not checked. - [MINOR] functional
AbstractWidget.afterConfigUpdateBase: Child frames have a non-nullparent(Frame.class)(the walk starts atparent()), so they are no longer raised when first made visible, which also bypassesFrameGuiImpl.moveToTopand itsKEEP-FRAME-Z-ORDERhandling. TheplaceFrameIntraise inThinClientcovers only the viewed frame, not its child frames. Trigger: overlapping child framesc1/c2,FRAME c2:HIDDEN = FALSE.,FRAME c1:HIDDEN = FALSE.while parentfpis not visible, thenVIEW FRAME fp.- beforec1ended abovec2, now stacking follows definition order only. The OE baseline for this case was not checked. - [MINOR] performance
GenericFrame.makeFrameVisible: Each child frame reached from the parent'smakeFrameVisibleloop performs its own synchronousmoveToBatchround trip, so a parent with N child frames containing hidden widgets costs N+1 blocking calls on the first VIEW where one batch would do. - [MINOR] style
GenericFrame.makeFrameVisible: The localList widgetsshadows thewidgetsarray field while the surrounding comments refer to both; rename it (e.g.fieldWidgets).
#19 Updated by Hynek Cihlar 8 days ago
- File 11294a_fix.patch
added