Bug #10938
Date FILL-IN: Incorrect SCREEN-VALUE after widget enablement
90%
History
#1 Updated by Vladimir Tsichevski 8 months ago
Run the following example in either GUI or ChUI mode:
SESSION:DATE-FORMAT = 'mdy'. DEFINE VARIABLE dateField AS DATE VIEW-AS FILL-IN NO-UNDO FORMAT '99/99/9999'. DEFINE FRAME f dateField NO-LABEL. MESSAGE "Disabled:" dateField:SCREEN-VALUE IN FRAME f. ENABLE ALL WITH FRAME f. MESSAGE "Enabled:" dateField:SCREEN-VALUE IN FRAME f. WAIT-FOR WINDOW-CLOSE OF FRAME f.
This creates a frame containing a date FILL-IN widget with a delimited format (99/99/9999). The SCREEN-VALUE is displayed before and after enabling the widget.
Expected Behavior¶
- Before enablement
SCREEN-VALUEis empty (""). - After enablement:
SCREEN-VALUEinitializes to the format's placeholder delimiters (" / /").
Actual Behavior (FWD)¶
- Before enablement (disabled):
SCREEN-VALUEis empty (""). - After enablement:
SCREEN-VALUEremains empty.
The visible (displayed) value in the UI correctly renders the expected delimiters.
#2 Updated by Razvan-Nicolae Chichirau 4 months ago
- Status changed from New to WIP
- Assignee set to Razvan-Nicolae Chichirau
The core issue is that after processing the ENABLE statement on client and handling the new screen buffers on the server, the widgets are still unitialized, so the screen-value they report is an empty string. This is not isolated to fill-ins, but to most of the widgets which support SCREEN-VALUE:
Since during enable, the view worker on the client works with the screen buffer provided by the server (which states that all widgets are uninitialized, and that is correct), I'm thinking the state update should be done at the end of the ThinClient.enable() to ensure they are not overridden.
#3 Updated by Razvan-Nicolae Chichirau 4 months ago
I've tested this patch:
=== modified file 'src/com/goldencode/p2j/ui/client/Frame.java'
--- old/src/com/goldencode/p2j/ui/client/Frame.java 2025-09-16 08:33:50 +0000
+++ new/src/com/goldencode/p2j/ui/client/Frame.java 2026-03-19 12:22:10 +0000
@@ -6086,7 +6086,16 @@
public void initializeWidget(Widget<O> comp)
{
Widget<O> field = getField(comp);
-
+ if (!(field instanceof DataContainer))
+ {
+ return;
+ }
+
+ if (field.isEnabled())
+ {
+ field.setState(ScreenBuffer.CHANGED);
+ }
+
if (field instanceof RadioSet)
{
((RadioSet<O>) field).setInitial();
When initializing the widget, its state is set to CHANGED so that it is included in the screen buffer sent to the server and can be processed there. Overall, this works well, but issues arise when fill-ins are rendered.
Previously, in legacy ChUI or GUI fill-ins (i.e., FillInImpl and FillInGuiImpl), there was a check for the actual state of the fill-in: whether it was uninitialized or not. If it was uninitialized, empty characters were displayed; otherwise, its actual content was shown.
The problem now is that the state is CHANGED instead of UNINITIALIZED, while the internal buffer from the screen presentation is still null. As a result, the screen value returns the ? string.
Note that this issue currently affects only fill-ins, while other widgets behave correctly.
Hynek: Could you please advise on this situation? What seems to be the issue here? Is it that the widget state should not be changed during initialization? Or does this change simply expose another issue related to fill-ins? Alternatively, could the problem come from the fact that the reported screen-value is "?"? I have tested multiple fill-in scenarios but was unable to reproduce a case where the screen-value becomes the unknown value. However, it’s possible that I may be overlooking something.
#4 Updated by Hynek Cihlar 4 months ago
Razvan, it is important to figure out the exact point in the 4GL execution when widgets are initialized. Is it really the ENABLE statement? Or is it the widget realization phase? Does it work the same for dynamic and static widgets? Etc. Once you have enough evidence, present it here and we can talk about the actual implementation.
#5 Updated by Razvan-Nicolae Chichirau 4 months ago
Hynek Cihlar wrote:
Razvan, it is important to figure out the exact point in the 4GL execution when widgets are initialized. Is it really the
ENABLEstatement? Or is it the widget realization phase? Does it work the same for dynamic and static widgets? Etc. Once you have enough evidence, present it here and we can talk about the actual implementation.
The widget is initialized when it is realized. I've tested with different field-level widgets, static + dynamic, GUI + ChUI. Observed that the same widgets can be realized at different times if they are defined as static or dynamic, but a consistent aspect is that when the widget was realized, its screen-value attribute became "available".
For example, a static radio-set has the screen-value equal to ? when it is first defined, but it takes a real value when it is realized in a frame. For a dynamic radio-set, things change. If an invalid widget definition is wrote, such as create radio-set rsDyn (i.e., does not have the radio buttons set), the widget can not be realized right away so the screen-value remains ?. When you try to display it into a frame, the following error is shown: ** Unable to realize RADIO-SET widget. (4025). Now, if the radio-set is defined correctly from the start, AVM can realize this widget, so the screen-value is available right away, without displaying it into a frame.
Anyway, the idea is that the screen-value should be updated when the widget is realized, so it is not directly dependent upon ENABLE, DISPLAY, VIEW or other statements that may or may not realize the widget.
As for the actual implementation, I guess that the idea of setting the widget's state to CHANGED still stands, right? I don't see another way of propagating the updated screen value to the server.
#6 Updated by Razvan-Nicolae Chichirau 4 months ago
- % Done changed from 0 to 100
- Status changed from WIP to Review
Hynek: Please review 10938a/rev. 16470.
#7 Updated by Hynek Cihlar 3 months ago
- Status changed from Review to WIP
- % Done changed from 100 to 90
Code review 10938a revisions 16469..16470
- [CRITICAL] functional
AbstractWidget.widgetRealized:ComboBox,SelectionList, andRadioSetoverridewidgetRealized()without callingsuper.widgetRealized(), so the newsetState(ScreenBuffer.CHANGED)added inAbstractWidgetis dead code for theseDataContainerwidgets. The Redmine summary states the fix should affect allDataContainerwidgets.
- [MAJOR] functional
ComboBox.activate: ThesetState(ScreenBuffer.CHANGED)added for history entry 130 ("Modify the widget internal state to CHANGED when realized") is placed at the end ofactivate()- a drop-down interaction method - not inwidgetRealized(). Furthermore,ComboBox.widgetRealized()does not callsuper.widgetRealized(), so thesetState(CHANGED)added inAbstractWidget.widgetRealized()is also never reached. This means: (1) the intended realization-time state change never fires forComboBox, defeating the purpose of history entry 130; and (2) every dropdown activation unconditionally marks the widget asCHANGED, potentially causing unintended screen-value refreshes unrelated to the enablement issue.
- [MAJOR] functional
FillIn.shouldDisplayBlanks/FillInImpl.draw: The behavioral scope has changed compared to the original logic. The old code (both CHUIFillInImpl.draw()and GUIFillInGuiImpl.isUninitialized()) only displayed blanks forUNINITIALIZEDstate; the newshouldDisplayBlanksalso returns true forUNCHANGEDstate (when the variable is null or unknown), widening the set of states that trigger blank display. Was this change in behavior intentional?
- [MAJOR] style
FillIn.shouldDisplayBlanks: Missing parentheses around the second operand of||. The condition reads(state != UNINITIALIZED && state != UNCHANGED) || appVar != null && !appVar.isUnknown(). The first operand is explicitly parenthesized but the second is not. Add parentheses for clarity and consistency:|| (appVar != null && !appVar.isUnknown()).
- [MINOR] style
FillIn.shouldDisplayBlanks: Continuation line alignment. Theifcondition wraps the second operand at column 9 instead of aligning with the content afterif ((column 11). Same issue for theboolean isDateassignment continuation.
- [MINOR] style
ThinClient.collectScreenValues: Continuation line alignment at the multi-lineifcondition, one space short of aligning with the opening parenthesis.
- [MINOR] functional
FillInImpl.draw: The old CHUI code only exemptedDateFormatfrom the blank-display logic; the newshouldDisplayBlanks()also exemptsDatetimeFormat. This is a behavioral change for CHUI datetime fill-ins which will now show separators when enabled and uninitialized. Was this intentional?
#8 Updated by Razvan-Nicolae Chichirau 3 months ago
- % Done changed from 90 to 100
Hynek Cihlar wrote:
- [MAJOR] functional
ComboBox.activate: ThesetState(ScreenBuffer.CHANGED)added for history entry 130 ("Modify the widget internal state to CHANGED when realized") is placed at the end ofactivate()- a drop-down interaction method - not inwidgetRealized(). Furthermore,ComboBox.widgetRealized()does not callsuper.widgetRealized(), so thesetState(CHANGED)added inAbstractWidget.widgetRealized()is also never reached. This means: (1) the intended realization-time state change never fires forComboBox, defeating the purpose of history entry 130; and (2) every dropdown activation unconditionally marks the widget asCHANGED, potentially causing unintended screen-value refreshes unrelated to the enablement issue.
Oops, I think this was some leftover from the first implementation. The purpose of this branch is to modify the state to CHANGED only when the widget is realized.
- [MAJOR] functional
FillIn.shouldDisplayBlanks/FillInImpl.draw: The behavioral scope has changed compared to the original logic. The old code (both CHUIFillInImpl.draw()and GUIFillInGuiImpl.isUninitialized()) only displayed blanks forUNINITIALIZEDstate; the newshouldDisplayBlanksalso returns true forUNCHANGEDstate (when the variable is null or unknown), widening the set of states that trigger blank display. Was this change in behavior intentional?
Yes. Now that we have the state changing on widget realization (after the update is passed to the server), the widget state is reset to ScreenBuffer.UNCHANGED, but it should still display blanks and not ?.
- [MINOR] functional
FillInImpl.draw: The old CHUI code only exemptedDateFormatfrom the blank-display logic; the newshouldDisplayBlanks()also exemptsDatetimeFormat. This is a behavioral change for CHUI datetime fill-ins which will now show separators when enabled and uninitialized. Was this intentional?
Yes, I've tested on ChUI and date, datetime and datetime-tz have the same behavior in this area.
Please check 10938a / rev. 16516.
#9 Updated by Razvan-Nicolae Chichirau 3 months ago
- Status changed from WIP to Review
#10 Updated by Hynek Cihlar 3 months ago
- % Done changed from 100 to 90
- Status changed from Review to WIP
Code review 10938a revisions 16514..16516
- [MAJOR] functional
ThinClient.enableDisable: On thepreview && enabledpath (around line 6577),viewWorkeris invoked with the defaultskipScreenBuffer=false, which still triggerssetScreenBuffers(frameBuf, true)insideviewWorker. SubsequentlyenableWorker->processEnablenow also callssetScreenBuffers(sb)unconditionally (the change fromif (!enabled && sb != null)toif (sb != null)). Net effect: on this branch screen buffers are applied twice, contradicting the stated intent of the change ("Do not override any changed widget state during ENABLE by setting the screen buffers multiple times"). Either the previewviewWorkercall should also passskipScreenBuffer=true, orprocessEnableshould still gate on!preview || !enabled.
- [MINOR] style
ThinClient.viewWorker: The new 6-arg overload parametersint windowIdandboolean skipScreenBufferlack thefinalmodifier while the other parameters (widget,frameBuf,widgetIds,isView) are declaredfinal. The newly-addedskipScreenBufferparameter continues the inconsistency instead of following the prevailing style.
- [MINOR] style
FillIn.shouldDisplayBlanks: The local variableappVarshadows the existing instance fieldappVardeclared at the top of the class, which hurts readability.
- [MINOR] style
ThinClient.collectScreenValues: The cast(FillIn) compis a raw-type cast assigned to a parameterized localFillIn, ?> fillIn, producing an unchecked-cast pattern. Use(FillIn, ?>) compinstead.
- [MINOR] functional
AbstractWidget.widgetRealized: The unconditionalsetState(ScreenBuffer.CHANGED)applies to everyDataContainerimplementation, including widgets likeTreeGuiImpl, which are not mentioned in the scope. Is this expected? Should the scope of the change be explicitly controlled?
- [MINOR] functional
FillIn.shouldDisplayBlanks: The state test uses strict equality (state != UNINITIALIZED && state != UNCHANGED).ScreenBuffertreats state as bit flags (UNINITIALIZED=1,CHANGED=2,ENTERED=4,STATE_MAX=7) and the codebase elsewhere composes them with|=(e.g.swr.state |= ENTERED). Composite states such asUNINITIALIZED | ENTEREDwould fail both equality checks. The priorisUninitializedused the same pattern so this is latent rather than a new regression, but bitwise checks ((state & UNINITIALIZED) != 0) would be more defensive and match existingisUninitialized/isChangedhelpers.
- [MINOR] functional
FillIn.shouldDisplayBlanks: The new helper exemptsDatetimeFormatfill-ins from blanks when enabled; the old CHUIFillInImpl.draw()only exemptedDateFormat.ThinClient.collectScreenValuesstill special-cases onlydataType.equalsIgnoreCase("date")for thegetText()retrieval, so datetime fill-ins fall into theelsebranch and may receivecharacter.of("")whenshouldDisplayBlanksreturns true while their UI now shows datetime separators. This creates an inconsistency between what the widget displays and what the server receives for newly-exempt datetime fill-ins.
- [MINOR] functional
FillInImpl.draw: The old inline check gated only onuninit && !(DateFormat && enabled). The newshouldDisplayBlanks()additionally returnsfalsewhenappVar != null && !appVar.isUnknown(). A CHUI FILL-IN inUNINITIALIZEDstate but with a known variable value will now render its value where previously it rendered blanks. Please check this case.
- [MINOR] functional
FillInImpl.draw: The updated comment "If uninitialized or unchanged and not for input and not date fill-in (in this case separators should be displayed), display blanks instead" could imply as if "not for input" applies universally. Please clarify.
- [MINOR] functional
ThinClient.collectScreenValues: The class checkclazz FillInGuiImpl.class || clazz FillInImpl.classuses exact class equality to deliberately exclude subclasses (EntryFieldGuiImpl,LineEditor,BrowseFilterEntryField, all extendingFillInGuiImpl). A comment explaining whyinstanceofis deliberately avoided would protect the combo/editor/browse paths from a well-meaning future refactor that replaces the checks withinstanceof.
Do not hard code the base data type iterals (like "date"), instead use the BaseDataType.Type enum.
#11 Updated by Razvan-Nicolae Chichirau 3 months ago
- % Done changed from 90 to 100
- Status changed from WIP to Review
Hynek Cihlar wrote:
- [MINOR] style
ThinClient.viewWorker: The new 6-arg overload parametersint windowIdandboolean skipScreenBufferlack thefinalmodifier while the other parameters (widget,frameBuf,widgetIds,isView) are declaredfinal. The newly-addedskipScreenBufferparameter continues the inconsistency instead of following the prevailing style.
The only parameter which is not defined as final from the old code is windowId, but that's because it is assigned in the method. I've only modified skipScreenBuffer to be final.
- [MINOR] functional
AbstractWidget.widgetRealized: The unconditionalsetState(ScreenBuffer.CHANGED)applies to everyDataContainerimplementation, including widgets likeTreeGuiImpl, which are not mentioned in the scope. Is this expected? Should the scope of the change be explicitly controlled?
The change is meant for all widgets that have a SCREEN-VALUE, so yes, this is expected. TreeGuiImpl is not something from 4GL AFAIK, but since it is implementing DataContainer, it should have a SCREEN-VALUE or some form of internal data value.
- [MINOR] functional
FillIn.shouldDisplayBlanks: The state test uses strict equality (state != UNINITIALIZED && state != UNCHANGED).ScreenBuffertreats state as bit flags (UNINITIALIZED=1,CHANGED=2,ENTERED=4,STATE_MAX=7) and the codebase elsewhere composes them with|=(e.g.swr.state |= ENTERED). Composite states such asUNINITIALIZED | ENTEREDwould fail both equality checks. The priorisUninitializedused the same pattern so this is latent rather than a new regression, but bitwise checks ((state & UNINITIALIZED) != 0) would be more defensive and match existingisUninitialized/isChangedhelpers.
Since ScreenBuffer.UNCHANGED is 0, this is only possible for UNINITIALIZED. Solved.
- [MINOR] functional
FillInImpl.draw: The old inline check gated only onuninit && !(DateFormat && enabled). The newshouldDisplayBlanks()additionally returnsfalsewhenappVar != null && !appVar.isUnknown(). A CHUI FILL-IN inUNINITIALIZEDstate but with a known variable value will now render its value where previously it rendered blanks. Please check this case.
That's not true. The old inline was uninit && (var == null || var.isUnknown()) && !(DateFormat && enabled). So, if the appVar is not unknown, the whole thing will return false, which is also happening with 10938a.
The other bullet points not mentioned were solved. Please check 10938a/rev. 16517.
#12 Updated by Hynek Cihlar 3 months ago
- Status changed from Review to WIP
- % Done changed from 100 to 90
Code review 10938a revisions 16514..16517
There is a regressin, please see the following 4GL sample:
/*
* With the new widgetRealized() setting CHANGED on every DataContainer
* and Frame.resetChanged() flipping CHANGED -> UNCHANGED, a fresh paint
* that observes the post-resetChanged state with an unknown variable now
* suppresses the format-output of unknown ("?") and paints blanks.
*
* Test layout (three FILL-INs in one frame):
* fUnknown INITIAL ? - bound variable is unknown
* fAssignedQ INITIAL 999, then = ? - bound variable is unknown after
* cycling through a known value
* fKnown INITIAL 12345 - control / sanity field
*
* Sequence executed:
* 1. DISPLAY frame -> initial paint (state cycles UNINIT -> UNCHANGED ->
* CHANGED via widgetRealized -> UNCHANGED via resetChanged).
* 2. HIDE / VIEW frame -> visibility toggle (does NOT re-fire
* widgetRealized because config.realized stays true).
* 3. Mutate :fgcolor on the two unknown-variable FILL-INs -> forces a
* widget invalidation and a fresh draw call. State is still UNCHANGED
* from step 1, variable is still unknown. shouldDisplayBlanks()
* observes (state == UNCHANGED, var == ?) and returns true: the
* patched draw skips text output and paints blanks.
*
* Observed outcome on 10938a:
* fUnknown -> BLANK (regression)
* fAssignedQ -> BLANK (regression)
* fKnown -> "12,345" (rendered in red after :fgcolor change)
*
* Trunk (and native OE):
* fUnknown -> "?"
* fAssignedQ -> "?"
* fKnown -> "12,345"
*/
define variable fUnknown as integer format "->,>>>,>>9" no-undo initial ?.
define variable fAssignedQ as integer format "->,>>>,>>9" no-undo initial 999.
define variable fKnown as integer format "->,>>>,>>9" no-undo initial 12345.
define variable svUnknown as character no-undo.
define variable svAssignedQ as character no-undo.
define variable svKnown as character no-undo.
fAssignedQ = ?.
define frame f1
fUnknown label "Never assigned"
fAssignedQ label "Assigned unknown"
fKnown label "Known value"
with side-labels title "Issue 10938 - shouldDisplayBlanks regression"
size 60 by 8.
display fUnknown fAssignedQ fKnown with frame f1.
hide frame f1 no-pause.
view frame f1.
/* Force a fresh draw call with widget already at UNCHANGED. The :fgcolor
* change invalidates the widget and re-runs FillInGuiImpl.draw(). At this
* point widgetRealized() does not re-fire (config.realized is already
* true), so state stays UNCHANGED and shouldDisplayBlanks() returns true
* for the unknown-variable FILL-INs. */
fUnknown:fgcolor in frame f1 = 4.
fAssignedQ:fgcolor in frame f1 = 4.
svUnknown = string(fUnknown:screen-value in frame f1).
svAssignedQ = string(fAssignedQ:screen-value in frame f1).
svKnown = string(fKnown:screen-value in frame f1).
output to "issue10938_screen_values.txt".
put unformatted "fUnknown SCREEN-VALUE = [" svUnknown "]" skip.
put unformatted "fAssignedQ SCREEN-VALUE = [" svAssignedQ "]" skip.
put unformatted "fKnown SCREEN-VALUE = [" svKnown "]" skip.
output close.
pause.
quit.
#13 Updated by Razvan-Nicolae Chichirau 3 months ago
Hynek, sorry, you are 100% right. The ScreenBuffer.UNCHANGED check from shouldDisplayBlanks() was added as a result of testing a customer app and spotting some regressions. Now that I've dug deeper into the source code, I've realized we should not modify the old isUnitialized(). The regression which 10938a exposes is highlighted by this testcase:
def frame f with size 100 by 20.
def var fi as handle.
create fill-in fi
ASSIGN
FRAME = FRAME f:handle
WIDTH = 20
FORMAT = "x(15)"
row = 2
col = 1.
enable all with frame f.
wait-for window-close of current-window.
In 4GL and trunk the fill-in is empty, but with 10938a the ? is displayed. Trunk accidentally displays the empty string because, although the widget was realized, its state is still UNINITIALIZED, which is wrong. With 10938a the state becomes UNCHANGED but for some reason dynamic fill-ins will be instantiated with unknown appVar, hence why ? appears.
Question: Do you want to treat this bug here or have it resolved in another separate task?
#14 Updated by Hynek Cihlar 3 months ago
Razvan-Nicolae Chichirau wrote:
Question: Do you want to treat this bug here or have it resolved in another separate task?
Since this is a regression from the changes, it should be addressed here.
#15 Updated by Razvan-Nicolae Chichirau 3 months ago
- Status changed from WIP to Review
- % Done changed from 90 to 100
Commited the fix to 10938a/rev. 16518. Your testcase and the customer app regressions are now solved.
Speaking of the appVar.setUnknown() deletion from FillIn, I've checked switching between the data-types for text, entry field and fill-in widgets, GUI + ChUI. None rendered the unknown value on the UI. Please note that the widget's SCREEN-VALUE attribute might be different from what the 4GL UI is rendering. This change is mainly targeting the UI rendering. What is actually passed as the SCREEN-VALUE to the server is handled in ThinClient.getScreenBuffer().
Hynek: Please review.
#16 Updated by Hynek Cihlar about 2 months ago
- % Done changed from 100 to 90
- Status changed from Review to WIP
Code review 10938a revisions 16514..16518
Javadoc for FillIn.shouldDisplayBlanks mentions the method is only intended for FillInGuiImpl and FillInImpl but FillIn is also inherited by other widget classes.
Also please check the following points:
- [MAJOR] functional
ThinClient.getScreenBuffer: The change fromnew character(((FillIn) comp).getText())tocharacter.of(fillIn.getText())for DATE/DATETIME/DATETIME-TZ fill-ins interns every dynamic, user-formatted date string into the JVM string pool viavalue.intern()and stores it in theCHARACTER_CONSTANTSidentity-hash cache until that cache fills. The factory's own javadoc explicitly warns "The keys in the constant map will be interned - use with care" and "The result needs to be assigned to a real character instance, and not cached in other maps" — exactly the misuse here, sincegetText()returns the formatted screen value which varies with every keystroke and format mask. Usenew character(fillIn.getText())for dynamic text; reservecharacter.offor known constants.
- [MAJOR] functional
FillIn.setDataType: RemovingappVar.setUnknown()unconditionally leavesappVarinDisplayFormat.instanceOfType()'s default-initialized state for non-date types (integer(0),character(""),logical(false),decimal(0.0),int64(0L),recid(0),longchar("")); onlydate()/datetime()/datetimetz()default to unknown. Triggered wheneversetDataTyperuns — i.e. fromafterConfigUpdateon every config update whereappVar.getTypeName()differs fromconfig.dataType. DownstreamisUnknown()consumers (notablyshouldDisplayBlanks'swidgetVariable != null && !widgetVariable.isUnknown()guard, used by bothFillInImpl.drawandFillInGuiImpl; and thesetValue(unknown, ...)override propagation) now observe a known default where they previously observed unknown. The Redmine note "When changing the date type do not make the app variable unknown" misrepresents the scope: the change has no date-type guard and affects all data types. Either narrow the code to dates only (e.g.if (appVar instanceof date) appVar.setUnknown();), or update the changelog and tests to acknowledge the cross-type rendering implications.
- [MAJOR] functional
FillInImpl.draw: The CHUI-side blanks check is tightened. The old inlineuninitwas solelygetState() ScreenBuffer.UNINITIALIZEDwith no variable check. The new sharedFillIn.shouldDisplayBlanksadditionally requiresgetVariable() null || getVariable().isUnknown(). Reachable trigger: a CHUI character/integer/decimal FillIn that has been assigned a known value via DISPLAY/setValue, followed byCLEAR FRAME(Frame.clearWorker→clearComponents→clearWidget→AbstractWidget.clearWidgetsets state toUNINITIALIZED;FillIn.resetdoes not clearappVar). On the nextdrawthe old code rendered blanks; the new code renders the stale value. The asymmetry between CHUI (no variable check) and GUI (variable check) in the old code is removed; the unification is coordinated with the new SCREEN-VALUE emission inThinClient, but the CHUI rendering behavior change is real and not called out in the changelog.
- [MAJOR] functional
FillInGuiImpl.draw: With the newAbstractWidget.widgetRealized()unconditionally callingsetState(ScreenBuffer.CHANGED)for anyDataContainer, andAbstractWidget.setStateoverwriting (not OR-merging) the state byte, by the timedrawruns the state has noUNINITIALIZEDbit andshouldDisplayBlanks()returns false at its first guard. Combined with the removal ofappVar.setUnknown()inFillIn.setDataType, a typed-but-unset static fill-in (integer/int64/decimal/logical/character/...) first realized viasetVisible(true)before any value assignment now renders its data type's default value (e.g. "0" forinteger, "no" forlogical) instead of blanks. The new server-side push inThinClient.getScreenBufferthat emitscharacter.of("")is also unreachable for this case because it inspectsstate & ~CHANGED, which collapses pure-CHANGEDback toUNCHANGED(still noUNINITIALIZEDbit). Either restoreappVar.setUnknown()inFillIn.setDataType(and preserve the dynamic-fill-in?rendering by other means), or makewidgetRealized()OR-merge:setState((byte)(getState() | ScreenBuffer.CHANGED)).
- [MAJOR] functional
FillInGuiImpl.initSelection: WithwidgetRealized()overwriting the widget state toCHANGEDfor everyDataContainer, andsetDataTypeno longer callingappVar.setUnknown(), the previous "uninitialized" classification used byinitSelectionno longer holds. In particularBrowse.createEditingFillInsetsUNINITIALIZEDbeforesetVisible(true), which then realizes the widget and clobbers the state toCHANGED; wheninitSelection()runs immediately afterwards,shouldDisplayBlanks()returns false,getText(true)produces the formatted default value (e.g." 0"for integer),TRAILING_SPACES_REGEXPyields a non-zerolastOffset, and the code pre-selects content instead of callinginvalidateSelection()as it did when the oldisUninitialized()returned true. Either reordersetState(UNINITIALIZED)aftersetVisible(true)inBrowse.createEditingFillIn, gate thesetState(CHANGED)inwidgetRealized()to skip widgets whose explicit pre-realize state isUNINITIALIZED, or haveinitSelectionconsult the underlying variable's unknown-ness independently of state.
- [MAJOR] functional
ThinClient.getScreenBuffer: The new empty-string emission branch for legacy FILL-INs is dead code. The outer guard at the screen-value collection site requirescomp.getState() ScreenBuffer.CHANGED(exact equality), andAbstractWidget.setState(byte)replaces rather than OR-merging the state byte — confirmed by inspection of allsetState(...)call sites incom.goldencode.p2j.ui, none of which OR existing state bits. Furthermore, the newAbstractWidget.widgetRealized()unconditionally callssetState(ScreenBuffer.CHANGED)for everyDataContainerat realization, wiping any priorUNINITIALIZED. ConsequentlyfillIn.getState() & ~ScreenBuffer.CHANGEDis always0when the inner branch runs, andshouldDisplayBlanks(0)returns false at its first guard(state & UNINITIALIZED) 0. Fix: either OR-merge inwidgetRealized()(setState((byte)(getState() | ScreenBuffer.CHANGED))) and drop the misleading& ~CHANGEDmask, or relax the outercomp.getState() ScreenBuffer.CHANGEDguard to a bitmask test. As written, the feature for emitting blanks on never-assigned realized legacy FILL-INs over SCREEN-VALUE does not function.
- [MINOR] functional
AbstractWidget.widgetRealized: The unconditionalsetState(ScreenBuffer.CHANGED)for allDataContainerwidgets is a replace-style call (perAbstractWidget.setState). Any priorUNINITIALIZEDbit is lost. Trigger: everyDataContainerrealization. This is the root cause of the dead& ~CHANGEDbranch inThinClient.getScreenBufferand contributes to the rendering regressions inFillInGuiImpl.draw/initSelection/mousePressed. Either OR-merge theCHANGEDbit (setState((byte)(getState() | ScreenBuffer.CHANGED))), or remove the misleading& ~CHANGEDmasking downstream — pick one to remove the inconsistency.
- [MINOR] functional
FillInGuiImpl.mousePressed:getUIText()now returnsgetText()(formatted default value) instead of "" for an enabled fill-in that has been realized but never assigned a value, becausewidgetRealized()overwrites the state toCHANGED(clearingUNINITIALIZED) andsetDataTypeno longer marksappVarunknown. The double-click branch (e.getClickCount() 2 && !content.isEmpty()) consequently activates word selection on default-value content (e.g., "0" for an integer fill-in, the date separators for a disabled date fill-in, padded blanks for a character format) and publishes it viagd.setCurrentSelection(getSelectedText()). Pre-fix this branch was dead for never-assigned fill-ins.
#17 Updated by Razvan-Nicolae Chichirau about 2 months ago
- Status changed from WIP to Review
- % Done changed from 90 to 100
Hynek Cihlar wrote:
Code review 10938a revisions 16514..16518
Javadoc for
FillIn.shouldDisplayBlanksmentions the method is only intended forFillInGuiImplandFillInImplbutFillInis also inherited by other widget classes.
Removed this entirely. There are no explicit usages of shouldDisplayBlanks() in FillIn subclasses other than legacy fill-ins. For subclasses that do not override methods using shouldDisplayBlanks(), the trunk flow is preserved as this method is still invoked, contrary to that javadoc.
Also please check the following points:
- [MAJOR] functional
ThinClient.getScreenBuffer: The change fromnew character(((FillIn) comp).getText())tocharacter.of(fillIn.getText())for DATE/DATETIME/DATETIME-TZ fill-ins interns every dynamic, user-formatted date string into the JVM string pool viavalue.intern()and stores it in theCHARACTER_CONSTANTSidentity-hash cache until that cache fills. The factory's own javadoc explicitly warns "The keys in the constant map will be interned - use with care" and "The result needs to be assigned to a real character instance, and not cached in other maps" — exactly the misuse here, sincegetText()returns the formatted screen value which varies with every keystroke and format mask. Usenew character(fillIn.getText())for dynamic text; reservecharacter.offor known constants.
Reverted the change.
- [MAJOR] functional
FillIn.setDataType: RemovingappVar.setUnknown()unconditionally leavesappVarinDisplayFormat.instanceOfType()'s default-initialized state for non-date types (integer(0),character(""),logical(false),decimal(0.0),int64(0L),recid(0),longchar("")); onlydate()/datetime()/datetimetz()default to unknown. Triggered wheneversetDataTyperuns — i.e. fromafterConfigUpdateon every config update whereappVar.getTypeName()differs fromconfig.dataType. DownstreamisUnknown()consumers (notablyshouldDisplayBlanks'swidgetVariable != null && !widgetVariable.isUnknown()guard, used by bothFillInImpl.drawandFillInGuiImpl; and thesetValue(unknown, ...)override propagation) now observe a known default where they previously observed unknown. The Redmine note "When changing the date type do not make the app variable unknown" misrepresents the scope: the change has no date-type guard and affects all data types. Either narrow the code to dates only (e.g.if (appVar instanceof date) appVar.setUnknown();), or update the changelog and tests to acknowledge the cross-type rendering implications.
I've tested fill-ins widgets when enabled/disabled on GUI + ChUI. None rendered the unknown value on either the UI or in the :SCREEN-VALUE attribute. In addition, :INPUT-VALUE is unknown only for dates, meanwhile the other data types have their default value, so the appVar.setUnknown() in the general case really does not make any sense. I've also tried, as much as it was possible, to change the DATA-TYPE of the widget to another, different type. Received the same result.
- [MAJOR] functional
FillInImpl.draw: The CHUI-side blanks check is tightened. The old inlineuninitwas solelygetState() ScreenBuffer.UNINITIALIZEDwith no variable check. The new sharedFillIn.shouldDisplayBlanksadditionally requiresgetVariable() null || getVariable().isUnknown(). Reachable trigger: a CHUI character/integer/decimal FillIn that has been assigned a known value via DISPLAY/setValue, followed byCLEAR FRAME(Frame.clearWorker→clearComponents→clearWidget→AbstractWidget.clearWidgetsets state toUNINITIALIZED;FillIn.resetdoes not clearappVar). On the nextdrawthe old code rendered blanks; the new code renders the stale value. The asymmetry between CHUI (no variable check) and GUI (variable check) in the old code is removed; the unification is coordinated with the new SCREEN-VALUE emission inThinClient, but the CHUI rendering behavior change is real and not called out in the changelog.
Let's say we keep the ChUI version for shouldDisplayBlanks(), without the appVar check. If we are clearing the frame, blanks will be shown on UI. If we'll insert a key, that character will still be appended to the stale fill-in value. So for example the pre-clear value was "abc", after clear it is "", but when inserting '1' the shown value will be "1abc". As such, the fault here is on the CLEAR implementation. Updated the changelog of FillInImpl to reflect the modification.
- [MAJOR] functional
FillInGuiImpl.draw: With he newAbstractWidget.widgetRealized()unconditionally callingsetState(ScreenBuffer.CHANGED)for anyDataContainer, andAbstractWidget.setStateoverwriting (not OR-merging) the state byte, by the timedrawruns the state has noUNINITIALIZEDbit andshouldDisplayBlanks()returns false at its first guard. Combined with the removal ofappVar.setUnknown()inFillIn.setDataType, a typed-but-unset static fill-in (integer/int64/decimal/logical/character/...) first realized viasetVisible(true)before any value assignment now renders its data type's default value (e.g. "0" forinteger, "no" forlogical) instead of blanks. The new server-side push inThinClient.getScreenBufferthat emitscharacter.of("")is also unreachable for this case because it inspectsstate & ~CHANGED, which collapses pure-CHANGEDback toUNCHANGED(still noUNINITIALIZEDbit). Either restoreappVar.setUnknown()inFillIn.setDataType(and preserve the dynamic-fill-in?rendering by other means), or makewidgetRealized()OR-merge:setState((byte)(getState() | ScreenBuffer.CHANGED)).
This would break the current TC.getScreenBuffer() which uses strict equality checks. Also, freshly realized widgets display blanks because of the data presentation buffer, and not the default value. This is true only for numeric based presentation, but that is solved by #11170.
- [MAJOR] functional
FillInGuiImpl.initSelection: WithwidgetRealized()overwriting the widget state toCHANGEDfor everyDataContainer, andsetDataTypeno longer callingappVar.setUnknown(), the previous "uninitialized" classification used byinitSelectionno longer holds. In particularBrowse.createEditingFillInsetsUNINITIALIZEDbeforesetVisible(true), which then realizes the widget and clobbers the state toCHANGED; wheninitSelection()runs immediately afterwards,shouldDisplayBlanks()returns false,getText(true)produces the formatted default value (e.g." 0"for integer),TRAILING_SPACES_REGEXPyields a non-zerolastOffset, and the code pre-selects content instead of callinginvalidateSelection()as it did when the oldisUninitialized()returned true. Either reordersetState(UNINITIALIZED)aftersetVisible(true)inBrowse.createEditingFillIn, gate thesetState(CHANGED)inwidgetRealized()to skip widgets whose explicit pre-realize state isUNINITIALIZED, or haveinitSelectionconsult the underlying variable's unknown-ness independently of state.
getText(true) will return the empty string because the internal data presentation has an empty buffer. As such, the selection will be invalidated.
- [MAJOR] functional
ThinClient.getScreenBuffer: The new empty-string emission branch for legacy FILL-INs is dead code. The outer guard at the screen-value collection site requirescomp.getState() ScreenBuffer.CHANGED(exact equality), andAbstractWidget.setState(byte)replaces rather than OR-merging the state byte — confirmed by inspection of allsetState(...)call sites incom.goldencode.p2j.ui, none of which OR existing state bits. Furthermore, the newAbstractWidget.widgetRealized()unconditionally callssetState(ScreenBuffer.CHANGED)for everyDataContainerat realization, wiping any priorUNINITIALIZED. ConsequentlyfillIn.getState() & ~ScreenBuffer.CHANGEDis always0when the inner branch runs, andshouldDisplayBlanks(0)returns false at its first guard(state & UNINITIALIZED) 0. Fix: either OR-merge inwidgetRealized()(setState((byte)(getState() | ScreenBuffer.CHANGED))) and drop the misleading& ~CHANGEDmask, or relax the outercomp.getState() ScreenBuffer.CHANGEDguard to a bitmask test. As written, the feature for emitting blanks on never-assigned realized legacy FILL-INs over SCREEN-VALUE does not function.
This is correct, removed the if branch.
- [MINOR] functional
AbstractWidget.widgetRealized: The unconditionalsetState(ScreenBuffer.CHANGED)for allDataContainerwidgets is a replace-style call (perAbstractWidget.setState). Any priorUNINITIALIZEDbit is lost. Trigger: everyDataContainerrealization. This is the root cause of the dead& ~CHANGEDbranch inThinClient.getScreenBufferand contributes to the rendering regressions inFillInGuiImpl.draw/initSelection/mousePressed. Either OR-merge theCHANGEDbit (setState((byte)(getState() | ScreenBuffer.CHANGED))), or remove the misleading& ~CHANGEDmasking downstream — pick one to remove the inconsistency.
If the widget was realized, we can not call it UNINITIALIZED anymore, so we need to remove that bit if it is present.
- [MINOR] functional
FillInGuiImpl.mousePressed:getUIText()now returnsgetText()(formatted default value) instead of "" for an enabled fill-in that has been realized but never assigned a value, becausewidgetRealized()overwrites the state toCHANGED(clearingUNINITIALIZED) andsetDataTypeno longer marksappVarunknown. The double-click branch (e.getClickCount() 2 && !content.isEmpty()) consequently activates word selection on default-value content (e.g., "0" for an integer fill-in, the date separators for a disabled date fill-in, padded blanks for a character format) and publishes it viagd.setCurrentSelection(getSelectedText()). Pre-fix this branch was dead for never-assigned fill-ins.
Again, getText() will return the empty string because of its data presentation.
In addition, the state usage is inconsistent across the project. For widgets, strict equality checks are used !=, == and setting the state will clear any previous bits. Meanwhile, ScreenBuffer checks and marks individual bits. I will stick with the equality checks for now.
Please check 10938a/rev. 16519.
#18 Updated by Hynek Cihlar about 1 month ago
- Status changed from Review to WIP
- % Done changed from 100 to 90
Code review 10938a revisions 16514..16519
- [CRITICAL] functional
FillIn.shouldDisplayBlanks: The state check onFillIn.java:3633is inverted. The early-return condition readsif (state ScreenBuffer.UNINITIALIZED || (widgetVariable != null && !widgetVariable.isUnknown())) return false;— i.e. "do not blank when state IS uninitialized". The method's own javadoc/comment on lines 3630-3631 documents the opposite intent ("If the widget is NOT uninitialized, or it has an internal value which is NOT unknown, it should display its internal state"), and the priorFillInGuiImpl.isUninitializedrequiredstate UNINITIALIZEDAND(var null || var.isUnknown())to consider blanking. As a side effect, the "Unknown and uninitialized DATE-based fill-ins should still display separators" block below is dead code for genuinelyUNINITIALIZEDdate fill-ins. Affects every caller:FillInImpl.draw,FillInGuiImpl.draw,FillInGuiImpl.initSelection,FillInGuiImpl.getUIText. Concrete triggers:Browse.regenerateInputFieldsetsstate = UNINITIALIZEDthen routes throughinitSelection→shouldDisplayBlanks;AbstractWidget.clearWidgetonCLEAR FRAME;Framedown-frame insert/scroll; and the defaultWidgetConfig.state = UNINITIALIZEDbeforewidgetRealizedruns for non-DataContainerpaths. Fix: changestate ScreenBuffer.UNINITIALIZEDtostate != ScreenBuffer.UNINITIALIZED.
See the sample showing the issue:
DEFINE VARIABLE cName AS CHARACTER FORMAT "x(15)" INITIAL "Hello World" NO-UNDO. DISPLAY cName WITH FRAME f. MESSAGE "Field shows its value. Press OK to CLEAR FRAME.". CLEAR FRAME f. MESSAGE "After CLEAR FRAME the field MUST be blank.".
- [MINOR] style
FillInImpl.draw: The descriptive comment block ("If the state is uninitialized, and the widget is not used for input, and it is not a date fill-in (where separators must still be displayed), then display blanks instead") still reflects only the pre-widening semantics. After the widening to also blankUNCHANGEDstates with null/unknownappVar, the comment is inaccurate. The identical comment inFillInGuiImpl.drawhas the same issue.
- [MINOR] style
FillIn.shouldDisplayBlanks: Visibility widened fromprivate(oldFillInGuiImpl.isUninitialized) topublic. The byte-arg overloadshouldDisplayBlanks(byte state)in particular is exposed publicly with no documented external use case;protectedwould be a tighter API surface.
#19 Updated by Razvan-Nicolae Chichirau about 1 month ago
- % Done changed from 90 to 100
- Status changed from WIP to Review
The inverted check was a typo when switching from bit to strict equality checks, my bad. Also fixed the 2 minor points.
Please check rev. 16520.
#20 Updated by Hynek Cihlar 21 days ago
- Status changed from Review to Internal Test
Code review 10938a, the changes look good.
#21 Updated by Razvan-Nicolae Chichirau 1 day ago
- Status changed from Internal Test to WIP
- % Done changed from 100 to 90
Regression found on ChUI regression tests:
def var fi2 as char initial "A". message UPDATE fi2 AUTO-RETURN FORMAT "x(8)".
The character displayed in the variable should be A, but in 10938a the screen buffers are not updated. The change that skips screen buffer updates during ENABLE is too restrictive, so I need to rethink the approach.
What I actually need is to ensure that every newly realized widget satisfies two conditions:
- Its state is set to
CHANGEDand stays the same until the end of the call so it is included in its frame's screen buffer. - Its frame's screen buffer is sent to the server. This would assure the state is updated on the server, and on the next server-client trip this widget should NOT be in the
UNINITIALIZEDstate.
My current idea is to do a "recording" of the widgets at the window level that are newly realized. When the ENABLE call finishes, the widget state should be modified again to CHANGED, and include its frame in the final screen buffer array, as it can be different from the frame that received ENABLED (e.g., an enable call of an root frame will realize all widgets from the child frame).