diff --git a/CodenameOne/src/com/codename1/components/InteractionDialog.java b/CodenameOne/src/com/codename1/components/InteractionDialog.java index d06c410259..00c2f6bdca 100644 --- a/CodenameOne/src/com/codename1/components/InteractionDialog.java +++ b/CodenameOne/src/com/codename1/components/InteractionDialog.java @@ -637,9 +637,10 @@ public void showPopupDialog(Component c) { /// /// - `c`: the context component which is used to position the dialog and can also be pointed at /// - /// - `bias`: @param bias biases the dialog to appear above/below or to the sides. - /// This is ignored if there isn't enough space - public void showPopupDialog(Component c, boolean bias) { + /// - `prioritizeTopOrRightPosition`: if `true`, prefer showing above the target (portrait layout) or to + /// the right of the target (landscape layout) when both positions fit. If `false`, prefer below/left. + /// If there isn't enough room in the preferred position, the dialog falls back automatically. + public void showPopupDialog(Component c, boolean prioritizeTopOrRightPosition) { if (c == null) { throw new IllegalArgumentException("Component cannot be null"); } @@ -653,7 +654,7 @@ public void showPopupDialog(Component c, boolean bias) { componentPos.setX(componentPos.getX() - c.getScrollX()); componentPos.setY(componentPos.getY() - c.getScrollY()); setOwner(c); - showPopupDialog(componentPos, bias); + showPopupDialog(componentPos, prioritizeTopOrRightPosition); } /// A popup dialog is shown with the context of a component and its selection. You should use `#setDisposeWhenPointerOutOfBounds(boolean)` to make it dispose @@ -675,9 +676,10 @@ public void showPopupDialog(Rectangle rect) { /// /// - `rect`: the screen rectangle to which the popup should point /// - /// - `bias`: @param bias biases the dialog to appear above/below or to the sides. - /// This is ignored if there isn't enough space - public void showPopupDialog(Rectangle rect, boolean bias) { + /// - `prioritizeTopOrRightPosition`: if `true`, prefer showing above the target (portrait layout) or to + /// the right of the target (landscape layout) when both positions fit. If `false`, prefer below/left. + /// If there isn't enough room in the preferred position, the dialog falls back automatically. + public void showPopupDialog(Rectangle rect, boolean prioritizeTopOrRightPosition) { if (rect == null) { throw new IllegalArgumentException("rect cannot be null"); } @@ -762,7 +764,7 @@ public void showPopupDialog(Rectangle rect, boolean bias) { int x = 0; int y = 0; - boolean showPortrait = bias; + boolean showPortrait = Display.getInstance().isPortrait(); // if we don't have enough space then disregard device orientation if (showPortrait) { @@ -790,7 +792,22 @@ public void showPopupDialog(Rectangle rect, boolean bias) { } } } - if (rect.getY() + rect.getHeight() < availableHeight / 2) { + int spaceAbove = rect.getY(); + int spaceBelow = availableHeight - (rect.getY() + rect.getHeight()); + boolean canShowAbove = prefHeight <= spaceAbove; + boolean canShowBelow = prefHeight <= spaceBelow; + boolean showAbove; + if (canShowAbove && canShowBelow) { + showAbove = prioritizeTopOrRightPosition; + } else if (canShowAbove) { + showAbove = true; + } else if (canShowBelow) { + showAbove = false; + } else { + showAbove = spaceAbove >= spaceBelow; + } + + if (!showAbove) { // popup downwards y = rect.getY() + rect.getHeight(); int height = Math.min(prefHeight, Math.max(0, availableHeight - y)); @@ -798,30 +815,13 @@ public void showPopupDialog(Rectangle rect, boolean bias) { 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 { // popup upwards int height = Math.min(prefHeight, rect.getY()); 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 - y = rect.getY() + CN.convertToPixels(3); - - int height = Math.min(prefHeight, availableHeight - y); - padOrientation(contentPaneStyle, BOTTOM, 1); - show(y, Math.max(0, availableHeight - height - y), - Math.max(0, x), Math.max(0, availableWidth - width - x)); - padOrientation(contentPaneStyle, BOTTOM, -1); - } else { - // popup over aligned with bottom of rect but inset a few mm - y = Math.max(0, rect.getY() + rect.getHeight() - CN.convertToPixels(3) - prefHeight); - int height = prefHeight; - padOrientation(contentPaneStyle, TOP, 1); - show(y, Math.max(0, availableHeight - height - y), - Math.max(0, x), Math.max(0, availableWidth - width - x)); - padOrientation(contentPaneStyle, TOP, -1); } } else { int height = Math.min(prefHeight, availableHeight); @@ -839,23 +839,31 @@ public void showPopupDialog(Rectangle rect, boolean bias) { } } - if (prefWidth < availableWidth - rect.getX() - rect.getWidth()) { + int spaceRight = availableWidth - rect.getX() - rect.getWidth(); + int spaceLeft = rect.getX(); + boolean canShowRight = prefWidth <= spaceRight; + boolean canShowLeft = prefWidth <= spaceLeft; + boolean showRight; + if (canShowRight && canShowLeft) { + showRight = prioritizeTopOrRightPosition; + } else if (canShowRight) { + showRight = true; + } else if (canShowLeft) { + showRight = false; + } else { + showRight = spaceRight >= spaceLeft; + } + + if (showRight) { // popup right x = rect.getX() + rect.getWidth(); - - width = Math.min(prefWidth, availableWidth - x); - show(y, availableHeight - height - y, Math.max(0, x), Math.max(0, availableWidth - width - x)); - } else if (prefWidth < rect.getX()) { - x = rect.getX() - prefWidth; - width = prefWidth; - show(y, availableHeight - height - y, Math.max(0, x), Math.max(0, availableWidth - width - x)); } else { // popup left - width = Math.min(prefWidth, availableWidth - (availableWidth - rect.getX())); + width = Math.min(prefWidth, rect.getX()); x = rect.getX() - width; - show(y, availableHeight - height - y, Math.max(0, x), Math.max(0, availableWidth - width - x)); } + show(y, availableHeight - height - y, Math.max(0, x), Math.max(0, availableWidth - width - x)); } } 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 d72211c9eb..4f1924f87e 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 @@ -5,6 +5,7 @@ import com.codename1.ui.Container; import com.codename1.ui.Form; import com.codename1.ui.Label; +import com.codename1.ui.layouts.FlowLayout; import com.codename1.ui.geom.Rectangle; import com.codename1.ui.layouts.BorderLayout; @@ -97,6 +98,156 @@ void formModeUsesFormLayeredPane() { dialog.dispose(); } + @Test + void showPopupDialogBiasTruePrefersTopWhenBothFit() { + Form form = new Form(new BorderLayout()); + implementation.setCurrentForm(form); + + InteractionDialog dialog = new InteractionDialog(); + dialog.setLayout(new FlowLayout()); + dialog.add(new Label("Popup")); + dialog.setAnimateShow(false); + + Rectangle rect = new Rectangle(120, 220, 60, 40); + dialog.showPopupDialog(rect, true); + + assertTrue(dialog.getY() + dialog.getHeight() <= rect.getY(), + "Expected popup above target when prioritizeTopOrRightPosition=true"); + dialog.dispose(); + } + + @Test + void showPopupDialogBiasFalsePrefersBottomWhenBothFit() { + Form form = new Form(new BorderLayout()); + implementation.setCurrentForm(form); + + InteractionDialog dialog = new InteractionDialog(); + dialog.setLayout(new FlowLayout()); + dialog.add(new Label("Popup")); + dialog.setAnimateShow(false); + + Rectangle rect = new Rectangle(120, 220, 60, 40); + dialog.showPopupDialog(rect, false); + + assertTrue(dialog.getY() >= rect.getY() + rect.getHeight(), + "Expected popup below target when prioritizeTopOrRightPosition=false"); + dialog.dispose(); + } + + @Test + void showPopupDialogFallsBackWhenPreferredTopDoesNotFit() { + Form form = new Form(new BorderLayout()); + implementation.setCurrentForm(form); + + InteractionDialog dialog = new InteractionDialog(); + dialog.setLayout(new FlowLayout()); + dialog.add(new Label("Popup")); + dialog.setAnimateShow(false); + + Rectangle rect = new Rectangle(120, 2, 60, 40); + dialog.showPopupDialog(rect, true); + + assertTrue(dialog.getY() >= rect.getY() + rect.getHeight(), + "Expected fallback below when preferred top side does not fit"); + dialog.dispose(); + } + + @Test + void showPopupDialogFallsBackWhenPreferredBottomDoesNotFit() { + Form form = new Form(new BorderLayout()); + implementation.setCurrentForm(form); + + InteractionDialog dialog = new InteractionDialog(); + dialog.setLayout(new FlowLayout()); + dialog.add(new Label("Popup")); + dialog.setAnimateShow(false); + + int displayHeight = implementation.getDisplayHeight(); + Rectangle rect = new Rectangle(120, Math.max(0, displayHeight - 8), 60, 6); + dialog.showPopupDialog(rect, false); + + assertTrue(dialog.getY() + dialog.getHeight() <= rect.getY(), + "Expected fallback above when preferred bottom side does not fit"); + dialog.dispose(); + } + + @Test + void showPopupDialogBiasTruePrefersRightInLandscapeWhenBothFit() { + implementation.setPortrait(false); + Form form = new Form(new BorderLayout()); + implementation.setCurrentForm(form); + + InteractionDialog dialog = new InteractionDialog(); + dialog.setLayout(new FlowLayout()); + dialog.add(new Label("Popup")); + dialog.setAnimateShow(false); + + Rectangle rect = new Rectangle(120, 140, 60, 40); + dialog.showPopupDialog(rect, true); + + assertTrue(dialog.getX() >= rect.getX() + rect.getWidth(), + "Expected popup on right side when prioritizeTopOrRightPosition=true in landscape"); + dialog.dispose(); + } + + @Test + void showPopupDialogBiasFalsePrefersLeftInLandscapeWhenBothFit() { + implementation.setPortrait(false); + Form form = new Form(new BorderLayout()); + implementation.setCurrentForm(form); + + InteractionDialog dialog = new InteractionDialog(); + dialog.setLayout(new FlowLayout()); + dialog.add(new Label("Popup")); + dialog.setAnimateShow(false); + + Rectangle rect = new Rectangle(120, 140, 60, 40); + dialog.showPopupDialog(rect, false); + + assertTrue(dialog.getX() + dialog.getWidth() <= rect.getX(), + "Expected popup on left side when prioritizeTopOrRightPosition=false in landscape"); + dialog.dispose(); + } + + @Test + void showPopupDialogFallsBackWhenPreferredRightDoesNotFit() { + implementation.setPortrait(false); + Form form = new Form(new BorderLayout()); + implementation.setCurrentForm(form); + + InteractionDialog dialog = new InteractionDialog(); + dialog.setLayout(new FlowLayout()); + dialog.add(new Label("Popup")); + dialog.setAnimateShow(false); + + int displayWidth = implementation.getDisplayWidth(); + Rectangle rect = new Rectangle(Math.max(0, displayWidth - 8), 140, 6, 40); + dialog.showPopupDialog(rect, true); + + assertTrue(dialog.getX() + dialog.getWidth() <= rect.getX(), + "Expected fallback to left when preferred right side does not fit"); + dialog.dispose(); + } + + @Test + void showPopupDialogFallsBackWhenPreferredLeftDoesNotFit() { + implementation.setPortrait(false); + Form form = new Form(new BorderLayout()); + implementation.setCurrentForm(form); + + InteractionDialog dialog = new InteractionDialog(); + dialog.setLayout(new FlowLayout()); + dialog.add(new Label("Popup")); + dialog.setAnimateShow(false); + + Rectangle rect = new Rectangle(2, 140, 6, 40); + dialog.showPopupDialog(rect, false); + + assertTrue(dialog.getX() >= rect.getX() + rect.getWidth(), + "Expected fallback to right when preferred left side does not fit"); + dialog.dispose(); + } + private T getPrivateField(Object target, String name, Class type) throws Exception { Field field = target.getClass().getDeclaredField(name); field.setAccessible(true); diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java index 29d31a3a1f..6ad6468272 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java @@ -69,6 +69,7 @@ public final class Cn1ssDeviceRunner extends DeviceRunner { new BrowserComponentScreenshotTest(), new MediaPlaybackScreenshotTest(), new SheetScreenshotTest(), + new InteractionDialogPopupBiasScreenshotTest(), new ImageViewerNavigationScreenshotTest(), new TextAreaAlignmentScreenshotTest(), // Keep this as the last screenshot test; orientation changes can leak into subsequent screenshots. diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/InteractionDialogPopupBiasScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/InteractionDialogPopupBiasScreenshotTest.java new file mode 100644 index 0000000000..639844ce39 --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/InteractionDialogPopupBiasScreenshotTest.java @@ -0,0 +1,69 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.components.InteractionDialog; +import com.codename1.ui.Component; +import com.codename1.ui.Container; +import com.codename1.ui.Form; +import com.codename1.ui.Label; +import com.codename1.ui.geom.Rectangle; +import com.codename1.ui.layouts.BorderLayout; +import com.codename1.ui.layouts.BoxLayout; +import com.codename1.ui.layouts.FlowLayout; +import com.codename1.ui.util.UITimer; + +public class InteractionDialogPopupBiasScreenshotTest extends BaseTest { + private InteractionDialog topPreferredDialog; + private InteractionDialog bottomPreferredDialog; + private InteractionDialog topFallbackDialog; + + @Override + public boolean runTest() { + Form form = createForm("InteractionDialog Popup", new BorderLayout(), "InteractionDialogPopupBias"); + Container center = new Container(new FlowLayout(Component.CENTER, Component.CENTER)); + Label target = new Label("CENTER TARGET (T:true above / B:false below)"); + target.getAllStyles().setPadding(2, 2, 2, 2); + center.add(target); + form.add(BorderLayout.CENTER, center); + form.show(); + return true; + } + + @Override + protected void registerReadyCallback(Form parent, Runnable run) { + topPreferredDialog = createDialog("T: bias=true (prefer top)"); + bottomPreferredDialog = createDialog("B: bias=false (prefer bottom)"); + topFallbackDialog = createDialog("F: bias=true fallback (shown below)"); + int width = parent.getWidth(); + int height = parent.getHeight(); + + // Keep the targets far apart to avoid overlap in screenshot. + Rectangle upperTarget = new Rectangle(width / 2 - 30, Math.max(56, (height / 2) - 130), 60, 18); + Rectangle lowerTarget = new Rectangle(width / 2 - 30, Math.min(height - 76, (height / 2) + 90), 60, 18); + Rectangle nearTopTarget = new Rectangle(width / 2 - 30, 2, 60, 18); + topPreferredDialog.showPopupDialog(upperTarget, true); + bottomPreferredDialog.showPopupDialog(lowerTarget, false); + topFallbackDialog.showPopupDialog(nearTopTarget, true); + UITimer.timer(600, false, parent, run); + } + + @Override + public void cleanup() { + if (topPreferredDialog != null && topPreferredDialog.isShowing()) { + topPreferredDialog.dispose(); + } + if (bottomPreferredDialog != null && bottomPreferredDialog.isShowing()) { + bottomPreferredDialog.dispose(); + } + if (topFallbackDialog != null && topFallbackDialog.isShowing()) { + topFallbackDialog.dispose(); + } + } + + private InteractionDialog createDialog(String text) { + InteractionDialog dialog = new InteractionDialog(); + dialog.setLayout(BoxLayout.y()); + dialog.add(new Label(text)); + dialog.setDisposeWhenPointerOutOfBounds(false); + return dialog; + } +}