diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java index 632ca0b996..5dfb5ea1b0 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java @@ -4929,16 +4929,27 @@ public void installNativeTheme() { return; } try { - // Resolve desired theme flavor. cn1.androidTheme is the new per-CN1 - // hint (material | hololight | legacy). The Material 3 modern theme - // is opt-in via cn1.androidTheme=material / modern. Default is - // android_holo_light - what master shipped and what existing - // screenshot goldens are anchored against. The ancient pre-Holo - // androidTheme.res is only reached via explicit and.hololight=true - // (historical back-compat) or cn1.androidTheme=legacy. - String mode = Display.getInstance().getProperty("cn1.androidTheme", null); + // Resolve desired theme flavor. and.themeMode is the per-platform + // hint (auto | modern | material | hololight | legacy); the legacy + // name cn1.androidTheme is still honored for back-compat. The + // cross-platform shortcut nativeTheme=modern/legacy (deprecated + // alias: cn1.nativeTheme) feeds in when no platform-specific hint + // is set. Default stays on android_holo_light - what master + // shipped and what existing screenshot goldens are anchored + // against. The ancient pre-Holo androidTheme.res is only reached + // via explicit and.hololight=true (historical back-compat) or + // and.themeMode=legacy. + Display d = Display.getInstance(); + String mode = d.getProperty("and.themeMode", + d.getProperty("cn1.androidTheme", null)); if (mode == null) { - if ("true".equalsIgnoreCase(Display.getInstance().getProperty("and.hololight", "false"))) { + String shared = d.getProperty("nativeTheme", + d.getProperty("cn1.nativeTheme", null)); + if ("modern".equalsIgnoreCase(shared)) { + mode = "material"; + } else if ("legacy".equalsIgnoreCase(shared)) { + mode = "hololight"; + } else if ("true".equalsIgnoreCase(d.getProperty("and.hololight", "false"))) { mode = "legacy"; } else { mode = "hololight"; @@ -4948,7 +4959,7 @@ public void installNativeTheme() { } String resPath; - if ("material".equals(mode) || "modern".equals(mode)) { + if ("material".equals(mode) || "modern".equals(mode) || "auto".equals(mode)) { resPath = "/AndroidMaterialTheme.res"; } else if ("hololight".equals(mode) || "holo".equals(mode)) { resPath = "/android_holo_light.res"; diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/BuildHintSchemaDefaults.java b/Ports/JavaSE/src/com/codename1/impl/javase/BuildHintSchemaDefaults.java index 484d9d1cdd..a749a31e14 100644 --- a/Ports/JavaSE/src/com/codename1/impl/javase/BuildHintSchemaDefaults.java +++ b/Ports/JavaSE/src/com/codename1/impl/javase/BuildHintSchemaDefaults.java @@ -12,10 +12,16 @@ /** * Registers schema metadata for the native-theme build hints - * (ios.themeMode, cn1.androidTheme, cn1.nativeTheme) so that the + * (ios.themeMode, and.themeMode, nativeTheme) so that the * Build Hints UI inside the Codename One Simulator can show them as * labelled Select dropdowns instead of opaque key/value entries. * + *

The deprecated keys {@code cn1.nativeTheme} and + * {@code cn1.androidTheme} are still honored at runtime but are no + * longer surfaced in the schema - new projects should use + * {@code nativeTheme} / {@code and.themeMode} (matching the + * {@code ios.themeMode} pattern). + * *

Why this class exists: {@link com.codename1.impl.javase.BuildHintEditor} * is the dialog that lets developers set build hints from the * Simulator menu (Project → Build Hints). It populates its rows by @@ -57,14 +63,15 @@ static void register() { + "legacy themes remain selectable via the values below."); // Cross-platform meta hint. - set("{{#nativeTheme#cn1.nativeTheme}}.label", "Shared override"); - set("{{#nativeTheme#cn1.nativeTheme}}.type", "Select"); - set("{{#nativeTheme#cn1.nativeTheme}}.values", "modern,legacy,custom"); - set("{{#nativeTheme#cn1.nativeTheme}}.description", + set("{{#nativeTheme#nativeTheme}}.label", "Shared override"); + set("{{#nativeTheme#nativeTheme}}.type", "Select"); + set("{{#nativeTheme#nativeTheme}}.values", "modern,legacy,custom"); + set("{{#nativeTheme#nativeTheme}}.description", "Overrides both iOS and Android native theme selection. " + "\"modern\" = liquid glass / Material 3. \"legacy\" = iOS 7 " + "flat / Android Holo Light. \"custom\" disables the framework " - + "default and expects the app to install its own."); + + "default and expects the app to install its own. " + + "(Deprecated alias: cn1.nativeTheme.)"); // iOS. set("{{#nativeTheme#ios.themeMode}}.label", "iOS theme"); @@ -76,13 +83,14 @@ static void register() { + "legacy / iphone = pre-iOS7 theme."); // Android. - set("{{#nativeTheme#cn1.androidTheme}}.label", "Android theme"); - set("{{#nativeTheme#cn1.androidTheme}}.type", "Select"); - set("{{#nativeTheme#cn1.androidTheme}}.values", "material,hololight,legacy"); - set("{{#nativeTheme#cn1.androidTheme}}.description", - "material = Material 3 (default). hololight = Android Holo " - + "Light (API 14+). legacy = pre-Holo Android theme. " - + "and.hololight=true is accepted for back-compat."); + set("{{#nativeTheme#and.themeMode}}.label", "Android theme"); + set("{{#nativeTheme#and.themeMode}}.type", "Select"); + set("{{#nativeTheme#and.themeMode}}.values", "auto,modern,hololight,legacy"); + set("{{#nativeTheme#and.themeMode}}.description", + "auto = modern (default). modern / material = Material 3. " + + "hololight = Android Holo Light (API 14+). legacy = pre-Holo " + + "Android theme. (Deprecated alias: cn1.androidTheme; " + + "and.hololight=true is also accepted for back-compat.)"); } /** Idempotent setter: does not overwrite user / project-level hint metadata. */ diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java index 72320f31e7..28cc75a623 100644 --- a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java +++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java @@ -1231,6 +1231,126 @@ public void installNativeTheme() { } } + /** + * Resolves the override theme for the "auto" Native Theme menu choice. + * Reads ios.themeMode / and.themeMode / nativeTheme from the project's + * build hints (loaded into system properties as + * codename1.arg.<hint>), falling back to the deprecated + * cn1.androidTheme / cn1.nativeTheme aliases. When no hint is set, the + * platformName drives the choice and "ios" / "and" map to the modern + * themes that initializr / Playground now default to. Returns the + * theme resource basename (without ".res") or {@code null} to fall back + * to the skin's embedded theme. + */ + private static String resolveAutoNativeTheme(String platformName) { + if ("ios".equals(platformName)) { + String iosMode = buildHint("ios.themeMode"); + if (iosMode != null) { + if ("modern".equalsIgnoreCase(iosMode) || "liquid".equalsIgnoreCase(iosMode)) { + return "iOSModernTheme"; + } + if ("ios7".equalsIgnoreCase(iosMode) || "flat".equalsIgnoreCase(iosMode)) { + return "iOS7Theme"; + } + if ("legacy".equalsIgnoreCase(iosMode) || "iphone".equalsIgnoreCase(iosMode)) { + return "iPhoneTheme"; + } + } + String shared = sharedNativeThemeHint(); + if ("legacy".equalsIgnoreCase(shared)) { + return "iOS7Theme"; + } + if ("custom".equalsIgnoreCase(shared)) { + return null; + } + // Default for an iOS skin is the modern theme. + return "iOSModernTheme"; + } + if ("and".equals(platformName)) { + String andMode = buildHint("and.themeMode"); + if (andMode == null) { + andMode = buildHint("cn1.androidTheme"); + } + if (andMode != null) { + if ("modern".equalsIgnoreCase(andMode) || "material".equalsIgnoreCase(andMode)) { + return "AndroidMaterialTheme"; + } + if ("hololight".equalsIgnoreCase(andMode) || "holo".equalsIgnoreCase(andMode)) { + return "android_holo_light"; + } + if ("legacy".equalsIgnoreCase(andMode)) { + return "androidTheme"; + } + } + String shared = sharedNativeThemeHint(); + if ("legacy".equalsIgnoreCase(shared)) { + return "android_holo_light"; + } + if ("custom".equalsIgnoreCase(shared)) { + return null; + } + // Default for an Android skin is Material 3. + return "AndroidMaterialTheme"; + } + return null; + } + + private static String sharedNativeThemeHint() { + String v = buildHint("nativeTheme"); + if (v == null) { + v = buildHint("cn1.nativeTheme"); + } + return v; + } + + /** + * Reads a build hint from the runtime - first the + * codename1.arg.<name> system property (set by the + * Maven plugin / Simulator), then the loaded + * codenameone_settings.properties on disk so unit-test invocations + * without the simulator wrapper still see the value. + */ + private static String buildHint(String name) { + String v = System.getProperty("codename1.arg." + name); + if (v != null && !v.isEmpty()) { + return v; + } + Properties cnop = loadCodenameOneSettings(); + if (cnop != null) { + v = cnop.getProperty("codename1.arg." + name); + if (v != null && !v.isEmpty()) { + return v; + } + } + return null; + } + + private static Properties cachedCnopProperties; + private static long cachedCnopMtime = -1L; + + private static Properties loadCodenameOneSettings() { + File f = new File(getCWD(), "codenameone_settings.properties"); + if (!f.exists()) { + f = new File(getCWD(), "common" + File.separator + "codenameone_settings.properties"); + } + if (!f.exists()) { + return null; + } + long mtime = f.lastModified(); + if (cachedCnopProperties != null && cachedCnopMtime == mtime) { + return cachedCnopProperties; + } + Properties p = new Properties(); + try (FileInputStream in = new FileInputStream(f)) { + p.load(in); + } catch (IOException ex) { + return cachedCnopProperties; + } + cachedCnopProperties = p; + cachedCnopMtime = mtime; + return p; + } + /** * @return the useNativeInput */ @@ -2775,20 +2895,21 @@ private void loadSkinFile(InputStream skin, final JFrame frm) { // plus the legacy ones). The user can override via the // Simulator's "Native Theme" submenu (stored in the // simulatorNativeTheme Preference) or the cn1.forceSimulatorTheme - // system property. If neither is set, platformName maps ios -> - // iOSModernTheme and and -> AndroidMaterialTheme. Anything else - // keeps whatever the skin archive embedded. + // system property. When "auto" is selected we consult the + // project's build hints (ios.themeMode / and.themeMode / + // nativeTheme, plus the deprecated cn1.androidTheme / + // cn1.nativeTheme aliases) so a project that opted in to + // ios.themeMode=modern actually previews with the modern + // theme instead of an unrelated default. If no hint is set we + // keep mapping platformName ios -> iOSModernTheme and + // and -> AndroidMaterialTheme - the new defaults shipped by + // the initializr / Playground - so a brand new simulator run + // matches what the device build will look like. String overrideTheme = System.getProperty("cn1.forceSimulatorTheme", Preferences.userNodeForPackage(JavaSEPort.class) .get("simulatorNativeTheme", null)); if (overrideTheme == null || overrideTheme.isEmpty() || "auto".equalsIgnoreCase(overrideTheme)) { - if ("ios".equals(platformName)) { - overrideTheme = "iOSModernTheme"; - } else if ("and".equals(platformName)) { - overrideTheme = "AndroidMaterialTheme"; - } else { - overrideTheme = null; - } + overrideTheme = resolveAutoNativeTheme(platformName); } else if ("embedded".equalsIgnoreCase(overrideTheme)) { // Explicit "keep the skin's embedded theme". overrideTheme = null; @@ -3667,6 +3788,41 @@ public void itemStateChanged(ItemEvent e) { }); simulatorMenu.add(autoLocalizationMenu); + // Rotate menu item: only added when app-frame mode is off. The + // app-frame toolbar already exposes Portrait / Landscape RotateAction + // buttons, so duplicating them in the menu would be confusing. When + // the user has Single Window mode disabled there is no toolbar, so + // the only way to rotate is via this menu item - it was lost when + // the toolbar buttons were introduced and the menu entry was + // removed wholesale instead of gated behind appFrame == null. + final JMenuItem rotateMenu = new JMenuItem("Rotate"); + rotateMenu.setEnabled(!desktopSkin); + rotateMenu.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent ae) { + setPortrait(!portrait); + Container parent = canvas.getParent(); + parent.remove(canvas); + canvas.setForcedSize(new java.awt.Dimension( + (int) (getSkin().getWidth() * zoomLevel), + (int) (getSkin().getHeight() * zoomLevel))); + if (window != null) { + window.setSize(new java.awt.Dimension( + (int) (getSkin().getWidth() * zoomLevel), + (int) (getSkin().getHeight() * zoomLevel))); + } + java.awt.Container top = ((JComponent) parent).getTopLevelAncestor(); + top.revalidate(); + top.repaint(); + parent.add(BorderLayout.CENTER, canvas); + if (window != null) { + window.pack(); + } + JavaSEPort.this.sizeChanged(getScreenCoordinates().width, getScreenCoordinates().height); + } + }); + if (appFrame == null) simulatorMenu.add(rotateMenu); + final JCheckBoxMenuItem zoomMenu = new JCheckBoxMenuItem("Zoom", scrollableSkin); if (appFrame == null) simulatorMenu.add(zoomMenu); @@ -4671,7 +4827,7 @@ public void actionPerformed(ActionEvent e) { bar.add(simulateMenu); bar.add(toolsMenu); bar.add(skinMenu); - bar.add(createNativeThemeMenu()); + bar.add(createNativeThemeMenu(frm)); bar.add(helpMenu); } @@ -4780,18 +4936,21 @@ private String getCurrentSkinName() { } /** - * Build the Native Theme override menu. By default the simulator picks a - * theme from the current skin's platformName ("ios" -> iOSModernTheme, - * "and" -> AndroidMaterialTheme); this menu lets the user force one - * of the shipped themes or "Use skin's embedded theme" to bypass the - * heuristic entirely. Selection is written to the simulatorNativeTheme - * Preference and the simulator is reloaded. + * Build the Native Theme override menu. "Auto" defers to the project's + * build hints (ios.themeMode / and.themeMode / nativeTheme) so a project + * that opted in via codenameone_settings.properties previews with the + * theme it would ship with; the explicit choices below force a + * specific theme regardless of build hints. Selection is written to + * the simulatorNativeTheme Preference and the simulator is reloaded + * via {@code reload.simulator} - the same mechanism the skin menu + * uses, so disposing the JFrame is what actually triggers the + * Simulator polling thread to pick up the new theme. */ - private JMenu createNativeThemeMenu() { + private JMenu createNativeThemeMenu(final JFrame frm) { JMenu m = new JMenu("Native Theme"); m.setDoubleBuffered(true); String[][] items = { - {"auto", "Auto (based on skin)"}, + {"auto", "Auto (from build hints)"}, {"iOSModernTheme", "iOS Modern (Liquid Glass)"}, {"iOS7Theme", "iOS 7 (Flat)"}, {"iPhoneTheme", "iPhone (Pre-Flat)"}, @@ -4810,10 +4969,9 @@ private JMenu createNativeThemeMenu() { public void actionPerformed(ActionEvent e) { Preferences.userNodeForPackage(JavaSEPort.class) .put("simulatorNativeTheme", entry[0]); + deinitializeSync(); + frm.dispose(); System.setProperty("reload.simulator", "true"); - if (window != null) { - window.dispose(); - } } }); group.add(mi); diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java index 0ecfd85d57..3a7010d046 100644 --- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java @@ -2658,29 +2658,53 @@ public void setHeight(HTMLCanvasElement canvas, int canvasHeight) { public void installNativeTheme(){ try { // Prefer the modern native theme when explicitly requested via - // ios.themeMode / cn1.androidTheme / javascript.native.theme. If - // the hint isn't set we keep the pre-existing JS-port default - // (iOS 7 / Holo Light) since the JS bundle may not include the - // modern .res files (scripts/build-native-themes.sh has to have - // mirrored them before the JS bundle was produced). + // ios.themeMode / and.themeMode (legacy alias: cn1.androidTheme) + // / nativeTheme (legacy alias: cn1.nativeTheme) / + // javascript.native.theme. If no hint is set we keep the + // pre-existing JS-port default (iOS 7 / Holo Light) since the JS + // bundle may not include the modern .res files + // (scripts/build-native-themes.sh has to have mirrored them + // before the JS bundle was produced). String defaultTheme = isAndroid_() ? "/android_holo_light.res" : "/iOS7Theme.res"; - String iosMode = Display.getInstance().getProperty("ios.themeMode", null); - String androidMode = Display.getInstance().getProperty("cn1.androidTheme", null); - if (isAndroid_() && androidMode != null) { - androidMode = androidMode.toLowerCase(); - if ("material".equals(androidMode) || "modern".equals(androidMode)) { - defaultTheme = "/AndroidMaterialTheme.res"; - } else if ("legacy".equals(androidMode)) { - defaultTheme = "/androidTheme.res"; - } else if ("hololight".equals(androidMode) || "holo".equals(androidMode)) { - defaultTheme = "/android_holo_light.res"; - } - } else if (!isAndroid_() && iosMode != null) { - iosMode = iosMode.toLowerCase(); - if ("modern".equals(iosMode) || "liquid".equals(iosMode) || "auto".equals(iosMode)) { - defaultTheme = "/iOSModernTheme.res"; - } else if ("legacy".equals(iosMode) || "iphone".equals(iosMode)) { - defaultTheme = "/iPhoneTheme.res"; + Display d = Display.getInstance(); + String iosMode = d.getProperty("ios.themeMode", null); + String androidMode = d.getProperty("and.themeMode", + d.getProperty("cn1.androidTheme", null)); + String shared = d.getProperty("nativeTheme", + d.getProperty("cn1.nativeTheme", null)); + if (isAndroid_()) { + if (androidMode == null && shared != null) { + if ("modern".equalsIgnoreCase(shared)) { + androidMode = "material"; + } else if ("legacy".equalsIgnoreCase(shared)) { + androidMode = "hololight"; + } + } + if (androidMode != null) { + androidMode = androidMode.toLowerCase(); + if ("material".equals(androidMode) || "modern".equals(androidMode) || "auto".equals(androidMode)) { + defaultTheme = "/AndroidMaterialTheme.res"; + } else if ("legacy".equals(androidMode)) { + defaultTheme = "/androidTheme.res"; + } else if ("hololight".equals(androidMode) || "holo".equals(androidMode)) { + defaultTheme = "/android_holo_light.res"; + } + } + } else { + if (iosMode == null && shared != null) { + if ("modern".equalsIgnoreCase(shared)) { + iosMode = "modern"; + } else if ("legacy".equalsIgnoreCase(shared)) { + iosMode = "ios7"; + } + } + if (iosMode != null) { + iosMode = iosMode.toLowerCase(); + if ("modern".equals(iosMode) || "liquid".equals(iosMode) || "auto".equals(iosMode)) { + defaultTheme = "/iOSModernTheme.res"; + } else if ("legacy".equals(iosMode) || "iphone".equals(iosMode)) { + defaultTheme = "/iPhoneTheme.res"; + } } } String nativeTheme = Display.getInstance().getProperty("javascript.native.theme", defaultTheme); diff --git a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc index e2be5d4afa..c0e62174b7 100644 --- a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc +++ b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc @@ -313,11 +313,11 @@ Only supported for App Store builds. See https://www.codenameone.com/developer-g |ios.themeMode |`auto` (default), `modern`, `ios7`, `legacy`. `auto` (unset) keeps the existing iOS 7 flat theme so pre-refactor screenshot goldens and apps see no behavior change. `modern` / `liquid` opts in to the CSS-generated iOS Modern (liquid-glass) theme shipped from `native-themes/ios-modern/theme.css`. `ios7` / `flat` is the same as `auto` - pre-liquid iOS 7 flat theme; `legacy` / `iphone` loads the pre-iOS 7 iPhone theme. The `auto` -> modern flip is planned for a future release. -|cn1.androidTheme -|`hololight` (default), `material`, `legacy`. Default is `hololight` (Android Holo Light, what the framework shipped on API 14+ before this refactor). `material` / `modern` opts in to the CSS-generated Android Material 3 theme from `native-themes/android-material/theme.css`. `legacy` loads the pre-Holo Android theme. `and.hololight=true` is still accepted for back-compat and maps to `hololight`. The default stays on `hololight` until you flip in a future release. +|and.themeMode +|`auto`, `modern` / `material`, `hololight` (default for existing apps), `legacy`. `auto` and `modern` / `material` opt in to the CSS-generated Android Material 3 theme from `native-themes/android-material/theme.css`. `hololight` is Android Holo Light (what the framework shipped on API 14+ before this refactor). `legacy` loads the pre-Holo Android theme. The legacy alias `cn1.androidTheme` is still accepted, and `and.hololight=true` still maps to `hololight`. The default stays on `hololight` for existing apps until you flip in a future release. -|cn1.nativeTheme -|`modern`, `legacy`, `custom` (default unset). Cross-platform override that sets both `ios.themeMode` and `cn1.androidTheme` together when those aren't set explicitly. `modern` = liquid glass + Material 3, `legacy` = iOS 7 flat + Holo Light, `custom` disables the framework native theme entirely. +|nativeTheme +|`modern`, `legacy`, `custom` (default unset). Cross-platform override that sets both `ios.themeMode` and `and.themeMode` together when those aren't set explicitly. `modern` = liquid glass + Material 3, `legacy` = iOS 7 flat + Holo Light, `custom` disables the framework native theme entirely. The legacy alias `cn1.nativeTheme` is still accepted. |ios.interface_orientation |UIInterfaceOrientationPortrait by default. Indicates the orientation, one or more of (separated by colon :): `UIInterfaceOrientationPortrait`, `UIInterfaceOrientationPortraitUpsideDown`, `UIInterfaceOrientationLandscapeLeft`, `UIInterfaceOrientationLandscapeRight`. Notice that the IDE plugin has an "Interface Orientation" combo box you *should* use under the iOS section. diff --git a/docs/developer-guide/Native-Themes.asciidoc b/docs/developer-guide/Native-Themes.asciidoc index 2401407fe8..b4affb0f85 100644 --- a/docs/developer-guide/Native-Themes.asciidoc +++ b/docs/developer-guide/Native-Themes.asciidoc @@ -30,25 +30,27 @@ legacy theme it always did. pre-iOS 7 theme. The `auto` -> `modern` flip is planned for a future release. -|`cn1.androidTheme` -|`hololight` (default) + -`material` / `modern` + +|`and.themeMode` +|`auto` / `modern` / `material` + +`hololight` (legacy default) + `legacy` -|`hololight` is Android Holo Light. `material` / `modern` opts in -to the modern theme generated from -`native-themes/android-material/theme.css`. `legacy` is the -pre-Holo theme. +|`auto` and `modern` / `material` opt in to the modern theme +generated from `native-themes/android-material/theme.css`. +`hololight` is Android Holo Light (the pre-modern default for +existing apps). `legacy` is the pre-Holo theme. The legacy +alias `cn1.androidTheme` is still honored for back-compat. -|`cn1.nativeTheme` +|`nativeTheme` |`modern` / `legacy` / `custom` |Cross-platform shortcut that sets both `ios.themeMode` and -`cn1.androidTheme` together. `modern` = liquid glass + Material 3, +`and.themeMode` together. `modern` = liquid glass + Material 3, `legacy` = iOS 7 + Holo Light, `custom` disables the framework -theme entirely so your own `theme.css` owns the base layer. +theme entirely so your own `theme.css` owns the base layer. The +legacy alias `cn1.nativeTheme` is still honored for back-compat. |=== The legacy `and.hololight=true` hint still works and maps to -`cn1.androidTheme=hololight`. +`and.themeMode=hololight`. === Light and dark mode diff --git a/docs/website/content/blog/liquid-glass-material-3-modern-native-themes.md b/docs/website/content/blog/liquid-glass-material-3-modern-native-themes.md index 3bfab9944f..404abcdd73 100644 --- a/docs/website/content/blog/liquid-glass-material-3-modern-native-themes.md +++ b/docs/website/content/blog/liquid-glass-material-3-modern-native-themes.md @@ -93,15 +93,15 @@ Three pieces, all live: - **Themes are bundled.** The simulator jar-with-dependencies includes both modern themes alongside the four legacy themes (`iPhoneTheme`, `iOS7Theme`, `androidTheme`, `android_holo_light`) at the root of the jar. The simulator can pick any one of them at runtime without touching the skin repo. - **A new "Native Theme" menu.** Right next to the Skins menu there is now a Native Theme menu with a radio group for the six themes plus "Auto" and "Use skin's embedded theme". Selecting one writes the `simulatorNativeTheme` Preference, flips the simulator-reload flag, and disposes the current window so the skin reloader kicks in with the new theme. You can sit on a single skin and flip through every native theme in seconds. -- **Build hints know about it.** The new `cn1.nativeTheme`, `ios.themeMode`, and `cn1.androidTheme` build hints are registered with the simulator's Build Hints UI on launch — labels, types, value lists, descriptions, the lot. Set them in the Build Hints dialog, in `codenameone_settings.properties`, or via `-D` system properties; they flow through to the device build and the simulator both. +- **Build hints know about it.** The new `nativeTheme`, `ios.themeMode`, and `and.themeMode` build hints are registered with the simulator's Build Hints UI on launch — labels, types, value lists, descriptions, the lot. (The legacy keys `cn1.nativeTheme` and `cn1.androidTheme` are still honored for back-compat.) Set them in the Build Hints dialog, in `codenameone_settings.properties`, or via `-D` system properties; they flow through to the device build and the simulator both. -By default an iOS skin maps to the iOS Modern theme, an Android skin maps to Android Material 3. Set `-Dcn1.forceSimulatorTheme` or pick from the menu to override. Pick "Use skin's embedded theme" to bypass the override entirely and get whatever the skin shipped with. +The "Auto" choice in the Native Theme menu defers to those build hints — set `ios.themeMode=modern` in your project's settings and "Auto" previews iOS Modern; flip the same project to `ios.themeMode=ios7` and "Auto" previews iOS 7. The explicit menu entries (iOS Modern, iOS 7, etc.) override the hints regardless. `-Dcn1.forceSimulatorTheme` is still honored as the highest-priority override; pick "Use skin's embedded theme" to bypass the framework theme entirely and get whatever the skin shipped with. ## On devices -The opt-in is the same on iOS and Android. Set `ios.themeMode=modern` (other accepted values: `liquid`, `auto`, `ios7`, `flat`) and `cn1.androidTheme=material` (`material`, `hololight`, `legacy`) in your project's `codenameone_settings.properties`, or as Build Hints in the simulator UI. There is a single cross-platform shortcut, `cn1.nativeTheme=modern`, which the iOS builder consults when `ios.themeMode` is unset and which the Android port reads at runtime as a default for `cn1.androidTheme`. Existing `and.hololight=true` projects keep their Holo Light look — that hint is still honored for back-compat. +The opt-in is the same on iOS and Android. The platform knobs follow a single naming pattern — `ios.themeMode` and `and.themeMode` — and accept `modern` / `liquid` / `auto` / `ios7` / `flat` on iOS, `modern` / `material` / `auto` / `hololight` / `legacy` on Android. There is a single cross-platform shortcut, `nativeTheme=modern`, which the iOS builder consults when `ios.themeMode` is unset and which the Android port reads at runtime as a default for `and.themeMode`. The legacy aliases `cn1.androidTheme` and `cn1.nativeTheme` are still honored for back-compat, as is `and.hololight=true`. -The default for an existing app stays on legacy on every platform. We do not flip a 15-year-old app's look without an opt-in. New apps generated from the initializr ship with `ios.themeMode=modern`, `cn1.androidTheme=material`, and `cn1.nativeTheme=modern` already set in `codenameone_settings.properties`, so a brand new project starts with the modern themes preselected. The Playground does the same. +The default for an existing app stays on legacy on every platform. We do not flip a 15-year-old app's look without an opt-in. New apps generated from the initializr ship with `nativeTheme=modern`, `ios.themeMode=modern`, and `and.themeMode=modern` already set in `codenameone_settings.properties`, so a brand new project starts with the modern themes preselected. The Playground does the same, and Playground project downloads carry the same defaults into the generated `codenameone_settings.properties`. The HTML5 port has the runtime support for the modern themes but does not bundle them with user apps yet — that is one of the loose ends we want to close in the next round. @@ -167,7 +167,7 @@ Neither half is finished. They are both ongoing, and they both depend on communi We are sitting at **496 open issues** as of this post. That is slow but steady progress — the number is moving in the right direction week over week, and the issues that close tend to ship as features or fixes you can see, not as silent triage. If you have a problem, [file it](https://github.com/codenameone/CodenameOne/issues). If you have an RFE, file that too. The themes you saw above started as an RFE. -You can try the new themes today by opening the [Playground](/playground), by setting `ios.themeMode=modern` and `cn1.androidTheme=material` in your project's `codenameone_settings.properties`, or by picking them from the simulator's new Native Theme menu. New projects from the initializr already have them on. The shipping resources are bundled in the iOS and Android ports as of this week. +You can try the new themes today by opening the [Playground](/playground), by setting `nativeTheme=modern` (or `ios.themeMode=modern` / `and.themeMode=modern` for finer control) in your project's `codenameone_settings.properties`, or by picking them from the simulator's new Native Theme menu. New projects from the initializr already have them on. The shipping resources are bundled in the iOS and Android ports as of this week. --- diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java index 376b111e9d..50f4fe80fb 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java @@ -2051,6 +2051,35 @@ public void usesClassMethod(String cls, String method) { storeIds = ""; } + // Native modern theme hints - propagated to Display.getProperty so + // AndroidImplementation.installNativeTheme() can pick the + // material / hololight / legacy variant. and.themeMode is the + // current platform-specific knob (cn1.androidTheme is honored as a + // deprecated alias). nativeTheme is the cross-platform shortcut + // (cn1.nativeTheme is its deprecated alias). + String andThemeHint = request.getArg("and.themeMode", null); + String androidThemeHint = request.getArg("cn1.androidTheme", null); + String nativeThemeHintNew = request.getArg("nativeTheme", null); + String nativeThemeHint = request.getArg("cn1.nativeTheme", null); + StringBuilder nativeThemeProps = new StringBuilder(); + if (andThemeHint != null) { + nativeThemeProps.append(" Display.getInstance().setProperty(\"and.themeMode\", \"") + .append(andThemeHint).append("\");\n"); + } + if (androidThemeHint != null) { + nativeThemeProps.append(" Display.getInstance().setProperty(\"cn1.androidTheme\", \"") + .append(androidThemeHint).append("\");\n"); + } + if (nativeThemeHintNew != null) { + nativeThemeProps.append(" Display.getInstance().setProperty(\"nativeTheme\", \"") + .append(nativeThemeHintNew).append("\");\n"); + } + if (nativeThemeHint != null) { + nativeThemeProps.append(" Display.getInstance().setProperty(\"cn1.nativeTheme\", \"") + .append(nativeThemeHint).append("\");\n"); + } + String nativeThemeStubProps = nativeThemeProps.toString(); + String gcmSenderId = request.getArg("gcm.sender_id", null); if (gcmSenderId != null) { gcmSenderId = " Display.getInstance().setProperty(\"gcm.sender_id\", \"" + gcmSenderId + "\");\n"; @@ -2770,6 +2799,7 @@ public void usesClassMethod(String cls, String method) { + reinitCode0 + storeIds + gcmSenderId + + nativeThemeStubProps + " Display.getInstance().setProperty(\"build_key\", d(BUILD_KEY));\n" + " Display.getInstance().setProperty(\"package_name\", PACKAGE_NAME);\n" + " Display.getInstance().setProperty(\"built_by_user\", d(BUILT_BY_USER));\n" diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java index 609536dee9..73c8796a38 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java @@ -356,12 +356,14 @@ public boolean build(File sourceZip, BuildRequest request) throws BuildException } debug("Xcode version is "+xcodeVersion); - // ios.themeMode stays the platform-specific knob; cn1.nativeTheme is + // ios.themeMode stays the platform-specific knob; nativeTheme is // the cross-platform meta hint. modern / legacy on the meta hint // translate to the equivalent iOS values when ios.themeMode is unset. + // cn1.nativeTheme is honored as a deprecated alias for nativeTheme. String iosMode = request.getArg("ios.themeMode", null); if (iosMode == null) { - String sharedMode = request.getArg("cn1.nativeTheme", null); + String sharedMode = request.getArg("nativeTheme", + request.getArg("cn1.nativeTheme", null)); if ("legacy".equalsIgnoreCase(sharedMode)) { iosMode = "ios7"; } else if ("modern".equalsIgnoreCase(sharedMode)) { diff --git a/scripts/cn1playground/common/codenameone_settings.properties b/scripts/cn1playground/common/codenameone_settings.properties index 5836a1ef55..454539b07f 100644 --- a/scripts/cn1playground/common/codenameone_settings.properties +++ b/scripts/cn1playground/common/codenameone_settings.properties @@ -7,9 +7,9 @@ codename1.arg.ios.NSCameraUsageDescription=Some functionality of the application # Preview the iOS Modern (liquid-glass) and Android Material 3 # themes inside the playground so users see the modern look when # they explore components. +codename1.arg.nativeTheme=modern codename1.arg.ios.themeMode=modern -codename1.arg.cn1.androidTheme=material -codename1.arg.cn1.nativeTheme=modern +codename1.arg.and.themeMode=modern codename1.arg.java.version=17 codename1.arg.javascript.inject_proxy=false codename1.cssTheme=true diff --git a/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundExamples.java b/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundExamples.java index 3a6f67d971..051544d413 100644 --- a/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundExamples.java +++ b/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundExamples.java @@ -46,7 +46,6 @@ static final class Sample { fab.addActionListener(e -> Dialog.show("FAB", "Tapped!", "OK", null)); ctx.log("Preview built successfully"); - form; """; static final String HELLO_WORLD_SCRIPT = """ @@ -55,7 +54,6 @@ static final class Sample { Container root = new Container(BoxLayout.y()); root.add(new Label("Hello, World!")); - root; """; static final String DATE_PICKER_SCRIPT = """ @@ -68,7 +66,6 @@ static final class Sample { datePicker.setType(Display.PICKER_TYPE_DATE); root.add(new Label("Pick a date:")); root.add(datePicker); - root; """; static final String BUILD_METHOD_SCRIPT = """ @@ -137,8 +134,6 @@ public void actionPerformed(ActionEvent evt) { Button cancel = new Button("Cancel"); FontImage.setMaterialIcon(cancel, FontImage.MATERIAL_CLOSE); form.add(cancel); - - form; """; static final String LIST_SCRIPT = """ @@ -166,7 +161,6 @@ public void actionPerformed(ActionEvent evt) { form.add(row); } ctx.log("List sample loaded"); - form; """; static final String UI_SHOWCASE_SCRIPT = """ @@ -209,8 +203,6 @@ public void actionPerformed(ActionEvent evt) { FloatingActionButton fab = FloatingActionButton.createFAB(FontImage.MATERIAL_EDIT); fab.bindFabToContainer(form.getContentPane()); fab.addActionListener(e -> Dialog.show("Edit", "FAB tapped.", "OK", null)); - - form; """; static final String TABS_SCRIPT = """ @@ -241,7 +233,6 @@ public void actionPerformed(ActionEvent evt) { new Label("ada@analytical.engine"), new Button("Sign out"))); form.add(BorderLayout.CENTER, tabs); - form; """; static final String BROWSER_SCRIPT = """ @@ -252,7 +243,6 @@ public void actionPerformed(ActionEvent evt) { BrowserComponent browser = new BrowserComponent(); browser.setPage("

BrowserComponent

Embedded web content works in the preview.

", "https://www.codenameone.com"); root.add(BorderLayout.CENTER, browser); - root; """; static final String NETWORK_SCRIPT = """ @@ -288,7 +278,6 @@ public void actionPerformed(ActionEvent event) { } }); root.addAll(fetch, output); - root; """; static final String REST_SCRIPT = """ @@ -314,7 +303,6 @@ public void actionPerformed(ActionEvent event) { }); }); root.addAll(load, output); - root; """; static final String CAMERA_SCRIPT = """ @@ -348,7 +336,6 @@ public void actionPerformed(ActionEvent evt) { } }); root.addAll(photo, audio, status); - root; """; static final Sample[] SAMPLES = new Sample[]{ diff --git a/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundProjectExporter.java b/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundProjectExporter.java index 9ddd7e942c..6e5f6ed104 100644 --- a/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundProjectExporter.java +++ b/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundProjectExporter.java @@ -266,6 +266,9 @@ private static byte[] readToBytesNoClose(InputStream is) throws IOException { private String codenameOneSettings(String appName) { return "codename1.arg.java.version=17\n" + + "codename1.arg.nativeTheme=modern\n" + + "codename1.arg.ios.themeMode=modern\n" + + "codename1.arg.and.themeMode=modern\n" + "codename1.mainName=" + appName + "\n" + "codename1.packageName=" + PACKAGE_NAME + "\n" + "codename1.displayName=" + appName + "\n" @@ -499,7 +502,13 @@ private static String buildSnippetLifecycle(String script) { if (trimmed.startsWith("package ")) { continue; } - if ("root;".equals(trimmed) || "ctx.log(\"Preview built successfully\");".equals(trimmed)) { + // Skip a bare trailing component-identifier line - the playground + // runner accepts that as the implicit return value, but the + // generated Lifecycle source has no use for it (it shows the + // detected Form / wraps the detected Container instead). + if ("root;".equals(trimmed) + || "form;".equals(trimmed) + || "ctx.log(\"Preview built successfully\");".equals(trimmed)) { continue; } String formVar = detectDeclaredVariableName(trimmed, "Form"); diff --git a/scripts/initializr/common/codenameone_settings.properties b/scripts/initializr/common/codenameone_settings.properties index 3156747c7b..7628c9a3b5 100644 --- a/scripts/initializr/common/codenameone_settings.properties +++ b/scripts/initializr/common/codenameone_settings.properties @@ -5,9 +5,9 @@ codename1.arg.ios.newStorageLocation=true # Opt new projects into the iOS Modern (liquid-glass) and Android # Material 3 themes by default. See docs/developer-guide # Native-Themes for the values these hints accept. +codename1.arg.nativeTheme=modern codename1.arg.ios.themeMode=modern -codename1.arg.cn1.androidTheme=material -codename1.arg.cn1.nativeTheme=modern +codename1.arg.and.themeMode=modern codename1.arg.java.version=8 codename1.displayName=Initializr codename1.icon=icon.png diff --git a/scripts/initializr/common/src/main/resources/common.zip b/scripts/initializr/common/src/main/resources/common.zip index 5f35bf517a..a9de13ecf9 100644 Binary files a/scripts/initializr/common/src/main/resources/common.zip and b/scripts/initializr/common/src/main/resources/common.zip differ