Project

General

Profile

Bug #10938

Date FILL-IN: Incorrect SCREEN-VALUE after widget enablement

Added by Vladimir Tsichevski 8 months ago. Updated 1 day ago.

Status:
WIP
Priority:
Normal
Target version:
-
Start date:
Due date:
% Done:

90%

billable:
No
vendor_id:
GCD
case_num:
version_reported:
16292
version_resolved:
reviewer:
production:
No
env_name:
topics:

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-VALUE is empty ("").
  • After enablement: SCREEN-VALUE initializes to the format's placeholder delimiters (" / /").

Actual Behavior (FWD)

  • Before enablement (disabled): SCREEN-VALUE is empty ("").
  • After enablement: SCREEN-VALUE remains 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 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.

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, and RadioSet override widgetRealized() without calling super.widgetRealized(), so the new setState(ScreenBuffer.CHANGED) added in AbstractWidget is dead code for these DataContainer widgets. The Redmine summary states the fix should affect all DataContainer widgets.
  • [MAJOR] functional ComboBox.activate: The setState(ScreenBuffer.CHANGED) added for history entry 130 ("Modify the widget internal state to CHANGED when realized") is placed at the end of activate() - a drop-down interaction method - not in widgetRealized(). Furthermore, ComboBox.widgetRealized() does not call super.widgetRealized(), so the setState(CHANGED) added in AbstractWidget.widgetRealized() is also never reached. This means: (1) the intended realization-time state change never fires for ComboBox, defeating the purpose of history entry 130; and (2) every dropdown activation unconditionally marks the widget as CHANGED, 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 CHUI FillInImpl.draw() and GUI FillInGuiImpl.isUninitialized()) only displayed blanks for UNINITIALIZED state; the new shouldDisplayBlanks also returns true for UNCHANGED state (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. The if condition wraps the second operand at column 9 instead of aligning with the content after if ( (column 11). Same issue for the boolean isDate assignment continuation.
  • [MINOR] style ThinClient.collectScreenValues: Continuation line alignment at the multi-line if condition, one space short of aligning with the opening parenthesis.
  • [MINOR] functional FillInImpl.draw: The old CHUI code only exempted DateFormat from the blank-display logic; the new shouldDisplayBlanks() also exempts DatetimeFormat. 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: The setState(ScreenBuffer.CHANGED) added for history entry 130 ("Modify the widget internal state to CHANGED when realized") is placed at the end of activate() - a drop-down interaction method - not in widgetRealized(). Furthermore, ComboBox.widgetRealized() does not call super.widgetRealized(), so the setState(CHANGED) added in AbstractWidget.widgetRealized() is also never reached. This means: (1) the intended realization-time state change never fires for ComboBox, defeating the purpose of history entry 130; and (2) every dropdown activation unconditionally marks the widget as CHANGED, 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 CHUI FillInImpl.draw() and GUI FillInGuiImpl.isUninitialized()) only displayed blanks for UNINITIALIZED state; the new shouldDisplayBlanks also returns true for UNCHANGED state (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 exempted DateFormat from the blank-display logic; the new shouldDisplayBlanks() also exempts DatetimeFormat. 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 the preview && enabled path (around line 6577), viewWorker is invoked with the default skipScreenBuffer=false, which still triggers setScreenBuffers(frameBuf, true) inside viewWorker. Subsequently enableWorker -> processEnable now also calls setScreenBuffers(sb) unconditionally (the change from if (!enabled && sb != null) to if (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 preview viewWorker call should also pass skipScreenBuffer=true, or processEnable should still gate on !preview || !enabled.
  • [MINOR] style ThinClient.viewWorker: The new 6-arg overload parameters int windowId and boolean skipScreenBuffer lack the final modifier while the other parameters (widget, frameBuf, widgetIds, isView) are declared final. The newly-added skipScreenBuffer parameter continues the inconsistency instead of following the prevailing style.
  • [MINOR] style FillIn.shouldDisplayBlanks: The local variable appVar shadows the existing instance field appVar declared at the top of the class, which hurts readability.
  • [MINOR] style ThinClient.collectScreenValues: The cast (FillIn) comp is a raw-type cast assigned to a parameterized local FillIn fillIn, producing an unchecked-cast pattern. Use (FillIn) comp instead.
  • [MINOR] functional AbstractWidget.widgetRealized: The unconditional setState(ScreenBuffer.CHANGED) applies to every DataContainer implementation, including widgets like TreeGuiImpl, 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). ScreenBuffer treats 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 as UNINITIALIZED | ENTERED would fail both equality checks. The prior isUninitialized used the same pattern so this is latent rather than a new regression, but bitwise checks ((state & UNINITIALIZED) != 0) would be more defensive and match existing isUninitialized/isChanged helpers.
  • [MINOR] functional FillIn.shouldDisplayBlanks: The new helper exempts DatetimeFormat fill-ins from blanks when enabled; the old CHUI FillInImpl.draw() only exempted DateFormat. ThinClient.collectScreenValues still special-cases only dataType.equalsIgnoreCase("date") for the getText() retrieval, so datetime fill-ins fall into the else branch and may receive character.of("") when shouldDisplayBlanks returns 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 on uninit && !(DateFormat && enabled). The new shouldDisplayBlanks() additionally returns false when appVar != null && !appVar.isUnknown(). A CHUI FILL-IN in UNINITIALIZED state 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 check clazz FillInGuiImpl.class || clazz FillInImpl.class uses exact class equality to deliberately exclude subclasses (EntryFieldGuiImpl, LineEditor, BrowseFilterEntryField, all extending FillInGuiImpl). A comment explaining why instanceof is deliberately avoided would protect the combo/editor/browse paths from a well-meaning future refactor that replaces the checks with instanceof.

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 parameters int windowId and boolean skipScreenBuffer lack the final modifier while the other parameters (widget, frameBuf, widgetIds, isView) are declared final. The newly-added skipScreenBuffer parameter 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 unconditional setState(ScreenBuffer.CHANGED) applies to every DataContainer implementation, including widgets like TreeGuiImpl, 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). ScreenBuffer treats 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 as UNINITIALIZED | ENTERED would fail both equality checks. The prior isUninitialized used the same pattern so this is latent rather than a new regression, but bitwise checks ((state & UNINITIALIZED) != 0) would be more defensive and match existing isUninitialized/isChanged helpers.

Since ScreenBuffer.UNCHANGED is 0, this is only possible for UNINITIALIZED. Solved.

  • [MINOR] functional FillInImpl.draw: The old inline check gated only on uninit && !(DateFormat && enabled). The new shouldDisplayBlanks() additionally returns false when appVar != null && !appVar.isUnknown(). A CHUI FILL-IN in UNINITIALIZED state 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 from new character(((FillIn) comp).getText()) to character.of(fillIn.getText()) for DATE/DATETIME/DATETIME-TZ fill-ins interns every dynamic, user-formatted date string into the JVM string pool via value.intern() and stores it in the CHARACTER_CONSTANTS identity-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, since getText() returns the formatted screen value which varies with every keystroke and format mask. Use new character(fillIn.getText()) for dynamic text; reserve character.of for known constants.
  • [MAJOR] functional FillIn.setDataType: Removing appVar.setUnknown() unconditionally leaves appVar in DisplayFormat.instanceOfType()'s default-initialized state for non-date types (integer(0), character(""), logical(false), decimal(0.0), int64(0L), recid(0), longchar("")); only date()/datetime()/datetimetz() default to unknown. Triggered whenever setDataType runs — i.e. from afterConfigUpdate on every config update where appVar.getTypeName() differs from config.dataType. Downstream isUnknown() consumers (notably shouldDisplayBlanks's widgetVariable != null && !widgetVariable.isUnknown() guard, used by both FillInImpl.draw and FillInGuiImpl; and the setValue(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 inline uninit was solely getState() ScreenBuffer.UNINITIALIZED with no variable check. The new shared FillIn.shouldDisplayBlanks additionally requires getVariable() null || getVariable().isUnknown(). Reachable trigger: a CHUI character/integer/decimal FillIn that has been assigned a known value via DISPLAY/setValue, followed by CLEAR FRAME (Frame.clearWorkerclearComponentsclearWidgetAbstractWidget.clearWidget sets state to UNINITIALIZED; FillIn.reset does not clear appVar). On the next draw the 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 in ThinClient, but the CHUI rendering behavior change is real and not called out in the changelog.
  • [MAJOR] functional FillInGuiImpl.draw: With the new AbstractWidget.widgetRealized() unconditionally calling setState(ScreenBuffer.CHANGED) for any DataContainer, and AbstractWidget.setState overwriting (not OR-merging) the state byte, by the time draw runs the state has no UNINITIALIZED bit and shouldDisplayBlanks() returns false at its first guard. Combined with the removal of appVar.setUnknown() in FillIn.setDataType, a typed-but-unset static fill-in (integer/int64/decimal/logical/character/...) first realized via setVisible(true) before any value assignment now renders its data type's default value (e.g. "0" for integer, "no" for logical) instead of blanks. The new server-side push in ThinClient.getScreenBuffer that emits character.of("") is also unreachable for this case because it inspects state & ~CHANGED, which collapses pure-CHANGED back to UNCHANGED (still no UNINITIALIZED bit). Either restore appVar.setUnknown() in FillIn.setDataType (and preserve the dynamic-fill-in ? rendering by other means), or make widgetRealized() OR-merge: setState((byte)(getState() | ScreenBuffer.CHANGED)).
  • [MAJOR] functional FillInGuiImpl.initSelection: With widgetRealized() overwriting the widget state to CHANGED for every DataContainer, and setDataType no longer calling appVar.setUnknown(), the previous "uninitialized" classification used by initSelection no longer holds. In particular Browse.createEditingFillIn sets UNINITIALIZED before setVisible(true), which then realizes the widget and clobbers the state to CHANGED; when initSelection() runs immediately afterwards, shouldDisplayBlanks() returns false, getText(true) produces the formatted default value (e.g. " 0" for integer), TRAILING_SPACES_REGEXP yields a non-zero lastOffset, and the code pre-selects content instead of calling invalidateSelection() as it did when the old isUninitialized() returned true. Either reorder setState(UNINITIALIZED) after setVisible(true) in Browse.createEditingFillIn, gate the setState(CHANGED) in widgetRealized() to skip widgets whose explicit pre-realize state is UNINITIALIZED, or have initSelection consult 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 requires comp.getState() ScreenBuffer.CHANGED (exact equality), and AbstractWidget.setState(byte) replaces rather than OR-merging the state byte — confirmed by inspection of all setState(...) call sites in com.goldencode.p2j.ui, none of which OR existing state bits. Furthermore, the new AbstractWidget.widgetRealized() unconditionally calls setState(ScreenBuffer.CHANGED) for every DataContainer at realization, wiping any prior UNINITIALIZED. Consequently fillIn.getState() & ~ScreenBuffer.CHANGED is always 0 when the inner branch runs, and shouldDisplayBlanks(0) returns false at its first guard (state & UNINITIALIZED) 0. Fix: either OR-merge in widgetRealized() (setState((byte)(getState() | ScreenBuffer.CHANGED))) and drop the misleading & ~CHANGED mask, or relax the outer comp.getState() ScreenBuffer.CHANGED guard 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 unconditional setState(ScreenBuffer.CHANGED) for all DataContainer widgets is a replace-style call (per AbstractWidget.setState). Any prior UNINITIALIZED bit is lost. Trigger: every DataContainer realization. This is the root cause of the dead & ~CHANGED branch in ThinClient.getScreenBuffer and contributes to the rendering regressions in FillInGuiImpl.draw/initSelection/mousePressed. Either OR-merge the CHANGED bit (setState((byte)(getState() | ScreenBuffer.CHANGED))), or remove the misleading & ~CHANGED masking downstream — pick one to remove the inconsistency.
  • [MINOR] functional FillInGuiImpl.mousePressed: getUIText() now returns getText() (formatted default value) instead of "" for an enabled fill-in that has been realized but never assigned a value, because widgetRealized() overwrites the state to CHANGED (clearing UNINITIALIZED) and setDataType no longer marks appVar unknown. 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 via gd.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.shouldDisplayBlanks mentions the method is only intended for FillInGuiImpl and FillInImpl but FillIn is 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 from new character(((FillIn) comp).getText()) to character.of(fillIn.getText()) for DATE/DATETIME/DATETIME-TZ fill-ins interns every dynamic, user-formatted date string into the JVM string pool via value.intern() and stores it in the CHARACTER_CONSTANTS identity-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, since getText() returns the formatted screen value which varies with every keystroke and format mask. Use new character(fillIn.getText()) for dynamic text; reserve character.of for known constants.

Reverted the change.

  • [MAJOR] functional FillIn.setDataType: Removing appVar.setUnknown() unconditionally leaves appVar in DisplayFormat.instanceOfType()'s default-initialized state for non-date types (integer(0), character(""), logical(false), decimal(0.0), int64(0L), recid(0), longchar("")); only date()/datetime()/datetimetz() default to unknown. Triggered whenever setDataType runs — i.e. from afterConfigUpdate on every config update where appVar.getTypeName() differs from config.dataType. Downstream isUnknown() consumers (notably shouldDisplayBlanks's widgetVariable != null && !widgetVariable.isUnknown() guard, used by both FillInImpl.draw and FillInGuiImpl; and the setValue(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 inline uninit was solely getState() ScreenBuffer.UNINITIALIZED with no variable check. The new shared FillIn.shouldDisplayBlanks additionally requires getVariable() null || getVariable().isUnknown(). Reachable trigger: a CHUI character/integer/decimal FillIn that has been assigned a known value via DISPLAY/setValue, followed by CLEAR FRAME (Frame.clearWorkerclearComponentsclearWidgetAbstractWidget.clearWidget sets state to UNINITIALIZED; FillIn.reset does not clear appVar). On the next draw the 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 in ThinClient, 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 new AbstractWidget.widgetRealized() unconditionally calling setState(ScreenBuffer.CHANGED) for any DataContainer, and AbstractWidget.setState overwriting (not OR-merging) the state byte, by the time draw runs the state has no UNINITIALIZED bit and shouldDisplayBlanks() returns false at its first guard. Combined with the removal of appVar.setUnknown() in FillIn.setDataType, a typed-but-unset static fill-in (integer/int64/decimal/logical/character/...) first realized via setVisible(true) before any value assignment now renders its data type's default value (e.g. "0" for integer, "no" for logical) instead of blanks. The new server-side push in ThinClient.getScreenBuffer that emits character.of("") is also unreachable for this case because it inspects state & ~CHANGED, which collapses pure-CHANGED back to UNCHANGED (still no UNINITIALIZED bit). Either restore appVar.setUnknown() in FillIn.setDataType (and preserve the dynamic-fill-in ? rendering by other means), or make widgetRealized() 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: With widgetRealized() overwriting the widget state to CHANGED for every DataContainer, and setDataType no longer calling appVar.setUnknown(), the previous "uninitialized" classification used by initSelection no longer holds. In particular Browse.createEditingFillIn sets UNINITIALIZED before setVisible(true), which then realizes the widget and clobbers the state to CHANGED; when initSelection() runs immediately afterwards, shouldDisplayBlanks() returns false, getText(true) produces the formatted default value (e.g. " 0" for integer), TRAILING_SPACES_REGEXP yields a non-zero lastOffset, and the code pre-selects content instead of calling invalidateSelection() as it did when the old isUninitialized() returned true. Either reorder setState(UNINITIALIZED) after setVisible(true) in Browse.createEditingFillIn, gate the setState(CHANGED) in widgetRealized() to skip widgets whose explicit pre-realize state is UNINITIALIZED, or have initSelection consult 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 requires comp.getState() ScreenBuffer.CHANGED (exact equality), and AbstractWidget.setState(byte) replaces rather than OR-merging the state byte — confirmed by inspection of all setState(...) call sites in com.goldencode.p2j.ui, none of which OR existing state bits. Furthermore, the new AbstractWidget.widgetRealized() unconditionally calls setState(ScreenBuffer.CHANGED) for every DataContainer at realization, wiping any prior UNINITIALIZED. Consequently fillIn.getState() & ~ScreenBuffer.CHANGED is always 0 when the inner branch runs, and shouldDisplayBlanks(0) returns false at its first guard (state & UNINITIALIZED) 0. Fix: either OR-merge in widgetRealized() (setState((byte)(getState() | ScreenBuffer.CHANGED))) and drop the misleading & ~CHANGED mask, or relax the outer comp.getState() ScreenBuffer.CHANGED guard 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 unconditional setState(ScreenBuffer.CHANGED) for all DataContainer widgets is a replace-style call (per AbstractWidget.setState). Any prior UNINITIALIZED bit is lost. Trigger: every DataContainer realization. This is the root cause of the dead & ~CHANGED branch in ThinClient.getScreenBuffer and contributes to the rendering regressions in FillInGuiImpl.draw/initSelection/mousePressed. Either OR-merge the CHANGED bit (setState((byte)(getState() | ScreenBuffer.CHANGED))), or remove the misleading & ~CHANGED masking 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 returns getText() (formatted default value) instead of "" for an enabled fill-in that has been realized but never assigned a value, because widgetRealized() overwrites the state to CHANGED (clearing UNINITIALIZED) and setDataType no longer marks appVar unknown. 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 via gd.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 on FillIn.java:3633 is inverted. The early-return condition reads if (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 prior FillInGuiImpl.isUninitialized required state UNINITIALIZED AND (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 genuinely UNINITIALIZED date fill-ins. Affects every caller: FillInImpl.draw, FillInGuiImpl.draw, FillInGuiImpl.initSelection, FillInGuiImpl.getUIText. Concrete triggers: Browse.regenerateInputField sets state = UNINITIALIZED then routes through initSelectionshouldDisplayBlanks; AbstractWidget.clearWidget on CLEAR FRAME; Frame down-frame insert/scroll; and the default WidgetConfig.state = UNINITIALIZED before widgetRealized runs for non-DataContainer paths. Fix: change state ScreenBuffer.UNINITIALIZED to state != 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 blank UNCHANGED states with null/unknown appVar, the comment is inaccurate. The identical comment in FillInGuiImpl.draw has the same issue.
  • [MINOR] style FillIn.shouldDisplayBlanks: Visibility widened from private (old FillInGuiImpl.isUninitialized) to public. The byte-arg overload shouldDisplayBlanks(byte state) in particular is exposed publicly with no documented external use case; protected would 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 CHANGED and 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 UNINITIALIZED state.

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).

Also available in: Atom PDF