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
114 changes: 114 additions & 0 deletions Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,16 @@ public static void setShowEDTViolationStacks(boolean aShowEDTViolationStacks) {
private boolean roundedSkin;
private Rectangle safeAreaPortrait = null;
private Rectangle safeAreaLandscape = null;
/// Display-relative rectangles the skin paints over the screen -- a notch,
/// a Dynamic Island, a punch-hole camera. They are inside the display
/// rect, so the pointer path would otherwise deliver a click on an opaque
/// cutout to whatever app content is hidden underneath it. Empty unless
/// the skin declares a `cutouts` property, so skins that do not are
/// unaffected. Note this deliberately does NOT hit-test the skin image's
/// alpha: the screen's rounded corners are opaque skin material too, and
/// unlike a cutout they are real touch surface on the hardware.
private java.util.List<Rectangle> cutoutsPortrait = null;
private java.util.List<Rectangle> cutoutsLandscape = null;
private Map<java.awt.Point, Integer> portraitSkinHotspots;
private java.awt.Rectangle portraitScreenCoordinates;
private Map<java.awt.Point, Integer> landscapeSkinHotspots;
Expand Down Expand Up @@ -4037,6 +4047,9 @@ private int scaleCoordinateY(int coordinate) {
}
Integer triggeredKeyCode;
private boolean mouseDown;
/// Set when a press lands on a skin cutout, so the drag and release
/// that follow are swallowed with it.
private boolean pointerOnCutout;
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger()) {
if (showContextMenu(e)) {
Expand All @@ -4062,6 +4075,16 @@ public void mousePressed(MouseEvent e) {
releaseLock = false;
int x = scaleCoordinateX(e.getX());
int y = scaleCoordinateY(e.getY());
// A press that starts on a cutout is swallowed, and so is the
// rest of the gesture: mouseDragged and mouseReleased both
// deliver on `mouseDown` alone, so suppressing only the press
// would send the app a drag and a release it never saw a
// press for.
pointerOnCutout = isPointerOnCutout(x, y);
if (pointerOnCutout) {
requestFocus();
return;
}
if (x >= 0 && x < surfaceWidth() && y >= 0 && y < surfaceHeight()) {
if (touchDevice) {
if (testRecorder != null) {
Expand Down Expand Up @@ -4118,6 +4141,10 @@ public void mouseReleased(MouseEvent e) {
this.mouseDown = false;
cn1GrabbedDrag = false;
e.consume();
if (pointerOnCutout) {
pointerOnCutout = false;
return;
}
if (!isEnabled()) {
return;
}
Expand Down Expand Up @@ -4156,6 +4183,9 @@ public void mouseExited(MouseEvent e) {
}
public void mouseDragged(MouseEvent e) {
e.consume();
if (pointerOnCutout) {
return;
}
if (!isEnabled()) {
return;
}
Expand Down Expand Up @@ -5038,6 +5068,17 @@ private void loadSkinFile(InputStream skin, final JFrame frm) {
landscapeSkinHotspots = new HashMap<Point, Integer>();
landscapeScreenCoordinates = new Rectangle();
boolean roundScreen = props.getProperty("roundScreen", "false").equalsIgnoreCase("true");
// Every field below describes THIS skin. Clear them up front:
// they used to be assigned only on the branches that had a value
// for them, so loading a legacy skin after a skin designer one
// left the previous device's safe area in force and kept
// painting the skin over the UI (see the roundedSkin branch in
// paintComponent).
roundedSkin = false;
safeAreaPortrait = null;
safeAreaLandscape = null;
cutoutsPortrait = parseCutouts(props.getProperty("cutouts"));
cutoutsLandscape = null;
boolean hasSafeAreaProps =
props.getProperty("safePortraitX") != null ||
props.getProperty("safePortraitY") != null ||
Expand Down Expand Up @@ -5095,6 +5136,9 @@ private void loadSkinFile(InputStream skin, final JFrame frm) {
}


cutoutsLandscape = rotateCutoutsForLandscape(cutoutsPortrait,
portraitScreenCoordinates.height);

platformName = props.getProperty("platformName", "se");
platformOverrides = props.getProperty("overrideNames", "").split(",");

Expand Down Expand Up @@ -5245,6 +5289,76 @@ public void run() {



/// Parses the skin's `cutouts` property: `x,y,w,h` rectangles in
/// display-relative pixels, separated by `;`. Returns null when the skin
/// declares none, which is every skin written before the property
/// existed.
private static java.util.List<Rectangle> parseCutouts(String spec) {
if (spec == null || spec.trim().length() == 0) {
return null;
}
java.util.List<Rectangle> out = new java.util.ArrayList<Rectangle>();
for (String entry : spec.split(";")) {
String[] parts = entry.trim().split(",");
if (parts.length != 4) {
continue;
}
try {
int x = Integer.parseInt(parts[0].trim());
int y = Integer.parseInt(parts[1].trim());
int w = Integer.parseInt(parts[2].trim());
int h = Integer.parseInt(parts[3].trim());
if (w > 0 && h > 0) {
out.add(new Rectangle(x, y, w, h));
}
} catch (NumberFormatException err) {
// A malformed entry disables that rectangle rather than the
// whole skin; the cutout stays clickable, which is the old
// behaviour.
}
}
return out.isEmpty() ? null : out;
}

/// skin_l.png is skin.png through Image.rotate90Degrees, which maps
/// (x, y) to (height - 1 - y, x). The same rotation applied to a display
/// rectangle sends (x, y, w, h) to (displayH - y - h, x, h, w), which is
/// why a notch anchored to the portrait top edge lands on the landscape
/// RIGHT edge.
private static java.util.List<Rectangle> rotateCutoutsForLandscape(
java.util.List<Rectangle> portraitCutouts, int displayH) {
if (portraitCutouts == null) {
return null;
}
java.util.List<Rectangle> out = new java.util.ArrayList<Rectangle>(portraitCutouts.size());
for (Rectangle r : portraitCutouts) {
out.add(new Rectangle(displayH - r.y - r.height, r.x, r.height, r.width));
}
return out;
}

/// True when a display-relative point falls inside one of the cutouts the
/// skin paints over the screen, so the pointer path should ignore it.
/// Static and given its list explicitly so a test can exercise it without
/// building a JavaSEPort: the constructor assigns the `instance`
/// singleton, and a throwaway port silently replaces the one the rest of
/// the suite is running against.
private static boolean isPointerOnCutout(java.util.List<Rectangle> rects, int x, int y) {
if (rects == null) {
return false;
}
for (Rectangle r : rects) {
if (x >= r.x && x < r.x + r.width && y >= r.y && y < r.y + r.height) {
return true;
Comment thread
shai-almog marked this conversation as resolved.
}
}
return false;
}

private boolean isPointerOnCutout(int x, int y) {
return isPointerOnCutout(portrait ? cutoutsPortrait : cutoutsLandscape, x, y);
}

@Override
public com.codename1.ui.geom.Rectangle getDisplaySafeArea(com.codename1.ui.geom.Rectangle rect) {
if (!isSimulator() || safeAreaPortrait == null || safeAreaLandscape == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,33 @@ pixelRatio=18.110236220472443
# what makes the Dynamic Island / punch-hole shapes appear "floating" on
# top of the iOS status bar instead of being carved out of the display.
roundScreen=true
displayX=89
displayY=89
displayX=151
displayY=151
displayWidth=1206
displayHeight=2622

# Safe area in display-relative coordinates (origin = screen top-left).
# The cutouts the skin paints on the screen, as display-relative
# x,y,w,h rectangles separated by ";". The simulator drops pointer events
# inside them, so a click on the opaque island cannot reach app content
# hidden underneath. A skin that omits this key keeps every cutout
# clickable.
cutouts=377,53,452,132

# Safe area in display-relative coordinates (origin = screen top-left),
# in PIXELS. The Info tab's Top/Bottom fields are points (iOS) or dp
# (Android); the wizard multiplies them by the device's density scale.
# Here the Dynamic Island's bottom edge sits below the 59pt status-bar
# inset, so it is the island that sets safePortraitY.
safePortraitX=0
safePortraitY=237
safePortraitY=185
Comment thread
shai-almog marked this conversation as resolved.
safePortraitWidth=1206
safePortraitHeight=2272
safeLandscapeX=237
safePortraitHeight=2335
# skin_l.png is skin.png rotated clockwise, so the portrait TOP inset lands
# on the landscape RIGHT edge. X therefore carries the BOTTOM inset (34pt x
# 3), and the 185px island inset is what is left on the right.
safeLandscapeX=102
safeLandscapeY=0
safeLandscapeWidth=2272
safeLandscapeWidth=2335
safeLandscapeHeight=1206

# These get composed by overrideNames(device) — they let users layer
Expand Down
49 changes: 31 additions & 18 deletions docs/developer-guide/Skin-Designer.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -106,27 +106,34 @@ to expand its width / height / offset fields; the X button on the
right removes it. The three add buttons at the bottom seed a sensible
default of each type:

* *Notch* (180 × 30 viewbox px) — physical hardware cutout. Notches
are drawn in the device frame *above* the screen rect, with the
bottom edge touching the screen top. Mirrors iPhone X / 11 / 12 /
13 hardware.
* *Notch* (180 × 30 viewbox px) — the iPhone X / 11 / 12 / 13
hardware cutout. Drawn as an opaque shape hanging from the top edge
*inside* the screen rect, square at the top and rounded at the
bottom, because that's where it sits on the hardware: an iPhone 13
Pro Max reports 1284 × 2778 pixels and the notch eats into the top
of that rectangle.
* *Island* (120 × 35) — Dynamic Island. Software-reserved space
rendered as an opaque pill *inside* the screen rect, floating on
top of the iOS status bar.
rendered as an opaque pill inside the screen rect, floating on top
of the iOS status bar.
* *Hole* (28 × 28) — Android punch-hole camera. Rendered like the
island: opaque circle inside the screen rect.

Offsets are relative to the top-center of the screen, in viewbox
pixels. For islands and holes, `Offset Y` is the gap between the
cutout's top edge and the screen top. For notches, `Offset Y` is
ignored (the notch is anchored to the frame).
ignored, because a notch is anchored to the screen's top edge.

The wizard also writes each cutout's rectangle into the `cutouts`
property. The simulator drops pointer events that land inside one, so a
click on an opaque notch or island can't reach app content hidden
underneath it, the way the hardware cutout can't be touched at all.

WARNING: When the wizard generates the `.skin` file, it
automatically extends `safePortraitTop` to cover any in-screen
cutouts (islands, holes), so app content lands below the floating
shape. You don't need to manually adjust safe area for the cutout
itself; only adjust it if you need extra padding above your title
bar.
automatically pushes `safePortraitY` down to the lowest cutout edge
whenever that sits below the platform's own status-bar inset, so app
content lands below the shape instead of underneath it. You don't
need to adjust the safe area for a cutout yourself; only adjust it
if you want extra padding above your title bar.

==== Info tab

Expand All @@ -142,9 +149,14 @@ written into `skin.properties`:
* *PPI / Pixels per millimeter* — the simulator uses
`pixelMilliRatio = ppi / 25.4` to derive font sizes when none are
specified in the skin.
* *Safe area Top / Bottom* — the user-editable safe-area insets in
viewbox pixels. The wizard converts these to display-relative
pixels at save time.
* *Safe area Top / Bottom* — the user-editable safe-area insets,
in the density-independent units the platform vendors publish:
points on iOS, dp on Android. A notched iPhone reserves 47pt at
the top and 34pt at the bottom, a Dynamic Island iPhone 59pt and
34pt, and an iPad 24pt on both edges. The wizard multiplies them
by the device's density scale to get the display-relative pixel
insets it writes into `skin.properties`, so the same 47 is
correct on every notched iPhone regardless of resolution.

=== Stage 4 — finish and download

Expand All @@ -153,16 +165,17 @@ image::img/skin-designer/skin-designer-stage-4-done.png[Done page showing the gr
Clicking *Finish* in the editor's footer:

. Renders the portrait skin image (`skin.png`) at the device's
actual resolution + bezel, with rounded corners, transparent
screen, opaque cutouts, and a home indicator if enabled.
actual resolution plus the bezel on all four sides, with rounded
corners, a transparent screen, opaque cutouts painted on that
screen, and a home indicator if enabled.
. Synthesises the landscape skin (`skin_l.png`) by 90° rotation.
. Writes `skin_map.png` / `skin_map_l.png` overlays that mark the
screen rectangle for the simulator's screen-position detection.
. Bundles the appropriate native theme (`iOS7Theme.res`,
`android_holo_light.res`, or `winTheme.res`) inside the skin zip.
. Writes `skin.properties` with `roundScreen=true`,
`displayX/Y/Width/Height`, `safePortrait*` / `safeLandscape*`,
the platform name, override names, fonts, and PPI.
`cutouts`, the platform name, override names, fonts, and PPI.

Clicking *Download skin* on the done page hands the file to the
browser's download dialog. The button is the only reliable trigger
Expand Down
7 changes: 7 additions & 0 deletions docs/developer-guide/languagetool-accept.txt
Original file line number Diff line number Diff line change
Expand Up @@ -685,3 +685,10 @@ Bonjour
# The Apple verb for signing a build. Not "code signing" here: this is the
# spelling in the error an unavailable entitlement produces.
codesigning

# -----------------------------------------------------------------------------
# Skin designer (Skin-Designer.asciidoc) units.
# -----------------------------------------------------------------------------
# Android's density-independent pixel, the unit its safe-area insets are
# published in. Spelled the way Google spells it, alongside the iOS "pt".
dp
Loading
Loading