From 61446fa79f5175936a057664a6ce70778b2f0c0b 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 01/17] Add advanced theme CSS editing with live preview # Conflicts: # scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java --- .../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 9c342572c8..51d6a39807 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[]{false}; 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 27fd27899c..25f0b66f32 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); @@ -469,13 +469,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") @@ -491,7 +495,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") @@ -504,14 +508,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") @@ -527,12 +531,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; } @@ -545,6 +549,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 1d082c0758..fa5546003f 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 cdcd876dfe..1701aee206 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 @@ -23,10 +23,34 @@ public boolean runTest() throws Exception { } validateExperimentalJava17Generation(); validateExperimentalJava17RegressionFixes(); + 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 26dd909cfb9d1eea66cb142f85f146cfc71b7093 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 02/17] Rename advanced theme mode to append custom CSS --- .../com/codename1/initializr/Initializr.java | 30 +++++-------------- .../initializr/model/GeneratorModel.java | 2 +- .../model/GeneratorModelMatrixTest.java | 5 ++-- 3 files changed, 11 insertions(+), 26 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 51d6a39807..4f17b4ec3c 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[]{false}; 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 25f0b66f32..9a20453144 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 @@ -472,7 +472,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 1701aee206..cf210299b4 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 @@ -24,12 +24,13 @@ public boolean runTest() throws Exception { validateExperimentalJava17Generation(); validateExperimentalJava17RegressionFixes(); 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"; @@ -47,7 +48,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 aba47badb40361b9b2583fa49a652df318594804 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 03/17] 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 59cd1f6fc40f0b65155a470cebdce02671f13d6a 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 04/17] 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 c52cf926a6b31404409f4b097ba17363a98b08c9 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 05/17] 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 4f17b4ec3c..3844ae57d5 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(); } From 2de50daa906fc122dbf0989ee5958efd3a7de662 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Wed, 11 Mar 2026 09:39:37 +0200 Subject: [PATCH 06/17] Fix Initializr compile compatibility and preserve CSS parse stack traces --- CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java | 6 +++++- .../src/main/java/com/codename1/initializr/Initializr.java | 3 +-- .../com/codename1/initializr/ui/TemplatePreviewPanel.java | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java b/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java index 1e02408714..7096490163 100644 --- a/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java +++ b/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java @@ -45,6 +45,10 @@ public static class CSSSyntaxException extends IllegalArgumentException { public CSSSyntaxException(String message) { super(message); } + + public CSSSyntaxException(String message, Throwable cause) { + super(message, cause); + } } public void compile(String css, MutableResource resources, String themeName) { @@ -373,7 +377,7 @@ private int parseRgbChannel(String value, String originalColor) { try { out = Integer.parseInt(value.trim()); } catch (RuntimeException err) { - throw new CSSSyntaxException("Invalid rgb() channel value in " + originalColor + ": " + value); + throw new CSSSyntaxException("Invalid rgb() channel value in " + originalColor + ": " + value, err); } if (out < 0 || out > 255) { throw new CSSSyntaxException("rgb() channel out of range in " + originalColor + ": " + value); 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 3844ae57d5..4b544913e6 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,7 +24,6 @@ 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; @@ -88,7 +87,7 @@ public void run() { try { previewPanel.setTemplate(selectedTemplate[0]); previewPanel.setOptions(options); - } catch (CSSThemeCompiler.CSSSyntaxException cssErr) { + } catch (IllegalArgumentException cssErr) { customCssValid[0] = false; customCssError.setText("Custom CSS Error: " + cssErr.getMessage()); customCssError.setHidden(false); 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 1d89cdeab2..ab5ab833a5 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 @@ -196,7 +196,7 @@ private void updateMode() { staticPreview.setImage(Resources.getGlobalResources().getImage(template.IMAGE_NAME)); previewHolder.add(BorderLayout.CENTER, staticPreview); } - } catch (CSSThemeCompiler.CSSSyntaxException cssError) { + } catch (IllegalArgumentException cssError) { staticPreviewFallback.setText("Custom CSS error: " + cssError.getMessage()); previewHolder.add(BorderLayout.CENTER, staticPreviewFallback); throw cssError; From 6d66268007e446c0070d9c43ec8f135eee36d372 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:10:27 +0200 Subject: [PATCH 07/17] Fix custom CSS runtime application and prevent uncaught preview errors --- .../src/com/codename1/ui/css/CSSThemeCompiler.java | 10 +++++----- .../com/codename1/ui/css/CSSThemeCompilerTest.java | 11 ++++++++++- .../java/com/codename1/initializr/Initializr.java | 1 - 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java b/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java index 7096490163..b42d2e4979 100644 --- a/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java +++ b/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java @@ -178,7 +178,7 @@ private boolean applySimpleThemeProperty(Hashtable theme, String uiid, String st return true; } if ("text-align".equals(property)) { - String align = normalizeAlignment(value); + Integer align = normalizeAlignment(value); theme.put(uiid + "." + statePrefix + "align", align); return true; } @@ -324,16 +324,16 @@ private String normalizeHexColor(String cssColor) { return value; } - private String normalizeAlignment(String value) { + private Integer normalizeAlignment(String value) { String v = value == null ? "" : value.trim().toLowerCase(); if ("left".equals(v) || "start".equals(v)) { - return String.valueOf(Component.LEFT); + return Integer.valueOf(Component.LEFT); } if ("center".equals(v)) { - return String.valueOf(Component.CENTER); + return Integer.valueOf(Component.CENTER); } if ("right".equals(v) || "end".equals(v)) { - return String.valueOf(Component.RIGHT); + return Integer.valueOf(Component.RIGHT); } throw new CSSSyntaxException("Unsupported text-align value: " + value); } 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 3744123807..fdada830b6 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,9 +1,11 @@ package com.codename1.ui.css; import com.codename1.junit.UITestBase; +import com.codename1.ui.Button; import com.codename1.ui.Component; import com.codename1.ui.Image; import com.codename1.ui.plaf.CSSBorder; +import com.codename1.ui.plaf.UIManager; import com.codename1.ui.util.MutableResource; import java.util.Hashtable; import org.junit.jupiter.api.Test; @@ -41,9 +43,16 @@ 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"))); + assertEquals(Integer.valueOf(Component.CENTER), theme.get("Button.align")); assertTrue(theme.get("Button.press#border") instanceof CSSBorder); + + UIManager.getInstance().addThemeProps(theme); + Button runtimeButton = new Button("Runtime"); + runtimeButton.setUIID("Button"); + assertEquals(0xffc0cb, runtimeButton.getUnselectedStyle().getFgColor()); + assertEquals(Component.CENTER, runtimeButton.getUnselectedStyle().getAlignment()); + Image mutable = resource.getImage("btnBg"); assertNotNull(mutable); assertNotNull(theme.get("Button.press#bgImage")); 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 4b544913e6..6445bd9bca 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 @@ -210,7 +210,6 @@ private Container createLocalizationPanel(boolean[] includeLocalizationBundles, String selected = languagePicker.getSelectedString(); previewLanguage[0] = findLanguageByLabel(selected); onSelectionChanged.run(); - previewPanel.showUpdatedLivePreview(); }); return BoxLayout.encloseY(includeBundles, labeledField("Preview Language", languagePicker)); From cd08eb0645e715ca6a25640b4a757543e1dccf2d Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:42:39 +0200 Subject: [PATCH 08/17] Fix preview CSS application and keep preview visible on CSS errors --- .../initializr/ui/TemplatePreviewPanel.java | 39 ++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) 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 ab5ab833a5..97b91b4f61 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 @@ -14,7 +14,6 @@ import com.codename1.ui.FontImage; import com.codename1.ui.Form; import com.codename1.ui.InterFormContainer; -import com.codename1.ui.Label; import com.codename1.ui.layouts.BorderLayout; import com.codename1.ui.layouts.BoxLayout; import com.codename1.ui.plaf.UIManager; @@ -30,7 +29,6 @@ public class TemplatePreviewPanel { private final Container root; private final Container previewHolder; private final ImageViewer staticPreview; - private final Label staticPreviewFallback; private InterFormContainer liveFormPreview; private Template template; @@ -40,9 +38,6 @@ public TemplatePreviewPanel(Template template) { this.template = template; staticPreview = new ImageViewer(); - staticPreviewFallback = new Label("Preview unavailable"); - staticPreviewFallback.setUIID("InitializrTip"); - previewHolder = new Container(new BorderLayout()); previewHolder.setUIID("InitializrPreviewHolder"); @@ -69,8 +64,9 @@ public void setOptions(ProjectOptions options) { public void showUpdatedLivePreview() { if (template == Template.BAREBONES || template == Template.KOTLIN) { Form liveForm = createBarebonesPreviewForm(options); - liveFormPreview = new InterFormContainer(liveForm); - liveFormPreview.setUIID("InitializrLiveFrame"); + InterFormContainer next = new InterFormContainer(liveForm); + next.setUIID("InitializrLiveFrame"); + liveFormPreview = next; previewHolder.removeAll(); previewHolder.add(BorderLayout.CENTER, liveFormPreview); previewHolder.revalidate(); @@ -87,6 +83,9 @@ 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); + if (options.customThemeCss != null && options.customThemeCss.trim().length() > 0) { + helloButton.setUIID("Button"); + } applyLiveCssOverrides(form, options); return form; } @@ -185,21 +184,17 @@ private void restoreThemeDefaults() { } private void updateMode() { - previewHolder.removeAll(); - 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 (IllegalArgumentException cssError) { - staticPreviewFallback.setText("Custom CSS error: " + cssError.getMessage()); - previewHolder.add(BorderLayout.CENTER, staticPreviewFallback); - throw cssError; + if (template == Template.BAREBONES || template == Template.KOTLIN) { + Form liveForm = createBarebonesPreviewForm(options); + InterFormContainer next = new InterFormContainer(liveForm); + next.setUIID("InitializrLiveFrame"); + liveFormPreview = next; + previewHolder.removeAll(); + previewHolder.add(BorderLayout.CENTER, liveFormPreview); + } else { + staticPreview.setImage(Resources.getGlobalResources().getImage(template.IMAGE_NAME)); + previewHolder.removeAll(); + previewHolder.add(BorderLayout.CENTER, staticPreview); } previewHolder.revalidate(); } From 44bba33bbdb2b464a94359784bc5cfe74a426439 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:20:52 +0200 Subject: [PATCH 09/17] Fix border shorthand and pseudo-state parsing in CSS theme compiler --- .../codename1/ui/css/CSSThemeCompiler.java | 77 ++++++++++++++++++- .../ui/css/CSSThemeCompilerTest.java | 11 ++- 2 files changed, 81 insertions(+), 7 deletions(-) diff --git a/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java b/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java index b42d2e4979..5756f3a8c9 100644 --- a/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java +++ b/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java @@ -212,6 +212,17 @@ private boolean appendBorderProperty(StringBuilder borderCss, String property, S if (!isBorderProperty(property)) { return false; } + if ("border".equals(property)) { + String expanded = expandBorderShorthand(value); + if (expanded.length() == 0) { + return true; + } + if (borderCss.length() > 0) { + borderCss.append(';'); + } + borderCss.append(expanded); + return true; + } if (borderCss.length() > 0) { borderCss.append(';'); } @@ -219,6 +230,53 @@ private boolean appendBorderProperty(StringBuilder borderCss, String property, S return true; } + private String expandBorderShorthand(String value) { + String[] parts = splitOnWhitespace(value); + if (parts.length == 0) { + throw new CSSSyntaxException("border shorthand is missing value"); + } + String width = null; + String style = null; + String color = null; + for (String part : parts) { + String token = part.trim().toLowerCase(); + if (token.length() == 0) { + continue; + } + if (width == null && (token.endsWith("px") || token.endsWith("mm") || token.endsWith("pt") || token.endsWith("%") || "0".equals(token))) { + width = part; + continue; + } + if (style == null && ("none".equals(token) || "solid".equals(token) || "dashed".equals(token) || "dotted".equals(token))) { + style = token; + continue; + } + if (color == null) { + normalizeHexColor(part); + color = part; + continue; + } + throw new CSSSyntaxException("Unsupported border shorthand token: " + part); + } + StringBuilder out = new StringBuilder(); + if (width != null) { + out.append("border-width:").append(width); + } + if (style != null) { + if (out.length() > 0) { + out.append(';'); + } + out.append("border-style:").append(style); + } + if (color != null) { + if (out.length() > 0) { + out.append(';'); + } + out.append("border-color:").append(color); + } + return out.toString(); + } + private String resolveVars(Hashtable theme, String value) { String out = value; int varPos = out.indexOf("var(--"); @@ -239,10 +297,21 @@ private String resolveVars(Hashtable theme, String value) { private String[] selector(String selector) { String statePrefix = ""; String uiid = selector.trim(); + int pseudoPos = uiid.indexOf(':'); - if (pseudoPos > -1) { - String pseudo = uiid.substring(pseudoPos + 1).trim(); - uiid = uiid.substring(0, pseudoPos).trim(); + int classStatePos = uiid.indexOf('.'); + int statePos = -1; + if (pseudoPos > -1 && classStatePos > -1) { + statePos = Math.min(pseudoPos, classStatePos); + } else if (pseudoPos > -1) { + statePos = pseudoPos; + } else if (classStatePos > -1) { + statePos = classStatePos; + } + + if (statePos > -1) { + String pseudo = uiid.substring(statePos + 1).trim(); + uiid = uiid.substring(0, statePos).trim(); statePrefix = statePrefix(pseudo); } if ("*".equals(uiid) || uiid.length() == 0) { @@ -261,7 +330,7 @@ private String statePrefix(String pseudo) { if ("disabled".equals(pseudo)) { return "dis#"; } - return ""; + throw new CSSSyntaxException("Unsupported pseudo state: " + pseudo); } private Image createSolidImage(String color) { 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 fdada830b6..f06d92ee1e 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 @@ -28,7 +28,8 @@ public void testCompilesThemeConstantsDeriveAndMutableImages() { + "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;}" - + "Button{color:pink;text-align:center;}", + + "Button{color:pink;text-align:center;border:1px solid #00ff00;}" + + "Button.pressed{color:#00ff00;}", resource, "Theme" ); @@ -44,14 +45,15 @@ public void testCompilesThemeConstantsDeriveAndMutableImages() { assertEquals("4px", theme.get("@spacing")); assertEquals("#abc", theme.get("@primarycolor")); assertEquals(Integer.valueOf(Component.CENTER), theme.get("Button.align")); - assertTrue(theme.get("Button.press#border") instanceof CSSBorder); - + assertTrue(theme.get("Button.border") instanceof CSSBorder); + assertEquals("00ff00", theme.get("Button.press#fgColor")); UIManager.getInstance().addThemeProps(theme); Button runtimeButton = new Button("Runtime"); runtimeButton.setUIID("Button"); assertEquals(0xffc0cb, runtimeButton.getUnselectedStyle().getFgColor()); assertEquals(Component.CENTER, runtimeButton.getUnselectedStyle().getAlignment()); + assertNotNull(runtimeButton.getUnselectedStyle().getBorder()); Image mutable = resource.getImage("btnBg"); assertNotNull(mutable); @@ -68,6 +70,9 @@ public void testThrowsOnMalformedCss() { assertThrows(CSSThemeCompiler.CSSSyntaxException.class, () -> compiler.compile("Button{color:#ff00ff;text-align:middle;}", resource, "Theme") ); + assertThrows(CSSThemeCompiler.CSSSyntaxException.class, () -> + compiler.compile("Button:hover{color:#ff00ff;}", resource, "Theme") + ); } } From b9ab7ce73982ea45d05043c6c1dcce289ba69006 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:11:47 +0200 Subject: [PATCH 10/17] Added theme overrides and normalizations --- .../initializr/model/GeneratorModel.java | 37 +++++++++++++++---- .../model/GeneratorModelMatrixTest.java | 26 +++++++++++++ 2 files changed, 56 insertions(+), 7 deletions(-) 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 9a20453144..7b179ef6c6 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 @@ -471,13 +471,15 @@ 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 Appended Custom CSS */\n" + effective.customThemeCss + "\n"; - } - if (isDefaultBarebonesOptions(effective)) { + String customCss = normalizeCustomCss(effective.customThemeCss); + boolean hasCustomCss = customCss.length() > 0; + if (isDefaultBarebonesOptions(effective) && !hasCustomCss) { return ""; } - StringBuilder out = new StringBuilder("\n\n/* Initializr Theme Overrides */\n"); + StringBuilder out = new StringBuilder(); + if (!isDefaultBarebonesOptions(effective)) { + out.append("\n\n/* Initializr Theme Overrides */\n"); + } if (effective.themeMode == ProjectOptions.ThemeMode.DARK) { out.append("Form {\n") @@ -506,11 +508,14 @@ public static String buildThemeOverrides(ProjectOptions options) { .append(" background-color: #334155;\n") .append(" border: 1px solid #64748b;\n") .append("}\n"); + appendCustomCss(out, customCss); return out.toString(); } } else if (effective.accent == ProjectOptions.Accent.DEFAULT) { - // Light + Clean intentionally inherits template defaults (rounded ignored). - return ""; + // Light + Clean intentionally inherits template defaults (rounded ignored) unless custom CSS is provided. + out.setLength(0); + appendCustomCss(out, customCss); + return out.toString(); } int accent = resolveAccentColor(effective); @@ -528,9 +533,27 @@ public static String buildThemeOverrides(ProjectOptions options) { .append(" color: #ffffff;\n") .append(" border-radius: ").append(buttonRadius).append(";\n") .append("}\n"); + appendCustomCss(out, customCss); return out.toString(); } + private static String normalizeCustomCss(String css) { + if (css == null) { + return ""; + } + String trimmed = css.trim(); + return trimmed.length() == 0 ? "" : trimmed; + } + + private static void appendCustomCss(StringBuilder out, String customCss) { + if (customCss.length() == 0) { + return; + } + out.append("\n/* Initializr Appended Custom CSS */\n") + .append(customCss) + .append('\n'); + } + private static boolean isDefaultBarebonesOptions(ProjectOptions options) { return options.themeMode == ProjectOptions.ThemeMode.LIGHT && options.accent == ProjectOptions.Accent.DEFAULT; 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 cf210299b4..8f704fa3b8 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 @@ -25,6 +25,7 @@ public boolean runTest() throws Exception { validateExperimentalJava17RegressionFixes(); validateAdvancedThemeCssGeneration(); validateAppendedCustomCssGeneration(); + validateCustomCssWithoutPresetOverrides(); return true; } @@ -48,10 +49,35 @@ private void validateAppendedCustomCssGeneration() throws Exception { Map entries = readZipEntries(zipData); String themeCss = getText(entries, "common/src/main/css/theme.css"); + assertContains(themeCss, "Initializr Theme Overrides", "Theme CSS should include generated theme overrides marker"); + assertContains(themeCss, "background-color: #1d4ed8", "Theme CSS should preserve selected accent overrides when custom CSS is appended"); 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"); } + private void validateCustomCssWithoutPresetOverrides() throws Exception { + String mainClassName = "DemoCustomOnly"; + String packageName = "com.acme.custom.only"; + String customCss = "Button {\n border-radius: 0;\n}\n"; + ProjectOptions options = new ProjectOptions( + ProjectOptions.ThemeMode.LIGHT, + ProjectOptions.Accent.DEFAULT, + 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"); + assertFalse(themeCss.indexOf("Initializr Theme Overrides") >= 0, "Theme CSS should not add preset overrides for light/default mode"); + assertContains(themeCss, "Initializr Appended Custom CSS", "Theme CSS should include custom CSS marker in custom-only mode"); + assertContains(themeCss, "border-radius: 0", "Theme CSS should include custom CSS in custom-only mode"); + } + private void validateExperimentalJava17Generation() throws Exception { String mainClassName = "DemoExperimentalJava17"; String packageName = "com.acme.experimental.java17"; From bca96a7ad924331f37dba1315ec571e5582cf663 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:35:46 +0200 Subject: [PATCH 11/17] Fixed broken merge --- .../com/codename1/initializr/model/GeneratorModelMatrixTest.java | 1 - 1 file changed, 1 deletion(-) 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 8f704fa3b8..dc8e091156 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 @@ -23,7 +23,6 @@ public boolean runTest() throws Exception { } validateExperimentalJava17Generation(); validateExperimentalJava17RegressionFixes(); - validateAdvancedThemeCssGeneration(); validateAppendedCustomCssGeneration(); validateCustomCssWithoutPresetOverrides(); return true; From 391feaec8385d4730dff0a76dafd2df68a1ea3ad Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Mar 2026 05:52:56 +0200 Subject: [PATCH 12/17] Trying to fix preview regressions --- .../initializr/ui/TemplatePreviewPanel.java | 46 +++++++++++++++---- .../model/GeneratorModelMatrixTest.java | 37 +++++++++++++++ 2 files changed, 75 insertions(+), 8 deletions(-) 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 97b91b4f61..a03edd37f0 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 @@ -156,19 +156,49 @@ 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) { + ProjectOptions baseOptions = new ProjectOptions( + options.themeMode, + options.accent, + options.roundedButtons, + options.includeLocalizationBundles, + options.previewLanguage, + options.javaVersion, + null + ); + String generatedCss = com.codename1.initializr.model.GeneratorModel.buildThemeOverrides(baseOptions); + applyCompiledTheme(generatedCss, "InitializrLiveThemeBase"); + + String customCss = normalizeCustomCss(options.customThemeCss); + if (customCss.length() > 0) { + String wrappedCustomCss = "\n/* Initializr Appended Custom CSS */\n" + customCss + "\n"; + try { + applyCompiledTheme(wrappedCustomCss, "InitializrLiveThemeCustom"); + } catch (RuntimeException err) { + throw new IllegalArgumentException(err.getMessage(), err); + } + } + form.refreshTheme(); + } + + private void applyCompiledTheme(String css, String themeName) { + if (css == null || css.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; + compiler.compile(css, resource, themeName); + Hashtable generatedTheme = resource.getTheme(themeName); + if (generatedTheme != null && !generatedTheme.isEmpty()) { + UIManager.getInstance().addThemeProps(generatedTheme); } - UIManager.getInstance().addThemeProps(generatedTheme); - form.refreshTheme(); + } + + private String normalizeCustomCss(String css) { + if (css == null) { + return ""; + } + String trimmed = css.trim(); + return trimmed.length() == 0 ? "" : trimmed; } private void restoreThemeDefaults() { 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 dc8e091156..b3ae939ca7 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 @@ -2,6 +2,8 @@ import com.codename1.io.Util; import com.codename1.testing.AbstractTest; +import com.codename1.ui.css.CSSThemeCompiler; +import com.codename1.ui.util.MutableResource; import com.codename1.util.StringUtil; import net.sf.zipme.ZipEntry; import net.sf.zipme.ZipInputStream; @@ -25,6 +27,7 @@ public boolean runTest() throws Exception { validateExperimentalJava17RegressionFixes(); validateAppendedCustomCssGeneration(); validateCustomCssWithoutPresetOverrides(); + validateThemeCssCompilesForAllVariants(); return true; } @@ -77,6 +80,40 @@ private void validateCustomCssWithoutPresetOverrides() throws Exception { assertContains(themeCss, "border-radius: 0", "Theme CSS should include custom CSS in custom-only mode"); } + private void validateThemeCssCompilesForAllVariants() { + for (ProjectOptions.ThemeMode mode : ProjectOptions.ThemeMode.values()) { + for (ProjectOptions.Accent accent : ProjectOptions.Accent.values()) { + validateThemeCssCompiles(new ProjectOptions( + mode, + accent, + true, + true, + ProjectOptions.PreviewLanguage.ENGLISH, + ProjectOptions.JavaVersion.JAVA_8 + )); + validateThemeCssCompiles(new ProjectOptions( + mode, + accent, + false, + true, + ProjectOptions.PreviewLanguage.ENGLISH, + ProjectOptions.JavaVersion.JAVA_8, + "Button { border-radius: 0; }" + )); + } + } + } + + private void validateThemeCssCompiles(ProjectOptions options) { + String css = GeneratorModel.buildThemeOverrides(options); + if (css == null || css.trim().length() == 0) { + return; + } + MutableResource resource = new MutableResource(); + new CSSThemeCompiler().compile(css, resource, "CompileCheckTheme"); + assertNotNull(resource.getTheme("CompileCheckTheme"), "Generated CSS should compile to a theme"); + } + private void validateExperimentalJava17Generation() throws Exception { String mainClassName = "DemoExperimentalJava17"; String packageName = "com.acme.experimental.java17"; From 18e157384ecb7eebade9e9d2aae0a5e06796d03c Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Mar 2026 06:34:19 +0200 Subject: [PATCH 13/17] Added validation testing and hopefully fixed the theme issues --- .../initializr/ui/TemplatePreviewPanel.java | 15 +++- .../ui/TemplatePreviewPanelThemeTest.java | 69 +++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 scripts/initializr/common/src/test/java/com/codename1/initializr/ui/TemplatePreviewPanelThemeTest.java 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 a03edd37f0..a8715a9a5a 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 @@ -33,6 +33,8 @@ public class TemplatePreviewPanel { private Template template; private ProjectOptions options = ProjectOptions.defaults(); + private Form lastLiveForm; + private Button lastLiveHelloButton; public TemplatePreviewPanel(Template template) { this.template = template; @@ -83,13 +85,20 @@ 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); - if (options.customThemeCss != null && options.customThemeCss.trim().length() > 0) { - helloButton.setUIID("Button"); - } applyLiveCssOverrides(form, options); + lastLiveForm = form; + lastLiveHelloButton = helloButton; return form; } + Form getLastLiveFormForTesting() { + return lastLiveForm; + } + + Button getLastLiveHelloButtonForTesting() { + return lastLiveHelloButton; + } + private void installBundle(ProjectOptions options) { if (!options.includeLocalizationBundles) { UIManager.getInstance().setBundle(null); diff --git a/scripts/initializr/common/src/test/java/com/codename1/initializr/ui/TemplatePreviewPanelThemeTest.java b/scripts/initializr/common/src/test/java/com/codename1/initializr/ui/TemplatePreviewPanelThemeTest.java new file mode 100644 index 0000000000..04b82cd495 --- /dev/null +++ b/scripts/initializr/common/src/test/java/com/codename1/initializr/ui/TemplatePreviewPanelThemeTest.java @@ -0,0 +1,69 @@ +package com.codename1.initializr.ui; + +import com.codename1.initializr.model.ProjectOptions; +import com.codename1.initializr.model.Template; +import com.codename1.testing.AbstractTest; +import com.codename1.ui.Button; +import com.codename1.ui.Form; + +public class TemplatePreviewPanelThemeTest extends AbstractTest { + + @Override + public boolean shouldExecuteOnEDT() { + return true; + } + + @Override + public boolean runTest() throws Exception { + validateModeAndAccentUiidUpdates(); + validateThemeTogglesStillApplyWithCustomCss(); + return true; + } + + private void validateModeAndAccentUiidUpdates() { + TemplatePreviewPanel panel = new TemplatePreviewPanel(Template.BAREBONES); + + panel.setOptions(options(ProjectOptions.ThemeMode.LIGHT, ProjectOptions.Accent.TEAL, true, null)); + Button firstButton = panel.getLastLiveHelloButtonForTesting(); + Form firstForm = panel.getLastLiveFormForTesting(); + assertNotNull(firstButton, "Preview should include a hello button"); + assertNotNull(firstForm, "Preview should include a form"); + assertEqual("InitializrLiveButtonLightTealRound", firstButton.getUIID(), "Light/Teal/Rounded should map to expected UIID"); + assertEqual("Container", firstForm.getContentPane().getUIID(), "Light mode should use container content UIID"); + + panel.setOptions(options(ProjectOptions.ThemeMode.DARK, ProjectOptions.Accent.ORANGE, false, null)); + Button secondButton = panel.getLastLiveHelloButtonForTesting(); + Form secondForm = panel.getLastLiveFormForTesting(); + assertEqual("InitializrLiveButtonDarkOrangeSquare", secondButton.getUIID(), "Dark/Orange/Square should map to expected UIID"); + assertEqual("InitializrLiveContentDark", secondForm.getContentPane().getUIID(), "Dark mode should use dark content UIID"); + assertNotEqual(firstButton.getUIID(), secondButton.getUIID(), "Toggling options should change button UIID"); + } + + private void validateThemeTogglesStillApplyWithCustomCss() { + TemplatePreviewPanel panel = new TemplatePreviewPanel(Template.BAREBONES); + String customCss = "Button { border-radius: 0; }"; + + panel.setOptions(options(ProjectOptions.ThemeMode.LIGHT, ProjectOptions.Accent.BLUE, false, customCss)); + Button blueButton = panel.getLastLiveHelloButtonForTesting(); + int blueBg = blueButton.getUnselectedStyle().getBgColor(); + assertEqual("InitializrLiveButtonLightBlueSquare", blueButton.getUIID(), "Custom CSS should not replace computed UIID"); + + panel.setOptions(options(ProjectOptions.ThemeMode.LIGHT, ProjectOptions.Accent.ORANGE, false, customCss)); + Button orangeButton = panel.getLastLiveHelloButtonForTesting(); + int orangeBg = orangeButton.getUnselectedStyle().getBgColor(); + assertEqual("InitializrLiveButtonLightOrangeSquare", orangeButton.getUIID(), "Accent toggle should continue to update UIID with custom CSS"); + assertNotEqual(blueBg, orangeBg, "Accent toggles should still change applied button colors with custom CSS"); + } + + private ProjectOptions options(ProjectOptions.ThemeMode mode, ProjectOptions.Accent accent, boolean rounded, String customCss) { + return new ProjectOptions( + mode, + accent, + rounded, + true, + ProjectOptions.PreviewLanguage.ENGLISH, + ProjectOptions.JavaVersion.JAVA_8, + customCss + ); + } +} From 2927b4a29bafab9a662c8a1b6ce02a568e04caaf Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Mar 2026 07:43:50 +0200 Subject: [PATCH 14/17] Fixed theme so it applies to preview --- .../com/codename1/initializr/Initializr.java | 1 + .../initializr/ui/TemplatePreviewPanel.java | 52 ++------------ .../InitializrThemeInteractionTest.java | 71 +++++++++++++++++++ 3 files changed, 79 insertions(+), 45 deletions(-) create mode 100644 scripts/initializr/common/src/test/java/com/codename1/initializr/InitializrThemeInteractionTest.java 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 6445bd9bca..1d50691909 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 @@ -332,6 +332,7 @@ private Container createThemeOptionsPanel(ProjectOptions.ThemeMode[] selectedThe }); TextArea cssEditor = new TextArea(customThemeCss[0], 8, 30); + cssEditor.setName("appendCustomCssEditor"); cssEditor.setUIID("InitializrField"); cssEditor.setHint("/* Appended to generated theme.css */\nButton {\n border-radius: 0;\n}"); cssEditor.setGrowByContent(true); 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 a8715a9a5a..6672b87b37 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,7 +6,6 @@ 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,7 +16,6 @@ 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; @@ -79,13 +77,14 @@ private Form createBarebonesPreviewForm(ProjectOptions options) { installBundle(options); Form form = new Form("Hi World", BoxLayout.y()); Button helloButton = new Button("Hello World"); + helloButton.setName("previewHelloButton"); helloButton.setUIID("Button"); helloButton.addActionListener(e -> Dialog.show("Hello Codename One", "Welcome to Codename One", "OK", null)); form.add(helloButton); 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); + validateCustomCss(options.customThemeCss); lastLiveForm = form; lastLiveHelloButton = helloButton; return form; @@ -163,43 +162,18 @@ private Hashtable loadBundleProperties(String resourcePath) { } } - private void applyLiveCssOverrides(Form form, ProjectOptions options) { - restoreThemeDefaults(); - ProjectOptions baseOptions = new ProjectOptions( - options.themeMode, - options.accent, - options.roundedButtons, - options.includeLocalizationBundles, - options.previewLanguage, - options.javaVersion, - null - ); - String generatedCss = com.codename1.initializr.model.GeneratorModel.buildThemeOverrides(baseOptions); - applyCompiledTheme(generatedCss, "InitializrLiveThemeBase"); - - String customCss = normalizeCustomCss(options.customThemeCss); + private void validateCustomCss(String rawCustomCss) { + String customCss = normalizeCustomCss(rawCustomCss); if (customCss.length() > 0) { String wrappedCustomCss = "\n/* Initializr Appended Custom CSS */\n" + customCss + "\n"; try { - applyCompiledTheme(wrappedCustomCss, "InitializrLiveThemeCustom"); + com.codename1.ui.css.CSSThemeCompiler compiler = new com.codename1.ui.css.CSSThemeCompiler(); + com.codename1.ui.util.MutableResource resource = new com.codename1.ui.util.MutableResource(); + compiler.compile(wrappedCustomCss, resource, "InitializrLiveThemeValidation"); } catch (RuntimeException err) { throw new IllegalArgumentException(err.getMessage(), err); } } - form.refreshTheme(); - } - - private void applyCompiledTheme(String css, String themeName) { - if (css == null || css.trim().length() == 0) { - return; - } - MutableResource resource = new MutableResource(); - CSSThemeCompiler compiler = new CSSThemeCompiler(); - compiler.compile(css, resource, themeName); - Hashtable generatedTheme = resource.getTheme(themeName); - if (generatedTheme != null && !generatedTheme.isEmpty()) { - UIManager.getInstance().addThemeProps(generatedTheme); - } } private String normalizeCustomCss(String css) { @@ -210,18 +184,6 @@ private String normalizeCustomCss(String css) { return trimmed.length() == 0 ? "" : trimmed; } - 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() { if (template == Template.BAREBONES || template == Template.KOTLIN) { Form liveForm = createBarebonesPreviewForm(options); diff --git a/scripts/initializr/common/src/test/java/com/codename1/initializr/InitializrThemeInteractionTest.java b/scripts/initializr/common/src/test/java/com/codename1/initializr/InitializrThemeInteractionTest.java new file mode 100644 index 0000000000..d039d0185b --- /dev/null +++ b/scripts/initializr/common/src/test/java/com/codename1/initializr/InitializrThemeInteractionTest.java @@ -0,0 +1,71 @@ +package com.codename1.initializr; + +import com.codename1.testing.AbstractTest; +import com.codename1.ui.Button; +import com.codename1.ui.Component; +import com.codename1.ui.Display; +import com.codename1.ui.Form; + +public class InitializrThemeInteractionTest extends AbstractTest { + + @Override + public boolean shouldExecuteOnEDT() { + return true; + } + + @Override + public boolean runTest() throws Exception { + new Initializr().runApp(); + Form current = Display.getInstance().getCurrent(); + assertNotNull(current, "Initializr should show a form"); + + Button initialHello = getPreviewHelloButton(); + assertNotNull(initialHello, "Preview should include Hello World button"); + assertEqual("Button", initialHello.getUIID(), "Default preview should start in clean light mode"); + + clickByLabel("DARK"); + Button darkHello = getPreviewHelloButton(); + assertNotNull(darkHello, "Preview button should still exist after switching to dark mode"); + assertEqual("InitializrLiveButtonDarkClean", darkHello.getUIID(), "Dark mode should update preview button UIID"); + + clickByLabel("TEAL"); + Button darkTealHello = getPreviewHelloButton(); + assertNotNull(darkTealHello, "Preview button should still exist after switching accent"); + assertEqual("InitializrLiveButtonDarkTealRound", darkTealHello.getUIID(), "Teal accent should update preview button UIID"); + + clickByLabel("LIGHT"); + Button lightTealHello = getPreviewHelloButton(); + assertNotNull(lightTealHello, "Preview button should still exist after switching back to light mode"); + assertEqual("InitializrLiveButtonLightTealRound", lightTealHello.getUIID(), "Light mode should update preview button UIID"); + + setText("appendCustomCssEditor", "Button { border-radius: 0; }"); + waitFor(100); + + clickByLabel("DARK"); + clickByLabel("BLUE"); + Button darkBlueHello = getPreviewHelloButton(); + assertNotNull(darkBlueHello, "Preview button should exist after applying custom CSS and toggling"); + assertEqual("InitializrLiveButtonDarkBlueRound", darkBlueHello.getUIID(), + "Mode/accent toggles should still update preview with custom CSS"); + + clickByLabel("LIGHT"); + clickByLabel("ORANGE"); + Button lightOrangeHello = getPreviewHelloButton(); + assertNotNull(lightOrangeHello, "Preview button should exist after switching back with custom CSS"); + assertEqual("InitializrLiveButtonLightOrangeRound", lightOrangeHello.getUIID(), + "Accent toggles should keep working after custom CSS is set"); + return true; + } + + private void clickByLabel(String text) { + clickButtonByLabel(text); + waitFor(50); + } + + private Button getPreviewHelloButton() { + Component component = findByName("previewHelloButton"); + assertNotNull(component, "Unable to find preview hello button"); + assertTrue(component instanceof Button, "previewHelloButton should be a Button"); + return (Button) component; + } +} From 339b4cf29ae0846cde8fee47eeeefe1144c182cf Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:32:48 +0200 Subject: [PATCH 15/17] Updated CSS Processing logic --- .../initializr/ui/TemplatePreviewPanel.java | 42 ++++++++++++++----- .../InitializrThemeInteractionTest.java | 19 ++++++++- 2 files changed, 50 insertions(+), 11 deletions(-) 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 6672b87b37..9a1ba217be 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; @@ -16,6 +17,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; @@ -74,6 +76,7 @@ public void showUpdatedLivePreview() { } private Form createBarebonesPreviewForm(ProjectOptions options) { + restoreThemeDefaults(); installBundle(options); Form form = new Form("Hi World", BoxLayout.y()); Button helloButton = new Button("Hello World"); @@ -84,7 +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); - validateCustomCss(options.customThemeCss); + applyCustomCssToPreview(form, options.customThemeCss); lastLiveForm = form; lastLiveHelloButton = helloButton; return form; @@ -162,17 +165,24 @@ private Hashtable loadBundleProperties(String resourcePath) { } } - private void validateCustomCss(String rawCustomCss) { + private void applyCustomCssToPreview(Form form, String rawCustomCss) { String customCss = normalizeCustomCss(rawCustomCss); - if (customCss.length() > 0) { - String wrappedCustomCss = "\n/* Initializr Appended Custom CSS */\n" + customCss + "\n"; - try { - com.codename1.ui.css.CSSThemeCompiler compiler = new com.codename1.ui.css.CSSThemeCompiler(); - com.codename1.ui.util.MutableResource resource = new com.codename1.ui.util.MutableResource(); - compiler.compile(wrappedCustomCss, resource, "InitializrLiveThemeValidation"); - } catch (RuntimeException err) { - throw new IllegalArgumentException(err.getMessage(), err); + if (customCss.length() == 0) { + form.refreshTheme(); + return; + } + String wrappedCustomCss = "\n/* Initializr Appended Custom CSS */\n" + customCss + "\n"; + try { + CSSThemeCompiler compiler = new CSSThemeCompiler(); + MutableResource resource = new MutableResource(); + compiler.compile(wrappedCustomCss, resource, "InitializrLiveThemeCustom"); + Hashtable customTheme = resource.getTheme("InitializrLiveThemeCustom"); + if (customTheme != null && !customTheme.isEmpty()) { + UIManager.getInstance().addThemeProps(customTheme); } + form.refreshTheme(); + } catch (RuntimeException err) { + throw new IllegalArgumentException(err.getMessage(), err); } } @@ -184,6 +194,18 @@ private String normalizeCustomCss(String css) { return trimmed.length() == 0 ? "" : trimmed; } + 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() { if (template == Template.BAREBONES || template == Template.KOTLIN) { Form liveForm = createBarebonesPreviewForm(options); diff --git a/scripts/initializr/common/src/test/java/com/codename1/initializr/InitializrThemeInteractionTest.java b/scripts/initializr/common/src/test/java/com/codename1/initializr/InitializrThemeInteractionTest.java index d039d0185b..f87679ae4d 100644 --- a/scripts/initializr/common/src/test/java/com/codename1/initializr/InitializrThemeInteractionTest.java +++ b/scripts/initializr/common/src/test/java/com/codename1/initializr/InitializrThemeInteractionTest.java @@ -38,15 +38,32 @@ public boolean runTest() throws Exception { assertNotNull(lightTealHello, "Preview button should still exist after switching back to light mode"); assertEqual("InitializrLiveButtonLightTealRound", lightTealHello.getUIID(), "Light mode should update preview button UIID"); - setText("appendCustomCssEditor", "Button { border-radius: 0; }"); + clickByLabel("CLEAN"); + Button cleanBeforeCustom = getPreviewHelloButton(); + int baselineCleanBg = cleanBeforeCustom.getUnselectedStyle().getBgColor(); + assertNotEqual(0x010203, baselineCleanBg, "Baseline clean mode color should differ from custom CSS probe color"); + + setText("appendCustomCssEditor", + "Button { background-color: #010203; color: #ffffff; }\n" + + "InitializrLiveButtonDarkBlueRound { background-color: #112233; color: #ffffff; }"); waitFor(100); + Button cleanCustomHello = getPreviewHelloButton(); + assertNotNull(cleanCustomHello, "Preview button should exist after custom CSS in clean mode"); + assertEqual("Button", cleanCustomHello.getUIID(), "Clean accent should map to base Button UIID"); + assertNotEqual(baselineCleanBg, cleanCustomHello.getUnselectedStyle().getBgColor(), + "Custom CSS should change preview button color from baseline"); + assertEqual(0x010203, cleanCustomHello.getUnselectedStyle().getBgColor(), + "Custom CSS should apply to preview button style"); + clickByLabel("DARK"); clickByLabel("BLUE"); Button darkBlueHello = getPreviewHelloButton(); assertNotNull(darkBlueHello, "Preview button should exist after applying custom CSS and toggling"); assertEqual("InitializrLiveButtonDarkBlueRound", darkBlueHello.getUIID(), "Mode/accent toggles should still update preview with custom CSS"); + assertEqual(0x112233, darkBlueHello.getUnselectedStyle().getBgColor(), + "Custom CSS should apply to non-clean preview UIID selectors"); clickByLabel("LIGHT"); clickByLabel("ORANGE"); From 2d80ea7a202c5e4053a37e7918359be054ff05bb Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:47:05 +0200 Subject: [PATCH 16/17] Fixed appended CSS --- .../initializr/model/GeneratorModel.java | 63 ++++++++++++++++++- .../initializr/ui/TemplatePreviewPanel.java | 5 +- .../InitializrThemeInteractionTest.java | 9 +++ 3 files changed, 75 insertions(+), 2 deletions(-) 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 7b179ef6c6..b092fdfb04 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 @@ -542,7 +542,68 @@ private static String normalizeCustomCss(String css) { return ""; } String trimmed = css.trim(); - return trimmed.length() == 0 ? "" : trimmed; + if (trimmed.length() == 0) { + return ""; + } + return normalizeCustomCssForCompiler(trimmed); + } + + public static String normalizeCustomCssForCompiler(String css) { + String out = css; + out = replaceKnownNamedColors(out); + out = addAlignFallback(out); + return out; + } + + private static String replaceKnownNamedColors(String css) { + String out = css; + out = replaceCssColorValue(out, "pink", "#ffc0cb"); + out = replaceCssColorValue(out, "orange", "#ffa500"); + out = replaceCssColorValue(out, "purple", "#800080"); + out = replaceCssColorValue(out, "yellow", "#ffff00"); + out = replaceCssColorValue(out, "gray", "#808080"); + out = replaceCssColorValue(out, "grey", "#808080"); + return out; + } + + private static String replaceCssColorValue(String css, String namedColor, String hexColor) { + String out = css; + out = StringUtil.replaceAll(out, ": " + namedColor + ";", ": " + hexColor + ";"); + out = StringUtil.replaceAll(out, ":" + namedColor + ";", ":" + hexColor + ";"); + out = StringUtil.replaceAll(out, ": " + namedColor + " ;", ": " + hexColor + " ;"); + out = StringUtil.replaceAll(out, ":" + namedColor + " ;", ":" + hexColor + " ;"); + out = StringUtil.replaceAll(out, ": " + namedColor.toUpperCase() + ";", ": " + hexColor + ";"); + out = StringUtil.replaceAll(out, ":" + namedColor.toUpperCase() + ";", ":" + hexColor + ";"); + return out; + } + + private static String addAlignFallback(String css) { + String out = css; + int searchFrom = 0; + while (searchFrom < out.length()) { + int idx = indexOfIgnoreCase(out, "text-align", searchFrom); + if (idx < 0) { + break; + } + int colon = out.indexOf(':', idx); + if (colon < 0) { + break; + } + int semi = out.indexOf(';', colon); + if (semi < 0) { + break; + } + String value = out.substring(colon + 1, semi).trim(); + String fallback = "\n align: " + value + ";"; + out = out.substring(0, semi + 1) + fallback + out.substring(semi + 1); + searchFrom = semi + fallback.length() + 1; + } + return out; + } + + private static int indexOfIgnoreCase(String text, String needle, int fromIndex) { + String lowerText = text.toLowerCase(); + return lowerText.indexOf(needle.toLowerCase(), fromIndex); } private static void appendCustomCss(StringBuilder out, String customCss) { 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 9a1ba217be..7835d79830 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 @@ -191,7 +191,10 @@ private String normalizeCustomCss(String css) { return ""; } String trimmed = css.trim(); - return trimmed.length() == 0 ? "" : trimmed; + if (trimmed.length() == 0) { + return ""; + } + return com.codename1.initializr.model.GeneratorModel.normalizeCustomCssForCompiler(trimmed); } private void restoreThemeDefaults() { diff --git a/scripts/initializr/common/src/test/java/com/codename1/initializr/InitializrThemeInteractionTest.java b/scripts/initializr/common/src/test/java/com/codename1/initializr/InitializrThemeInteractionTest.java index f87679ae4d..93647e45bc 100644 --- a/scripts/initializr/common/src/test/java/com/codename1/initializr/InitializrThemeInteractionTest.java +++ b/scripts/initializr/common/src/test/java/com/codename1/initializr/InitializrThemeInteractionTest.java @@ -71,6 +71,15 @@ public boolean runTest() throws Exception { assertNotNull(lightOrangeHello, "Preview button should exist after switching back with custom CSS"); assertEqual("InitializrLiveButtonLightOrangeRound", lightOrangeHello.getUIID(), "Accent toggles should keep working after custom CSS is set"); + + setText("appendCustomCssEditor", "Button { color: pink; text-align: center; }"); + waitFor(100); + clickByLabel("CLEAN"); + Button pinkCenteredHello = getPreviewHelloButton(); + assertEqual(0xffc0cb, pinkCenteredHello.getUnselectedStyle().getFgColor(), + "Named color 'pink' should normalize and apply in preview"); + assertEqual(Component.CENTER, pinkCenteredHello.getUnselectedStyle().getAlignment(), + "text-align: center should apply in preview"); return true; } From 3d1b2874d6b6d80b037e0fce2a3dbceb858e5040 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:58:28 +0200 Subject: [PATCH 17/17] Fixed CSS when setting accents or modes --- .../initializr/model/GeneratorModel.java | 26 +++++++++++++++++++ .../InitializrThemeInteractionTest.java | 16 ++++++++++++ 2 files changed, 42 insertions(+) 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 b092fdfb04..7423e98c98 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 @@ -19,6 +19,22 @@ public class GeneratorModel { private static final String CN1_PLUGIN_VERSION = "7.0.227"; + private static final String PREVIEW_BUTTON_SELECTOR = + "Button, InitializrLiveButtonDarkClean, " + + "InitializrLiveButtonLightTealRound, InitializrLiveButtonLightTealSquare, " + + "InitializrLiveButtonDarkTealRound, InitializrLiveButtonDarkTealSquare, " + + "InitializrLiveButtonLightBlueRound, InitializrLiveButtonLightBlueSquare, " + + "InitializrLiveButtonDarkBlueRound, InitializrLiveButtonDarkBlueSquare, " + + "InitializrLiveButtonLightOrangeRound, InitializrLiveButtonLightOrangeSquare, " + + "InitializrLiveButtonDarkOrangeRound, InitializrLiveButtonDarkOrangeSquare"; + private static final String PREVIEW_BUTTON_PRESSED_SELECTOR = + "Button.pressed, InitializrLiveButtonDarkClean.pressed, " + + "InitializrLiveButtonLightTealRound.pressed, InitializrLiveButtonLightTealSquare.pressed, " + + "InitializrLiveButtonDarkTealRound.pressed, InitializrLiveButtonDarkTealSquare.pressed, " + + "InitializrLiveButtonLightBlueRound.pressed, InitializrLiveButtonLightBlueSquare.pressed, " + + "InitializrLiveButtonDarkBlueRound.pressed, InitializrLiveButtonDarkBlueSquare.pressed, " + + "InitializrLiveButtonLightOrangeRound.pressed, InitializrLiveButtonLightOrangeSquare.pressed, " + + "InitializrLiveButtonDarkOrangeRound.pressed, InitializrLiveButtonDarkOrangeSquare.pressed"; private static final String GENERATED_GITIGNORE = "**/target/\n" + ".idea/\n" + @@ -550,11 +566,21 @@ private static String normalizeCustomCss(String css) { public static String normalizeCustomCssForCompiler(String css) { String out = css; + out = expandPreviewButtonAliases(out); out = replaceKnownNamedColors(out); out = addAlignFallback(out); return out; } + private static String expandPreviewButtonAliases(String css) { + String out = css; + out = StringUtil.replaceAll(out, "Button.pressed {", PREVIEW_BUTTON_PRESSED_SELECTOR + " {"); + out = StringUtil.replaceAll(out, "Button.pressed{", PREVIEW_BUTTON_PRESSED_SELECTOR + "{"); + out = StringUtil.replaceAll(out, "Button {", PREVIEW_BUTTON_SELECTOR + " {"); + out = StringUtil.replaceAll(out, "Button{", PREVIEW_BUTTON_SELECTOR + "{"); + return out; + } + private static String replaceKnownNamedColors(String css) { String out = css; out = replaceCssColorValue(out, "pink", "#ffc0cb"); diff --git a/scripts/initializr/common/src/test/java/com/codename1/initializr/InitializrThemeInteractionTest.java b/scripts/initializr/common/src/test/java/com/codename1/initializr/InitializrThemeInteractionTest.java index 93647e45bc..72383d4ce3 100644 --- a/scripts/initializr/common/src/test/java/com/codename1/initializr/InitializrThemeInteractionTest.java +++ b/scripts/initializr/common/src/test/java/com/codename1/initializr/InitializrThemeInteractionTest.java @@ -80,6 +80,22 @@ public boolean runTest() throws Exception { "Named color 'pink' should normalize and apply in preview"); assertEqual(Component.CENTER, pinkCenteredHello.getUnselectedStyle().getAlignment(), "text-align: center should apply in preview"); + + clickByLabel("DARK"); + clickByLabel("TEAL"); + Button pinkDarkTealHello = getPreviewHelloButton(); + assertEqual("InitializrLiveButtonDarkTealRound", pinkDarkTealHello.getUIID(), + "Mode/accent toggles should still update UIID after pink custom CSS"); + assertEqual(0xffc0cb, pinkDarkTealHello.getUnselectedStyle().getFgColor(), + "Button selector custom color should apply even in dark/teal mode"); + assertEqual(Component.CENTER, pinkDarkTealHello.getUnselectedStyle().getAlignment(), + "Button selector custom alignment should apply even in dark/teal mode"); + + clickByLabel("LIGHT"); + clickByLabel("CLEAN"); + Button pinkBackToCleanHello = getPreviewHelloButton(); + assertEqual(0xffc0cb, pinkBackToCleanHello.getUnselectedStyle().getFgColor(), + "Button selector custom color should still apply after returning to clean mode"); return true; }