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
48 changes: 48 additions & 0 deletions Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -3370,6 +3370,54 @@ - (void)viewDidLayoutSubviews {
screenSizeChanged(displayWidth, displayHeight);
}

#elif !TARGET_OS_OSX && !TARGET_OS_WATCH
// Defined further down, beside the drawing code that reads it.
extern bool lockDrawing;

// viewWillTransitionToSize: is how an iPhone learns about a rotation, and it
// is not delivered when the interface rotates while a full-screen modal that
// supports only portrait covers this view -- the camera of
// UIImagePickerController is one. The device is turned with the modal up, the
// interface stays portrait, and when the modal is dismissed the view comes back
// laid out landscape with no transition at all. Every form then drew at the
// portrait size stretched across the landscape view until the next rotation
// (issue #6080). Layout always runs with the real bounds, so any size the
// transition callbacks did not publish is published here.
//
// Nothing to debounce, unlike Catalyst above: an iPhone view changes size on a
// rotation or a split-screen resize, not continuously. In the usual case
// viewWillTransitionToSize: has already published the new size and this
// returns at the comparison. The keyboard moves this view's origin, never its
// size, so it does not reach this either.
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
// The same reason viewWillTransitionToSize: gives: iOS lays the view out
// at the opposite orientation for its background snapshot.
if (isAppSuspended || self.view == nil) {
return;
}
CGSize sz = self.view.bounds.size;
int newW = (int)(sz.width * scaleValue);
int newH = (int)(sz.height * scaleValue);
if (newW <= 0 || newH <= 0) {
return;
}
int dw = newW - displayWidth;
int dh = newH - displayHeight;
if (dw < 0) dw = -dw;
if (dh < 0) dh = -dh;
if (dw <= 1 && dh <= 1) {
return;
}
[[self renderingView] updateFrameBufferSize:newW h:newH];
[[self renderingView] deleteFramebuffer];
displayWidth = newW;
displayHeight = newH;
lockDrawing = NO;
screenSizeChanged(displayWidth, displayHeight);
repaintUI();
}

#endif

#ifdef INCLUDE_MOPUB
Expand Down
6 changes: 5 additions & 1 deletion docs/website/data/port_status.json
Original file line number Diff line number Diff line change
Expand Up @@ -573,8 +573,9 @@
"id": "orientation",
"category": "Input and lifecycle",
"name": "Orientation locking",
"description": "Checks orientation changes and the rendered landscape state.",
"description": "Checks orientation changes, the rendered landscape state, and a rotation hidden by a native modal.",
"tests": [
"NativeModalRotationTest",
"OrientationLockScreenshotTest"
]
},
Expand Down Expand Up @@ -911,6 +912,9 @@
}
],
"test_scopes": {
"NativeModalRotationTest": [
"ios-metal"
],
"SystemBackNavigationTest": [
"android"
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codenameone.examples.hellocodenameone;

/// Only iOS presents a native modal over the Codename One view this way.
public class NativeModalRotationNativeImpl {
public boolean presentPortraitOnlyModal() {
return false;
}

public boolean dismissModal() {
return false;
}

public boolean isModalPresented() {
return false;
}

public int getViewPixelWidth() {
return 0;
}

public int getViewPixelHeight() {
return 0;
}

public boolean isSupported() {
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codenameone.examples.hellocodenameone;

import com.codename1.system.NativeInterface;

/// Drives a full-screen native modal that only supports portrait -- the shape
/// of the iOS camera (`UIImagePickerController`) -- so a test can rotate the
/// device while it covers the Codename One view and then dismiss it
/// (issue #6080). Every call returns at once; the work runs on the main
/// thread, and the getters answer the state captured by the previous call.
public interface NativeModalRotationNative extends NativeInterface {
/// Presents the portrait-only modal over the Codename One view.
boolean presentPortraitOnlyModal();

/// Dismisses it, animated, as the camera dismisses itself.
boolean dismissModal();

/// True while the modal is presented.
boolean isModalPresented();

/// The Codename One view's current width in pixels, as UIKit sees it.
int getViewPixelWidth();

/// The Codename One view's current height in pixels, as UIKit sees it.
int getViewPixelHeight();
}
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,8 @@ private static int testTimeoutMs(BaseTest testClass) {
new Gpu3DAnimationTest(),
// Keep this as the last portrait screenshot test; orientation changes can leak into subsequent screenshots.
new OrientationLockScreenshotTest(),
// Rotates under a native modal and restores portrait itself (#6080).
new NativeModalRotationTest(),
new InPlaceEditViewTest(),
new BytecodeTranslatorRegressionTest(),
new SimdApiTest(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codenameone.examples.hellocodenameone.tests;

import com.codename1.system.NativeLookup;
import com.codename1.ui.CN;
import com.codename1.ui.Display;
import com.codename1.ui.Form;
import com.codename1.ui.Label;
import com.codename1.ui.layouts.BoxLayout;
import com.codename1.ui.util.UITimer;
import com.codenameone.examples.hellocodenameone.NativeModalRotationNative;

/// Issue #6080: rotate the device while a full-screen native modal that only
/// supports portrait covers the Codename One view -- the iOS camera is one --
/// and dismiss it. The interface rotates to landscape as the modal goes away,
/// and the framework has to learn the new size then; when it did not, every
/// form drew with the portrait width stretched across the landscape view until
/// the device was rotated again.
///
/// Asserts that once the modal is gone and the view is landscape, the display
/// size the framework uses equals the view's size in pixels. iOS only; other
/// platforms have no such modal and finish at once.
///
/// No screenshot -- this is an assertion test.
public class NativeModalRotationTest extends BaseTest {
private static final int POLL_INTERVAL_MS = 100;
// The modal is presented without animation, the dismissal is animated and
// the rotation follows it; 5s is far beyond either on a starved runner.
private static final int POLL_ATTEMPTS = 50;
// Lets a rotation that the modal failed to hold back land before checking.
private static final int UNDER_MODAL_SETTLE_MS = 1500;
private static final int TOLERANCE_PX = 2;

private NativeModalRotationNative modal;

@Override
public boolean shouldTakeScreenshot() {
return false;
}

@Override
public boolean runTest() {
modal = NativeLookup.create(NativeModalRotationNative.class);
if (modal == null || !modal.isSupported() || !CN.canForceOrientation()) {
done();
return true;
}
Form form = new Form("Native Modal Rotation", new BoxLayout(BoxLayout.Y_AXIS)) {
@Override
protected void onShowCompleted() {
start(this);
}
};
form.add(new Label("Rotating under a native modal..."));
form.show();
return true;
}

private void start(final Form form) {
CN.lockOrientation(true);
poll(form, POLL_ATTEMPTS, new Condition() {
public boolean holds() {
return modal.getViewPixelHeight() > modal.getViewPixelWidth();
}
}, "the device to start in portrait", new Runnable() {
public void run() {
modal.presentPortraitOnlyModal();
poll(form, POLL_ATTEMPTS, new Condition() {
public boolean holds() {
return modal.isModalPresented();
}
}, "the native modal to be presented", new Runnable() {
public void run() {
rotateUnderModal(form);
}
});
}
});
}

private void rotateUnderModal(final Form form) {
// The device turns to landscape; the modal keeps the interface
// portrait, exactly as the camera does.
CN.lockOrientation(false);
UITimer.timer(UNDER_MODAL_SETTLE_MS, false, form, new Runnable() {
public void run() {
// Portrait here is what makes this the camera's path: a modal
// that let the interface rotate would test an ordinary rotation.
System.out.println("CN1SS:INFO:test=NativeModalRotationTest under the modal view="
+ modal.getViewPixelWidth() + "x" + modal.getViewPixelHeight()
+ " display=" + Display.getInstance().getDisplayWidth()
+ "x" + Display.getInstance().getDisplayHeight());
modal.dismissModal();
poll(form, POLL_ATTEMPTS, new Condition() {
public boolean holds() {
return !modal.isModalPresented()
&& modal.getViewPixelWidth() > modal.getViewPixelHeight();
}
}, "the modal to close and the view to turn landscape", new Runnable() {
public void run() {
awaitDisplayMatchesView(form, POLL_ATTEMPTS);
}
});
}
});
}

/// Polls rather than checking once: the size reaches the EDT as an event,
/// so a correct port may need a frame or two. A port that never delivers it
/// keeps the portrait size for the whole budget.
private void awaitDisplayMatchesView(final Form form, final int attemptsLeft) {
int viewW = modal.getViewPixelWidth();
int viewH = modal.getViewPixelHeight();
int displayW = Display.getInstance().getDisplayWidth();
int displayH = Display.getInstance().getDisplayHeight();
if (Math.abs(viewW - displayW) <= TOLERANCE_PX && Math.abs(viewH - displayH) <= TOLERANCE_PX) {
finish(form, null);
return;
}
if (attemptsLeft <= 0) {
finish(form, "after a native modal closed over a rotation the view is "
+ viewW + "x" + viewH + " pixels but the display is "
+ displayW + "x" + displayH + " (issue #6080)");
return;
}
UITimer.timer(POLL_INTERVAL_MS, false, form, new Runnable() {
public void run() {
awaitDisplayMatchesView(form, attemptsLeft - 1);
}
});
}

private void finish(final Form form, final String failure) {
modal.dismissModal();
LandscapeCapture.restorePortrait(form, new Runnable() {
public void run() {
if (failure != null) {
fail(failure);
} else {
done();
}
}
});
}

private interface Condition {
boolean holds();
}

private void poll(final Form form, final int attemptsLeft, final Condition condition,
final String what, final Runnable then) {
if (condition.holds()) {
then.run();
return;
}
if (attemptsLeft <= 0) {
finish(form, "timed out waiting for " + what + "; view="
+ modal.getViewPixelWidth() + "x" + modal.getViewPixelHeight());
return;
}
UITimer.timer(POLL_INTERVAL_MS, false, form, new Runnable() {
public void run() {
poll(form, attemptsLeft - 1, condition, what, then);
}
});
}
}
Loading
Loading