diff --git a/CodenameOne/src/com/codename1/components/InteractionDialog.java b/CodenameOne/src/com/codename1/components/InteractionDialog.java index cbd2ebf2fd..dbedbc3ceb 100644 --- a/CodenameOne/src/com/codename1/components/InteractionDialog.java +++ b/CodenameOne/src/com/codename1/components/InteractionDialog.java @@ -839,23 +839,48 @@ private void showPopupDialogImpl(Rectangle rect, boolean bias) { } } } - if (rect.getY() + rect.getHeight() < availableHeight / 2) { + // Pick the side of the rect (above vs. below) the popup goes + // on. The original logic chose purely by which half of the + // screen the rect sat in, which placed the popup ON TOP of + // the rect whenever it straddled the midline -- the symptom + // in #5028 (popup covers target) and #5029 (CSSBorder.Arrow + // can't pick a consistent direction so the tip renders on + // the wrong edge). Prefer whichever side fits the popup's + // preferred height, falling back to the larger side. The + // historical "over the rect" branches are kept as a last + // resort for the degenerate case where neither side has any + // room at all. + int spaceAbove = Math.max(0, rect.getY()); + int spaceBelow = Math.max(0, availableHeight - rect.getY() - rect.getHeight()); + boolean placeBelow; + if (spaceBelow >= prefHeight) { + placeBelow = true; + } else if (spaceAbove >= prefHeight) { + placeBelow = false; + } else if (spaceBelow >= spaceAbove) { + placeBelow = spaceBelow > 0; + } else { + placeBelow = false; + } + if (placeBelow && spaceBelow > 0) { // popup downwards y = rect.getY() + rect.getHeight(); - int height = Math.min(prefHeight, Math.max(0, availableHeight - y)); + int height = Math.min(prefHeight, spaceBelow); padOrientation(contentPaneStyle, TOP, 1); show(Math.max(0, y), Math.max(0, availableHeight - height - y), Math.max(0, x), Math.max(0, availableWidth - width - x)); padOrientation(contentPaneStyle, TOP, -1); - } else if (rect.getY() > availableHeight / 2) { + } else if (!placeBelow && spaceAbove > 0) { // popup upwards - int height = Math.min(prefHeight, rect.getY()); + int height = Math.min(prefHeight, spaceAbove); y = rect.getY() - height; padOrientation(contentPaneStyle, BOTTOM, 1); show(y, Math.max(0, availableHeight - rect.getY()), x, Math.max(0, availableWidth - width - x)); padOrientation(contentPaneStyle, BOTTOM, -1); } else if (rect.getY() < availableHeight / 2) { - // popup over aligned with top of rect, but inset a few mm + // popup over aligned with top of rect, but inset a few + // mm. Fallback for the truly degenerate case where the + // rect fills the viewport top-to-bottom. y = rect.getY() + CN.convertToPixels(3); int height = Math.min(prefHeight, availableHeight - y); diff --git a/maven/core-unittests/src/test/java/com/codename1/components/InteractionDialogTest.java b/maven/core-unittests/src/test/java/com/codename1/components/InteractionDialogTest.java index d0aa56acc3..2e6d387ddd 100644 --- a/maven/core-unittests/src/test/java/com/codename1/components/InteractionDialogTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/components/InteractionDialogTest.java @@ -97,6 +97,83 @@ void formModeUsesFormLayeredPane() { dialog.dispose(); } + @Test + void showPopupDialogStraddlingMidlineDoesNotOverlapTarget() { + // Regression for #5028: when the anchor rect straddles the + // vertical midline, the legacy placement logic fell through to a + // "popup over aligned with top of rect" branch that drew the + // popup ON TOP of the target (covering the Close button in the + // reporter's screenshot). The fix prefers above / below based on + // available space; the popup must end up entirely outside the + // target rect. + implementation.setDisplaySize(1080, 1920); + implementation.setPortrait(true); + Form form = new Form(new BorderLayout()); + implementation.setCurrentForm(form); + InteractionDialog dialog = new InteractionDialog(); + dialog.setAnimateShow(false); + dialog.addComponent(new Label("Popup body content")); + + // 60px target straddling the midline at y=960. + int targetHeight = 60; + int targetY = 1920 / 2 - targetHeight / 2; + Rectangle anchor = new Rectangle(490, targetY, 100, targetHeight); + dialog.showPopupDialog(anchor); + + int dlgTop = dialog.getAbsoluteY(); + int dlgBottom = dlgTop + dialog.getHeight(); + int targetBottom = targetY + targetHeight; + assertTrue(dialog.getHeight() > 0, "popup must have non-zero height"); + boolean overlaps = dlgTop < targetBottom && dlgBottom > targetY; + assertFalse(overlaps, + "#5028: popup [" + dlgTop + ".." + dlgBottom + + ") overlaps anchor [" + targetY + ".." + targetBottom + + ") -- expected the popup to land entirely above or below the rect"); + + dialog.dispose(); + } + + @Test + void showPopupDialogArrowDirectionConsistentWithPlacement() { + // Regression for #5029: with the popup ending up overlapping the + // target (the #5028 bug), CSSBorder.Arrow could not pick a + // consistent direction (cabsY straddles trackY..trackY+h) so the + // arrow tip rendered on the wrong edge. The arrow logic needs the + // popup to be either fully above or fully below the target; this + // test mirrors the reporter's geometry (target halfway down a + // tall column) and pins that invariant. + implementation.setDisplaySize(1080, 1920); + implementation.setPortrait(true); + Form form = new Form(new BorderLayout()); + implementation.setCurrentForm(form); + InteractionDialog dialog = new InteractionDialog(); + dialog.setAnimateShow(false); + dialog.addComponent(new Label("Popup body content")); + + // Target lives at y = available/2 - 1 (rect.getY() < availableHeight/2, + // rect.bottom > availableHeight/2). This is the exact case that + // used to hit the buggy "popup over aligned with top of rect" + // branch before the fix. + Rectangle anchor = new Rectangle(490, 1920 / 2 - 1, 100, 80); + dialog.showPopupDialog(anchor); + + int dlgTop = dialog.getAbsoluteY(); + int dlgBottom = dlgTop + dialog.getHeight(); + int targetTop = anchor.getY(); + int targetBottom = targetTop + anchor.getHeight(); + + boolean popupBelowTarget = dlgTop >= targetBottom; + boolean popupAboveTarget = dlgBottom <= targetTop; + assertTrue(popupBelowTarget || popupAboveTarget, + "#5029: popup at [" + dlgTop + ".." + dlgBottom + + ") is neither fully above nor fully below target [" + + targetTop + ".." + targetBottom + + ") -- CSSBorder.Arrow has no consistent direction" + + " to point at the target"); + + dialog.dispose(); + } + @Test void showPopupDialogLandscapeFullWidthRectGetsVisibleSize() { // Regression for #4991: in landscape, when the anchor rect spans the