Skip to content
Merged
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
106 changes: 94 additions & 12 deletions Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,16 @@ public class JavaSEPort extends CodenameOneImplementation {
private boolean largerTextEnabled = false;
private static final String PREF_LARGER_TEXT_SCALE = "cn1.simulator.largerTextScale";

// Floor below which any persisted window dimension is treated as the
// product of a layout glitch (eg. a pack() with a collapsed canvas, an
// iconified frame, or hand-edited prefs) rather than a real user state.
static final int MIN_PERSISTED_WINDOW_DIMENSION = 100;

// Defensive ceiling so absurd values from a corrupted pref or runaway
// scaling math cannot make a subsequent setBounds() throw or place the
// frame off every reachable display.
static final int MAX_PERSISTED_WINDOW_DIMENSION = 32767;

static {
IOS_NATIVE_FONT_CANDIDATES.put("native:MainThin", new String[] {
"SF Pro Display", "SF Pro Text",
Expand Down Expand Up @@ -5825,6 +5835,75 @@ public void run() {
});
}

/**
* Returns true when {@code r} represents a window position we are willing
* to persist or restore. Rejects null, NaN-equivalent (non-positive) sizes,
* sub-floor dimensions, oversized values, and frames that no longer have a
* useful overlap with any connected display. Any caller that fails this
* check should fall back to default bounds and remove the corrupt pref so
* the bad state does not survive across launches.
*/
public static boolean isUsableWindowBounds(Rectangle r) {
if (r == null) {
return false;
}
if (r.width < MIN_PERSISTED_WINDOW_DIMENSION || r.height < MIN_PERSISTED_WINDOW_DIMENSION) {
return false;
}
if (r.width > MAX_PERSISTED_WINDOW_DIMENSION || r.height > MAX_PERSISTED_WINDOW_DIMENSION) {
return false;
}
try {
if (GraphicsEnvironment.isHeadless()) {
// No screens to validate against; size has already cleared the
// sanity floor, so accept. The simulator is not really expected
// to run headless, but tests do.
return true;
}
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice[] devs = ge.getScreenDevices();
if (devs == null || devs.length == 0) {
return true;
}
Rectangle union = new Rectangle(0, 0, 0, 0);
for (GraphicsDevice gd : devs) {
union.add(gd.getDefaultConfiguration().getBounds());
}
Rectangle visible = union.intersection(r);
// Require enough overlap that the title bar and drag region remain
// reachable; a one-pixel touch is not good enough to "remember".
return visible.width >= MIN_PERSISTED_WINDOW_DIMENSION && visible.height >= 50;
} catch (Throwable t) {
return false;
}
}

/**
* Parses the comma-separated {@code "x,y,width,height"} form written to
* {@code window.bounds}. Returns {@code null} when the input is missing,
* has the wrong field count, or contains non-numeric data. The bounds
* returned have not been validated for screen overlap or dimensions;
* pair with {@link #isUsableWindowBounds(Rectangle)}.
*/
static Rectangle parsePersistedBounds(String s) {
if (s == null) {
return null;
}
try {
String[] parts = s.split(",");
if (parts.length != 4) {
return null;
}
return new Rectangle(
Integer.parseInt(parts[0].trim()),
Integer.parseInt(parts[1].trim()),
Integer.parseInt(parts[2].trim()),
Integer.parseInt(parts[3].trim()));
} catch (NumberFormatException e) {
return null;
}
}


private ArrayList<Runnable> deinitializeHooks = new ArrayList<>();
public void addDeinitializeHook(Runnable r) {
Expand Down Expand Up @@ -6249,9 +6328,17 @@ public void windowDeactivated(WindowEvent e) {
private void saveBounds(ComponentEvent e) {
if (e.getComponent() instanceof JFrame) {
Frame f = (JFrame)e.getComponent();
// The NORMAL check filters maximized/iconified states,
// but a misbehaving pack() can still produce tiny or
// off-screen geometry while remaining NORMAL. Run the
// shared sanity check so a broken layout pass cannot
// corrupt the persisted bounds.
if (f.getExtendedState() == JFrame.NORMAL) {
Preferences pref = Preferences.userNodeForPackage(JavaSEPort.class);
Rectangle bounds = f.getBounds();
if (!isUsableWindowBounds(bounds)) {
return;
}
Preferences pref = Preferences.userNodeForPackage(JavaSEPort.class);
pref.put("window.bounds", bounds.x+","+bounds.y+","+bounds.width+","+bounds.height);
}
}
Expand Down Expand Up @@ -6360,18 +6447,13 @@ public void componentHidden(ComponentEvent e) {
}
String lastBounds = pref.get("window.bounds", null);
if (lastBounds != null) {
String[] parts = lastBounds.split(",");
Rectangle r = new Rectangle(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), Integer.parseInt(parts[2]), Integer.parseInt(parts[3]));
Rectangle bounds = new Rectangle(0, 0, 0, 0);
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice lstGDs[] = ge.getScreenDevices();
for (GraphicsDevice gd : lstGDs) {
bounds.add(gd.getDefaultConfiguration().getBounds());
}

if (bounds.intersects(r)) {

Rectangle r = parsePersistedBounds(lastBounds);
if (isUsableWindowBounds(r)) {
window.setBounds(r);
} else {
// Drop the bad value so a single bad pack() cannot lock the
// user out across restarts.
pref.remove("window.bounds");
}
}

Expand Down
27 changes: 22 additions & 5 deletions Ports/JavaSE/src/com/codename1/impl/javase/simulator/AppPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,11 @@ public void savePreferences(AppFrame frame, Preferences prefs) {
JFrame window = frame.getWindow(this);
if (window != null) {
Rectangle r = window.getBounds();
// Skip persistence when the geometry is degenerate: a layout
// glitch or minimized frame must not corrupt the saved state.
if (!JavaSEPort.isUsableWindowBounds(r)) {
return;
}
prefs.putInt(getPreferencesPrefix(frame) + "preferredWindowBounds.x", r.x);
prefs.putInt(getPreferencesPrefix(frame) + "preferredWindowBounds.y", r.y);
prefs.putInt(getPreferencesPrefix(frame) + "preferredWindowBounds.width", r.width);
Expand All @@ -327,11 +332,23 @@ public void applyPreferences(AppFrame frame, Preferences prefs) {
setPreferredFrame(AppFrame.FrameLocation.valueOf(preferredFrameName));
} catch (IllegalArgumentException ex){}
}
preferredWindowBounds = new Rectangle(0, 0, getPreferredSize().width, getPreferredSize().height);
preferredWindowBounds.x = prefs.getInt(getPreferencesPrefix(frame)+"preferredWindowBounds.x", preferredWindowBounds.x);
preferredWindowBounds.y = prefs.getInt(getPreferencesPrefix(frame)+"preferredWindowBounds.y", preferredWindowBounds.y);
preferredWindowBounds.width = prefs.getInt(getPreferencesPrefix(frame)+"preferredWindowBounds.width", preferredWindowBounds.width);
preferredWindowBounds.height = prefs.getInt(getPreferencesPrefix(frame)+"preferredWindowBounds.height", preferredWindowBounds.height);
Rectangle defaults = new Rectangle(0, 0, getPreferredSize().width, getPreferredSize().height);
Rectangle loaded = new Rectangle(
prefs.getInt(getPreferencesPrefix(frame)+"preferredWindowBounds.x", defaults.x),
prefs.getInt(getPreferencesPrefix(frame)+"preferredWindowBounds.y", defaults.y),
prefs.getInt(getPreferencesPrefix(frame)+"preferredWindowBounds.width", defaults.width),
prefs.getInt(getPreferencesPrefix(frame)+"preferredWindowBounds.height", defaults.height));
if (JavaSEPort.isUsableWindowBounds(loaded)) {
preferredWindowBounds = loaded;
} else {
preferredWindowBounds = defaults;
// Clear the corrupt entries so a one-time glitch does not haunt
// every future launch of the simulator.
prefs.remove(getPreferencesPrefix(frame)+"preferredWindowBounds.x");
prefs.remove(getPreferencesPrefix(frame)+"preferredWindowBounds.y");
prefs.remove(getPreferencesPrefix(frame)+"preferredWindowBounds.width");
prefs.remove(getPreferencesPrefix(frame)+"preferredWindowBounds.height");
}
preferredAlwaysOnTop = prefs.getBoolean(getPreferencesPrefix(frame)+"preferredAlwaysOnTop", preferredAlwaysOnTop);


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package com.codename1.impl.javase;

import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeFalse;

/**
* Guards against regressions in the simulator's window-bounds persistence.
* The bug that prompted these tests: picking Simulate -> Larger Text ->
* Extra Extra Extra Large collapsed the frame, the resulting tiny geometry
* was written to prefs, and every subsequent launch restored the unusable
* window. The helpers tested here are the choke points that must reject
* those values on both write and read.
*/
public class JavaSEPortWindowBoundsTest {

@Test
public void isUsableWindowBoundsRejectsNull() {
assertFalse(JavaSEPort.isUsableWindowBounds(null));
}

@Test
public void isUsableWindowBoundsRejectsCollapsedFrame() {
assertFalse(JavaSEPort.isUsableWindowBounds(new Rectangle(0, 0, 0, 0)));
assertFalse(JavaSEPort.isUsableWindowBounds(new Rectangle(0, 0, 1, 1)));
assertFalse(JavaSEPort.isUsableWindowBounds(new Rectangle(0, 0, 50, 50)));
}

@Test
public void isUsableWindowBoundsRejectsJustBelowFloor() {
// 99 sits just under MIN_PERSISTED_WINDOW_DIMENSION (100). Verifies the
// floor is enforced strictly, not as "approximately".
assertFalse(JavaSEPort.isUsableWindowBounds(new Rectangle(0, 0, 99, 200)));
assertFalse(JavaSEPort.isUsableWindowBounds(new Rectangle(0, 0, 200, 99)));
}

@Test
public void isUsableWindowBoundsRejectsAbsurdSize() {
// A corrupt or overflowing pref could produce arbitrarily large values.
// Both dimensions are checked independently.
assertFalse(JavaSEPort.isUsableWindowBounds(new Rectangle(0, 0, 40000, 800)));
assertFalse(JavaSEPort.isUsableWindowBounds(new Rectangle(0, 0, 800, 40000)));
assertFalse(JavaSEPort.isUsableWindowBounds(new Rectangle(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE)));
}

@Test
public void isUsableWindowBoundsAcceptsReasonableOnDefaultScreen() {
assumeFalse(GraphicsEnvironment.isHeadless(), "needs a display");
GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
Rectangle screen = gd.getDefaultConfiguration().getBounds();
Rectangle window = new Rectangle(screen.x + 50, screen.y + 50, 800, 600);
assertTrue(JavaSEPort.isUsableWindowBounds(window),
"a normally placed 800x600 window on the default screen must be accepted");
}

@Test
public void isUsableWindowBoundsRejectsFarOffScreen() {
assumeFalse(GraphicsEnvironment.isHeadless(), "needs a display");
// No reachable display extends out this far; the user could never
// recover this window with the mouse.
Rectangle window = new Rectangle(-100000, -100000, 800, 600);
assertFalse(JavaSEPort.isUsableWindowBounds(window));
}

@Test
public void isUsableWindowBoundsRejectsSliverOnScreen() {
assumeFalse(GraphicsEnvironment.isHeadless(), "needs a display");
GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
Rectangle screen = gd.getDefaultConfiguration().getBounds();
// Window is mostly off the right edge of the screen, leaving only ~10px
// visible. The old intersects() check accepted this; the new floor of
// 100px visible width must reject it.
Rectangle window = new Rectangle(screen.x + screen.width - 10, screen.y + 50, 800, 600);
assertFalse(JavaSEPort.isUsableWindowBounds(window));
}

@Test
public void parsePersistedBoundsAcceptsWellFormed() {
Rectangle r = JavaSEPort.parsePersistedBounds("10,20,800,600");
assertNotNull(r);
assertEquals(10, r.x);
assertEquals(20, r.y);
assertEquals(800, r.width);
assertEquals(600, r.height);
}

@Test
public void parsePersistedBoundsToleratesWhitespace() {
// Defensive against hand-edited prefs or future formatting tweaks; the
// current writer does not insert spaces but readers should not be
// fragile about it.
Rectangle r = JavaSEPort.parsePersistedBounds(" 10 , 20 , 800 , 600 ");
assertNotNull(r);
assertEquals(10, r.x);
assertEquals(600, r.height);
}

@Test
public void parsePersistedBoundsAcceptsNegativeOrigin() {
// Multi-monitor setups can legitimately place a window at negative
// coordinates relative to the primary display.
Rectangle r = JavaSEPort.parsePersistedBounds("-1200,-300,800,600");
assertNotNull(r);
assertEquals(-1200, r.x);
assertEquals(-300, r.y);
}

@Test
public void parsePersistedBoundsRejectsNull() {
assertNull(JavaSEPort.parsePersistedBounds(null));
}

@Test
public void parsePersistedBoundsRejectsEmpty() {
assertNull(JavaSEPort.parsePersistedBounds(""));
}

@Test
public void parsePersistedBoundsRejectsWrongFieldCount() {
assertNull(JavaSEPort.parsePersistedBounds("10,20,30"));
assertNull(JavaSEPort.parsePersistedBounds("10,20,30,40,50"));
assertNull(JavaSEPort.parsePersistedBounds("10"));
}

@Test
public void parsePersistedBoundsRejectsNonNumeric() {
// A NumberFormatException leaking out of init() would abort the entire
// simulator startup; the parser must swallow it and return null.
assertNull(JavaSEPort.parsePersistedBounds("not,a,number,here"));
assertNull(JavaSEPort.parsePersistedBounds("10,20,800,abc"));
assertNull(JavaSEPort.parsePersistedBounds("10.5,20,800,600"));
}

@Test
public void roundTripWriteAndParse() {
// Mirrors the save format used by saveBounds(): "x,y,width,height".
Rectangle saved = new Rectangle(150, 75, 1024, 768);
String written = saved.x + "," + saved.y + "," + saved.width + "," + saved.height;
Rectangle restored = JavaSEPort.parsePersistedBounds(written);
assertEquals(saved, restored);
}
}
Loading