diff --git a/.github/workflows/designer.yml b/.github/workflows/designer.yml index 0fac4af16c..164162b7ec 100644 --- a/.github/workflows/designer.yml +++ b/.github/workflows/designer.yml @@ -6,6 +6,7 @@ on: - master paths: - 'CodenameOneDesigner/**' + - 'maven/css-compiler/**' - 'maven/designer/**' - '.github/workflows/designer.yml' pull_request: @@ -13,6 +14,7 @@ on: - master paths: - 'CodenameOneDesigner/**' + - 'maven/css-compiler/**' - 'maven/designer/**' - '.github/workflows/designer.yml' @@ -38,30 +40,47 @@ jobs: wget https://github.com/codenameone/cn1-binaries/archive/refs/heads/master.zip unzip master.zip -d .. mv ../cn1-binaries-master ../cn1-binaries + mkdir -p maven/target + rm -rf maven/target/cn1-binaries + cp -R ../cn1-binaries maven/target/cn1-binaries - - name: Build core dependencies + - name: Build Designer (pulls in codenameone-css-compiler as dep) + env: + CN1_BINARIES: ${{ github.workspace }}/maven/target/cn1-binaries run: | - xvfb-run -a ant -noinput -buildfile Ports/CLDC11/build.xml jar - xvfb-run -a ant -noinput -buildfile Ports/JavaSE/build.xml jar - xvfb-run -a ant -noinput -buildfile Ports/JavaSEWithSVGSupport/build.xml jar - xvfb-run -a ant -noinput -buildfile CodenameOne/build.xml jar - - - name: Run designer CSS localization tests - run: xvfb-run -a ant -noinput -buildfile CodenameOneDesigner/build.xml test-css-localization + cd maven + mvn -B -pl designer -am -DskipTests -Plocal-dev-javase \ + -Dmaven.javadoc.skip=true -Dcn1.binaries="${CN1_BINARIES}" install - name: Run designer XML parser unit tests (Maven) + env: + CN1_BINARIES: ${{ github.workspace }}/maven/target/cn1-binaries run: | - mkdir -p maven/target - rm -rf maven/target/cn1-binaries - cp -R ../cn1-binaries maven/target/cn1-binaries cd maven - mvn -B -pl designer -am -DunitTests=true -Dcodename1.platform=javase -Plocal-dev-javase -Dmaven.javadoc.skip=true -Dmaven.antrun.skip=true -Dtest=SimpleXmlParserTest -DfailIfNoTests=false test - - - name: Build designer release jar - run: xvfb-run -a ant -noinput -buildfile CodenameOneDesigner/build.xml release + mvn -B -pl designer -am -DunitTests=true -Dcodename1.platform=javase \ + -Plocal-dev-javase -Dmaven.javadoc.skip=true -Dmaven.antrun.skip=true \ + -Dcn1.binaries="${CN1_BINARIES}" \ + -Dtest=SimpleXmlParserTest -DfailIfNoTests=false test - name: Verify designer CLI CSS compilation run: | + # The Maven-built jar-with-dependencies is a ZIP wrapper around the + # actual runnable designer_1.jar (see the antrun + # add-designer-jar-with-dependencies execution in maven/designer/pom.xml). + # Unpack and run the inner jar directly. + wrapped=$(ls maven/designer/target/codenameone-designer-*-jar-with-dependencies.jar | head -n1) + if [ -z "${wrapped}" ] || [ ! -f "${wrapped}" ]; then + echo "designer jar-with-dependencies not found" >&2 + exit 1 + fi + extract_dir="$(mktemp -d)" + unzip -q "${wrapped}" -d "${extract_dir}" + designer_jar="${extract_dir}/designer_1.jar" + if [ ! -f "${designer_jar}" ]; then + echo "designer_1.jar not found inside ${wrapped}" >&2 + ls -la "${extract_dir}" + exit 1 + fi tmp_dir="CodenameOneDesigner/tmp-cli-test" css_file="$tmp_dir/test.css" l10n_dir="$tmp_dir/localization" @@ -75,12 +94,21 @@ jobs: cat <<'EOF' > "$l10n_dir/Strings.properties" greeting=Hello from CLI EOF - xvfb-run -a java -Dcli=true -jar CodenameOneDesigner/dist/designer.jar \ + xvfb-run -a java -Dcli=true -jar "${designer_jar}" \ -css -stateless -input "$css_file" -output "$output_file" -localization "$l10n_dir" test -s "$output_file" + - name: Verify native-themes CEF-free build + run: | + cd maven + mvn -B -pl css-compiler -am install -DskipTests -Dmaven.javadoc.skip=true -Plocal-dev-javase + cd .. + ./scripts/build-native-themes.sh + test -f Themes/iOSModernTheme.res || { echo "missing Themes/iOSModernTheme.res"; exit 1; } + test -f Themes/AndroidMaterialTheme.res || { echo "missing Themes/AndroidMaterialTheme.res"; exit 1; } + - name: Upload designer jar artifact uses: actions/upload-artifact@v4 with: name: designer-jar - path: CodenameOneDesigner/dist/designer.jar + path: maven/designer/target/codenameone-designer-*-jar-with-dependencies.jar diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 5e254c1b29..06dc633b27 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -89,6 +89,14 @@ jobs: cd maven mvn clean verify -DunitTests=true -pl core-unittests -am -Dmaven.javadoc.skip=true -Plocal-dev-javase $MVN_ARGS cd .. + - name: Build CSS compiler and smoke native-themes + run: | + cd maven + mvn -B -pl css-compiler -am install -DskipTests -Dmaven.javadoc.skip=true -Plocal-dev-javase + cd .. + ./scripts/build-native-themes.sh + test -f Themes/iOSModernTheme.res || { echo "missing Themes/iOSModernTheme.res"; exit 1; } + test -f Themes/AndroidMaterialTheme.res || { echo "missing Themes/AndroidMaterialTheme.res"; exit 1; } - name: Prepare Codename One binaries for Maven plugin tests run: | set -euo pipefail diff --git a/.gitignore b/.gitignore index 7ee5bfbc32..82e5eba372 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,7 @@ node_modules **/genfiles.properties **/private/private.properties .idea/ +.claude/ target pom.xml.versionsBackup pom.xml.releaseBackup diff --git a/CodenameOne/src/com/codename1/io/Util.java b/CodenameOne/src/com/codename1/io/Util.java index 509bc54b46..fa245b93e9 100644 --- a/CodenameOne/src/com/codename1/io/Util.java +++ b/CodenameOne/src/com/codename1/io/Util.java @@ -208,8 +208,39 @@ public static void copy(InputStream i, OutputStream o, int bufferSize) throws IO try { copyNoClose(i, o, bufferSize); } finally { - Util.getImplementation().cleanup(o); - Util.getImplementation().cleanup(i); + CodenameOneImplementation impl = Util.getImplementation(); + if (impl != null) { + impl.cleanup(o); + impl.cleanup(i); + } else { + // Headless callers (e.g. the css-compiler native-themes build) + // use this method before Display is initialized. Fall back to + // closing the streams directly so we do not NPE. + closeQuietly(o); + closeQuietly(i); + } + } + } + + private static void closeQuietly(InputStream c) { + if (c == null) { + return; + } + try { + c.close(); + } catch (IOException e) { + System.err.println("Util.copy: ignoring " + e); + } + } + + private static void closeQuietly(OutputStream c) { + if (c == null) { + return; + } + try { + c.close(); + } catch (IOException e) { + System.err.println("Util.copy: ignoring " + e); } } diff --git a/CodenameOne/src/com/codename1/ui/Display.java b/CodenameOne/src/com/codename1/ui/Display.java index 34bf4c4034..1d3b3ee8aa 100644 --- a/CodenameOne/src/com/codename1/ui/Display.java +++ b/CodenameOne/src/com/codename1/ui/Display.java @@ -2752,6 +2752,14 @@ public int convertToPixels(float value, byte unitType, boolean horizontal) { /// /// value in pixels public int convertToPixels(float dipCount) { + if (impl == null) { + // Headless callers (e.g. the css-compiler native-themes build) + // compile theme constants before Display is initialized; return + // a 1:1 fallback so border/padding serialization succeeds. The + // actual pixel conversion happens at app runtime when a full + // implementation is available. + return Math.round(dipCount); + } return Math.round(impl.convertToPixels((int) (dipCount * 1000), true) / 1000.0f); } diff --git a/CodenameOne/src/com/codename1/ui/Font.java b/CodenameOne/src/com/codename1/ui/Font.java index ed4e4a580f..6353efa073 100644 --- a/CodenameOne/src/com/codename1/ui/Font.java +++ b/CodenameOne/src/com/codename1/ui/Font.java @@ -171,11 +171,27 @@ public class Font extends CN { } Font(int face, int style, int size) { + this.headlessFace = face; + this.headlessStyle = style; + this.headlessSize = size; Display d = Display.getInstance(); CodenameOneImplementation i = d.getImplementation(); - font = i.createFont(face, style, size); + if (i != null) { + font = i.createFont(face, style, size); + } + // Headless callers (e.g. the css-compiler native-themes build) never + // render text; the serialized theme only stores face/style/size and + // the native font object is recreated when the resource is loaded + // inside a running CN1 app. } + /// Face/style/size copies used by getFace/getStyle/getSize when Display.impl + /// is null (headless native-themes build). Only populated by the system-font + /// constructor; other code paths (TTF, bitmap) keep them at zero. + private int headlessFace; + private int headlessStyle; + private int headlessSize; + /// Returns a previously loaded bitmap font from cache /// /// #### Parameters @@ -733,6 +749,9 @@ void drawChars(Graphics g, char[] data, int offset, int length, int x, int y) { /// /// Optional operation returning the font face for system fonts public int getFace() { + if (Display.impl == null) { + return headlessFace; + } return Display.impl.getFace(font); } @@ -742,6 +761,9 @@ public int getFace() { /// /// Optional operation returning the font size for system fonts public int getSize() { + if (Display.impl == null) { + return headlessSize; + } return Display.impl.getSize(font); } @@ -751,6 +773,9 @@ public int getSize() { /// /// Optional operation returning the font style for system fonts public int getStyle() { + if (Display.impl == null) { + return headlessStyle; + } return Display.impl.getStyle(font); } diff --git a/CodenameOne/src/com/codename1/ui/plaf/UIManager.java b/CodenameOne/src/com/codename1/ui/plaf/UIManager.java index c9d131c9d7..841416ff62 100644 --- a/CodenameOne/src/com/codename1/ui/plaf/UIManager.java +++ b/CodenameOne/src/com/codename1/ui/plaf/UIManager.java @@ -1498,6 +1498,25 @@ public void addThemeProps(Hashtable themeProps) { } } + /// Invalidates the cached Style instances and re-runs the theme build pass + /// against the currently installed theme properties. Callers use this after + /// state changes that affect style resolution (notably `Display.setDarkMode`, + /// which makes `$Dark` entries eligible) without reloading the theme + /// from a resource file. Components styled after this call resolve against + /// the refreshed theme; already-resolved Style references on existing + /// components keep their old values until those components re-fetch their + /// styles. + public void refreshTheme() { + if (!accessible || themeProps == null) { + return; + } + Hashtable props = new Hashtable(); + for (String key : themeProps.keySet()) { + props.put(key, themeProps.get(key)); + } + setThemePropsImpl(props); + } + /// Returns a theme constant defined in the resource editor /// /// #### Parameters diff --git a/CodenameOneDesigner/src/com/codename1/designer/AddThemeResource.java b/CodenameOneDesigner/src/com/codename1/designer/AddThemeResource.java index 368d88911d..6972672a57 100644 --- a/CodenameOneDesigner/src/com/codename1/designer/AddThemeResource.java +++ b/CodenameOneDesigner/src/com/codename1/designer/AddThemeResource.java @@ -26,6 +26,7 @@ package com.codename1.designer; import com.codename1.ui.util.EditableResources; +import com.codename1.ui.util.EditableResourcesEditor; import java.io.IOException; import java.io.InputStream; import java.util.Hashtable; @@ -198,7 +199,7 @@ public String addResource(EditableResources res, ResourceEditorView view) { InputStream is = getClass().getResourceAsStream("/templates/" + template.getSelectedItem().toString() + ".res"); if(is != null) { try { - EditableResources r = new EditableResources(); + EditableResources r = new EditableResourcesEditor(); r.openFile(is); is.close(); theme = r.getTheme(r.getThemeResourceNames()[0]); diff --git a/CodenameOneDesigner/src/com/codename1/designer/AddUIResource.java b/CodenameOneDesigner/src/com/codename1/designer/AddUIResource.java index b4e4c2b2b2..a46c1b8901 100644 --- a/CodenameOneDesigner/src/com/codename1/designer/AddUIResource.java +++ b/CodenameOneDesigner/src/com/codename1/designer/AddUIResource.java @@ -26,6 +26,7 @@ package com.codename1.designer; import com.codename1.ui.util.EditableResources; +import com.codename1.ui.util.EditableResourcesEditor; import java.io.IOException; import java.io.InputStream; import java.util.Hashtable; @@ -199,7 +200,7 @@ public String addResource(EditableResources res, ResourceEditorView view) { InputStream is = getClass().getResourceAsStream("/templates/" + template.getSelectedItem().toString() + ".res"); if(is != null) { try { - EditableResources r = new EditableResources(); + EditableResources r = new EditableResourcesEditor(); r.openFile(is); is.close(); ui = r.getResourceObject("Main"); diff --git a/CodenameOneDesigner/src/com/codename1/designer/ResourceEditorApp.java b/CodenameOneDesigner/src/com/codename1/designer/ResourceEditorApp.java index bd068a68f1..a33b44bde6 100644 --- a/CodenameOneDesigner/src/com/codename1/designer/ResourceEditorApp.java +++ b/CodenameOneDesigner/src/com/codename1/designer/ResourceEditorApp.java @@ -36,7 +36,8 @@ //import com.codename1.impl.javase.JavaFXLoader; import com.codename1.ui.plaf.Style; import com.codename1.ui.resource.util.QuitAction; -import com.codename1.ui.util.EditableResources; +import com.codename1.ui.util.EditableResources; +import com.codename1.ui.util.EditableResourcesEditor; import com.codename1.ui.util.Resources; import com.codename1.ui.util.UIBuilderOverride; import java.awt.BorderLayout; @@ -298,7 +299,7 @@ public static void _main(String[] args) throws Exception { boolean isXMLEnabled = Preferences.userNodeForPackage(ResourceEditorView.class).getBoolean("XMLFileMode", true); EditableResources.setXMLEnabled(isXMLEnabled); - EditableResources res = new EditableResources(); + EditableResources res = new EditableResourcesEditor(); File resourceFile = new File(args[1]); res.openFileWithXMLSupport(resourceFile); @@ -424,7 +425,7 @@ public void actionPerformed(ActionEvent e) { boolean isXMLEnabled = Preferences.userNodeForPackage(ResourceEditorView.class).getBoolean("XMLFileMode", true); EditableResources.setXMLEnabled(isXMLEnabled); - EditableResources res = new EditableResources(); + EditableResources res = new EditableResourcesEditor(); res.openFileWithXMLSupport(resourceFile); res.setImage(imageName, img); @@ -499,7 +500,7 @@ public void actionPerformed(ActionEvent e) { boolean isXMLEnabled = Preferences.userNodeForPackage(ResourceEditorView.class).getBoolean("XMLFileMode", true); EditableResources.setXMLEnabled(isXMLEnabled); - EditableResources res = new EditableResources(); + EditableResources res = new EditableResourcesEditor(); res.openFileWithXMLSupport(resourceFile); AddAndScaleMultiImage.generateImpl(new File[] {imageFile}, @@ -524,7 +525,7 @@ public void actionPerformed(ActionEvent e) { com.codename1.ui.Display.init(cnt); File projectDir = new File(args[1]); EditableResources.setXMLEnabled(true); - EditableResources res = new EditableResources(); + EditableResources res = new EditableResourcesEditor(); res.openFileWithXMLSupport(new File(args[2])); migrateGuiBuilder(projectDir, res, args[3]); System.exit(0); @@ -535,7 +536,7 @@ public void actionPerformed(ActionEvent e) { com.codename1.ui.Display.init(cnt); File output = new File(args[1]); EditableResources.setXMLEnabled(true); - EditableResources res = new EditableResources(); + EditableResources res = new EditableResourcesEditor(); res.openFileWithXMLSupport(output); FileOutputStream fos = new FileOutputStream(output); res.save(fos); @@ -550,7 +551,7 @@ public void actionPerformed(ActionEvent e) { com.codename1.ui.Display.init(cnt); File output = new File(args[1]); EditableResources.setXMLEnabled(true); - EditableResources res = new EditableResources(); + EditableResources res = new EditableResourcesEditor(); res.openFileWithXMLSupport(output); FileOutputStream fos = new FileOutputStream(output); res.save(fos); @@ -579,7 +580,7 @@ public void actionPerformed(ActionEvent e) { private static void generateResourceFile(File f, String themeName, String ui) throws Exception { System.out.println("Generating resource file " + f + " theme " + themeName + " template " + ui); - EditableResources res = new EditableResources(); + EditableResources res = new EditableResourcesEditor(); //"native", "leather", "tzone", "tipster", "blank" String template = "Native_Theme"; @@ -1232,7 +1233,7 @@ private static Hashtable importRes(EditableResources res, String file) { Hashtable theme = new Hashtable(); if(is != null) { try { - EditableResources r = new EditableResources(); + EditableResources r = new EditableResourcesEditor(); r.openFile(is); is.close(); if(r.getThemeResourceNames().length > 0) { diff --git a/CodenameOneDesigner/src/com/codename1/designer/ResourceEditorView.java b/CodenameOneDesigner/src/com/codename1/designer/ResourceEditorView.java index e124a7b015..ca3b798651 100644 --- a/CodenameOneDesigner/src/com/codename1/designer/ResourceEditorView.java +++ b/CodenameOneDesigner/src/com/codename1/designer/ResourceEditorView.java @@ -35,6 +35,7 @@ import com.codename1.ui.plaf.Border; import com.codename1.tools.resourcebuilder.ThemeTaskConstants; import com.codename1.ui.util.EditableResources; +import com.codename1.ui.util.EditableResourcesEditor; import com.codename1.ui.Font; import com.codename1.ui.animations.AnimationAccessor; import com.codename1.ui.animations.Timeline; @@ -152,7 +153,7 @@ public class ResourceEditorView extends FrameView { private HelpAction helpAction = new HelpAction(); private static final String IMAGE_DIR = "/com/codename1/designer/resources/"; - private static EditableResources loadedResources = new EditableResources(); + private static EditableResources loadedResources = new EditableResourcesEditor(); private Properties projectGeneratorSettings; private static String manualIDESettings; private List recentFiles = new ArrayList(); @@ -509,7 +510,7 @@ public void actionPerformed(ActionEvent ae) { } File f = getPlatformOverrideFile(); if(f != null) { - EditableResources platformOverrideResource = new EditableResources(); + EditableResources platformOverrideResource = new EditableResourcesEditor(); if(f.exists()) { try { platformOverrideResource.openFile(new FileInputStream(f)); @@ -657,7 +658,7 @@ public void setSelectedResource(String selectedResource) { // tree tries to restore selection sometimes with a non-existing resource: for(String s : loadedResources.getResourceNames()) { if(s.equals(selectedResource)) { - BaseForm b = (BaseForm)loadedResources.getResourceEditor(selectedResource, ResourceEditorView.this); + BaseForm b = (BaseForm)((EditableResourcesEditor)loadedResources).getResourceEditor(selectedResource, ResourceEditorView.this); if(loadedResources.isOverrideMode() && !loadedResources.isOverridenResource(selectedResource)) { b.setOverrideMode(true, mainPanel); } @@ -2052,7 +2053,7 @@ private static void checkDuplicateResources(EditableResources r, String[] loaded } public void importResourceStream(InputStream is) throws IOException { - EditableResources r = new EditableResources(); + EditableResources r = new EditableResourcesEditor(); r.openFile(is); checkDuplicateResourcesLoop(r, loadedResources.getThemeResourceNames(), r.getThemeResourceNames(), "Rename Theme", "Theme "); @@ -2826,7 +2827,7 @@ private void duplicateItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN loadedResources.setModified(); } bo.close(); - EditableResources r = new EditableResources(); + EditableResources r = new EditableResourcesEditor(); r.openFile(new ByteArrayInputStream(bo.toByteArray())); loadedResources.addResourceObjectDuplicate(selectedResource, val, r.getResourceObject(selectedResource)); setSelectedResource(val); @@ -3292,7 +3293,7 @@ private void setNativeTheme(String file, boolean local) { } else { i = new FileInputStream(file); } - EditableResources er = new EditableResources(); + EditableResources er = new EditableResourcesEditor(); er.openFile(i); JavaSEPortWithSVGSupport.setNativeTheme(er); JavaSEPortWithSVGSupport.setShowEDTWarnings(false); diff --git a/CodenameOneDesigner/src/com/codename1/designer/css/CN1CSSCLI.java b/CodenameOneDesigner/src/com/codename1/designer/css/CN1CSSCLI.java index c0c3da67cb..811e5b2e16 100644 --- a/CodenameOneDesigner/src/com/codename1/designer/css/CN1CSSCLI.java +++ b/CodenameOneDesigner/src/com/codename1/designer/css/CN1CSSCLI.java @@ -615,14 +615,22 @@ public static void main(String[] args) throws Exception { System.out.println(" -l, -localization Directory containing Java resource bundle .properties files to include."); System.out.println(" -w, -watch Run in watch mode."); System.out.println(" Watches input files for changes and automatically recompiles."); + System.out.println(" -no-cef Disallow any CSS feature that would trigger CEF-backed image"); + System.out.println(" rasterization (9-piece borders, complex gradients, shadows, filters)."); + System.out.println(" The compile fails listing offending rules instead of falling back."); + System.out.println(" Used by the framework native-themes build."); System.out.println("\nSystem Properties:"); System.out.println(" cef.dir The path to the CEF directory."); System.out.println(" Required for generation of image borders."); System.out.println(" parent.port The port number to connect to the parent process for watch mode so that it knows "); System.out.println(" to exit if the parent process ends."); return; - - + + + } + if (getArgByName(args, "no-cef") != null) { + CSSTheme.strictNoCef = true; + System.out.println("CSS compiler running in no-cef mode: any rule requiring CEF rasterization will fail the build."); } statelessMode = getArgByName(args, "i", "input") != null; String localizationPath = getArgByName(args, "l", "localization"); diff --git a/CodenameOneDesigner/src/com/codename1/designer/css/CN1CSSCompiler.java b/CodenameOneDesigner/src/com/codename1/designer/css/CN1CSSCompiler.java index 708f972ef7..151ac37519 100644 --- a/CodenameOneDesigner/src/com/codename1/designer/css/CN1CSSCompiler.java +++ b/CodenameOneDesigner/src/com/codename1/designer/css/CN1CSSCompiler.java @@ -28,6 +28,7 @@ import com.codename1.ui.CN; import com.codename1.ui.Display; import com.codename1.ui.util.EditableResources; +import com.codename1.ui.util.EditableResourcesEditor; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; @@ -165,7 +166,7 @@ public CN1CSSCompiler() { public void startDocument(InputSource is) throws CSSException { //props = new Properties(); theme = new Hashtable(); - res = new EditableResources(); + res = new EditableResourcesEditor(); includedDensities.clear(); includedDensities.addAll(defaultDensities); } @@ -206,7 +207,7 @@ public void endDocument(InputSource is) throws CSSException { //cn1.addTheme(theme); //cn1.execute(); - EditableResources output = new EditableResources(); + EditableResources output = new EditableResourcesEditor(); theme.addToResources(output); System.out.println(output.getTheme(inputFile.getName())); @@ -225,7 +226,7 @@ public void endDocument(InputSource is) throws CSSException { /* - EditableResources output = new EditableResources(); + EditableResources output = new EditableResourcesEditor(); Hashtable theme = new Hashtable(); for (String key : props.stringPropertyNames()) { theme.put(key, props.get(key)); diff --git a/CodenameOneDesigner/src/com/codename1/designer/css/CN1CSSInstallerCLI.java b/CodenameOneDesigner/src/com/codename1/designer/css/CN1CSSInstallerCLI.java index 395ec26815..ca2b4d8859 100644 --- a/CodenameOneDesigner/src/com/codename1/designer/css/CN1CSSInstallerCLI.java +++ b/CodenameOneDesigner/src/com/codename1/designer/css/CN1CSSInstallerCLI.java @@ -25,6 +25,7 @@ import com.codename1.impl.javase.JavaSEPort; import com.codename1.ui.Display; import com.codename1.ui.util.EditableResources; +import com.codename1.ui.util.EditableResourcesEditor; import java.awt.EventQueue; import java.io.DataOutputStream; import java.io.File; @@ -52,7 +53,7 @@ private static void install(String[] args) throws Exception { frm.setVisible(false); Display.init(frm.getContentPane()); JavaSEPort.setBaseResourceDir(resFile.getParentFile()); - EditableResources res = new EditableResources(); + EditableResources res = new EditableResourcesEditor(); res.openFile(new FileInputStream(resFile)); String mainTheme = res.getThemeResourceNames()[0]; res.setThemeProperty(mainTheme, "@OverlayThemes", cssFile.getName()); diff --git a/CodenameOneDesigner/src/com/codename1/tools/resourcebuilder/CodenameOneTask.java b/CodenameOneDesigner/src/com/codename1/tools/resourcebuilder/CodenameOneTask.java index 4f4fab8a43..c4d5c7cc32 100644 --- a/CodenameOneDesigner/src/com/codename1/tools/resourcebuilder/CodenameOneTask.java +++ b/CodenameOneDesigner/src/com/codename1/tools/resourcebuilder/CodenameOneTask.java @@ -26,6 +26,7 @@ import com.codename1.ui.Display; import com.codename1.ui.util.EditableResources; +import com.codename1.ui.util.EditableResourcesEditor; import java.io.DataOutputStream; import java.io.File; import java.io.FileOutputStream; @@ -88,7 +89,7 @@ public void execute() throws BuildException { System.out.println("Processing " + dest); Display.init(null); - EditableResources output = new EditableResources(); + EditableResources output = new EditableResourcesEditor(); for(ResourceTask task : resources) { task.addToResources(output); } diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/EditableResourcesEditor.java b/CodenameOneDesigner/src/com/codename1/ui/util/EditableResourcesEditor.java new file mode 100644 index 0000000000..376b23f101 --- /dev/null +++ b/CodenameOneDesigner/src/com/codename1/ui/util/EditableResourcesEditor.java @@ -0,0 +1,152 @@ +/* + * 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + */ +package com.codename1.ui.util; + +import com.codename1.designer.DataEditor; +import com.codename1.designer.FontEditor; +import com.codename1.designer.ImageMultiEditor; +import com.codename1.designer.ImageRGBEditor; +import com.codename1.designer.L10nEditor; +import com.codename1.designer.MultiImageSVGEditor; +import com.codename1.designer.ResourceEditorView; +import com.codename1.designer.ThemeEditor; +import com.codename1.designer.TimelineEditor; +import com.codename1.designer.UserInterfaceEditor; +import com.codename1.impl.javase.JavaSEPortWithSVGSupport; +import com.codename1.ui.Container; +import com.codename1.ui.Image; +import com.codename1.ui.animations.Timeline; +import com.codename1.ui.util.xml.comps.ComponentEntry; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import javax.swing.JComponent; + +/** + * Designer-side subclass of EditableResources that wires the GUI hooks. + * EditableResources itself lives in the css-compiler module with clean + * dependencies so the native-themes build can use it without pulling in + * JavaSE / JavaFX / CEF / Designer GUI classes. + */ +public class EditableResourcesEditor extends EditableResources { + + public EditableResourcesEditor() { + super(); + } + + public EditableResourcesEditor(InputStream input) throws IOException { + super(input); + } + + @Override + protected byte[] persistUIContainer(Container cnt) { + return UserInterfaceEditor.persistContainer(cnt, this); + } + + @Override + protected Container loadUIContainerFromXml(ComponentEntry uiXMLData) { + UIBuilderOverride uib = new UIBuilderOverride(); + return uib.createInstance(uiXMLData, this); + } + + @Override + protected Container materializeUIContainer(String resourceName) { + UIBuilderOverride u = new UIBuilderOverride(); + return u.createContainer(this, resourceName); + } + + @Override + protected void writeUIXml(Container cnt, FileOutputStream dest) throws IOException { + Writer w = new OutputStreamWriter(dest, "UTF-8"); + w.write("\n\n"); + StringBuilder bld = new StringBuilder(); + UserInterfaceEditor.persistToXML(cnt, cnt, bld, this, ""); + w.write(bld.toString()); + w.flush(); + } + + @Override + protected void onOpenFileComplete() { + ThemeEditor.resetThemeLoaded(); + } + + @Override + protected EditableResources getRuntimeNativeTheme() { + return (EditableResources) JavaSEPortWithSVGSupport.getNativeTheme(); + } + + @Override + protected File getLoadedFile() { + File override = super.getLoadedFile(); + if (override != null) { + return override; + } + return ResourceEditorView.getLoadedFile(); + } + + /** + * Opens a GUI editor for the named resource. Only available on the editor + * subclass because the editors themselves live in the Designer module. + */ + public JComponent getResourceEditor(String name, ResourceEditorView view) { + byte magic = getResourceType(name); + switch (magic) { + case MAGIC_IMAGE: + case MAGIC_IMAGE_LEGACY: + Image i = getImage(name); + if (getResourceObject(name) instanceof MultiImage) { + ImageMultiEditor tl = new ImageMultiEditor(this, name, view); + tl.setImage((MultiImage) getResourceObject(name)); + return tl; + } + if (i instanceof Timeline) { + TimelineEditor tl = new TimelineEditor(this, name, view); + tl.setImage((Timeline) i); + return tl; + } + if (i.isSVG()) { + MultiImageSVGEditor img = new MultiImageSVGEditor(this, name); + img.setImage(i); + return img; + } + ImageRGBEditor img = new ImageRGBEditor(this, name, view); + img.setImage(i); + return img; + case MAGIC_TIMELINE: + TimelineEditor tl = new TimelineEditor(this, name, view); + tl.setImage((Timeline) getImage(name)); + return tl; + case MAGIC_THEME: + case MAGIC_THEME_LEGACY: + ThemeEditor theme = new ThemeEditor(this, name, getTheme(name), view); + return theme; + case MAGIC_FONT: + case MAGIC_FONT_LEGACY: + case MAGIC_INDEXED_FONT_LEGACY: + FontEditor fonts = new FontEditor(this, getFont(name), name); + return fonts; + case MAGIC_DATA: + DataEditor data = new DataEditor(this, name); + return data; + case MAGIC_UI: + UserInterfaceEditor uie = new UserInterfaceEditor(name, this, view.getProjectGeneratorSettings(), view); + return uie; + case MAGIC_L10N: + L10nEditor l10n = new L10nEditor(this, name); + return l10n; + default: + throw new IllegalArgumentException("Unrecognized magic number: " + Integer.toHexString(magic & 0xff)); + } + } +} diff --git a/CodenameOneDesigner/test/com/codename1/designer/css/CSSDarkModeMediaQueryTest.java b/CodenameOneDesigner/test/com/codename1/designer/css/CSSDarkModeMediaQueryTest.java index cbc4b3ced0..263bcc528f 100644 --- a/CodenameOneDesigner/test/com/codename1/designer/css/CSSDarkModeMediaQueryTest.java +++ b/CodenameOneDesigner/test/com/codename1/designer/css/CSSDarkModeMediaQueryTest.java @@ -13,6 +13,7 @@ public class CSSDarkModeMediaQueryTest { public static void main(String[] args) throws Exception { testDarkMediaCompilesToDarkUiids(); + testAtMediaInsideHeaderCommentIsIgnored(); } private static void testDarkMediaCompilesToDarkUiids() throws Exception { @@ -42,6 +43,39 @@ private static void testDarkMediaCompilesToDarkUiids() throws Exception { } } + /** + * Regression: the dark-mode rewriter must not trigger on the literal + * "@media (prefers-color-scheme:" string sitting inside a header + * comment. Before the fix it swallowed everything up to the next {, + * treated the subsequent block's properties as dark selectors, and + * ran the tokenizer off EOF later on. + */ + private static void testAtMediaInsideHeaderCommentIsIgnored() throws Exception { + Path cssFile = Files.createTempFile("cn1-dark-comment", ".css"); + Path resFile = Files.createTempFile("cn1-dark-comment", ".res"); + try { + String css = "/* header doc mentions @media (prefers-color-scheme: dark) for reference */\n" + + "#Constants { tabsGridBool: true; }\n" + + "Button { color: #111111; }\n" + + "@media (prefers-color-scheme: dark) {\n" + + " Button { color: #eeeeee; }\n" + + "}\n"; + Files.write(cssFile, css.getBytes(StandardCharsets.UTF_8)); + + CSSTheme theme = CSSTheme.load(cssFile.toUri().toURL()); + theme.resourceFile = resFile.toFile(); + theme.updateResources(); + + Hashtable themeProps = theme.res.getTheme("Theme"); + assertEquals("111111", themeProps.get("Button.fgColor"), "Light Button fgColor survives comment"); + assertEquals("eeeeee", themeProps.get("$DarkButton.fgColor"), "Real dark block still compiles"); + assertEquals("true", themeProps.get("@tabsGridBool"), "#Constants block isn't mangled"); + } finally { + deleteIfExists(cssFile); + deleteIfExists(resFile); + } + } + private static void deleteIfExists(Path path) { try { Files.deleteIfExists(path); diff --git a/Ports/Android/build.xml b/Ports/Android/build.xml index 7fbaa816d5..7fbc590bae 100644 --- a/Ports/Android/build.xml +++ b/Ports/Android/build.xml @@ -74,6 +74,12 @@ + + diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java index 373b0dfab2..018d3d252b 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java @@ -1064,85 +1064,85 @@ public static String[] getPendingPush(String type, Context a) { private static AndroidImplementation instance; private static final String INTENT_PROPERTY_PREFIX = "android.intent."; private static final String INTENT_EXTRA_PROPERTY_PREFIX = "android.intent.extra."; - private static final Set intentPropertyKeys = new HashSet(); - private static final Object intentPropertyLock = new Object(); - private static Intent lastPublishedIntent; + private static final Set intentPropertyKeys = new HashSet(); + private static final Object intentPropertyLock = new Object(); + private static Intent lastPublishedIntent; public static AndroidImplementation getInstance() { return instance; } - public static void clearAppArg() { - if (instance != null) { - instance.setAppArg(null); - clearIntentProperties(); - } - } - - private static void clearIntentProperties() { - synchronized (intentPropertyLock) { - if (Display.isInitialized()) { - for (String key : new ArrayList(intentPropertyKeys)) { - Display.getInstance().setProperty(key, null); - } - } - intentPropertyKeys.clear(); - lastPublishedIntent = null; - } - } - - private static void publishIntentProperties(Activity activity, Intent intent) { - if (intent == null) { - return; - } - - synchronized (intentPropertyLock) { - if (intent == lastPublishedIntent) { - return; - } - - Map nextProperties = new HashMap(); - nextProperties.put(INTENT_PROPERTY_PREFIX + "action", intent.getAction()); - nextProperties.put(INTENT_PROPERTY_PREFIX + "data", intent.getDataString()); - nextProperties.put(INTENT_PROPERTY_PREFIX + "type", intent.getType()); - - // Only getCallingPackage() is a verified caller identity. Referrer values are caller-controlled. - String callerPackage = activity.getCallingPackage(); - nextProperties.put(INTENT_PROPERTY_PREFIX + "caller", callerPackage); - nextProperties.put(INTENT_PROPERTY_PREFIX + "caller.verified", callerPackage != null ? "true" : "false"); - - Bundle extras = intent.getExtras(); - if (extras != null) { - for (String key : extras.keySet()) { - Object value = extras.get(key); - String propertyKey = key.startsWith(INTENT_EXTRA_PROPERTY_PREFIX) ? key : INTENT_EXTRA_PROPERTY_PREFIX + key; - nextProperties.put(propertyKey, value == null ? null : String.valueOf(value)); - } - } - - if (Display.isInitialized()) { - ArrayList keysToRemove = new ArrayList(); - for (String key : intentPropertyKeys) { - if (!nextProperties.containsKey(key)) { - keysToRemove.add(key); - } - } - for (String key : keysToRemove) { - Display.getInstance().setProperty(key, null); - intentPropertyKeys.remove(key); - } - for (Map.Entry entry : nextProperties.entrySet()) { - Display.getInstance().setProperty(entry.getKey(), entry.getValue()); - intentPropertyKeys.add(entry.getKey()); - } - } else { - intentPropertyKeys.clear(); - intentPropertyKeys.addAll(nextProperties.keySet()); - } - - lastPublishedIntent = intent; - } - } + public static void clearAppArg() { + if (instance != null) { + instance.setAppArg(null); + clearIntentProperties(); + } + } + + private static void clearIntentProperties() { + synchronized (intentPropertyLock) { + if (Display.isInitialized()) { + for (String key : new ArrayList(intentPropertyKeys)) { + Display.getInstance().setProperty(key, null); + } + } + intentPropertyKeys.clear(); + lastPublishedIntent = null; + } + } + + private static void publishIntentProperties(Activity activity, Intent intent) { + if (intent == null) { + return; + } + + synchronized (intentPropertyLock) { + if (intent == lastPublishedIntent) { + return; + } + + Map nextProperties = new HashMap(); + nextProperties.put(INTENT_PROPERTY_PREFIX + "action", intent.getAction()); + nextProperties.put(INTENT_PROPERTY_PREFIX + "data", intent.getDataString()); + nextProperties.put(INTENT_PROPERTY_PREFIX + "type", intent.getType()); + + // Only getCallingPackage() is a verified caller identity. Referrer values are caller-controlled. + String callerPackage = activity.getCallingPackage(); + nextProperties.put(INTENT_PROPERTY_PREFIX + "caller", callerPackage); + nextProperties.put(INTENT_PROPERTY_PREFIX + "caller.verified", callerPackage != null ? "true" : "false"); + + Bundle extras = intent.getExtras(); + if (extras != null) { + for (String key : extras.keySet()) { + Object value = extras.get(key); + String propertyKey = key.startsWith(INTENT_EXTRA_PROPERTY_PREFIX) ? key : INTENT_EXTRA_PROPERTY_PREFIX + key; + nextProperties.put(propertyKey, value == null ? null : String.valueOf(value)); + } + } + + if (Display.isInitialized()) { + ArrayList keysToRemove = new ArrayList(); + for (String key : intentPropertyKeys) { + if (!nextProperties.containsKey(key)) { + keysToRemove.add(key); + } + } + for (String key : keysToRemove) { + Display.getInstance().setProperty(key, null); + intentPropertyKeys.remove(key); + } + for (Map.Entry entry : nextProperties.entrySet()) { + Display.getInstance().setProperty(entry.getKey(), entry.getValue()); + intentPropertyKeys.add(entry.getKey()); + } + } else { + intentPropertyKeys.clear(); + intentPropertyKeys.addAll(nextProperties.keySet()); + } + + lastPublishedIntent = intent; + } + } public static Context getContext() { Context out = getActivity(); @@ -1444,15 +1444,15 @@ public static boolean isImmersive() { } return isImmersive(getActivity().getWindow()); } - public static boolean isImmersive(Window window) { - if (Build.VERSION.SDK_INT >= 35) { - // Android 15+ is always immersive (overlay mode by default) - return true; - } - // On Android 34 and below, we can't detect decorFitsSystemWindows - // reliably at runtime. So the app must make the decision explicitly. - return false; - } + public static boolean isImmersive(Window window) { + if (Build.VERSION.SDK_INT >= 35) { + // Android 15+ is always immersive (overlay mode by default) + return true; + } + // On Android 34 and below, we can't detect decorFitsSystemWindows + // reliably at runtime. So the app must make the decision explicitly. + return false; + } public static Rect getSystemBarInsets(final View rootView) { final Rect result = new Rect(0, 0, 0, 0); try { @@ -1471,34 +1471,34 @@ public static Rect getSystemBarInsets(final View rootView) { .invoke(insets, new Object[]{systemBarsMask}); if (insetsObject == null) return result; Class insetsClass = insetsObject.getClass(); - int left = ((Integer) insetsClass.getField("left").get(insetsObject)).intValue(); - int top = ((Integer) insetsClass.getField("top").get(insetsObject)).intValue(); - int right = ((Integer) insetsClass.getField("right").get(insetsObject)).intValue(); - int bottom = ((Integer) insetsClass.getField("bottom").get(insetsObject)).intValue(); - // Include mandatory gesture insets (e.g. gesture navigation handle area). - // Some devices expose a larger interaction-protected bottom region here - // than in plain system bar insets. - try { - int mandatoryGesturesMask = ((Integer) typeClass - .getMethod("mandatorySystemGestures") - .invoke(null)).intValue(); - Object mandatoryInsetsObject = insets.getClass() - .getMethod("getInsets", new Class[]{int.class}) - .invoke(insets, new Object[]{mandatoryGesturesMask}); - if (mandatoryInsetsObject != null) { - Class mandatoryInsetsClass = mandatoryInsetsObject.getClass(); - left = Math.max(left, ((Integer) mandatoryInsetsClass.getField("left").get(mandatoryInsetsObject)).intValue()); - top = Math.max(top, ((Integer) mandatoryInsetsClass.getField("top").get(mandatoryInsetsObject)).intValue()); - right = Math.max(right, ((Integer) mandatoryInsetsClass.getField("right").get(mandatoryInsetsObject)).intValue()); - bottom = Math.max(bottom, ((Integer) mandatoryInsetsClass.getField("bottom").get(mandatoryInsetsObject)).intValue()); - } - } catch (Throwable t) { - // Ignore if mandatory gesture insets are unavailable. - } - result.set(left, top, right, bottom); - } catch (Throwable t) { - t.printStackTrace(); // Optional: log this or suppress if expected - } + int left = ((Integer) insetsClass.getField("left").get(insetsObject)).intValue(); + int top = ((Integer) insetsClass.getField("top").get(insetsObject)).intValue(); + int right = ((Integer) insetsClass.getField("right").get(insetsObject)).intValue(); + int bottom = ((Integer) insetsClass.getField("bottom").get(insetsObject)).intValue(); + // Include mandatory gesture insets (e.g. gesture navigation handle area). + // Some devices expose a larger interaction-protected bottom region here + // than in plain system bar insets. + try { + int mandatoryGesturesMask = ((Integer) typeClass + .getMethod("mandatorySystemGestures") + .invoke(null)).intValue(); + Object mandatoryInsetsObject = insets.getClass() + .getMethod("getInsets", new Class[]{int.class}) + .invoke(insets, new Object[]{mandatoryGesturesMask}); + if (mandatoryInsetsObject != null) { + Class mandatoryInsetsClass = mandatoryInsetsObject.getClass(); + left = Math.max(left, ((Integer) mandatoryInsetsClass.getField("left").get(mandatoryInsetsObject)).intValue()); + top = Math.max(top, ((Integer) mandatoryInsetsClass.getField("top").get(mandatoryInsetsObject)).intValue()); + right = Math.max(right, ((Integer) mandatoryInsetsClass.getField("right").get(mandatoryInsetsObject)).intValue()); + bottom = Math.max(bottom, ((Integer) mandatoryInsetsClass.getField("bottom").get(mandatoryInsetsObject)).intValue()); + } + } catch (Throwable t) { + // Ignore if mandatory gesture insets are unavailable. + } + result.set(left, top, right, bottom); + } catch (Throwable t) { + t.printStackTrace(); // Optional: log this or suppress if expected + } return result; } @@ -4933,23 +4933,49 @@ public boolean hasNativeTheme() { */ public void installNativeTheme() { hasNativeTheme(); - if (nativeThemeAvailable) { - try { - InputStream is; - if (android.os.Build.VERSION.SDK_INT < 14 && !isTablet() || Display.getInstance().getProperty("and.hololight", "false").equals("true")) { - is = getResourceAsStream(getClass(), "/androidTheme.res"); + if (!nativeThemeAvailable) { + return; + } + try { + // Resolve desired theme flavor. cn1.androidTheme is the new per-CN1 + // hint (material | hololight | legacy). and.hololight stays + // recognized for back-compat. Anything else, including unset, + // defaults to the Material 3 modern theme. + String mode = Display.getInstance().getProperty("cn1.androidTheme", null); + if (mode == null) { + if ("true".equalsIgnoreCase(Display.getInstance().getProperty("and.hololight", "false"))) { + mode = "hololight"; } else { - is = getResourceAsStream(getClass(), "/android_holo_light.res"); + mode = "material"; } - Resources r = Resources.open(is); - Hashtable h = r.getTheme(r.getThemeResourceNames()[0]); - h.put("@commandBehavior", "Native"); - UIManager.getInstance().setThemeProps(h); - is.close(); - Display.getInstance().setCommandBehavior(Display.COMMAND_BEHAVIOR_NATIVE); - } catch (IOException ex) { - ex.printStackTrace(); + } else { + mode = mode.toLowerCase(); + } + + String resPath; + if ("material".equals(mode) || "modern".equals(mode)) { + resPath = "/AndroidMaterialTheme.res"; + } else if ("hololight".equals(mode) || "holo".equals(mode)) { + resPath = "/android_holo_light.res"; + } else { + resPath = "/androidTheme.res"; + } + + InputStream is = getResourceAsStream(getClass(), resPath); + if (is == null) { + // Modern theme may not be in the apk if the framework build + // skipped native-themes generation. Fall back to holo light + // so the app still boots. + is = getResourceAsStream(getClass(), "/android_holo_light.res"); } + Resources r = Resources.open(is); + Hashtable h = r.getTheme(r.getThemeResourceNames()[0]); + h.put("@commandBehavior", "Native"); + UIManager.getInstance().setThemeProps(h); + is.close(); + Display.getInstance().setCommandBehavior(Display.COMMAND_BEHAVIOR_NATIVE); + } catch (IOException ex) { + ex.printStackTrace(); } } diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/BuildHintSchemaDefaults.java b/Ports/JavaSE/src/com/codename1/impl/javase/BuildHintSchemaDefaults.java new file mode 100644 index 0000000000..ee07067fd5 --- /dev/null +++ b/Ports/JavaSE/src/com/codename1/impl/javase/BuildHintSchemaDefaults.java @@ -0,0 +1,73 @@ +/* + * 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + */ +package com.codename1.impl.javase; + +/** + * Registers BuildHintEditor schema defaults for the native-theme build + * hints introduced in the CSS-driven native-themes work. The editor + * (BuildHintEditor) discovers known hints by scanning System properties + * matching {@code codename1.arg.{{ HintName }}.}; cn1libs and the + * framework itself can register hints by setting such properties before + * the editor loads. + * + *

Called from {@link Simulator#main(String[])} so the Native Theme + * group appears in the Build Hints dialog by default. + */ +final class BuildHintSchemaDefaults { + + private BuildHintSchemaDefaults() { + } + + static void register() { + // Group. + set("{{@nativeTheme}}.label", "Native Theme"); + set("{{@nativeTheme}}.description", + "Controls the Codename One look & feel on iOS and Android. " + + "Modern themes are generated from CSS under native-themes/; " + + "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", + "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."); + + // iOS. + set("{{#nativeTheme#ios.themeMode}}.label", "iOS theme"); + set("{{#nativeTheme#ios.themeMode}}.type", "Select"); + set("{{#nativeTheme#ios.themeMode}}.values", "auto,modern,ios7,legacy"); + set("{{#nativeTheme#ios.themeMode}}.description", + "auto = modern (default). modern / liquid = Liquid Glass. " + + "ios7 / flat = pre-liquid flat iOS 7 theme. " + + "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."); + } + + /** Idempotent setter: does not overwrite user / project-level hint metadata. */ + private static void set(String suffix, String value) { + String key = "codename1.arg." + suffix; + if (System.getProperty(key) == null) { + System.setProperty(key, value); + } + } +} diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java index 86117ff3f3..404e329d76 100644 --- a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java +++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java @@ -2769,6 +2769,48 @@ private void loadSkinFile(InputStream skin, final JFrame frm) { platformName = props.getProperty("platformName", "se"); platformOverrides = props.getProperty("overrideNames", "").split(","); + + // Native theme override: the simulator ships all shipped-with- + // framework themes (iOSModernTheme.res, AndroidMaterialTheme.res, + // 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. + 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; + } + } else if ("embedded".equalsIgnoreCase(overrideTheme)) { + // Explicit "keep the skin's embedded theme". + overrideTheme = null; + } + if (overrideTheme != null) { + InputStream bundled = JavaSEPort.class.getResourceAsStream("/" + overrideTheme + ".res"); + if (bundled != null) { + try { + ByteArrayOutputStream bo = new ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int n; + while ((n = bundled.read(buf)) > 0) { + bo.write(buf, 0, n); + } + nativeThemeData = bo.toByteArray(); + } catch (IOException ioErr) { + ioErr.printStackTrace(); + } finally { + try { bundled.close(); } catch (IOException ignored) { System.err.println("close: " + ignored); } + } + } + } String ua = null; if (platformName.equals("and")) { ua = "Mozilla/5.0 (Linux; U; Android 2.2; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1"; @@ -4629,6 +4671,7 @@ public void actionPerformed(ActionEvent e) { bar.add(simulateMenu); bar.add(toolsMenu); bar.add(skinMenu); + bar.add(createNativeThemeMenu()); bar.add(helpMenu); } @@ -4736,6 +4779,49 @@ private String getCurrentSkinName() { return skin; } + /** + * 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. + */ + private JMenu createNativeThemeMenu() { + JMenu m = new JMenu("Native Theme"); + m.setDoubleBuffered(true); + String[][] items = { + {"auto", "Auto (based on skin)"}, + {"iOSModernTheme", "iOS Modern (Liquid Glass)"}, + {"iOS7Theme", "iOS 7 (Flat)"}, + {"iPhoneTheme", "iPhone (Pre-Flat)"}, + {"AndroidMaterialTheme", "Android Material"}, + {"android_holo_light", "Android Holo Light"}, + {"androidTheme", "Android Legacy"}, + {"embedded", "Use skin's embedded theme"} + }; + String current = Preferences.userNodeForPackage(JavaSEPort.class) + .get("simulatorNativeTheme", "auto"); + ButtonGroup group = new ButtonGroup(); + for (final String[] entry : items) { + JRadioButtonMenuItem mi = new JRadioButtonMenuItem(entry[1]); + mi.setSelected(current.equals(entry[0])); + mi.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + Preferences.userNodeForPackage(JavaSEPort.class) + .put("simulatorNativeTheme", entry[0]); + System.setProperty("reload.simulator", "true"); + if (window != null) { + window.dispose(); + } + } + }); + group.add(mi); + m.add(mi); + } + return m; + } + private JMenu createSkinsMenu(final JFrame frm, final JMenu menu) throws MalformedURLException { JMenu m; if (menu == null) { diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/Simulator.java b/Ports/JavaSE/src/com/codename1/impl/javase/Simulator.java index 1c75fc1b69..e5d7587b46 100644 --- a/Ports/JavaSE/src/com/codename1/impl/javase/Simulator.java +++ b/Ports/JavaSE/src/com/codename1/impl/javase/Simulator.java @@ -93,6 +93,12 @@ public static void main(final String[] argv) throws Exception { } catch (ClassNotFoundException ex) { } System.setProperty("NSHighResolutionCapable", "true"); + + // Register framework-level BuildHintEditor schema defaults (see + // Ports/JavaSE/src/com/codename1/impl/javase/BuildHintSchemaDefaults) + // before any code reads codename1.arg.{{*}} system properties. + BuildHintSchemaDefaults.register(); + String skin = System.getProperty("dskin"); if (skin == null) { System.setProperty("dskin", DEFAULT_SKIN); 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 46979f06b6..0ecfd85d57 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 @@ -2657,9 +2657,44 @@ public void setHeight(HTMLCanvasElement canvas, int canvasHeight) { @Override public void installNativeTheme(){ try { - String nativeTheme = Display.getInstance().getProperty("javascript.native.theme", isAndroid_() ? "/android_holo_light.res" : "/iOS7Theme.res"); + // 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). + 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"; + } + } + String nativeTheme = Display.getInstance().getProperty("javascript.native.theme", defaultTheme); Log.p("[installNativeTheme] attempting to load theme from " + nativeTheme); - Resources r = Resources.open(nativeTheme); + Resources r; + try { + r = Resources.open(nativeTheme); + } catch (Throwable notFound) { + // Fall back to the legacy theme if the chosen .res isn't in + // the JS bundle (partial build, missing mirror step, etc.). + String fallback = isAndroid_() ? "/android_holo_light.res" : "/iOS7Theme.res"; + Log.p("[installNativeTheme] " + nativeTheme + " missing, falling back to " + fallback); + r = Resources.open(fallback); + } Log.p("[installNativeTheme] loaded theme resources, theme names: " + java.util.Arrays.toString(r.getThemeResourceNames())); Hashtable tp = r.getTheme(r.getThemeResourceNames()[0]); diff --git a/Ports/JavaScriptPort/src/main/webapp/assets/.gitignore b/Ports/JavaScriptPort/src/main/webapp/assets/.gitignore new file mode 100644 index 0000000000..caa62305a9 --- /dev/null +++ b/Ports/JavaScriptPort/src/main/webapp/assets/.gitignore @@ -0,0 +1,5 @@ +# Generated by scripts/build-native-themes.sh. Mirrors of Themes/ so the +# JS port runtime picks up the modern native themes. The CSS sources in +# native-themes/ are authoritative. +iOSModernTheme.res +AndroidMaterialTheme.res diff --git a/Ports/JavaScriptPort/src/main/webapp/port.js b/Ports/JavaScriptPort/src/main/webapp/port.js index c58bf484c1..1a0ff1051f 100644 --- a/Ports/JavaScriptPort/src/main/webapp/port.js +++ b/Ports/JavaScriptPort/src/main/webapp/port.js @@ -2783,7 +2783,21 @@ const baseTestFailMethodId = "cn1_com_codenameone_examples_hellocodenameone_test const baseTestDoneMethodId = "cn1_com_codenameone_examples_hellocodenameone_tests_BaseTest_done"; const cn1ssForcedTimeoutTestClasses = Object.freeze({ "com_codenameone_examples_hellocodenameone_tests_MediaPlaybackScreenshotTest": "mediaPlayback", - "com_codenameone_examples_hellocodenameone_tests_BytecodeTranslatorRegressionTest": "bytecodeTranslatorRegression" + "com_codenameone_examples_hellocodenameone_tests_BytecodeTranslatorRegressionTest": "bytecodeTranslatorRegression", + "com_codenameone_examples_hellocodenameone_tests_ButtonThemeScreenshotTest": "themeScreenshot", + "com_codenameone_examples_hellocodenameone_tests_TextFieldThemeScreenshotTest": "themeScreenshot", + "com_codenameone_examples_hellocodenameone_tests_CheckBoxRadioThemeScreenshotTest": "themeScreenshot", + "com_codenameone_examples_hellocodenameone_tests_SwitchThemeScreenshotTest": "themeScreenshot", + "com_codenameone_examples_hellocodenameone_tests_PickerThemeScreenshotTest": "themeScreenshot", + "com_codenameone_examples_hellocodenameone_tests_ToolbarThemeScreenshotTest": "themeScreenshot", + "com_codenameone_examples_hellocodenameone_tests_TabsThemeScreenshotTest": "themeScreenshot", + "com_codenameone_examples_hellocodenameone_tests_MultiButtonThemeScreenshotTest": "themeScreenshot", + "com_codenameone_examples_hellocodenameone_tests_ListThemeScreenshotTest": "themeScreenshot", + "com_codenameone_examples_hellocodenameone_tests_DialogThemeScreenshotTest": "themeScreenshot", + "com_codenameone_examples_hellocodenameone_tests_FloatingActionButtonThemeScreenshotTest": "themeScreenshot", + "com_codenameone_examples_hellocodenameone_tests_SpanLabelThemeScreenshotTest": "themeScreenshot", + "com_codenameone_examples_hellocodenameone_tests_DarkLightShowcaseThemeScreenshotTest": "themeScreenshot", + "com_codenameone_examples_hellocodenameone_tests_PaletteOverrideThemeScreenshotTest": "themeScreenshot" }); const cn1ssForcedTimeoutTestNames = Object.freeze({ "MediaPlaybackScreenshotTest": "mediaPlayback", @@ -2793,7 +2807,21 @@ const cn1ssForcedTimeoutTestNames = Object.freeze({ "CallDetectionAPITest": "callDetectionApi", "LocalNotificationOverrideTest": "localNotificationOverride", "Base64NativePerformanceTest": "base64NativePerformance", - "AccessibilityTest": "accessibility" + "AccessibilityTest": "accessibility", + "ButtonThemeScreenshotTest": "themeScreenshot", + "TextFieldThemeScreenshotTest": "themeScreenshot", + "CheckBoxRadioThemeScreenshotTest": "themeScreenshot", + "SwitchThemeScreenshotTest": "themeScreenshot", + "PickerThemeScreenshotTest": "themeScreenshot", + "ToolbarThemeScreenshotTest": "themeScreenshot", + "TabsThemeScreenshotTest": "themeScreenshot", + "MultiButtonThemeScreenshotTest": "themeScreenshot", + "ListThemeScreenshotTest": "themeScreenshot", + "DialogThemeScreenshotTest": "themeScreenshot", + "FloatingActionButtonThemeScreenshotTest": "themeScreenshot", + "SpanLabelThemeScreenshotTest": "themeScreenshot", + "DarkLightShowcaseThemeScreenshotTest": "themeScreenshot", + "PaletteOverrideThemeScreenshotTest": "themeScreenshot" }); if (jvm && typeof jvm.addVirtualMethod === "function" && jvm.classes && jvm.classes["java_lang_String"]) { diff --git a/Ports/iOSPort/build.xml b/Ports/iOSPort/build.xml index 3bd0652555..c7aafebde3 100644 --- a/Ports/iOSPort/build.xml +++ b/Ports/iOSPort/build.xml @@ -74,6 +74,11 @@ + + diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java index c839d10da4..7fe64a1fa5 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java @@ -1341,8 +1341,19 @@ public void run() { public void installNativeTheme() { try { Resources r; - - if(iosMode.equals("modern")) { + String mode = iosMode == null ? "auto" : iosMode.toLowerCase(); + // Liquid-glass modern theme generated from native-themes/ios-modern/theme.css. + // "auto" defaults to modern; previous iOS 7 flat theme kept reachable as "ios7" + // or "flat"; pre-flat iPhone theme as "legacy" or "iphone". + if(mode.equals("modern") || mode.equals("liquid") || mode.equals("auto")) { + InputStream in = getResourceAsStream("/iOSModernTheme.res"); + if (in != null) { + r = Resources.open(in); + UIManager.getInstance().setThemeProps(r.getTheme(r.getThemeResourceNames()[0])); + return; + } + // Modern theme isn't in the jar (e.g. framework build hasn't generated + // it yet) - fall back to the iOS 7 flat theme so the app still boots. r = Resources.open("/iOS7Theme.res"); Hashtable tp = r.getTheme(r.getThemeResourceNames()[0]); if(!nativeInstance.isIOS7()) { @@ -1351,20 +1362,25 @@ public void installNativeTheme() { UIManager.getInstance().setThemeProps(tp); return; } - if(iosMode.equals("auto")) { - if(nativeInstance.isIOS7()) { - r = Resources.open("/iOS7Theme.res"); - } else { - r = Resources.open("/iPhoneTheme.res"); + if(mode.equals("ios7") || mode.equals("flat")) { + r = Resources.open("/iOS7Theme.res"); + Hashtable tp = r.getTheme(r.getThemeResourceNames()[0]); + if(!nativeInstance.isIOS7()) { + tp.put("TitleArea.padding", "0,0,0,0"); } - UIManager.getInstance().setThemeProps(r.getTheme(r.getThemeResourceNames()[0])); + UIManager.getInstance().setThemeProps(tp); return; } + // "legacy" / "iphone" / anything else: pre-flat iPhone theme. r = Resources.open("/iPhoneTheme.res"); UIManager.getInstance().setThemeProps(r.getTheme(r.getThemeResourceNames()[0])); } catch (IOException ex) { ex.printStackTrace(); - } + } + } + + private InputStream getResourceAsStream(String name) { + return IOSImplementation.class.getResourceAsStream(name); } private long getNSData(InputStream i) { diff --git a/Themes/.gitignore b/Themes/.gitignore new file mode 100644 index 0000000000..36ce6ac0c2 --- /dev/null +++ b/Themes/.gitignore @@ -0,0 +1,4 @@ +# Generated by scripts/build-native-themes.sh from native-themes/*/theme.css. +# These are build artifacts; the CSS sources in native-themes/ are authoritative. +iOSModernTheme.res +AndroidMaterialTheme.res diff --git a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc index b1351ef427..d49ce234e5 100644 --- a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc +++ b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc @@ -310,7 +310,13 @@ Currently only supported for App Store builds. See https://www.codenameone.com/ |Comma separated list of url schemes that `canExecute` will respect on iOS. If the url scheme isn't mentioned here `canExecute` will return false starting with iOS 9. Notice that this collides with `ios.plistInject` when used with the `LSApplicationQueriesSchemes...` value so you should use one or the other. E.g. to enable `canExecute` for a url like `myurl://xys` you can use: `myurl,myotherurl` |ios.themeMode -|default/legacy/modern/auto (defaults to default). Default means you don't define a theme mode. Currently this is equivalent to legacy. In the future we'll switch this to be equivalent to auto. legacy - this will behave like iOS 6 regardless of the device you're running on. modern - this will behave like iOS 7 regardless of the device you're running on. auto - this will behave like iOS 6 on older devices and iOS 7 on newer devices. +|`auto` (default), `modern`, `ios7`, `legacy`. `auto` and `modern` both load the CSS-generated iOS Modern (liquid-glass) theme shipped from `native-themes/ios-modern/theme.css`. `ios7` / `flat` loads the pre-liquid iOS 7 flat theme; `legacy` / `iphone` loads the pre-iOS 7 iPhone theme. The default flipped to modern with the CSS-driven native themes refactor; use `ios7` to keep the previous behavior. + +|cn1.androidTheme +|`material` (default), `hololight`, `legacy`. `material` / `modern` loads the CSS-generated Android Material 3 theme shipped from `native-themes/android-material/theme.css`. `hololight` loads Android Holo Light (previous default on API 14+). `legacy` loads the pre-Holo Android theme. `and.hololight=true` is still accepted for back-compat and maps to `hololight`. + +|cn1.nativeTheme +|`modern`, `legacy`, `custom` (default unset). Cross-platform override that sets both `ios.themeMode` and `cn1.androidTheme` together when those are not set explicitly. `modern` = liquid glass + Material 3, `legacy` = iOS 7 flat + Holo Light, `custom` disables the framework native theme entirely. |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/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 5585e6a5a9..b888fbb8cc 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,7 +356,20 @@ public boolean build(File sourceZip, BuildRequest request) throws BuildException } debug("Xcode version is "+xcodeVersion); - String iosMode = request.getArg("ios.themeMode", "auto"); + // ios.themeMode stays the platform-specific knob; cn1.nativeTheme is + // the cross-platform meta hint. modern / legacy on the meta hint + // translate to the equivalent iOS values when ios.themeMode is unset. + String iosMode = request.getArg("ios.themeMode", null); + if (iosMode == null) { + String sharedMode = request.getArg("cn1.nativeTheme", null); + if ("legacy".equalsIgnoreCase(sharedMode)) { + iosMode = "ios7"; + } else if ("modern".equalsIgnoreCase(sharedMode)) { + iosMode = "modern"; + } else { + iosMode = "auto"; + } + } tmpFile = getBuildDirectory(); if (tmpFile == null) { diff --git a/maven/css-compiler/pom.xml b/maven/css-compiler/pom.xml new file mode 100644 index 0000000000..9189e8fb4f --- /dev/null +++ b/maven/css-compiler/pom.xml @@ -0,0 +1,100 @@ + + + + + com.codenameone + codenameone + 8.0-SNAPSHOT + + 4.0.0 + com.codenameone + codenameone-css-compiler + 8.0-SNAPSHOT + jar + codenameone-css-compiler + + + UTF-8 + 1.8 + 1.8 + + + + + com.codenameone + codenameone-core + + + com.vaadin.external.flute + flute + 1.3.0.gg2 + + + org.w3c.css + sac + 1.3 + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + build-jar-with-dependencies + package + + single + + + + + com.codename1.designer.css.NoCefCSSCLI + + + + jar-with-dependencies + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-jar-with-dependencies + package + + attach-artifact + + + + + ${project.build.directory}/${project.build.finalName}-jar-with-dependencies.jar + jar + jar-with-dependencies + + + + + + + + + diff --git a/CodenameOneDesigner/src/com/codename1/designer/css/CSSTheme.java b/maven/css-compiler/src/main/java/com/codename1/designer/css/CSSTheme.java similarity index 98% rename from CodenameOneDesigner/src/com/codename1/designer/css/CSSTheme.java rename to maven/css-compiler/src/main/java/com/codename1/designer/css/CSSTheme.java index 341e20e7ac..5be54f55d4 100644 --- a/CodenameOneDesigner/src/com/codename1/designer/css/CSSTheme.java +++ b/maven/css-compiler/src/main/java/com/codename1/designer/css/CSSTheme.java @@ -117,7 +117,15 @@ public class CSSTheme { EditableResources res; private String themeName = "Theme"; private ImagesMetadata imagesMetadata = new ImagesMetadata(); - + + /** + * When true, {@link #createImageBorders(WebViewProvider)} refuses to rasterize any + * style via CEF and instead throws an {@link IllegalStateException} listing offending + * rules. Used by the native-themes build to guarantee that shipped platform themes + * contain no rasterized fallback images (which would bloat the .res file). + */ + public static boolean strictNoCef = false; + private List fontFaces = new ArrayList(); public static final int DEFAULT_TARGET_DENSITY = com.codename1.ui.Display.DENSITY_HD; public static final String[] supportedNativeBorderTypes = new String[]{ @@ -2611,10 +2619,51 @@ public static interface WebViewProvider { com.codename1.ui.BrowserComponent getWebView(); } private static String currentId; + + private void enforceNoCef() { + List offenders = new ArrayList(); + String[] states = new String[] {"unselected", "selected", "pressed", "disabled"}; + for (String id : elements.keySet()) { + if (!isModified(id)) { + continue; + } + Element e = (Element) elements.get(id); + Element[] stateElements = new Element[] { + e.getUnselected(), e.getSelected(), e.getPressed(), e.getDisabled() + }; + for (int i = 0; i < stateElements.length; i++) { + Map styles = + (Map) stateElements[i].getFlattenedStyle(); + if (e.requiresImageBorder(styles)) { + offenders.add(id + "." + states[i] + " (image border)"); + } else if (e.requiresBackgroundImageGeneration(styles)) { + offenders.add(id + "." + states[i] + " (background image)"); + } + } + } + if (!offenders.isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append("CSS rules require CEF-backed image rasterization, which is disabled "); + sb.append("in no-cef mode (native-themes build). Offending rules:\n"); + for (String o : offenders) { + sb.append(" - ").append(o).append("\n"); + } + sb.append("Fix: avoid box-shadow, border-radius combined with a visible border, "); + sb.append("mixed-side borders, filter, and complex gradients. "); + sb.append("Use cn1-round-border / cn1-pill-border or solid backgrounds instead. "); + sb.append("If the effect is genuinely required, extend the CSS compiler and/or "); + sb.append("the resource format with a native primitive rather than rasterizing."); + throw new IllegalStateException(sb.toString()); + } + } + public void createImageBorders(WebViewProvider webviewProvider) { if (res == null) { res = new EditableResourcesForCSS(resourceFile); } + if (strictNoCef) { + enforceNoCef(); + } ArrayList borders = new ArrayList(); ResourcesMutator resm = new ResourcesMutator(res, Display.DENSITY_VERY_HIGH, minDpi, maxDpi); @@ -6947,7 +6996,7 @@ private static String transformDarkModeMediaQueries(String css) { int len = css.length(); int pos = 0; while (pos < len) { - int mediaPos = css.indexOf("@media", pos); + int mediaPos = indexOfOutsideComments(css, "@media", pos); if (mediaPos < 0) { out.append(css.substring(pos)); break; @@ -7036,6 +7085,32 @@ private static String toDarkSelectors(String selectors) { return out.toString(); } + /// Finds the next occurrence of `needle` in `css` starting at `fromPos`, + /// skipping over any `/* ... */` block-comment regions. Used by the + /// dark-mode rewriter so a literal "@media" token documented inside a + /// header comment isn't mistaken for a real media block. + private static int indexOfOutsideComments(String css, String needle, int fromPos) { + int len = css.length(); + int pos = fromPos; + while (pos < len) { + int commentStart = css.indexOf("/*", pos); + int hit = css.indexOf(needle, pos); + if (hit < 0) { + return -1; + } + if (commentStart < 0 || hit < commentStart) { + return hit; + } + int commentEnd = css.indexOf("*/", commentStart + 2); + if (commentEnd < 0) { + // Unterminated comment - everything from here on is comment. + return -1; + } + pos = commentEnd + 2; + } + return -1; + } + private static int findMatchingBrace(String css, int openPos) { int depth = 0; int len = css.length(); @@ -7320,11 +7395,12 @@ private void property_(String string, LexicalUnit _lu, boolean bln) throws CSSEx } catch (InstantiationException ex) { Logger.getLogger(CSSTheme.class.getName()).log(Level.SEVERE, null, ex); } catch (NullPointerException ex) { - if (ex.getMessage().contains("encoding properties")) { + String msg = ex.getMessage(); + if (msg != null && msg.contains("encoding properties")) { // This error always happens and there doesn't seem to be a way to fix it... so let's just hide // it . Doesn't seem to hurt anything. } else { - //Logger.getLogger(CSSTheme.class.getName()).log(Level.SEVERE, null, ex); + Logger.getLogger(CSSTheme.class.getName()).log(Level.SEVERE, "Failed to load CSS theme", ex); } } catch (ClassCastException ex) { Logger.getLogger(CSSTheme.class.getName()).log(Level.SEVERE, null, ex); diff --git a/CodenameOneDesigner/src/com/codename1/designer/css/Color.java b/maven/css-compiler/src/main/java/com/codename1/designer/css/Color.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/designer/css/Color.java rename to maven/css-compiler/src/main/java/com/codename1/designer/css/Color.java diff --git a/CodenameOneDesigner/src/com/codename1/designer/css/MissingNativeBrowserException.java b/maven/css-compiler/src/main/java/com/codename1/designer/css/MissingNativeBrowserException.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/designer/css/MissingNativeBrowserException.java rename to maven/css-compiler/src/main/java/com/codename1/designer/css/MissingNativeBrowserException.java diff --git a/maven/css-compiler/src/main/java/com/codename1/designer/css/NoCefCSSCLI.java b/maven/css-compiler/src/main/java/com/codename1/designer/css/NoCefCSSCLI.java new file mode 100644 index 0000000000..36726d1808 --- /dev/null +++ b/maven/css-compiler/src/main/java/com/codename1/designer/css/NoCefCSSCLI.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * + * Minimal CLI entry point for compiling a CSS theme to a .res file with the + * strictNoCef flag enabled. Unlike CN1CSSCLI (in the Designer module), + * this class does NOT + * pull in any JavaSE port classes or initialize a BrowserComponent host -- + * it is designed to live in a thin "css-compiler" jar that depends only on + * codenameone-core, flute, and sac. + * + * The intended consumer is the native-themes build, which generates the + * shipped platform themes from CSS source. Any CSS rule that would require + * CEF-backed image rasterization fails the compile (see CSSTheme.enforceNoCef). + * + * Usage: + * java -jar codenameone-css-compiler-jar-with-dependencies.jar \ + * -input path/to/theme.css \ + * -output path/to/Theme.res + */ +package com.codename1.designer.css; + +import java.io.File; +import java.net.URL; + +public class NoCefCSSCLI { + + public static void main(String[] args) throws Exception { + if (hasArg(args, "help") || hasArg(args, "h") || args.length == 0) { + printUsage(); + return; + } + String inputPath = getArg(args, "input", "i"); + String outputPath = getArg(args, "output", "o"); + if (inputPath == null || outputPath == null) { + printUsage(); + System.exit(1); + } + + File inputFile = new File(inputPath); + File outputFile = new File(outputPath); + if (!inputFile.exists()) { + System.err.println("Input CSS file does not exist: " + inputFile.getAbsolutePath()); + System.exit(2); + } + File parent = outputFile.getAbsoluteFile().getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs()) { + System.err.println("Could not create output directory: " + parent.getAbsolutePath()); + System.exit(3); + } + + CSSTheme.strictNoCef = true; + System.out.println("Compiling " + inputFile.getName() + " -> " + outputFile.getName() + " (no-cef)"); + + URL url = inputFile.toURI().toURL(); + CSSTheme theme = CSSTheme.load(url); + if (theme == null) { + System.err.println("CSSTheme.load returned null for " + inputFile + + " - parser probably failed to initialize. See stderr above for details."); + System.exit(4); + } + theme.cssFile = inputFile; + theme.resourceFile = outputFile; + + // createImageBorders walks every rule. With strictNoCef=true it + // throws an IllegalStateException listing any rule that would need + // CEF rasterization before the unreachable webview path would run. + // The provider below is a safety net: if a post-enforceNoCef code + // path still asks for a WebView, fail loud instead of NPE. + theme.createImageBorders(new CSSTheme.WebViewProvider() { + @Override + public com.codename1.ui.BrowserComponent getWebView() { + throw new IllegalStateException( + "CSS compile in no-cef mode must not request a WebView. " + + "enforceNoCef should have rejected the offending rule; " + + "please report this bug."); + } + }); + + theme.updateResources(); + theme.save(outputFile); + System.out.println("Wrote " + outputFile.getAbsolutePath()); + } + + private static boolean hasArg(String[] args, String... names) { + return getArg(args, names) != null; + } + + private static String getArg(String[] args, String... names) { + for (int i = 0; i < args.length; i++) { + String a = args[i]; + if (a == null) { + continue; + } + if (a.startsWith("-")) { + String key = a.substring(1); + while (key.startsWith("-")) { + key = key.substring(1); + } + int eq = key.indexOf('='); + String value; + if (eq >= 0) { + value = key.substring(eq + 1); + key = key.substring(0, eq); + } else if (i + 1 < args.length && !args[i + 1].startsWith("-")) { + value = args[i + 1]; + } else { + value = "true"; + } + for (String n : names) { + if (n.equals(key)) { + return value; + } + } + } + } + return null; + } + + private static void printUsage() { + System.out.println("Codename One CSS Compiler (no-cef, native-themes build)"); + System.out.println(); + System.out.println("Usage:"); + System.out.println(" java -jar codenameone-css-compiler--jar-with-dependencies.jar \\"); + System.out.println(" -input \\"); + System.out.println(" -output "); + System.out.println(); + System.out.println("Any CSS rule requiring CEF-backed image rasterization (box-shadow,"); + System.out.println("border-radius combined with visible border, filter, complex gradients)"); + System.out.println("fails the compile with the list of offending rules."); + } +} diff --git a/CodenameOneDesigner/src/com/codename1/designer/css/PollingFileWatcher.java b/maven/css-compiler/src/main/java/com/codename1/designer/css/PollingFileWatcher.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/designer/css/PollingFileWatcher.java rename to maven/css-compiler/src/main/java/com/codename1/designer/css/PollingFileWatcher.java diff --git a/CodenameOneDesigner/src/com/codename1/designer/css/ResourcesMutator.java b/maven/css-compiler/src/main/java/com/codename1/designer/css/ResourcesMutator.java similarity index 99% rename from CodenameOneDesigner/src/com/codename1/designer/css/ResourcesMutator.java rename to maven/css-compiler/src/main/java/com/codename1/designer/css/ResourcesMutator.java index 5011ba220c..9764c5b21a 100644 --- a/CodenameOneDesigner/src/com/codename1/designer/css/ResourcesMutator.java +++ b/maven/css-compiler/src/main/java/com/codename1/designer/css/ResourcesMutator.java @@ -625,9 +625,9 @@ public void run() { web.execute("$(document).ready(function(){ captureScreenshots();});"); //web.getEngine().executeScript("window.onload = function(){window.app.ready()};"); } catch (IllegalArgumentException ex) { - Logger.getLogger(CN1CSSCompiler.class.getName()).log(Level.SEVERE, null, ex); + Logger.getLogger(ResourcesMutator.class.getName()).log(Level.SEVERE, null, ex); } catch (SecurityException ex) { - Logger.getLogger(CN1CSSCompiler.class.getName()).log(Level.SEVERE, null, ex); + Logger.getLogger(ResourcesMutator.class.getName()).log(Level.SEVERE, null, ex); } }; @@ -696,7 +696,7 @@ BufferedImage createHtmlScreenshot(BrowserComponent web, String html) { try { lock.wait(); } catch (InterruptedException ex) { - Logger.getLogger(CN1CSSCompiler.class.getName()).log(Level.SEVERE, null, ex); + Logger.getLogger(ResourcesMutator.class.getName()).log(Level.SEVERE, null, ex); } } } diff --git a/CodenameOneDesigner/src/com/codename1/designer/css/SimpleWebServer.java b/maven/css-compiler/src/main/java/com/codename1/designer/css/SimpleWebServer.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/designer/css/SimpleWebServer.java rename to maven/css-compiler/src/main/java/com/codename1/designer/css/SimpleWebServer.java diff --git a/CodenameOneDesigner/src/com/codename1/designer/css/WebviewSnapshotter.java b/maven/css-compiler/src/main/java/com/codename1/designer/css/WebviewSnapshotter.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/designer/css/WebviewSnapshotter.java rename to maven/css-compiler/src/main/java/com/codename1/designer/css/WebviewSnapshotter.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/CodenameOneAccessor.java b/maven/css-compiler/src/main/java/com/codename1/ui/CodenameOneAccessor.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/CodenameOneAccessor.java rename to maven/css-compiler/src/main/java/com/codename1/ui/CodenameOneAccessor.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/EditorFont.java b/maven/css-compiler/src/main/java/com/codename1/ui/EditorFont.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/EditorFont.java rename to maven/css-compiler/src/main/java/com/codename1/ui/EditorFont.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/EditorTTFFont.java b/maven/css-compiler/src/main/java/com/codename1/ui/EditorTTFFont.java similarity index 94% rename from CodenameOneDesigner/src/com/codename1/ui/EditorTTFFont.java rename to maven/css-compiler/src/main/java/com/codename1/ui/EditorTTFFont.java index f2d967602c..7646e53dc3 100644 --- a/CodenameOneDesigner/src/com/codename1/ui/EditorTTFFont.java +++ b/maven/css-compiler/src/main/java/com/codename1/ui/EditorTTFFont.java @@ -108,6 +108,14 @@ public void refresh() { throw new IllegalArgumentException("Unsupported native font type: " + nativeFontName); } InputStream is = getClass().getResourceAsStream("/com/codename1/impl/javase/Roboto-" + res + ".ttf"); + if (is == null) { + // Headless css-compiler run (native-themes build) does + // not ship the Roboto TTF resources from javase. The + // serialized .res stores the nativeFontName; the native + // handle is recreated at app runtime when a full CN1 + // implementation is available. + return; + } try { f = java.awt.Font.createFont(java.awt.Font.TRUETYPE_FONT, is); is.close(); diff --git a/CodenameOneDesigner/src/com/codename1/ui/animations/AnimationAccessor.java b/maven/css-compiler/src/main/java/com/codename1/ui/animations/AnimationAccessor.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/animations/AnimationAccessor.java rename to maven/css-compiler/src/main/java/com/codename1/ui/animations/AnimationAccessor.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/plaf/Accessor.java b/maven/css-compiler/src/main/java/com/codename1/ui/plaf/Accessor.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/plaf/Accessor.java rename to maven/css-compiler/src/main/java/com/codename1/ui/plaf/Accessor.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/plaf/ProtectedUIManager.java b/maven/css-compiler/src/main/java/com/codename1/ui/plaf/ProtectedUIManager.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/plaf/ProtectedUIManager.java rename to maven/css-compiler/src/main/java/com/codename1/ui/plaf/ProtectedUIManager.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/EditableResources.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/EditableResources.java similarity index 95% rename from CodenameOneDesigner/src/com/codename1/ui/util/EditableResources.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/EditableResources.java index 7a0ebeb2a2..2b4d6eef1f 100644 --- a/CodenameOneDesigner/src/com/codename1/ui/util/EditableResources.java +++ b/maven/css-compiler/src/main/java/com/codename1/ui/util/EditableResources.java @@ -25,16 +25,6 @@ package com.codename1.ui.util; import com.codename1.ui.Display; -import com.codename1.designer.ResourceEditorView; -import com.codename1.designer.DataEditor; -import com.codename1.designer.FontEditor; -import com.codename1.designer.ImageMultiEditor; -import com.codename1.designer.ImageRGBEditor; -import com.codename1.designer.L10nEditor; -import com.codename1.designer.MultiImageSVGEditor; -import com.codename1.designer.ThemeEditor; -import com.codename1.designer.TimelineEditor; -import com.codename1.designer.UserInterfaceEditor; import com.codename1.ui.EditorFont; import com.codename1.ui.EditorTTFFont; import com.codename1.ui.EncodedImage; @@ -44,12 +34,9 @@ import com.codename1.ui.animations.AnimationObject; import com.codename1.ui.animations.Motion; import com.codename1.ui.animations.Timeline; -import com.codename1.impl.javase.SVG; import com.codename1.ui.plaf.Border; import com.codename1.ui.plaf.Accessor; import com.codename1.ui.plaf.Style; -import com.codename1.designer.ResourceEditorApp; -import com.codename1.impl.javase.JavaSEPortWithSVGSupport; import com.codename1.ui.plaf.CSSBorder; import com.codename1.ui.plaf.RoundBorder; import com.codename1.ui.plaf.RoundRectBorder; @@ -93,7 +80,6 @@ import java.util.Map; import javax.imageio.ImageIO; import javax.swing.Icon; -import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.JPasswordField; @@ -112,10 +98,16 @@ public class EditableResources extends Resources implements TreeModel { private static final short MINOR_VERSION = 12; private static final short MAJOR_VERSION = 1; + private static final boolean IS_MAC; + static { + String osName = System.getProperty("os.name", ""); + IS_MAC = osName.toLowerCase().contains("mac"); + } + private boolean modified; private boolean loadingMode = false; private boolean xmlUI; - + private boolean ignoreSVGMode; private boolean ignorePNGMode; @@ -123,8 +115,77 @@ public class EditableResources extends Resources implements TreeModel { private File overrideFile; private EditableResources parentResource; private static boolean xmlEnabled; - + private HashSet themeLoadingErrors; + + /** + * Optional file context used to resolve relative TTF font paths embedded in + * the resource. The Designer subclass wires this to the currently loaded + * resource file; headless callers (e.g. the CSS native-themes build) leave + * it null and treat the legacy-font fallback as unavailable. + */ + private File loadedBaseFile; + + public void setLoadedBaseFile(File loadedBaseFile) { + this.loadedBaseFile = loadedBaseFile; + } + + /** + * Returns the base file used for TTF font path resolution, or null when + * no resource is currently anchored to a file. Overridable by the Designer + * subclass to query its GUI-managed loaded-file state. + */ + protected File getLoadedFile() { + return loadedBaseFile; + } + + /** + * Serialize a freshly-loaded UI container to the binary resource format. + * Called during openFile() when reading an XML-sourced resource. Throws + * in the base class: UI container persistence lives in the Designer + * editor subclass, which has the UserInterfaceEditor on its classpath. + */ + protected byte[] persistUIContainer(com.codename1.ui.Container cnt) { + throw new UnsupportedOperationException( + "UI container persistence requires EditableResourcesEditor"); + } + + /** + * Materialize a UI container from an XML component description. Called + * during openFile() when the resource was saved as XML. Returns null in + * the base class so openFile falls back to reading the binary UI blob; + * the Designer subclass overrides to drive UIBuilderOverride. + */ + protected com.codename1.ui.Container loadUIContainerFromXml(com.codename1.ui.util.xml.comps.ComponentEntry uiXMLData) { + return null; + } + + /** + * Materialize a UI container from a named binary UI resource so the + * containing resource can be re-serialized as XML. The Designer subclass + * overrides this with UIBuilderOverride; headless callers never hit the + * XML save path. + */ + protected com.codename1.ui.Container materializeUIContainer(String resourceName) { + throw new UnsupportedOperationException( + "UI container materialization for XML save requires EditableResourcesEditor"); + } + + /** + * Invoked at the end of openFile(). The Designer subclass overrides this + * to reset GUI-side theme caches; headless callers do nothing. + */ + protected void onOpenFileComplete() { + } + + /** + * Returns the runtime native theme as an EditableResources (may be null). + * The Designer subclass queries JavaSEPortWithSVGSupport; headless callers + * have no runtime theme so the base returns null. + */ + protected EditableResources getRuntimeNativeTheme() { + return null; + } public static void setXMLEnabled(boolean b) { xmlEnabled = b; @@ -836,9 +897,8 @@ public int compare(ComponentEntry o1, ComponentEntry o2) { } }); for(ComponentEntry uiXMLData : guiElements) { - UIBuilderOverride uib = new UIBuilderOverride(); - com.codename1.ui.Container cnt = uib.createInstance(uiXMLData, this); - + com.codename1.ui.Container cnt = loadUIContainerFromXml(uiXMLData); + // encountered an error loading the component fallback to loading with the binary types if(cnt == null) { for(Ui ui : xmlData.getUi()) { @@ -846,7 +906,7 @@ public int compare(ComponentEntry o1, ComponentEntry o2) { } break; } else { - byte[] data = UserInterfaceEditor.persistContainer(cnt, this); + byte[] data = persistUIContainer(cnt); setResource(uiXMLData.getName(), MAGIC_UI, data); } } @@ -982,7 +1042,7 @@ private void saveXMLFile(File xml, File resourcesDir) throws IOException { case 0xf5: // multiimage with SVG case 0xf7: - SVG s = (SVG)image.getSVGDocument(); + SvgBridge s = SvgBridge.of(image.getSVGDocument()); writeToFile(s.getSvgData(), new File(resourcesDir, normalizeFileName(resourceNames[iter]))); if(s.getBaseURL() != null && s.getBaseURL().length() > 0) { @@ -1414,8 +1474,7 @@ private void saveXMLFile(File xml, File resourcesDir) throws IOException { } case MAGIC_UI: { File uiXML = new File(resourcesDir, resourceNames[iter] + ".ui"); - UIBuilderOverride u = new UIBuilderOverride(); - com.codename1.ui.Container cnt = u.createContainer(this, resourceNames[iter]); + com.codename1.ui.Container cnt = materializeUIContainer(resourceNames[iter]); FileOutputStream fos = new FileOutputStream(uiXML); writeUIXml(cnt, fos); fos.close(); @@ -1463,14 +1522,14 @@ private void saveXMLFile(File xml, File resourcesDir) throws IOException { } } - private void writeUIXml(com.codename1.ui.Container cnt, FileOutputStream dest) throws IOException { - Writer w = new OutputStreamWriter(dest, "UTF-8"); - w.write("\n\n"); - - StringBuilder bld = new StringBuilder(); - UserInterfaceEditor.persistToXML(cnt, cnt, bld, this, ""); - w.write(bld.toString()); - w.flush(); + /** + * Writes a UI container as XML to the given stream. Overridden by the + * Designer editor subclass; the base class throws because the XML + * persister lives in the Designer module. + */ + protected void writeUIXml(com.codename1.ui.Container cnt, FileOutputStream dest) throws IOException { + throw new UnsupportedOperationException( + "UI container XML persistence requires EditableResourcesEditor"); } public void saveXML(File resFile) throws IOException { @@ -1511,7 +1570,7 @@ public void openFile(final InputStream input) throws IOException { undoQueue.clear(); redoQueue.clear(); } - ThemeEditor.resetThemeLoaded(); + onOpenFileComplete(); } /** @@ -1574,7 +1633,7 @@ private void updateModified() { overrideResource.updateModified(); return; } - if(ResourceEditorApp.IS_MAC) { + if(IS_MAC) { for(java.awt.Window w : java.awt.Frame.getWindows()) { if(w instanceof JFrame) { if(modified) { @@ -1933,13 +1992,14 @@ public String[] getDataResourceNames() { com.codename1.ui.Font createTrueTypeFont(com.codename1.ui.Font f, String fontName, String fileName, float fontSize, int sizeSetting) { // workaround for NPE in case of people doing stupid things like moving the res file. - if(ResourceEditorView.getLoadedFile() == null && !fileName.startsWith("native:")) { + File loadedFile = getLoadedFile(); + if(loadedFile == null && !fileName.startsWith("native:")) { return f; } if(fileName.startsWith("native:")) { - return new EditorTTFFont(fileName, sizeSetting, fontSize, f); + return new EditorTTFFont(fileName, sizeSetting, fontSize, f); } - File fontFile = new File(ResourceEditorView.getLoadedFile().getParentFile(), fileName); + File fontFile = new File(loadedFile.getParentFile(), fileName); if(fontFile.exists()) { return new EditorTTFFont(fontFile, sizeSetting, fontSize, f); } @@ -2594,7 +2654,7 @@ private void writeMotion(Motion m, DataOutputStream output) throws IOException { } private void saveSVG(DataOutputStream out, Image i, boolean isMultiImage) throws IOException { - SVG s = (SVG)i.getSVGDocument(); + SvgBridge s = SvgBridge.of(i.getSVGDocument()); out.writeInt(s.getSvgData().length); out.write(s.getSvgData()); if(s.getBaseURL() == null) { @@ -2634,7 +2694,7 @@ private com.codename1.ui.EncodedImage toEncodedImage(Image image) throws IOExcep } private MultiImage svgToMulti(Image image) throws IOException { - SVG s = (SVG)image.getSVGDocument(); + SvgBridge s = SvgBridge.of(image.getSVGDocument()); MultiImage mi = new MultiImage(); mi.dpi = s.getDpis(); if(mi.dpi == null || mi.dpi.length == 0) { @@ -2654,7 +2714,7 @@ private MultiImage svgToMulti(Image image) throws IOException { @Override com.codename1.ui.Image createSVG(boolean animated, byte[] data) throws IOException { com.codename1.ui.Image img = super.createSVG(animated, data); - SVG s = (SVG)img.getSVGDocument(); + SvgBridge s = SvgBridge.of(img.getSVGDocument()); if(s != null) { s.setDpis(dpisLoaded); s.setWidthForDPI(widthForDPI); @@ -2730,7 +2790,7 @@ void loadSVGRatios(DataInputStream input) throws IOException { Image createImage() throws IOException { Image i = super.createImage(); if(i.isSVG()) { - SVG s = (SVG)i.getSVGDocument(); + SvgBridge s = SvgBridge.of(i.getSVGDocument()); s.setRatioH(ratioH); s.setRatioW(ratioW); } @@ -2741,7 +2801,7 @@ Image createImage() throws IOException { Image createImage(DataInputStream input) throws IOException { Image i = super.createImage(input); if(i.isSVG()) { - SVG s = (SVG)i.getSVGDocument(); + SvgBridge s = SvgBridge.of(i.getSVGDocument()); s.setRatioH(ratioH); s.setRatioW(ratioW); } @@ -2835,7 +2895,7 @@ protected String performUndo() { } public void setSVGDPIs(final String name, final int[] dpi, final int[] widths, final int[] heights) { - final SVG sv = (SVG)getImage(name).getSVGDocument(); + final SvgBridge sv = SvgBridge.of(getImage(name).getSVGDocument()); final int[] currentDPIs = sv.getDpis(); final int[] currentWidths = sv.getWidthForDPI(); final int[] currentHeights = sv.getHeightForDPI(); @@ -3191,7 +3251,7 @@ protected String performUndo() { } public void refreshThemeMultiImages() { - EditableResources ed = (EditableResources)JavaSEPortWithSVGSupport.getNativeTheme(); + EditableResources ed = getRuntimeNativeTheme(); if(ed != null && ed != this) { ed.refreshThemeMultiImages(); } @@ -3344,58 +3404,11 @@ byte getResourceType(String name) { return super.getResourceType(name); } - public JComponent getResourceEditor(String name, ResourceEditorView view) { - byte magic = getResourceType(name); - switch(magic) { - case MAGIC_IMAGE: - case MAGIC_IMAGE_LEGACY: - Image i = getImage(name); - if(getResourceObject(name) instanceof MultiImage) { - ImageMultiEditor tl = new ImageMultiEditor(this, name, view); - tl.setImage((MultiImage)getResourceObject(name)); - return tl; - } - if(i instanceof Timeline) { - TimelineEditor tl = new TimelineEditor(this, name, view); - tl.setImage((Timeline)i); - return tl; - } - if(i.isSVG()) { - MultiImageSVGEditor img = new MultiImageSVGEditor(this, name); - img.setImage(i); - return img; - } - ImageRGBEditor img = new ImageRGBEditor(this, name, view); - img.setImage(i); - return img; - case MAGIC_TIMELINE: - TimelineEditor tl = new TimelineEditor(this, name, view); - tl.setImage((Timeline)getImage(name)); - return tl; - case MAGIC_THEME: - case MAGIC_THEME_LEGACY: - ThemeEditor theme = new ThemeEditor(this, name, getTheme(name), view); - return theme; - case MAGIC_FONT: - case MAGIC_FONT_LEGACY: - case MAGIC_INDEXED_FONT_LEGACY: - FontEditor fonts = new FontEditor(this, getFont(name), name); - return fonts; - case MAGIC_DATA: - DataEditor data = new DataEditor(this, name); - return data; - case MAGIC_UI: - UserInterfaceEditor uie = new UserInterfaceEditor(name, this, view.getProjectGeneratorSettings(), view); - return uie; - case MAGIC_L10N: - // we are cheating this isn't a theme but it should work since - // this is a hashtable that will include the nested locales - L10nEditor l10n = new L10nEditor(this, name); - return l10n; - default: - throw new IllegalArgumentException("Unrecognized magic number: " + Integer.toHexString(magic & 0xff)); - } - } + // getResourceEditor(String, ResourceEditorView) lives on EditableResourcesEditor, + // which extends this class inside the Designer module. Headless callers (e.g. + // the native-themes CSS build) never open a GUI editor, so keeping it out of + // the base class lets this module compile without the Designer GUI on its + // classpath. public static EditableResources open(InputStream resource) throws IOException { return new EditableResources(resource); @@ -3631,4 +3644,52 @@ public com.codename1.ui.EncodedImage getBest() { return getInternalImages()[bestFitOffset]; } } + + /** + * Reflective bridge to the javase-svg SVG class. Kept internal so that + * neither core nor the css-compiler module has to expose an SVG-facing + * public API, and so that neither module has to name the SVG class at + * compile time (it lives in javase-svg). The reflection paths are cold + * in the headless css-compiler run - they execute only when the resource + * being serialized actually contains SVG images. + */ + static final class SvgBridge { + private final Object svg; + + private SvgBridge(Object svg) { + this.svg = svg; + } + + static SvgBridge of(Object svg) { + return svg == null ? null : new SvgBridge(svg); + } + + byte[] getSvgData() { return (byte[]) call("getSvgData"); } + String getBaseURL() { return (String) call("getBaseURL"); } + float getRatioW() { return ((Number) call("getRatioW")).floatValue(); } + float getRatioH() { return ((Number) call("getRatioH")).floatValue(); } + int[] getDpis() { return (int[]) call("getDpis"); } + int[] getWidthForDPI() { return (int[]) call("getWidthForDPI"); } + int[] getHeightForDPI() { return (int[]) call("getHeightForDPI"); } + + void setRatioW(float v) { call("setRatioW", new Class[]{float.class}, v); } + void setRatioH(float v) { call("setRatioH", new Class[]{float.class}, v); } + void setDpis(int[] v) { call("setDpis", new Class[]{int[].class}, (Object) v); } + void setWidthForDPI(int[] v) { call("setWidthForDPI", new Class[]{int[].class}, (Object) v); } + void setHeightForDPI(int[] v) { call("setHeightForDPI", new Class[]{int[].class}, (Object) v); } + + private Object call(String method) { + return call(method, new Class[0]); + } + + private Object call(String method, Class[] ptypes, Object... args) { + try { + return svg.getClass().getMethod(method, ptypes).invoke(svg, args); + } catch (Exception e) { + throw new RuntimeException( + "SVG bridge failed to invoke " + method + " on " + + (svg == null ? "null" : svg.getClass().getName()), e); + } + } + } } diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/EditableResourcesForCSS.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/EditableResourcesForCSS.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/EditableResourcesForCSS.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/EditableResourcesForCSS.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/xml/Border.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/xml/Border.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/xml/Border.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/xml/Border.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/xml/Data.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/xml/Data.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/xml/Data.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/xml/Data.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/xml/Entry.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/xml/Entry.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/xml/Entry.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/xml/Entry.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/xml/Font.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/xml/Font.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/xml/Font.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/xml/Font.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/xml/Gradient.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/xml/Gradient.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/xml/Gradient.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/xml/Gradient.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/xml/Image.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/xml/Image.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/xml/Image.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/xml/Image.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/xml/L10n.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/xml/L10n.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/xml/L10n.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/xml/L10n.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/xml/Lang.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/xml/Lang.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/xml/Lang.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/xml/Lang.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/xml/LegacyFont.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/xml/LegacyFont.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/xml/LegacyFont.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/xml/LegacyFont.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/xml/ResourceFileXML.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/xml/ResourceFileXML.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/xml/ResourceFileXML.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/xml/ResourceFileXML.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/xml/SimpleXmlParser.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/xml/SimpleXmlParser.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/xml/SimpleXmlParser.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/xml/SimpleXmlParser.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/xml/Theme.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/xml/Theme.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/xml/Theme.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/xml/Theme.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/xml/Ui.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/xml/Ui.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/xml/Ui.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/xml/Ui.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/xml/Val.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/xml/Val.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/xml/Val.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/xml/Val.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/xml/comps/ArrayEntry.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/xml/comps/ArrayEntry.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/xml/comps/ArrayEntry.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/xml/comps/ArrayEntry.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/xml/comps/CommandEntry.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/xml/comps/CommandEntry.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/xml/comps/CommandEntry.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/xml/comps/CommandEntry.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/xml/comps/ComponentEntry.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/xml/comps/ComponentEntry.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/xml/comps/ComponentEntry.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/xml/comps/ComponentEntry.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/xml/comps/Custom.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/xml/comps/Custom.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/xml/comps/Custom.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/xml/comps/Custom.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/xml/comps/LayoutConstraint.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/xml/comps/LayoutConstraint.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/xml/comps/LayoutConstraint.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/xml/comps/LayoutConstraint.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/xml/comps/MapItems.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/xml/comps/MapItems.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/xml/comps/MapItems.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/xml/comps/MapItems.java diff --git a/CodenameOneDesigner/src/com/codename1/ui/util/xml/comps/StringEntry.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/xml/comps/StringEntry.java similarity index 100% rename from CodenameOneDesigner/src/com/codename1/ui/util/xml/comps/StringEntry.java rename to maven/css-compiler/src/main/java/com/codename1/ui/util/xml/comps/StringEntry.java diff --git a/maven/designer/pom.xml b/maven/designer/pom.xml index 0745ea4749..8b5e310b81 100644 --- a/maven/designer/pom.xml +++ b/maven/designer/pom.xml @@ -28,6 +28,10 @@ codenameone-javase-svg + + com.codenameone + codenameone-css-compiler + org.swinglabs diff --git a/maven/javase/pom.xml b/maven/javase/pom.xml index f63b923550..bf65ded8c7 100644 --- a/maven/javase/pom.xml +++ b/maven/javase/pom.xml @@ -142,6 +142,24 @@ + + + + + + + + + + + diff --git a/maven/pom.xml b/maven/pom.xml index 116039f3ee..104f8ecd52 100644 --- a/maven/pom.xml +++ b/maven/pom.xml @@ -60,6 +60,7 @@ java-runtime core factory + css-compiler sqlite-jdbc javase javase-svg @@ -93,6 +94,11 @@ codenameone-core ${project.version} + + com.codenameone + codenameone-css-compiler + ${project.version} + com.codenameone sqlite-jdbc diff --git a/native-themes/README.md b/native-themes/README.md new file mode 100644 index 0000000000..bcda4dd3e6 --- /dev/null +++ b/native-themes/README.md @@ -0,0 +1,72 @@ +# Native theme CSS sources + +This directory holds the Codename One platform native themes authored in CSS. +They are compiled by `scripts/build-native-themes.sh` (which invokes the thin +`maven/css-compiler` jar with `strictNoCef=true`) into `.res` files under the +repo's `Themes/` directory, alongside the legacy hand-authored themes. + +## Layout + +``` +native-themes/ + base/ shared tokens, @constants, @font-face (future) + ios-modern/theme.css iOS liquid-glass theme + android-material/theme.css Android Material 3 theme +``` + +Each `theme.css` is fed directly to the compiler. Until `@import` support is +confirmed in Flute/SAC, `theme.css` is a single self-contained file (no +`@import`). + +## Authoring rules + +Because these themes ship inside the port jars, rasterized image fallbacks +are forbidden. The compiler runs in `strictNoCef` mode: any rule that would +require CEF rasterization fails the build and lists the offending UIID. + +**Allowed:** + +- Solid `color` / `background-color`. +- `cn1-round-border`, `cn1-pill-border`, simple matched-side `border`. +- `padding`, `margin`, typography (`font-family`/`font-size`/`font-weight`). +- `cn1-derive`, `cn1-image-id` (resource images shipped as PNG), `cn1-mutable-image`. +- `cn1-source-dpi` for multi-DPI image variants. +- `.pressed`, `.selected`, `.unselected`, `.disabled` state selectors (dot-class + syntax — the CN1 CSS compiler translates these to the binding state of the + UIID, not CSS classes in the HTML sense). +- `@media (prefers-color-scheme: dark)` for dark palette overrides. +- `var(--x)` and `@constants { ... }`. + +**Forbidden (trigger CEF):** + +- `box-shadow`, `cn1-box-shadow-*` -> 9-piece fallback. +- `border-radius` combined with a visible border -> 9-piece fallback. +- Mixed border widths/styles/colors per side. +- `filter`. +- Complex `linear-gradient` / `radial-gradient` that can't be expressed as a + native gradient. + +If a visual effect isn't in the allowed list, extend the CSS compiler and/or +`.res` format with a new native primitive -- don't rasterize. + +## Mandatory constants + +Each theme must declare these in `#Constants`: + +- `includeNativeBool: false` -- native themes are the base; user themes set + this to `true` and inherit from us. If we set it to `true` ourselves we'd + try to inherit from ourselves and recurse at load time. +- `darkModeBool: true` -- enables UIManager's `$Dark` style resolution, + which is populated from the theme's `@media (prefers-color-scheme: dark)` + blocks. + +## Rebuilding + +``` +./scripts/build-native-themes.sh +``` + +Outputs: + +- `Themes/iOSModernTheme.res` +- `Themes/AndroidMaterialTheme.res` diff --git a/native-themes/android-material/theme.css b/native-themes/android-material/theme.css new file mode 100644 index 0000000000..eb91a79845 --- /dev/null +++ b/native-themes/android-material/theme.css @@ -0,0 +1,313 @@ +/* + * Android Material 3 native theme. + * + * CEF-free subset of the Codename One CSS compiler: solid fills, native + * rounded/pill borders, state selectors via .pressed / .selected / + * .disabled. Light and dark palettes live at the bottom of this file + * in a prefers-color-scheme block the compiler rewrites into $DarkUIID + * entries. Colors are inlined (not CSS variables) because the rewriter + * operates at string level and doesn't re-scope :root declarations. + * + * Material 3 baseline palette reference: + * primary #6750a4 dark #d0bcff + * on-primary #ffffff dark #381e72 + * primary-container #eaddff dark #4f378b + * on-primary-container #21005d dark #eaddff + * surface #fef7ff dark #141218 + * on-surface #1d1b20 dark #e6e0e9 + * surface-variant #e7e0ec dark #49454f + * on-surface-variant #49454f dark #cac4d0 + * surface-container #f3edf7 dark #211f26 + * outline #79747e dark #938f99 + * outline-variant #cac4d0 dark #49454f + * state-pressed #d0bcff dark #4f378b + * state-disabled #e0dce4 dark #2b2930 + * on-disabled #a5a0ab dark #5c5967 + * + * Overridable palette: user apps layer their own theme.css or runtime + * UIManager.addThemeProps on top of this theme - see + * PaletteOverrideThemeScreenshotTest for a working example. + */ + +#Constants { + includeNativeBool: false; + darkModeBool: true; + commandBehavior: Native; + paintsTitleBarBool: false; + tabsGridBool: true; + tabsFillRowsBool: false; + switchThumbPaddingInt: 2; + switchThumbScaleY: "1.5"; + switchTrackScaleY: "0.9"; + switchTrackScaleX: "3"; + switchTrackOffOutlineWidthMM: "0"; + switchTrackOnOutlineWidthMM: "0"; + switchTrackOffOutlineColor: "79747e"; + switchThumbInsetMM: "0"; +} + +Component { + color: #1d1b20; + background-color: #fef7ff; + padding: 0; + margin: 0; +} + +Form { background-color: #fef7ff; padding: 0; margin: 0; } +ContentPane { background-color: #fef7ff; padding: 0; margin: 0; } +Container { padding: 0; margin: 0; } + +Label { + color: #1d1b20; + padding: 1mm 2mm 1mm 2mm; + margin: 0; + font-family: "native:MainRegular"; +} + +SecondaryLabel { cn1-derive: Label; color: #49454f; } +TertiaryLabel { cn1-derive: Label; color: #79747e; } +SpanLabel { cn1-derive: Label; } +SpanLabelText { cn1-derive: Label; } + +Button { + color: #ffffff; + background-color: #6750a4; + padding: 2mm 4mm 2mm 4mm; + margin: 1mm; + font-family: "native:MainRegular"; + cn1-background-type: cn1-pill-border; +} +Button.pressed { background-color: #d0bcff; color: #21005d; } +Button.disabled { color: #a5a0ab; background-color: #e0dce4; } + +RaisedButton { cn1-derive: Button; } +RaisedButton.pressed { background-color: #d0bcff; } +RaisedButton.disabled { background-color: #e0dce4; color: #a5a0ab; } + +FlatButton { + cn1-derive: Button; + background-color: transparent; + color: #6750a4; + cn1-background-type: cn1-pill-border; +} +FlatButton.pressed { background-color: #eaddff; color: #21005d; } + +TextField { + color: #1d1b20; + background-color: #f3edf7; + padding: 2mm 3mm 2mm 3mm; + margin: 1mm 2mm 1mm 2mm; + font-family: "native:MainRegular"; + cn1-background-type: cn1-round-border; +} +TextField.pressed { background-color: #e7e0ec; } +TextField.disabled { color: #a5a0ab; background-color: #e0dce4; } + +TextArea { cn1-derive: TextField; padding: 2mm 3mm 2mm 3mm; } + +TextHint { + color: #79747e; + padding: 2mm 3mm 2mm 3mm; + font-family: "native:MainRegular"; +} + +CheckBox { + cn1-derive: Label; + color: #1d1b20; + background-color: transparent; + padding: 1mm 2mm 1mm 2mm; +} +CheckBox.selected { color: #6750a4; } +CheckBox.disabled { color: #a5a0ab; background-color: #e0dce4; } + +RadioButton { + cn1-derive: CheckBox; + color: #1d1b20; + background-color: transparent; + padding: 1mm 2mm 1mm 2mm; +} +RadioButton.selected { color: #6750a4; } +RadioButton.disabled { color: #a5a0ab; background-color: #e0dce4; } + +Switch { + color: #79747e; + background-color: #e7e0ec; + padding: 1mm; + margin: 1mm 2mm 1mm 2mm; +} +Switch.selected { color: #ffffff; background-color: #6750a4; } +Switch.disabled { color: #a5a0ab; background-color: #e0dce4; } + +OnOffSwitch { + cn1-derive: Label; + color: #ffffff; + background-color: #e7e0ec; + padding: 1mm 2mm 1mm 2mm; + cn1-background-type: cn1-pill-border; +} +OnOffSwitch.selected { background-color: #6750a4; color: #ffffff; } + +Toolbar { + background-color: #fef7ff; + color: #1d1b20; + padding: 1mm; + margin: 0; +} +TitleArea { cn1-derive: Toolbar; padding: 1mm 2mm 1mm 2mm; } + +Title { + color: #1d1b20; + padding: 1mm; + font-family: "native:MainBold"; +} +MainTitle { cn1-derive: Title; } + +BackCommand { cn1-derive: Button; background-color: transparent; color: #6750a4; padding: 1mm 2mm 1mm 2mm; } +TitleCommand { cn1-derive: Button; background-color: transparent; color: #6750a4; padding: 1mm 2mm 1mm 2mm; } + +Tabs { background-color: #fef7ff; padding: 0; margin: 0; } +TabsContainer { background-color: #fef7ff; padding: 0; margin: 0; } + +Tab { + color: #49454f; + background-color: #fef7ff; + padding: 2mm 3mm 2mm 3mm; + margin: 0; + font-family: "native:MainRegular"; + text-align: center; +} +Tab.selected { color: #6750a4; } +Tab.pressed { color: #21005d; background-color: #eaddff; } + +SelectedTab { cn1-derive: Tab; color: #6750a4; } +UnselectedTab { cn1-derive: Tab; color: #49454f; } + +SideNavigationPanel { background-color: #fef7ff; padding: 0; margin: 0; } + +SideCommand { + cn1-derive: Button; + background-color: transparent; + color: #1d1b20; + padding: 2mm 3mm 2mm 3mm; + margin: 0; +} +SideCommand.pressed { background-color: #eaddff; } + +List { background-color: #fef7ff; padding: 0; margin: 0; } + +ListRenderer { cn1-derive: Label; padding: 2mm 3mm 2mm 3mm; } + +MultiButton { + cn1-derive: Button; + background-color: #fef7ff; + color: #1d1b20; + padding: 2mm 3mm 2mm 3mm; + margin: 0; +} +MultiButton.pressed { background-color: #eaddff; } + +MultiLine1 { cn1-derive: Label; color: #1d1b20; font-family: "native:MainBold"; } +MultiLine2 { cn1-derive: Label; color: #49454f; } +MultiLine3 { cn1-derive: Label; color: #79747e; } +MultiLine4 { cn1-derive: Label; color: #79747e; } + +Dialog { + background-color: #f3edf7; + color: #1d1b20; + padding: 3mm; + margin: 0; + cn1-background-type: cn1-round-border; +} +DialogBody { cn1-derive: Dialog; padding: 2mm; } +DialogTitle { cn1-derive: Title; padding: 2mm; } +DialogContentPane { background-color: #f3edf7; padding: 2mm; margin: 0; } +DialogCommandArea { background-color: #f3edf7; padding: 1mm; } + +FloatingActionButton { + color: #21005d; + background-color: #eaddff; + padding: 3mm; + margin: 3mm; + font-family: "native:MainBold"; + cn1-background-type: cn1-pill-border; +} +FloatingActionButton.pressed { background-color: #d0bcff; } + +Separator { background-color: #cac4d0; padding: 0; margin: 0; } +PopupContent { cn1-derive: Dialog; padding: 2mm; } + +@media (prefers-color-scheme: dark) { + Component { color: #e6e0e9; background-color: #141218; } + Form { background-color: #141218; } + ContentPane { background-color: #141218; } + + Label { color: #e6e0e9; } + SecondaryLabel { color: #cac4d0; } + TertiaryLabel { color: #938f99; } + + Button { color: #381e72; background-color: #d0bcff; } + Button.pressed { background-color: #4f378b; color: #eaddff; } + Button.disabled { color: #5c5967; background-color: #2b2930; } + + RaisedButton { color: #381e72; background-color: #d0bcff; } + RaisedButton.pressed { background-color: #4f378b; } + RaisedButton.disabled { background-color: #2b2930; color: #5c5967; } + + FlatButton { color: #d0bcff; background-color: transparent; } + FlatButton.pressed { background-color: #4f378b; color: #eaddff; } + + TextField { color: #e6e0e9; background-color: #211f26; } + TextField.pressed { background-color: #49454f; } + TextField.disabled { color: #5c5967; background-color: #2b2930; } + TextHint { color: #938f99; } + + CheckBox { color: #e6e0e9; background-color: transparent; } + CheckBox.selected { color: #d0bcff; } + CheckBox.disabled { color: #5c5967; background-color: #2b2930; } + RadioButton { color: #e6e0e9; background-color: transparent; } + RadioButton.selected { color: #d0bcff; } + RadioButton.disabled { color: #5c5967; background-color: #2b2930; } + + Switch { color: #938f99; background-color: #49454f; } + Switch.selected { color: #381e72; background-color: #d0bcff; } + Switch.disabled { color: #5c5967; background-color: #2b2930; } + + OnOffSwitch { color: #381e72; background-color: #49454f; } + OnOffSwitch.selected { background-color: #d0bcff; color: #381e72; } + + Toolbar { background-color: #141218; color: #e6e0e9; } + Title { color: #e6e0e9; } + BackCommand { color: #d0bcff; background-color: transparent; } + TitleCommand { color: #d0bcff; background-color: transparent; } + + Tabs { background-color: #141218; } + TabsContainer { background-color: #141218; } + Tab { color: #cac4d0; background-color: #141218; } + Tab.selected { color: #d0bcff; } + Tab.pressed { color: #eaddff; background-color: #4f378b; } + SelectedTab { color: #d0bcff; } + UnselectedTab { color: #cac4d0; } + + SideNavigationPanel { background-color: #141218; } + SideCommand { color: #e6e0e9; background-color: transparent; } + SideCommand.pressed { background-color: #4f378b; } + + List { background-color: #141218; } + MultiButton { background-color: #141218; color: #e6e0e9; } + MultiButton.pressed { background-color: #4f378b; } + MultiLine1 { color: #e6e0e9; } + MultiLine2 { color: #cac4d0; } + MultiLine3 { color: #938f99; } + MultiLine4 { color: #938f99; } + + Dialog { background-color: #211f26; color: #e6e0e9; } + DialogBody { background-color: #211f26; } + DialogTitle { color: #e6e0e9; } + DialogContentPane { background-color: #211f26; } + DialogCommandArea { background-color: #211f26; } + + FloatingActionButton { color: #eaddff; background-color: #4f378b; } + FloatingActionButton.pressed { background-color: #d0bcff; } + + Separator { background-color: #49454f; } +} diff --git a/native-themes/ios-modern/theme.css b/native-themes/ios-modern/theme.css new file mode 100644 index 0000000000..8a88972f8c --- /dev/null +++ b/native-themes/ios-modern/theme.css @@ -0,0 +1,306 @@ +/* + * iOS modern (liquid-glass) native theme. + * + * CEF-free subset of the Codename One CSS compiler: solid fills, native + * rounded/pill borders, state selectors via .pressed / .selected / + * .disabled. Light and dark palettes live at the bottom of this file + * in a prefers-color-scheme block the compiler rewrites into $DarkUIID + * entries. Colors are inlined (not CSS variables) because the rewriter + * operates at string level and doesn't re-scope :root declarations. + * + * Apple system palette reference (light / dark): + * accent 007aff / 0a84ff + * accent-pressed 0064d1 / 64b1ff + * accent-disabled b3d4ff / 004a99 + * text-primary 000000 / ffffff + * text-secondary 3c3c43 / ebebf5 + * text-tertiary 8e8e93 / 8e8e93 + * text-disabled c7c7cc / 48484a + * surface ffffff / 000000 + * surface-grouped f2f2f7 / 1c1c1e + * surface-tertiary e5e5ea / 2c2c2e + * separator c6c6c8 / 38383a + * success 34c759 / 30d158 + * + * Overridable palette: user apps layer their own theme.css or runtime + * UIManager.addThemeProps on top of this theme - see + * PaletteOverrideThemeScreenshotTest for a working example that flips + * the accent to magenta at runtime. + */ + +#Constants { + includeNativeBool: false; + darkModeBool: true; + commandBehavior: Native; + ios7StatusBarHack: true; + paintsTitleBarBool: true; + tabsGridBool: true; + tabsFillRowsBool: false; + switchThumbPaddingInt: 2; + switchThumbScaleY: "1.4"; + switchTrackScaleY: "1.5"; + switchTrackScaleX: "2.5"; + switchTrackOffOutlineWidthMM: "0.25"; + switchTrackOnOutlineWidthMM: "0"; + switchTrackOffOutlineColor: "cccccc"; + switchThumbInsetMM: "0.25"; +} + +Component { + color: #000000; + background-color: #ffffff; + padding: 0; + margin: 0; +} + +Form { background-color: #f2f2f7; padding: 0; margin: 0; } +ContentPane { background-color: #f2f2f7; padding: 0; margin: 0; } +Container { padding: 0; margin: 0; } + +Label { + color: #000000; + padding: 1mm 2mm 1mm 2mm; + margin: 0; + font-family: "native:MainRegular"; +} + +SecondaryLabel { cn1-derive: Label; color: #3c3c43; } +TertiaryLabel { cn1-derive: Label; color: #8e8e93; } +SpanLabel { cn1-derive: Label; } +SpanLabelText { cn1-derive: Label; } + +Button { + color: #007aff; + padding: 2mm 3mm 2mm 3mm; + margin: 1mm; + font-family: "native:MainRegular"; + cn1-background-type: cn1-pill-border; +} +Button.pressed { color: #0064d1; background-color: #e5e5ea; } +Button.disabled { color: #b3d4ff; } + +RaisedButton { + cn1-derive: Button; + color: #ffffff; + background-color: #007aff; +} +RaisedButton.pressed { background-color: #0064d1; } +RaisedButton.disabled { background-color: #b3d4ff; color: #ffffff; } + +FlatButton { cn1-derive: Button; } + +TextField { + color: #000000; + background-color: #ffffff; + padding: 2mm 3mm 2mm 3mm; + margin: 1mm 2mm 1mm 2mm; + font-family: "native:MainRegular"; + cn1-background-type: cn1-round-border; +} +TextField.pressed { background-color: #e5e5ea; } +TextField.disabled { color: #c7c7cc; background-color: #e5e5ea; } + +TextArea { cn1-derive: TextField; padding: 2mm 3mm 2mm 3mm; } + +TextHint { + color: #8e8e93; + padding: 2mm 3mm 2mm 3mm; + font-family: "native:MainRegular"; +} + +CheckBox { + cn1-derive: Label; + color: #000000; + background-color: transparent; + padding: 1mm 2mm 1mm 2mm; +} +CheckBox.selected { color: #007aff; } +CheckBox.disabled { color: #c7c7cc; background-color: #e5e5ea; } + +RadioButton { + cn1-derive: CheckBox; + color: #000000; + background-color: transparent; + padding: 1mm 2mm 1mm 2mm; +} +RadioButton.selected { color: #007aff; } +RadioButton.disabled { color: #c7c7cc; background-color: #e5e5ea; } + +Switch { + color: #ffffff; + background-color: #ffffff; + padding: 1mm; + margin: 1mm 2mm 1mm 2mm; +} +Switch.selected { color: #ffffff; background-color: #34c759; } +Switch.disabled { color: #c7c7cc; background-color: #e5e5ea; } + +OnOffSwitch { + cn1-derive: Label; + color: #007aff; + background-color: #e5e5ea; + padding: 1mm 2mm 1mm 2mm; + cn1-background-type: cn1-pill-border; +} +OnOffSwitch.selected { background-color: #34c759; color: #ffffff; } + +Toolbar { + background-color: #ffffff; + color: #000000; + padding: 1mm; + margin: 0; +} +TitleArea { cn1-derive: Toolbar; padding: 1mm 2mm 1mm 2mm; } + +Title { + color: #000000; + padding: 1mm; + font-family: "native:MainBold"; +} +MainTitle { cn1-derive: Title; } + +BackCommand { cn1-derive: Button; color: #007aff; padding: 1mm 2mm 1mm 2mm; } +TitleCommand { cn1-derive: Button; color: #007aff; padding: 1mm 2mm 1mm 2mm; } + +Tabs { background-color: #ffffff; padding: 0; margin: 0; } +TabsContainer { background-color: #ffffff; padding: 0; margin: 0; } + +Tab { + color: #3c3c43; + background-color: #ffffff; + padding: 2mm 3mm 2mm 3mm; + margin: 0; + font-family: "native:MainRegular"; + text-align: center; +} +Tab.selected { color: #007aff; } +Tab.pressed { color: #0064d1; background-color: #e5e5ea; } + +SelectedTab { cn1-derive: Tab; color: #007aff; } +UnselectedTab { cn1-derive: Tab; color: #3c3c43; } + +SideNavigationPanel { background-color: #ffffff; padding: 0; margin: 0; } + +SideCommand { + cn1-derive: Button; + color: #000000; + padding: 2mm 3mm 2mm 3mm; + margin: 0; +} +SideCommand.pressed { background-color: #e5e5ea; } + +List { background-color: #ffffff; padding: 0; margin: 0; } + +ListRenderer { cn1-derive: Label; padding: 2mm 3mm 2mm 3mm; } + +MultiButton { + cn1-derive: Button; + background-color: #ffffff; + color: #000000; + padding: 2mm 3mm 2mm 3mm; + margin: 0; +} +MultiButton.pressed { background-color: #e5e5ea; } + +MultiLine1 { cn1-derive: Label; color: #000000; font-family: "native:MainBold"; } +MultiLine2 { cn1-derive: Label; color: #3c3c43; } +MultiLine3 { cn1-derive: Label; color: #8e8e93; } +MultiLine4 { cn1-derive: Label; color: #8e8e93; } + +Dialog { + background-color: #ffffff; + color: #000000; + padding: 3mm; + margin: 0; + cn1-background-type: cn1-round-border; +} +DialogBody { cn1-derive: Dialog; padding: 2mm; } +DialogTitle { cn1-derive: Title; padding: 2mm; } +DialogContentPane { background-color: #ffffff; padding: 2mm; margin: 0; } +DialogCommandArea { background-color: #ffffff; padding: 1mm; } + +FloatingActionButton { + color: #ffffff; + background-color: #007aff; + padding: 3mm; + margin: 3mm; + font-family: "native:MainBold"; + cn1-background-type: cn1-pill-border; +} +FloatingActionButton.pressed { background-color: #0064d1; } + +Separator { background-color: #c6c6c8; padding: 0; margin: 0; } +PopupContent { cn1-derive: Dialog; padding: 2mm; } + +@media (prefers-color-scheme: dark) { + Component { color: #ffffff; background-color: #000000; } + Form { background-color: #1c1c1e; } + ContentPane { background-color: #1c1c1e; } + + Label { color: #ffffff; } + SecondaryLabel { color: #ebebf5; } + TertiaryLabel { color: #8e8e93; } + + Button { color: #0a84ff; } + Button.pressed { color: #64b1ff; background-color: #3a3a3c; } + Button.disabled { color: #004a99; } + + RaisedButton { color: #ffffff; background-color: #0a84ff; } + RaisedButton.pressed { background-color: #64b1ff; } + RaisedButton.disabled { background-color: #004a99; } + + TextField { color: #ffffff; background-color: #1c1c1e; } + TextField.pressed { background-color: #2c2c2e; } + TextField.disabled { color: #48484a; background-color: #2c2c2e; } + TextHint { color: #8e8e93; } + + CheckBox { color: #ffffff; background-color: transparent; } + CheckBox.selected { color: #0a84ff; } + CheckBox.disabled { color: #48484a; background-color: #2c2c2e; } + RadioButton { color: #ffffff; background-color: transparent; } + RadioButton.selected { color: #0a84ff; } + RadioButton.disabled { color: #48484a; background-color: #2c2c2e; } + + Switch { color: #ffffff; background-color: #2c2c2e; } + Switch.selected { color: #ffffff; background-color: #30d158; } + Switch.disabled { color: #48484a; background-color: #2c2c2e; } + + OnOffSwitch { color: #0a84ff; background-color: #2c2c2e; } + OnOffSwitch.selected { background-color: #30d158; color: #ffffff; } + + Toolbar { background-color: #000000; color: #ffffff; } + Title { color: #ffffff; } + BackCommand { color: #0a84ff; } + TitleCommand { color: #0a84ff; } + + Tabs { background-color: #000000; } + TabsContainer { background-color: #000000; } + Tab { color: #ebebf5; background-color: #000000; } + Tab.selected { color: #0a84ff; } + Tab.pressed { color: #64b1ff; background-color: #3a3a3c; } + SelectedTab { color: #0a84ff; } + UnselectedTab { color: #ebebf5; } + + SideNavigationPanel { background-color: #000000; } + SideCommand { color: #ffffff; } + SideCommand.pressed { background-color: #3a3a3c; } + + List { background-color: #000000; } + MultiButton { background-color: #000000; color: #ffffff; } + MultiButton.pressed { background-color: #3a3a3c; } + MultiLine1 { color: #ffffff; } + MultiLine2 { color: #ebebf5; } + MultiLine3 { color: #8e8e93; } + MultiLine4 { color: #8e8e93; } + + Dialog { background-color: #1c1c1e; color: #ffffff; } + DialogBody { background-color: #1c1c1e; } + DialogTitle { color: #ffffff; } + DialogContentPane { background-color: #1c1c1e; } + DialogCommandArea { background-color: #1c1c1e; } + + FloatingActionButton { color: #ffffff; background-color: #0a84ff; } + FloatingActionButton.pressed { background-color: #64b1ff; } + + Separator { background-color: #38383a; } +} diff --git a/scripts/build-android-port.sh b/scripts/build-android-port.sh index fada3e3866..7a56e86c7c 100755 --- a/scripts/build-android-port.sh +++ b/scripts/build-android-port.sh @@ -131,4 +131,12 @@ if [ ! -f "$BUILD_CLIENT" ]; then fi fi +# Compile native CSS themes (AndroidMaterialTheme.res) and stage them in the +# Android port's src/ so the Maven build packages them into the port jar. The +# runtime falls back to android_holo_light.res if AndroidMaterialTheme.res is +# missing, which loses the Material 3 palette + all $DarkUIID entries. +./scripts/build-native-themes.sh +mkdir -p Ports/Android/src +cp Themes/AndroidMaterialTheme.res Ports/Android/src/AndroidMaterialTheme.res + run_maven -q -f maven/pom.xml -pl android -am -Dcn1.binaries="$CN1_BINARIES" -P !download-cn1-binaries -T 1C -Dmaven.javadoc.skip=true -Dmaven.source.skip=true -Djava.awt.headless=true clean install "$@" diff --git a/scripts/build-ios-port.sh b/scripts/build-ios-port.sh index 5fe8af9d97..0b75835ba9 100755 --- a/scripts/build-ios-port.sh +++ b/scripts/build-ios-port.sh @@ -37,4 +37,13 @@ if [ ! -f "$BUILD_CLIENT" ]; then fi fi +# Compile native CSS themes (iOSModernTheme.res) and copy into the iOS port's +# native sources so the Maven iOS build packages them into nativeios.jar. The +# iOS runtime falls back to iOS7Theme.res when iOSModernTheme.res is missing, +# which loses all $DarkUIID entries (dark mode appears broken) and the liquid- +# glass styling — so make sure this runs before the port is built. +./scripts/build-native-themes.sh +mkdir -p Ports/iOSPort/nativeSources +cp Themes/iOSModernTheme.res Ports/iOSPort/nativeSources/iOSModernTheme.res + "$MAVEN_HOME/bin/mvn" -q -f maven/pom.xml -pl ios -am -Djava.awt.headless=true clean install "$@" diff --git a/scripts/build-native-themes.sh b/scripts/build-native-themes.sh new file mode 100755 index 0000000000..68be044ee7 --- /dev/null +++ b/scripts/build-native-themes.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +### +# Compile the shipped platform native themes from CSS source. +# +# Uses the thin codenameone-css-compiler jar (no JavaFX / no CEF, depends only +# on codenameone-core + flute + sac) so it runs fast and fails loudly if any +# CSS rule would require CEF-backed rasterization (box-shadow, border-radius +# with visible border, filter, complex gradients). +# +# Source layout: +# native-themes/ +# ios-modern/theme.css +# android-material/theme.css +# (see native-themes/README for authoring rules) +# +# Outputs land in the existing Themes/ directory next to the hand-authored +# legacy themes, and are picked up by each port's build.xml the same way the +# legacy .res files are today. Outputs are gitignored (build artifacts). +### +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +cd "$REPO_ROOT" + +log() { echo "[build-native-themes] $1" >&2; } + +CSS_COMPILER_MODULE="$REPO_ROOT/maven/css-compiler" +CSS_SRC_ROOT="$REPO_ROOT/native-themes" +OUT_DIR="$REPO_ROOT/Themes" +# JavaScriptPort's runtime serves themes out of its webapp assets folder; +# mirror the generated .res files there too so the JS port picks them up. +JS_ASSETS_DIR="$REPO_ROOT/Ports/JavaScriptPort/src/main/webapp/assets" + +# Resolve the compiler jar. Prefer a freshly-built target/ jar (so CSS compiler +# source edits are always picked up); fall back to the installed copy in ~/.m2 +# when the module hasn't been rebuilt in this session. +locate_jar() { + local target_jar installed_jar version + target_jar="$(ls "$CSS_COMPILER_MODULE"/target/codenameone-css-compiler-*-jar-with-dependencies.jar 2>/dev/null | head -n1 || true)" + if [ -n "$target_jar" ] && [ -f "$target_jar" ]; then + echo "$target_jar" + return + fi + version="$(grep -m1 '' "$CSS_COMPILER_MODULE/pom.xml" | sed -E 's#.*([^<]+).*#\1#')" + if [ -z "$version" ]; then + # Fall back to parent pom version if the module inherits it. + version="$(grep -m1 '' "$REPO_ROOT/maven/pom.xml" | sed -E 's#.*([^<]+).*#\1#')" + fi + installed_jar="$HOME/.m2/repository/com/codenameone/codenameone-css-compiler/$version/codenameone-css-compiler-${version}-jar-with-dependencies.jar" + if [ -f "$installed_jar" ]; then + echo "$installed_jar" + return + fi + return 1 +} + +ensure_jar() { + local jar + if jar="$(locate_jar)"; then + log "Using CSS compiler jar: $jar" + echo "$jar" + return + fi + log "CSS compiler jar not found; building it via Maven." + local mvn="${MAVEN_HOME:+$MAVEN_HOME/bin/mvn}" + mvn="${mvn:-mvn}" + ( + cd "$REPO_ROOT/maven" + "$mvn" -pl css-compiler -am -q -DskipTests install + ) + if jar="$(locate_jar)"; then + echo "$jar" + return + fi + log "FAILED: CSS compiler jar could not be located after build." >&2 + exit 1 +} + +compile_theme() { + local jar="$1" name="$2" basename="$3" + local css="$CSS_SRC_ROOT/$name/theme.css" + local out="$OUT_DIR/$basename" + if [ ! -f "$css" ]; then + log "Skipping $name: no source at $css" + return + fi + mkdir -p "$OUT_DIR" + log "Compiling $name -> $out" + java -jar "$jar" -input "$css" -output "$out" + if [ -d "$JS_ASSETS_DIR" ]; then + cp "$out" "$JS_ASSETS_DIR/$basename" + log "Mirrored -> $JS_ASSETS_DIR/$basename" + fi +} + +main() { + local jar + jar="$(ensure_jar)" + compile_theme "$jar" ios-modern iOSModernTheme.res + compile_theme "$jar" android-material AndroidMaterialTheme.res + log "Native themes written to $OUT_DIR/" +} + +main "$@" diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/ButtonThemeScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/ButtonThemeScreenshotTest.java new file mode 100644 index 0000000000..1f756f1b9e --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/ButtonThemeScreenshotTest.java @@ -0,0 +1,56 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.ui.Button; +import com.codename1.ui.Form; +import com.codename1.ui.layouts.BoxLayout; +import com.codename1.ui.layouts.Layout; + +/** + * Theme-fidelity screenshot of Button / RaisedButton / FlatButton in + * default, pressed, and disabled states. Emits light + dark pair. + */ +public class ButtonThemeScreenshotTest extends DualAppearanceBaseTest { + @Override + protected String baseName() { + return "ButtonTheme"; + } + + @Override + protected Layout newLayout() { + return BoxLayout.y(); + } + + @Override + protected void populate(Form form, String suffix) { + form.add(stateRow(new Button("Default"))); + form.add(stateRow(pressed(new Button("Pressed")))); + form.add(stateRow(disabled(new Button("Disabled")))); + + Button raised = new Button("Raised"); + raised.setUIID("RaisedButton"); + form.add(raised); + + Button raisedPressed = new Button("Raised pressed"); + raisedPressed.setUIID("RaisedButton"); + form.add(pressed(raisedPressed)); + + Button flat = new Button("Flat"); + flat.setUIID("FlatButton"); + form.add(flat); + } + + private static Button stateRow(Button b) { + return b; + } + + private static Button pressed(Button b) { + // Toggle the pressed UIID state so the .pressed style is rendered. + b.pressed(); + return b; + } + + private static Button disabled(Button b) { + b.setEnabled(false); + return b; + } +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/CheckBoxRadioThemeScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/CheckBoxRadioThemeScreenshotTest.java new file mode 100644 index 0000000000..72f8991cff --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/CheckBoxRadioThemeScreenshotTest.java @@ -0,0 +1,41 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.ui.CheckBox; +import com.codename1.ui.Form; +import com.codename1.ui.Label; +import com.codename1.ui.RadioButton; +import com.codename1.ui.layouts.BoxLayout; +import com.codename1.ui.layouts.Layout; + +public class CheckBoxRadioThemeScreenshotTest extends DualAppearanceBaseTest { + @Override + protected String baseName() { + return "CheckBoxRadioTheme"; + } + + @Override + protected Layout newLayout() { + return BoxLayout.y(); + } + + @Override + protected void populate(Form form, String suffix) { + form.add(new Label("CheckBoxes")); + form.add(new CheckBox("Unselected")); + CheckBox selected = new CheckBox("Selected"); + selected.setSelected(true); + form.add(selected); + CheckBox disabled = new CheckBox("Disabled"); + disabled.setEnabled(false); + form.add(disabled); + + form.add(new Label("RadioButtons")); + form.add(new RadioButton("Unselected")); + RadioButton rSel = new RadioButton("Selected"); + rSel.setSelected(true); + form.add(rSel); + RadioButton rDis = new RadioButton("Disabled"); + rDis.setEnabled(false); + form.add(rDis); + } +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java index 2520a2a7eb..844711619f 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java @@ -82,6 +82,22 @@ public final class Cn1ssDeviceRunner extends DeviceRunner { new ValidatorLightweightPickerScreenshotTest(), new LightweightPickerButtonsScreenshotTest(), new ToastBarTopPositionScreenshotTest(), + // Native-theme fidelity tests (Phase 7): each emits a light+dark PNG pair + // so the iOS Modern and Android Material themes get exercised per UIID. + new ButtonThemeScreenshotTest(), + new TextFieldThemeScreenshotTest(), + new CheckBoxRadioThemeScreenshotTest(), + new SwitchThemeScreenshotTest(), + new PickerThemeScreenshotTest(), + new ToolbarThemeScreenshotTest(), + new TabsThemeScreenshotTest(), + new MultiButtonThemeScreenshotTest(), + new ListThemeScreenshotTest(), + new DialogThemeScreenshotTest(), + new FloatingActionButtonThemeScreenshotTest(), + new SpanLabelThemeScreenshotTest(), + new DarkLightShowcaseThemeScreenshotTest(), + new PaletteOverrideThemeScreenshotTest(), // Keep this as the last screenshot test; orientation changes can leak into subsequent screenshots. new OrientationLockScreenshotTest(), new InPlaceEditViewTest(), @@ -154,7 +170,27 @@ private boolean shouldForceTimeoutInHtml5(String testName) { || "CallDetectionAPITest".equals(testName) || "LocalNotificationOverrideTest".equals(testName) || "Base64NativePerformanceTest".equals(testName) - || "AccessibilityTest".equals(testName); + || "AccessibilityTest".equals(testName) + // The native-theme fidelity tests (each emits a light+dark PNG + // pair) matter for iOS/Android/JavaSE where the user actually + // looks at visual output. The JS port run has a tight 150s + // browser-lifetime budget that doesn't accommodate another + // 13 x 2 captures; skip them here. Re-enable selectively when + // we move the JS port to a longer-lived harness. + || "ButtonThemeScreenshotTest".equals(testName) + || "TextFieldThemeScreenshotTest".equals(testName) + || "CheckBoxRadioThemeScreenshotTest".equals(testName) + || "SwitchThemeScreenshotTest".equals(testName) + || "PickerThemeScreenshotTest".equals(testName) + || "ToolbarThemeScreenshotTest".equals(testName) + || "TabsThemeScreenshotTest".equals(testName) + || "MultiButtonThemeScreenshotTest".equals(testName) + || "ListThemeScreenshotTest".equals(testName) + || "DialogThemeScreenshotTest".equals(testName) + || "FloatingActionButtonThemeScreenshotTest".equals(testName) + || "SpanLabelThemeScreenshotTest".equals(testName) + || "DarkLightShowcaseThemeScreenshotTest".equals(testName) + || "PaletteOverrideThemeScreenshotTest".equals(testName); } private void awaitTestCompletion(int index, BaseTest testClass, String testName, long deadline) { diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/DarkLightShowcaseThemeScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/DarkLightShowcaseThemeScreenshotTest.java new file mode 100644 index 0000000000..64da2cf48a --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/DarkLightShowcaseThemeScreenshotTest.java @@ -0,0 +1,60 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.components.SpanLabel; +import com.codename1.ui.Button; +import com.codename1.ui.CheckBox; +import com.codename1.ui.Container; +import com.codename1.ui.Form; +import com.codename1.ui.Label; +import com.codename1.ui.RadioButton; +import com.codename1.ui.TextField; +import com.codename1.ui.layouts.BoxLayout; +import com.codename1.ui.layouts.Layout; + +/** + * Mixed-components showcase: one screen with Button / RaisedButton / + * TextField / CheckBox / RadioButton / SpanLabel stacked together, to + * catch regressions where the light and dark palettes diverge in contrast + * across a realistic form mix. + */ +public class DarkLightShowcaseThemeScreenshotTest extends DualAppearanceBaseTest { + @Override + protected String baseName() { + return "ShowcaseTheme"; + } + + @Override + protected Layout newLayout() { + return BoxLayout.y(); + } + + @Override + protected void populate(Form form, String suffix) { + form.add(new Label("Showcase " + suffix)); + + Container row = new Container(BoxLayout.x()); + row.add(new Button("Default")); + Button raised = new Button("Raised"); + raised.setUIID("RaisedButton"); + row.add(raised); + form.add(row); + + TextField tf = new TextField("hello@example.com"); + form.add(tf); + + Container toggles = new Container(BoxLayout.x()); + CheckBox cb = new CheckBox("Remember me"); + cb.setSelected(true); + toggles.add(cb); + RadioButton rb = new RadioButton("Agree"); + rb.setSelected(true); + toggles.add(rb); + form.add(toggles); + + SpanLabel body = new SpanLabel( + "Body copy using the theme's default SpanLabel styling. This " + + "should be clearly legible against the form background in " + + "both light and dark appearances."); + form.add(body); + } +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/DialogThemeScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/DialogThemeScreenshotTest.java new file mode 100644 index 0000000000..4e13068130 --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/DialogThemeScreenshotTest.java @@ -0,0 +1,57 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.components.SpanLabel; +import com.codename1.ui.Button; +import com.codename1.ui.Component; +import com.codename1.ui.Container; +import com.codename1.ui.Dialog; +import com.codename1.ui.Form; +import com.codename1.ui.Label; +import com.codename1.ui.layouts.BorderLayout; +import com.codename1.ui.layouts.BoxLayout; +import com.codename1.ui.layouts.FlowLayout; +import com.codename1.ui.layouts.Layout; +import com.codename1.ui.plaf.Style; + +/** + * Screenshot coverage for Dialog / DialogBody / DialogTitle / dialog command + * area. The dialog is rendered inline as a styled container (not as a modal + * show()) so the screenshot captures the dialog chrome reliably without + * waiting for modal animation to settle. + */ +public class DialogThemeScreenshotTest extends DualAppearanceBaseTest { + @Override + protected String baseName() { + return "DialogTheme"; + } + + @Override + protected Layout newLayout() { + return BoxLayout.y(); + } + + @Override + protected void populate(Form form, String suffix) { + Container dialog = new Container(new BorderLayout()); + dialog.setUIID("Dialog"); + + Container body = new Container(BoxLayout.y()); + body.setUIID("DialogBody"); + + Label title = new Label("Example dialog"); + title.setUIID("DialogTitle"); + body.add(title); + + SpanLabel message = new SpanLabel( + "Are you sure you want to continue with this action? " + + "This is a sample of a dialog body with a span label message."); + body.add(message); + + Container commands = new Container(new FlowLayout(Component.RIGHT)); + commands.setUIID("DialogCommandArea"); + commands.add(new Button("Cancel")).add(new Button("OK")); + + dialog.add(BorderLayout.CENTER, body).add(BorderLayout.SOUTH, commands); + form.add(dialog); + } +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/DualAppearanceBaseTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/DualAppearanceBaseTest.java new file mode 100644 index 0000000000..f89a08fa3f --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/DualAppearanceBaseTest.java @@ -0,0 +1,162 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.ui.Display; +import com.codename1.ui.Form; +import com.codename1.ui.Graphics; +import com.codename1.ui.Painter; +import com.codename1.ui.geom.Rectangle; +import com.codename1.ui.layouts.Layout; +import com.codename1.ui.plaf.UIManager; + +/** + * Base for theme-fidelity screenshot tests that emit a light + dark image + * pair. Subclasses implement {@link #populate(Form, String)} to add the + * component(s) to be captured; the helper takes care of toggling + * {@code Display.setDarkMode(...)}, refreshing the UIManager style cache + * so the next style lookups re-resolve against the new appearance, showing + * the form, waiting for onShowCompleted, and emitting the CN1SS chunk + * with the right filename suffix. + * + * A design-system gridline overlay is painted on top of every capture so + * reviewers can eyeball component sizing against a physical 4mm grid + * (rough 8pt-equivalent rhythm). Subclasses can opt out via + * {@link #gridOverlayEnabled()} when the overlay would obscure the signal + * the test is trying to establish. + */ +public abstract class DualAppearanceBaseTest extends BaseTest { + + /** + * Populate the given form with the component(s) to exercise. Called + * once per appearance (first light, then dark) on a fresh form. + * + * @param form fresh form with its Layout already set + * @param suffix "light" or "dark" - useful if populate() wants to + * surface the active appearance in a Label, for example. + */ + protected abstract void populate(Form form, String suffix); + + /** + * Subclasses override to provide the image-name prefix used for both + * captures. The emitted chunks will be named {@code _light} + * and {@code _dark}. + */ + protected abstract String baseName(); + + /** + * Subclasses override to provide the root layout. A fresh instance is + * requested for each appearance. + */ + protected abstract Layout newLayout(); + + /** + * Whether the 4mm design-system gridline overlay is painted above the + * form contents. Defaults to {@code true}. Override to {@code false} + * for tests where the grid would obscure the signal (e.g. a gradient + * rendering test). + */ + protected boolean gridOverlayEnabled() { + return true; + } + + @Override + public boolean runTest() { + runAppearance(false, "light", () -> runAppearance(true, "dark", this::finish)); + return true; + } + + private void runAppearance(boolean dark, final String suffix, final Runnable next) { + Display.getInstance().setDarkMode(dark); + // UIManager caches resolved Style objects per UIID; without this call + // the next lookup returns the Style that was resolved while the other + // appearance was active, and the screenshot comes out in the wrong + // appearance. UIManager.refreshTheme() clears the caches and re-runs + // the theme build pass against CN.isDarkMode()'s current value, so + // fresh components on the new Form pick up the correct $Dark + // entries (emitted by the native theme's + // @media (prefers-color-scheme: dark) block). + UIManager.getInstance().refreshTheme(); + + final String imageName = baseName() + "_" + suffix; + final boolean showGrid = gridOverlayEnabled(); + Form form = new Form(baseName() + " / " + suffix, newLayout()) { + @Override + protected void onShowCompleted() { + registerReadyCallback(this, () -> { + Cn1ssDeviceRunnerHelper.emitCurrentFormScreenshot(imageName); + next.run(); + }); + } + }; + if (showGrid) { + form.setGlassPane(new GridOverlayPainter(dark)); + } + populate(form, suffix); + form.show(); + } + + private void finish() { + // Restore platform-default dark mode so subsequent tests in the + // suite start from a clean slate, and refresh the theme once more + // so any follow-up test resolves styles against the restored state. + Display.getInstance().setDarkMode(null); + UIManager.getInstance().refreshTheme(); + done(); + } + + /** + * Low-contrast 4mm major / 1mm minor grid painted above the form. + * 4mm is roughly the 8pt Apple / 8dp Material design-system cell, + * scaled via Display.convertToPixels so it matches physical size + * on every DPI. + */ + private static final class GridOverlayPainter implements Painter { + private final boolean dark; + + GridOverlayPainter(boolean dark) { + this.dark = dark; + } + + @Override + public void paint(Graphics g, Rectangle rect) { + int minor = Display.getInstance().convertToPixels(1f); + int major = Display.getInstance().convertToPixels(4f); + if (minor < 1) { + minor = 1; + } + if (major < minor) { + major = minor * 4; + } + int x0 = rect.getX(); + int y0 = rect.getY(); + int x1 = x0 + rect.getWidth(); + int y1 = y0 + rect.getHeight(); + + int minorColor = dark ? 0x202020 : 0xe8e8e8; + int majorColor = dark ? 0x353535 : 0xc8c8c8; + + int prevColor = g.getColor(); + int prevAlpha = g.getAlpha(); + g.setAlpha(90); + + g.setColor(minorColor); + for (int x = x0; x <= x1; x += minor) { + g.drawLine(x, y0, x, y1); + } + for (int y = y0; y <= y1; y += minor) { + g.drawLine(x0, y, x1, y); + } + + g.setColor(majorColor); + g.setAlpha(150); + for (int x = x0; x <= x1; x += major) { + g.drawLine(x, y0, x, y1); + } + for (int y = y0; y <= y1; y += major) { + g.drawLine(x0, y, x1, y); + } + + g.setAlpha(prevAlpha); + g.setColor(prevColor); + } + } +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/FloatingActionButtonThemeScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/FloatingActionButtonThemeScreenshotTest.java new file mode 100644 index 0000000000..dc38003c50 --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/FloatingActionButtonThemeScreenshotTest.java @@ -0,0 +1,29 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.components.FloatingActionButton; +import com.codename1.ui.Container; +import com.codename1.ui.FontImage; +import com.codename1.ui.Form; +import com.codename1.ui.Label; +import com.codename1.ui.layouts.BorderLayout; +import com.codename1.ui.layouts.Layout; + +public class FloatingActionButtonThemeScreenshotTest extends DualAppearanceBaseTest { + @Override + protected String baseName() { + return "FloatingActionButtonTheme"; + } + + @Override + protected Layout newLayout() { + return new BorderLayout(); + } + + @Override + protected void populate(Form form, String suffix) { + Container content = new Container(new BorderLayout()); + content.add(BorderLayout.CENTER, new Label("Body content")); + FloatingActionButton fab = FloatingActionButton.createFAB(FontImage.MATERIAL_ADD); + form.add(BorderLayout.CENTER, fab.bindFabToContainer(content)); + } +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/ListThemeScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/ListThemeScreenshotTest.java new file mode 100644 index 0000000000..1b962161bc --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/ListThemeScreenshotTest.java @@ -0,0 +1,38 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.ui.Container; +import com.codename1.ui.Form; +import com.codename1.ui.List; +import com.codename1.ui.layouts.BorderLayout; +import com.codename1.ui.layouts.Layout; + +public class ListThemeScreenshotTest extends DualAppearanceBaseTest { + @Override + protected String baseName() { + return "ListTheme"; + } + + @Override + protected Layout newLayout() { + return new BorderLayout(); + } + + @Override + protected void populate(Form form, String suffix) { + List list = new List(new Object[]{ + "First item", + "Second item", + "Third item", + "Fourth item", + "Fifth item", + "Sixth item", + "Seventh item", + "Eighth item" + }); + list.setSelectedIndex(1); + + Container wrap = new Container(new BorderLayout()); + wrap.add(BorderLayout.CENTER, list); + form.add(BorderLayout.CENTER, wrap); + } +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/MultiButtonThemeScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/MultiButtonThemeScreenshotTest.java new file mode 100644 index 0000000000..9356f1a0bc --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/MultiButtonThemeScreenshotTest.java @@ -0,0 +1,42 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.components.MultiButton; +import com.codename1.ui.FontImage; +import com.codename1.ui.Form; +import com.codename1.ui.layouts.BoxLayout; +import com.codename1.ui.layouts.Layout; + +public class MultiButtonThemeScreenshotTest extends DualAppearanceBaseTest { + @Override + protected String baseName() { + return "MultiButtonTheme"; + } + + @Override + protected Layout newLayout() { + return BoxLayout.y(); + } + + @Override + protected void populate(Form form, String suffix) { + form.add(build("Title only", null, null, null, FontImage.MATERIAL_PERSON)); + form.add(build("First row", "Secondary line", null, null, FontImage.MATERIAL_EMAIL)); + form.add(build("Three lines", "Secondary line", "Tertiary line", null, FontImage.MATERIAL_PHONE)); + form.add(build("Four lines", "Secondary", "Tertiary", "Quaternary line", FontImage.MATERIAL_SCHEDULE)); + } + + private static MultiButton build(String l1, String l2, String l3, String l4, char icon) { + MultiButton b = new MultiButton(l1); + if (l2 != null) { + b.setTextLine2(l2); + } + if (l3 != null) { + b.setTextLine3(l3); + } + if (l4 != null) { + b.setTextLine4(l4); + } + FontImage.setMaterialIcon(b, icon); + return b; + } +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/PaletteOverrideThemeScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/PaletteOverrideThemeScreenshotTest.java new file mode 100644 index 0000000000..af0555a085 --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/PaletteOverrideThemeScreenshotTest.java @@ -0,0 +1,106 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.ui.Button; +import com.codename1.ui.Form; +import com.codename1.ui.Label; +import com.codename1.ui.layouts.BoxLayout; +import com.codename1.ui.layouts.Layout; +import com.codename1.ui.plaf.UIManager; + +import java.util.Hashtable; + +/** + * Verifies that a sub-theme can re-skin the native palette without + * touching the native theme's CSS source. + * + * The native CSS declares a palette (--cn1-accent, --cn1-primary etc.) + * that's inlined into each UIID at compile time. At runtime a user app + * overrides specific colors by layering an additional {@link Hashtable} + * of theme props on top of the installed native theme via + * {@link UIManager#addThemeProps}. This test installs a magenta + * override - vivid enough that a visual diff against the native + * baseline is unmistakable - and verifies both the light and dark + * captures pick it up. + * + * The override is installed once when the suite reaches this test; the + * light capture exercises it with the light base styles, the dark + * capture exercises it with the base styles picking up the + * {@code $Dark} variants merged under the same override layer. + * Because {@link Style#setBgColor} on an override key blows away the + * {@code $Dark} variant for that specific key, the dark capture also + * ends up showing the override color - proving the override reaches + * every appearance. + */ +public class PaletteOverrideThemeScreenshotTest extends DualAppearanceBaseTest { + + private static final String OVERRIDE_ACCENT = "ff2d95"; + private static final String OVERRIDE_ACCENT_PRESSED = "c71a75"; + private static final String OVERRIDE_ACCENT_TEXT = "ffffff"; + private boolean overrideInstalled; + + @Override + protected String baseName() { + return "PaletteOverrideTheme"; + } + + @Override + protected Layout newLayout() { + return BoxLayout.y(); + } + + @Override + protected void populate(Form form, String suffix) { + if (!overrideInstalled) { + installPaletteOverride(); + overrideInstalled = true; + } + + form.add(new Label("Primary / accent UIIDs")); + + Button primary = new Button("Raised"); + primary.setUIID("RaisedButton"); + form.add(primary); + + Button text = new Button("Text"); + form.add(text); + + form.add(new Label("Disabled state")); + Button disabled = new Button("Disabled"); + disabled.setUIID("RaisedButton"); + disabled.setEnabled(false); + form.add(disabled); + + Label footer = new Label("Magenta override active in both appearances"); + footer.setUIID("SecondaryLabel"); + form.add(footer); + } + + /** + * Adds a palette-override layer on top of the installed native + * theme. Uses {@link UIManager#addThemeProps} so the native theme + * stays resident underneath - the override table only has to + * redeclare the handful of keys it wants to change, plus the + * matching {@code $Dark} keys so the override applies in dark + * mode too. + */ + private void installPaletteOverride() { + Hashtable override = new Hashtable(); + override.put("RaisedButton.bgColor", OVERRIDE_ACCENT); + override.put("RaisedButton.fgColor", OVERRIDE_ACCENT_TEXT); + override.put("RaisedButton.press#bgColor", OVERRIDE_ACCENT_PRESSED); + override.put("RaisedButton.press#fgColor", OVERRIDE_ACCENT_TEXT); + override.put("Button.fgColor", OVERRIDE_ACCENT); + override.put("Button.press#fgColor", OVERRIDE_ACCENT_PRESSED); + // Dark override mirrors the light override so the magenta + // applies across both appearances. A real user theme would + // probably choose two variants; this test keeps them identical + // for easy visual confirmation. + override.put("$DarkRaisedButton.bgColor", OVERRIDE_ACCENT); + override.put("$DarkRaisedButton.fgColor", OVERRIDE_ACCENT_TEXT); + override.put("$DarkRaisedButton.press#bgColor", OVERRIDE_ACCENT_PRESSED); + override.put("$DarkRaisedButton.press#fgColor", OVERRIDE_ACCENT_TEXT); + override.put("$DarkButton.fgColor", OVERRIDE_ACCENT); + override.put("$DarkButton.press#fgColor", OVERRIDE_ACCENT_PRESSED); + UIManager.getInstance().addThemeProps(override); + } +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/PickerThemeScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/PickerThemeScreenshotTest.java new file mode 100644 index 0000000000..9da5e261ea --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/PickerThemeScreenshotTest.java @@ -0,0 +1,35 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.ui.Form; +import com.codename1.ui.Label; +import com.codename1.ui.spinner.Picker; +import com.codename1.ui.layouts.BoxLayout; +import com.codename1.ui.layouts.Layout; + +public class PickerThemeScreenshotTest extends DualAppearanceBaseTest { + @Override + protected String baseName() { + return "PickerTheme"; + } + + @Override + protected Layout newLayout() { + return BoxLayout.y(); + } + + @Override + protected void populate(Form form, String suffix) { + form.add(new Label("String picker")); + Picker stringPicker = new Picker(); + stringPicker.setStrings("Red", "Green", "Blue", "Yellow", "Purple"); + stringPicker.setSelectedString("Green"); + form.add(stringPicker); + + form.add(new Label("Disabled picker")); + Picker disabled = new Picker(); + disabled.setStrings("Option 1", "Option 2"); + disabled.setSelectedString("Option 1"); + disabled.setEnabled(false); + form.add(disabled); + } +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/SpanLabelThemeScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/SpanLabelThemeScreenshotTest.java new file mode 100644 index 0000000000..a1d8fb9a54 --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/SpanLabelThemeScreenshotTest.java @@ -0,0 +1,42 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.components.SpanLabel; +import com.codename1.ui.Form; +import com.codename1.ui.Label; +import com.codename1.ui.layouts.BoxLayout; +import com.codename1.ui.layouts.Layout; + +public class SpanLabelThemeScreenshotTest extends DualAppearanceBaseTest { + @Override + protected String baseName() { + return "SpanLabelTheme"; + } + + @Override + protected Layout newLayout() { + return BoxLayout.y(); + } + + @Override + protected void populate(Form form, String suffix) { + form.add(new Label("Single-line Label for reference")); + + SpanLabel shortSpan = new SpanLabel( + "Short SpanLabel text that fits on one or two lines depending " + + "on width."); + form.add(shortSpan); + + SpanLabel longSpan = new SpanLabel( + "Longer SpanLabel paragraph. SpanLabel wraps across lines " + + "using the current theme's font settings. This lets us " + + "verify that paragraph text spacing, line height, color, " + + "and contrast all render correctly in both light and dark " + + "appearances."); + form.add(longSpan); + + SpanLabel secondary = new SpanLabel( + "Secondary caption styled via the SecondaryLabel UIID."); + secondary.setUIID("SecondaryLabel"); + form.add(secondary); + } +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/SwitchThemeScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/SwitchThemeScreenshotTest.java new file mode 100644 index 0000000000..c9ff829587 --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/SwitchThemeScreenshotTest.java @@ -0,0 +1,38 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.components.Switch; +import com.codename1.ui.Form; +import com.codename1.ui.Label; +import com.codename1.ui.layouts.BoxLayout; +import com.codename1.ui.layouts.Layout; + +public class SwitchThemeScreenshotTest extends DualAppearanceBaseTest { + @Override + protected String baseName() { + return "SwitchTheme"; + } + + @Override + protected Layout newLayout() { + return BoxLayout.y(); + } + + @Override + protected void populate(Form form, String suffix) { + form.add(new Label("Switch off")); + Switch off = new Switch(); + off.setValue(false); + form.add(off); + + form.add(new Label("Switch on")); + Switch on = new Switch(); + on.setValue(true); + form.add(on); + + form.add(new Label("Disabled switch")); + Switch disabled = new Switch(); + disabled.setValue(true); + disabled.setEnabled(false); + form.add(disabled); + } +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/TabsThemeScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/TabsThemeScreenshotTest.java new file mode 100644 index 0000000000..1941bc4d1c --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/TabsThemeScreenshotTest.java @@ -0,0 +1,40 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.ui.Button; +import com.codename1.ui.Container; +import com.codename1.ui.FontImage; +import com.codename1.ui.Form; +import com.codename1.ui.Label; +import com.codename1.ui.Tabs; +import com.codename1.ui.layouts.BorderLayout; +import com.codename1.ui.layouts.Layout; + +public class TabsThemeScreenshotTest extends DualAppearanceBaseTest { + @Override + protected String baseName() { + return "TabsTheme"; + } + + @Override + protected Layout newLayout() { + return new BorderLayout(); + } + + @Override + protected void populate(Form form, String suffix) { + Tabs tabs = new Tabs(); + Container first = new Container(new BorderLayout()); + first.add(BorderLayout.CENTER, new Label("First tab content")); + Container second = new Container(new BorderLayout()); + second.add(BorderLayout.CENTER, new Button("Second tab button")); + Container third = new Container(new BorderLayout()); + third.add(BorderLayout.CENTER, new Label("Third tab content")); + + tabs.addTab("Home", FontImage.MATERIAL_HOME, 8, first); + tabs.addTab("Search", FontImage.MATERIAL_SEARCH, 8, second); + tabs.addTab("Info", FontImage.MATERIAL_INFO, 8, third); + tabs.setSelectedIndex(0); + + form.add(BorderLayout.CENTER, tabs); + } +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/TextFieldThemeScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/TextFieldThemeScreenshotTest.java new file mode 100644 index 0000000000..1c848ccc66 --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/TextFieldThemeScreenshotTest.java @@ -0,0 +1,41 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.ui.Form; +import com.codename1.ui.Label; +import com.codename1.ui.TextArea; +import com.codename1.ui.TextField; +import com.codename1.ui.layouts.BoxLayout; +import com.codename1.ui.layouts.Layout; + +public class TextFieldThemeScreenshotTest extends DualAppearanceBaseTest { + @Override + protected String baseName() { + return "TextFieldTheme"; + } + + @Override + protected Layout newLayout() { + return BoxLayout.y(); + } + + @Override + protected void populate(Form form, String suffix) { + TextField filled = new TextField("Hello theme"); + form.add(new Label("TextField")); + form.add(filled); + + TextField empty = new TextField(); + empty.setHint("Type here"); + form.add(new Label("TextField (hint)")); + form.add(empty); + + TextField disabled = new TextField("Disabled"); + disabled.setEnabled(false); + form.add(new Label("TextField disabled")); + form.add(disabled); + + TextArea area = new TextArea("Multi-line text\nacross several lines.", 3, 20); + form.add(new Label("TextArea")); + form.add(area); + } +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/ToolbarThemeScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/ToolbarThemeScreenshotTest.java new file mode 100644 index 0000000000..61b4657c1b --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/ToolbarThemeScreenshotTest.java @@ -0,0 +1,43 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.ui.Command; +import com.codename1.ui.FontImage; +import com.codename1.ui.Form; +import com.codename1.ui.Label; +import com.codename1.ui.Toolbar; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.layouts.BoxLayout; +import com.codename1.ui.layouts.Layout; + +public class ToolbarThemeScreenshotTest extends DualAppearanceBaseTest { + @Override + protected String baseName() { + return "ToolbarTheme"; + } + + @Override + protected Layout newLayout() { + return BoxLayout.y(); + } + + @Override + protected void populate(Form form, String suffix) { + Toolbar tb = form.getToolbar(); + if (tb == null) { + tb = new Toolbar(); + form.setToolbar(tb); + } + tb.setTitle("Theme Gallery"); + tb.addMaterialCommandToLeftBar("Menu", FontImage.MATERIAL_MENU, + (ActionEvent e) -> { /* no-op */ }); + tb.addMaterialCommandToRightBar("Search", FontImage.MATERIAL_SEARCH, + (ActionEvent e) -> { /* no-op */ }); + Command moreCmd = new Command("More") { + public void actionPerformed(ActionEvent evt) { + } + }; + tb.addCommandToOverflowMenu(moreCmd); + + form.add(new Label("Body content under the Toolbar.")); + } +}