From 429d9150063e244a0f311ba17f103978e997bfb1 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:38:08 +0200 Subject: [PATCH 1/5] Fixed initializr theming (again) --- scripts/initializr/README.adoc | 24 ++++++++- .../initializr/model/GeneratorModel.java | 54 +++++++++++++++---- .../model/GeneratorModelMatrixTest.java | 4 +- scripts/initializr/pom.xml | 16 +++++- scripts/initializr/update-cn1-version.sh | 2 + 5 files changed, 86 insertions(+), 14 deletions(-) diff --git a/scripts/initializr/README.adoc b/scripts/initializr/README.adoc index 3c48268908..2ccc909727 100644 --- a/scripts/initializr/README.adoc +++ b/scripts/initializr/README.adoc @@ -40,4 +40,26 @@ The project should work in IntelliJ out of the box. No need to copy any files. == Help and Support -See the https://www.codenameone.com[Codename One Web Site]. \ No newline at end of file +See the https://www.codenameone.com[Codename One Web Site]. + +== Running Initializr Against Local Codename One Sources + +To test Initializr with local (workspace) Codename One changes instead of Maven Central releases: + +. Build/install local Codename One Maven artifacts: ++ +[source,bash] +---- +cd ../../maven +mvn -DskipTests install +---- + +. Build Initializr with the local workspace profile: ++ +[source,bash] +---- +cd ../scripts/initializr +./mvnw package -Dcodename1.platform=javascript -Dcodename1.buildTarget=javascript -Dcn1.localWorkspace=true +---- + +This switches Initializr to `8.0-SNAPSHOT` so JavaScript builds use your local Codename One code. 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 7423e98c98..f5fb02aba1 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 @@ -18,7 +18,7 @@ import static com.codename1.ui.CN.*; public class GeneratorModel { - private static final String CN1_PLUGIN_VERSION = "7.0.227"; + private static final String CN1_PLUGIN_VERSION = "7.0.228"; private static final String PREVIEW_BUTTON_SELECTOR = "Button, InitializrLiveButtonDarkClean, " + "InitializrLiveButtonLightTealRound, InitializrLiveButtonLightTealSquare, " @@ -593,14 +593,50 @@ private static String replaceKnownNamedColors(String css) { } 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; + StringBuilder out = new StringBuilder(); + int from = 0; + while (from < css.length()) { + int colon = css.indexOf(':', from); + if (colon < 0) { + out.append(css.substring(from)); + break; + } + out.append(css.substring(from, colon + 1)); + int valueStart = colon + 1; + while (valueStart < css.length() && Character.isWhitespace(css.charAt(valueStart))) { + valueStart++; + } + int valueEnd = valueStart + namedColor.length(); + if (matchesIgnoreCase(css, valueStart, namedColor)) { + int semiPos = valueEnd; + while (semiPos < css.length() && Character.isWhitespace(css.charAt(semiPos))) { + semiPos++; + } + if (semiPos < css.length() && css.charAt(semiPos) == ';') { + out.append(css.substring(colon + 1, valueStart)); + out.append(hexColor); + out.append(css.substring(valueEnd, semiPos + 1)); + from = semiPos + 1; + continue; + } + } + from = colon + 1; + } + return out.toString(); + } + + private static boolean matchesIgnoreCase(String text, int start, String token) { + if (start < 0 || start + token.length() > text.length()) { + return false; + } + for (int i = 0; i < token.length(); i++) { + char a = Character.toLowerCase(text.charAt(start + i)); + char b = Character.toLowerCase(token.charAt(i)); + if (a != b) { + return false; + } + } + return true; } private static String addAlignFallback(String css) { 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 b3ae939ca7..bab4df80b4 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 @@ -247,8 +247,8 @@ private void assertRootPom(Map entries, String packageName, Stri String pom = getText(entries, "pom.xml"); assertContains(pom, packageName, "Root pom should include package as groupId"); assertContains(pom, mainClassName.toLowerCase(), "Root pom should include app artifact/name"); - assertContains(pom, "7.0.227", "Root pom should use current CN1 plugin version"); - assertContains(pom, "7.0.227", "Root pom should align CN1 runtime version with plugin version"); + assertContains(pom, "7.0.228", "Root pom should use current CN1 plugin version"); + assertContains(pom, "7.0.228", "Root pom should align CN1 runtime version with plugin version"); assertFalse(pom.indexOf("com.example.myapp") >= 0, "Root pom still contains placeholder package"); assertFalse(pom.indexOf("myappname") >= 0, "Root pom still contains placeholder app name"); } diff --git a/scripts/initializr/pom.xml b/scripts/initializr/pom.xml index 86d6f93e6f..5f42b0fb28 100644 --- a/scripts/initializr/pom.xml +++ b/scripts/initializr/pom.xml @@ -19,8 +19,8 @@ common - 7.0.227 - LATEST + 7.0.228 + ${cn1.version} UTF-8 1.8 11 @@ -109,6 +109,18 @@ + + cn1-local-workspace + + + cn1.localWorkspace + true + + + + 8.0-SNAPSHOT + + javascript diff --git a/scripts/initializr/update-cn1-version.sh b/scripts/initializr/update-cn1-version.sh index 708cc87fe4..010a23cc34 100755 --- a/scripts/initializr/update-cn1-version.sh +++ b/scripts/initializr/update-cn1-version.sh @@ -71,10 +71,12 @@ GENERATOR_MODEL="$ROOT_DIR/scripts/initializr/common/src/main/java/com/codename1 MATRIX_TEST="$ROOT_DIR/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java" while IFS= read -r pom; do replace_file "$pom" "s|[^<]+|$VERSION|g;" + replace_file "$pom" "s|[^<]+|$VERSION|g;" done < <(find "$ROOT_INITIALIZR_DIR" -name pom.xml -type f) replace_file "$GENERATOR_MODEL" "s|private static final String CN1_PLUGIN_VERSION = \\\"[^\\\"]+\\\";|private static final String CN1_PLUGIN_VERSION = \\\"$VERSION\\\";|g;" replace_file "$MATRIX_TEST" "s|[^<]+|$VERSION|g;" +replace_file "$MATRIX_TEST" "s|[^<]+|$VERSION|g;" echo "Updated Initializr Codename One versions to $VERSION" From 5765ac56e2d5288b0ef8a198e62656bb2b934d5f Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:05:19 +0200 Subject: [PATCH 2/5] Added an apply button for testing --- .../com/codename1/initializr/Initializr.java | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 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 1d50691909..9f2c57a6f9 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 @@ -25,6 +25,7 @@ import com.codename1.ui.RadioButton; import com.codename1.ui.TextArea; import com.codename1.ui.TextField; +import com.codename1.ui.events.FocusListener; import com.codename1.ui.layouts.BorderLayout; import com.codename1.ui.layouts.BoxLayout; import com.codename1.ui.layouts.GridLayout; @@ -336,16 +337,39 @@ private Container createThemeOptionsPanel(ProjectOptions.ThemeMode[] selectedThe 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(); + Runnable applyCustomCss = () -> { + String text = cssEditor.getText(); + if (text == null) { + text = ""; + } + if (text.equals(customThemeCss[0])) { + return; + } + customThemeCss[0] = text; onSelectionChanged.run(); + }; + cssEditor.addDataChangedListener((type, index) -> applyCustomCss.run()); + cssEditor.addActionListener(e -> applyCustomCss.run()); + cssEditor.addFocusListener(new FocusListener() { + @Override + public void focusGained(Component cmp) { + } + + @Override + public void focusLost(Component cmp) { + applyCustomCss.run(); + } }); + Button applyCssButton = new Button("Apply CSS"); + applyCssButton.setUIID("InitializrChoice"); + applyCssButton.addActionListener(e -> applyCustomCss.run()); return BoxLayout.encloseY( labeledField("Mode", modeRow), labeledField("Accent", accentRow), rounded, labeledField("Append Custom CSS", cssEditor), + applyCssButton, customCssError ); } From 2af62de60f69768383cf5ef456183795d895c5f6 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:19:42 +0200 Subject: [PATCH 3/5] Moved class to local to make sure changes apply --- .../initializr/css/CSSThemeCompiler.java | 666 ++++++++++++++++++ .../initializr/ui/TemplatePreviewPanel.java | 2 +- 2 files changed, 667 insertions(+), 1 deletion(-) create mode 100644 scripts/initializr/common/src/main/java/com/codename1/initializr/css/CSSThemeCompiler.java diff --git a/scripts/initializr/common/src/main/java/com/codename1/initializr/css/CSSThemeCompiler.java b/scripts/initializr/common/src/main/java/com/codename1/initializr/css/CSSThemeCompiler.java new file mode 100644 index 0000000000..22899ec265 --- /dev/null +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/css/CSSThemeCompiler.java @@ -0,0 +1,666 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + */ +package com.codename1.initializr.css; + +import com.codename1.ui.Component; +import com.codename1.ui.EncodedImage; +import com.codename1.ui.Image; +import com.codename1.ui.plaf.CSSBorder; +import com.codename1.ui.util.MutableResource; +import java.util.ArrayList; +import java.util.Hashtable; + +/// Compiles a subset of Codename One CSS into theme properties stored in a {@link MutableResource}. +/// +/// ## Supported selector syntax +/// +/// - `UIID` +/// - `UIID:selected` +/// - `UIID:pressed` +/// - `UIID:disabled` +/// - `*` (mapped to `Component`) +/// - `:root` (for constants only) +/// +/// ## Supported declarations +/// +/// - `color` +/// - `background-color` +/// - `padding` +/// - `margin` +/// - `font-family` (mapped to `font` string for later resolution) +/// - `cn1-derive` +/// - `cn1-image-id` +/// - `cn1-mutable-image` +/// - border-related properties: `border`, `border-*`, `background-image`, `background-position`, `background-repeat` +/// +/// ## Theme constants +/// +/// - CSS custom property definitions in `:root`, e.g. `--primary: #ff00ff;` +/// - `@constants { name: value; other: value; }` +/// - `var(--name)` dereferencing in declaration values. +public class CSSThemeCompiler { + + 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) { + Hashtable theme = resources.getTheme(themeName); + if (theme == null) { + theme = new Hashtable(); + } + + compileConstants(css, theme); + Rule[] rules = parseRules(css); + for (Rule rule : rules) { + applyRule(theme, resources, rule); + } + resolveThemeConstantVars(theme); + resources.setTheme(themeName, theme); + } + + private void resolveThemeConstantVars(Hashtable theme) { + for (Object keyObj : theme.keySet()) { + String key = String.valueOf(keyObj); + if (!key.startsWith("@")) { + continue; + } + Object value = theme.get(key); + if (!(value instanceof String)) { + continue; + } + theme.put(key, resolveVars(theme, (String) value)); + } + } + + private void compileConstants(String css, Hashtable theme) { + String stripped = stripComments(css); + int constantsStart = stripped.indexOf("@constants"); + if (constantsStart < 0) { + return; + } + int open = stripped.indexOf('{', constantsStart); + if (open < 0) { + return; + } + int close = stripped.indexOf('}', open + 1); + if (close <= open) { + throw new CSSSyntaxException("Unterminated @constants block"); + } + Declaration[] declarations = parseDeclarations(stripped.substring(open + 1, close)); + for (Declaration declaration : declarations) { + theme.put("@" + declaration.property, declaration.value); + } + } + + private void applyRule(Hashtable theme, MutableResource resources, Rule rule) { + if (":root".equals(rule.selector)) { + applyRootDeclarations(theme, rule.declarations); + return; + } + + String[] selectorParts = selector(rule.selector); + String uiid = selectorParts[0]; + String statePrefix = selectorParts[1]; + StringBuilder borderCss = new StringBuilder(); + + for (int i = 0; i < rule.declarations.length; i++) { + Declaration declaration = rule.declarations[i]; + String property = declaration.property; + String value = resolveVars(theme, declaration.value); + + if (applyThemeConstantProperty(theme, property, value)) { + continue; + } + if (applySimpleThemeProperty(theme, uiid, statePrefix, property, value)) { + continue; + } + if (applyImageProperty(theme, resources, uiid, statePrefix, property, value)) { + continue; + } + if (appendBorderProperty(borderCss, property, value)) { + continue; + } + } + + if (borderCss.length() == 0) { + return; + } + theme.put(uiid + "." + statePrefix + "border", new CSSBorder(null, borderCss.toString())); + } + + private void applyRootDeclarations(Hashtable theme, Declaration[] declarations) { + for (Declaration declaration : declarations) { + if (!declaration.property.startsWith("--")) { + continue; + } + theme.put("@" + declaration.property.substring(2), resolveVars(theme, declaration.value)); + } + } + + private boolean applyThemeConstantProperty(Hashtable theme, String property, String value) { + if (!property.startsWith("--")) { + return false; + } + theme.put("@" + property.substring(2), value); + return true; + } + + private boolean applySimpleThemeProperty(Hashtable theme, String uiid, String statePrefix, String property, String value) { + if ("color".equals(property)) { + theme.put(uiid + "." + statePrefix + "fgColor", normalizeHexColor(value)); + return true; + } + if ("background-color".equals(property)) { + theme.put(uiid + "." + statePrefix + "bgColor", normalizeHexColor(value)); + if (!"transparent".equalsIgnoreCase(value)) { + theme.put(uiid + "." + statePrefix + "transparency", "255"); + } + return true; + } + if ("padding".equals(property) || "margin".equals(property)) { + theme.put(uiid + "." + statePrefix + property, normalizeBox(value)); + return true; + } + if ("cn1-derive".equals(property)) { + theme.put(uiid + "." + statePrefix + "derive", value); + return true; + } + if ("font-family".equals(property)) { + theme.put(uiid + "." + statePrefix + "font", value); + return true; + } + if ("text-align".equals(property)) { + Integer align = normalizeAlignment(value); + theme.put(uiid + "." + statePrefix + "align", align); + return true; + } + return false; + } + + private boolean applyImageProperty(Hashtable theme, MutableResource resources, String uiid, String statePrefix, String property, String value) { + if ("cn1-image-id".equals(property)) { + Image image = resources.getImage(value); + if (image == null) { + return true; + } + theme.put(uiid + "." + statePrefix + "bgImage", image); + return true; + } + if (!"cn1-mutable-image".equals(property)) { + return false; + } + String[] parts = splitOnWhitespace(value); + if (parts.length < 2) { + return true; + } + String imageId = parts[0]; + Image image = createSolidImage(parts[1]); + resources.setImage(imageId, image); + theme.put(uiid + "." + statePrefix + "bgImage", image); + return true; + } + + private boolean appendBorderProperty(StringBuilder borderCss, String property, String value) { + 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(';'); + } + borderCss.append(property).append(':').append(value); + 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(--"); + while (varPos > -1) { + int end = out.indexOf(')', varPos); + if (end < 0) { + break; + } + String key = out.substring(varPos + "var(--".length(), end).trim(); + Object replacement = theme.get("@" + key); + String replaceValue = replacement == null ? "" : replacement.toString(); + out = out.substring(0, varPos) + replaceValue + out.substring(end + 1); + varPos = out.indexOf("var(--"); + } + return out; + } + + private String[] selector(String selector) { + String statePrefix = ""; + String uiid = selector.trim(); + + int pseudoPos = uiid.indexOf(':'); + 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) { + uiid = "Component"; + } + return new String[]{uiid, statePrefix}; + } + + private String statePrefix(String pseudo) { + if ("selected".equals(pseudo)) { + return "sel#"; + } + if ("pressed".equals(pseudo)) { + return "press#"; + } + if ("disabled".equals(pseudo)) { + return "dis#"; + } + throw new CSSSyntaxException("Unsupported pseudo state: " + pseudo); + } + + private Image createSolidImage(String color) { + int rgb = parseColor(color); + return EncodedImage.createFromRGB(new int[]{rgb}, 1, 1, false); + } + + private int parseColor(String cssColor) { + String hex = normalizeHexColor(cssColor); + return Integer.parseInt(hex, 16) | 0xff000000; + } + + private boolean isBorderProperty(String property) { + return "border".equals(property) + || property.startsWith("border-") + || property.startsWith("background-image") + || property.startsWith("background-position") + || property.startsWith("background-repeat"); + } + + private String normalizeHexColor(String cssColor) { + 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); + } + if (value.length() == 3) { + value = "" + value.charAt(0) + value.charAt(0) + + value.charAt(1) + value.charAt(1) + + value.charAt(2) + value.charAt(2); + } + if (value.length() != 6 || !isHexColor(value)) { + throw new CSSSyntaxException("Unsupported color value: " + cssColor); + } + return value; + } + + private Integer normalizeAlignment(String value) { + String v = value == null ? "" : value.trim().toLowerCase(); + if ("left".equals(v) || "start".equals(v)) { + return Integer.valueOf(Component.LEFT); + } + if ("center".equals(v)) { + return Integer.valueOf(Component.CENTER); + } + if ("right".equals(v) || "end".equals(v)) { + return Integer.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, err); + } + 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) { + String[] parts = splitOnWhitespace(cssValue.trim()); + if (parts.length == 1) { + return scalar(parts[0]) + "," + scalar(parts[0]) + "," + scalar(parts[0]) + "," + scalar(parts[0]); + } + if (parts.length == 2) { + return scalar(parts[0]) + "," + scalar(parts[1]) + "," + scalar(parts[0]) + "," + scalar(parts[1]); + } + if (parts.length == 3) { + return scalar(parts[0]) + "," + scalar(parts[1]) + "," + scalar(parts[2]) + "," + scalar(parts[1]); + } + if (parts.length >= 4) { + return scalar(parts[0]) + "," + scalar(parts[1]) + "," + scalar(parts[2]) + "," + scalar(parts[3]); + } + return "0,0,0,0"; + } + + private String scalar(String value) { + String out = value.trim(); + if (out.endsWith("px")) { + out = out.substring(0, out.length() - 2); + } + return out; + } + + private Rule[] parseRules(String css) { + String stripped = stripComments(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) { + throw new CSSSyntaxException("Missing '{' in CSS rule near: " + stripped.substring(pos)); + } + int close = stripped.indexOf('}', open + 1); + if (close < 0) { + 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(); + if (selectors.startsWith("@constants")) { + 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); + String[] selectorsList = splitOnChar(selectors, ','); + for (String selectorEntry : selectorsList) { + String selector = selectorEntry.trim(); + if (selector.length() == 0) { + throw new CSSSyntaxException("Empty selector in selector list: " + selectors); + } + Rule rule = new Rule(); + rule.selector = selector; + rule.declarations = declarations; + out.add(rule); + } + + pos = close + 1; + } + return out.toArray(new Rule[out.size()]); + } + + private String stripComments(String css) { + StringBuilder out = new StringBuilder(); + int i = 0; + while (i < css.length()) { + 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); + i++; + } + return out.toString(); + } + + private Declaration[] parseDeclarations(String body) { + ArrayList out = new ArrayList(); + String[] segments = splitOnChar(body, ';'); + for (String line : segments) { + 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 = 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()]); + } + + private String[] splitOnChar(String input, char delimiter) { + ArrayList out = new ArrayList(); + int start = 0; + for (int i = 0; i < input.length(); i++) { + if (input.charAt(i) != delimiter) { + continue; + } + out.add(input.substring(start, i)); + start = i + 1; + } + out.add(input.substring(start)); + 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(); + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + if (Character.isWhitespace(c)) { + if (token.length() > 0) { + out.add(token.toString()); + token.setLength(0); + } + continue; + } + token.append(c); + } + if (token.length() > 0) { + out.add(token.toString()); + } + return out.toArray(new String[out.size()]); + } + + private static class Rule { + String selector; + Declaration[] declarations; + } + + private static class Declaration { + String property; + String value; + } +} 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 7835d79830..b2fbf012ff 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 @@ -4,9 +4,9 @@ import com.codename1.initializr.model.ProjectOptions; import com.codename1.initializr.model.ProjectOptions.PreviewLanguage; import com.codename1.initializr.model.Template; +import com.codename1.initializr.css.CSSThemeCompiler; 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; From acf047b6a6466b46c452fc26875f08e2663ba415 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:20:45 +0200 Subject: [PATCH 4/5] Another attempt to cleanup the code --- .../com/codename1/initializr/Initializr.java | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 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 9f2c57a6f9..7aeac64131 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 @@ -54,6 +54,7 @@ public void runApp() { 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 TextArea[] customCssEditorRef = new TextArea[1]; final RadioButton[] templateButtons = new RadioButton[Template.values().length]; final SpanLabel summaryLabel = new SpanLabel(); final TemplatePreviewPanel previewPanel = new TemplatePreviewPanel(selectedTemplate[0]); @@ -76,10 +77,15 @@ public void runApp() { final Runnable refresh = new Runnable() { public void run() { + String liveCustomCss = ""; + if (customCssEditorRef[0] != null && customCssEditorRef[0].getText() != null) { + liveCustomCss = customCssEditorRef[0].getText(); + } + customThemeCss[0] = liveCustomCss; ProjectOptions options = new ProjectOptions( selectedThemeMode[0], selectedAccent[0], roundedButtons[0], includeLocalizationBundles[0], previewLanguage[0], javaVersion[0], - customThemeCss[0] + liveCustomCss ); customCssValid[0] = true; customCssError.setText(""); @@ -118,7 +124,7 @@ public void run() { createTemplateSelector(selectedTemplate, templateButtons, refresh) ); final Container idePanel = createIdeSelectorPanel(selectedIde, refresh); - final Container themePanel = createThemeOptionsPanel(selectedThemeMode, selectedAccent, roundedButtons, customThemeCss, customCssError, refresh); + final Container themePanel = createThemeOptionsPanel(selectedThemeMode, selectedAccent, roundedButtons, customThemeCss, customCssEditorRef, customCssError, refresh); final Container localizationPanel = createLocalizationPanel(includeLocalizationBundles, previewLanguage, refresh, previewPanel); final Container javaPanel = createJavaOptionsPanel(javaVersion, refresh); themePanelRef[0] = themePanel; @@ -154,10 +160,14 @@ public void run() { } String appName = appNameField.getText() == null ? "" : appNameField.getText().trim(); String packageName = packageField.getText() == null ? "" : packageField.getText().trim(); + String liveCustomCss = ""; + if (customCssEditorRef[0] != null && customCssEditorRef[0].getText() != null) { + liveCustomCss = customCssEditorRef[0].getText(); + } ProjectOptions options = new ProjectOptions( selectedThemeMode[0], selectedAccent[0], roundedButtons[0], includeLocalizationBundles[0], previewLanguage[0], javaVersion[0], - customThemeCss[0] + liveCustomCss ); GeneratorModel.create(selectedIde[0], selectedTemplate[0], appName, packageName, options).generate(); }); @@ -282,6 +292,7 @@ private Container createThemeOptionsPanel(ProjectOptions.ThemeMode[] selectedThe ProjectOptions.Accent[] selectedAccent, boolean[] roundedButtons, String[] customThemeCss, + TextArea[] customCssEditorRef, Label customCssError, Runnable onSelectionChanged) { Container modeRow = new Container(new GridLayout(1, 2)); @@ -337,19 +348,9 @@ private Container createThemeOptionsPanel(ProjectOptions.ThemeMode[] selectedThe cssEditor.setUIID("InitializrField"); cssEditor.setHint("/* Appended to generated theme.css */\nButton {\n border-radius: 0;\n}"); cssEditor.setGrowByContent(true); - Runnable applyCustomCss = () -> { - String text = cssEditor.getText(); - if (text == null) { - text = ""; - } - if (text.equals(customThemeCss[0])) { - return; - } - customThemeCss[0] = text; - onSelectionChanged.run(); - }; - cssEditor.addDataChangedListener((type, index) -> applyCustomCss.run()); - cssEditor.addActionListener(e -> applyCustomCss.run()); + customCssEditorRef[0] = cssEditor; + cssEditor.addDataChangedListener((type, index) -> onSelectionChanged.run()); + cssEditor.addActionListener(e -> onSelectionChanged.run()); cssEditor.addFocusListener(new FocusListener() { @Override public void focusGained(Component cmp) { @@ -357,19 +358,15 @@ public void focusGained(Component cmp) { @Override public void focusLost(Component cmp) { - applyCustomCss.run(); + onSelectionChanged.run(); } }); - Button applyCssButton = new Button("Apply CSS"); - applyCssButton.setUIID("InitializrChoice"); - applyCssButton.addActionListener(e -> applyCustomCss.run()); return BoxLayout.encloseY( labeledField("Mode", modeRow), labeledField("Accent", accentRow), rounded, labeledField("Append Custom CSS", cssEditor), - applyCssButton, customCssError ); } From eead69b1936bf0f788c091e42e67899be397443e Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:42:35 +0200 Subject: [PATCH 5/5] Improved The process of applying CSS --- .../com/codename1/initializr/Initializr.java | 10 ++++ .../initializr/ui/TemplatePreviewPanel.java | 3 + .../InitializrThemeInteractionTest.java | 21 +++++++ .../ui/TemplatePreviewPanelThemeTest.java | 59 +++++++++++++++++++ 4 files changed, 93 insertions(+) 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 7aeac64131..1aa43b6743 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 @@ -54,6 +54,7 @@ public void runApp() { 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 String[] lastValidCustomThemeCss = new String[]{""}; final TextArea[] customCssEditorRef = new TextArea[1]; final RadioButton[] templateButtons = new RadioButton[Template.values().length]; final SpanLabel summaryLabel = new SpanLabel(); @@ -94,11 +95,20 @@ public void run() { try { previewPanel.setTemplate(selectedTemplate[0]); previewPanel.setOptions(options); + lastValidCustomThemeCss[0] = liveCustomCss; } catch (IllegalArgumentException cssErr) { customCssValid[0] = false; customCssError.setText("Custom CSS Error: " + cssErr.getMessage()); customCssError.setHidden(false); customCssError.setVisible(true); + // Keep the preview responsive to theme toggles while the current CSS is invalid. + ProjectOptions fallbackOptions = new ProjectOptions( + selectedThemeMode[0], selectedAccent[0], roundedButtons[0], + includeLocalizationBundles[0], previewLanguage[0], javaVersion[0], + lastValidCustomThemeCss[0] + ); + previewPanel.setTemplate(selectedTemplate[0]); + previewPanel.setOptions(fallbackOptions); } boolean canCustomizeTheme = supportsLivePreview(selectedTemplate[0]); if (themePanelRef[0] != null) { 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 b2fbf012ff..4071bd9a72 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 @@ -54,6 +54,9 @@ public Component getComponent() { } public void setTemplate(Template template) { + if (this.template == template) { + return; + } this.template = template; updateMode(); } 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 72383d4ce3..e7a0fda072 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 @@ -96,6 +96,27 @@ public boolean runTest() throws Exception { Button pinkBackToCleanHello = getPreviewHelloButton(); assertEqual(0xffc0cb, pinkBackToCleanHello.getUnselectedStyle().getFgColor(), "Button selector custom color should still apply after returning to clean mode"); + + // Invalid intermediate CSS edits should not freeze theme/accent toggles in preview. + setText("appendCustomCssEditor", "Button { color: pink;"); + waitFor(100); + clickByLabel("DARK"); + clickByLabel("ORANGE"); + Button fallbackDarkOrangeHello = getPreviewHelloButton(); + assertEqual("InitializrLiveButtonDarkOrangeRound", fallbackDarkOrangeHello.getUIID(), + "Theme toggles should continue updating while CSS is temporarily invalid"); + assertEqual(0xffc0cb, fallbackDarkOrangeHello.getUnselectedStyle().getFgColor(), + "Preview should retain last valid custom CSS while invalid CSS is being edited"); + + setText("appendCustomCssEditor", "Button { color: orange; }"); + waitFor(100); + clickByLabel("LIGHT"); + clickByLabel("TEAL"); + Button recoveredLightTealHello = getPreviewHelloButton(); + assertEqual("InitializrLiveButtonLightTealRound", recoveredLightTealHello.getUIID(), + "Theme toggles should still work after recovering from invalid CSS"); + assertEqual(0xffa500, recoveredLightTealHello.getUnselectedStyle().getFgColor(), + "Recovered valid CSS should apply after invalid intermediate edit"); return true; } 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 index 04b82cd495..f84d2ea8d1 100644 --- 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 @@ -17,6 +17,8 @@ public boolean shouldExecuteOnEDT() { public boolean runTest() throws Exception { validateModeAndAccentUiidUpdates(); validateThemeTogglesStillApplyWithCustomCss(); + validateRepeatedCustomCssEditsDoNotBreakToggles(); + validateRecoveryAfterInvalidIntermediateCss(); return true; } @@ -66,4 +68,61 @@ private ProjectOptions options(ProjectOptions.ThemeMode mode, ProjectOptions.Acc customCss ); } + + private void validateRepeatedCustomCssEditsDoNotBreakToggles() { + TemplatePreviewPanel panel = new TemplatePreviewPanel(Template.BAREBONES); + + panel.setOptions(options(ProjectOptions.ThemeMode.DARK, ProjectOptions.Accent.DEFAULT, true, + "Button { color: pink; }")); + Button pinkButton = panel.getLastLiveHelloButtonForTesting(); + assertEqual(0xffc0cb, pinkButton.getUnselectedStyle().getFgColor(), + "First custom CSS edit should apply pink color"); + + panel.setOptions(options(ProjectOptions.ThemeMode.DARK, ProjectOptions.Accent.DEFAULT, true, + "Button { color: orange; }")); + Button orangeButton = panel.getLastLiveHelloButtonForTesting(); + assertEqual(0xffa500, orangeButton.getUnselectedStyle().getFgColor(), + "Second custom CSS edit should replace previous color"); + assertEqual("InitializrLiveButtonDarkClean", orangeButton.getUIID(), + "Dark clean UIID should remain stable after repeated custom CSS edits"); + + panel.setOptions(options(ProjectOptions.ThemeMode.LIGHT, ProjectOptions.Accent.TEAL, true, + "Button { color: orange; }")); + Button lightTealButton = panel.getLastLiveHelloButtonForTesting(); + assertEqual("InitializrLiveButtonLightTealRound", lightTealButton.getUIID(), + "Theme toggles should keep updating UIID after repeated CSS edits"); + assertEqual(0xffa500, lightTealButton.getUnselectedStyle().getFgColor(), + "Updated custom CSS should persist across theme toggles"); + + panel.setOptions(options(ProjectOptions.ThemeMode.LIGHT, ProjectOptions.Accent.TEAL, true, "")); + Button clearedCssButton = panel.getLastLiveHelloButtonForTesting(); + assertNotEqual(0xffa500, clearedCssButton.getUnselectedStyle().getFgColor(), + "Clearing custom CSS should remove previous custom fgColor"); + } + + private void validateRecoveryAfterInvalidIntermediateCss() { + TemplatePreviewPanel panel = new TemplatePreviewPanel(Template.BAREBONES); + + panel.setOptions(options(ProjectOptions.ThemeMode.DARK, ProjectOptions.Accent.DEFAULT, true, + "Button { color: pink; }")); + Button pinkButton = panel.getLastLiveHelloButtonForTesting(); + assertEqual(0xffc0cb, pinkButton.getUnselectedStyle().getFgColor(), + "Known-good CSS should apply before invalid intermediate edit"); + + try { + panel.setOptions(options(ProjectOptions.ThemeMode.DARK, ProjectOptions.Accent.DEFAULT, true, + "Button { color: pink;")); + fail("Invalid custom CSS should fail fast"); + } catch (IllegalArgumentException expected) { + // expected + } + + panel.setOptions(options(ProjectOptions.ThemeMode.LIGHT, ProjectOptions.Accent.ORANGE, true, + "Button { color: orange; }")); + Button recoveredButton = panel.getLastLiveHelloButtonForTesting(); + assertEqual("InitializrLiveButtonLightOrangeRound", recoveredButton.getUIID(), + "Preview should recover after invalid intermediate CSS edit"); + assertEqual(0xffa500, recoveredButton.getUnselectedStyle().getFgColor(), + "Recovered valid CSS should apply after invalid edit"); + } }