Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 44 additions & 46 deletions CodenameOne/src/com/codename1/components/InteractionDialog.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand All @@ -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
Expand All @@ -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");
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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> T getPrivateField(Object target, String name, Class<T> type) throws Exception {
Field field = target.getClass().getDeclaredField(name);
field.setAccessible(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading