Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions CodenameOne/src/com/codename1/ui/util/Resources.java
Original file line number Diff line number Diff line change
Expand Up @@ -929,7 +929,11 @@ InputStream getUi(String id) {
///
/// Hashtable containing key value pairs for localized data
public Hashtable<String, String> getL10N(String id, String locale) {
return (Hashtable<String, String>) ((Hashtable) resources.get(id)).get(locale);
Hashtable bundles = (Hashtable) resources.get(id);
if (bundles == null) {
return null;
}
return (Hashtable<String, String>) bundles.get(locale);
}

/// Returns an enumration of the locales supported by this resource id
Expand All @@ -942,7 +946,11 @@ public Hashtable<String, String> getL10N(String id, String locale) {
///
/// enumeration of strings containing bundle names
public Enumeration listL10NLocales(String id) {
return ((Hashtable) resources.get(id)).keys();
Hashtable bundles = (Hashtable) resources.get(id);
if (bundles == null) {
return null;
}
return bundles.keys();
}

/// Returns a collection of the l10 locale names
Expand All @@ -955,7 +963,11 @@ public Enumeration listL10NLocales(String id) {
///
/// collection of strings containing bundle names
public Collection<String> l10NLocaleSet(String id) {
return ((Hashtable<String, String>) resources.get(id)).keySet();
Hashtable<String, String> bundles = (Hashtable<String, String>) resources.get(id);
if (bundles == null) {
return null;
}
return bundles.keySet();
}

/// Returns the font resource from the file
Expand Down
16 changes: 16 additions & 0 deletions Ports/JavaSE/src/com/codename1/impl/javase/CSSWatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,22 @@ public void run() {
File designerJar = new File(cn1Home, "designer_1.jar");
if (System.getProperty("codename1.designer.jar", null) != null) {
designerJar = new File(System.getProperty("codename1.designer.jar", null));
} else {
// The Maven plugin declares codenameone-designer as a plugin dependency, so any
// CN1 mojo invocation has already pulled the version-pinned designer jar into m2.
// Prefer that over ~/.codenameone/designer_1.jar (which is managed by UpdateCodenameOne
// and routinely lags behind the plugin version, producing confusing CSS failures).
File m2Designer = com.codename1.impl.javase.util.MavenUtils.findDesignerJarInM2();
if (m2Designer != null) {
designerJar = m2Designer;
} else if (designerJar.exists()) {
System.err.println("[CSSWatcher] Warning: codename1.designer.jar system property is not set "
+ "and no version-pinned designer was found in the local Maven repository; "
+ "falling back to " + designerJar.getAbsolutePath() + ". This file is "
+ "managed by UpdateCodenameOne and may be older than the CN1 plugin in use. "
+ "If CSS compilation fails, launch the simulator via the Maven cn1:run goal "
+ "(which both fetches the right designer into m2 and pins it via -Dcodename1.designer.jar).");
}
}
String cefDir = System.getProperty("cef.dir", cn1Home + File.separator + "cef");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -580,18 +580,32 @@ private void findScrollableContainers(Container cnt, List<Container> response) {
}

private void editStyle() {
File cn1dir = new File(System.getProperty("user.home"), ".codenameone");
if(!cn1dir.exists()) {
JOptionPane.showMessageDialog(this, "Please open the designer once by opening the theme.res file", "Error Opening Designer", JOptionPane.ERROR_MESSAGE);
return;
// Prefer the version-pinned designer jar that the Maven plugin pulled into m2.
// Fallback to the legacy ~/.codenameone/designer_*.jar files (managed by UpdateCodenameOne).
File resourceEditor = null;
if (System.getProperty("codename1.designer.jar", null) != null) {
resourceEditor = new File(System.getProperty("codename1.designer.jar"));
}
File resourceEditor = new File(cn1dir, "designer_1.jar");
if(!resourceEditor.exists()) {
resourceEditor = new File(cn1dir, "designer.jar");
if (resourceEditor == null || !resourceEditor.exists()) {
File m2Designer = com.codename1.impl.javase.util.MavenUtils.findDesignerJarInM2();
if (m2Designer != null) {
resourceEditor = m2Designer;
}
}
if(!resourceEditor.exists()) {
JOptionPane.showMessageDialog(this, "Please open the designer once by opening the theme.res file", "Error Opening Designer", JOptionPane.ERROR_MESSAGE);
return;
if (resourceEditor == null || !resourceEditor.exists()) {
File cn1dir = new File(System.getProperty("user.home"), ".codenameone");
if(!cn1dir.exists()) {
JOptionPane.showMessageDialog(this, "Please open the designer once by opening the theme.res file", "Error Opening Designer", JOptionPane.ERROR_MESSAGE);
return;
}
resourceEditor = new File(cn1dir, "designer_1.jar");
if(!resourceEditor.exists()) {
resourceEditor = new File(cn1dir, "designer.jar");
}
if(!resourceEditor.exists()) {
JOptionPane.showMessageDialog(this, "Please open the designer once by opening the theme.res file", "Error Opening Designer", JOptionPane.ERROR_MESSAGE);
return;
}
}

File javaBin = new File(System.getProperty("java.home") + File.separator + "bin" + File.separator + "java.exe");
Expand Down
9 changes: 9 additions & 0 deletions Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
Original file line number Diff line number Diff line change
Expand Up @@ -14295,6 +14295,15 @@ public synchronized String get(Object key) {
if (key instanceof String) {
String strKey = (String) key;
if (value == null) {
// Don't auto-fabricate values for meta-keys like @rtl, @im, @im-<name>.
// These are configuration entries that callers (e.g. UIManager.setBundle)
// distinguish from "missing" by checking for null. If we echo the key back
// as the value, setBundle will treat "@im" as a real input-mode descriptor,
// tokenize it, and crash inside parseTextFieldInputMode when the resulting
// token has no '='.
if (strKey.startsWith("@")) {
return null;
}
String autoValue = strKey;
putInternal(strKey, autoValue);
storeEntry(strKey, autoValue, true);
Expand Down
45 changes: 45 additions & 0 deletions Ports/JavaSE/src/com/codename1/impl/javase/util/MavenUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
package com.codename1.impl.javase.util;

import com.codename1.io.Log;
import com.codename1.ui.Display;
import java.io.File;
import java.net.URL;

/**
*
Expand Down Expand Up @@ -63,6 +65,49 @@ public static File findJavac() {
return null;
}

/**
* Locate the codenameone-designer:jar-with-dependencies jar inside the local
* Maven (~/.m2) repository, using the version of the codenameone-core jar that
* is currently loaded into this JVM. Returns null if the running framework is
* not loaded from m2 (e.g. running from a build directory) or if the matching
* designer jar has not been resolved yet.
*
* <p>The Maven plugin declares codenameone-designer as a plugin dependency, so
* any plugin invocation (cn1:run, mvn compile when bound to the css goal, etc.)
* implicitly fetches the matching designer jar into m2. This lookup lets the
* simulator runtime use that exact version even when codename1.designer.jar
* isn't passed as a system property -- avoiding a stale ~/.codenameone/designer_1.jar
* fallback.
*/
public static File findDesignerJarInM2() {
try {
URL location = Display.class.getProtectionDomain().getCodeSource().getLocation();
if (location == null) {
return null;
}
File coreJar = new File(location.toURI());
// Expected layout: <repo>/com/codenameone/codenameone-core/<version>/codenameone-core-<version>.jar
File versionDir = coreJar.getParentFile();
if (versionDir == null) return null;
File coreDir = versionDir.getParentFile();
if (coreDir == null) return null;
File codenameoneGroupDir = coreDir.getParentFile();
if (codenameoneGroupDir == null) return null;
if (!"codenameone-core".equals(coreDir.getName())) {
return null;
}
String version = versionDir.getName();
File designerVersionDir = new File(codenameoneGroupDir, "codenameone-designer" + File.separator + version);
File designer = new File(designerVersionDir, "codenameone-designer-" + version + "-jar-with-dependencies.jar");
if (designer.isFile()) {
return designer;
}
} catch (Throwable t) {
// Best-effort lookup. Any unexpected layout means we can't resolve via m2.
}
return null;
}

public static boolean isRunningInJDK() {
if (!isRunningInJDKChecked) {
isRunningInJDKChecked = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,27 @@
*
* @author shannah
*/
@Mojo(name = "css", defaultPhase = LifecyclePhase.PROCESS_RESOURCES,
@Mojo(name = "css", defaultPhase = LifecyclePhase.PROCESS_RESOURCES,
requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
public class CompileCSSMojo extends AbstractCN1Mojo {

/**
* Override the default DEBUG log level so the forked CSS compiler's stdout
* is visible in normal mvn output. When the CSS subprocess throws (e.g.
* StringIndexOutOfBoundsException in CN1CSSCLI), users currently only see
* the wrapper "An error occurred while compiling the CSS files" message
* with no usable detail unless they re-run with -X.
*
* Routed through createJava() (not the call site) so subclasses that
* override createJava() in tests still get to substitute their recording
* Java task without having to know about the log level.
*/
@Override
public org.apache.tools.ant.taskdefs.Java createJava() {
return createJava(org.apache.maven.doxia.logging.Log.LEVEL_INFO);
}


@Override
protected void executeImpl() throws MojoExecutionException, MojoFailureException {
Expand Down Expand Up @@ -232,11 +248,14 @@ private void executeImpl(String themePrefix) throws MojoExecutionException, Mojo



// Run the CSS compiler which is contained inside the codenameone-designer jar
// Run the CSS compiler which is contained inside the codenameone-designer jar.
// NOTE: The codenameone-designer.jar is a dependency of the codenameone-maven-plugin as
// zip file (which is the designer jar with all dependencies). We use this jar
// rather than the central designer_1.jar located in the user's home directory to make it
// easier to pin to a particular version.
// The Java task is created via createJava() (overridden in this class to use INFO log
// level) so subprocess output -- including stack traces from CN1CSSCLI failures --
// shows up in normal mvn output instead of being hidden at DEBUG.
Java java = createJava();
java.setDir(getCN1ProjectDir());
java.setJar(getDesignerJar());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ void writeProjectZip(OutputStream outputStream) throws IOException {
copyZipEntriesToMap(template.CSS, mergedEntries, ZipEntryType.TEMPLATE_CSS);
copyZipEntriesToMap(template.SOURCE_ZIP, mergedEntries, ZipEntryType.TEMPLATE_SOURCE);
addLocalizationEntries(mergedEntries);
addAutoLocalizationBundleStub(mergedEntries);

try (ZipOutputStream zos = new ZipOutputStream(outputStream)) {
for (Map.Entry<String, byte[]> fileEntry : mergedEntries.entrySet()) {
Expand All @@ -102,12 +103,53 @@ void writeProjectZip(OutputStream outputStream) throws IOException {
}


/**
* Workaround for a bug in shipped Codename One versions (<= 7.0.236) where the
* simulator's AutoLocalizationBundle echoes any missing key back as its own value.
* UIManager.setBundle queries `@im` on every bundle install, gets `"@im"` back from
* the wormhole, tokenizes it, queries `"@im-@im"`, gets `"@im-@im"` back, then
* crashes inside parseTextFieldInputMode on substring(0, indexOf('=')) for a token
* with no `=`. The CSS compiler subprocess (CN1CSSCLI -> Display.init -> JavaSEPort.init
* -> enableAutoLocalizationBundle) hits this on every initializr-generated project.
*
* The proper fix lives in JavaSEPort.AutoLocalizationBundle (don't fabricate values
* for `@`-prefixed meta-keys), but that requires a new framework release. As a
* workaround we ship an empty `Bundle.properties` with `@im=`, which:
* 1. Is preferred by JavaSEPort.findDefaultLocalizationBundleFile over any other
* bundle file in src/main/l10n, so the AutoLocalizationBundle loads it as base.
* 2. Pre-populates `@im=""` in the bundle's underlying Hashtable, so
* AutoLocalizationBundle.get("@im") returns "" (not the fabricated "@im"),
* which has length 0, so setBundle skips the input-mode block entirely.
*
* This is unconditional (added to every generated project) because
* enableAutoLocalizationBundle auto-creates `src/main/l10n` even when the user
* didn't ask for localization bundles, so the crash hits projects without any
* localization too. Remove this stub once the framework fix has shipped and
* cn1.plugin.version is bumped past it.
*/
private void addAutoLocalizationBundleStub(Map<String, byte[]> mergedEntries) throws IOException {
String stub = "# Workaround for the simulator AutoLocalizationBundle @im fabrication crash\n"
+ "# in Codename One <= 7.0.236. Once the framework fix ships, this file can be removed.\n"
+ "# See GeneratorModel.addAutoLocalizationBundleStub for the full story.\n"
+ "@im=\n";
copySingleTextEntryToMap(
"common/src/main/l10n/Bundle.properties",
stub,
mergedEntries,
ZipEntryType.COMMON
);
}

private void addLocalizationEntries(Map<String, byte[]> mergedEntries) throws IOException {
if (!isBareTemplate() || !options.includeLocalizationBundles) {
return;
}
// The Codename One Maven plugin's CSS compiler scans src/main/l10n (or src/main/i18n)
// for *.properties bundles and bakes them into theme.res. If the bundles are placed
// anywhere else (e.g. src/main/resources) they are NOT baked into the resource file
// and Resources.getGlobalResources().getL10N("messages", lang) returns null at runtime.
copySingleTextEntryToMap(
"common/src/main/resources/messages.properties",
"common/src/main/l10n/messages.properties",
readResourceToString("/messages.properties"),
mergedEntries,
ZipEntryType.COMMON
Expand All @@ -117,7 +159,7 @@ private void addLocalizationEntries(Map<String, byte[]> mergedEntries) throws IO
continue;
}
copySingleTextEntryToMap(
"common/src/main/resources/messages_" + language.bundleSuffix + ".properties",
"common/src/main/l10n/messages_" + language.bundleSuffix + ".properties",
readResourceToString("/messages_" + language.bundleSuffix + ".properties"),
mergedEntries,
ZipEntryType.COMMON
Expand Down Expand Up @@ -356,8 +398,14 @@ private String injectJavaLocalizationBootstrap(String content) {
+ " public void init(Object context) {\n"
+ " super.init(context);\n"
+ " String language = L10NManager.getInstance().getLanguage();\n"
+ " Hashtable<String, String> bundle = Resources.getGlobalResources().getL10N(\"messages\", language);\n"
+ " UIManager.getInstance().setBundle(bundle);\n"
+ " Resources global = Resources.getGlobalResources();\n"
+ " Hashtable<String, String> bundle = global == null ? null : global.getL10N(\"messages\", language);\n"
+ " if (bundle == null && global != null) {\n"
+ " bundle = global.getL10N(\"messages\", \"\");\n"
+ " }\n"
+ " if (bundle != null) {\n"
+ " UIManager.getInstance().setBundle(bundle);\n"
+ " }\n"
+ " }\n\n";
int firstBrace = content.indexOf('{');
if (firstBrace > -1) {
Expand All @@ -374,8 +422,14 @@ private String injectKotlinLocalizationBootstrap(String content) {
String method = "\n override fun init(context: Any?) {\n"
+ " super.init(context)\n"
+ " val language = L10NManager.getInstance().language\n"
+ " val bundle: Hashtable<String, String>? = Resources.getGlobalResources().getL10N(\"messages\", language)\n"
+ " UIManager.getInstance().setBundle(bundle)\n"
+ " val global = Resources.getGlobalResources()\n"
+ " var bundle: Hashtable<String, String>? = global?.getL10N(\"messages\", language)\n"
+ " if (bundle == null) {\n"
+ " bundle = global?.getL10N(\"messages\", \"\")\n"
+ " }\n"
+ " if (bundle != null) {\n"
+ " UIManager.getInstance().setBundle(bundle)\n"
+ " }\n"
+ " }\n\n";
int firstBrace = content.indexOf('{');
if (firstBrace > -1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,18 @@

<mkdir dir="${user.home}/.codenameone"/>
<mkdir dir="${project.build.directory}/codenameone/tmpProject"/>
<!-- usetimestamp re-downloads UpdateCodenameOne.jar when the
remote is newer than the local copy; skipexisting="true"
pinned users to whatever stale copy was first downloaded
and led to outdated guibuilder/designer jars. -->
<get src="https://www.codenameone.com/files/updates/UpdateCodenameOne.jar"
dest="${user.home}/UpdateCodenameOne.jar"
skipexisting="true"
usetimestamp="true"
ignoreerrors="true"/>

<get src="https://github.com/codenameone/CodenameOne/raw/refs/heads/master/maven/UpdateCodenameOne.jar"
dest="${user.home}/UpdateCodenameOne.jar"
skipexisting="true"
usetimestamp="true"
ignoreerrors="false"/>
<java jar="${user.home}/UpdateCodenameOne.jar" fork="true">
<arg value="${project.build.directory}/codenameone/tmpProject"/>
Expand Down
Loading
Loading