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
82 changes: 45 additions & 37 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 @@ -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) {
Expand Down Expand Up @@ -790,38 +792,36 @@ 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));
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 {
// 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);
Expand All @@ -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));
}
}

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
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading