diff --git a/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java b/CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java index 7a5065bd83..1e02408714 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; @@ -40,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) { @@ -81,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) { @@ -166,6 +173,11 @@ 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); + theme.put(uiid + "." + statePrefix + "align", align); + return true; + } return false; } @@ -267,10 +279,33 @@ private boolean isBorderProperty(String property) { } private String normalizeHexColor(String cssColor) { - String value = cssColor.trim(); - if ("transparent".equalsIgnoreCase(value)) { + 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(")) { + if (!value.endsWith(")")) { + throw new CSSSyntaxException("Malformed rgb() color: " + cssColor); + } + String[] parts = splitOnComma(value.substring(4, value.length() - 1)); + 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); + if (keyword != null) { + return keyword; + } + if (value.startsWith("#")) { value = value.substring(1); } @@ -279,7 +314,92 @@ private String normalizeHexColor(String cssColor) { + value.charAt(1) + value.charAt(1) + value.charAt(2) + value.charAt(2); } - return value.toLowerCase(); + if (value.length() != 6 || !isHexColor(value)) { + throw new CSSSyntaxException("Unsupported color value: " + cssColor); + } + 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); + } + throw new CSSSyntaxException("Unsupported text-align value: " + 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"; + } + return null; + } + + private int parseRgbChannel(String value, String originalColor) { + int out; + try { + out = Integer.parseInt(value.trim()); + } catch (RuntimeException err) { + 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) { + String hex = Integer.toHexString(color & 0xffffff); + while (hex.length() < 6) { + hex = "0" + hex; + } + return hex; } private String normalizeBox(String cssValue) { @@ -312,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(); @@ -326,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); @@ -333,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; @@ -353,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); @@ -372,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()]); @@ -398,6 +542,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..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 @@ -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; @@ -10,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 { @@ -23,13 +25,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,10 +41,24 @@ 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"); 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 87133bcc8a..739e421643 100644 --- a/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/Initializr.java @@ -23,6 +23,8 @@ import com.codename1.ui.Form; 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; @@ -51,10 +53,13 @@ public void runApp() { final boolean[] includeLocalizationBundles = new boolean[]{true}; final ProjectOptions.PreviewLanguage[] previewLanguage = new ProjectOptions.PreviewLanguage[]{ProjectOptions.PreviewLanguage.ENGLISH}; final ProjectOptions.JavaVersion[] javaVersion = new ProjectOptions.JavaVersion[]{ProjectOptions.JavaVersion.JAVA_8}; + final String[] customThemeCss = new String[]{""}; final RadioButton[] templateButtons = new RadioButton[Template.values().length]; 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"); @@ -65,15 +70,30 @@ 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() { ProjectOptions options = new ProjectOptions( selectedThemeMode[0], selectedAccent[0], roundedButtons[0], - includeLocalizationBundles[0], previewLanguage[0], javaVersion[0] + 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); @@ -98,7 +118,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, customThemeCss, customCssError, refresh); final Container localizationPanel = createLocalizationPanel(includeLocalizationBundles, previewLanguage, refresh, previewPanel); final Container javaPanel = createJavaOptionsPanel(javaVersion, refresh); themePanelRef[0] = themePanel; @@ -126,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; } @@ -136,7 +156,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], + customThemeCss[0] ); GeneratorModel.create(selectedIde[0], selectedTemplate[0], appName, packageName, options).generate(); }); @@ -261,6 +282,8 @@ private Container createIdeSelectorPanel(IDE[] selectedIde, Runnable onSelection private Container createThemeOptionsPanel(ProjectOptions.ThemeMode[] selectedThemeMode, ProjectOptions.Accent[] selectedAccent, boolean[] roundedButtons, + String[] customThemeCss, + Label customCssError, Runnable onSelectionChanged) { Container modeRow = new Container(new GridLayout(1, 2)); modeRow.setUIID("InitializrChoicesGrid"); @@ -310,10 +333,21 @@ private Container createThemeOptionsPanel(ProjectOptions.ThemeMode[] selectedThe onSelectionChanged.run(); }); + TextArea cssEditor = new TextArea(customThemeCss[0], 8, 30); + cssEditor.setUIID("InitializrField"); + cssEditor.setHint("/* Appended to generated theme.css */\nButton {\n border-radius: 0;\n}"); + cssEditor.setGrowByContent(true); + cssEditor.addDataChangedListener((type, index) -> { + customThemeCss[0] = cssEditor.getText(); + onSelectionChanged.run(); + }); + return BoxLayout.encloseY( labeledField("Mode", modeRow), labeledField("Accent", accentRow), - rounded + rounded, + labeledField("Append Custom CSS", cssEditor), + customCssError ); } @@ -538,6 +572,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" + + "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 e0cf3fa54d..d9d722597e 100644 --- a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java @@ -185,7 +185,7 @@ private byte[] applyDataReplacements(String targetPath, byte[] sourceData) throw content = injectLocalizationBootstrap(targetPath, content); } if (isBareTemplate() && "common/src/main/css/theme.css".equals(targetPath)) { - content += buildThemeOverrides(); + content += buildThemeCss(); } if ("common/pom.xml".equals(targetPath)) { content = applyJavaVersionToPom(content); @@ -366,13 +366,17 @@ private void appendIdeSection(StringBuilder out) { .append("Open the project folder in VS Code and make sure Java + Maven extensions are installed.\n\n"); } - private String buildThemeOverrides() { - if (isDefaultBarebonesOptions()) { + public static String buildThemeOverrides(ProjectOptions options) { + ProjectOptions effective = options == null ? ProjectOptions.defaults() : options; + if (effective.customThemeCss != null && effective.customThemeCss.trim().length() > 0) { + return "\n\n/* Initializr Appended Custom CSS */\n" + effective.customThemeCss + "\n"; + } + if (isDefaultBarebonesOptions(effective)) { return ""; } StringBuilder out = new StringBuilder("\n\n/* Initializr Theme Overrides */\n"); - if (options.themeMode == ProjectOptions.ThemeMode.DARK) { + if (effective.themeMode == ProjectOptions.ThemeMode.DARK) { out.append("Form {\n") .append(" background-color: #0f172a;\n") .append(" color: #e2e8f0;\n") @@ -388,7 +392,7 @@ private String buildThemeOverrides() { .append(" color: #e2e8f0;\n") .append("}\n"); - if (options.accent == ProjectOptions.Accent.DEFAULT) { + if (effective.accent == ProjectOptions.Accent.DEFAULT) { out.append("Button {\n") .append(" color: #e2e8f0;\n") .append(" background-color: #1f2937;\n") @@ -401,14 +405,14 @@ private String buildThemeOverrides() { .append("}\n"); return out.toString(); } - } else if (options.accent == ProjectOptions.Accent.DEFAULT) { + } else if (effective.accent == ProjectOptions.Accent.DEFAULT) { // Light + Clean intentionally inherits template defaults (rounded ignored). return ""; } - int accent = resolveAccentColor(); + int accent = resolveAccentColor(effective); int accentPressed = darkenColor(accent, 0.22f); - String buttonRadius = options.roundedButtons ? "3mm" : "0"; + String buttonRadius = effective.roundedButtons ? "3mm" : "0"; out.append("Button {\n") .append(" background-color: ").append(toCssColor(accent)).append(";\n") .append(" color: #ffffff;\n") @@ -424,12 +428,12 @@ private String buildThemeOverrides() { return out.toString(); } - private boolean isDefaultBarebonesOptions() { + private static boolean isDefaultBarebonesOptions(ProjectOptions options) { return options.themeMode == ProjectOptions.ThemeMode.LIGHT && options.accent == ProjectOptions.Accent.DEFAULT; } - private int resolveAccentColor() { + private static int resolveAccentColor(ProjectOptions options) { if (options.accent == ProjectOptions.Accent.DEFAULT) { return 0x0f766e; } @@ -442,6 +446,11 @@ private int resolveAccentColor() { return 0x0f766e; } + + private String buildThemeCss() { + return buildThemeOverrides(options); + } + private static String toCssColor(int color) { String hex = Integer.toHexString(color & 0xffffff); while (hex.length() < 6) { diff --git a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/ProjectOptions.java b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/ProjectOptions.java index d33361ec9d..a84602ab43 100644 --- a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/ProjectOptions.java +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/ProjectOptions.java @@ -65,16 +65,24 @@ public String toString() { public final boolean includeLocalizationBundles; public final PreviewLanguage previewLanguage; public final JavaVersion javaVersion; + public final String customThemeCss; public ProjectOptions(ThemeMode themeMode, Accent accent, boolean roundedButtons, boolean includeLocalizationBundles, PreviewLanguage previewLanguage, JavaVersion javaVersion) { + this(themeMode, accent, roundedButtons, includeLocalizationBundles, previewLanguage, javaVersion, null); + } + + public ProjectOptions(ThemeMode themeMode, Accent accent, boolean roundedButtons, + boolean includeLocalizationBundles, PreviewLanguage previewLanguage, + JavaVersion javaVersion, String customThemeCss) { this.themeMode = themeMode; this.accent = accent; this.roundedButtons = roundedButtons; this.includeLocalizationBundles = includeLocalizationBundles; this.previewLanguage = previewLanguage == null ? PreviewLanguage.ENGLISH : previewLanguage; this.javaVersion = javaVersion == null ? JavaVersion.JAVA_8 : javaVersion; + this.customThemeCss = customThemeCss; } public static ProjectOptions defaults() { diff --git a/scripts/initializr/common/src/main/java/com/codename1/initializr/ui/TemplatePreviewPanel.java b/scripts/initializr/common/src/main/java/com/codename1/initializr/ui/TemplatePreviewPanel.java index 74d83ce090..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 @@ -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,16 +155,51 @@ 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) { - 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(); } diff --git a/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java b/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java index fe4c56d105..e10143d464 100644 --- a/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java +++ b/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java @@ -22,10 +22,34 @@ public boolean runTest() throws Exception { } } validateExperimentalJava17Generation(); + validateAppendedCustomCssGeneration(); return true; } + + private void validateAppendedCustomCssGeneration() 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 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 validateExperimentalJava17Generation() throws Exception { String mainClassName = "DemoExperimentalJava17"; String packageName = "com.acme.experimental.java17";