diff --git a/CodenameOne/src/com/codename1/components/InteractionDialog.java b/CodenameOne/src/com/codename1/components/InteractionDialog.java index d06c410259..e5313ce80d 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"); } @@ -719,13 +721,15 @@ public void showPopupDialog(Rectangle rect, boolean bias) { // allows a text area to recalculate its preferred size if embedded within a dialog revalidate(); - Style contentPaneStyle = getStyle(); // PMD Fix: UnusedLocalVariable removed redundant contentPane reference + Style contentPaneStyle = getDialogStyle(); - if (manager.isThemeConstant(getUIID() + "ArrowBool", false)) { - Image t = manager.getThemeImageConstant(getUIID() + "ArrowTopImage"); - Image b = manager.getThemeImageConstant(getUIID() + "ArrowBottomImage"); - Image l = manager.getThemeImageConstant(getUIID() + "ArrowLeftImage"); - Image r = manager.getThemeImageConstant(getUIID() + "ArrowRightImage"); + boolean arrowEnabled = manager.isThemeConstant(getUIID() + "ArrowBool", false); + Image t = manager.getThemeImageConstant(getUIID() + "ArrowTopImage"); + Image b = manager.getThemeImageConstant(getUIID() + "ArrowBottomImage"); + Image l = manager.getThemeImageConstant(getUIID() + "ArrowLeftImage"); + Image r = manager.getThemeImageConstant(getUIID() + "ArrowRightImage"); + boolean hasArrowImages = t != null || b != null || l != null || r != null; + if (arrowEnabled && hasArrowImages) { Border border = contentPaneStyle.getBorder(); if (border != null) { border.setImageBorderSpecialTile(t, b, l, r, rect); @@ -762,7 +766,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,37 +794,29 @@ 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 decision: honor preference when both sides fit, otherwise use the fitting side, + // and if neither fits use the side with more available space. + boolean showAbove = canShowAbove && (canShowBelow ? prioritizeTopOrRightPosition : true) + || !canShowAbove && !canShowBelow && spaceAbove >= spaceBelow; + + if (!showAbove) { // popup downwards y = rect.getY() + rect.getHeight(); int height = Math.min(prefHeight, Math.max(0, availableHeight - y)); - padOrientation(contentPaneStyle, TOP, 1); + padOrientation(contentPaneStyle, BOTTOM, 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) { + padOrientation(contentPaneStyle, BOTTOM, -1); + } 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)); + show(y, Math.max(0, availableHeight - rect.getY()), x, Math.max(0, availableWidth - width - x)); padOrientation(contentPaneStyle, TOP, -1); } } else { @@ -839,23 +835,25 @@ 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 decision: honor preference when both sides fit, otherwise use the fitting side, + // and if neither fits use the side with more available space. + boolean showRight = canShowRight && (canShowLeft ? prioritizeTopOrRightPosition : true) + || !canShowRight && !canShowLeft && 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..1fa1df25a3 --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/InteractionDialogPopupBiasScreenshotTest.java @@ -0,0 +1,89 @@ +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.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 northFallbackDialog; + private InteractionDialog centerTopDialog; + private InteractionDialog centerBottomDialog; + private InteractionDialog southFallbackDialog; + private Label northTarget; + private Label centerTarget; + private Label southTarget; + + @Override + public boolean runTest() { + Form form = createForm("InteractionDialog Popup", new BorderLayout(), "InteractionDialogPopupBias"); + northTarget = createTarget("NORTH TARGET"); + centerTarget = createTarget("CENTER TARGET"); + southTarget = createTarget("SOUTH TARGET"); + Container center = new Container(new FlowLayout(Component.CENTER, Component.CENTER)); + center.add(centerTarget); + form.add(BorderLayout.NORTH, wrapTarget(northTarget)); + form.add(BorderLayout.CENTER, center); + form.add(BorderLayout.SOUTH, wrapTarget(southTarget)); + form.show(); + return true; + } + + @Override + protected void registerReadyCallback(Form parent, Runnable run) { + northFallbackDialog = createDialog("1) N/T -> down"); + centerTopDialog = createDialog("2) C/T -> up"); + centerBottomDialog = createDialog("3) C/B -> down"); + southFallbackDialog = createDialog("4) S/B -> up"); + parent.revalidate(); + northFallbackDialog.showPopupDialog(northTarget, true); + centerTopDialog.showPopupDialog(centerTarget, true); + centerBottomDialog.showPopupDialog(centerTarget, false); + southFallbackDialog.showPopupDialog(southTarget, false); + UITimer.timer(600, false, parent, run); + } + + @Override + public void cleanup() { + if (northFallbackDialog != null && northFallbackDialog.isShowing()) { + northFallbackDialog.dispose(); + } + if (centerTopDialog != null && centerTopDialog.isShowing()) { + centerTopDialog.dispose(); + } + if (centerBottomDialog != null && centerBottomDialog.isShowing()) { + centerBottomDialog.dispose(); + } + if (southFallbackDialog != null && southFallbackDialog.isShowing()) { + southFallbackDialog.dispose(); + } + } + + private InteractionDialog createDialog(String text) { + InteractionDialog dialog = new InteractionDialog(); + dialog.setLayout(BoxLayout.y()); + dialog.setAnimateShow(false); + Label label = new Label(text); + label.getAllStyles().setPadding(1, 1, 1, 1); + dialog.add(label); + dialog.setDisposeWhenPointerOutOfBounds(false); + return dialog; + } + + private Label createTarget(String text) { + Label target = new Label(text); + target.getAllStyles().setPadding(2, 2, 2, 2); + return target; + } + + private Container wrapTarget(Component target) { + Container wrapper = new Container(new FlowLayout(Component.CENTER, Component.CENTER)); + wrapper.add(target); + return wrapper; + } +}