Skip to content

Commit d042971

Browse files
shai-almogclaude
andauthored
Fix #4912 form remains shaded after closing Toolbar side menu (#4913)
closeLeftSideMenu zeroed the Toolbar layered pane's bgTransparency and detached it once the dispose animation completed, but cnt.remove() does not by itself trigger a form-level repaint. The previous frame's shaded pixels (the dim backdrop painted while the menu was open) then lingered in the simulator and JS port render buffer until something else forced a redraw, so the underlying Form appeared darker than it should after the menu closed. The user's confirmed workaround was a manual form.revalidateLater() in response to the close. The fix moves that revalidateLater into detachToolbarLayeredPane: the host Form is captured before cnt.remove() severs its parent link, and revalidateLater is queued once the pane is gone so the next paint cycle overdraws any stale shaded pixels. Regression test added: ToolbarTest disables the dispose animation via reflection so the detach runs synchronously, then asserts the form is queued for revalidate after closeLeftSideMenu returns. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 16e63cc commit d042971

2 files changed

Lines changed: 92 additions & 0 deletions

File tree

CodenameOne/src/com/codename1/ui/Toolbar.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,9 +505,20 @@ private void detachToolbarLayeredPane(Container cnt) {
505505
if (rightSidemenuDialog != null && rightSidemenuDialog.isShowing()) {
506506
return;
507507
}
508+
// Capture the host form before remove() detaches cnt -- after
509+
// remove() cnt.getComponentForm() returns null.
510+
Form host = cnt.getComponentForm();
508511
Style s = cnt.getUnselectedStyle();
509512
s.setBgTransparency(0);
510513
cnt.remove();
514+
// Issue #4912: cnt.remove() does not by itself trigger a
515+
// form-level repaint, so the previous frame's shaded pixels
516+
// can linger in the simulator and JS render buffer until
517+
// something else forces a redraw. revalidateLater queues a
518+
// single relayout/repaint pass for the next paint cycle.
519+
if (host != null) {
520+
host.revalidateLater();
521+
}
511522
}
512523

513524
/// Returns the Toolbar title Component.

maven/core-unittests/src/test/java/com/codename1/ui/ToolbarTest.java

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,87 @@ void sideMenuAndOverflowCommands() {
208208
assertEquals(1, overflowInvocation[0], "Overflow command should be invoked");
209209
}
210210

211+
/// Regression test for issue #4912: after closing the hamburger
212+
/// side menu the underlying Form remained "shaded" in the
213+
/// simulator and the JS port. The Toolbar.class layered pane
214+
/// carries a dim backdrop (bgTransparency over bgColor=0) that
215+
/// is painted over the form while the menu is open. The dispose
216+
/// onDisposed callback detached the pane and zeroed the tint,
217+
/// but did not queue a form-level revalidate; the user's
218+
/// confirmed workaround was to call form.revalidateLater() in
219+
/// response to the close, and that workaround was promoted into
220+
/// detachToolbarLayeredPane.
221+
///
222+
/// The dispose animation is disabled via reflection so the
223+
/// detach runs synchronously inside closeLeftSideMenu, and the
224+
/// assertions check both that the pane is gone AND that the
225+
/// form has been added back to the revalidate queue so the
226+
/// next paint cycle overdraws any stale shaded pixels.
227+
@FormTest
228+
void closeLeftSideMenuClearsShadedBackdropAfterAnimation() throws Exception {
229+
implementation.setBuiltinSoundsEnabled(false);
230+
Toolbar.setOnTopSideMenu(true);
231+
232+
Form form = Display.getInstance().getCurrent();
233+
Toolbar toolbar = new Toolbar();
234+
form.setToolbar(toolbar);
235+
form.show();
236+
form.getAnimationManager().flush();
237+
flushSerialCalls();
238+
239+
toolbar.addCommandToSideMenu("Entry", null, evt -> { });
240+
241+
toolbar.openSideMenu();
242+
form.getAnimationManager().flush();
243+
flushSerialCalls();
244+
awaitAnimations(form);
245+
assertTrue(toolbar.isSideMenuShowing(), "Side menu should be showing after open");
246+
247+
Container pane = form.getFormLayeredPane(Toolbar.class, false);
248+
int openTransparency = pane.getUnselectedStyle().getBgTransparency() & 0xff;
249+
assertTrue(openTransparency > 0,
250+
"Backdrop pane should be tinted while menu is open (was " + openTransparency + ")");
251+
252+
// Disable the dispose animation so closeLeftSideMenu runs the
253+
// detach callback synchronously. The bug being tested is not
254+
// about animation timing -- it is about whether the form is
255+
// re-queued for layout/repaint once the dim pane is gone.
256+
java.lang.reflect.Field dialogField = Toolbar.class.getDeclaredField("sidemenuDialog");
257+
dialogField.setAccessible(true);
258+
Object dialog = dialogField.get(toolbar);
259+
java.lang.reflect.Method setAnimateShow = dialog.getClass().getMethod("setAnimateShow", boolean.class);
260+
setAnimateShow.invoke(dialog, false);
261+
262+
// Drain any pending revalidate state so we can detect a fresh
263+
// revalidate request triggered specifically by close.
264+
flushSerialCalls();
265+
boolean revalidatePendingBeforeClose = isFormInRevalidateQueue(form);
266+
267+
toolbar.closeLeftSideMenu();
268+
269+
assertNull(pane.getParent(),
270+
"Toolbar layered pane should be detached once the synchronous dispose runs");
271+
assertEquals(0, pane.getUnselectedStyle().getBgTransparency() & 0xff,
272+
"Backdrop tint must be cleared so a stale reference cannot re-shade the form (issue #4912)");
273+
assertFalse(toolbar.isSideMenuShowing(),
274+
"Side menu should no longer be reported as showing after synchronous close");
275+
276+
assertTrue(isFormInRevalidateQueue(form) && !revalidatePendingBeforeClose,
277+
"closeLeftSideMenu should queue a form revalidateLater after detaching the "
278+
+ "shaded backdrop pane (issue #4912 -- without this the form stays "
279+
+ "shaded until the user does something that forces a redraw)");
280+
}
281+
282+
private static boolean isFormInRevalidateQueue(Form form) throws Exception {
283+
java.lang.reflect.Field f = Form.class.getDeclaredField("pendingRevalidateQueue");
284+
f.setAccessible(true);
285+
Object queue = f.get(form);
286+
if (queue instanceof java.util.Collection) {
287+
return ((java.util.Collection<?>) queue).contains(form);
288+
}
289+
return false;
290+
}
291+
211292
/// Regression test for the JavaScript port "ghost side menu +
212293
/// previous preview visible as background" bug. closeLeftSideMenu
213294
/// used to synchronously detach the Toolbar's FormLayeredPane

0 commit comments

Comments
 (0)