Skip to content

Commit a3effb5

Browse files
shai-almogclaude
andauthored
Standardize native-theme build hints; fix simulator menu bugs (#4855)
Build hints rename: nativeTheme (global), ios.themeMode and and.themeMode (platform-specific). The old keys cn1.nativeTheme and cn1.androidTheme stay accepted as deprecated aliases on every runtime, builder (maven plugin + BuildDaemon), and the simulator's schema editor. Simulator fixes: - Native Theme menu reload now uses frm.dispose() + deinitializeSync() (matching the working skin selector) instead of the unreliable window field, so selecting a theme actually triggers a reload. - "Auto" in the Native Theme menu defers to the project's build hints (ios.themeMode / and.themeMode / nativeTheme), so a project that set ios.themeMode=modern previews iOS Modern instead of being hard-coded to one default. - Restored the "Rotate" menu item for non-single-window mode. It was removed wholesale when the toolbar Portrait/Landscape buttons landed; now gated behind appFrame == null like the sibling Zoom item so single-window users still see only the toolbar buttons. Defaults: - initializr / Playground codenameone_settings.properties (and the bundled common.zip template) ship the renamed keys: nativeTheme, ios.themeMode, and.themeMode all set to modern. - PlaygroundProjectExporter now writes the same defaults into generated project zips downloaded from cn1playground. Playground samples: - Removed trailing form;/root; lines from the bundled samples - the runner already falls back to the first created Form/Component. The exporter also strips form; / root; lines defensively when generating Lifecycle source from a snippet. Docs: - Native-Themes.asciidoc, Advanced-Topics-Under-The-Hood.asciidoc, and the liquid-glass blog post all describe the new naming with the deprecated aliases noted. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ff335a5 commit a3effb5

14 files changed

Lines changed: 338 additions & 107 deletions

File tree

Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4929,16 +4929,27 @@ public void installNativeTheme() {
49294929
return;
49304930
}
49314931
try {
4932-
// Resolve desired theme flavor. cn1.androidTheme is the new per-CN1
4933-
// hint (material | hololight | legacy). The Material 3 modern theme
4934-
// is opt-in via cn1.androidTheme=material / modern. Default is
4935-
// android_holo_light - what master shipped and what existing
4936-
// screenshot goldens are anchored against. The ancient pre-Holo
4937-
// androidTheme.res is only reached via explicit and.hololight=true
4938-
// (historical back-compat) or cn1.androidTheme=legacy.
4939-
String mode = Display.getInstance().getProperty("cn1.androidTheme", null);
4932+
// Resolve desired theme flavor. and.themeMode is the per-platform
4933+
// hint (auto | modern | material | hololight | legacy); the legacy
4934+
// name cn1.androidTheme is still honored for back-compat. The
4935+
// cross-platform shortcut nativeTheme=modern/legacy (deprecated
4936+
// alias: cn1.nativeTheme) feeds in when no platform-specific hint
4937+
// is set. Default stays on android_holo_light - what master
4938+
// shipped and what existing screenshot goldens are anchored
4939+
// against. The ancient pre-Holo androidTheme.res is only reached
4940+
// via explicit and.hololight=true (historical back-compat) or
4941+
// and.themeMode=legacy.
4942+
Display d = Display.getInstance();
4943+
String mode = d.getProperty("and.themeMode",
4944+
d.getProperty("cn1.androidTheme", null));
49404945
if (mode == null) {
4941-
if ("true".equalsIgnoreCase(Display.getInstance().getProperty("and.hololight", "false"))) {
4946+
String shared = d.getProperty("nativeTheme",
4947+
d.getProperty("cn1.nativeTheme", null));
4948+
if ("modern".equalsIgnoreCase(shared)) {
4949+
mode = "material";
4950+
} else if ("legacy".equalsIgnoreCase(shared)) {
4951+
mode = "hololight";
4952+
} else if ("true".equalsIgnoreCase(d.getProperty("and.hololight", "false"))) {
49424953
mode = "legacy";
49434954
} else {
49444955
mode = "hololight";
@@ -4948,7 +4959,7 @@ public void installNativeTheme() {
49484959
}
49494960

49504961
String resPath;
4951-
if ("material".equals(mode) || "modern".equals(mode)) {
4962+
if ("material".equals(mode) || "modern".equals(mode) || "auto".equals(mode)) {
49524963
resPath = "/AndroidMaterialTheme.res";
49534964
} else if ("hololight".equals(mode) || "holo".equals(mode)) {
49544965
resPath = "/android_holo_light.res";

Ports/JavaSE/src/com/codename1/impl/javase/BuildHintSchemaDefaults.java

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,16 @@
1212

1313
/**
1414
* Registers schema metadata for the native-theme build hints
15-
* (ios.themeMode, cn1.androidTheme, cn1.nativeTheme) so that the
15+
* (ios.themeMode, and.themeMode, nativeTheme) so that the
1616
* Build Hints UI inside the Codename One Simulator can show them as
1717
* labelled Select dropdowns instead of opaque key/value entries.
1818
*
19+
* <p>The deprecated keys {@code cn1.nativeTheme} and
20+
* {@code cn1.androidTheme} are still honored at runtime but are no
21+
* longer surfaced in the schema - new projects should use
22+
* {@code nativeTheme} / {@code and.themeMode} (matching the
23+
* {@code ios.themeMode} pattern).
24+
*
1925
* <p><b>Why this class exists:</b> {@link com.codename1.impl.javase.BuildHintEditor}
2026
* is the dialog that lets developers set build hints from the
2127
* Simulator menu (Project &rarr; Build Hints). It populates its rows by
@@ -57,14 +63,15 @@ static void register() {
5763
+ "legacy themes remain selectable via the values below.");
5864

5965
// Cross-platform meta hint.
60-
set("{{#nativeTheme#cn1.nativeTheme}}.label", "Shared override");
61-
set("{{#nativeTheme#cn1.nativeTheme}}.type", "Select");
62-
set("{{#nativeTheme#cn1.nativeTheme}}.values", "modern,legacy,custom");
63-
set("{{#nativeTheme#cn1.nativeTheme}}.description",
66+
set("{{#nativeTheme#nativeTheme}}.label", "Shared override");
67+
set("{{#nativeTheme#nativeTheme}}.type", "Select");
68+
set("{{#nativeTheme#nativeTheme}}.values", "modern,legacy,custom");
69+
set("{{#nativeTheme#nativeTheme}}.description",
6470
"Overrides both iOS and Android native theme selection. "
6571
+ "\"modern\" = liquid glass / Material 3. \"legacy\" = iOS 7 "
6672
+ "flat / Android Holo Light. \"custom\" disables the framework "
67-
+ "default and expects the app to install its own.");
73+
+ "default and expects the app to install its own. "
74+
+ "(Deprecated alias: cn1.nativeTheme.)");
6875

6976
// iOS.
7077
set("{{#nativeTheme#ios.themeMode}}.label", "iOS theme");
@@ -76,13 +83,14 @@ static void register() {
7683
+ "legacy / iphone = pre-iOS7 theme.");
7784

7885
// Android.
79-
set("{{#nativeTheme#cn1.androidTheme}}.label", "Android theme");
80-
set("{{#nativeTheme#cn1.androidTheme}}.type", "Select");
81-
set("{{#nativeTheme#cn1.androidTheme}}.values", "material,hololight,legacy");
82-
set("{{#nativeTheme#cn1.androidTheme}}.description",
83-
"material = Material 3 (default). hololight = Android Holo "
84-
+ "Light (API 14+). legacy = pre-Holo Android theme. "
85-
+ "and.hololight=true is accepted for back-compat.");
86+
set("{{#nativeTheme#and.themeMode}}.label", "Android theme");
87+
set("{{#nativeTheme#and.themeMode}}.type", "Select");
88+
set("{{#nativeTheme#and.themeMode}}.values", "auto,modern,hololight,legacy");
89+
set("{{#nativeTheme#and.themeMode}}.description",
90+
"auto = modern (default). modern / material = Material 3. "
91+
+ "hololight = Android Holo Light (API 14+). legacy = pre-Holo "
92+
+ "Android theme. (Deprecated alias: cn1.androidTheme; "
93+
+ "and.hololight=true is also accepted for back-compat.)");
8694
}
8795

8896
/** Idempotent setter: does not overwrite user / project-level hint metadata. */

Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java

Lines changed: 180 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,126 @@ public void installNativeTheme() {
12311231
}
12321232
}
12331233

1234+
/**
1235+
* Resolves the override theme for the "auto" Native Theme menu choice.
1236+
* Reads ios.themeMode / and.themeMode / nativeTheme from the project's
1237+
* build hints (loaded into system properties as
1238+
* codename1.arg.&lt;hint&gt;), falling back to the deprecated
1239+
* cn1.androidTheme / cn1.nativeTheme aliases. When no hint is set, the
1240+
* platformName drives the choice and "ios" / "and" map to the modern
1241+
* themes that initializr / Playground now default to. Returns the
1242+
* theme resource basename (without ".res") or {@code null} to fall back
1243+
* to the skin's embedded theme.
1244+
*/
1245+
private static String resolveAutoNativeTheme(String platformName) {
1246+
if ("ios".equals(platformName)) {
1247+
String iosMode = buildHint("ios.themeMode");
1248+
if (iosMode != null) {
1249+
if ("modern".equalsIgnoreCase(iosMode) || "liquid".equalsIgnoreCase(iosMode)) {
1250+
return "iOSModernTheme";
1251+
}
1252+
if ("ios7".equalsIgnoreCase(iosMode) || "flat".equalsIgnoreCase(iosMode)) {
1253+
return "iOS7Theme";
1254+
}
1255+
if ("legacy".equalsIgnoreCase(iosMode) || "iphone".equalsIgnoreCase(iosMode)) {
1256+
return "iPhoneTheme";
1257+
}
1258+
}
1259+
String shared = sharedNativeThemeHint();
1260+
if ("legacy".equalsIgnoreCase(shared)) {
1261+
return "iOS7Theme";
1262+
}
1263+
if ("custom".equalsIgnoreCase(shared)) {
1264+
return null;
1265+
}
1266+
// Default for an iOS skin is the modern theme.
1267+
return "iOSModernTheme";
1268+
}
1269+
if ("and".equals(platformName)) {
1270+
String andMode = buildHint("and.themeMode");
1271+
if (andMode == null) {
1272+
andMode = buildHint("cn1.androidTheme");
1273+
}
1274+
if (andMode != null) {
1275+
if ("modern".equalsIgnoreCase(andMode) || "material".equalsIgnoreCase(andMode)) {
1276+
return "AndroidMaterialTheme";
1277+
}
1278+
if ("hololight".equalsIgnoreCase(andMode) || "holo".equalsIgnoreCase(andMode)) {
1279+
return "android_holo_light";
1280+
}
1281+
if ("legacy".equalsIgnoreCase(andMode)) {
1282+
return "androidTheme";
1283+
}
1284+
}
1285+
String shared = sharedNativeThemeHint();
1286+
if ("legacy".equalsIgnoreCase(shared)) {
1287+
return "android_holo_light";
1288+
}
1289+
if ("custom".equalsIgnoreCase(shared)) {
1290+
return null;
1291+
}
1292+
// Default for an Android skin is Material 3.
1293+
return "AndroidMaterialTheme";
1294+
}
1295+
return null;
1296+
}
1297+
1298+
private static String sharedNativeThemeHint() {
1299+
String v = buildHint("nativeTheme");
1300+
if (v == null) {
1301+
v = buildHint("cn1.nativeTheme");
1302+
}
1303+
return v;
1304+
}
1305+
1306+
/**
1307+
* Reads a build hint from the runtime - first the
1308+
* codename1.arg.&lt;name&gt; system property (set by the
1309+
* Maven plugin / Simulator), then the loaded
1310+
* codenameone_settings.properties on disk so unit-test invocations
1311+
* without the simulator wrapper still see the value.
1312+
*/
1313+
private static String buildHint(String name) {
1314+
String v = System.getProperty("codename1.arg." + name);
1315+
if (v != null && !v.isEmpty()) {
1316+
return v;
1317+
}
1318+
Properties cnop = loadCodenameOneSettings();
1319+
if (cnop != null) {
1320+
v = cnop.getProperty("codename1.arg." + name);
1321+
if (v != null && !v.isEmpty()) {
1322+
return v;
1323+
}
1324+
}
1325+
return null;
1326+
}
1327+
1328+
private static Properties cachedCnopProperties;
1329+
private static long cachedCnopMtime = -1L;
1330+
1331+
private static Properties loadCodenameOneSettings() {
1332+
File f = new File(getCWD(), "codenameone_settings.properties");
1333+
if (!f.exists()) {
1334+
f = new File(getCWD(), "common" + File.separator + "codenameone_settings.properties");
1335+
}
1336+
if (!f.exists()) {
1337+
return null;
1338+
}
1339+
long mtime = f.lastModified();
1340+
if (cachedCnopProperties != null && cachedCnopMtime == mtime) {
1341+
return cachedCnopProperties;
1342+
}
1343+
Properties p = new Properties();
1344+
try (FileInputStream in = new FileInputStream(f)) {
1345+
p.load(in);
1346+
} catch (IOException ex) {
1347+
return cachedCnopProperties;
1348+
}
1349+
cachedCnopProperties = p;
1350+
cachedCnopMtime = mtime;
1351+
return p;
1352+
}
1353+
12341354
/**
12351355
* @return the useNativeInput
12361356
*/
@@ -2775,20 +2895,21 @@ private void loadSkinFile(InputStream skin, final JFrame frm) {
27752895
// plus the legacy ones). The user can override via the
27762896
// Simulator's "Native Theme" submenu (stored in the
27772897
// simulatorNativeTheme Preference) or the cn1.forceSimulatorTheme
2778-
// system property. If neither is set, platformName maps ios ->
2779-
// iOSModernTheme and and -> AndroidMaterialTheme. Anything else
2780-
// keeps whatever the skin archive embedded.
2898+
// system property. When "auto" is selected we consult the
2899+
// project's build hints (ios.themeMode / and.themeMode /
2900+
// nativeTheme, plus the deprecated cn1.androidTheme /
2901+
// cn1.nativeTheme aliases) so a project that opted in to
2902+
// ios.themeMode=modern actually previews with the modern
2903+
// theme instead of an unrelated default. If no hint is set we
2904+
// keep mapping platformName ios -> iOSModernTheme and
2905+
// and -> AndroidMaterialTheme - the new defaults shipped by
2906+
// the initializr / Playground - so a brand new simulator run
2907+
// matches what the device build will look like.
27812908
String overrideTheme = System.getProperty("cn1.forceSimulatorTheme",
27822909
Preferences.userNodeForPackage(JavaSEPort.class)
27832910
.get("simulatorNativeTheme", null));
27842911
if (overrideTheme == null || overrideTheme.isEmpty() || "auto".equalsIgnoreCase(overrideTheme)) {
2785-
if ("ios".equals(platformName)) {
2786-
overrideTheme = "iOSModernTheme";
2787-
} else if ("and".equals(platformName)) {
2788-
overrideTheme = "AndroidMaterialTheme";
2789-
} else {
2790-
overrideTheme = null;
2791-
}
2912+
overrideTheme = resolveAutoNativeTheme(platformName);
27922913
} else if ("embedded".equalsIgnoreCase(overrideTheme)) {
27932914
// Explicit "keep the skin's embedded theme".
27942915
overrideTheme = null;
@@ -3667,6 +3788,41 @@ public void itemStateChanged(ItemEvent e) {
36673788
});
36683789
simulatorMenu.add(autoLocalizationMenu);
36693790

3791+
// Rotate menu item: only added when app-frame mode is off. The
3792+
// app-frame toolbar already exposes Portrait / Landscape RotateAction
3793+
// buttons, so duplicating them in the menu would be confusing. When
3794+
// the user has Single Window mode disabled there is no toolbar, so
3795+
// the only way to rotate is via this menu item - it was lost when
3796+
// the toolbar buttons were introduced and the menu entry was
3797+
// removed wholesale instead of gated behind appFrame == null.
3798+
final JMenuItem rotateMenu = new JMenuItem("Rotate");
3799+
rotateMenu.setEnabled(!desktopSkin);
3800+
rotateMenu.addActionListener(new ActionListener() {
3801+
@Override
3802+
public void actionPerformed(ActionEvent ae) {
3803+
setPortrait(!portrait);
3804+
Container parent = canvas.getParent();
3805+
parent.remove(canvas);
3806+
canvas.setForcedSize(new java.awt.Dimension(
3807+
(int) (getSkin().getWidth() * zoomLevel),
3808+
(int) (getSkin().getHeight() * zoomLevel)));
3809+
if (window != null) {
3810+
window.setSize(new java.awt.Dimension(
3811+
(int) (getSkin().getWidth() * zoomLevel),
3812+
(int) (getSkin().getHeight() * zoomLevel)));
3813+
}
3814+
java.awt.Container top = ((JComponent) parent).getTopLevelAncestor();
3815+
top.revalidate();
3816+
top.repaint();
3817+
parent.add(BorderLayout.CENTER, canvas);
3818+
if (window != null) {
3819+
window.pack();
3820+
}
3821+
JavaSEPort.this.sizeChanged(getScreenCoordinates().width, getScreenCoordinates().height);
3822+
}
3823+
});
3824+
if (appFrame == null) simulatorMenu.add(rotateMenu);
3825+
36703826
final JCheckBoxMenuItem zoomMenu = new JCheckBoxMenuItem("Zoom", scrollableSkin);
36713827
if (appFrame == null) simulatorMenu.add(zoomMenu);
36723828

@@ -4671,7 +4827,7 @@ public void actionPerformed(ActionEvent e) {
46714827
bar.add(simulateMenu);
46724828
bar.add(toolsMenu);
46734829
bar.add(skinMenu);
4674-
bar.add(createNativeThemeMenu());
4830+
bar.add(createNativeThemeMenu(frm));
46754831
bar.add(helpMenu);
46764832
}
46774833

@@ -4780,18 +4936,21 @@ private String getCurrentSkinName() {
47804936
}
47814937

47824938
/**
4783-
* Build the Native Theme override menu. By default the simulator picks a
4784-
* theme from the current skin's platformName ("ios" -&gt; iOSModernTheme,
4785-
* "and" -&gt; AndroidMaterialTheme); this menu lets the user force one
4786-
* of the shipped themes or "Use skin's embedded theme" to bypass the
4787-
* heuristic entirely. Selection is written to the simulatorNativeTheme
4788-
* Preference and the simulator is reloaded.
4939+
* Build the Native Theme override menu. "Auto" defers to the project's
4940+
* build hints (ios.themeMode / and.themeMode / nativeTheme) so a project
4941+
* that opted in via codenameone_settings.properties previews with the
4942+
* theme it would ship with; the explicit choices below force a
4943+
* specific theme regardless of build hints. Selection is written to
4944+
* the simulatorNativeTheme Preference and the simulator is reloaded
4945+
* via {@code reload.simulator} - the same mechanism the skin menu
4946+
* uses, so disposing the JFrame is what actually triggers the
4947+
* Simulator polling thread to pick up the new theme.
47894948
*/
4790-
private JMenu createNativeThemeMenu() {
4949+
private JMenu createNativeThemeMenu(final JFrame frm) {
47914950
JMenu m = new JMenu("Native Theme");
47924951
m.setDoubleBuffered(true);
47934952
String[][] items = {
4794-
{"auto", "Auto (based on skin)"},
4953+
{"auto", "Auto (from build hints)"},
47954954
{"iOSModernTheme", "iOS Modern (Liquid Glass)"},
47964955
{"iOS7Theme", "iOS 7 (Flat)"},
47974956
{"iPhoneTheme", "iPhone (Pre-Flat)"},
@@ -4810,10 +4969,9 @@ private JMenu createNativeThemeMenu() {
48104969
public void actionPerformed(ActionEvent e) {
48114970
Preferences.userNodeForPackage(JavaSEPort.class)
48124971
.put("simulatorNativeTheme", entry[0]);
4972+
deinitializeSync();
4973+
frm.dispose();
48134974
System.setProperty("reload.simulator", "true");
4814-
if (window != null) {
4815-
window.dispose();
4816-
}
48174975
}
48184976
});
48194977
group.add(mi);

0 commit comments

Comments
 (0)