From d015a501d751a3027f2223e00af2edf9471bc68d Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 10 Mar 2026 06:19:00 +0200 Subject: [PATCH 1/5] Add advanced theme CSS editing with live preview --- .../com/codename1/initializr/Initializr.java | 40 +++++++++++++++++-- .../initializr/model/GeneratorModel.java | 29 +++++++++----- .../initializr/model/ProjectOptions.java | 8 ++++ .../initializr/ui/TemplatePreviewPanel.java | 32 +++++++++++++++ .../model/GeneratorModelMatrixTest.java | 24 +++++++++++ 5 files changed, 119 insertions(+), 14 deletions(-) diff --git a/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java b/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java index 87133bcc8a..896a4b69fe 100644 --- a/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java @@ -23,6 +23,7 @@ import com.codename1.ui.Form; import com.codename1.ui.Label; import com.codename1.ui.RadioButton; +import com.codename1.ui.TextArea; import com.codename1.ui.TextField; import com.codename1.ui.layouts.BorderLayout; import com.codename1.ui.layouts.BoxLayout; @@ -51,6 +52,8 @@ public void runApp() { final boolean[] includeLocalizationBundles = new boolean[]{true}; final ProjectOptions.PreviewLanguage[] previewLanguage = new ProjectOptions.PreviewLanguage[]{ProjectOptions.PreviewLanguage.ENGLISH}; final ProjectOptions.JavaVersion[] javaVersion = new ProjectOptions.JavaVersion[]{ProjectOptions.JavaVersion.JAVA_8}; + final boolean[] advancedThemeMode = new boolean[]{false}; + final String[] customThemeCss = new String[]{""}; final RadioButton[] templateButtons = new RadioButton[Template.values().length]; final SpanLabel summaryLabel = new SpanLabel(); final TemplatePreviewPanel previewPanel = new TemplatePreviewPanel(selectedTemplate[0]); @@ -70,7 +73,8 @@ public void runApp() { public void run() { ProjectOptions options = new ProjectOptions( selectedThemeMode[0], selectedAccent[0], roundedButtons[0], - includeLocalizationBundles[0], previewLanguage[0], javaVersion[0] + includeLocalizationBundles[0], previewLanguage[0], javaVersion[0], + advancedThemeMode[0] ? customThemeCss[0] : null ); previewPanel.setTemplate(selectedTemplate[0]); previewPanel.setOptions(options); @@ -98,7 +102,7 @@ public void run() { createTemplateSelector(selectedTemplate, templateButtons, refresh) ); final Container idePanel = createIdeSelectorPanel(selectedIde, refresh); - final Container themePanel = createThemeOptionsPanel(selectedThemeMode, selectedAccent, roundedButtons, refresh); + final Container themePanel = createThemeOptionsPanel(selectedThemeMode, selectedAccent, roundedButtons, advancedThemeMode, customThemeCss, refresh); final Container localizationPanel = createLocalizationPanel(includeLocalizationBundles, previewLanguage, refresh, previewPanel); final Container javaPanel = createJavaOptionsPanel(javaVersion, refresh); themePanelRef[0] = themePanel; @@ -136,7 +140,8 @@ public void run() { String packageName = packageField.getText() == null ? "" : packageField.getText().trim(); ProjectOptions options = new ProjectOptions( selectedThemeMode[0], selectedAccent[0], roundedButtons[0], - includeLocalizationBundles[0], previewLanguage[0], javaVersion[0] + includeLocalizationBundles[0], previewLanguage[0], javaVersion[0], + advancedThemeMode[0] ? customThemeCss[0] : null ); GeneratorModel.create(selectedIde[0], selectedTemplate[0], appName, packageName, options).generate(); }); @@ -261,6 +266,8 @@ private Container createIdeSelectorPanel(IDE[] selectedIde, Runnable onSelection private Container createThemeOptionsPanel(ProjectOptions.ThemeMode[] selectedThemeMode, ProjectOptions.Accent[] selectedAccent, boolean[] roundedButtons, + boolean[] advancedThemeMode, + String[] customThemeCss, Runnable onSelectionChanged) { Container modeRow = new Container(new GridLayout(1, 2)); modeRow.setUIID("InitializrChoicesGrid"); @@ -310,10 +317,34 @@ private Container createThemeOptionsPanel(ProjectOptions.ThemeMode[] selectedThe onSelectionChanged.run(); }); + CheckBox advanced = new CheckBox("Advanced Theme Mode (Edit CSS)"); + advanced.setUIID("InitializrChoice"); + advanced.setSelected(advancedThemeMode[0]); + + TextArea cssEditor = new TextArea(customThemeCss[0], 8, 30); + cssEditor.setUIID("InitializrField"); + cssEditor.setHint("/* Optional CSS overrides generated into theme.css */\nButton {\n border-radius: 0;\n}"); + cssEditor.setGrowByContent(true); + cssEditor.setEnabled(advancedThemeMode[0]); + + advanced.addActionListener(e -> { + advancedThemeMode[0] = advanced.isSelected(); + cssEditor.setEnabled(advancedThemeMode[0]); + onSelectionChanged.run(); + }); + cssEditor.addDataChangedListener((type, index) -> { + customThemeCss[0] = cssEditor.getText(); + if (advancedThemeMode[0]) { + onSelectionChanged.run(); + } + }); + return BoxLayout.encloseY( labeledField("Mode", modeRow), labeledField("Accent", accentRow), - rounded + rounded, + advanced, + labeledField("Custom Theme CSS", cssEditor) ); } @@ -538,6 +569,7 @@ private String createSummary(String appName, String packageName, Template templa + "Localization Bundles: " + (options.includeLocalizationBundles ? "Yes" : "No") + "\n" + "Preview Language: " + options.previewLanguage.label + "\n" + "Java: " + options.javaVersion.label + "\n" + + "Advanced Theme CSS: " + (options.customThemeCss == null || options.customThemeCss.trim().length() == 0 ? "No" : "Yes") + "\n" + "Kotlin: " + (template.IS_KOTLIN ? "Yes" : "No"); } diff --git a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java index e0cf3fa54d..cf95411425 100644 --- a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java @@ -185,7 +185,7 @@ private byte[] applyDataReplacements(String targetPath, byte[] sourceData) throw content = injectLocalizationBootstrap(targetPath, content); } if (isBareTemplate() && "common/src/main/css/theme.css".equals(targetPath)) { - content += buildThemeOverrides(); + content += buildThemeCss(); } if ("common/pom.xml".equals(targetPath)) { content = applyJavaVersionToPom(content); @@ -366,13 +366,17 @@ private void appendIdeSection(StringBuilder out) { .append("Open the project folder in VS Code and make sure Java + Maven extensions are installed.\n\n"); } - private String buildThemeOverrides() { - if (isDefaultBarebonesOptions()) { + public static String buildThemeOverrides(ProjectOptions options) { + ProjectOptions effective = options == null ? ProjectOptions.defaults() : options; + if (effective.customThemeCss != null && effective.customThemeCss.trim().length() > 0) { + return "\n\n/* Initializr Advanced Theme Overrides */\n" + effective.customThemeCss + "\n"; + } + if (isDefaultBarebonesOptions(effective)) { return ""; } StringBuilder out = new StringBuilder("\n\n/* Initializr Theme Overrides */\n"); - if (options.themeMode == ProjectOptions.ThemeMode.DARK) { + if (effective.themeMode == ProjectOptions.ThemeMode.DARK) { out.append("Form {\n") .append(" background-color: #0f172a;\n") .append(" color: #e2e8f0;\n") @@ -388,7 +392,7 @@ private String buildThemeOverrides() { .append(" color: #e2e8f0;\n") .append("}\n"); - if (options.accent == ProjectOptions.Accent.DEFAULT) { + if (effective.accent == ProjectOptions.Accent.DEFAULT) { out.append("Button {\n") .append(" color: #e2e8f0;\n") .append(" background-color: #1f2937;\n") @@ -401,14 +405,14 @@ private String buildThemeOverrides() { .append("}\n"); return out.toString(); } - } else if (options.accent == ProjectOptions.Accent.DEFAULT) { + } else if (effective.accent == ProjectOptions.Accent.DEFAULT) { // Light + Clean intentionally inherits template defaults (rounded ignored). return ""; } - int accent = resolveAccentColor(); + int accent = resolveAccentColor(effective); int accentPressed = darkenColor(accent, 0.22f); - String buttonRadius = options.roundedButtons ? "3mm" : "0"; + String buttonRadius = effective.roundedButtons ? "3mm" : "0"; out.append("Button {\n") .append(" background-color: ").append(toCssColor(accent)).append(";\n") .append(" color: #ffffff;\n") @@ -424,12 +428,12 @@ private String buildThemeOverrides() { return out.toString(); } - private boolean isDefaultBarebonesOptions() { + private static boolean isDefaultBarebonesOptions(ProjectOptions options) { return options.themeMode == ProjectOptions.ThemeMode.LIGHT && options.accent == ProjectOptions.Accent.DEFAULT; } - private int resolveAccentColor() { + private static int resolveAccentColor(ProjectOptions options) { if (options.accent == ProjectOptions.Accent.DEFAULT) { return 0x0f766e; } @@ -442,6 +446,11 @@ private int resolveAccentColor() { return 0x0f766e; } + + private String buildThemeCss() { + return buildThemeOverrides(options); + } + private static String toCssColor(int color) { String hex = Integer.toHexString(color & 0xffffff); while (hex.length() < 6) { diff --git a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/ProjectOptions.java b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/ProjectOptions.java index d33361ec9d..a84602ab43 100644 --- a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/ProjectOptions.java +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/ProjectOptions.java @@ -65,16 +65,24 @@ public String toString() { public final boolean includeLocalizationBundles; public final PreviewLanguage previewLanguage; public final JavaVersion javaVersion; + public final String customThemeCss; public ProjectOptions(ThemeMode themeMode, Accent accent, boolean roundedButtons, boolean includeLocalizationBundles, PreviewLanguage previewLanguage, JavaVersion javaVersion) { + this(themeMode, accent, roundedButtons, includeLocalizationBundles, previewLanguage, javaVersion, null); + } + + public ProjectOptions(ThemeMode themeMode, Accent accent, boolean roundedButtons, + boolean includeLocalizationBundles, PreviewLanguage previewLanguage, + JavaVersion javaVersion, String customThemeCss) { this.themeMode = themeMode; this.accent = accent; this.roundedButtons = roundedButtons; this.includeLocalizationBundles = includeLocalizationBundles; this.previewLanguage = previewLanguage == null ? PreviewLanguage.ENGLISH : previewLanguage; this.javaVersion = javaVersion == null ? JavaVersion.JAVA_8 : javaVersion; + this.customThemeCss = customThemeCss; } public static ProjectOptions defaults() { diff --git a/scripts/initializr/common/src/main/java/com/codename1/initializr/ui/TemplatePreviewPanel.java b/scripts/initializr/common/src/main/java/com/codename1/initializr/ui/TemplatePreviewPanel.java index 74d83ce090..d407c2b5d3 100644 --- a/scripts/initializr/common/src/main/java/com/codename1/initializr/ui/TemplatePreviewPanel.java +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/ui/TemplatePreviewPanel.java @@ -6,6 +6,7 @@ import com.codename1.initializr.model.Template; import com.codename1.io.Log; import com.codename1.io.Properties; +import com.codename1.ui.css.CSSThemeCompiler; import com.codename1.ui.Button; import com.codename1.ui.Component; import com.codename1.ui.Container; @@ -17,6 +18,7 @@ import com.codename1.ui.layouts.BorderLayout; import com.codename1.ui.layouts.BoxLayout; import com.codename1.ui.plaf.UIManager; +import com.codename1.ui.util.MutableResource; import com.codename1.ui.util.Resources; import java.io.InputStream; @@ -85,6 +87,7 @@ private Form createBarebonesPreviewForm(ProjectOptions options) { form.getToolbar().addMaterialCommandToSideMenu("Hello Command", FontImage.MATERIAL_CHECK, 4, e -> Dialog.show("Hello Codename One", "Welcome to Codename One", "OK", null)); applyLivePreviewOptions(form, helloButton, null, options); + applyLiveCssOverrides(form, options); return form; } @@ -152,6 +155,35 @@ private Hashtable loadBundleProperties(String resourcePath) { } } + private void applyLiveCssOverrides(Form form, ProjectOptions options) { + restoreThemeDefaults(); + String generatedCss = com.codename1.initializr.model.GeneratorModel.buildThemeOverrides(options); + if (generatedCss == null || generatedCss.trim().length() == 0) { + return; + } + MutableResource resource = new MutableResource(); + CSSThemeCompiler compiler = new CSSThemeCompiler(); + compiler.compile(generatedCss, resource, "InitializrLiveTheme"); + Hashtable generatedTheme = resource.getTheme("InitializrLiveTheme"); + if (generatedTheme == null || generatedTheme.isEmpty()) { + return; + } + UIManager.getInstance().addThemeProps(generatedTheme); + form.refreshTheme(); + } + + private void restoreThemeDefaults() { + Resources resources = Resources.getGlobalResources(); + if (resources == null) { + return; + } + String[] names = resources.getThemeResourceNames(); + if (names == null || names.length == 0) { + return; + } + UIManager.getInstance().setThemeProps(resources.getTheme(names[0])); + } + private void updateMode() { previewHolder.removeAll(); if (template == Template.BAREBONES || template == Template.KOTLIN) { diff --git a/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java b/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java index fe4c56d105..6d90ad6828 100644 --- a/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java +++ b/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java @@ -22,10 +22,34 @@ public boolean runTest() throws Exception { } } validateExperimentalJava17Generation(); + validateAdvancedThemeCssGeneration(); return true; } + + private void validateAdvancedThemeCssGeneration() throws Exception { + String mainClassName = "DemoAdvancedTheme"; + String packageName = "com.acme.advanced.theme"; + String customCss = "Button {\n border-radius: 0;\n}\n"; + ProjectOptions options = new ProjectOptions( + ProjectOptions.ThemeMode.LIGHT, + ProjectOptions.Accent.BLUE, + true, + true, + ProjectOptions.PreviewLanguage.ENGLISH, + ProjectOptions.JavaVersion.JAVA_8, + customCss + ); + + byte[] zipData = createProjectZip(IDE.INTELLIJ, Template.BAREBONES, mainClassName, packageName, options); + Map entries = readZipEntries(zipData); + + String themeCss = getText(entries, "common/src/main/css/theme.css"); + assertContains(themeCss, "Initializr Advanced Theme Overrides", "Theme CSS should include advanced mode marker"); + assertContains(themeCss, "border-radius: 0", "Theme CSS should include custom advanced CSS"); + } + private void validateExperimentalJava17Generation() throws Exception { String mainClassName = "DemoExperimentalJava17"; String packageName = "com.acme.experimental.java17"; From 261fb2a6d7f44092596796d737987fd51904e18f Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 10 Mar 2026 19:15:53 +0200 Subject: [PATCH 2/5] Rename advanced theme mode to append custom CSS --- .../com/codename1/initializr/Initializr.java | 30 +++++-------------- .../initializr/model/GeneratorModel.java | 2 +- .../model/GeneratorModelMatrixTest.java | 6 ++-- 3 files changed, 11 insertions(+), 27 deletions(-) diff --git a/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java b/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java index 896a4b69fe..4d0763336b 100644 --- a/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java @@ -52,7 +52,6 @@ public void runApp() { final boolean[] includeLocalizationBundles = new boolean[]{true}; final ProjectOptions.PreviewLanguage[] previewLanguage = new ProjectOptions.PreviewLanguage[]{ProjectOptions.PreviewLanguage.ENGLISH}; final ProjectOptions.JavaVersion[] javaVersion = new ProjectOptions.JavaVersion[]{ProjectOptions.JavaVersion.JAVA_8}; - final boolean[] advancedThemeMode = new boolean[]{false}; final String[] customThemeCss = new String[]{""}; final RadioButton[] templateButtons = new RadioButton[Template.values().length]; final SpanLabel summaryLabel = new SpanLabel(); @@ -74,7 +73,7 @@ public void run() { ProjectOptions options = new ProjectOptions( selectedThemeMode[0], selectedAccent[0], roundedButtons[0], includeLocalizationBundles[0], previewLanguage[0], javaVersion[0], - advancedThemeMode[0] ? customThemeCss[0] : null + customThemeCss[0] ); previewPanel.setTemplate(selectedTemplate[0]); previewPanel.setOptions(options); @@ -102,7 +101,7 @@ public void run() { createTemplateSelector(selectedTemplate, templateButtons, refresh) ); final Container idePanel = createIdeSelectorPanel(selectedIde, refresh); - final Container themePanel = createThemeOptionsPanel(selectedThemeMode, selectedAccent, roundedButtons, advancedThemeMode, customThemeCss, refresh); + final Container themePanel = createThemeOptionsPanel(selectedThemeMode, selectedAccent, roundedButtons, customThemeCss, refresh); final Container localizationPanel = createLocalizationPanel(includeLocalizationBundles, previewLanguage, refresh, previewPanel); final Container javaPanel = createJavaOptionsPanel(javaVersion, refresh); themePanelRef[0] = themePanel; @@ -141,7 +140,7 @@ public void run() { ProjectOptions options = new ProjectOptions( selectedThemeMode[0], selectedAccent[0], roundedButtons[0], includeLocalizationBundles[0], previewLanguage[0], javaVersion[0], - advancedThemeMode[0] ? customThemeCss[0] : null + customThemeCss[0] ); GeneratorModel.create(selectedIde[0], selectedTemplate[0], appName, packageName, options).generate(); }); @@ -266,7 +265,6 @@ private Container createIdeSelectorPanel(IDE[] selectedIde, Runnable onSelection private Container createThemeOptionsPanel(ProjectOptions.ThemeMode[] selectedThemeMode, ProjectOptions.Accent[] selectedAccent, boolean[] roundedButtons, - boolean[] advancedThemeMode, String[] customThemeCss, Runnable onSelectionChanged) { Container modeRow = new Container(new GridLayout(1, 2)); @@ -317,34 +315,20 @@ private Container createThemeOptionsPanel(ProjectOptions.ThemeMode[] selectedThe onSelectionChanged.run(); }); - CheckBox advanced = new CheckBox("Advanced Theme Mode (Edit CSS)"); - advanced.setUIID("InitializrChoice"); - advanced.setSelected(advancedThemeMode[0]); - TextArea cssEditor = new TextArea(customThemeCss[0], 8, 30); cssEditor.setUIID("InitializrField"); - cssEditor.setHint("/* Optional CSS overrides generated into theme.css */\nButton {\n border-radius: 0;\n}"); + cssEditor.setHint("/* Appended to generated theme.css */\nButton {\n border-radius: 0;\n}"); cssEditor.setGrowByContent(true); - cssEditor.setEnabled(advancedThemeMode[0]); - - advanced.addActionListener(e -> { - advancedThemeMode[0] = advanced.isSelected(); - cssEditor.setEnabled(advancedThemeMode[0]); - onSelectionChanged.run(); - }); cssEditor.addDataChangedListener((type, index) -> { customThemeCss[0] = cssEditor.getText(); - if (advancedThemeMode[0]) { - onSelectionChanged.run(); - } + onSelectionChanged.run(); }); return BoxLayout.encloseY( labeledField("Mode", modeRow), labeledField("Accent", accentRow), rounded, - advanced, - labeledField("Custom Theme CSS", cssEditor) + labeledField("Append Custom CSS", cssEditor) ); } @@ -569,7 +553,7 @@ private String createSummary(String appName, String packageName, Template templa + "Localization Bundles: " + (options.includeLocalizationBundles ? "Yes" : "No") + "\n" + "Preview Language: " + options.previewLanguage.label + "\n" + "Java: " + options.javaVersion.label + "\n" - + "Advanced Theme CSS: " + (options.customThemeCss == null || options.customThemeCss.trim().length() == 0 ? "No" : "Yes") + "\n" + + "Append Custom CSS: " + (options.customThemeCss == null || options.customThemeCss.trim().length() == 0 ? "No" : "Yes") + "\n" + "Kotlin: " + (template.IS_KOTLIN ? "Yes" : "No"); } diff --git a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java index cf95411425..d9d722597e 100644 --- a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java @@ -369,7 +369,7 @@ private void appendIdeSection(StringBuilder out) { public static String buildThemeOverrides(ProjectOptions options) { ProjectOptions effective = options == null ? ProjectOptions.defaults() : options; if (effective.customThemeCss != null && effective.customThemeCss.trim().length() > 0) { - return "\n\n/* Initializr Advanced Theme Overrides */\n" + effective.customThemeCss + "\n"; + return "\n\n/* Initializr Appended Custom CSS */\n" + effective.customThemeCss + "\n"; } if (isDefaultBarebonesOptions(effective)) { return ""; diff --git a/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java b/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java index 6d90ad6828..e10143d464 100644 --- a/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java +++ b/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java @@ -22,13 +22,13 @@ public boolean runTest() throws Exception { } } validateExperimentalJava17Generation(); - validateAdvancedThemeCssGeneration(); + validateAppendedCustomCssGeneration(); return true; } - private void validateAdvancedThemeCssGeneration() throws Exception { + private void validateAppendedCustomCssGeneration() throws Exception { String mainClassName = "DemoAdvancedTheme"; String packageName = "com.acme.advanced.theme"; String customCss = "Button {\n border-radius: 0;\n}\n"; @@ -46,7 +46,7 @@ private void validateAdvancedThemeCssGeneration() throws Exception { Map entries = readZipEntries(zipData); String themeCss = getText(entries, "common/src/main/css/theme.css"); - assertContains(themeCss, "Initializr Advanced Theme Overrides", "Theme CSS should include advanced mode marker"); + assertContains(themeCss, "Initializr Appended Custom CSS", "Theme CSS should include appended custom CSS marker"); assertContains(themeCss, "border-radius: 0", "Theme CSS should include custom advanced CSS"); } From 18e9178326eb58ffc853ff8b09ec9fb9f263a463 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 10 Mar 2026 20:16:49 +0200 Subject: [PATCH 3/5] Support CSS named colors and text-align in theme compiler --- .../codename1/ui/css/CSSThemeCompiler.java | 103 +++++++++++++++++- .../ui/css/CSSThemeCompilerTest.java | 7 +- 2 files changed, 105 insertions(+), 5 deletions(-) diff --git a/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java b/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java index 7a5065bd83..395ebd7061 100644 --- a/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java +++ b/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java @@ -3,6 +3,7 @@ */ package com.codename1.ui.css; +import com.codename1.ui.Component; import com.codename1.ui.EncodedImage; import com.codename1.ui.Image; import com.codename1.ui.plaf.CSSBorder; @@ -166,6 +167,13 @@ private boolean applySimpleThemeProperty(Hashtable theme, String uiid, String st theme.put(uiid + "." + statePrefix + "font", value); return true; } + if ("text-align".equals(property)) { + String align = normalizeAlignment(value); + if (align != null) { + theme.put(uiid + "." + statePrefix + "align", align); + } + return true; + } return false; } @@ -267,10 +275,26 @@ private boolean isBorderProperty(String property) { } private String normalizeHexColor(String cssColor) { - String value = cssColor.trim(); - if ("transparent".equalsIgnoreCase(value)) { + String value = cssColor.trim().toLowerCase(); + if ("transparent".equals(value)) { return "000000"; } + + if (value.startsWith("rgb(") && value.endsWith(")")) { + String[] parts = splitOnComma(value.substring(4, value.length() - 1)); + if (parts.length == 3) { + int r = clampColor(parts[0]); + int g = clampColor(parts[1]); + int b = clampColor(parts[2]); + return toHexColor((r << 16) | (g << 8) | b); + } + } + + String keyword = cssColorKeyword(value); + if (keyword != null) { + return keyword; + } + if (value.startsWith("#")) { value = value.substring(1); } @@ -279,7 +303,61 @@ private String normalizeHexColor(String cssColor) { + value.charAt(1) + value.charAt(1) + value.charAt(2) + value.charAt(2); } - return value.toLowerCase(); + if (value.length() != 6) { + return "000000"; + } + return value; + } + + private String normalizeAlignment(String value) { + String v = value == null ? "" : value.trim().toLowerCase(); + if ("left".equals(v) || "start".equals(v)) { + return String.valueOf(Component.LEFT); + } + if ("center".equals(v)) { + return String.valueOf(Component.CENTER); + } + if ("right".equals(v) || "end".equals(v)) { + return String.valueOf(Component.RIGHT); + } + return null; + } + + private String cssColorKeyword(String value) { + if ("black".equals(value)) return "000000"; + if ("white".equals(value)) return "ffffff"; + if ("red".equals(value)) return "ff0000"; + if ("green".equals(value)) return "008000"; + if ("blue".equals(value)) return "0000ff"; + if ("pink".equals(value)) return "ffc0cb"; + if ("orange".equals(value)) return "ffa500"; + if ("yellow".equals(value)) return "ffff00"; + if ("purple".equals(value)) return "800080"; + if ("gray".equals(value) || "grey".equals(value)) return "808080"; + return null; + } + + private int clampColor(String value) { + try { + int out = Integer.parseInt(value.trim()); + if (out < 0) { + return 0; + } + if (out > 255) { + return 255; + } + return out; + } catch (RuntimeException err) { + return 0; + } + } + + private String toHexColor(int color) { + String hex = Integer.toHexString(color & 0xffffff); + while (hex.length() < 6) { + hex = "0" + hex; + } + return hex; } private String normalizeBox(String cssValue) { @@ -398,6 +476,25 @@ private String[] splitOnChar(String input, char delimiter) { return out.toArray(new String[out.size()]); } + private String[] splitOnComma(String input) { + ArrayList parts = new ArrayList(); + int start = 0; + for (int i = 0; i < input.length(); i++) { + if (input.charAt(i) == ',') { + String token = input.substring(start, i).trim(); + if (token.length() > 0) { + parts.add(token); + } + start = i + 1; + } + } + String tail = input.substring(start).trim(); + if (tail.length() > 0) { + parts.add(tail); + } + return parts.toArray(new String[parts.size()]); + } + private String[] splitOnWhitespace(String input) { ArrayList out = new ArrayList(); StringBuilder token = new StringBuilder(); diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/css/CSSThemeCompilerTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/css/CSSThemeCompilerTest.java index b49b05cd94..7db4b98ecc 100644 --- a/maven/core-unittests/src/test/java/com/codename1/ui/css/CSSThemeCompilerTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/ui/css/CSSThemeCompilerTest.java @@ -1,6 +1,7 @@ package com.codename1.ui.css; import com.codename1.junit.UITestBase; +import com.codename1.ui.Component; import com.codename1.ui.Image; import com.codename1.ui.plaf.CSSBorder; import com.codename1.ui.util.MutableResource; @@ -23,13 +24,14 @@ public void testCompilesThemeConstantsDeriveAndMutableImages() { + "@constants{spacing: 4px; primaryColor: var(--primary);}" + "Button{color:var(--primary);background-color:#112233;padding:1px 2px;cn1-derive:Label;}" + "Button:pressed{border-width:2px;border-style:solid;border-color:#ffffff;cn1-mutable-image:btnBg #ff00ff;}" - + "Label{margin:2px 4px 6px 8px;}", + + "Label{margin:2px 4px 6px 8px;}" + + "Button{color:pink;text-align:center;}", resource, "Theme" ); Hashtable theme = resource.getTheme("Theme"); - assertEquals("aabbcc", theme.get("Button.fgColor")); + assertEquals("ffc0cb", theme.get("Button.fgColor")); assertEquals("112233", theme.get("Button.bgColor")); assertEquals("255", theme.get("Button.transparency")); assertEquals("1,2,1,2", theme.get("Button.padding")); @@ -38,6 +40,7 @@ public void testCompilesThemeConstantsDeriveAndMutableImages() { assertEquals("#abc", theme.get("@primary")); assertEquals("4px", theme.get("@spacing")); assertEquals("#abc", theme.get("@primarycolor")); + assertEquals(String.valueOf(Component.CENTER), String.valueOf(theme.get("Button.align"))); assertTrue(theme.get("Button.press#border") instanceof CSSBorder); Image mutable = resource.getImage("btnBg"); From 3e5b23e2a2525d614541df1e1d016a0bdf90837d Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 10 Mar 2026 20:45:22 +0200 Subject: [PATCH 4/5] Fix PMD control statement braces in CSS color keyword mapping --- .../codename1/ui/css/CSSThemeCompiler.java | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java b/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java index 395ebd7061..b7bac9eb90 100644 --- a/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java +++ b/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java @@ -324,16 +324,36 @@ private String normalizeAlignment(String value) { } private String cssColorKeyword(String value) { - if ("black".equals(value)) return "000000"; - if ("white".equals(value)) return "ffffff"; - if ("red".equals(value)) return "ff0000"; - if ("green".equals(value)) return "008000"; - if ("blue".equals(value)) return "0000ff"; - if ("pink".equals(value)) return "ffc0cb"; - if ("orange".equals(value)) return "ffa500"; - if ("yellow".equals(value)) return "ffff00"; - if ("purple".equals(value)) return "800080"; - if ("gray".equals(value) || "grey".equals(value)) return "808080"; + if ("black".equals(value)) { + return "000000"; + } + if ("white".equals(value)) { + return "ffffff"; + } + if ("red".equals(value)) { + return "ff0000"; + } + if ("green".equals(value)) { + return "008000"; + } + if ("blue".equals(value)) { + return "0000ff"; + } + if ("pink".equals(value)) { + return "ffc0cb"; + } + if ("orange".equals(value)) { + return "ffa500"; + } + if ("yellow".equals(value)) { + return "ffff00"; + } + if ("purple".equals(value)) { + return "800080"; + } + if ("gray".equals(value) || "grey".equals(value)) { + return "808080"; + } return null; } From 5dd1f702ac30376534be915737e2800981cbc835 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 10 Mar 2026 21:25:56 +0200 Subject: [PATCH 5/5] Add strict CSS syntax validation and surface errors in Initializr UI --- .../codename1/ui/css/CSSThemeCompiler.java | 108 +++++++++++++----- .../ui/css/CSSThemeCompilerTest.java | 14 +++ .../com/codename1/initializr/Initializr.java | 31 ++++- .../initializr/ui/TemplatePreviewPanel.java | 22 ++-- 4 files changed, 130 insertions(+), 45 deletions(-) diff --git a/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java b/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java index b7bac9eb90..1e02408714 100644 --- a/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java +++ b/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java @@ -41,6 +41,12 @@ /// - `var(--name)` dereferencing in declaration values. public class CSSThemeCompiler { + public static class CSSSyntaxException extends IllegalArgumentException { + public CSSSyntaxException(String message) { + super(message); + } + } + public void compile(String css, MutableResource resources, String themeName) { Hashtable theme = resources.getTheme(themeName); if (theme == null) { @@ -82,7 +88,7 @@ private void compileConstants(String css, Hashtable theme) { } int close = stripped.indexOf('}', open + 1); if (close <= open) { - return; + throw new CSSSyntaxException("Unterminated @constants block"); } Declaration[] declarations = parseDeclarations(stripped.substring(open + 1, close)); for (Declaration declaration : declarations) { @@ -169,9 +175,7 @@ private boolean applySimpleThemeProperty(Hashtable theme, String uiid, String st } if ("text-align".equals(property)) { String align = normalizeAlignment(value); - if (align != null) { - theme.put(uiid + "." + statePrefix + "align", align); - } + theme.put(uiid + "." + statePrefix + "align", align); return true; } return false; @@ -275,19 +279,26 @@ private boolean isBorderProperty(String property) { } private String normalizeHexColor(String cssColor) { - String value = cssColor.trim().toLowerCase(); + String value = cssColor == null ? "" : cssColor.trim().toLowerCase(); + if (value.length() == 0) { + throw new CSSSyntaxException("Color value cannot be empty"); + } if ("transparent".equals(value)) { return "000000"; } - if (value.startsWith("rgb(") && value.endsWith(")")) { + if (value.startsWith("rgb(")) { + if (!value.endsWith(")")) { + throw new CSSSyntaxException("Malformed rgb() color: " + cssColor); + } String[] parts = splitOnComma(value.substring(4, value.length() - 1)); - if (parts.length == 3) { - int r = clampColor(parts[0]); - int g = clampColor(parts[1]); - int b = clampColor(parts[2]); - return toHexColor((r << 16) | (g << 8) | b); + if (parts.length != 3) { + throw new CSSSyntaxException("rgb() must have exactly 3 components: " + cssColor); } + int r = parseRgbChannel(parts[0], cssColor); + int g = parseRgbChannel(parts[1], cssColor); + int b = parseRgbChannel(parts[2], cssColor); + return toHexColor((r << 16) | (g << 8) | b); } String keyword = cssColorKeyword(value); @@ -303,8 +314,8 @@ private String normalizeHexColor(String cssColor) { + value.charAt(1) + value.charAt(1) + value.charAt(2) + value.charAt(2); } - if (value.length() != 6) { - return "000000"; + if (value.length() != 6 || !isHexColor(value)) { + throw new CSSSyntaxException("Unsupported color value: " + cssColor); } return value; } @@ -320,7 +331,7 @@ private String normalizeAlignment(String value) { if ("right".equals(v) || "end".equals(v)) { return String.valueOf(Component.RIGHT); } - return null; + throw new CSSSyntaxException("Unsupported text-align value: " + value); } private String cssColorKeyword(String value) { @@ -357,19 +368,30 @@ private String cssColorKeyword(String value) { return null; } - private int clampColor(String value) { + private int parseRgbChannel(String value, String originalColor) { + int out; try { - int out = Integer.parseInt(value.trim()); - if (out < 0) { - return 0; - } - if (out > 255) { - return 255; - } - return out; + out = Integer.parseInt(value.trim()); } catch (RuntimeException err) { - return 0; + throw new CSSSyntaxException("Invalid rgb() channel value in " + originalColor + ": " + value); + } + if (out < 0 || out > 255) { + throw new CSSSyntaxException("rgb() channel out of range in " + originalColor + ": " + value); + } + return out; + } + + private boolean isHexColor(String value) { + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + boolean hex = (c >= '0' && c <= '9') + || (c >= 'a' && c <= 'f') + || (c >= 'A' && c <= 'F'); + if (!hex) { + return false; + } } + return true; } private String toHexColor(int color) { @@ -410,13 +432,22 @@ private Rule[] parseRules(String css) { ArrayList out = new ArrayList(); int pos = 0; while (pos < stripped.length()) { + while (pos < stripped.length() && Character.isWhitespace(stripped.charAt(pos))) { + pos++; + } + if (pos >= stripped.length()) { + break; + } int open = stripped.indexOf('{', pos); if (open < 0) { - break; + throw new CSSSyntaxException("Missing '{' in CSS rule near: " + stripped.substring(pos)); } int close = stripped.indexOf('}', open + 1); if (close < 0) { - break; + throw new CSSSyntaxException("Missing '}' for CSS rule: " + stripped.substring(pos, open).trim()); + } + if (stripped.indexOf('{', open + 1) > -1 && stripped.indexOf('{', open + 1) < close) { + throw new CSSSyntaxException("Nested '{' is not supported in CSS block: " + stripped.substring(pos, open).trim()); } String selectors = stripped.substring(pos, open).trim(); @@ -424,6 +455,9 @@ private Rule[] parseRules(String css) { pos = close + 1; continue; } + if (selectors.length() == 0) { + throw new CSSSyntaxException("Missing selector before '{'"); + } String body = stripped.substring(open + 1, close).trim(); Declaration[] declarations = parseDeclarations(body); @@ -431,7 +465,7 @@ private Rule[] parseRules(String css) { for (String selectorEntry : selectorsList) { String selector = selectorEntry.trim(); if (selector.length() == 0) { - continue; + throw new CSSSyntaxException("Empty selector in selector list: " + selectors); } Rule rule = new Rule(); rule.selector = selector; @@ -451,13 +485,18 @@ private String stripComments(String css) { char c = css.charAt(i); if (c == '/' && i + 1 < css.length() && css.charAt(i + 1) == '*') { i += 2; + boolean closed = false; while (i + 1 < css.length()) { if (css.charAt(i) == '*' && css.charAt(i + 1) == '/') { i += 2; + closed = true; break; } i++; } + if (!closed) { + throw new CSSSyntaxException("Unterminated CSS comment"); + } continue; } out.append(c); @@ -470,13 +509,20 @@ private Declaration[] parseDeclarations(String body) { ArrayList out = new ArrayList(); String[] segments = splitOnChar(body, ';'); for (String line : segments) { - int colon = line.indexOf(':'); - if (colon <= 0) { + String trimmed = line.trim(); + if (trimmed.length() == 0) { continue; } + int colon = trimmed.indexOf(':'); + if (colon <= 0 || colon == trimmed.length() - 1) { + throw new CSSSyntaxException("Malformed declaration: " + trimmed); + } Declaration dec = new Declaration(); - dec.property = line.substring(0, colon).trim().toLowerCase(); - dec.value = line.substring(colon + 1).trim(); + dec.property = trimmed.substring(0, colon).trim().toLowerCase(); + dec.value = trimmed.substring(colon + 1).trim(); + if (dec.property.length() == 0 || dec.value.length() == 0) { + throw new CSSSyntaxException("Malformed declaration: " + trimmed); + } out.add(dec); } return out.toArray(new Declaration[out.size()]); diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/css/CSSThemeCompilerTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/css/CSSThemeCompilerTest.java index 7db4b98ecc..3744123807 100644 --- a/maven/core-unittests/src/test/java/com/codename1/ui/css/CSSThemeCompilerTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/ui/css/CSSThemeCompilerTest.java @@ -11,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; public class CSSThemeCompilerTest extends UITestBase { @@ -47,4 +48,17 @@ public void testCompilesThemeConstantsDeriveAndMutableImages() { assertNotNull(mutable); assertNotNull(theme.get("Button.press#bgImage")); } + @Test + public void testThrowsOnMalformedCss() { + CSSThemeCompiler compiler = new CSSThemeCompiler(); + MutableResource resource = new MutableResource(); + + assertThrows(CSSThemeCompiler.CSSSyntaxException.class, () -> + compiler.compile("Button{color:#12;}", resource, "Theme") + ); + assertThrows(CSSThemeCompiler.CSSSyntaxException.class, () -> + compiler.compile("Button{color:#ff00ff;text-align:middle;}", resource, "Theme") + ); + } + } diff --git a/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java b/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java index 4d0763336b..739e421643 100644 --- a/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java @@ -24,6 +24,7 @@ import com.codename1.ui.Label; import com.codename1.ui.RadioButton; import com.codename1.ui.TextArea; +import com.codename1.ui.css.CSSThemeCompiler; import com.codename1.ui.TextField; import com.codename1.ui.layouts.BorderLayout; import com.codename1.ui.layouts.BoxLayout; @@ -57,6 +58,8 @@ public void runApp() { final SpanLabel summaryLabel = new SpanLabel(); final TemplatePreviewPanel previewPanel = new TemplatePreviewPanel(selectedTemplate[0]); final Container[] themePanelRef = new Container[1]; + final Label customCssError = new Label(""); + final boolean[] customCssValid = new boolean[]{true}; appNameField.setUIID("InitializrField"); packageField.setUIID("InitializrField"); @@ -67,6 +70,9 @@ public void runApp() { packageError.setHidden(true); packageError.setVisible(false); summaryLabel.setUIID("InitializrSummary"); + customCssError.setUIID("InitializrValidationError"); + customCssError.setHidden(true); + customCssError.setVisible(false); final Runnable refresh = new Runnable() { public void run() { @@ -75,8 +81,19 @@ public void run() { includeLocalizationBundles[0], previewLanguage[0], javaVersion[0], customThemeCss[0] ); - previewPanel.setTemplate(selectedTemplate[0]); - previewPanel.setOptions(options); + customCssValid[0] = true; + customCssError.setText(""); + customCssError.setHidden(true); + customCssError.setVisible(false); + try { + previewPanel.setTemplate(selectedTemplate[0]); + previewPanel.setOptions(options); + } catch (CSSThemeCompiler.CSSSyntaxException cssErr) { + customCssValid[0] = false; + customCssError.setText("Custom CSS Error: " + cssErr.getMessage()); + customCssError.setHidden(false); + customCssError.setVisible(true); + } boolean canCustomizeTheme = supportsLivePreview(selectedTemplate[0]); if (themePanelRef[0] != null) { setEnabledRecursive(themePanelRef[0], canCustomizeTheme); @@ -101,7 +118,7 @@ public void run() { createTemplateSelector(selectedTemplate, templateButtons, refresh) ); final Container idePanel = createIdeSelectorPanel(selectedIde, refresh); - final Container themePanel = createThemeOptionsPanel(selectedThemeMode, selectedAccent, roundedButtons, customThemeCss, refresh); + final Container themePanel = createThemeOptionsPanel(selectedThemeMode, selectedAccent, roundedButtons, customThemeCss, customCssError, refresh); final Container localizationPanel = createLocalizationPanel(includeLocalizationBundles, previewLanguage, refresh, previewPanel); final Container javaPanel = createJavaOptionsPanel(javaVersion, refresh); themePanelRef[0] = themePanel; @@ -129,9 +146,9 @@ public void run() { generateButton.setUIID("InitializrPrimaryButton"); generateButton.addActionListener(e -> { - if (!validateInputs(appNameField, packageField)) { + if (!validateInputs(appNameField, packageField) || !customCssValid[0]) { updateValidationErrorLabels(appNameField, packageField, appNameError, packageError); - ToastBar.showErrorMessage("Please fix validation errors before generating."); + ToastBar.showErrorMessage(customCssValid[0] ? "Please fix validation errors before generating." : "Please fix custom CSS errors before generating."); form.revalidate(); return; } @@ -266,6 +283,7 @@ private Container createThemeOptionsPanel(ProjectOptions.ThemeMode[] selectedThe ProjectOptions.Accent[] selectedAccent, boolean[] roundedButtons, String[] customThemeCss, + Label customCssError, Runnable onSelectionChanged) { Container modeRow = new Container(new GridLayout(1, 2)); modeRow.setUIID("InitializrChoicesGrid"); @@ -328,7 +346,8 @@ private Container createThemeOptionsPanel(ProjectOptions.ThemeMode[] selectedThe labeledField("Mode", modeRow), labeledField("Accent", accentRow), rounded, - labeledField("Append Custom CSS", cssEditor) + labeledField("Append Custom CSS", cssEditor), + customCssError ); } diff --git a/scripts/initializr/common/src/main/java/com/codename1/initializr/ui/TemplatePreviewPanel.java b/scripts/initializr/common/src/main/java/com/codename1/initializr/ui/TemplatePreviewPanel.java index d407c2b5d3..1d89cdeab2 100644 --- a/scripts/initializr/common/src/main/java/com/codename1/initializr/ui/TemplatePreviewPanel.java +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/ui/TemplatePreviewPanel.java @@ -186,14 +186,20 @@ private void restoreThemeDefaults() { private void updateMode() { previewHolder.removeAll(); - if (template == Template.BAREBONES || template == Template.KOTLIN) { - Form liveForm = createBarebonesPreviewForm(options); - liveFormPreview = new InterFormContainer(liveForm); - liveFormPreview.setUIID("InitializrLiveFrame"); - previewHolder.add(BorderLayout.CENTER, liveFormPreview); - } else { - staticPreview.setImage(Resources.getGlobalResources().getImage(template.IMAGE_NAME)); - previewHolder.add(BorderLayout.CENTER, staticPreview); + try { + if (template == Template.BAREBONES || template == Template.KOTLIN) { + Form liveForm = createBarebonesPreviewForm(options); + liveFormPreview = new InterFormContainer(liveForm); + liveFormPreview.setUIID("InitializrLiveFrame"); + previewHolder.add(BorderLayout.CENTER, liveFormPreview); + } else { + staticPreview.setImage(Resources.getGlobalResources().getImage(template.IMAGE_NAME)); + previewHolder.add(BorderLayout.CENTER, staticPreview); + } + } catch (CSSThemeCompiler.CSSSyntaxException cssError) { + staticPreviewFallback.setText("Custom CSS error: " + cssError.getMessage()); + previewHolder.add(BorderLayout.CENTER, staticPreviewFallback); + throw cssError; } previewHolder.revalidate(); }