@@ -86,13 +85,6 @@
radial-gradient(720px 360px at 100% 100%, rgba(52, 168, 196, 0.24), rgba(52, 168, 196, 0) 65%);
}
-.cn1-skindesigner-loader .icon {
- width: 110px;
- height: auto;
- border-radius: 18px;
- box-shadow: 0 10px 28px rgba(0, 0, 0, 0.25);
-}
-
.cn1-loader-ring {
width: 46px;
height: 46px;
From a55dca46a097d073ce483f5d3218be4932978f56 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 16 Apr 2026 06:14:52 +0300
Subject: [PATCH 03/55] Ensure skindesigner app is built and embedded from app
root
---
docs/website/layouts/_default/skindesigner.html | 2 +-
scripts/website/build.sh | 8 ++------
2 files changed, 3 insertions(+), 7 deletions(-)
diff --git a/docs/website/layouts/_default/skindesigner.html b/docs/website/layouts/_default/skindesigner.html
index e91f63055f..9d59f43e4c 100644
--- a/docs/website/layouts/_default/skindesigner.html
+++ b/docs/website/layouts/_default/skindesigner.html
@@ -11,7 +11,7 @@
diff --git a/scripts/website/build.sh b/scripts/website/build.sh
index 15160cd91c..a289b1cd8e 100755
--- a/scripts/website/build.sh
+++ b/scripts/website/build.sh
@@ -22,7 +22,7 @@ WEBSITE_INCLUDE_JAVADOCS="${WEBSITE_INCLUDE_JAVADOCS:-false}"
WEBSITE_INCLUDE_DEVGUIDE="${WEBSITE_INCLUDE_DEVGUIDE:-auto}"
WEBSITE_INCLUDE_INITIALIZR="${WEBSITE_INCLUDE_INITIALIZR:-false}"
WEBSITE_INCLUDE_PLAYGROUND="${WEBSITE_INCLUDE_PLAYGROUND:-false}"
-WEBSITE_INCLUDE_SKINDESIGNER="${WEBSITE_INCLUDE_SKINDESIGNER:-false}"
+WEBSITE_INCLUDE_SKINDESIGNER="${WEBSITE_INCLUDE_SKINDESIGNER:-true}"
WEBSITE_BOOTSTRAP_CN1_SNAPSHOTS="${WEBSITE_BOOTSTRAP_CN1_SNAPSHOTS:-auto}"
WEBSITE_CN1_VERSION="${WEBSITE_CN1_VERSION:-auto}"
CN1_USER="${CN1_USER:-}"
@@ -75,11 +75,7 @@ if [ "${WEBSITE_INCLUDE_PLAYGROUND}" = "auto" ]; then
fi
if [ "${WEBSITE_INCLUDE_SKINDESIGNER}" = "auto" ]; then
- if [ -n "${CN1_USER}" ] && [ -n "${CN1_TOKEN}" ]; then
- WEBSITE_INCLUDE_SKINDESIGNER="true"
- else
- WEBSITE_INCLUDE_SKINDESIGNER="false"
- fi
+ WEBSITE_INCLUDE_SKINDESIGNER="true"
fi
if [ "${WEBSITE_BOOTSTRAP_CN1_SNAPSHOTS}" = "auto" ]; then
From 715fcb9a99cf1c5d2a8b7dbdba6e4f458601228e Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 16 Apr 2026 06:40:36 +0300
Subject: [PATCH 04/55] Bootstrap skindesigner ZipSupport before website JS
build
---
scripts/website/build.sh | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/scripts/website/build.sh b/scripts/website/build.sh
index a289b1cd8e..cf780ce531 100755
--- a/scripts/website/build.sh
+++ b/scripts/website/build.sh
@@ -699,6 +699,14 @@ build_skindesigner_for_site() {
skindesigner_workspace_args+=(-Dcn1.localWorkspace=true)
fi
+ # Ensure attached classifier artifact skindesigner-ZipSupport:jar:common
+ # is present in the local Maven repo before building skindesigner-common.
+ run_skindesigner_mvn -q -U -pl cn1libs/ZipSupport -am \
+ "${skindesigner_workspace_args[@]}" \
+ -DskipTests \
+ -Dcodename1.platform=javascript \
+ install
+
run_skindesigner_mvn -q -U -pl javascript -am \
"${skindesigner_workspace_args[@]}" \
-DskipTests \
From 85d7eedba7cd51e095a1a02e55d1f1dfe2b0721b Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 16 Apr 2026 07:23:19 +0300
Subject: [PATCH 05/55] Prepare skindesigner ZipSupport jar from repository
assets
---
.../tools/sync-zipsupport-from-initializr.sh | 16 ++++++++++++++++
scripts/website/build.sh | 1 +
2 files changed, 17 insertions(+)
create mode 100755 scripts/skindesigner/tools/sync-zipsupport-from-initializr.sh
diff --git a/scripts/skindesigner/tools/sync-zipsupport-from-initializr.sh b/scripts/skindesigner/tools/sync-zipsupport-from-initializr.sh
new file mode 100755
index 0000000000..ecef8c3d37
--- /dev/null
+++ b/scripts/skindesigner/tools/sync-zipsupport-from-initializr.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+set -eu
+
+ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
+INITIALIZR_CN1LIBS_DIR="$ROOT_DIR/../initializr/cn1libs"
+TARGET_CN1LIBS_DIR="$ROOT_DIR/cn1libs"
+
+if [ ! -f "$INITIALIZR_CN1LIBS_DIR/ZipSupport/jars/main.zip" ]; then
+ echo "Missing source file: $INITIALIZR_CN1LIBS_DIR/ZipSupport/jars/main.zip" >&2
+ exit 1
+fi
+
+mkdir -p "$TARGET_CN1LIBS_DIR/ZipSupport/jars"
+cp "$INITIALIZR_CN1LIBS_DIR/ZipSupport/jars/main.zip" "$TARGET_CN1LIBS_DIR/ZipSupport/jars/main.zip"
+
+echo "Prepared skindesigner ZipSupport jars/main.zip from scripts/initializr/cn1libs"
diff --git a/scripts/website/build.sh b/scripts/website/build.sh
index cf780ce531..4beb0f38a7 100755
--- a/scripts/website/build.sh
+++ b/scripts/website/build.sh
@@ -681,6 +681,7 @@ build_skindesigner_for_site() {
echo "Building Skin Designer JavaScript bundle for website..." >&2
(
cd "${REPO_ROOT}/scripts/skindesigner"
+ ./tools/sync-zipsupport-from-initializr.sh
if [ "${WEBSITE_BOOTSTRAP_CN1_SNAPSHOTS}" = "true" ]; then
activate_bootstrapped_java17
fi
From c356f44e3b94ae71d98b9f314352ff8b78b9053a Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 16 Apr 2026 07:48:48 +0300
Subject: [PATCH 06/55] Normalize skindesigner ZipSupport metadata during sync
---
.../tools/sync-zipsupport-from-initializr.sh | 61 +++++++++++++++++--
1 file changed, 57 insertions(+), 4 deletions(-)
diff --git a/scripts/skindesigner/tools/sync-zipsupport-from-initializr.sh b/scripts/skindesigner/tools/sync-zipsupport-from-initializr.sh
index ecef8c3d37..eba00a320d 100755
--- a/scripts/skindesigner/tools/sync-zipsupport-from-initializr.sh
+++ b/scripts/skindesigner/tools/sync-zipsupport-from-initializr.sh
@@ -4,13 +4,66 @@ set -eu
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
INITIALIZR_CN1LIBS_DIR="$ROOT_DIR/../initializr/cn1libs"
TARGET_CN1LIBS_DIR="$ROOT_DIR/cn1libs"
+SOURCE_ZIP="$INITIALIZR_CN1LIBS_DIR/ZipSupport/jars/main.zip"
+TARGET_ZIP="$TARGET_CN1LIBS_DIR/ZipSupport/jars/main.zip"
-if [ ! -f "$INITIALIZR_CN1LIBS_DIR/ZipSupport/jars/main.zip" ]; then
- echo "Missing source file: $INITIALIZR_CN1LIBS_DIR/ZipSupport/jars/main.zip" >&2
+if [ ! -f "$SOURCE_ZIP" ]; then
+ echo "Missing source file: $SOURCE_ZIP" >&2
exit 1
fi
mkdir -p "$TARGET_CN1LIBS_DIR/ZipSupport/jars"
-cp "$INITIALIZR_CN1LIBS_DIR/ZipSupport/jars/main.zip" "$TARGET_CN1LIBS_DIR/ZipSupport/jars/main.zip"
-echo "Prepared skindesigner ZipSupport jars/main.zip from scripts/initializr/cn1libs"
+python3 - "$SOURCE_ZIP" "$TARGET_ZIP" <<'PY'
+import os
+import tempfile
+import zipfile
+import sys
+
+source_zip = sys.argv[1]
+target_zip = sys.argv[2]
+
+old_prefix = "META-INF/codenameone/com.codename1.initializr/initializr-ZipSupport/"
+new_prefix = "META-INF/codenameone/com.codename1.tools.skindesigner/skindesigner-ZipSupport/"
+
+with tempfile.NamedTemporaryFile(delete=False) as tmp:
+ tmp_path = tmp.name
+
+try:
+ with zipfile.ZipFile(source_zip, "r") as src, zipfile.ZipFile(tmp_path, "w") as dst:
+ for info in src.infolist():
+ data = src.read(info.filename)
+ filename = info.filename
+
+ if filename.startswith(old_prefix):
+ filename = new_prefix + filename[len(old_prefix):]
+
+ if filename.endswith("codenameone_library_required.properties"):
+ text = data.decode("utf-8")
+ filtered = []
+ for line in text.splitlines():
+ if line.startswith("codename1.arg.java.version="):
+ continue
+ filtered.append(line)
+ data = ("\n".join(filtered) + "\n").encode("utf-8")
+
+ out = zipfile.ZipInfo(filename)
+ out.date_time = info.date_time
+ out.compress_type = info.compress_type
+ out.external_attr = info.external_attr
+ out.comment = info.comment
+ out.extra = info.extra
+ out.create_system = info.create_system
+ out.create_version = info.create_version
+ out.extract_version = info.extract_version
+ out.flag_bits = info.flag_bits
+ out.internal_attr = info.internal_attr
+ dst.writestr(out, data)
+
+ os.replace(tmp_path, target_zip)
+finally:
+ if os.path.exists(tmp_path):
+ os.unlink(tmp_path)
+PY
+
+echo "Prepared skindesigner ZipSupport jars/main.zip from scripts/initializr/cn1libs with skindesigner metadata"
From 6a9c56dfcccc526b290cf04f458351abde87b6ee Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 16 Apr 2026 09:30:00 +0300
Subject: [PATCH 07/55] Polish Skin Designer UI and wire website dark mode
---
.../common/src/main/css/theme.css | 138 +++++++++++++----
.../tools/skindesigner/SkinDesigner.java | 142 ++++++++++++++++--
.../skindesigner/WebsiteThemeNative.java | 8 +
...ename1_tools_skindesigner_ShouldExecute.js | 21 ---
...1_tools_skindesigner_WebsiteThemeNative.js | 76 ++++++++++
5 files changed, 320 insertions(+), 65 deletions(-)
create mode 100644 scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/WebsiteThemeNative.java
create mode 100644 scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_WebsiteThemeNative.js
diff --git a/scripts/skindesigner/common/src/main/css/theme.css b/scripts/skindesigner/common/src/main/css/theme.css
index 43dcf1f4a7..07c240d0d7 100644
--- a/scripts/skindesigner/common/src/main/css/theme.css
+++ b/scripts/skindesigner/common/src/main/css/theme.css
@@ -1,55 +1,135 @@
-/** Define Theme Constants here */
#Constants {
includeNativeBool: true;
defaultSourceDPIInt: "0";
useLargerTextScaleBool: true;
}
-/** Style for Button class */
-Button {
- font-family: "native:MainLight";
- font-size: 3mm;
+SkinDesignerForm {
+ background: #eef3ff;
+}
+
+SkinDesignerFormDark {
+ background: #0a111d;
+}
+
+SkinDesignerTabsContainer {
+ background: #eef3ff;
+ border: none;
+}
+
+SkinDesignerTabsContainerDark {
+ background: #0a111d;
+ border: none;
}
-/** Style for App Title Bar Text */
-Title {
+Tab {
+ background: #f3f4f7;
+ color: #112247;
+ border: 1px solid #d9dee8;
font-family: "native:MainLight";
- font-size: 6mm;
+ font-size: 2.8mm;
+ text-align: center;
+ margin: 0;
+ padding: 0.8mm;
}
-/** Style for Dialog body */
-DialogBody {
+TabDark {
+ background: #102b66;
+ color: #f5f8ff;
+ border: 1px solid #4c6ea8;
font-family: "native:MainLight";
font-size: 2.8mm;
+ text-align: center;
+ margin: 0;
+ padding: 0.8mm;
+}
+
+TabSelected {
+ background: #ffffff;
+ color: #112247;
+ border: 2px solid #2f6bff;
+}
+
+TabSelectedDark {
+ background: #163575;
+ color: #f5f8ff;
+ border: 2px solid #4d86ff;
+}
+
+TabsContainer {
+ background: #eef3ff;
+}
+
+TabsContainerDark {
+ background: #0a111d;
}
-/** Style for Dialog title bar text */
-DialogTitle {
+SkinDesignerCard {
+ background: #ffffff;
+ border: 1px solid #d9dee8;
+ border-radius: 2mm;
+ margin: 1mm;
+ padding: 1.2mm;
+}
+
+SkinDesignerCardDark {
+ background: #102b66;
+ border: 1px solid #4c6ea8;
+ border-radius: 2mm;
+ margin: 1mm;
+ padding: 1.2mm;
+}
+
+SkinDesignerFieldLabel {
+ color: #112247;
font-family: "native:MainLight";
- font-size: 4.5mm;
+ font-size: 2.8mm;
+ margin: 0;
+ padding: 0.8mm 0.3mm 0.2mm 0.3mm;
+ background: transparent;
}
-/** Style for the side menu */
-SideNavigationPanel {
- background: white;
- padding: 2mm 1mm 1mm 1mm;
+SkinDesignerFieldLabelDark {
+ color: #f5f8ff;
+ font-family: "native:MainLight";
+ font-size: 2.8mm;
+ margin: 0;
+ padding: 0.8mm 0.3mm 0.2mm 0.3mm;
+ background: transparent;
}
-@media platform-ios {
- /** iOS Only styles for side menu. */
- SideNavigationPanel {
- /** Extra top padding to deal with notch on iPhoneX */
- padding: 6mm 1mm 1mm 1mm;
- }
+SkinDesignerField {
+ color: #112247;
+ background: #ffffff;
+ border: 1px solid #d9dee8;
+ padding: 1mm;
+ font-family: "native:MainLight";
+ font-size: 3mm;
}
-/** Style for commands in side menu. */
-SideCommand {
+SkinDesignerFieldDark {
+ color: #f5f8ff;
+ background: #163575;
+ border: 1px solid #4c6ea8;
padding: 1mm;
+ font-family: "native:MainLight";
+ font-size: 3mm;
+}
+
+SkinDesignerActionButton {
+ color: #ffffff;
+ background: #2f6bff;
+ border: none;
+ padding: 0.9mm 1.4mm;
+ font-family: "native:MainLight";
+ font-size: 2.7mm;
+}
+
+SkinDesignerActionButtonDark {
+ color: #f5f8ff;
+ background: #4d86ff;
border: none;
- text-decoration: none;
- color: black;
+ padding: 0.9mm 1.4mm;
font-family: "native:MainLight";
- font-size: 4mm;
- border-bottom: 2px solid #cccccc;
+ font-size: 2.7mm;
}
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index 95fd8546de..c9f6ad2fb6 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -28,6 +28,7 @@
import com.codename1.ui.TextArea;
import com.codename1.ui.TextField;
import com.codename1.ui.Toolbar;
+import com.codename1.ui.UITimer;
import com.codename1.ui.geom.Rectangle;
import com.codename1.ui.layouts.BorderLayout;
import com.codename1.ui.layouts.BoxLayout;
@@ -49,12 +50,16 @@ public class SkinDesigner extends Lifecycle {
private static final String[] NATIVE_THEMES = {"iOS 7+", "iOS 6", "Android 4 +","Android 2.x", "Windows"};
private static final String[] NATIVE_THEME_FILES = {"iOS7Theme.res", "iPhoneTheme.res",
"android_holo_light.res","androidTheme.res", "winTheme.res"};
+ private boolean websiteDarkMode;
@Override
public void runApp() {
Form skinDesignerForm = new Form("Skin Designer", new BorderLayout());
+ skinDesignerForm.setUIID("SkinDesignerForm");
Validator vl = new Validator();
final Tabs details = new Tabs();
+ details.getTabsContainer().setUIID("SkinDesignerTabsContainer");
+ details.getTabsContainer().setScrollableX(false);
Style titleCommand = UIManager.getInstance().getComponentStyle("Command");
ImageSettings imPortrait = createImageSettings("/skin.png", "port", vl);
ImageSettings imLandscape = createImageSettings("/skin_l.png", "lan", vl);
@@ -62,24 +67,28 @@ public void runApp() {
skinDesignerForm.add(BorderLayout.CENTER, details);
Picker nativeTheme = new Picker();
+ nativeTheme.setUIID("SkinDesignerField");
nativeTheme.setStrings(NATIVE_THEMES);
nativeTheme.setSelectedString(NATIVE_THEMES[0]);
nativeTheme.setRenderingPrototype("XXXXXXXXXXXXXXXXXXX");
autoSave(nativeTheme, "nativeTheme");
Picker platformName = new Picker();
+ platformName.setUIID("SkinDesignerField");
platformName.setStrings("ios", "and", "win","rim", "se");
platformName.setSelectedString("ios");
platformName.setRenderingPrototype("XXXX");
autoSave(platformName, "platformName");
OnOffSwitch tablet = new OnOffSwitch();
+ tablet.setUIID("SkinDesignerField");
tablet.setValue(false);
autoSave(tablet, "tablet");
TextField systemFontFamily = new TextField("Helvetica", "System Font Family", 20, TextField.ANY);
TextField proportionalFontFamily = new TextField("Helvetica", "Proportional Font Family", 20, TextField.ANY);
TextField monospaceFontFamily = new TextField("Courier", "Monospace Font Family", 20, TextField.ANY);
+ styleFields(systemFontFamily, proportionalFontFamily, monospaceFontFamily);
autoSave(systemFontFamily, "systemFontFamily");
autoSave(proportionalFontFamily, "proportionalFontFamily");
autoSave(monospaceFontFamily, "monospaceFontFamily");
@@ -87,26 +96,31 @@ public void runApp() {
TextField smallFontSize = new TextField("11", "Small Font Size", 20, TextField.NUMERIC);
TextField mediumFontSize = new TextField("14", "Medium Font Size", 20, TextField.NUMERIC);
TextField largeFontSize = new TextField("20", "Large Font Size", 20, TextField.NUMERIC);
+ styleFields(smallFontSize, mediumFontSize, largeFontSize);
autoSave(smallFontSize, "smallFontSize");
autoSave(mediumFontSize, "mediumFontSize");
autoSave(largeFontSize, "largeFontSize");
TextField pixelRatio = new TextField("6.4173236936575", "Pixel Ratio - pixels per millimeter", 20, TextField.DECIMAL);
+ pixelRatio.setUIID("SkinDesignerField");
autoSave(pixelRatio, "pixelRatio");
Picker overrideNamePrimary = new Picker();
+ overrideNamePrimary.setUIID("SkinDesignerField");
overrideNamePrimary.setStrings("phone", "tablet", "desktop");
overrideNamePrimary.setSelectedString("phone");
overrideNamePrimary.setRenderingPrototype("XXXXXXXX");
autoSave(overrideNamePrimary, "overrideNamePrimary");
Picker overrideNameSecondary = new Picker();
+ overrideNameSecondary.setUIID("SkinDesignerField");
overrideNameSecondary.setStrings("ios", "android", "windows");
overrideNameSecondary.setSelectedString("ios");
overrideNameSecondary.setRenderingPrototype("XXXXXXXX");
autoSave(overrideNameSecondary, "overrideNameSecondary");
Picker overrideNameLast = new Picker();
+ overrideNameLast.setUIID("SkinDesignerField");
overrideNameLast.setStrings("iphone", "ipad", "android-phone", "android-tablet", "desktop");
overrideNameLast.setSelectedString("iphone");
overrideNameLast.setRenderingPrototype("XXXXXXXX");
@@ -114,27 +128,28 @@ public void runApp() {
Container settingsContainer = BoxLayout.encloseY(
- new Label("Native Theme"),
+ labeledFieldTitle("Native Theme"),
nativeTheme,
- new Label("Platform Name"),
+ labeledFieldTitle("Platform Name"),
platformName,
- BorderLayout.center(new Label("Tablet")).add(BorderLayout.EAST, tablet),
- new Label(systemFontFamily.getHint()),
+ BorderLayout.center(labeledFieldTitle("Tablet")).add(BorderLayout.EAST, tablet),
+ labeledFieldTitle(systemFontFamily.getHint()),
systemFontFamily,
- new Label(proportionalFontFamily.getHint()),
+ labeledFieldTitle(proportionalFontFamily.getHint()),
proportionalFontFamily,
new FloatingHint(monospaceFontFamily),
- new Label(smallFontSize.getHint()),
+ labeledFieldTitle(smallFontSize.getHint()),
smallFontSize,
- new Label(mediumFontSize.getHint()),
+ labeledFieldTitle(mediumFontSize.getHint()),
mediumFontSize,
- new Label(largeFontSize.getHint()),
+ labeledFieldTitle(largeFontSize.getHint()),
largeFontSize,
- new Label(pixelRatio.getHint()),
+ labeledFieldTitle(pixelRatio.getHint()),
pixelRatio,
- new Label("Platform Overrides"),
+ labeledFieldTitle("Platform Overrides"),
BoxLayout.encloseX(overrideNamePrimary, overrideNameSecondary, overrideNameLast)
);
+ settingsContainer.setUIID("SkinDesignerCard");
settingsContainer.setScrollableY(true);
Style tab = UIManager.getInstance().getComponentStyle("Tab");
@@ -145,9 +160,9 @@ public void runApp() {
FontImage landscapeIconSel = FontImage.createMaterial(FontImage.MATERIAL_STAY_CURRENT_LANDSCAPE, tabSel, 4.5f);
FontImage settingsIcon = FontImage.createMaterial(FontImage.MATERIAL_SETTINGS, tab, 3.5f);
FontImage settingsIconSel = FontImage.createMaterial(FontImage.MATERIAL_SETTINGS, tabSel, 3.5f);
- details.addTab("Portrait", portraitIcon, imPortrait.getContainer());
- details.addTab("Landscape", landscapeIcon, imLandscape.getContainer());
- details.addTab("Settings", settingsIcon, settingsContainer);
+ details.addTab("", portraitIcon, imPortrait.getContainer());
+ details.addTab("", landscapeIcon, imLandscape.getContainer());
+ details.addTab("", settingsIcon, settingsContainer);
details.setTabSelectedIcon(0, portraitIconSel);
details.setTabSelectedIcon(1, landscapeIconSel);
details.setTabSelectedIcon(2, settingsIconSel);
@@ -209,9 +224,103 @@ public void runApp() {
});
}
+ initWebsiteThemeSync(skinDesignerForm);
skinDesignerForm.show();
}
+ private Label labeledFieldTitle(String text) {
+ Label label = new Label(text);
+ label.setUIID("SkinDesignerFieldLabel");
+ return label;
+ }
+
+ private void styleFields(TextField... fields) {
+ for (TextField field : fields) {
+ field.setUIID("SkinDesignerField");
+ }
+ }
+
+ private void initWebsiteThemeSync(Form form) {
+ WebsiteThemeNative websiteThemeNative = NativeLookup.create(WebsiteThemeNative.class);
+ if (websiteThemeNative == null || !websiteThemeNative.isSupported()) {
+ return;
+ }
+ websiteDarkMode = websiteThemeNative.isDarkMode();
+ Display.getInstance().setDarkMode(websiteDarkMode);
+ applyWebsiteTheme(form, websiteDarkMode);
+ form.refreshTheme();
+ websiteThemeNative.notifyUiReady();
+ UITimer.timer(900, true, form, () -> {
+ boolean dark = websiteThemeNative.isDarkMode();
+ if (dark != websiteDarkMode) {
+ websiteDarkMode = dark;
+ Display.getInstance().setDarkMode(dark);
+ applyWebsiteTheme(form, dark);
+ form.refreshTheme();
+ }
+ });
+ }
+
+ private void applyWebsiteTheme(Container component, boolean dark) {
+ for (int i = 0; i < component.getComponentCount(); i++) {
+ com.codename1.ui.Component child = component.getComponentAt(i);
+ String uiid = child.getUIID();
+ String themed = themedUiid(uiid, dark);
+ if (uiid != null && !uiid.equals(themed)) {
+ child.setUIID(themed);
+ }
+ if (child instanceof Container) {
+ applyWebsiteTheme((Container) child, dark);
+ }
+ }
+ String containerUiid = component.getUIID();
+ String themedContainer = themedUiid(containerUiid, dark);
+ if (containerUiid != null && !containerUiid.equals(themedContainer)) {
+ component.setUIID(themedContainer);
+ }
+ }
+
+ private String themedUiid(String uiid, boolean dark) {
+ if (uiid == null || uiid.length() == 0) {
+ return uiid;
+ }
+ if (dark) {
+ if (uiid.endsWith("Dark")) {
+ return uiid;
+ }
+ switch (uiid) {
+ case "SkinDesignerForm":
+ case "SkinDesignerTabsContainer":
+ case "SkinDesignerCard":
+ case "SkinDesignerField":
+ case "SkinDesignerFieldLabel":
+ case "Tab":
+ case "TabSelected":
+ case "TabsContainer":
+ return uiid + "Dark";
+ default:
+ return uiid;
+ }
+ }
+ if (!uiid.endsWith("Dark")) {
+ return uiid;
+ }
+ String base = uiid.substring(0, uiid.length() - "Dark".length());
+ switch (base) {
+ case "SkinDesignerForm":
+ case "SkinDesignerTabsContainer":
+ case "SkinDesignerCard":
+ case "SkinDesignerField":
+ case "SkinDesignerFieldLabel":
+ case "Tab":
+ case "TabSelected":
+ case "TabsContainer":
+ return base;
+ default:
+ return uiid;
+ }
+ }
+
interface ImageSettings {
Container getContainer();
Image createSkinOverlay();
@@ -257,6 +366,7 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
}
ScaleImageLabel sl = new ScaleImageLabel(img);
Button imagePicker = new Button("Select Image");
+ imagePicker.setUIID("SkinDesignerActionButton");
imagePicker.addActionListener((e) -> {
Display.getInstance().openGallery((ee) -> {
if(ee != null && ee.getSource() != null) {
@@ -284,6 +394,7 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
final TextField screenHeightPixels = new TextField("480", "Height", 8, TextField.NUMERIC);
final TextField screenPositionX = new TextField("40", "X", 8, TextField.NUMERIC);
final TextField screenPositionY = new TextField("40", "Y", 8, TextField.NUMERIC);
+ styleFields(screenWidthPixels, screenHeightPixels, screenPositionX, screenPositionY);
autoSave(screenWidthPixels, prefix + "Width");
autoSave(screenHeightPixels, prefix + "Height");
autoSave(screenPositionX, prefix + "X");
@@ -294,6 +405,7 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
addConstraint(screenPositionY, new NumericConstraint(false, 0, 5000, "Screen position must be a valid integer in the 0-5000 range"));
Button aim = new Button();
+ aim.setUIID("SkinDesignerActionButton");
FontImage.setMaterialIcon(aim, FontImage.MATERIAL_PAN_TOOL);
aim.addActionListener(e ->
@@ -304,11 +416,11 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
screenHeightPixels.getAsInt(1024)));
final Container cnt = BoxLayout.encloseY(imagePicker,
- BorderLayout.center(
- new Label("Screen Position (X/Y/Width/Height)")).
+ BorderLayout.center(labeledFieldTitle("Screen Position (X/Y/Width/Height)")).
add(BorderLayout.EAST, aim),
GridLayout.encloseIn(4, screenPositionX, screenPositionY, screenWidthPixels, screenHeightPixels),
sl);
+ cnt.setUIID("SkinDesignerCard");
cnt.setScrollableY(true);
return new ImageSettings() {
@Override
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/WebsiteThemeNative.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/WebsiteThemeNative.java
new file mode 100644
index 0000000000..0c02cc0637
--- /dev/null
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/WebsiteThemeNative.java
@@ -0,0 +1,8 @@
+package com.codename1.tools.skindesigner;
+
+import com.codename1.system.NativeInterface;
+
+public interface WebsiteThemeNative extends NativeInterface {
+ boolean isDarkMode();
+ void notifyUiReady();
+}
diff --git a/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_ShouldExecute.js b/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_ShouldExecute.js
index 5b82fd091d..02638a1254 100644
--- a/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_ShouldExecute.js
+++ b/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_ShouldExecute.js
@@ -2,32 +2,11 @@
var o = {};
- function notifyUiReady() {
- var sendReady = function() {
- try {
- if (window.parent && window.parent !== window && window.parent.postMessage) {
- window.parent.postMessage({ type: "cn1-skindesigner-ui-ready" }, "*");
- }
- } catch (ignored) {
- // Ignore cross-origin/sandbox restrictions in embedded website mode.
- }
- };
-
- if (window.requestAnimationFrame) {
- window.requestAnimationFrame(function() {
- window.requestAnimationFrame(sendReady);
- });
- } else {
- window.setTimeout(sendReady, 48);
- }
- }
-
o.shouldExecute_ = function(callback) {
callback.complete(true);
};
o.isSupported_ = function(callback) {
- notifyUiReady();
callback.complete(true);
};
diff --git a/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_WebsiteThemeNative.js b/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_WebsiteThemeNative.js
new file mode 100644
index 0000000000..b4cd9b5471
--- /dev/null
+++ b/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_WebsiteThemeNative.js
@@ -0,0 +1,76 @@
+(function(exports){
+
+var o = {};
+
+ function readWebsiteThemePreference() {
+ try {
+ var parentWindow = (window.parent && window.parent !== window) ? window.parent : null;
+ var parentDoc = parentWindow && parentWindow.document ? parentWindow.document : null;
+ var parentBody = parentDoc && parentDoc.body ? parentDoc.body : null;
+ var classes = parentBody && parentBody.classList ? parentBody.classList : null;
+ if (classes) {
+ if (classes.contains("dark") || classes.contains("cn1-skindesigner-dark")) {
+ return true;
+ }
+ if (classes.contains("light") || classes.contains("cn1-skindesigner-light")) {
+ return false;
+ }
+ }
+
+ if (parentWindow && parentWindow.localStorage) {
+ var pref = parentWindow.localStorage.getItem("pref-theme");
+ if (pref === "dark") {
+ return true;
+ }
+ if (pref === "light") {
+ return false;
+ }
+ }
+
+ var mediaWindow = parentWindow || window;
+ if (mediaWindow.matchMedia) {
+ return mediaWindow.matchMedia("(prefers-color-scheme: dark)").matches;
+ }
+ } catch (ignored) {
+ // Ignore parent access failures and fallback below.
+ }
+
+ if (window.matchMedia) {
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
+ }
+
+ return false;
+ }
+
+ o.isDarkMode_ = function(callback) {
+ callback.complete(!!readWebsiteThemePreference());
+ };
+
+ o.notifyUiReady_ = function(callback) {
+ var sendReady = function() {
+ try {
+ if (window.parent && window.parent !== window && window.parent.postMessage) {
+ window.parent.postMessage({ type: "cn1-skindesigner-ui-ready" }, "*");
+ }
+ } catch (ignored) {
+ // Ignore cross-origin or sandbox restrictions.
+ }
+ callback.complete();
+ };
+
+ if (window.requestAnimationFrame) {
+ window.requestAnimationFrame(function() {
+ window.requestAnimationFrame(sendReady);
+ });
+ } else {
+ window.setTimeout(sendReady, 48);
+ }
+ };
+
+ o.isSupported_ = function(callback) {
+ callback.complete(true);
+ };
+
+exports.com_codename1_tools_skindesigner_WebsiteThemeNative = o;
+
+})(cn1_get_native_interfaces());
From a548fc39d9dcf98a88db965591277352bbca3dc6 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 16 Apr 2026 09:55:45 +0300
Subject: [PATCH 08/55] Fix SkinDesigner UITimer import package
---
.../java/com/codename1/tools/skindesigner/SkinDesigner.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index c9f6ad2fb6..2400a90075 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -28,7 +28,6 @@
import com.codename1.ui.TextArea;
import com.codename1.ui.TextField;
import com.codename1.ui.Toolbar;
-import com.codename1.ui.UITimer;
import com.codename1.ui.geom.Rectangle;
import com.codename1.ui.layouts.BorderLayout;
import com.codename1.ui.layouts.BoxLayout;
@@ -37,6 +36,7 @@
import com.codename1.ui.plaf.Style;
import com.codename1.ui.spinner.Picker;
import com.codename1.ui.util.ImageIO;
+import com.codename1.ui.util.UITimer;
import com.codename1.ui.validation.NumericConstraint;
import com.codename1.ui.validation.Validator;
import java.io.ByteArrayOutputStream;
From 9f15dffc6bf423237f1c45ef510b0cdac822208b Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 16 Apr 2026 10:46:23 +0300
Subject: [PATCH 09/55] Remove skindesigner tab overlays and improve dark mode
sync
---
.../common/src/main/css/theme.css | 74 +++++++++++++++++++
.../tools/skindesigner/SkinDesigner.java | 50 ++++++++++++-
...1_tools_skindesigner_WebsiteThemeNative.js | 19 +++++
3 files changed, 142 insertions(+), 1 deletion(-)
diff --git a/scripts/skindesigner/common/src/main/css/theme.css b/scripts/skindesigner/common/src/main/css/theme.css
index 07c240d0d7..7d199a96b9 100644
--- a/scripts/skindesigner/common/src/main/css/theme.css
+++ b/scripts/skindesigner/common/src/main/css/theme.css
@@ -22,6 +22,80 @@ SkinDesignerTabsContainerDark {
border: none;
}
+SkinDesignerTabBar {
+ background: #eef3ff;
+ border: none;
+ padding: 0.4mm;
+}
+
+SkinDesignerTabBarDark {
+ background: #0a111d;
+ border: none;
+ padding: 0.4mm;
+}
+
+SkinDesignerTabButton {
+ background: #f3f4f7;
+ color: #112247;
+ border: 1px solid #d9dee8;
+ margin: 0.2mm;
+ padding: 0.9mm;
+}
+
+SkinDesignerTabButtonDark {
+ background: #102b66;
+ color: #f5f8ff;
+ border: 1px solid #4c6ea8;
+ margin: 0.2mm;
+ padding: 0.9mm;
+}
+
+SkinDesignerTabButtonSelected {
+ background: #ffffff;
+ color: #112247;
+ border: 2px solid #2f6bff;
+ margin: 0.2mm;
+ padding: 0.9mm;
+}
+
+SkinDesignerTabButtonSelectedDark {
+ background: #163575;
+ color: #f5f8ff;
+ border: 2px solid #4d86ff;
+ margin: 0.2mm;
+ padding: 0.9mm;
+}
+
+Toolbar {
+ background: #eef3ff;
+}
+
+ToolbarDark {
+ background: #0a111d;
+}
+
+Title {
+ color: #112247;
+ background: #eef3ff;
+ font-family: "native:MainLight";
+}
+
+TitleDark {
+ color: #f5f8ff;
+ background: #0a111d;
+ font-family: "native:MainLight";
+}
+
+Command {
+ color: #112247;
+ background: transparent;
+}
+
+CommandDark {
+ color: #f5f8ff;
+ background: transparent;
+}
+
Tab {
background: #f3f4f7;
color: #112247;
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index 2400a90075..cb7251c78d 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -20,6 +20,7 @@
import com.codename1.ui.BrowserComponent;
import com.codename1.ui.Button;
import com.codename1.ui.Command;
+import com.codename1.ui.Component;
import com.codename1.ui.Container;
import com.codename1.ui.FontImage;
import com.codename1.ui.Graphics;
@@ -64,6 +65,7 @@ public void runApp() {
ImageSettings imPortrait = createImageSettings("/skin.png", "port", vl);
ImageSettings imLandscape = createImageSettings("/skin_l.png", "lan", vl);
+ details.hideTabs();
skinDesignerForm.add(BorderLayout.CENTER, details);
Picker nativeTheme = new Picker();
@@ -166,6 +168,39 @@ public void runApp() {
details.setTabSelectedIcon(0, portraitIconSel);
details.setTabSelectedIcon(1, landscapeIconSel);
details.setTabSelectedIcon(2, settingsIconSel);
+ Button portraitTab = new Button(portraitIconSel);
+ portraitTab.setUIID("SkinDesignerTabButtonSelected");
+ Button landscapeTab = new Button(landscapeIcon);
+ landscapeTab.setUIID("SkinDesignerTabButton");
+ Button settingsTab = new Button(settingsIcon);
+ settingsTab.setUIID("SkinDesignerTabButton");
+ Container customTabBar = GridLayout.encloseIn(3, portraitTab, landscapeTab, settingsTab);
+ customTabBar.setUIID("SkinDesignerTabBar");
+ skinDesignerForm.add(BorderLayout.NORTH, customTabBar);
+
+ Runnable refreshCustomTabs = () -> {
+ int selectedIndex = details.getSelectedIndex();
+ portraitTab.setIcon(selectedIndex == 0 ? portraitIconSel : portraitIcon);
+ landscapeTab.setIcon(selectedIndex == 1 ? landscapeIconSel : landscapeIcon);
+ settingsTab.setIcon(selectedIndex == 2 ? settingsIconSel : settingsIcon);
+ portraitTab.setUIID(selectedIndex == 0 ? "SkinDesignerTabButtonSelected" : "SkinDesignerTabButton");
+ landscapeTab.setUIID(selectedIndex == 1 ? "SkinDesignerTabButtonSelected" : "SkinDesignerTabButton");
+ settingsTab.setUIID(selectedIndex == 2 ? "SkinDesignerTabButtonSelected" : "SkinDesignerTabButton");
+ portraitTab.getParent().revalidate();
+ };
+ portraitTab.addActionListener(e -> {
+ details.setSelectedIndex(0);
+ refreshCustomTabs.run();
+ });
+ landscapeTab.addActionListener(e -> {
+ details.setSelectedIndex(1);
+ refreshCustomTabs.run();
+ });
+ settingsTab.addActionListener(e -> {
+ details.setSelectedIndex(2);
+ refreshCustomTabs.run();
+ });
+ details.addSelectionListener((oldSelected, newSelected) -> refreshCustomTabs.run());
vl.addConstraint(smallFontSize, new NumericConstraint(false, 5, 400, "Font size must be a valid integer in the 5-400 range")).
addConstraint(mediumFontSize, new NumericConstraint(false, 5, 400, "Font size must be a valid integer in the 5-400 range")).
addConstraint(largeFontSize, new NumericConstraint(false, 5, 400, "Font size must be a valid integer in the 5-400 range")).
@@ -224,6 +259,7 @@ public void runApp() {
});
}
+ refreshCustomTabs.run();
initWebsiteThemeSync(skinDesignerForm);
skinDesignerForm.show();
}
@@ -263,7 +299,7 @@ private void initWebsiteThemeSync(Form form) {
private void applyWebsiteTheme(Container component, boolean dark) {
for (int i = 0; i < component.getComponentCount(); i++) {
- com.codename1.ui.Component child = component.getComponentAt(i);
+ Component child = component.getComponentAt(i);
String uiid = child.getUIID();
String themed = themedUiid(uiid, dark);
if (uiid != null && !uiid.equals(themed)) {
@@ -294,6 +330,12 @@ private String themedUiid(String uiid, boolean dark) {
case "SkinDesignerCard":
case "SkinDesignerField":
case "SkinDesignerFieldLabel":
+ case "SkinDesignerTabBar":
+ case "SkinDesignerTabButton":
+ case "SkinDesignerTabButtonSelected":
+ case "Toolbar":
+ case "Title":
+ case "Command":
case "Tab":
case "TabSelected":
case "TabsContainer":
@@ -312,6 +354,12 @@ private String themedUiid(String uiid, boolean dark) {
case "SkinDesignerCard":
case "SkinDesignerField":
case "SkinDesignerFieldLabel":
+ case "SkinDesignerTabBar":
+ case "SkinDesignerTabButton":
+ case "SkinDesignerTabButtonSelected":
+ case "Toolbar":
+ case "Title":
+ case "Command":
case "Tab":
case "TabSelected":
case "TabsContainer":
diff --git a/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_WebsiteThemeNative.js b/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_WebsiteThemeNative.js
index b4cd9b5471..01d88c0fd4 100644
--- a/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_WebsiteThemeNative.js
+++ b/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_WebsiteThemeNative.js
@@ -7,7 +7,9 @@ var o = {};
var parentWindow = (window.parent && window.parent !== window) ? window.parent : null;
var parentDoc = parentWindow && parentWindow.document ? parentWindow.document : null;
var parentBody = parentDoc && parentDoc.body ? parentDoc.body : null;
+ var parentHtml = parentDoc && parentDoc.documentElement ? parentDoc.documentElement : null;
var classes = parentBody && parentBody.classList ? parentBody.classList : null;
+ var htmlClasses = parentHtml && parentHtml.classList ? parentHtml.classList : null;
if (classes) {
if (classes.contains("dark") || classes.contains("cn1-skindesigner-dark")) {
return true;
@@ -16,6 +18,14 @@ var o = {};
return false;
}
}
+ if (htmlClasses) {
+ if (htmlClasses.contains("dark") || htmlClasses.contains("cn1-skindesigner-dark")) {
+ return true;
+ }
+ if (htmlClasses.contains("light") || htmlClasses.contains("cn1-skindesigner-light")) {
+ return false;
+ }
+ }
if (parentWindow && parentWindow.localStorage) {
var pref = parentWindow.localStorage.getItem("pref-theme");
@@ -26,6 +36,15 @@ var o = {};
return false;
}
}
+ if (window.localStorage) {
+ var localPref = window.localStorage.getItem("pref-theme");
+ if (localPref === "dark") {
+ return true;
+ }
+ if (localPref === "light") {
+ return false;
+ }
+ }
var mediaWindow = parentWindow || window;
if (mediaWindow.matchMedia) {
From 0ec6cdd66bdc69c02f3810110705f6e95f354d90 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 16 Apr 2026 11:02:43 +0300
Subject: [PATCH 10/55] Move skindesigner actions off toolbar and init theme
after show
---
.../tools/skindesigner/SkinDesigner.java | 94 ++++++++++---------
1 file changed, 51 insertions(+), 43 deletions(-)
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index cb7251c78d..50c1bfec9d 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -61,7 +61,6 @@ public void runApp() {
final Tabs details = new Tabs();
details.getTabsContainer().setUIID("SkinDesignerTabsContainer");
details.getTabsContainer().setScrollableX(false);
- Style titleCommand = UIManager.getInstance().getComponentStyle("Command");
ImageSettings imPortrait = createImageSettings("/skin.png", "port", vl);
ImageSettings imLandscape = createImageSettings("/skin_l.png", "lan", vl);
@@ -208,30 +207,10 @@ public void runApp() {
setShowErrorMessageForFocusedComponent(true);
ShouldExecute s = NativeLookup.create(ShouldExecute.class);
- if(s != null && s.isSupported()) {
- Command saveCommand = skinDesignerForm.getToolbar().addCommandToRightBar("",
- FontImage.createMaterial(FontImage.MATERIAL_SAVE, titleCommand), e -> {
- byte[] data = createSkinFile(imPortrait, imLandscape, nativeTheme, platformName, tablet, systemFontFamily,
- proportionalFontFamily, monospaceFontFamily, smallFontSize, mediumFontSize, largeFontSize,
- pixelRatio, overrideNamePrimary, overrideNameSecondary, overrideNameLast);
- if(data != null) {
- FileSystemStorage fs = FileSystemStorage.getInstance();
- try(OutputStream os = fs.openOutputStream(fs.getAppHomePath() + "skin-file.skin")) {
- os.write(data);
- } catch(IOException err) {
- Log.e(err);
- ToastBar.showErrorMessage("Error wring skin file " + err);
- }
- // in the JavaScript port this will trigger the download dialog
- if(s.shouldExecute()) {
- Display.getInstance().execute(fs.getAppHomePath() + "skin-file.skin");
- }
- }
- });
- vl.addSubmitButtons(skinDesignerForm.getToolbar().findCommandComponent(saveCommand));
- }
-
- skinDesignerForm.getToolbar().addMaterialCommandToLeftBar("", FontImage.MATERIAL_HELP, e -> {
+ Button helpAction = new Button("Help");
+ helpAction.setUIID("SkinDesignerActionButton");
+ FontImage.setMaterialIcon(helpAction, FontImage.MATERIAL_HELP);
+ helpAction.addActionListener(e -> {
BrowserComponent help = new BrowserComponent();
help.setURL("jar:///help.html");
Form helpForm = new Form("Help", new BorderLayout());
@@ -240,28 +219,57 @@ public void runApp() {
helpForm.show();
});
- if(Display.getInstance().isNativeShareSupported()) {
- skinDesignerForm.getToolbar().addCommandToRightBar("",
- FontImage.createMaterial(FontImage.MATERIAL_SHARE, titleCommand), e -> {
- byte[] data = createSkinFile(imPortrait, imLandscape, nativeTheme, platformName, tablet, systemFontFamily,
- proportionalFontFamily, monospaceFontFamily, smallFontSize, mediumFontSize, largeFontSize,
- pixelRatio, overrideNamePrimary, overrideNameSecondary, overrideNameLast);
- if(data != null) {
- FileSystemStorage fs = FileSystemStorage.getInstance();
- try(OutputStream os = fs.openOutputStream(fs.getAppHomePath() + "skin-file.skin")) {
- os.write(data);
- } catch(IOException err) {
- Log.e(err);
- ToastBar.showErrorMessage("Error wring skin file " + err);
- }
- Display.getInstance().share(null, fs.getAppHomePath() + "skin-file.skin", "application/vnd.codenameone-skin");
- }
- });
+ Button saveAction = new Button("Save");
+ saveAction.setUIID("SkinDesignerActionButton");
+ FontImage.setMaterialIcon(saveAction, FontImage.MATERIAL_SAVE);
+ saveAction.addActionListener(e -> {
+ byte[] data = createSkinFile(imPortrait, imLandscape, nativeTheme, platformName, tablet, systemFontFamily,
+ proportionalFontFamily, monospaceFontFamily, smallFontSize, mediumFontSize, largeFontSize,
+ pixelRatio, overrideNamePrimary, overrideNameSecondary, overrideNameLast);
+ if(data != null) {
+ FileSystemStorage fs = FileSystemStorage.getInstance();
+ try(OutputStream os = fs.openOutputStream(fs.getAppHomePath() + "skin-file.skin")) {
+ os.write(data);
+ } catch(IOException err) {
+ Log.e(err);
+ ToastBar.showErrorMessage("Error wring skin file " + err);
+ }
+ // in the JavaScript port this will trigger the download dialog
+ if(s != null && s.isSupported() && s.shouldExecute()) {
+ Display.getInstance().execute(fs.getAppHomePath() + "skin-file.skin");
+ }
+ }
+ });
+
+ Button shareAction = new Button("Share");
+ shareAction.setUIID("SkinDesignerActionButton");
+ FontImage.setMaterialIcon(shareAction, FontImage.MATERIAL_SHARE);
+ shareAction.addActionListener(e -> {
+ byte[] data = createSkinFile(imPortrait, imLandscape, nativeTheme, platformName, tablet, systemFontFamily,
+ proportionalFontFamily, monospaceFontFamily, smallFontSize, mediumFontSize, largeFontSize,
+ pixelRatio, overrideNamePrimary, overrideNameSecondary, overrideNameLast);
+ if(data != null) {
+ FileSystemStorage fs = FileSystemStorage.getInstance();
+ try(OutputStream os = fs.openOutputStream(fs.getAppHomePath() + "skin-file.skin")) {
+ os.write(data);
+ } catch(IOException err) {
+ Log.e(err);
+ ToastBar.showErrorMessage("Error wring skin file " + err);
+ }
+ Display.getInstance().share(null, fs.getAppHomePath() + "skin-file.skin", "application/vnd.codenameone-skin");
+ }
+ });
+ if (!Display.getInstance().isNativeShareSupported()) {
+ shareAction.setHidden(true);
+ shareAction.setVisible(false);
}
+ Container actions = GridLayout.encloseIn(3, helpAction, saveAction, shareAction);
+ actions.setUIID("SkinDesignerTabBar");
+ settingsContainer.addComponent(0, actions);
refreshCustomTabs.run();
- initWebsiteThemeSync(skinDesignerForm);
skinDesignerForm.show();
+ initWebsiteThemeSync(skinDesignerForm);
}
private Label labeledFieldTitle(String text) {
From 41dcc6337ec590a31b0f6b561692a956ef13b0c1 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 16 Apr 2026 12:05:03 +0300
Subject: [PATCH 11/55] Restore skindesigner tabs and move theme bridge into
ShouldExecute
---
.../common/codenameone_settings.properties | 1 +
.../tools/skindesigner/ShouldExecute.java | 2 +
.../tools/skindesigner/SkinDesigner.java | 20 ++--
.../skindesigner/WebsiteThemeNative.java | 8 --
...ename1_tools_skindesigner_ShouldExecute.js | 84 ++++++++++++++++
...1_tools_skindesigner_WebsiteThemeNative.js | 95 -------------------
6 files changed, 97 insertions(+), 113 deletions(-)
delete mode 100644 scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/WebsiteThemeNative.java
delete mode 100644 scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_WebsiteThemeNative.js
diff --git a/scripts/skindesigner/common/codenameone_settings.properties b/scripts/skindesigner/common/codenameone_settings.properties
index 8dc6a9c8df..a344a9f269 100644
--- a/scripts/skindesigner/common/codenameone_settings.properties
+++ b/scripts/skindesigner/common/codenameone_settings.properties
@@ -4,6 +4,7 @@ codename1.android.keystorePassword=
codename1.arg.ios.newStorageLocation=true
codename1.arg.ios.NSPhotoLibraryUsageDescription=Some functionality of the application requires access to your photo library
codename1.arg.java.version=17
+codename1.arg.javascript.inject_proxy=false
codename1.cssTheme=true
codename1.displayName=SkinDesigner
codename1.icon=icon.png
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/ShouldExecute.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/ShouldExecute.java
index 3ba8b7bd68..de93ab5aae 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/ShouldExecute.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/ShouldExecute.java
@@ -4,4 +4,6 @@
public interface ShouldExecute extends NativeInterface {
boolean shouldExecute();
+ boolean isDarkMode();
+ void notifyUiReady();
}
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index 50c1bfec9d..9cfd3f329c 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -56,6 +56,7 @@ public class SkinDesigner extends Lifecycle {
@Override
public void runApp() {
Form skinDesignerForm = new Form("Skin Designer", new BorderLayout());
+ skinDesignerForm.setTitle("");
skinDesignerForm.setUIID("SkinDesignerForm");
Validator vl = new Validator();
final Tabs details = new Tabs();
@@ -167,11 +168,11 @@ public void runApp() {
details.setTabSelectedIcon(0, portraitIconSel);
details.setTabSelectedIcon(1, landscapeIconSel);
details.setTabSelectedIcon(2, settingsIconSel);
- Button portraitTab = new Button(portraitIconSel);
+ Button portraitTab = new Button("Portrait", portraitIconSel);
portraitTab.setUIID("SkinDesignerTabButtonSelected");
- Button landscapeTab = new Button(landscapeIcon);
+ Button landscapeTab = new Button("Landscape", landscapeIcon);
landscapeTab.setUIID("SkinDesignerTabButton");
- Button settingsTab = new Button(settingsIcon);
+ Button settingsTab = new Button("Settings", settingsIcon);
settingsTab.setUIID("SkinDesignerTabButton");
Container customTabBar = GridLayout.encloseIn(3, portraitTab, landscapeTab, settingsTab);
customTabBar.setUIID("SkinDesignerTabBar");
@@ -269,7 +270,7 @@ public void runApp() {
refreshCustomTabs.run();
skinDesignerForm.show();
- initWebsiteThemeSync(skinDesignerForm);
+ initWebsiteThemeSync(skinDesignerForm, s);
}
private Label labeledFieldTitle(String text) {
@@ -284,18 +285,17 @@ private void styleFields(TextField... fields) {
}
}
- private void initWebsiteThemeSync(Form form) {
- WebsiteThemeNative websiteThemeNative = NativeLookup.create(WebsiteThemeNative.class);
- if (websiteThemeNative == null || !websiteThemeNative.isSupported()) {
+ private void initWebsiteThemeSync(Form form, ShouldExecute nativeBridge) {
+ if (nativeBridge == null || !nativeBridge.isSupported()) {
return;
}
- websiteDarkMode = websiteThemeNative.isDarkMode();
+ websiteDarkMode = nativeBridge.isDarkMode();
Display.getInstance().setDarkMode(websiteDarkMode);
applyWebsiteTheme(form, websiteDarkMode);
form.refreshTheme();
- websiteThemeNative.notifyUiReady();
+ nativeBridge.notifyUiReady();
UITimer.timer(900, true, form, () -> {
- boolean dark = websiteThemeNative.isDarkMode();
+ boolean dark = nativeBridge.isDarkMode();
if (dark != websiteDarkMode) {
websiteDarkMode = dark;
Display.getInstance().setDarkMode(dark);
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/WebsiteThemeNative.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/WebsiteThemeNative.java
deleted file mode 100644
index 0c02cc0637..0000000000
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/WebsiteThemeNative.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.codename1.tools.skindesigner;
-
-import com.codename1.system.NativeInterface;
-
-public interface WebsiteThemeNative extends NativeInterface {
- boolean isDarkMode();
- void notifyUiReady();
-}
diff --git a/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_ShouldExecute.js b/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_ShouldExecute.js
index 02638a1254..a75500a03d 100644
--- a/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_ShouldExecute.js
+++ b/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_ShouldExecute.js
@@ -2,10 +2,94 @@
var o = {};
+ function readWebsiteThemePreference() {
+ try {
+ var parentWindow = (window.parent && window.parent !== window) ? window.parent : null;
+ var parentDoc = parentWindow && parentWindow.document ? parentWindow.document : null;
+ var parentBody = parentDoc && parentDoc.body ? parentDoc.body : null;
+ var parentHtml = parentDoc && parentDoc.documentElement ? parentDoc.documentElement : null;
+ var classes = parentBody && parentBody.classList ? parentBody.classList : null;
+ var htmlClasses = parentHtml && parentHtml.classList ? parentHtml.classList : null;
+ if (classes) {
+ if (classes.contains("dark") || classes.contains("cn1-skindesigner-dark")) {
+ return true;
+ }
+ if (classes.contains("light") || classes.contains("cn1-skindesigner-light")) {
+ return false;
+ }
+ }
+ if (htmlClasses) {
+ if (htmlClasses.contains("dark") || htmlClasses.contains("cn1-skindesigner-dark")) {
+ return true;
+ }
+ if (htmlClasses.contains("light") || htmlClasses.contains("cn1-skindesigner-light")) {
+ return false;
+ }
+ }
+
+ if (parentWindow && parentWindow.localStorage) {
+ var pref = parentWindow.localStorage.getItem("pref-theme");
+ if (pref === "dark") {
+ return true;
+ }
+ if (pref === "light") {
+ return false;
+ }
+ }
+ if (window.localStorage) {
+ var localPref = window.localStorage.getItem("pref-theme");
+ if (localPref === "dark") {
+ return true;
+ }
+ if (localPref === "light") {
+ return false;
+ }
+ }
+
+ var mediaWindow = parentWindow || window;
+ if (mediaWindow.matchMedia) {
+ return mediaWindow.matchMedia("(prefers-color-scheme: dark)").matches;
+ }
+ } catch (ignored) {
+ // Ignore parent access failures and fallback below.
+ }
+
+ if (window.matchMedia) {
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
+ }
+
+ return false;
+ }
+
o.shouldExecute_ = function(callback) {
callback.complete(true);
};
+ o.isDarkMode_ = function(callback) {
+ callback.complete(!!readWebsiteThemePreference());
+ };
+
+ o.notifyUiReady_ = function(callback) {
+ var sendReady = function() {
+ try {
+ if (window.parent && window.parent !== window && window.parent.postMessage) {
+ window.parent.postMessage({ type: "cn1-skindesigner-ui-ready" }, "*");
+ }
+ } catch (ignored) {
+ // Ignore cross-origin/sandbox restrictions in embedded website mode.
+ }
+ callback.complete();
+ };
+
+ if (window.requestAnimationFrame) {
+ window.requestAnimationFrame(function() {
+ window.requestAnimationFrame(sendReady);
+ });
+ } else {
+ window.setTimeout(sendReady, 48);
+ }
+ };
+
o.isSupported_ = function(callback) {
callback.complete(true);
};
diff --git a/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_WebsiteThemeNative.js b/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_WebsiteThemeNative.js
deleted file mode 100644
index 01d88c0fd4..0000000000
--- a/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_WebsiteThemeNative.js
+++ /dev/null
@@ -1,95 +0,0 @@
-(function(exports){
-
-var o = {};
-
- function readWebsiteThemePreference() {
- try {
- var parentWindow = (window.parent && window.parent !== window) ? window.parent : null;
- var parentDoc = parentWindow && parentWindow.document ? parentWindow.document : null;
- var parentBody = parentDoc && parentDoc.body ? parentDoc.body : null;
- var parentHtml = parentDoc && parentDoc.documentElement ? parentDoc.documentElement : null;
- var classes = parentBody && parentBody.classList ? parentBody.classList : null;
- var htmlClasses = parentHtml && parentHtml.classList ? parentHtml.classList : null;
- if (classes) {
- if (classes.contains("dark") || classes.contains("cn1-skindesigner-dark")) {
- return true;
- }
- if (classes.contains("light") || classes.contains("cn1-skindesigner-light")) {
- return false;
- }
- }
- if (htmlClasses) {
- if (htmlClasses.contains("dark") || htmlClasses.contains("cn1-skindesigner-dark")) {
- return true;
- }
- if (htmlClasses.contains("light") || htmlClasses.contains("cn1-skindesigner-light")) {
- return false;
- }
- }
-
- if (parentWindow && parentWindow.localStorage) {
- var pref = parentWindow.localStorage.getItem("pref-theme");
- if (pref === "dark") {
- return true;
- }
- if (pref === "light") {
- return false;
- }
- }
- if (window.localStorage) {
- var localPref = window.localStorage.getItem("pref-theme");
- if (localPref === "dark") {
- return true;
- }
- if (localPref === "light") {
- return false;
- }
- }
-
- var mediaWindow = parentWindow || window;
- if (mediaWindow.matchMedia) {
- return mediaWindow.matchMedia("(prefers-color-scheme: dark)").matches;
- }
- } catch (ignored) {
- // Ignore parent access failures and fallback below.
- }
-
- if (window.matchMedia) {
- return window.matchMedia("(prefers-color-scheme: dark)").matches;
- }
-
- return false;
- }
-
- o.isDarkMode_ = function(callback) {
- callback.complete(!!readWebsiteThemePreference());
- };
-
- o.notifyUiReady_ = function(callback) {
- var sendReady = function() {
- try {
- if (window.parent && window.parent !== window && window.parent.postMessage) {
- window.parent.postMessage({ type: "cn1-skindesigner-ui-ready" }, "*");
- }
- } catch (ignored) {
- // Ignore cross-origin or sandbox restrictions.
- }
- callback.complete();
- };
-
- if (window.requestAnimationFrame) {
- window.requestAnimationFrame(function() {
- window.requestAnimationFrame(sendReady);
- });
- } else {
- window.setTimeout(sendReady, 48);
- }
- };
-
- o.isSupported_ = function(callback) {
- callback.complete(true);
- };
-
-exports.com_codename1_tools_skindesigner_WebsiteThemeNative = o;
-
-})(cn1_get_native_interfaces());
From 2f0e4b8877eb47c38d534a2e782eddbdd9f196a0 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 16 Apr 2026 20:28:03 +0300
Subject: [PATCH 12/55] Restore built-in skindesigner tabs and harden native
bridge lookup
---
.../tools/skindesigner/SkinDesigner.java | 82 ++++++++-----------
1 file changed, 32 insertions(+), 50 deletions(-)
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index 9cfd3f329c..2573917d21 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -52,6 +52,7 @@ public class SkinDesigner extends Lifecycle {
private static final String[] NATIVE_THEME_FILES = {"iOS7Theme.res", "iPhoneTheme.res",
"android_holo_light.res","androidTheme.res", "winTheme.res"};
private boolean websiteDarkMode;
+ private ShouldExecute nativeBridge;
@Override
public void runApp() {
@@ -65,7 +66,6 @@ public void runApp() {
ImageSettings imPortrait = createImageSettings("/skin.png", "port", vl);
ImageSettings imLandscape = createImageSettings("/skin_l.png", "lan", vl);
- details.hideTabs();
skinDesignerForm.add(BorderLayout.CENTER, details);
Picker nativeTheme = new Picker();
@@ -162,52 +162,18 @@ public void runApp() {
FontImage landscapeIconSel = FontImage.createMaterial(FontImage.MATERIAL_STAY_CURRENT_LANDSCAPE, tabSel, 4.5f);
FontImage settingsIcon = FontImage.createMaterial(FontImage.MATERIAL_SETTINGS, tab, 3.5f);
FontImage settingsIconSel = FontImage.createMaterial(FontImage.MATERIAL_SETTINGS, tabSel, 3.5f);
- details.addTab("", portraitIcon, imPortrait.getContainer());
- details.addTab("", landscapeIcon, imLandscape.getContainer());
- details.addTab("", settingsIcon, settingsContainer);
+ details.addTab("Portrait", portraitIcon, imPortrait.getContainer());
+ details.addTab("Landscape", landscapeIcon, imLandscape.getContainer());
+ details.addTab("Settings", settingsIcon, settingsContainer);
details.setTabSelectedIcon(0, portraitIconSel);
details.setTabSelectedIcon(1, landscapeIconSel);
details.setTabSelectedIcon(2, settingsIconSel);
- Button portraitTab = new Button("Portrait", portraitIconSel);
- portraitTab.setUIID("SkinDesignerTabButtonSelected");
- Button landscapeTab = new Button("Landscape", landscapeIcon);
- landscapeTab.setUIID("SkinDesignerTabButton");
- Button settingsTab = new Button("Settings", settingsIcon);
- settingsTab.setUIID("SkinDesignerTabButton");
- Container customTabBar = GridLayout.encloseIn(3, portraitTab, landscapeTab, settingsTab);
- customTabBar.setUIID("SkinDesignerTabBar");
- skinDesignerForm.add(BorderLayout.NORTH, customTabBar);
-
- Runnable refreshCustomTabs = () -> {
- int selectedIndex = details.getSelectedIndex();
- portraitTab.setIcon(selectedIndex == 0 ? portraitIconSel : portraitIcon);
- landscapeTab.setIcon(selectedIndex == 1 ? landscapeIconSel : landscapeIcon);
- settingsTab.setIcon(selectedIndex == 2 ? settingsIconSel : settingsIcon);
- portraitTab.setUIID(selectedIndex == 0 ? "SkinDesignerTabButtonSelected" : "SkinDesignerTabButton");
- landscapeTab.setUIID(selectedIndex == 1 ? "SkinDesignerTabButtonSelected" : "SkinDesignerTabButton");
- settingsTab.setUIID(selectedIndex == 2 ? "SkinDesignerTabButtonSelected" : "SkinDesignerTabButton");
- portraitTab.getParent().revalidate();
- };
- portraitTab.addActionListener(e -> {
- details.setSelectedIndex(0);
- refreshCustomTabs.run();
- });
- landscapeTab.addActionListener(e -> {
- details.setSelectedIndex(1);
- refreshCustomTabs.run();
- });
- settingsTab.addActionListener(e -> {
- details.setSelectedIndex(2);
- refreshCustomTabs.run();
- });
- details.addSelectionListener((oldSelected, newSelected) -> refreshCustomTabs.run());
vl.addConstraint(smallFontSize, new NumericConstraint(false, 5, 400, "Font size must be a valid integer in the 5-400 range")).
addConstraint(mediumFontSize, new NumericConstraint(false, 5, 400, "Font size must be a valid integer in the 5-400 range")).
addConstraint(largeFontSize, new NumericConstraint(false, 5, 400, "Font size must be a valid integer in the 5-400 range")).
addConstraint(pixelRatio, new NumericConstraint(true, 0.1, 60, "PixelRatio is a positive decimal size in the range of 0.1 to 60")).
setShowErrorMessageForFocusedComponent(true);
- ShouldExecute s = NativeLookup.create(ShouldExecute.class);
Button helpAction = new Button("Help");
helpAction.setUIID("SkinDesignerActionButton");
FontImage.setMaterialIcon(helpAction, FontImage.MATERIAL_HELP);
@@ -236,7 +202,8 @@ public void runApp() {
ToastBar.showErrorMessage("Error wring skin file " + err);
}
// in the JavaScript port this will trigger the download dialog
- if(s != null && s.isSupported() && s.shouldExecute()) {
+ ShouldExecute bridge = resolveNativeBridgeQuietly();
+ if(bridge != null && bridge.isSupported() && bridge.shouldExecute()) {
Display.getInstance().execute(fs.getAppHomePath() + "skin-file.skin");
}
}
@@ -268,9 +235,8 @@ public void runApp() {
actions.setUIID("SkinDesignerTabBar");
settingsContainer.addComponent(0, actions);
- refreshCustomTabs.run();
skinDesignerForm.show();
- initWebsiteThemeSync(skinDesignerForm, s);
+ initWebsiteThemeSync(skinDesignerForm);
}
private Label labeledFieldTitle(String text) {
@@ -285,17 +251,33 @@ private void styleFields(TextField... fields) {
}
}
- private void initWebsiteThemeSync(Form form, ShouldExecute nativeBridge) {
- if (nativeBridge == null || !nativeBridge.isSupported()) {
- return;
+ private ShouldExecute resolveNativeBridgeQuietly() {
+ if (nativeBridge != null) {
+ return nativeBridge;
+ }
+ try {
+ nativeBridge = NativeLookup.create(ShouldExecute.class);
+ } catch (Throwable ignored) {
+ nativeBridge = null;
+ }
+ return nativeBridge;
+ }
+
+ private void initWebsiteThemeSync(Form form) {
+ ShouldExecute bridge = resolveNativeBridgeQuietly();
+ if (bridge != null && bridge.isSupported()) {
+ websiteDarkMode = bridge.isDarkMode();
+ Display.getInstance().setDarkMode(websiteDarkMode);
+ applyWebsiteTheme(form, websiteDarkMode);
+ form.refreshTheme();
+ bridge.notifyUiReady();
}
- websiteDarkMode = nativeBridge.isDarkMode();
- Display.getInstance().setDarkMode(websiteDarkMode);
- applyWebsiteTheme(form, websiteDarkMode);
- form.refreshTheme();
- nativeBridge.notifyUiReady();
UITimer.timer(900, true, form, () -> {
- boolean dark = nativeBridge.isDarkMode();
+ ShouldExecute liveBridge = resolveNativeBridgeQuietly();
+ if (liveBridge == null || !liveBridge.isSupported()) {
+ return;
+ }
+ boolean dark = liveBridge.isDarkMode();
if (dark != websiteDarkMode) {
websiteDarkMode = dark;
Display.getInstance().setDarkMode(dark);
From 9be2061eb78e63bca3fa3fc0e7af0da3b379a6f9 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 16 Apr 2026 21:08:53 +0300
Subject: [PATCH 13/55] Drive skindesigner theme from iframe query and remove
native bridge dependency
---
.../layouts/_default/skindesigner.html | 15 +++-
.../tools/skindesigner/ShouldExecute.java | 2 -
.../tools/skindesigner/SkinDesigner.java | 77 +++++++++--------
...ename1_tools_skindesigner_ShouldExecute.js | 84 -------------------
4 files changed, 58 insertions(+), 120 deletions(-)
diff --git a/docs/website/layouts/_default/skindesigner.html b/docs/website/layouts/_default/skindesigner.html
index 9d59f43e4c..7495cec3e2 100644
--- a/docs/website/layouts/_default/skindesigner.html
+++ b/docs/website/layouts/_default/skindesigner.html
@@ -11,7 +11,7 @@
@@ -180,6 +180,19 @@
dark = body.classList.contains("dark") || !!(mediaQuery && mediaQuery.matches);
}
body.classList.toggle("cn1-skindesigner-dark", dark);
+ syncFrameTheme(dark);
+ }
+
+ function syncFrameTheme(dark) {
+ if (!frame) {
+ return;
+ }
+ var base = frame.getAttribute("data-base-src") || "/skindesigner-app/";
+ var target = base + (base.indexOf("?") >= 0 ? "&" : "?") + "theme=" + (dark ? "dark" : "light");
+ var current = frame.getAttribute("src");
+ if (current !== target) {
+ frame.setAttribute("src", target);
+ }
}
body.classList.add("cn1-skindesigner-page-body");
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/ShouldExecute.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/ShouldExecute.java
index de93ab5aae..3ba8b7bd68 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/ShouldExecute.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/ShouldExecute.java
@@ -4,6 +4,4 @@
public interface ShouldExecute extends NativeInterface {
boolean shouldExecute();
- boolean isDarkMode();
- void notifyUiReady();
}
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index 2573917d21..6d5757e18e 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -8,6 +8,7 @@
import com.codename1.components.ToastBar;
import com.codename1.io.FileSystemStorage;
import com.codename1.ui.Display;
+import com.codename1.ui.CN;
import com.codename1.ui.Form;
import com.codename1.ui.Label;
import com.codename1.ui.plaf.UIManager;
@@ -16,7 +17,6 @@
import com.codename1.io.Properties;
import com.codename1.io.Storage;
import com.codename1.io.Util;
-import com.codename1.system.NativeLookup;
import com.codename1.ui.BrowserComponent;
import com.codename1.ui.Button;
import com.codename1.ui.Command;
@@ -52,7 +52,6 @@ public class SkinDesigner extends Lifecycle {
private static final String[] NATIVE_THEME_FILES = {"iOS7Theme.res", "iPhoneTheme.res",
"android_holo_light.res","androidTheme.res", "winTheme.res"};
private boolean websiteDarkMode;
- private ShouldExecute nativeBridge;
@Override
public void runApp() {
@@ -202,10 +201,7 @@ public void runApp() {
ToastBar.showErrorMessage("Error wring skin file " + err);
}
// in the JavaScript port this will trigger the download dialog
- ShouldExecute bridge = resolveNativeBridgeQuietly();
- if(bridge != null && bridge.isSupported() && bridge.shouldExecute()) {
- Display.getInstance().execute(fs.getAppHomePath() + "skin-file.skin");
- }
+ Display.getInstance().execute(fs.getAppHomePath() + "skin-file.skin");
}
});
@@ -236,7 +232,7 @@ public void runApp() {
settingsContainer.addComponent(0, actions);
skinDesignerForm.show();
- initWebsiteThemeSync(skinDesignerForm);
+ initThemeFromUrl(skinDesignerForm);
}
private Label labeledFieldTitle(String text) {
@@ -251,33 +247,13 @@ private void styleFields(TextField... fields) {
}
}
- private ShouldExecute resolveNativeBridgeQuietly() {
- if (nativeBridge != null) {
- return nativeBridge;
- }
- try {
- nativeBridge = NativeLookup.create(ShouldExecute.class);
- } catch (Throwable ignored) {
- nativeBridge = null;
- }
- return nativeBridge;
- }
-
- private void initWebsiteThemeSync(Form form) {
- ShouldExecute bridge = resolveNativeBridgeQuietly();
- if (bridge != null && bridge.isSupported()) {
- websiteDarkMode = bridge.isDarkMode();
- Display.getInstance().setDarkMode(websiteDarkMode);
- applyWebsiteTheme(form, websiteDarkMode);
- form.refreshTheme();
- bridge.notifyUiReady();
- }
+ private void initThemeFromUrl(Form form) {
+ websiteDarkMode = readThemeFromUrl();
+ Display.getInstance().setDarkMode(websiteDarkMode);
+ applyWebsiteTheme(form, websiteDarkMode);
+ form.refreshTheme();
UITimer.timer(900, true, form, () -> {
- ShouldExecute liveBridge = resolveNativeBridgeQuietly();
- if (liveBridge == null || !liveBridge.isSupported()) {
- return;
- }
- boolean dark = liveBridge.isDarkMode();
+ boolean dark = readThemeFromUrl();
if (dark != websiteDarkMode) {
websiteDarkMode = dark;
Display.getInstance().setDarkMode(dark);
@@ -287,6 +263,41 @@ private void initWebsiteThemeSync(Form form) {
});
}
+ private boolean readThemeFromUrl() {
+ String href = CN.getProperty("browser.window.location.href", "");
+ String theme = queryParam(href, "theme");
+ if ("dark".equalsIgnoreCase(theme)) {
+ return true;
+ }
+ if ("light".equalsIgnoreCase(theme)) {
+ return false;
+ }
+ return Display.getInstance().isDarkMode();
+ }
+
+ private String queryParam(String href, String name) {
+ if (href == null || href.length() == 0) {
+ return null;
+ }
+ int queryStart = href.indexOf('?');
+ if (queryStart < 0 || queryStart == href.length() - 1) {
+ return null;
+ }
+ String query = href.substring(queryStart + 1);
+ int hash = query.indexOf('#');
+ if (hash >= 0) {
+ query = query.substring(0, hash);
+ }
+ String prefix = name + "=";
+ String[] pairs = Util.split(query, "&");
+ for (String pair : pairs) {
+ if (pair.startsWith(prefix) && pair.length() > prefix.length()) {
+ return pair.substring(prefix.length());
+ }
+ }
+ return null;
+ }
+
private void applyWebsiteTheme(Container component, boolean dark) {
for (int i = 0; i < component.getComponentCount(); i++) {
Component child = component.getComponentAt(i);
diff --git a/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_ShouldExecute.js b/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_ShouldExecute.js
index a75500a03d..02638a1254 100644
--- a/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_ShouldExecute.js
+++ b/scripts/skindesigner/javascript/src/main/javascript/com_codename1_tools_skindesigner_ShouldExecute.js
@@ -2,94 +2,10 @@
var o = {};
- function readWebsiteThemePreference() {
- try {
- var parentWindow = (window.parent && window.parent !== window) ? window.parent : null;
- var parentDoc = parentWindow && parentWindow.document ? parentWindow.document : null;
- var parentBody = parentDoc && parentDoc.body ? parentDoc.body : null;
- var parentHtml = parentDoc && parentDoc.documentElement ? parentDoc.documentElement : null;
- var classes = parentBody && parentBody.classList ? parentBody.classList : null;
- var htmlClasses = parentHtml && parentHtml.classList ? parentHtml.classList : null;
- if (classes) {
- if (classes.contains("dark") || classes.contains("cn1-skindesigner-dark")) {
- return true;
- }
- if (classes.contains("light") || classes.contains("cn1-skindesigner-light")) {
- return false;
- }
- }
- if (htmlClasses) {
- if (htmlClasses.contains("dark") || htmlClasses.contains("cn1-skindesigner-dark")) {
- return true;
- }
- if (htmlClasses.contains("light") || htmlClasses.contains("cn1-skindesigner-light")) {
- return false;
- }
- }
-
- if (parentWindow && parentWindow.localStorage) {
- var pref = parentWindow.localStorage.getItem("pref-theme");
- if (pref === "dark") {
- return true;
- }
- if (pref === "light") {
- return false;
- }
- }
- if (window.localStorage) {
- var localPref = window.localStorage.getItem("pref-theme");
- if (localPref === "dark") {
- return true;
- }
- if (localPref === "light") {
- return false;
- }
- }
-
- var mediaWindow = parentWindow || window;
- if (mediaWindow.matchMedia) {
- return mediaWindow.matchMedia("(prefers-color-scheme: dark)").matches;
- }
- } catch (ignored) {
- // Ignore parent access failures and fallback below.
- }
-
- if (window.matchMedia) {
- return window.matchMedia("(prefers-color-scheme: dark)").matches;
- }
-
- return false;
- }
-
o.shouldExecute_ = function(callback) {
callback.complete(true);
};
- o.isDarkMode_ = function(callback) {
- callback.complete(!!readWebsiteThemePreference());
- };
-
- o.notifyUiReady_ = function(callback) {
- var sendReady = function() {
- try {
- if (window.parent && window.parent !== window && window.parent.postMessage) {
- window.parent.postMessage({ type: "cn1-skindesigner-ui-ready" }, "*");
- }
- } catch (ignored) {
- // Ignore cross-origin/sandbox restrictions in embedded website mode.
- }
- callback.complete();
- };
-
- if (window.requestAnimationFrame) {
- window.requestAnimationFrame(function() {
- window.requestAnimationFrame(sendReady);
- });
- } else {
- window.setTimeout(sendReady, 48);
- }
- };
-
o.isSupported_ = function(callback) {
callback.complete(true);
};
From edd3b08af6370a3dc3f4b92ab15bc6024c904edf Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 16 Apr 2026 21:18:16 +0300
Subject: [PATCH 14/55] Harden skindesigner iframe theme URL assignment
---
docs/website/layouts/_default/skindesigner.html | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/docs/website/layouts/_default/skindesigner.html b/docs/website/layouts/_default/skindesigner.html
index 7495cec3e2..d4402ff64d 100644
--- a/docs/website/layouts/_default/skindesigner.html
+++ b/docs/website/layouts/_default/skindesigner.html
@@ -11,7 +11,6 @@
@@ -158,6 +157,7 @@
var loader = document.getElementById("cn1-skindesigner-loader");
var siteHeader = document.querySelector(".header");
var mediaQuery = window.matchMedia ? window.matchMedia("(prefers-color-scheme: dark)") : null;
+ var skinDesignerBasePath = "/skindesigner-app/";
function syncHeight() {
var h = siteHeader ? siteHeader.offsetHeight : 76;
@@ -187,8 +187,7 @@
if (!frame) {
return;
}
- var base = frame.getAttribute("data-base-src") || "/skindesigner-app/";
- var target = base + (base.indexOf("?") >= 0 ? "&" : "?") + "theme=" + (dark ? "dark" : "light");
+ var target = skinDesignerBasePath + "?theme=" + (dark ? "dark" : "light");
var current = frame.getAttribute("src");
if (current !== target) {
frame.setAttribute("src", target);
From 122d95f868a7ff5998001dc193304ff638b53317 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Fri, 17 Apr 2026 07:01:51 +0300
Subject: [PATCH 15/55] Improve skindesigner tab/icon theming and expose
primary actions
---
.../common/src/main/css/theme.css | 2 +
.../tools/skindesigner/SkinDesigner.java | 45 ++++++++++---------
2 files changed, 27 insertions(+), 20 deletions(-)
diff --git a/scripts/skindesigner/common/src/main/css/theme.css b/scripts/skindesigner/common/src/main/css/theme.css
index 7d199a96b9..273bfecb08 100644
--- a/scripts/skindesigner/common/src/main/css/theme.css
+++ b/scripts/skindesigner/common/src/main/css/theme.css
@@ -197,6 +197,7 @@ SkinDesignerActionButton {
padding: 0.9mm 1.4mm;
font-family: "native:MainLight";
font-size: 2.7mm;
+ text-align: center;
}
SkinDesignerActionButtonDark {
@@ -206,4 +207,5 @@ SkinDesignerActionButtonDark {
padding: 0.9mm 1.4mm;
font-family: "native:MainLight";
font-size: 2.7mm;
+ text-align: center;
}
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index 6d5757e18e..91668b82c3 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -11,7 +11,6 @@
import com.codename1.ui.CN;
import com.codename1.ui.Form;
import com.codename1.ui.Label;
-import com.codename1.ui.plaf.UIManager;
import com.codename1.io.Log;
import com.codename1.io.Preferences;
import com.codename1.io.Properties;
@@ -19,7 +18,6 @@
import com.codename1.io.Util;
import com.codename1.ui.BrowserComponent;
import com.codename1.ui.Button;
-import com.codename1.ui.Command;
import com.codename1.ui.Component;
import com.codename1.ui.Container;
import com.codename1.ui.FontImage;
@@ -34,7 +32,6 @@
import com.codename1.ui.layouts.BoxLayout;
import com.codename1.ui.layouts.GridLayout;
import com.codename1.ui.layouts.LayeredLayout;
-import com.codename1.ui.plaf.Style;
import com.codename1.ui.spinner.Picker;
import com.codename1.ui.util.ImageIO;
import com.codename1.ui.util.UITimer;
@@ -55,6 +52,7 @@ public class SkinDesigner extends Lifecycle {
@Override
public void runApp() {
+ CN.setProperty("platformHint.javascript.beforeUnloadMessage", null);
Form skinDesignerForm = new Form("Skin Designer", new BorderLayout());
skinDesignerForm.setTitle("");
skinDesignerForm.setUIID("SkinDesignerForm");
@@ -153,20 +151,9 @@ public void runApp() {
settingsContainer.setUIID("SkinDesignerCard");
settingsContainer.setScrollableY(true);
- Style tab = UIManager.getInstance().getComponentStyle("Tab");
- Style tabSel = UIManager.getInstance().getComponentSelectedStyle("Tab");
- FontImage portraitIcon = FontImage.createMaterial(FontImage.MATERIAL_STAY_CURRENT_PORTRAIT, tab, 4.5f);
- FontImage landscapeIcon = FontImage.createMaterial(FontImage.MATERIAL_STAY_CURRENT_LANDSCAPE, tab, 4.5f);
- FontImage portraitIconSel = FontImage.createMaterial(FontImage.MATERIAL_STAY_CURRENT_PORTRAIT, tabSel, 4.5f);
- FontImage landscapeIconSel = FontImage.createMaterial(FontImage.MATERIAL_STAY_CURRENT_LANDSCAPE, tabSel, 4.5f);
- FontImage settingsIcon = FontImage.createMaterial(FontImage.MATERIAL_SETTINGS, tab, 3.5f);
- FontImage settingsIconSel = FontImage.createMaterial(FontImage.MATERIAL_SETTINGS, tabSel, 3.5f);
- details.addTab("Portrait", portraitIcon, imPortrait.getContainer());
- details.addTab("Landscape", landscapeIcon, imLandscape.getContainer());
- details.addTab("Settings", settingsIcon, settingsContainer);
- details.setTabSelectedIcon(0, portraitIconSel);
- details.setTabSelectedIcon(1, landscapeIconSel);
- details.setTabSelectedIcon(2, settingsIconSel);
+ details.addTab("Portrait", FontImage.MATERIAL_STAY_CURRENT_PORTRAIT, 4.5f, imPortrait.getContainer());
+ details.addTab("Landscape", FontImage.MATERIAL_STAY_CURRENT_LANDSCAPE, 4.5f, imLandscape.getContainer());
+ details.addTab("Settings", FontImage.MATERIAL_SETTINGS, 3.5f, settingsContainer);
vl.addConstraint(smallFontSize, new NumericConstraint(false, 5, 400, "Font size must be a valid integer in the 5-400 range")).
addConstraint(mediumFontSize, new NumericConstraint(false, 5, 400, "Font size must be a valid integer in the 5-400 range")).
addConstraint(largeFontSize, new NumericConstraint(false, 5, 400, "Font size must be a valid integer in the 5-400 range")).
@@ -229,10 +216,10 @@ public void runApp() {
}
Container actions = GridLayout.encloseIn(3, helpAction, saveAction, shareAction);
actions.setUIID("SkinDesignerTabBar");
- settingsContainer.addComponent(0, actions);
+ skinDesignerForm.add(BorderLayout.NORTH, actions);
skinDesignerForm.show();
- initThemeFromUrl(skinDesignerForm);
+ initThemeFromUrl(skinDesignerForm, details);
}
private Label labeledFieldTitle(String text) {
@@ -247,10 +234,11 @@ private void styleFields(TextField... fields) {
}
}
- private void initThemeFromUrl(Form form) {
+ private void initThemeFromUrl(Form form, Tabs details) {
websiteDarkMode = readThemeFromUrl();
Display.getInstance().setDarkMode(websiteDarkMode);
applyWebsiteTheme(form, websiteDarkMode);
+ applyTabsTheme(details, websiteDarkMode);
form.refreshTheme();
UITimer.timer(900, true, form, () -> {
boolean dark = readThemeFromUrl();
@@ -258,11 +246,28 @@ private void initThemeFromUrl(Form form) {
websiteDarkMode = dark;
Display.getInstance().setDarkMode(dark);
applyWebsiteTheme(form, dark);
+ applyTabsTheme(details, dark);
form.refreshTheme();
}
});
}
+ private void applyTabsTheme(Tabs tabs, boolean dark) {
+ if (tabs == null) {
+ return;
+ }
+ String tabsUiid = dark ? "SkinDesignerTabsContainerDark" : "SkinDesignerTabsContainer";
+ String tabUiid = dark ? "TabDark" : "Tab";
+ tabs.setUIID(tabsUiid);
+ tabs.setTabUIID(tabUiid);
+ Container tabsContainer = tabs.getTabsContainer();
+ for (int i = 0; i < tabsContainer.getComponentCount(); i++) {
+ tabsContainer.getComponentAt(i).setUIID(tabUiid);
+ }
+ tabs.refreshTheme();
+ tabs.revalidate();
+ }
+
private boolean readThemeFromUrl() {
String href = CN.getProperty("browser.window.location.href", "");
String theme = queryParam(href, "theme");
From f49b0c5a5ad2d5fb70843c63dcb2fc3ffd953179 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Fri, 17 Apr 2026 12:35:12 +0300
Subject: [PATCH 16/55] Fix skindesigner action button UX and positioning form
chrome
---
.../tools/skindesigner/SkinDesigner.java | 35 ++++++++++++-------
1 file changed, 22 insertions(+), 13 deletions(-)
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index 91668b82c3..2b033e0f9c 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -26,7 +26,6 @@
import com.codename1.ui.Tabs;
import com.codename1.ui.TextArea;
import com.codename1.ui.TextField;
-import com.codename1.ui.Toolbar;
import com.codename1.ui.geom.Rectangle;
import com.codename1.ui.layouts.BorderLayout;
import com.codename1.ui.layouts.BoxLayout;
@@ -161,8 +160,7 @@ public void runApp() {
setShowErrorMessageForFocusedComponent(true);
Button helpAction = new Button("Help");
- helpAction.setUIID("SkinDesignerActionButton");
- FontImage.setMaterialIcon(helpAction, FontImage.MATERIAL_HELP);
+ styleActionButton(helpAction, FontImage.MATERIAL_HELP);
helpAction.addActionListener(e -> {
BrowserComponent help = new BrowserComponent();
help.setURL("jar:///help.html");
@@ -173,8 +171,7 @@ public void runApp() {
});
Button saveAction = new Button("Save");
- saveAction.setUIID("SkinDesignerActionButton");
- FontImage.setMaterialIcon(saveAction, FontImage.MATERIAL_SAVE);
+ styleActionButton(saveAction, FontImage.MATERIAL_SAVE);
saveAction.addActionListener(e -> {
byte[] data = createSkinFile(imPortrait, imLandscape, nativeTheme, platformName, tablet, systemFontFamily,
proportionalFontFamily, monospaceFontFamily, smallFontSize, mediumFontSize, largeFontSize,
@@ -193,8 +190,7 @@ public void runApp() {
});
Button shareAction = new Button("Share");
- shareAction.setUIID("SkinDesignerActionButton");
- FontImage.setMaterialIcon(shareAction, FontImage.MATERIAL_SHARE);
+ styleActionButton(shareAction, FontImage.MATERIAL_SHARE);
shareAction.addActionListener(e -> {
byte[] data = createSkinFile(imPortrait, imLandscape, nativeTheme, platformName, tablet, systemFontFamily,
proportionalFontFamily, monospaceFontFamily, smallFontSize, mediumFontSize, largeFontSize,
@@ -216,7 +212,7 @@ public void runApp() {
}
Container actions = GridLayout.encloseIn(3, helpAction, saveAction, shareAction);
actions.setUIID("SkinDesignerTabBar");
- skinDesignerForm.add(BorderLayout.NORTH, actions);
+ skinDesignerForm.add(BorderLayout.SOUTH, actions);
skinDesignerForm.show();
initThemeFromUrl(skinDesignerForm, details);
@@ -234,6 +230,13 @@ private void styleFields(TextField... fields) {
}
}
+ private void styleActionButton(Button button, char materialIcon) {
+ button.setUIID("SkinDesignerActionButton");
+ FontImage.setMaterialIcon(button, materialIcon);
+ button.getAllStyles().setAlignment(Component.CENTER);
+ button.setGap(CN.convertToPixels(0.7f));
+ }
+
private void initThemeFromUrl(Form form, Tabs details) {
websiteDarkMode = readThemeFromUrl();
Display.getInstance().setDarkMode(websiteDarkMode);
@@ -537,15 +540,20 @@ void aimPosition(final Image img, final TextField x, final TextField y, final in
String originalX = x.getText();
String originalY = y.getText();
Form editPosition = new Form("", new BorderLayout());
- Toolbar tb = new Toolbar(true);
- editPosition.setToolbar(tb);
- tb.setUIID("Container");
- tb.addCommandToRightBar("", FontImage.createMaterial(FontImage.MATERIAL_CHECK, "Label", 4), e -> x.getComponentForm().showBack());
- tb.addCommandToLeftBar("", FontImage.createMaterial(FontImage.MATERIAL_CANCEL, "Label", 4), e -> {
+ editPosition.setUIID("SkinDesignerForm");
+ Button done = new Button("Done");
+ styleActionButton(done, FontImage.MATERIAL_CHECK);
+ done.addActionListener(e -> x.getComponentForm().showBack());
+ Button cancel = new Button("Cancel");
+ styleActionButton(cancel, FontImage.MATERIAL_CANCEL);
+ cancel.addActionListener(e -> {
x.setText(originalX);
y.setText(originalY);
x.getComponentForm().showBack();
});
+ Container topActions = GridLayout.encloseIn(2, cancel, done);
+ topActions.setUIID("SkinDesignerTabBar");
+ editPosition.add(BorderLayout.NORTH, topActions);
Image mute = createMute(x.getAsInt(0), y.getAsInt(0), w, h, img);
class oo extends ImageViewer {
@@ -641,6 +649,7 @@ public void keyReleased(int key) {
editPosition.add(BorderLayout.CENTER, LayeredLayout.encloseIn(iv, overlay,
BorderLayout.south(GridLayout.encloseIn(6, zoomIn, zoomOut, left, right, up, down))));
+ applyWebsiteTheme(editPosition, websiteDarkMode);
editPosition.show();
}
From 443b8fd9f21c1c58bfed739565454ff7328ae616 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Fri, 17 Apr 2026 13:53:03 +0300
Subject: [PATCH 17/55] Refine SkinDesigner action buttons and help theming
---
.../tools/skindesigner/SkinDesigner.java | 89 +++++++++----------
1 file changed, 43 insertions(+), 46 deletions(-)
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index 2b033e0f9c..ab43bbb4ea 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -29,6 +29,7 @@
import com.codename1.ui.geom.Rectangle;
import com.codename1.ui.layouts.BorderLayout;
import com.codename1.ui.layouts.BoxLayout;
+import com.codename1.ui.layouts.FlowLayout;
import com.codename1.ui.layouts.GridLayout;
import com.codename1.ui.layouts.LayeredLayout;
import com.codename1.ui.spinner.Picker;
@@ -59,8 +60,8 @@ public void runApp() {
final Tabs details = new Tabs();
details.getTabsContainer().setUIID("SkinDesignerTabsContainer");
details.getTabsContainer().setScrollableX(false);
- ImageSettings imPortrait = createImageSettings("/skin.png", "port", vl);
- ImageSettings imLandscape = createImageSettings("/skin_l.png", "lan", vl);
+ final ImageSettings[] imPortraitRef = new ImageSettings[1];
+ final ImageSettings[] imLandscapeRef = new ImageSettings[1];
skinDesignerForm.add(BorderLayout.CENTER, details);
@@ -159,21 +160,8 @@ public void runApp() {
addConstraint(pixelRatio, new NumericConstraint(true, 0.1, 60, "PixelRatio is a positive decimal size in the range of 0.1 to 60")).
setShowErrorMessageForFocusedComponent(true);
- Button helpAction = new Button("Help");
- styleActionButton(helpAction, FontImage.MATERIAL_HELP);
- helpAction.addActionListener(e -> {
- BrowserComponent help = new BrowserComponent();
- help.setURL("jar:///help.html");
- Form helpForm = new Form("Help", new BorderLayout());
- helpForm.add(BorderLayout.CENTER, help);
- helpForm.getToolbar().setBackCommand("Back", ee -> skinDesignerForm.showBack());
- helpForm.show();
- });
-
- Button saveAction = new Button("Save");
- styleActionButton(saveAction, FontImage.MATERIAL_SAVE);
- saveAction.addActionListener(e -> {
- byte[] data = createSkinFile(imPortrait, imLandscape, nativeTheme, platformName, tablet, systemFontFamily,
+ Runnable saveAction = () -> {
+ byte[] data = createSkinFile(imPortraitRef[0], imLandscapeRef[0], nativeTheme, platformName, tablet, systemFontFamily,
proportionalFontFamily, monospaceFontFamily, smallFontSize, mediumFontSize, largeFontSize,
pixelRatio, overrideNamePrimary, overrideNameSecondary, overrideNameLast);
if(data != null) {
@@ -187,32 +175,12 @@ public void runApp() {
// in the JavaScript port this will trigger the download dialog
Display.getInstance().execute(fs.getAppHomePath() + "skin-file.skin");
}
- });
+ };
- Button shareAction = new Button("Share");
- styleActionButton(shareAction, FontImage.MATERIAL_SHARE);
- shareAction.addActionListener(e -> {
- byte[] data = createSkinFile(imPortrait, imLandscape, nativeTheme, platformName, tablet, systemFontFamily,
- proportionalFontFamily, monospaceFontFamily, smallFontSize, mediumFontSize, largeFontSize,
- pixelRatio, overrideNamePrimary, overrideNameSecondary, overrideNameLast);
- if(data != null) {
- FileSystemStorage fs = FileSystemStorage.getInstance();
- try(OutputStream os = fs.openOutputStream(fs.getAppHomePath() + "skin-file.skin")) {
- os.write(data);
- } catch(IOException err) {
- Log.e(err);
- ToastBar.showErrorMessage("Error wring skin file " + err);
- }
- Display.getInstance().share(null, fs.getAppHomePath() + "skin-file.skin", "application/vnd.codenameone-skin");
- }
- });
- if (!Display.getInstance().isNativeShareSupported()) {
- shareAction.setHidden(true);
- shareAction.setVisible(false);
- }
- Container actions = GridLayout.encloseIn(3, helpAction, saveAction, shareAction);
- actions.setUIID("SkinDesignerTabBar");
- skinDesignerForm.add(BorderLayout.SOUTH, actions);
+ imPortraitRef[0] = createImageSettings("/skin.png", "port", vl, () -> showHelpForm(skinDesignerForm), saveAction);
+ imLandscapeRef[0] = createImageSettings("/skin_l.png", "lan", vl, () -> showHelpForm(skinDesignerForm), saveAction);
+ ImageSettings imPortrait = imPortraitRef[0];
+ ImageSettings imLandscape = imLandscapeRef[0];
skinDesignerForm.show();
initThemeFromUrl(skinDesignerForm, details);
@@ -237,6 +205,12 @@ private void styleActionButton(Button button, char materialIcon) {
button.setGap(CN.convertToPixels(0.7f));
}
+ private void styleIconActionButton(Button button, char materialIcon) {
+ styleActionButton(button, materialIcon);
+ button.setText("");
+ button.setGap(0);
+ }
+
private void initThemeFromUrl(Form form, Tabs details) {
websiteDarkMode = readThemeFromUrl();
Display.getInstance().setDarkMode(websiteDarkMode);
@@ -414,7 +388,7 @@ private void autoSave(Picker p, String preferencesKey) {
});
}
- private ImageSettings createImageSettings(String imageFile, String prefix, Validator vl) {
+ private ImageSettings createImageSettings(String imageFile, String prefix, Validator vl, Runnable helpCallback, Runnable saveCallback) {
Image img = null;
try {
img = Image.createImage(Display.getInstance().getResourceAsStream(getClass(), imageFile));
@@ -462,8 +436,7 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
addConstraint(screenPositionY, new NumericConstraint(false, 0, 5000, "Screen position must be a valid integer in the 0-5000 range"));
Button aim = new Button();
- aim.setUIID("SkinDesignerActionButton");
- FontImage.setMaterialIcon(aim, FontImage.MATERIAL_PAN_TOOL);
+ styleIconActionButton(aim, FontImage.MATERIAL_PAN_TOOL);
aim.addActionListener(e ->
aimPosition(sl.getIcon(),
@@ -472,9 +445,17 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
screenWidthPixels.getAsInt(768),
screenHeightPixels.getAsInt(1024)));
+ Button helpButton = new Button();
+ styleIconActionButton(helpButton, FontImage.MATERIAL_HELP);
+ helpButton.addActionListener(e -> helpCallback.run());
+
+ Button saveButton = new Button();
+ styleIconActionButton(saveButton, FontImage.MATERIAL_SAVE);
+ saveButton.addActionListener(e -> saveCallback.run());
+
final Container cnt = BoxLayout.encloseY(imagePicker,
BorderLayout.center(labeledFieldTitle("Screen Position (X/Y/Width/Height)")).
- add(BorderLayout.EAST, aim),
+ add(BorderLayout.EAST, BoxLayout.encloseX(aim, helpButton, saveButton)),
GridLayout.encloseIn(4, screenPositionX, screenPositionY, screenWidthPixels, screenHeightPixels),
sl);
cnt.setUIID("SkinDesignerCard");
@@ -520,6 +501,22 @@ public Image createSkinOverlay() {
};
}
+ private void showHelpForm(Form backForm) {
+ BrowserComponent help = new BrowserComponent();
+ help.setURL("jar:///help.html");
+ Form helpForm = new Form("Help", new BorderLayout());
+ helpForm.setUIID("SkinDesignerForm");
+ helpForm.add(BorderLayout.CENTER, help);
+ Button back = new Button();
+ styleIconActionButton(back, FontImage.MATERIAL_ARROW_BACK);
+ back.addActionListener(ee -> backForm.showBack());
+ Container helpActions = FlowLayout.encloseRight(back);
+ helpActions.setUIID("SkinDesignerTabBar");
+ helpForm.add(BorderLayout.NORTH, helpActions);
+ applyWebsiteTheme(helpForm, websiteDarkMode);
+ helpForm.show();
+ }
+
private Image createMute(int x, int y, int w, int h, Image img) {
Image mute = Image.createImage(img.getWidth(), img.getHeight(), 0);
Graphics g = mute.getGraphics();
From 51b428fa3def71558d8b3843a9a88e83bbf80e1d Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Fri, 17 Apr 2026 14:27:49 +0300
Subject: [PATCH 18/55] Fix SkinDesigner tab initialization compile order
---
.../java/com/codename1/tools/skindesigner/SkinDesigner.java | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index ab43bbb4ea..085d8caf06 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -151,9 +151,6 @@ public void runApp() {
settingsContainer.setUIID("SkinDesignerCard");
settingsContainer.setScrollableY(true);
- details.addTab("Portrait", FontImage.MATERIAL_STAY_CURRENT_PORTRAIT, 4.5f, imPortrait.getContainer());
- details.addTab("Landscape", FontImage.MATERIAL_STAY_CURRENT_LANDSCAPE, 4.5f, imLandscape.getContainer());
- details.addTab("Settings", FontImage.MATERIAL_SETTINGS, 3.5f, settingsContainer);
vl.addConstraint(smallFontSize, new NumericConstraint(false, 5, 400, "Font size must be a valid integer in the 5-400 range")).
addConstraint(mediumFontSize, new NumericConstraint(false, 5, 400, "Font size must be a valid integer in the 5-400 range")).
addConstraint(largeFontSize, new NumericConstraint(false, 5, 400, "Font size must be a valid integer in the 5-400 range")).
@@ -181,6 +178,9 @@ public void runApp() {
imLandscapeRef[0] = createImageSettings("/skin_l.png", "lan", vl, () -> showHelpForm(skinDesignerForm), saveAction);
ImageSettings imPortrait = imPortraitRef[0];
ImageSettings imLandscape = imLandscapeRef[0];
+ details.addTab("Portrait", FontImage.MATERIAL_STAY_CURRENT_PORTRAIT, 4.5f, imPortrait.getContainer());
+ details.addTab("Landscape", FontImage.MATERIAL_STAY_CURRENT_LANDSCAPE, 4.5f, imLandscape.getContainer());
+ details.addTab("Settings", FontImage.MATERIAL_SETTINGS, 3.5f, settingsContainer);
skinDesignerForm.show();
initThemeFromUrl(skinDesignerForm, details);
From 32353307163862052f2cd0fb03461beed581da1f Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Fri, 17 Apr 2026 18:51:51 +0300
Subject: [PATCH 19/55] Add dark help HTML and drag positioning support
---
.../tools/skindesigner/SkinDesigner.java | 41 +++++++++++++++++-
.../common/src/main/resources/help.html | 43 +++++++++++++++++++
2 files changed, 83 insertions(+), 1 deletion(-)
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index 085d8caf06..f0f2ad528a 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -503,7 +503,7 @@ public Image createSkinOverlay() {
private void showHelpForm(Form backForm) {
BrowserComponent help = new BrowserComponent();
- help.setURL("jar:///help.html");
+ help.setURL("jar:///help.html?theme=" + (websiteDarkMode ? "dark" : "light"));
Form helpForm = new Form("Help", new BorderLayout());
helpForm.setUIID("SkinDesignerForm");
helpForm.add(BorderLayout.CENTER, help);
@@ -554,6 +554,9 @@ void aimPosition(final Image img, final TextField x, final TextField y, final in
Image mute = createMute(x.getAsInt(0), y.getAsInt(0), w, h, img);
class oo extends ImageViewer {
+ private int lastDragX = -1;
+ private int lastDragY = -1;
+
public oo(Image img) {
super(img);
}
@@ -561,6 +564,42 @@ public oo(Image img) {
public boolean pinch(float scale) {
return super.pinch(scale);
}
+
+ @Override
+ public void pointerPressed(int xPos, int yPos) {
+ super.pointerPressed(xPos, yPos);
+ lastDragX = xPos;
+ lastDragY = yPos;
+ }
+
+ @Override
+ public void pointerDragged(int xPos, int yPos) {
+ if(lastDragX < 0 || lastDragY < 0) {
+ lastDragX = xPos;
+ lastDragY = yPos;
+ return;
+ }
+ int dx = Math.round((xPos - lastDragX) / getZoom());
+ int dy = Math.round((yPos - lastDragY) / getZoom());
+ if(dx != 0 || dy != 0) {
+ int maxX = Math.max(0, img.getWidth() - w);
+ int maxY = Math.max(0, img.getHeight() - h);
+ int newX = Math.min(maxX, Math.max(0, x.getAsInt(0) + dx));
+ int newY = Math.min(maxY, Math.max(0, y.getAsInt(0) + dy));
+ x.setText("" + newX);
+ y.setText("" + newY);
+ setImageNoReposition(createMute(newX, newY, w, h, img));
+ lastDragX = xPos;
+ lastDragY = yPos;
+ }
+ }
+
+ @Override
+ public void pointerReleased(int xPos, int yPos) {
+ super.pointerReleased(xPos, yPos);
+ lastDragX = -1;
+ lastDragY = -1;
+ }
}
final oo overlay = new oo(mute);
ImageViewer iv = new ImageViewer(img) {
diff --git a/scripts/skindesigner/common/src/main/resources/help.html b/scripts/skindesigner/common/src/main/resources/help.html
index 25165b9daa..17c912b528 100644
--- a/scripts/skindesigner/common/src/main/resources/help.html
+++ b/scripts/skindesigner/common/src/main/resources/help.html
@@ -4,6 +4,49 @@
Help
+
+
Skin Designer Help
From fa743b51fa637a7df9c6b37e5b626c0c2777d285 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Fri, 17 Apr 2026 20:09:16 +0300
Subject: [PATCH 20/55] Improve landscape coordinate defaults and skin
safe-area support
---
.../com/codename1/impl/javase/JavaSEPort.java | 28 +++++++++++-
.../tools/skindesigner/SkinDesigner.java | 44 +++++++++++++++++++
2 files changed, 71 insertions(+), 1 deletion(-)
diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
index bcf4c861e1..c81a1bad02 100644
--- a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
+++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
@@ -2729,7 +2729,17 @@ private void loadSkinFile(InputStream skin, final JFrame frm) {
landscapeSkinHotspots = new HashMap
();
landscapeScreenCoordinates = new Rectangle();
- if(props.getProperty("roundScreen", "false").equalsIgnoreCase("true")) {
+ boolean roundScreen = props.getProperty("roundScreen", "false").equalsIgnoreCase("true");
+ boolean hasSafeAreaProps =
+ props.getProperty("safePortraitX") != null ||
+ props.getProperty("safePortraitY") != null ||
+ props.getProperty("safePortraitWidth") != null ||
+ props.getProperty("safePortraitHeight") != null ||
+ props.getProperty("safeLandscapeX") != null ||
+ props.getProperty("safeLandscapeY") != null ||
+ props.getProperty("safeLandscapeWidth") != null ||
+ props.getProperty("safeLandscapeHeight") != null;
+ if(roundScreen) {
safeAreaLandscape = new Rectangle();
safeAreaPortrait = new Rectangle();
@@ -2758,6 +2768,22 @@ private void loadSkinFile(InputStream skin, final JFrame frm) {
} else {
initializeCoordinates(map, props, portraitSkinHotspots, portraitScreenCoordinates);
initializeCoordinates(landscapeMap, props, landscapeSkinHotspots, landscapeScreenCoordinates);
+ if (hasSafeAreaProps) {
+ safeAreaPortrait = new Rectangle();
+ safeAreaLandscape = new Rectangle();
+ safeAreaPortrait.setBounds(
+ Integer.parseInt(props.getProperty("safePortraitX", "" + portraitScreenCoordinates.x)),
+ Integer.parseInt(props.getProperty("safePortraitY", "" + portraitScreenCoordinates.y)),
+ Integer.parseInt(props.getProperty("safePortraitWidth", "" + portraitScreenCoordinates.width)),
+ Integer.parseInt(props.getProperty("safePortraitHeight", "" + portraitScreenCoordinates.height))
+ );
+ safeAreaLandscape.setBounds(
+ Integer.parseInt(props.getProperty("safeLandscapeX", "" + landscapeScreenCoordinates.x)),
+ Integer.parseInt(props.getProperty("safeLandscapeY", "" + landscapeScreenCoordinates.y)),
+ Integer.parseInt(props.getProperty("safeLandscapeWidth", "" + landscapeScreenCoordinates.width)),
+ Integer.parseInt(props.getProperty("safeLandscapeHeight", "" + landscapeScreenCoordinates.height))
+ );
+ }
}
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index f0f2ad528a..dc76c89cfd 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -356,6 +356,10 @@ interface ImageSettings {
Container getContainer();
Image createSkinOverlay();
Image getSkinImage();
+ int getScreenX();
+ int getScreenY();
+ int getScreenWidth();
+ int getScreenHeight();
}
private void autoSave(TextArea ta, String preferencesKey) {
@@ -425,6 +429,18 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
final TextField screenHeightPixels = new TextField("480", "Height", 8, TextField.NUMERIC);
final TextField screenPositionX = new TextField("40", "X", 8, TextField.NUMERIC);
final TextField screenPositionY = new TextField("40", "Y", 8, TextField.NUMERIC);
+ if("lan".equals(prefix) && Preferences.get("lanX", null) == null) {
+ String portraitX = Preferences.get("portX", null);
+ String portraitY = Preferences.get("portY", null);
+ String portraitWidth = Preferences.get("portWidth", null);
+ String portraitHeight = Preferences.get("portHeight", null);
+ if(portraitX != null && portraitY != null && portraitWidth != null && portraitHeight != null) {
+ screenPositionX.setText(portraitY);
+ screenPositionY.setText(portraitX);
+ screenWidthPixels.setText(portraitHeight);
+ screenHeightPixels.setText(portraitWidth);
+ }
+ }
styleFields(screenWidthPixels, screenHeightPixels, screenPositionX, screenPositionY);
autoSave(screenWidthPixels, prefix + "Width");
autoSave(screenHeightPixels, prefix + "Height");
@@ -498,6 +514,26 @@ public Image createSkinOverlay() {
screenWidthPixels.getAsInt(50), screenHeightPixels.getAsInt(50));
return m;
}
+
+ @Override
+ public int getScreenX() {
+ return screenPositionX.getAsInt(0);
+ }
+
+ @Override
+ public int getScreenY() {
+ return screenPositionY.getAsInt(0);
+ }
+
+ @Override
+ public int getScreenWidth() {
+ return screenWidthPixels.getAsInt(50);
+ }
+
+ @Override
+ public int getScreenHeight() {
+ return screenHeightPixels.getAsInt(50);
+ }
};
}
@@ -753,6 +789,14 @@ byte[] createSkinFile(ImageSettings imPortrait, ImageSettings imLandscape, Picke
props.put("overrideNames", overrideNamePrimary.getSelectedString() + "," +
overrideNameSecondary.getSelectedString() + "," +
overrideNameLast.getSelectedString());
+ props.put("safePortraitX", "" + imPortrait.getScreenX());
+ props.put("safePortraitY", "" + imPortrait.getScreenY());
+ props.put("safePortraitWidth", "" + imPortrait.getScreenWidth());
+ props.put("safePortraitHeight", "" + imPortrait.getScreenHeight());
+ props.put("safeLandscapeX", "" + imLandscape.getScreenX());
+ props.put("safeLandscapeY", "" + imLandscape.getScreenY());
+ props.put("safeLandscapeWidth", "" + imLandscape.getScreenWidth());
+ props.put("safeLandscapeHeight", "" + imLandscape.getScreenHeight());
ze = new ZipEntry("skin.properties");
zos.putNextEntry(ze);
From 4dc2c5af68fa5b4cd30c1cb4059fc7a1615ee5d1 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Fri, 17 Apr 2026 21:46:45 +0300
Subject: [PATCH 21/55] Fix landscape pan defaults and expose advanced skin
settings
---
.../tools/skindesigner/SkinDesigner.java | 136 +++++++++++++++---
1 file changed, 115 insertions(+), 21 deletions(-)
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index dc76c89cfd..0de757229d 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -1,7 +1,6 @@
package com.codename1.tools.skindesigner;
import com.codename1.system.Lifecycle;
-import com.codename1.components.FloatingHint;
import com.codename1.components.ImageViewer;
import com.codename1.components.OnOffSwitch;
import com.codename1.components.ScaleImageLabel;
@@ -125,6 +124,25 @@ public void runApp() {
overrideNameLast.setRenderingPrototype("XXXXXXXX");
autoSave(overrideNameLast, "overrideNameLast");
+ TextField safePortraitX = new TextField("40", "Safe Portrait X", 8, TextField.NUMERIC);
+ TextField safePortraitY = new TextField("40", "Safe Portrait Y", 8, TextField.NUMERIC);
+ TextField safePortraitWidth = new TextField("320", "Safe Portrait Width", 8, TextField.NUMERIC);
+ TextField safePortraitHeight = new TextField("480", "Safe Portrait Height", 8, TextField.NUMERIC);
+ TextField safeLandscapeX = new TextField("40", "Safe Landscape X", 8, TextField.NUMERIC);
+ TextField safeLandscapeY = new TextField("40", "Safe Landscape Y", 8, TextField.NUMERIC);
+ TextField safeLandscapeWidth = new TextField("480", "Safe Landscape Width", 8, TextField.NUMERIC);
+ TextField safeLandscapeHeight = new TextField("320", "Safe Landscape Height", 8, TextField.NUMERIC);
+ styleFields(safePortraitX, safePortraitY, safePortraitWidth, safePortraitHeight,
+ safeLandscapeX, safeLandscapeY, safeLandscapeWidth, safeLandscapeHeight);
+ autoSave(safePortraitX, "safePortraitX");
+ autoSave(safePortraitY, "safePortraitY");
+ autoSave(safePortraitWidth, "safePortraitWidth");
+ autoSave(safePortraitHeight, "safePortraitHeight");
+ autoSave(safeLandscapeX, "safeLandscapeX");
+ autoSave(safeLandscapeY, "safeLandscapeY");
+ autoSave(safeLandscapeWidth, "safeLandscapeWidth");
+ autoSave(safeLandscapeHeight, "safeLandscapeHeight");
+
Container settingsContainer = BoxLayout.encloseY(
labeledFieldTitle("Native Theme"),
@@ -136,7 +154,8 @@ public void runApp() {
systemFontFamily,
labeledFieldTitle(proportionalFontFamily.getHint()),
proportionalFontFamily,
- new FloatingHint(monospaceFontFamily),
+ labeledFieldTitle(monospaceFontFamily.getHint()),
+ monospaceFontFamily,
labeledFieldTitle(smallFontSize.getHint()),
smallFontSize,
labeledFieldTitle(mediumFontSize.getHint()),
@@ -146,7 +165,11 @@ public void runApp() {
labeledFieldTitle(pixelRatio.getHint()),
pixelRatio,
labeledFieldTitle("Platform Overrides"),
- BoxLayout.encloseX(overrideNamePrimary, overrideNameSecondary, overrideNameLast)
+ BoxLayout.encloseX(overrideNamePrimary, overrideNameSecondary, overrideNameLast),
+ labeledFieldTitle("Safe Area Portrait (X/Y/Width/Height)"),
+ GridLayout.encloseIn(4, safePortraitX, safePortraitY, safePortraitWidth, safePortraitHeight),
+ labeledFieldTitle("Safe Area Landscape (X/Y/Width/Height)"),
+ GridLayout.encloseIn(4, safeLandscapeX, safeLandscapeY, safeLandscapeWidth, safeLandscapeHeight)
);
settingsContainer.setUIID("SkinDesignerCard");
settingsContainer.setScrollableY(true);
@@ -160,7 +183,9 @@ public void runApp() {
Runnable saveAction = () -> {
byte[] data = createSkinFile(imPortraitRef[0], imLandscapeRef[0], nativeTheme, platformName, tablet, systemFontFamily,
proportionalFontFamily, monospaceFontFamily, smallFontSize, mediumFontSize, largeFontSize,
- pixelRatio, overrideNamePrimary, overrideNameSecondary, overrideNameLast);
+ pixelRatio, overrideNamePrimary, overrideNameSecondary, overrideNameLast,
+ safePortraitX, safePortraitY, safePortraitWidth, safePortraitHeight,
+ safeLandscapeX, safeLandscapeY, safeLandscapeWidth, safeLandscapeHeight);
if(data != null) {
FileSystemStorage fs = FileSystemStorage.getInstance();
try(OutputStream os = fs.openOutputStream(fs.getAppHomePath() + "skin-file.skin")) {
@@ -450,6 +475,20 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
addConstraint(screenHeightPixels, new NumericConstraint(false, 20, 5000, "Screen size must be a valid integer in the 20-5000 range")).
addConstraint(screenPositionX, new NumericConstraint(false, 0, 5000, "Screen position must be a valid integer in the 0-5000 range")).
addConstraint(screenPositionY, new NumericConstraint(false, 0, 5000, "Screen position must be a valid integer in the 0-5000 range"));
+ if("lan".equals(prefix)) {
+ String portraitX = Preferences.get("portX", null);
+ String portraitY = Preferences.get("portY", null);
+ String portraitWidth = Preferences.get("portWidth", null);
+ String portraitHeight = Preferences.get("portHeight", null);
+ if(portraitX != null && portraitY != null && portraitWidth != null && portraitHeight != null &&
+ portraitX.equals(screenPositionX.getText()) && portraitY.equals(screenPositionY.getText()) &&
+ portraitWidth.equals(screenWidthPixels.getText()) && portraitHeight.equals(screenHeightPixels.getText())) {
+ screenPositionX.setText(portraitY);
+ screenPositionY.setText(portraitX);
+ screenWidthPixels.setText(portraitHeight);
+ screenHeightPixels.setText(portraitWidth);
+ }
+ }
Button aim = new Button();
styleIconActionButton(aim, FontImage.MATERIAL_PAN_TOOL);
@@ -469,10 +508,39 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
styleIconActionButton(saveButton, FontImage.MATERIAL_SAVE);
saveButton.addActionListener(e -> saveCallback.run());
+ ScaleImageLabel maskLabel = new ScaleImageLabel();
+ Button maskPicker = new Button("Select Screen Mask (Optional)");
+ maskPicker.setUIID("SkinDesignerActionButton");
+ maskPicker.addActionListener((e) -> {
+ Display.getInstance().openGallery((ee) -> {
+ if(ee != null && ee.getSource() != null) {
+ try {
+ String fileName = (String)ee.getSource();
+ Image mask = Image.createImage(fileName);
+ maskLabel.setIcon(mask);
+ maskLabel.getParent().revalidate();
+ Util.copy(FileSystemStorage.getInstance().openInputStream(fileName),
+ Storage.getInstance().createOutputStream(prefix + ".mask.png"));
+ } catch(IOException err) {
+ ToastBar.showErrorMessage("Error Loading Mask: " + err);
+ }
+ }
+ }, Display.GALLERY_IMAGE);
+ });
+ if(Storage.getInstance().exists(prefix + ".mask.png")) {
+ try(InputStream is = Storage.getInstance().createInputStream(prefix + ".mask.png")) {
+ maskLabel.setIcon(Image.createImage(is));
+ } catch(IOException err) {
+ Log.e(err);
+ }
+ }
+
final Container cnt = BoxLayout.encloseY(imagePicker,
BorderLayout.center(labeledFieldTitle("Screen Position (X/Y/Width/Height)")).
add(BorderLayout.EAST, BoxLayout.encloseX(aim, helpButton, saveButton)),
GridLayout.encloseIn(4, screenPositionX, screenPositionY, screenWidthPixels, screenHeightPixels),
+ maskPicker,
+ maskLabel,
sl);
cnt.setUIID("SkinDesignerCard");
cnt.setScrollableY(true);
@@ -488,12 +556,22 @@ public Image getSkinImage() {
int[] data = img.getRGB();
int width = img.getWidth();
int height = img.getHeight();
- Rectangle screen = new Rectangle(screenPositionX.getAsInt(0), screenPositionY.getAsInt(0),
- screenWidthPixels.getAsInt(50), screenHeightPixels.getAsInt(50));
- for(int x = 0 ; x < width ; x++) {
- for(int y = 0 ; y < height ; y++) {
- if(screen.contains(x, y, 1, 1)) {
- data[y * width + x] = 0;
+ Image mask = maskLabel.getIcon();
+ if(mask != null && mask.getWidth() == width && mask.getHeight() == height) {
+ int[] maskRgb = mask.getRGB();
+ for(int i = 0 ; i < maskRgb.length ; i++) {
+ if(maskRgb[i] != 0xff000000) {
+ data[i] = 0;
+ }
+ }
+ } else {
+ Rectangle screen = new Rectangle(screenPositionX.getAsInt(0), screenPositionY.getAsInt(0),
+ screenWidthPixels.getAsInt(50), screenHeightPixels.getAsInt(50));
+ for(int x = 0 ; x < width ; x++) {
+ for(int y = 0 ; y < height ; y++) {
+ if(screen.contains(x, y, 1, 1)) {
+ data[y * width + x] = 0;
+ }
}
}
}
@@ -510,8 +588,22 @@ public Image createSkinOverlay() {
Image m = Image.createImage(skinImage.getWidth(), skinImage.getHeight(), 0);
Graphics g = m.getGraphics();
g.setColor(0);
- g.fillRect(screenPositionX.getAsInt(0), screenPositionY.getAsInt(0),
- screenWidthPixels.getAsInt(50), screenHeightPixels.getAsInt(50));
+ Image mask = maskLabel.getIcon();
+ if(mask != null && mask.getWidth() == skinImage.getWidth() && mask.getHeight() == skinImage.getHeight()) {
+ int[] maskRgb = mask.getRGB();
+ int w = mask.getWidth();
+ int h = mask.getHeight();
+ for(int x = 0; x < w; x++) {
+ for(int y = 0; y < h; y++) {
+ if(maskRgb[y * w + x] == 0xff000000) {
+ g.drawLine(x, y, x, y);
+ }
+ }
+ }
+ } else {
+ g.fillRect(screenPositionX.getAsInt(0), screenPositionY.getAsInt(0),
+ screenWidthPixels.getAsInt(50), screenHeightPixels.getAsInt(50));
+ }
return m;
}
@@ -688,11 +780,13 @@ public void keyReleased(int key) {
zoomIn.addActionListener(e -> {
iv.setZoom(iv.getZoom() + 1);
overlay.setZoom(iv.getZoom());
+ overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, img));
});
zoomOut.addActionListener(e -> {
iv.setZoom(iv.getZoom() - 1);
overlay.setZoom(iv.getZoom());
+ overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, img));
});
left.addActionListener(e -> {
@@ -733,7 +827,7 @@ private byte[] imageToByteArray(Image img) throws IOException {
return bo.toByteArray();
}
- byte[] createSkinFile(ImageSettings imPortrait, ImageSettings imLandscape, Picker nativeTheme, Picker platformName, OnOffSwitch tablet, TextField systemFontFamily, TextField proportionalFontFamily, TextField monospaceFontFamily, TextField smallFontSize, TextField mediumFontSize, TextField largeFontSize, TextField pixelRatio, Picker overrideNamePrimary, Picker overrideNameSecondary, Picker overrideNameLast) {
+ byte[] createSkinFile(ImageSettings imPortrait, ImageSettings imLandscape, Picker nativeTheme, Picker platformName, OnOffSwitch tablet, TextField systemFontFamily, TextField proportionalFontFamily, TextField monospaceFontFamily, TextField smallFontSize, TextField mediumFontSize, TextField largeFontSize, TextField pixelRatio, Picker overrideNamePrimary, Picker overrideNameSecondary, Picker overrideNameLast, TextField safePortraitX, TextField safePortraitY, TextField safePortraitWidth, TextField safePortraitHeight, TextField safeLandscapeX, TextField safeLandscapeY, TextField safeLandscapeWidth, TextField safeLandscapeHeight) {
Image portrait = imPortrait.getSkinImage();
Image landscape = imLandscape.getSkinImage();
if (portrait == null) {
@@ -789,14 +883,14 @@ byte[] createSkinFile(ImageSettings imPortrait, ImageSettings imLandscape, Picke
props.put("overrideNames", overrideNamePrimary.getSelectedString() + "," +
overrideNameSecondary.getSelectedString() + "," +
overrideNameLast.getSelectedString());
- props.put("safePortraitX", "" + imPortrait.getScreenX());
- props.put("safePortraitY", "" + imPortrait.getScreenY());
- props.put("safePortraitWidth", "" + imPortrait.getScreenWidth());
- props.put("safePortraitHeight", "" + imPortrait.getScreenHeight());
- props.put("safeLandscapeX", "" + imLandscape.getScreenX());
- props.put("safeLandscapeY", "" + imLandscape.getScreenY());
- props.put("safeLandscapeWidth", "" + imLandscape.getScreenWidth());
- props.put("safeLandscapeHeight", "" + imLandscape.getScreenHeight());
+ props.put("safePortraitX", safePortraitX.getText());
+ props.put("safePortraitY", safePortraitY.getText());
+ props.put("safePortraitWidth", safePortraitWidth.getText());
+ props.put("safePortraitHeight", safePortraitHeight.getText());
+ props.put("safeLandscapeX", safeLandscapeX.getText());
+ props.put("safeLandscapeY", safeLandscapeY.getText());
+ props.put("safeLandscapeWidth", safeLandscapeWidth.getText());
+ props.put("safeLandscapeHeight", safeLandscapeHeight.getText());
ze = new ZipEntry("skin.properties");
zos.putNextEntry(ze);
From 65d3404b76704cc769eb675ee559db64cb458b77 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Fri, 17 Apr 2026 23:02:57 +0300
Subject: [PATCH 22/55] Add flood-fill screen selection and safe-area preview
controls
---
.../tools/skindesigner/SkinDesigner.java | 249 ++++++++++++++----
1 file changed, 201 insertions(+), 48 deletions(-)
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index 0de757229d..1560d6f3f1 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -124,26 +124,6 @@ public void runApp() {
overrideNameLast.setRenderingPrototype("XXXXXXXX");
autoSave(overrideNameLast, "overrideNameLast");
- TextField safePortraitX = new TextField("40", "Safe Portrait X", 8, TextField.NUMERIC);
- TextField safePortraitY = new TextField("40", "Safe Portrait Y", 8, TextField.NUMERIC);
- TextField safePortraitWidth = new TextField("320", "Safe Portrait Width", 8, TextField.NUMERIC);
- TextField safePortraitHeight = new TextField("480", "Safe Portrait Height", 8, TextField.NUMERIC);
- TextField safeLandscapeX = new TextField("40", "Safe Landscape X", 8, TextField.NUMERIC);
- TextField safeLandscapeY = new TextField("40", "Safe Landscape Y", 8, TextField.NUMERIC);
- TextField safeLandscapeWidth = new TextField("480", "Safe Landscape Width", 8, TextField.NUMERIC);
- TextField safeLandscapeHeight = new TextField("320", "Safe Landscape Height", 8, TextField.NUMERIC);
- styleFields(safePortraitX, safePortraitY, safePortraitWidth, safePortraitHeight,
- safeLandscapeX, safeLandscapeY, safeLandscapeWidth, safeLandscapeHeight);
- autoSave(safePortraitX, "safePortraitX");
- autoSave(safePortraitY, "safePortraitY");
- autoSave(safePortraitWidth, "safePortraitWidth");
- autoSave(safePortraitHeight, "safePortraitHeight");
- autoSave(safeLandscapeX, "safeLandscapeX");
- autoSave(safeLandscapeY, "safeLandscapeY");
- autoSave(safeLandscapeWidth, "safeLandscapeWidth");
- autoSave(safeLandscapeHeight, "safeLandscapeHeight");
-
-
Container settingsContainer = BoxLayout.encloseY(
labeledFieldTitle("Native Theme"),
nativeTheme,
@@ -165,11 +145,7 @@ public void runApp() {
labeledFieldTitle(pixelRatio.getHint()),
pixelRatio,
labeledFieldTitle("Platform Overrides"),
- BoxLayout.encloseX(overrideNamePrimary, overrideNameSecondary, overrideNameLast),
- labeledFieldTitle("Safe Area Portrait (X/Y/Width/Height)"),
- GridLayout.encloseIn(4, safePortraitX, safePortraitY, safePortraitWidth, safePortraitHeight),
- labeledFieldTitle("Safe Area Landscape (X/Y/Width/Height)"),
- GridLayout.encloseIn(4, safeLandscapeX, safeLandscapeY, safeLandscapeWidth, safeLandscapeHeight)
+ BoxLayout.encloseX(overrideNamePrimary, overrideNameSecondary, overrideNameLast)
);
settingsContainer.setUIID("SkinDesignerCard");
settingsContainer.setScrollableY(true);
@@ -183,9 +159,7 @@ public void runApp() {
Runnable saveAction = () -> {
byte[] data = createSkinFile(imPortraitRef[0], imLandscapeRef[0], nativeTheme, platformName, tablet, systemFontFamily,
proportionalFontFamily, monospaceFontFamily, smallFontSize, mediumFontSize, largeFontSize,
- pixelRatio, overrideNamePrimary, overrideNameSecondary, overrideNameLast,
- safePortraitX, safePortraitY, safePortraitWidth, safePortraitHeight,
- safeLandscapeX, safeLandscapeY, safeLandscapeWidth, safeLandscapeHeight);
+ pixelRatio, overrideNamePrimary, overrideNameSecondary, overrideNameLast);
if(data != null) {
FileSystemStorage fs = FileSystemStorage.getInstance();
try(OutputStream os = fs.openOutputStream(fs.getAppHomePath() + "skin-file.skin")) {
@@ -385,6 +359,10 @@ interface ImageSettings {
int getScreenY();
int getScreenWidth();
int getScreenHeight();
+ int getSafeX();
+ int getSafeY();
+ int getSafeWidth();
+ int getSafeHeight();
}
private void autoSave(TextArea ta, String preferencesKey) {
@@ -454,6 +432,11 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
final TextField screenHeightPixels = new TextField("480", "Height", 8, TextField.NUMERIC);
final TextField screenPositionX = new TextField("40", "X", 8, TextField.NUMERIC);
final TextField screenPositionY = new TextField("40", "Y", 8, TextField.NUMERIC);
+ final TextField safeX = new TextField("40", "Safe X", 8, TextField.NUMERIC);
+ final TextField safeY = new TextField("40", "Safe Y", 8, TextField.NUMERIC);
+ final TextField safeWidth = new TextField("320", "Safe Width", 8, TextField.NUMERIC);
+ final TextField safeHeight = new TextField("480", "Safe Height", 8, TextField.NUMERIC);
+ final TextField floodTolerance = new TextField("24", "Tolerance", 4, TextField.NUMERIC);
if("lan".equals(prefix) && Preferences.get("lanX", null) == null) {
String portraitX = Preferences.get("portX", null);
String portraitY = Preferences.get("portY", null);
@@ -464,17 +447,32 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
screenPositionY.setText(portraitX);
screenWidthPixels.setText(portraitHeight);
screenHeightPixels.setText(portraitWidth);
+ safeX.setText(portraitY);
+ safeY.setText(portraitX);
+ safeWidth.setText(portraitHeight);
+ safeHeight.setText(portraitWidth);
}
}
styleFields(screenWidthPixels, screenHeightPixels, screenPositionX, screenPositionY);
+ styleFields(safeX, safeY, safeWidth, safeHeight, floodTolerance);
autoSave(screenWidthPixels, prefix + "Width");
autoSave(screenHeightPixels, prefix + "Height");
autoSave(screenPositionX, prefix + "X");
autoSave(screenPositionY, prefix + "Y");
+ autoSave(safeX, prefix + "SafeX");
+ autoSave(safeY, prefix + "SafeY");
+ autoSave(safeWidth, prefix + "SafeWidth");
+ autoSave(safeHeight, prefix + "SafeHeight");
+ autoSave(floodTolerance, prefix + "FloodTolerance");
vl.addConstraint(screenWidthPixels, new NumericConstraint(false, 20, 5000, "Screen size must be a valid integer in the 20-5000 range")).
addConstraint(screenHeightPixels, new NumericConstraint(false, 20, 5000, "Screen size must be a valid integer in the 20-5000 range")).
addConstraint(screenPositionX, new NumericConstraint(false, 0, 5000, "Screen position must be a valid integer in the 0-5000 range")).
- addConstraint(screenPositionY, new NumericConstraint(false, 0, 5000, "Screen position must be a valid integer in the 0-5000 range"));
+ addConstraint(screenPositionY, new NumericConstraint(false, 0, 5000, "Screen position must be a valid integer in the 0-5000 range")).
+ addConstraint(safeX, new NumericConstraint(false, 0, 5000, "Safe area X must be a valid integer in the 0-5000 range")).
+ addConstraint(safeY, new NumericConstraint(false, 0, 5000, "Safe area Y must be a valid integer in the 0-5000 range")).
+ addConstraint(safeWidth, new NumericConstraint(false, 1, 5000, "Safe area width must be a valid integer in the 1-5000 range")).
+ addConstraint(safeHeight, new NumericConstraint(false, 1, 5000, "Safe area height must be a valid integer in the 1-5000 range")).
+ addConstraint(floodTolerance, new NumericConstraint(false, 0, 441, "Tolerance must be 0-441"));
if("lan".equals(prefix)) {
String portraitX = Preferences.get("portX", null);
String portraitY = Preferences.get("portY", null);
@@ -487,6 +485,10 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
screenPositionY.setText(portraitX);
screenWidthPixels.setText(portraitHeight);
screenHeightPixels.setText(portraitWidth);
+ safeX.setText(portraitY);
+ safeY.setText(portraitX);
+ safeWidth.setText(portraitHeight);
+ safeHeight.setText(portraitWidth);
}
}
@@ -497,6 +499,10 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
aimPosition(sl.getIcon(),
screenPositionX,
screenPositionY,
+ safeX,
+ safeY,
+ safeWidth,
+ safeHeight,
screenWidthPixels.getAsInt(768),
screenHeightPixels.getAsInt(1024)));
@@ -509,6 +515,30 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
saveButton.addActionListener(e -> saveCallback.run());
ScaleImageLabel maskLabel = new ScaleImageLabel();
+ Button detectScreenButton = new Button("Detect Screen by Color");
+ detectScreenButton.setUIID("SkinDesignerActionButton");
+ detectScreenButton.addActionListener(e -> {
+ Image source = sl.getIcon();
+ if(source == null) {
+ ToastBar.showErrorMessage("Please select a skin image first");
+ return;
+ }
+ Image generatedMask = createFloodFillMask(source,
+ screenPositionX.getAsInt(0),
+ screenPositionY.getAsInt(0),
+ floodTolerance.getAsInt(24));
+ if(generatedMask != null) {
+ maskLabel.setIcon(generatedMask);
+ maskLabel.getParent().revalidate();
+ try(OutputStream os = Storage.getInstance().createOutputStream(prefix + ".mask.png")) {
+ ImageIO.getImageIO().save(generatedMask, os, ImageIO.FORMAT_PNG, 1);
+ } catch(IOException err) {
+ Log.e(err);
+ ToastBar.showErrorMessage("Error saving generated mask: " + err.getMessage());
+ }
+ }
+ });
+
Button maskPicker = new Button("Select Screen Mask (Optional)");
maskPicker.setUIID("SkinDesignerActionButton");
maskPicker.addActionListener((e) -> {
@@ -539,6 +569,9 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
BorderLayout.center(labeledFieldTitle("Screen Position (X/Y/Width/Height)")).
add(BorderLayout.EAST, BoxLayout.encloseX(aim, helpButton, saveButton)),
GridLayout.encloseIn(4, screenPositionX, screenPositionY, screenWidthPixels, screenHeightPixels),
+ labeledFieldTitle("Safe Area (X/Y/Width/Height)"),
+ GridLayout.encloseIn(4, safeX, safeY, safeWidth, safeHeight),
+ BorderLayout.center(detectScreenButton).add(BorderLayout.EAST, floodTolerance),
maskPicker,
maskLabel,
sl);
@@ -626,9 +659,101 @@ public int getScreenWidth() {
public int getScreenHeight() {
return screenHeightPixels.getAsInt(50);
}
+
+ @Override
+ public int getSafeX() {
+ return safeX.getAsInt(getScreenX());
+ }
+
+ @Override
+ public int getSafeY() {
+ return safeY.getAsInt(getScreenY());
+ }
+
+ @Override
+ public int getSafeWidth() {
+ return safeWidth.getAsInt(getScreenWidth());
+ }
+
+ @Override
+ public int getSafeHeight() {
+ return safeHeight.getAsInt(getScreenHeight());
+ }
};
}
+ private Image createFloodFillMask(Image source, int seedX, int seedY, int tolerance) {
+ int width = source.getWidth();
+ int height = source.getHeight();
+ if(seedX < 0 || seedY < 0 || seedX >= width || seedY >= height) {
+ ToastBar.showErrorMessage("Seed point is outside image bounds");
+ return null;
+ }
+ int[] src = source.getRGB();
+ int[] mask = new int[src.length];
+ for(int i = 0; i < mask.length; i++) {
+ mask[i] = 0xffffffff;
+ }
+ boolean[] visited = new boolean[src.length];
+ int[] queue = new int[src.length];
+ int head = 0;
+ int tail = 0;
+ int seedIdx = seedY * width + seedX;
+ int seedColor = src[seedIdx];
+ queue[tail++] = seedIdx;
+ visited[seedIdx] = true;
+ while(head < tail) {
+ int idx = queue[head++];
+ int px = idx % width;
+ int py = idx / width;
+ if(colorDistance(src[idx], seedColor) <= tolerance) {
+ mask[idx] = 0xff000000;
+ if(px > 0) {
+ int n = idx - 1;
+ if(!visited[n]) {
+ visited[n] = true;
+ queue[tail++] = n;
+ }
+ }
+ if(px < width - 1) {
+ int n = idx + 1;
+ if(!visited[n]) {
+ visited[n] = true;
+ queue[tail++] = n;
+ }
+ }
+ if(py > 0) {
+ int n = idx - width;
+ if(!visited[n]) {
+ visited[n] = true;
+ queue[tail++] = n;
+ }
+ }
+ if(py < height - 1) {
+ int n = idx + width;
+ if(!visited[n]) {
+ visited[n] = true;
+ queue[tail++] = n;
+ }
+ }
+ }
+ }
+ return Image.createImage(mask, width, height);
+ }
+
+ private int colorDistance(int c1, int c2) {
+ int r1 = (c1 >> 16) & 0xff;
+ int g1 = (c1 >> 8) & 0xff;
+ int b1 = c1 & 0xff;
+ int r2 = (c2 >> 16) & 0xff;
+ int g2 = (c2 >> 8) & 0xff;
+ int b2 = c2 & 0xff;
+ int dr = r1 - r2;
+ int dg = g1 - g2;
+ int db = b1 - b2;
+ return (int)Math.sqrt(dr * dr + dg * dg + db * db);
+ }
+
private void showHelpForm(Form backForm) {
BrowserComponent help = new BrowserComponent();
help.setURL("jar:///help.html?theme=" + (websiteDarkMode ? "dark" : "light"));
@@ -645,19 +770,47 @@ private void showHelpForm(Form backForm) {
helpForm.show();
}
- private Image createMute(int x, int y, int w, int h, Image img) {
+ private Image createMute(int x, int y, int w, int h, int safeX, int safeY, int safeW, int safeH, Image img) {
Image mute = Image.createImage(img.getWidth(), img.getHeight(), 0);
Graphics g = mute.getGraphics();
g.setAlpha(150);
g.setColor(0);
g.fillRect(x, y, w, h);
+ g.setAlpha(80);
+ int checker = 12;
+ for(int yy = y; yy < y + h; yy += checker) {
+ for(int xx = x; xx < x + w; xx += checker) {
+ boolean dark = (((xx - x) / checker) + ((yy - y) / checker)) % 2 == 0;
+ g.setColor(dark ? 0x999999 : 0xcccccc);
+ g.fillRect(xx, yy, Math.min(checker, x + w - xx), Math.min(checker, y + h - yy));
+ }
+ }
+ g.setColor(0xff0000);
+ int safeRight = safeX + safeW;
+ int safeBottom = safeY + safeH;
+ int screenRight = x + w;
+ int screenBottom = y + h;
+ if(safeY > y) {
+ g.fillRect(x, y, w, Math.max(0, safeY - y));
+ }
+ if(safeX > x) {
+ g.fillRect(x, safeY, Math.max(0, safeX - x), Math.max(0, safeH));
+ }
+ if(safeRight < screenRight) {
+ g.fillRect(safeRight, safeY, Math.max(0, screenRight - safeRight), Math.max(0, safeH));
+ }
+ if(safeBottom < screenBottom) {
+ g.fillRect(x, safeBottom, w, Math.max(0, screenBottom - safeBottom));
+ }
g.setColor(0xff);
g.drawRect(x, y, w, h);
+ g.setColor(0xff0000);
+ g.drawRect(safeX, safeY, safeW, safeH);
g.setAlpha(255);
return mute;
}
- void aimPosition(final Image img, final TextField x, final TextField y, final int w, final int h) {
+ void aimPosition(final Image img, final TextField x, final TextField y, final TextField safeX, final TextField safeY, final TextField safeW, final TextField safeH, final int w, final int h) {
if(img == null) {
ToastBar.showErrorMessage("You need to pick a skin image first");
return;
@@ -680,7 +833,7 @@ void aimPosition(final Image img, final TextField x, final TextField y, final in
topActions.setUIID("SkinDesignerTabBar");
editPosition.add(BorderLayout.NORTH, topActions);
- Image mute = createMute(x.getAsInt(0), y.getAsInt(0), w, h, img);
+ Image mute = createMute(x.getAsInt(0), y.getAsInt(0), w, h, safeX.getAsInt(x.getAsInt(0)), safeY.getAsInt(y.getAsInt(0)), safeW.getAsInt(w), safeH.getAsInt(h), img);
class oo extends ImageViewer {
private int lastDragX = -1;
private int lastDragY = -1;
@@ -716,7 +869,7 @@ public void pointerDragged(int xPos, int yPos) {
int newY = Math.min(maxY, Math.max(0, y.getAsInt(0) + dy));
x.setText("" + newX);
y.setText("" + newY);
- setImageNoReposition(createMute(newX, newY, w, h, img));
+ setImageNoReposition(createMute(newX, newY, w, h, safeX.getAsInt(newX), safeY.getAsInt(newY), safeW.getAsInt(w), safeH.getAsInt(h), img));
lastDragX = xPos;
lastDragY = yPos;
}
@@ -780,37 +933,37 @@ public void keyReleased(int key) {
zoomIn.addActionListener(e -> {
iv.setZoom(iv.getZoom() + 1);
overlay.setZoom(iv.getZoom());
- overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, img));
+ overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, safeX.getAsInt(x.getAsInt(0)), safeY.getAsInt(y.getAsInt(0)), safeW.getAsInt(w), safeH.getAsInt(h), img));
});
zoomOut.addActionListener(e -> {
iv.setZoom(iv.getZoom() - 1);
overlay.setZoom(iv.getZoom());
- overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, img));
+ overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, safeX.getAsInt(x.getAsInt(0)), safeY.getAsInt(y.getAsInt(0)), safeW.getAsInt(w), safeH.getAsInt(h), img));
});
left.addActionListener(e -> {
int newX = x.getAsInt(0) - 1;
x.setText("" + newX);
- overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, img));
+ overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, safeX.getAsInt(x.getAsInt(0)), safeY.getAsInt(y.getAsInt(0)), safeW.getAsInt(w), safeH.getAsInt(h), img));
});
right.addActionListener(e -> {
int newX = x.getAsInt(0) + 1;
x.setText("" + newX);
- overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, img));
+ overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, safeX.getAsInt(x.getAsInt(0)), safeY.getAsInt(y.getAsInt(0)), safeW.getAsInt(w), safeH.getAsInt(h), img));
});
up.addActionListener(e -> {
int newY = y.getAsInt(0) - 1;
y.setText("" + newY);
- overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, img));
+ overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, safeX.getAsInt(x.getAsInt(0)), safeY.getAsInt(y.getAsInt(0)), safeW.getAsInt(w), safeH.getAsInt(h), img));
});
down.addActionListener(e -> {
int newY = y.getAsInt(0) + 1;
y.setText("" + newY);
- overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, img));
+ overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, safeX.getAsInt(x.getAsInt(0)), safeY.getAsInt(y.getAsInt(0)), safeW.getAsInt(w), safeH.getAsInt(h), img));
});
editPosition.add(BorderLayout.CENTER, LayeredLayout.encloseIn(iv, overlay,
@@ -827,7 +980,7 @@ private byte[] imageToByteArray(Image img) throws IOException {
return bo.toByteArray();
}
- byte[] createSkinFile(ImageSettings imPortrait, ImageSettings imLandscape, Picker nativeTheme, Picker platformName, OnOffSwitch tablet, TextField systemFontFamily, TextField proportionalFontFamily, TextField monospaceFontFamily, TextField smallFontSize, TextField mediumFontSize, TextField largeFontSize, TextField pixelRatio, Picker overrideNamePrimary, Picker overrideNameSecondary, Picker overrideNameLast, TextField safePortraitX, TextField safePortraitY, TextField safePortraitWidth, TextField safePortraitHeight, TextField safeLandscapeX, TextField safeLandscapeY, TextField safeLandscapeWidth, TextField safeLandscapeHeight) {
+ byte[] createSkinFile(ImageSettings imPortrait, ImageSettings imLandscape, Picker nativeTheme, Picker platformName, OnOffSwitch tablet, TextField systemFontFamily, TextField proportionalFontFamily, TextField monospaceFontFamily, TextField smallFontSize, TextField mediumFontSize, TextField largeFontSize, TextField pixelRatio, Picker overrideNamePrimary, Picker overrideNameSecondary, Picker overrideNameLast) {
Image portrait = imPortrait.getSkinImage();
Image landscape = imLandscape.getSkinImage();
if (portrait == null) {
@@ -883,14 +1036,14 @@ byte[] createSkinFile(ImageSettings imPortrait, ImageSettings imLandscape, Picke
props.put("overrideNames", overrideNamePrimary.getSelectedString() + "," +
overrideNameSecondary.getSelectedString() + "," +
overrideNameLast.getSelectedString());
- props.put("safePortraitX", safePortraitX.getText());
- props.put("safePortraitY", safePortraitY.getText());
- props.put("safePortraitWidth", safePortraitWidth.getText());
- props.put("safePortraitHeight", safePortraitHeight.getText());
- props.put("safeLandscapeX", safeLandscapeX.getText());
- props.put("safeLandscapeY", safeLandscapeY.getText());
- props.put("safeLandscapeWidth", safeLandscapeWidth.getText());
- props.put("safeLandscapeHeight", safeLandscapeHeight.getText());
+ props.put("safePortraitX", "" + imPortrait.getSafeX());
+ props.put("safePortraitY", "" + imPortrait.getSafeY());
+ props.put("safePortraitWidth", "" + imPortrait.getSafeWidth());
+ props.put("safePortraitHeight", "" + imPortrait.getSafeHeight());
+ props.put("safeLandscapeX", "" + imLandscape.getSafeX());
+ props.put("safeLandscapeY", "" + imLandscape.getSafeY());
+ props.put("safeLandscapeWidth", "" + imLandscape.getSafeWidth());
+ props.put("safeLandscapeHeight", "" + imLandscape.getSafeHeight());
ze = new ZipEntry("skin.properties");
zos.putNextEntry(ze);
From 6c59a7031e964b97667e98f4ed01f65abf27713b Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Sat, 18 Apr 2026 13:25:59 +0300
Subject: [PATCH 23/55] Refine screen detection UX and split-pane layout
---
.../tools/skindesigner/SkinDesigner.java | 38 +++++++++++++------
1 file changed, 27 insertions(+), 11 deletions(-)
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index 1560d6f3f1..7bef9c1fcd 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -4,6 +4,7 @@
import com.codename1.components.ImageViewer;
import com.codename1.components.OnOffSwitch;
import com.codename1.components.ScaleImageLabel;
+import com.codename1.components.SplitPane;
import com.codename1.components.ToastBar;
import com.codename1.io.FileSystemStorage;
import com.codename1.ui.Display;
@@ -436,7 +437,7 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
final TextField safeY = new TextField("40", "Safe Y", 8, TextField.NUMERIC);
final TextField safeWidth = new TextField("320", "Safe Width", 8, TextField.NUMERIC);
final TextField safeHeight = new TextField("480", "Safe Height", 8, TextField.NUMERIC);
- final TextField floodTolerance = new TextField("24", "Tolerance", 4, TextField.NUMERIC);
+ final TextField floodTolerance = new TextField("24", "Color Tol (0-441)", 4, TextField.NUMERIC);
if("lan".equals(prefix) && Preferences.get("lanX", null) == null) {
String portraitX = Preferences.get("portX", null);
String portraitY = Preferences.get("portY", null);
@@ -523,9 +524,11 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
ToastBar.showErrorMessage("Please select a skin image first");
return;
}
+ int seedX = screenPositionX.getAsInt(0) + (screenWidthPixels.getAsInt(0) / 2);
+ int seedY = screenPositionY.getAsInt(0) + (screenHeightPixels.getAsInt(0) / 2);
Image generatedMask = createFloodFillMask(source,
- screenPositionX.getAsInt(0),
- screenPositionY.getAsInt(0),
+ seedX,
+ seedY,
floodTolerance.getAsInt(24));
if(generatedMask != null) {
maskLabel.setIcon(generatedMask);
@@ -565,18 +568,31 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
}
}
- final Container cnt = BoxLayout.encloseY(imagePicker,
- BorderLayout.center(labeledFieldTitle("Screen Position (X/Y/Width/Height)")).
- add(BorderLayout.EAST, BoxLayout.encloseX(aim, helpButton, saveButton)),
+ Container actionButtons = FlowLayout.encloseCenter(aim, helpButton, saveButton);
+ Container detectionButtons = FlowLayout.encloseCenter(detectScreenButton, floodTolerance, maskPicker);
+ Label detectionHint = new Label("Uses flood-fill from the screen center");
+ detectionHint.setUIID("SkinDesignerFieldLabel");
+ Container controls = BoxLayout.encloseY(
+ imagePicker,
+ labeledFieldTitle("Screen Position (X/Y/Width/Height)"),
GridLayout.encloseIn(4, screenPositionX, screenPositionY, screenWidthPixels, screenHeightPixels),
labeledFieldTitle("Safe Area (X/Y/Width/Height)"),
GridLayout.encloseIn(4, safeX, safeY, safeWidth, safeHeight),
- BorderLayout.center(detectScreenButton).add(BorderLayout.EAST, floodTolerance),
- maskPicker,
- maskLabel,
- sl);
+ labeledFieldTitle("Screen Detection"),
+ detectionHint,
+ detectionButtons,
+ actionButtons
+ );
+ controls.setUIID("SkinDesignerCard");
+ controls.setScrollableY(true);
+ Container preview = BoxLayout.encloseY(maskLabel, sl);
+ preview.setUIID("SkinDesignerCard");
+ preview.setScrollableY(true);
+
+ int splitType = Display.getInstance().isPortrait() ? SplitPane.HORIZONTAL_SPLIT : SplitPane.VERTICAL_SPLIT;
+ Component split = new SplitPane(splitType, controls, preview, "35%", "45%", "55%");
+ final Container cnt = BorderLayout.center(split);
cnt.setUIID("SkinDesignerCard");
- cnt.setScrollableY(true);
return new ImageSettings() {
@Override
public Container getContainer() {
From 74702341e475ebf0b2cce414d02f6f2a9f3cb575 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Sat, 18 Apr 2026 19:06:07 +0300
Subject: [PATCH 24/55] Fix split orientation and tighten flood-fill detection
---
.../tools/skindesigner/SkinDesigner.java | 28 ++++++++++++-------
1 file changed, 18 insertions(+), 10 deletions(-)
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index 7bef9c1fcd..5f3b4baa67 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -45,9 +45,8 @@
import net.sf.zipme.ZipOutputStream;
public class SkinDesigner extends Lifecycle {
- private static final String[] NATIVE_THEMES = {"iOS 7+", "iOS 6", "Android 4 +","Android 2.x", "Windows"};
- private static final String[] NATIVE_THEME_FILES = {"iOS7Theme.res", "iPhoneTheme.res",
- "android_holo_light.res","androidTheme.res", "winTheme.res"};
+ private static final String[] NATIVE_THEMES = {"iOS", "Android", "Windows"};
+ private static final String[] NATIVE_THEME_FILES = {"iOS7Theme.res", "android_holo_light.res", "winTheme.res"};
private boolean websiteDarkMode;
@Override
@@ -589,7 +588,7 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
preview.setUIID("SkinDesignerCard");
preview.setScrollableY(true);
- int splitType = Display.getInstance().isPortrait() ? SplitPane.HORIZONTAL_SPLIT : SplitPane.VERTICAL_SPLIT;
+ int splitType = "lan".equals(prefix) ? SplitPane.VERTICAL_SPLIT : SplitPane.HORIZONTAL_SPLIT;
Component split = new SplitPane(splitType, controls, preview, "35%", "45%", "55%");
final Container cnt = BorderLayout.center(split);
cnt.setUIID("SkinDesignerCard");
@@ -716,13 +715,17 @@ private Image createFloodFillMask(Image source, int seedX, int seedY, int tolera
int tail = 0;
int seedIdx = seedY * width + seedX;
int seedColor = src[seedIdx];
+ if(((seedColor >>> 24) & 0xff) < 16) {
+ ToastBar.showErrorMessage("Detected seed point is transparent. Adjust screen position first.");
+ return null;
+ }
queue[tail++] = seedIdx;
visited[seedIdx] = true;
while(head < tail) {
int idx = queue[head++];
int px = idx % width;
int py = idx / width;
- if(colorDistance(src[idx], seedColor) <= tolerance) {
+ if(colorMatch(src[idx], seedColor, tolerance)) {
mask[idx] = 0xff000000;
if(px > 0) {
int n = idx - 1;
@@ -757,17 +760,22 @@ private Image createFloodFillMask(Image source, int seedX, int seedY, int tolera
return Image.createImage(mask, width, height);
}
- private int colorDistance(int c1, int c2) {
+ private boolean colorMatch(int c1, int c2, int tolerance) {
+ int a1 = (c1 >>> 24) & 0xff;
+ int a2 = (c2 >>> 24) & 0xff;
+ if(a1 < 16) {
+ return false;
+ }
int r1 = (c1 >> 16) & 0xff;
int g1 = (c1 >> 8) & 0xff;
int b1 = c1 & 0xff;
int r2 = (c2 >> 16) & 0xff;
int g2 = (c2 >> 8) & 0xff;
int b2 = c2 & 0xff;
- int dr = r1 - r2;
- int dg = g1 - g2;
- int db = b1 - b2;
- return (int)Math.sqrt(dr * dr + dg * dg + db * db);
+ return Math.abs(a1 - a2) <= tolerance &&
+ Math.abs(r1 - r2) <= tolerance &&
+ Math.abs(g1 - g2) <= tolerance &&
+ Math.abs(b1 - b2) <= tolerance;
}
private void showHelpForm(Form backForm) {
From c15de560c484ce62d06b8d65e1179560ec5b788b Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Sat, 18 Apr 2026 20:58:45 +0300
Subject: [PATCH 25/55] Make mask usage opt-in and handle transparent
flood-fill seeds
---
.../tools/skindesigner/SkinDesigner.java | 31 +++++++++++++------
1 file changed, 22 insertions(+), 9 deletions(-)
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index 5f3b4baa67..87f69ccfb3 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -515,6 +515,10 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
saveButton.addActionListener(e -> saveCallback.run());
ScaleImageLabel maskLabel = new ScaleImageLabel();
+ OnOffSwitch useMask = new OnOffSwitch();
+ useMask.setValue(false);
+ useMask.setUIID("SkinDesignerField");
+ autoSave(useMask, prefix + "UseMask");
Button detectScreenButton = new Button("Detect Screen by Color");
detectScreenButton.setUIID("SkinDesignerActionButton");
detectScreenButton.addActionListener(e -> {
@@ -531,6 +535,7 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
floodTolerance.getAsInt(24));
if(generatedMask != null) {
maskLabel.setIcon(generatedMask);
+ useMask.setValue(true);
maskLabel.getParent().revalidate();
try(OutputStream os = Storage.getInstance().createOutputStream(prefix + ".mask.png")) {
ImageIO.getImageIO().save(generatedMask, os, ImageIO.FORMAT_PNG, 1);
@@ -550,6 +555,7 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
String fileName = (String)ee.getSource();
Image mask = Image.createImage(fileName);
maskLabel.setIcon(mask);
+ useMask.setValue(true);
maskLabel.getParent().revalidate();
Util.copy(FileSystemStorage.getInstance().openInputStream(fileName),
Storage.getInstance().createOutputStream(prefix + ".mask.png"));
@@ -566,9 +572,19 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
Log.e(err);
}
}
+ Button clearMask = new Button("Clear Mask");
+ clearMask.setUIID("SkinDesignerActionButton");
+ clearMask.addActionListener(e -> {
+ maskLabel.setIcon(null);
+ useMask.setValue(false);
+ Storage.getInstance().deleteStorageFile(prefix + ".mask.png");
+ if(maskLabel.getParent() != null) {
+ maskLabel.getParent().revalidate();
+ }
+ });
Container actionButtons = FlowLayout.encloseCenter(aim, helpButton, saveButton);
- Container detectionButtons = FlowLayout.encloseCenter(detectScreenButton, floodTolerance, maskPicker);
+ Container detectionButtons = FlowLayout.encloseCenter(detectScreenButton, floodTolerance, maskPicker, clearMask);
Label detectionHint = new Label("Uses flood-fill from the screen center");
detectionHint.setUIID("SkinDesignerFieldLabel");
Container controls = BoxLayout.encloseY(
@@ -580,6 +596,7 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
labeledFieldTitle("Screen Detection"),
detectionHint,
detectionButtons,
+ BorderLayout.center(labeledFieldTitle("Use Screen Mask")).add(BorderLayout.EAST, useMask),
actionButtons
);
controls.setUIID("SkinDesignerCard");
@@ -605,7 +622,7 @@ public Image getSkinImage() {
int width = img.getWidth();
int height = img.getHeight();
Image mask = maskLabel.getIcon();
- if(mask != null && mask.getWidth() == width && mask.getHeight() == height) {
+ if(useMask.isValue() && mask != null && mask.getWidth() == width && mask.getHeight() == height) {
int[] maskRgb = mask.getRGB();
for(int i = 0 ; i < maskRgb.length ; i++) {
if(maskRgb[i] != 0xff000000) {
@@ -637,7 +654,7 @@ public Image createSkinOverlay() {
Graphics g = m.getGraphics();
g.setColor(0);
Image mask = maskLabel.getIcon();
- if(mask != null && mask.getWidth() == skinImage.getWidth() && mask.getHeight() == skinImage.getHeight()) {
+ if(useMask.isValue() && mask != null && mask.getWidth() == skinImage.getWidth() && mask.getHeight() == skinImage.getHeight()) {
int[] maskRgb = mask.getRGB();
int w = mask.getWidth();
int h = mask.getHeight();
@@ -715,10 +732,6 @@ private Image createFloodFillMask(Image source, int seedX, int seedY, int tolera
int tail = 0;
int seedIdx = seedY * width + seedX;
int seedColor = src[seedIdx];
- if(((seedColor >>> 24) & 0xff) < 16) {
- ToastBar.showErrorMessage("Detected seed point is transparent. Adjust screen position first.");
- return null;
- }
queue[tail++] = seedIdx;
visited[seedIdx] = true;
while(head < tail) {
@@ -763,8 +776,8 @@ private Image createFloodFillMask(Image source, int seedX, int seedY, int tolera
private boolean colorMatch(int c1, int c2, int tolerance) {
int a1 = (c1 >>> 24) & 0xff;
int a2 = (c2 >>> 24) & 0xff;
- if(a1 < 16) {
- return false;
+ if(a2 < 16) {
+ return a1 < 16;
}
int r1 = (c1 >> 16) & 0xff;
int g1 = (c1 >> 8) & 0xff;
From d6342a1528cca381ef87be8502564631c8baa1a7 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Sun, 19 Apr 2026 13:09:16 +0300
Subject: [PATCH 26/55] Make mask opt-in in UI and move safe area with pan
---
.../tools/skindesigner/SkinDesigner.java | 65 +++++++------------
1 file changed, 24 insertions(+), 41 deletions(-)
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index 87f69ccfb3..e1d1ccc7b0 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -546,25 +546,6 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
}
});
- Button maskPicker = new Button("Select Screen Mask (Optional)");
- maskPicker.setUIID("SkinDesignerActionButton");
- maskPicker.addActionListener((e) -> {
- Display.getInstance().openGallery((ee) -> {
- if(ee != null && ee.getSource() != null) {
- try {
- String fileName = (String)ee.getSource();
- Image mask = Image.createImage(fileName);
- maskLabel.setIcon(mask);
- useMask.setValue(true);
- maskLabel.getParent().revalidate();
- Util.copy(FileSystemStorage.getInstance().openInputStream(fileName),
- Storage.getInstance().createOutputStream(prefix + ".mask.png"));
- } catch(IOException err) {
- ToastBar.showErrorMessage("Error Loading Mask: " + err);
- }
- }
- }, Display.GALLERY_IMAGE);
- });
if(Storage.getInstance().exists(prefix + ".mask.png")) {
try(InputStream is = Storage.getInstance().createInputStream(prefix + ".mask.png")) {
maskLabel.setIcon(Image.createImage(is));
@@ -572,36 +553,22 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
Log.e(err);
}
}
- Button clearMask = new Button("Clear Mask");
- clearMask.setUIID("SkinDesignerActionButton");
- clearMask.addActionListener(e -> {
- maskLabel.setIcon(null);
- useMask.setValue(false);
- Storage.getInstance().deleteStorageFile(prefix + ".mask.png");
- if(maskLabel.getParent() != null) {
- maskLabel.getParent().revalidate();
- }
- });
Container actionButtons = FlowLayout.encloseCenter(aim, helpButton, saveButton);
- Container detectionButtons = FlowLayout.encloseCenter(detectScreenButton, floodTolerance, maskPicker, clearMask);
- Label detectionHint = new Label("Uses flood-fill from the screen center");
- detectionHint.setUIID("SkinDesignerFieldLabel");
+ Container detectionButtons = FlowLayout.encloseCenter(detectScreenButton, floodTolerance, useMask);
Container controls = BoxLayout.encloseY(
imagePicker,
labeledFieldTitle("Screen Position (X/Y/Width/Height)"),
GridLayout.encloseIn(4, screenPositionX, screenPositionY, screenWidthPixels, screenHeightPixels),
labeledFieldTitle("Safe Area (X/Y/Width/Height)"),
GridLayout.encloseIn(4, safeX, safeY, safeWidth, safeHeight),
- labeledFieldTitle("Screen Detection"),
- detectionHint,
+ labeledFieldTitle("Screen Mask Detection"),
detectionButtons,
- BorderLayout.center(labeledFieldTitle("Use Screen Mask")).add(BorderLayout.EAST, useMask),
actionButtons
);
controls.setUIID("SkinDesignerCard");
controls.setScrollableY(true);
- Container preview = BoxLayout.encloseY(maskLabel, sl);
+ Container preview = BoxLayout.encloseY(sl);
preview.setUIID("SkinDesignerCard");
preview.setScrollableY(true);
@@ -904,9 +871,17 @@ public void pointerDragged(int xPos, int yPos) {
int maxY = Math.max(0, img.getHeight() - h);
int newX = Math.min(maxX, Math.max(0, x.getAsInt(0) + dx));
int newY = Math.min(maxY, Math.max(0, y.getAsInt(0) + dy));
+ int safeWidthValue = safeW.getAsInt(w);
+ int safeHeightValue = safeH.getAsInt(h);
+ int safeMaxX = Math.max(0, img.getWidth() - safeWidthValue);
+ int safeMaxY = Math.max(0, img.getHeight() - safeHeightValue);
+ int newSafeX = Math.min(safeMaxX, Math.max(0, safeX.getAsInt(newX) + dx));
+ int newSafeY = Math.min(safeMaxY, Math.max(0, safeY.getAsInt(newY) + dy));
x.setText("" + newX);
y.setText("" + newY);
- setImageNoReposition(createMute(newX, newY, w, h, safeX.getAsInt(newX), safeY.getAsInt(newY), safeW.getAsInt(w), safeH.getAsInt(h), img));
+ safeX.setText("" + newSafeX);
+ safeY.setText("" + newSafeY);
+ setImageNoReposition(createMute(newX, newY, w, h, newSafeX, newSafeY, safeWidthValue, safeHeightValue, img));
lastDragX = xPos;
lastDragY = yPos;
}
@@ -980,26 +955,34 @@ public void keyReleased(int key) {
});
left.addActionListener(e -> {
- int newX = x.getAsInt(0) - 1;
+ int newX = Math.max(0, x.getAsInt(0) - 1);
x.setText("" + newX);
+ int safeMaxX = Math.max(0, img.getWidth() - safeW.getAsInt(w));
+ safeX.setText("" + Math.max(0, Math.min(safeMaxX, safeX.getAsInt(0) - 1)));
overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, safeX.getAsInt(x.getAsInt(0)), safeY.getAsInt(y.getAsInt(0)), safeW.getAsInt(w), safeH.getAsInt(h), img));
});
right.addActionListener(e -> {
- int newX = x.getAsInt(0) + 1;
+ int newX = Math.min(Math.max(0, img.getWidth() - w), x.getAsInt(0) + 1);
x.setText("" + newX);
+ int safeMaxX = Math.max(0, img.getWidth() - safeW.getAsInt(w));
+ safeX.setText("" + Math.max(0, Math.min(safeMaxX, safeX.getAsInt(0) + 1)));
overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, safeX.getAsInt(x.getAsInt(0)), safeY.getAsInt(y.getAsInt(0)), safeW.getAsInt(w), safeH.getAsInt(h), img));
});
up.addActionListener(e -> {
- int newY = y.getAsInt(0) - 1;
+ int newY = Math.max(0, y.getAsInt(0) - 1);
y.setText("" + newY);
+ int safeMaxY = Math.max(0, img.getHeight() - safeH.getAsInt(h));
+ safeY.setText("" + Math.max(0, Math.min(safeMaxY, safeY.getAsInt(0) - 1)));
overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, safeX.getAsInt(x.getAsInt(0)), safeY.getAsInt(y.getAsInt(0)), safeW.getAsInt(w), safeH.getAsInt(h), img));
});
down.addActionListener(e -> {
- int newY = y.getAsInt(0) + 1;
+ int newY = Math.min(Math.max(0, img.getHeight() - h), y.getAsInt(0) + 1);
y.setText("" + newY);
+ int safeMaxY = Math.max(0, img.getHeight() - safeH.getAsInt(h));
+ safeY.setText("" + Math.max(0, Math.min(safeMaxY, safeY.getAsInt(0) + 1)));
overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, safeX.getAsInt(x.getAsInt(0)), safeY.getAsInt(y.getAsInt(0)), safeW.getAsInt(w), safeH.getAsInt(h), img));
});
From aa80cc5dcc3a2e2466690f7d2257d95b1275fa9d Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Mon, 20 Apr 2026 07:52:59 +0300
Subject: [PATCH 27/55] Rewrite aim form to fix freezes, pan, and zoom drift
Replace the stacked ImageViewer + overlay with a single custom
Component. Eliminate per-drag full-image allocation from createMute,
add Screen/Safe/Pan mode toggles so the safe area can be moved
independently, and let zoom operate on one shared view so overlay
and skin no longer desync.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.../tools/skindesigner/SkinDesigner.java | 515 ++++++++++++------
1 file changed, 339 insertions(+), 176 deletions(-)
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index e1d1ccc7b0..897adc387d 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -1,7 +1,6 @@
package com.codename1.tools.skindesigner;
import com.codename1.system.Lifecycle;
-import com.codename1.components.ImageViewer;
import com.codename1.components.OnOffSwitch;
import com.codename1.components.ScaleImageLabel;
import com.codename1.components.SplitPane;
@@ -31,7 +30,6 @@
import com.codename1.ui.layouts.BoxLayout;
import com.codename1.ui.layouts.FlowLayout;
import com.codename1.ui.layouts.GridLayout;
-import com.codename1.ui.layouts.LayeredLayout;
import com.codename1.ui.spinner.Picker;
import com.codename1.ui.util.ImageIO;
import com.codename1.ui.util.UITimer;
@@ -774,44 +772,306 @@ private void showHelpForm(Form backForm) {
helpForm.show();
}
- private Image createMute(int x, int y, int w, int h, int safeX, int safeY, int safeW, int safeH, Image img) {
- Image mute = Image.createImage(img.getWidth(), img.getHeight(), 0);
- Graphics g = mute.getGraphics();
- g.setAlpha(150);
- g.setColor(0);
- g.fillRect(x, y, w, h);
- g.setAlpha(80);
- int checker = 12;
- for(int yy = y; yy < y + h; yy += checker) {
- for(int xx = x; xx < x + w; xx += checker) {
- boolean dark = (((xx - x) / checker) + ((yy - y) / checker)) % 2 == 0;
- g.setColor(dark ? 0x999999 : 0xcccccc);
- g.fillRect(xx, yy, Math.min(checker, x + w - xx), Math.min(checker, y + h - yy));
+ static final int AIM_MODE_SCREEN = 0;
+ static final int AIM_MODE_SAFE = 1;
+ static final int AIM_MODE_PAN = 2;
+
+ final class AimView extends Component {
+ private final Image img;
+ private final int imgW;
+ private final int imgH;
+ private final int screenW;
+ private final int screenH;
+ private final TextField xField;
+ private final TextField yField;
+ private final TextField safeXField;
+ private final TextField safeYField;
+ private final TextField safeWField;
+ private final TextField safeHField;
+ private int mode = AIM_MODE_SCREEN;
+ private float zoomMul = 1f;
+ private float panNX = 0.5f;
+ private float panNY = 0.5f;
+ private int lastPX = -1;
+ private int lastPY = -1;
+
+ AimView(Image img, TextField xField, TextField yField, TextField safeXField, TextField safeYField,
+ TextField safeWField, TextField safeHField, int screenW, int screenH) {
+ this.img = img;
+ this.imgW = img.getWidth();
+ this.imgH = img.getHeight();
+ this.xField = xField;
+ this.yField = yField;
+ this.safeXField = safeXField;
+ this.safeYField = safeYField;
+ this.safeWField = safeWField;
+ this.safeHField = safeHField;
+ this.screenW = screenW;
+ this.screenH = screenH;
+ setUIID("SkinDesignerCard");
+ setFocusable(true);
+ }
+
+ void setMode(int mode) {
+ this.mode = mode;
+ }
+
+ int getMode() {
+ return mode;
+ }
+
+ void zoomIn() {
+ float nz = Math.min(8f, zoomMul * 1.5f);
+ if (nz != zoomMul) {
+ zoomMul = nz;
+ repaint();
+ }
+ }
+
+ void zoomOut() {
+ float nz = Math.max(1f, zoomMul / 1.5f);
+ if (nz != zoomMul) {
+ zoomMul = nz;
+ if (zoomMul <= 1f) {
+ panNX = 0.5f;
+ panNY = 0.5f;
+ }
+ repaint();
+ }
+ }
+
+ void nudge(int dx, int dy) {
+ applyDelta(dx, dy);
+ repaint();
+ }
+
+ private float currentScale() {
+ int vw = getWidth();
+ int vh = getHeight();
+ if (vw <= 0 || vh <= 0 || imgW <= 0 || imgH <= 0) {
+ return 0f;
}
+ float fit = Math.min(((float) vw) / imgW, ((float) vh) / imgH);
+ return fit * zoomMul;
}
- g.setColor(0xff0000);
- int safeRight = safeX + safeW;
- int safeBottom = safeY + safeH;
- int screenRight = x + w;
- int screenBottom = y + h;
- if(safeY > y) {
- g.fillRect(x, y, w, Math.max(0, safeY - y));
+
+ private int drawOriginX(float scale) {
+ int vw = getWidth();
+ int drawW = Math.max(1, Math.round(imgW * scale));
+ if (drawW <= vw) {
+ return getX() + (vw - drawW) / 2;
+ }
+ int raw = getX() + vw / 2 - Math.round(panNX * drawW);
+ int min = getX() + vw - drawW;
+ int max = getX();
+ return Math.max(min, Math.min(max, raw));
}
- if(safeX > x) {
- g.fillRect(x, safeY, Math.max(0, safeX - x), Math.max(0, safeH));
+
+ private int drawOriginY(float scale) {
+ int vh = getHeight();
+ int drawH = Math.max(1, Math.round(imgH * scale));
+ if (drawH <= vh) {
+ return getY() + (vh - drawH) / 2;
+ }
+ int raw = getY() + vh / 2 - Math.round(panNY * drawH);
+ int min = getY() + vh - drawH;
+ int max = getY();
+ return Math.max(min, Math.min(max, raw));
}
- if(safeRight < screenRight) {
- g.fillRect(safeRight, safeY, Math.max(0, screenRight - safeRight), Math.max(0, safeH));
+
+ private int clamp(int v, int lo, int hi) {
+ if (hi < lo) {
+ hi = lo;
+ }
+ return Math.max(lo, Math.min(hi, v));
}
- if(safeBottom < screenBottom) {
- g.fillRect(x, safeBottom, w, Math.max(0, screenBottom - safeBottom));
+
+ private void applyDelta(int dImgX, int dImgY) {
+ if (dImgX == 0 && dImgY == 0) {
+ return;
+ }
+ if (mode == AIM_MODE_PAN) {
+ if (zoomMul > 1f) {
+ float scale = currentScale();
+ if (scale > 0f) {
+ float drawW = imgW * scale;
+ float drawH = imgH * scale;
+ float vw = getWidth();
+ float vh = getHeight();
+ if (drawW > vw) {
+ panNX = Math.max(0f, Math.min(1f, panNX - (dImgX * scale) / (drawW - vw)));
+ }
+ if (drawH > vh) {
+ panNY = Math.max(0f, Math.min(1f, panNY - (dImgY * scale) / (drawH - vh)));
+ }
+ }
+ }
+ return;
+ }
+ if (mode == AIM_MODE_SAFE) {
+ int saw = safeWField.getAsInt(screenW);
+ int sah = safeHField.getAsInt(screenH);
+ int curX = safeXField.getAsInt(0);
+ int curY = safeYField.getAsInt(0);
+ int newX = clamp(curX + dImgX, 0, imgW - saw);
+ int newY = clamp(curY + dImgY, 0, imgH - sah);
+ if (newX != curX) {
+ safeXField.setText("" + newX);
+ }
+ if (newY != curY) {
+ safeYField.setText("" + newY);
+ }
+ return;
+ }
+ int curX = xField.getAsInt(0);
+ int curY = yField.getAsInt(0);
+ int newX = clamp(curX + dImgX, 0, imgW - screenW);
+ int newY = clamp(curY + dImgY, 0, imgH - screenH);
+ int actualDx = newX - curX;
+ int actualDy = newY - curY;
+ if (actualDx != 0) {
+ xField.setText("" + newX);
+ }
+ if (actualDy != 0) {
+ yField.setText("" + newY);
+ }
+ if (actualDx != 0 || actualDy != 0) {
+ int saw = safeWField.getAsInt(screenW);
+ int sah = safeHField.getAsInt(screenH);
+ int curSafeX = safeXField.getAsInt(curX);
+ int curSafeY = safeYField.getAsInt(curY);
+ int newSafeX = clamp(curSafeX + actualDx, 0, imgW - saw);
+ int newSafeY = clamp(curSafeY + actualDy, 0, imgH - sah);
+ if (newSafeX != curSafeX) {
+ safeXField.setText("" + newSafeX);
+ }
+ if (newSafeY != curSafeY) {
+ safeYField.setText("" + newSafeY);
+ }
+ }
+ }
+
+ @Override
+ public void pointerPressed(int x, int y) {
+ lastPX = x;
+ lastPY = y;
+ }
+
+ @Override
+ public void pointerReleased(int x, int y) {
+ lastPX = -1;
+ lastPY = -1;
+ }
+
+ @Override
+ public void pointerDragged(int x, int y) {
+ if (lastPX < 0 || lastPY < 0) {
+ lastPX = x;
+ lastPY = y;
+ return;
+ }
+ int dpx = x - lastPX;
+ int dpy = y - lastPY;
+ float scale = currentScale();
+ if (scale <= 0f) {
+ lastPX = x;
+ lastPY = y;
+ return;
+ }
+ int dImgX = Math.round(dpx / scale);
+ int dImgY = Math.round(dpy / scale);
+ if (dImgX == 0 && dImgY == 0) {
+ return;
+ }
+ applyDelta(dImgX, dImgY);
+ lastPX = x;
+ lastPY = y;
+ repaint();
+ }
+
+ @Override
+ public void paint(Graphics g) {
+ super.paint(g);
+ float scale = currentScale();
+ if (scale <= 0f) {
+ return;
+ }
+ int drawW = Math.max(1, Math.round(imgW * scale));
+ int drawH = Math.max(1, Math.round(imgH * scale));
+ int drawX = drawOriginX(scale);
+ int drawY = drawOriginY(scale);
+
+ int[] clip = g.getClip();
+ g.pushClip();
+ g.clipRect(getX(), getY(), getWidth(), getHeight());
+
+ g.drawImage(img, drawX, drawY, drawW, drawH);
+
+ int screenImgX = xField.getAsInt(0);
+ int screenImgY = yField.getAsInt(0);
+ int safeImgX = safeXField.getAsInt(screenImgX);
+ int safeImgY = safeYField.getAsInt(screenImgY);
+ int safeImgW = safeWField.getAsInt(screenW);
+ int safeImgH = safeHField.getAsInt(screenH);
+
+ int sx = drawX + Math.round(screenImgX * scale);
+ int sy = drawY + Math.round(screenImgY * scale);
+ int sw = Math.max(1, Math.round(screenW * scale));
+ int sh = Math.max(1, Math.round(screenH * scale));
+ int fx = drawX + Math.round(safeImgX * scale);
+ int fy = drawY + Math.round(safeImgY * scale);
+ int fw = Math.max(1, Math.round(safeImgW * scale));
+ int fh = Math.max(1, Math.round(safeImgH * scale));
+
+ int oldAlpha = g.getAlpha();
+ int oldColor = g.getColor();
+
+ g.setAlpha(150);
+ g.setColor(0);
+ g.fillRect(sx, sy, sw, sh);
+
+ g.setAlpha(80);
+ int checker = Math.max(4, Math.round(12f * scale));
+ for (int yy = sy; yy < sy + sh; yy += checker) {
+ for (int xx = sx; xx < sx + sw; xx += checker) {
+ boolean dark = (((xx - sx) / checker) + ((yy - sy) / checker)) % 2 == 0;
+ g.setColor(dark ? 0x999999 : 0xcccccc);
+ int cw = Math.min(checker, sx + sw - xx);
+ int ch = Math.min(checker, sy + sh - yy);
+ g.fillRect(xx, yy, cw, ch);
+ }
+ }
+
+ g.setAlpha(150);
+ g.setColor(0xff0000);
+ int safeRight = fx + fw;
+ int safeBottom = fy + fh;
+ int screenRight = sx + sw;
+ int screenBottom = sy + sh;
+ if (fy > sy) {
+ g.fillRect(sx, sy, sw, Math.max(0, fy - sy));
+ }
+ if (fx > sx) {
+ g.fillRect(sx, fy, Math.max(0, fx - sx), Math.max(0, fh));
+ }
+ if (safeRight < screenRight) {
+ g.fillRect(safeRight, fy, Math.max(0, screenRight - safeRight), Math.max(0, fh));
+ }
+ if (safeBottom < screenBottom) {
+ g.fillRect(sx, safeBottom, sw, Math.max(0, screenBottom - safeBottom));
+ }
+
+ g.setAlpha(255);
+ g.setColor(mode == AIM_MODE_SCREEN ? 0x2f6bff : 0x00ffff);
+ g.drawRect(sx, sy, sw, sh);
+ g.setColor(mode == AIM_MODE_SAFE ? 0xffaa00 : 0xff0000);
+ g.drawRect(fx, fy, fw, fh);
+
+ g.setAlpha(oldAlpha);
+ g.setColor(oldColor);
+ g.popClip();
+ g.setClip(clip);
}
- g.setColor(0xff);
- g.drawRect(x, y, w, h);
- g.setColor(0xff0000);
- g.drawRect(safeX, safeY, safeW, safeH);
- g.setAlpha(255);
- return mute;
}
void aimPosition(final Image img, final TextField x, final TextField y, final TextField safeX, final TextField safeY, final TextField safeW, final TextField safeH, final int w, final int h) {
@@ -819,115 +1079,46 @@ void aimPosition(final Image img, final TextField x, final TextField y, final Te
ToastBar.showErrorMessage("You need to pick a skin image first");
return;
}
- String originalX = x.getText();
- String originalY = y.getText();
- Form editPosition = new Form("", new BorderLayout());
+ final String originalX = x.getText();
+ final String originalY = y.getText();
+ final String originalSafeX = safeX.getText();
+ final String originalSafeY = safeY.getText();
+ final Form editPosition = new Form("", new BorderLayout());
editPosition.setUIID("SkinDesignerForm");
Button done = new Button("Done");
styleActionButton(done, FontImage.MATERIAL_CHECK);
- done.addActionListener(e -> x.getComponentForm().showBack());
+ done.addActionListener(e -> editPosition.showBack());
Button cancel = new Button("Cancel");
styleActionButton(cancel, FontImage.MATERIAL_CANCEL);
cancel.addActionListener(e -> {
x.setText(originalX);
y.setText(originalY);
- x.getComponentForm().showBack();
+ safeX.setText(originalSafeX);
+ safeY.setText(originalSafeY);
+ editPosition.showBack();
});
Container topActions = GridLayout.encloseIn(2, cancel, done);
topActions.setUIID("SkinDesignerTabBar");
editPosition.add(BorderLayout.NORTH, topActions);
- Image mute = createMute(x.getAsInt(0), y.getAsInt(0), w, h, safeX.getAsInt(x.getAsInt(0)), safeY.getAsInt(y.getAsInt(0)), safeW.getAsInt(w), safeH.getAsInt(h), img);
- class oo extends ImageViewer {
- private int lastDragX = -1;
- private int lastDragY = -1;
-
- public oo(Image img) {
- super(img);
- }
- @Override
- public boolean pinch(float scale) {
- return super.pinch(scale);
- }
-
- @Override
- public void pointerPressed(int xPos, int yPos) {
- super.pointerPressed(xPos, yPos);
- lastDragX = xPos;
- lastDragY = yPos;
- }
-
- @Override
- public void pointerDragged(int xPos, int yPos) {
- if(lastDragX < 0 || lastDragY < 0) {
- lastDragX = xPos;
- lastDragY = yPos;
- return;
- }
- int dx = Math.round((xPos - lastDragX) / getZoom());
- int dy = Math.round((yPos - lastDragY) / getZoom());
- if(dx != 0 || dy != 0) {
- int maxX = Math.max(0, img.getWidth() - w);
- int maxY = Math.max(0, img.getHeight() - h);
- int newX = Math.min(maxX, Math.max(0, x.getAsInt(0) + dx));
- int newY = Math.min(maxY, Math.max(0, y.getAsInt(0) + dy));
- int safeWidthValue = safeW.getAsInt(w);
- int safeHeightValue = safeH.getAsInt(h);
- int safeMaxX = Math.max(0, img.getWidth() - safeWidthValue);
- int safeMaxY = Math.max(0, img.getHeight() - safeHeightValue);
- int newSafeX = Math.min(safeMaxX, Math.max(0, safeX.getAsInt(newX) + dx));
- int newSafeY = Math.min(safeMaxY, Math.max(0, safeY.getAsInt(newY) + dy));
- x.setText("" + newX);
- y.setText("" + newY);
- safeX.setText("" + newSafeX);
- safeY.setText("" + newSafeY);
- setImageNoReposition(createMute(newX, newY, w, h, newSafeX, newSafeY, safeWidthValue, safeHeightValue, img));
- lastDragX = xPos;
- lastDragY = yPos;
- }
- }
-
- @Override
- public void pointerReleased(int xPos, int yPos) {
- super.pointerReleased(xPos, yPos);
- lastDragX = -1;
- lastDragY = -1;
- }
+ final AimView view = new AimView(img, x, y, safeX, safeY, safeW, safeH, w, h);
+
+ Button modeScreen = new Button("Screen");
+ Button modeSafe = new Button("Safe");
+ Button modePan = new Button("Pan");
+ final Button[] modeButtons = { modeScreen, modeSafe, modePan };
+ final int[] modeValues = { AIM_MODE_SCREEN, AIM_MODE_SAFE, AIM_MODE_PAN };
+ updateModeButtons(modeButtons, view.getMode());
+ for (int i = 0; i < modeButtons.length; i++) {
+ final int target = modeValues[i];
+ modeButtons[i].addActionListener(e -> {
+ view.setMode(target);
+ updateModeButtons(modeButtons, target);
+ view.repaint();
+ });
}
- final oo overlay = new oo(mute);
- ImageViewer iv = new ImageViewer(img) {
- @Override
- public void pointerDragged(int x, int y) {
- super.pointerDragged(x, y);
- overlay.pointerDragged(x, y);
- }
-
- @Override
- protected boolean pinch(float scale) {
- boolean b = super.pinch(scale);
- overlay.pinch(scale);
- return b;
- }
-
- @Override
- public void pointerPressed(int x, int y) {
- super.pointerPressed(x, y);
- overlay.pointerPressed(x, y);
- }
-
- @Override
- public void pointerReleased(int x, int y) {
- super.pointerReleased(x, y);
- overlay.pointerReleased(x, y);
- }
-
- @Override
- public void keyReleased(int key) {
- super.keyReleased(key);
- overlay.keyReleased(key);
- }
- };
- overlay.setFocusable(false);
+ Container modeBar = GridLayout.encloseIn(3, modeScreen, modeSafe, modePan);
+ modeBar.setUIID("SkinDesignerTabBar");
Button zoomIn = new Button();
Button zoomOut = new Button();
@@ -942,57 +1133,29 @@ public void keyReleased(int key) {
FontImage.setMaterialIcon(up, FontImage.MATERIAL_KEYBOARD_ARROW_UP, 3);
FontImage.setMaterialIcon(down, FontImage.MATERIAL_KEYBOARD_ARROW_DOWN, 3);
- zoomIn.addActionListener(e -> {
- iv.setZoom(iv.getZoom() + 1);
- overlay.setZoom(iv.getZoom());
- overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, safeX.getAsInt(x.getAsInt(0)), safeY.getAsInt(y.getAsInt(0)), safeW.getAsInt(w), safeH.getAsInt(h), img));
- });
-
- zoomOut.addActionListener(e -> {
- iv.setZoom(iv.getZoom() - 1);
- overlay.setZoom(iv.getZoom());
- overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, safeX.getAsInt(x.getAsInt(0)), safeY.getAsInt(y.getAsInt(0)), safeW.getAsInt(w), safeH.getAsInt(h), img));
- });
-
- left.addActionListener(e -> {
- int newX = Math.max(0, x.getAsInt(0) - 1);
- x.setText("" + newX);
- int safeMaxX = Math.max(0, img.getWidth() - safeW.getAsInt(w));
- safeX.setText("" + Math.max(0, Math.min(safeMaxX, safeX.getAsInt(0) - 1)));
- overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, safeX.getAsInt(x.getAsInt(0)), safeY.getAsInt(y.getAsInt(0)), safeW.getAsInt(w), safeH.getAsInt(h), img));
- });
-
- right.addActionListener(e -> {
- int newX = Math.min(Math.max(0, img.getWidth() - w), x.getAsInt(0) + 1);
- x.setText("" + newX);
- int safeMaxX = Math.max(0, img.getWidth() - safeW.getAsInt(w));
- safeX.setText("" + Math.max(0, Math.min(safeMaxX, safeX.getAsInt(0) + 1)));
- overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, safeX.getAsInt(x.getAsInt(0)), safeY.getAsInt(y.getAsInt(0)), safeW.getAsInt(w), safeH.getAsInt(h), img));
- });
-
- up.addActionListener(e -> {
- int newY = Math.max(0, y.getAsInt(0) - 1);
- y.setText("" + newY);
- int safeMaxY = Math.max(0, img.getHeight() - safeH.getAsInt(h));
- safeY.setText("" + Math.max(0, Math.min(safeMaxY, safeY.getAsInt(0) - 1)));
- overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, safeX.getAsInt(x.getAsInt(0)), safeY.getAsInt(y.getAsInt(0)), safeW.getAsInt(w), safeH.getAsInt(h), img));
- });
-
- down.addActionListener(e -> {
- int newY = Math.min(Math.max(0, img.getHeight() - h), y.getAsInt(0) + 1);
- y.setText("" + newY);
- int safeMaxY = Math.max(0, img.getHeight() - safeH.getAsInt(h));
- safeY.setText("" + Math.max(0, Math.min(safeMaxY, safeY.getAsInt(0) + 1)));
- overlay.setImageNoReposition(createMute(x.getAsInt(0), y.getAsInt(0), w, h, safeX.getAsInt(x.getAsInt(0)), safeY.getAsInt(y.getAsInt(0)), safeW.getAsInt(w), safeH.getAsInt(h), img));
- });
-
- editPosition.add(BorderLayout.CENTER, LayeredLayout.encloseIn(iv, overlay,
- BorderLayout.south(GridLayout.encloseIn(6, zoomIn, zoomOut, left, right, up, down))));
+ zoomIn.addActionListener(e -> view.zoomIn());
+ zoomOut.addActionListener(e -> view.zoomOut());
+ left.addActionListener(e -> view.nudge(-1, 0));
+ right.addActionListener(e -> view.nudge(1, 0));
+ up.addActionListener(e -> view.nudge(0, -1));
+ down.addActionListener(e -> view.nudge(0, 1));
+
+ Container navBar = GridLayout.encloseIn(6, zoomIn, zoomOut, left, right, up, down);
+ Container south = BoxLayout.encloseY(modeBar, navBar);
+ editPosition.add(BorderLayout.CENTER, view);
+ editPosition.add(BorderLayout.SOUTH, south);
applyWebsiteTheme(editPosition, websiteDarkMode);
-
editPosition.show();
}
+ private void updateModeButtons(Button[] buttons, int selectedMode) {
+ for (int i = 0; i < buttons.length; i++) {
+ boolean selected = i == selectedMode;
+ String base = selected ? "SkinDesignerTabButtonSelected" : "SkinDesignerTabButton";
+ buttons[i].setUIID(websiteDarkMode ? base + "Dark" : base);
+ }
+ }
+
private byte[] imageToByteArray(Image img) throws IOException {
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ImageIO.getImageIO().save(img, bo, ImageIO.FORMAT_PNG, 1);
From b4c37b94fdaf157c56328dfc85af632781d26cdf Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Mon, 20 Apr 2026 16:57:59 +0300
Subject: [PATCH 28/55] Make safe area opt-in and polish aim form controls
Add a 'Use Safe Area' toggle on each image card; when off, the
safe area fields disable and the save file uses the screen values
for safe area. When on, defaults clamp safe area to stay inside the
screen and the aim form constrains safe-mode drags to the screen
rect.
Polish the aim form: style all nav buttons consistently with center
alignment, give every control a tooltip, show a ToastBar hint when
switching modes (especially Pan), give the form a title, and wire a
back command to the cancel action so ESC / system back still exits
cleanly even if the Cancel button is tapped through content.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.../tools/skindesigner/SkinDesigner.java | 203 +++++++++++++-----
1 file changed, 151 insertions(+), 52 deletions(-)
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index 897adc387d..0d7e579ea6 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -403,6 +403,7 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
ScaleImageLabel sl = new ScaleImageLabel(img);
Button imagePicker = new Button("Select Image");
imagePicker.setUIID("SkinDesignerActionButton");
+ imagePicker.setTooltip("Choose a skin image from your gallery");
imagePicker.addActionListener((e) -> {
Display.getInstance().openGallery((ee) -> {
if(ee != null && ee.getSource() != null) {
@@ -490,8 +491,47 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
}
}
+ final OnOffSwitch useSafeArea = new OnOffSwitch();
+ useSafeArea.setUIID("SkinDesignerField");
+ useSafeArea.setValue(false);
+ useSafeArea.setTooltip("Enable a separate safe area inside the screen");
+ autoSave(useSafeArea, prefix + "UseSafeArea");
+ Runnable applySafeEnabled = () -> {
+ boolean enabled = useSafeArea.isValue();
+ safeX.setEnabled(enabled);
+ safeY.setEnabled(enabled);
+ safeWidth.setEnabled(enabled);
+ safeHeight.setEnabled(enabled);
+ };
+ applySafeEnabled.run();
+ useSafeArea.addActionListener(e -> {
+ boolean enabled = useSafeArea.isValue();
+ if (enabled) {
+ int sx = screenPositionX.getAsInt(0);
+ int sy = screenPositionY.getAsInt(0);
+ int sw = screenWidthPixels.getAsInt(0);
+ int sh = screenHeightPixels.getAsInt(0);
+ int curX = safeX.getAsInt(sx);
+ int curY = safeY.getAsInt(sy);
+ int curW = safeWidth.getAsInt(sw);
+ int curH = safeHeight.getAsInt(sh);
+ if (curW < 1 || curW > sw) { curW = sw; }
+ if (curH < 1 || curH > sh) { curH = sh; }
+ if (curX < sx) { curX = sx; }
+ if (curY < sy) { curY = sy; }
+ if (curX + curW > sx + sw) { curX = sx + sw - curW; }
+ if (curY + curH > sy + sh) { curY = sy + sh - curH; }
+ safeX.setText("" + curX);
+ safeY.setText("" + curY);
+ safeWidth.setText("" + curW);
+ safeHeight.setText("" + curH);
+ }
+ applySafeEnabled.run();
+ });
+
Button aim = new Button();
styleIconActionButton(aim, FontImage.MATERIAL_PAN_TOOL);
+ aim.setTooltip("Visually position the screen and safe area");
aim.addActionListener(e ->
aimPosition(sl.getIcon(),
@@ -502,23 +542,28 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
safeWidth,
safeHeight,
screenWidthPixels.getAsInt(768),
- screenHeightPixels.getAsInt(1024)));
+ screenHeightPixels.getAsInt(1024),
+ useSafeArea));
Button helpButton = new Button();
styleIconActionButton(helpButton, FontImage.MATERIAL_HELP);
+ helpButton.setTooltip("Open help");
helpButton.addActionListener(e -> helpCallback.run());
Button saveButton = new Button();
styleIconActionButton(saveButton, FontImage.MATERIAL_SAVE);
+ saveButton.setTooltip("Save the skin file");
saveButton.addActionListener(e -> saveCallback.run());
ScaleImageLabel maskLabel = new ScaleImageLabel();
OnOffSwitch useMask = new OnOffSwitch();
useMask.setValue(false);
useMask.setUIID("SkinDesignerField");
+ useMask.setTooltip("Use the detected mask instead of a simple rectangle");
autoSave(useMask, prefix + "UseMask");
Button detectScreenButton = new Button("Detect Screen by Color");
detectScreenButton.setUIID("SkinDesignerActionButton");
+ detectScreenButton.setTooltip("Auto-detect the screen area via flood-fill from the center pixel");
detectScreenButton.addActionListener(e -> {
Image source = sl.getIcon();
if(source == null) {
@@ -554,11 +599,13 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
Container actionButtons = FlowLayout.encloseCenter(aim, helpButton, saveButton);
Container detectionButtons = FlowLayout.encloseCenter(detectScreenButton, floodTolerance, useMask);
+ Container safeAreaHeader = BorderLayout.center(labeledFieldTitle("Safe Area (X/Y/Width/Height)"))
+ .add(BorderLayout.EAST, useSafeArea);
Container controls = BoxLayout.encloseY(
imagePicker,
labeledFieldTitle("Screen Position (X/Y/Width/Height)"),
GridLayout.encloseIn(4, screenPositionX, screenPositionY, screenWidthPixels, screenHeightPixels),
- labeledFieldTitle("Safe Area (X/Y/Width/Height)"),
+ safeAreaHeader,
GridLayout.encloseIn(4, safeX, safeY, safeWidth, safeHeight),
labeledFieldTitle("Screen Mask Detection"),
detectionButtons,
@@ -659,22 +706,22 @@ public int getScreenHeight() {
@Override
public int getSafeX() {
- return safeX.getAsInt(getScreenX());
+ return useSafeArea.isValue() ? safeX.getAsInt(getScreenX()) : getScreenX();
}
@Override
public int getSafeY() {
- return safeY.getAsInt(getScreenY());
+ return useSafeArea.isValue() ? safeY.getAsInt(getScreenY()) : getScreenY();
}
@Override
public int getSafeWidth() {
- return safeWidth.getAsInt(getScreenWidth());
+ return useSafeArea.isValue() ? safeWidth.getAsInt(getScreenWidth()) : getScreenWidth();
}
@Override
public int getSafeHeight() {
- return safeHeight.getAsInt(getScreenHeight());
+ return useSafeArea.isValue() ? safeHeight.getAsInt(getScreenHeight()) : getScreenHeight();
}
};
}
@@ -788,6 +835,7 @@ final class AimView extends Component {
private final TextField safeYField;
private final TextField safeWField;
private final TextField safeHField;
+ private final OnOffSwitch useSafeArea;
private int mode = AIM_MODE_SCREEN;
private float zoomMul = 1f;
private float panNX = 0.5f;
@@ -796,7 +844,7 @@ final class AimView extends Component {
private int lastPY = -1;
AimView(Image img, TextField xField, TextField yField, TextField safeXField, TextField safeYField,
- TextField safeWField, TextField safeHField, int screenW, int screenH) {
+ TextField safeWField, TextField safeHField, int screenW, int screenH, OnOffSwitch useSafeArea) {
this.img = img;
this.imgW = img.getWidth();
this.imgH = img.getHeight();
@@ -808,10 +856,15 @@ final class AimView extends Component {
this.safeHField = safeHField;
this.screenW = screenW;
this.screenH = screenH;
+ this.useSafeArea = useSafeArea;
setUIID("SkinDesignerCard");
setFocusable(true);
}
+ boolean isSafeAreaEnabled() {
+ return useSafeArea != null && useSafeArea.isValue();
+ }
+
void setMode(int mode) {
this.mode = mode;
}
@@ -909,12 +962,17 @@ private void applyDelta(int dImgX, int dImgY) {
return;
}
if (mode == AIM_MODE_SAFE) {
- int saw = safeWField.getAsInt(screenW);
- int sah = safeHField.getAsInt(screenH);
- int curX = safeXField.getAsInt(0);
- int curY = safeYField.getAsInt(0);
- int newX = clamp(curX + dImgX, 0, imgW - saw);
- int newY = clamp(curY + dImgY, 0, imgH - sah);
+ if (!isSafeAreaEnabled()) {
+ return;
+ }
+ int sx = xField.getAsInt(0);
+ int sy = yField.getAsInt(0);
+ int saw = Math.min(safeWField.getAsInt(screenW), screenW);
+ int sah = Math.min(safeHField.getAsInt(screenH), screenH);
+ int curX = safeXField.getAsInt(sx);
+ int curY = safeYField.getAsInt(sy);
+ int newX = clamp(curX + dImgX, sx, sx + screenW - saw);
+ int newY = clamp(curY + dImgY, sy, sy + screenH - sah);
if (newX != curX) {
safeXField.setText("" + newX);
}
@@ -935,13 +993,13 @@ private void applyDelta(int dImgX, int dImgY) {
if (actualDy != 0) {
yField.setText("" + newY);
}
- if (actualDx != 0 || actualDy != 0) {
- int saw = safeWField.getAsInt(screenW);
- int sah = safeHField.getAsInt(screenH);
+ if ((actualDx != 0 || actualDy != 0) && isSafeAreaEnabled()) {
+ int saw = Math.min(safeWField.getAsInt(screenW), screenW);
+ int sah = Math.min(safeHField.getAsInt(screenH), screenH);
int curSafeX = safeXField.getAsInt(curX);
int curSafeY = safeYField.getAsInt(curY);
- int newSafeX = clamp(curSafeX + actualDx, 0, imgW - saw);
- int newSafeY = clamp(curSafeY + actualDy, 0, imgH - sah);
+ int newSafeX = clamp(curSafeX + actualDx, newX, newX + screenW - saw);
+ int newSafeY = clamp(curSafeY + actualDy, newY, newY + screenH - sah);
if (newSafeX != curSafeX) {
safeXField.setText("" + newSafeX);
}
@@ -1009,10 +1067,11 @@ public void paint(Graphics g) {
int screenImgX = xField.getAsInt(0);
int screenImgY = yField.getAsInt(0);
- int safeImgX = safeXField.getAsInt(screenImgX);
- int safeImgY = safeYField.getAsInt(screenImgY);
- int safeImgW = safeWField.getAsInt(screenW);
- int safeImgH = safeHField.getAsInt(screenH);
+ boolean safeOn = isSafeAreaEnabled();
+ int safeImgX = safeOn ? safeXField.getAsInt(screenImgX) : screenImgX;
+ int safeImgY = safeOn ? safeYField.getAsInt(screenImgY) : screenImgY;
+ int safeImgW = safeOn ? safeWField.getAsInt(screenW) : screenW;
+ int safeImgH = safeOn ? safeHField.getAsInt(screenH) : screenH;
int sx = drawX + Math.round(screenImgX * scale);
int sy = drawY + Math.round(screenImgY * scale);
@@ -1042,30 +1101,34 @@ public void paint(Graphics g) {
}
}
- g.setAlpha(150);
- g.setColor(0xff0000);
- int safeRight = fx + fw;
- int safeBottom = fy + fh;
- int screenRight = sx + sw;
- int screenBottom = sy + sh;
- if (fy > sy) {
- g.fillRect(sx, sy, sw, Math.max(0, fy - sy));
- }
- if (fx > sx) {
- g.fillRect(sx, fy, Math.max(0, fx - sx), Math.max(0, fh));
- }
- if (safeRight < screenRight) {
- g.fillRect(safeRight, fy, Math.max(0, screenRight - safeRight), Math.max(0, fh));
- }
- if (safeBottom < screenBottom) {
- g.fillRect(sx, safeBottom, sw, Math.max(0, screenBottom - safeBottom));
+ if (safeOn) {
+ g.setAlpha(150);
+ g.setColor(0xff0000);
+ int safeRight = fx + fw;
+ int safeBottom = fy + fh;
+ int screenRight = sx + sw;
+ int screenBottom = sy + sh;
+ if (fy > sy) {
+ g.fillRect(sx, sy, sw, Math.max(0, fy - sy));
+ }
+ if (fx > sx) {
+ g.fillRect(sx, fy, Math.max(0, fx - sx), Math.max(0, fh));
+ }
+ if (safeRight < screenRight) {
+ g.fillRect(safeRight, fy, Math.max(0, screenRight - safeRight), Math.max(0, fh));
+ }
+ if (safeBottom < screenBottom) {
+ g.fillRect(sx, safeBottom, sw, Math.max(0, screenBottom - safeBottom));
+ }
}
g.setAlpha(255);
g.setColor(mode == AIM_MODE_SCREEN ? 0x2f6bff : 0x00ffff);
g.drawRect(sx, sy, sw, sh);
- g.setColor(mode == AIM_MODE_SAFE ? 0xffaa00 : 0xff0000);
- g.drawRect(fx, fy, fw, fh);
+ if (safeOn) {
+ g.setColor(mode == AIM_MODE_SAFE ? 0xffaa00 : 0xff0000);
+ g.drawRect(fx, fy, fw, fh);
+ }
g.setAlpha(oldAlpha);
g.setColor(oldColor);
@@ -1074,7 +1137,7 @@ public void paint(Graphics g) {
}
}
- void aimPosition(final Image img, final TextField x, final TextField y, final TextField safeX, final TextField safeY, final TextField safeW, final TextField safeH, final int w, final int h) {
+ void aimPosition(final Image img, final TextField x, final TextField y, final TextField safeX, final TextField safeY, final TextField safeW, final TextField safeH, final int w, final int h, final OnOffSwitch useSafeArea) {
if(img == null) {
ToastBar.showErrorMessage("You need to pick a skin image first");
return;
@@ -1083,29 +1146,49 @@ void aimPosition(final Image img, final TextField x, final TextField y, final Te
final String originalY = y.getText();
final String originalSafeX = safeX.getText();
final String originalSafeY = safeY.getText();
- final Form editPosition = new Form("", new BorderLayout());
+ final Form editPosition = new Form("Positioning", new BorderLayout());
editPosition.setUIID("SkinDesignerForm");
+
+ final AimView view = new AimView(img, x, y, safeX, safeY, safeW, safeH, w, h, useSafeArea);
+
Button done = new Button("Done");
styleActionButton(done, FontImage.MATERIAL_CHECK);
+ done.setTooltip("Keep changes and return");
done.addActionListener(e -> editPosition.showBack());
Button cancel = new Button("Cancel");
styleActionButton(cancel, FontImage.MATERIAL_CANCEL);
- cancel.addActionListener(e -> {
+ cancel.setTooltip("Discard changes and return");
+ Runnable cancelAction = () -> {
x.setText(originalX);
y.setText(originalY);
safeX.setText(originalSafeX);
safeY.setText(originalSafeY);
editPosition.showBack();
+ };
+ cancel.addActionListener(e -> cancelAction.run());
+ editPosition.setBackCommand(new com.codename1.ui.Command("Cancel") {
+ @Override
+ public void actionPerformed(com.codename1.ui.events.ActionEvent evt) {
+ cancelAction.run();
+ }
});
+
Container topActions = GridLayout.encloseIn(2, cancel, done);
topActions.setUIID("SkinDesignerTabBar");
editPosition.add(BorderLayout.NORTH, topActions);
- final AimView view = new AimView(img, x, y, safeX, safeY, safeW, safeH, w, h);
-
+ final boolean safeEnabled = useSafeArea != null && useSafeArea.isValue();
Button modeScreen = new Button("Screen");
Button modeSafe = new Button("Safe");
Button modePan = new Button("Pan");
+ modeScreen.setTooltip("Drag to reposition the screen area");
+ modeSafe.setTooltip(safeEnabled
+ ? "Drag to adjust the safe area within the screen"
+ : "Enable 'Use Safe Area' on the previous screen to edit safe area");
+ modePan.setTooltip("Drag to scroll the zoomed view");
+ if (!safeEnabled) {
+ modeSafe.setEnabled(false);
+ }
final Button[] modeButtons = { modeScreen, modeSafe, modePan };
final int[] modeValues = { AIM_MODE_SCREEN, AIM_MODE_SAFE, AIM_MODE_PAN };
updateModeButtons(modeButtons, view.getMode());
@@ -1114,6 +1197,16 @@ void aimPosition(final Image img, final TextField x, final TextField y, final Te
modeButtons[i].addActionListener(e -> {
view.setMode(target);
updateModeButtons(modeButtons, target);
+ if (target == AIM_MODE_PAN) {
+ ToastBar.showMessage("Pan mode: zoom in and drag to scroll the view",
+ FontImage.MATERIAL_PAN_TOOL, 2500);
+ } else if (target == AIM_MODE_SAFE) {
+ ToastBar.showMessage("Safe mode: drag to adjust the safe area inside the screen",
+ FontImage.MATERIAL_CROP_FREE, 2500);
+ } else if (target == AIM_MODE_SCREEN) {
+ ToastBar.showMessage("Screen mode: drag to reposition the screen area",
+ FontImage.MATERIAL_STAY_CURRENT_PORTRAIT, 2000);
+ }
view.repaint();
});
}
@@ -1126,12 +1219,18 @@ void aimPosition(final Image img, final TextField x, final TextField y, final Te
Button right = new Button();
Button up = new Button();
Button down = new Button();
- FontImage.setMaterialIcon(zoomIn, FontImage.MATERIAL_ZOOM_IN, 3);
- FontImage.setMaterialIcon(zoomOut, FontImage.MATERIAL_ZOOM_OUT, 3);
- FontImage.setMaterialIcon(left, FontImage.MATERIAL_KEYBOARD_ARROW_LEFT, 3);
- FontImage.setMaterialIcon(right, FontImage.MATERIAL_KEYBOARD_ARROW_RIGHT, 3);
- FontImage.setMaterialIcon(up, FontImage.MATERIAL_KEYBOARD_ARROW_UP, 3);
- FontImage.setMaterialIcon(down, FontImage.MATERIAL_KEYBOARD_ARROW_DOWN, 3);
+ styleIconActionButton(zoomIn, FontImage.MATERIAL_ZOOM_IN);
+ styleIconActionButton(zoomOut, FontImage.MATERIAL_ZOOM_OUT);
+ styleIconActionButton(left, FontImage.MATERIAL_KEYBOARD_ARROW_LEFT);
+ styleIconActionButton(right, FontImage.MATERIAL_KEYBOARD_ARROW_RIGHT);
+ styleIconActionButton(up, FontImage.MATERIAL_KEYBOARD_ARROW_UP);
+ styleIconActionButton(down, FontImage.MATERIAL_KEYBOARD_ARROW_DOWN);
+ zoomIn.setTooltip("Zoom in");
+ zoomOut.setTooltip("Zoom out");
+ left.setTooltip("Nudge left (1 px)");
+ right.setTooltip("Nudge right (1 px)");
+ up.setTooltip("Nudge up (1 px)");
+ down.setTooltip("Nudge down (1 px)");
zoomIn.addActionListener(e -> view.zoomIn());
zoomOut.addActionListener(e -> view.zoomOut());
From 3ed9338f49ac5ac14cc0d45da719f1dc6deb5bc0 Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Mon, 20 Apr 2026 21:19:48 +0300
Subject: [PATCH 29/55] Fix aim form navigation, drop Pan, allow numeric +
resize edits
- Save the calling form on entry and call previousForm.showBack() on
Done/Cancel. Form.showBack() re-shows the form it is called on, so
calling it on the aim form was a no-op; that is why Done and Cancel
appeared unresponsive.
- Remove the Pan mode button and constant. Zoom in/out now auto-center
the view on the currently active rectangle, which covers the use
case Pan mode was supposed to address.
- Add mirrored X / Y / W / H TextFields inside the aim form for both
screen and safe area. Edits commit to the main form fields and the
view repaints; drags/nudges propagate back into the mirror fields.
- Add drag-to-resize on the safe area: press near an edge or corner
to resize, press in the interior to move. Draw small handles at the
corners and edge midpoints when Safe mode is active.
- Show a single entry ToastBar when the aim form opens that explains
how to drag, resize, and enter numeric values.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.../tools/skindesigner/SkinDesigner.java | 439 +++++++++++++-----
1 file changed, 313 insertions(+), 126 deletions(-)
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
index 0d7e579ea6..c3137e1241 100644
--- a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinDesigner.java
@@ -537,12 +537,12 @@ private ImageSettings createImageSettings(String imageFile, String prefix, Valid
aimPosition(sl.getIcon(),
screenPositionX,
screenPositionY,
+ screenWidthPixels,
+ screenHeightPixels,
safeX,
safeY,
safeWidth,
safeHeight,
- screenWidthPixels.getAsInt(768),
- screenHeightPixels.getAsInt(1024),
useSafeArea));
Button helpButton = new Button();
@@ -821,46 +821,76 @@ private void showHelpForm(Form backForm) {
static final int AIM_MODE_SCREEN = 0;
static final int AIM_MODE_SAFE = 1;
- static final int AIM_MODE_PAN = 2;
+
+ static final int DRAG_NONE = 0;
+ static final int DRAG_MOVE = 1;
+ static final int DRAG_N = 1 << 1;
+ static final int DRAG_S = 1 << 2;
+ static final int DRAG_W = 1 << 3;
+ static final int DRAG_E = 1 << 4;
+ static final int MIN_SAFE_SIZE = 10;
final class AimView extends Component {
private final Image img;
private final int imgW;
private final int imgH;
- private final int screenW;
- private final int screenH;
private final TextField xField;
private final TextField yField;
+ private final TextField screenWField;
+ private final TextField screenHField;
private final TextField safeXField;
private final TextField safeYField;
private final TextField safeWField;
private final TextField safeHField;
private final OnOffSwitch useSafeArea;
+ private Runnable onChange;
private int mode = AIM_MODE_SCREEN;
private float zoomMul = 1f;
private float panNX = 0.5f;
private float panNY = 0.5f;
private int lastPX = -1;
private int lastPY = -1;
+ private int dragOp = DRAG_NONE;
- AimView(Image img, TextField xField, TextField yField, TextField safeXField, TextField safeYField,
- TextField safeWField, TextField safeHField, int screenW, int screenH, OnOffSwitch useSafeArea) {
+ AimView(Image img, TextField xField, TextField yField,
+ TextField screenWField, TextField screenHField,
+ TextField safeXField, TextField safeYField,
+ TextField safeWField, TextField safeHField,
+ OnOffSwitch useSafeArea) {
this.img = img;
this.imgW = img.getWidth();
this.imgH = img.getHeight();
this.xField = xField;
this.yField = yField;
+ this.screenWField = screenWField;
+ this.screenHField = screenHField;
this.safeXField = safeXField;
this.safeYField = safeYField;
this.safeWField = safeWField;
this.safeHField = safeHField;
- this.screenW = screenW;
- this.screenH = screenH;
this.useSafeArea = useSafeArea;
setUIID("SkinDesignerCard");
setFocusable(true);
}
+ void setOnChange(Runnable onChange) {
+ this.onChange = onChange;
+ }
+
+ private void notifyChanged() {
+ if (onChange != null) {
+ onChange.run();
+ }
+ }
+
+ int screenW() {
+ return Math.max(1, screenWField.getAsInt(1));
+ }
+
+ int screenH() {
+ return Math.max(1, screenHField.getAsInt(1));
+ }
+
boolean isSafeAreaEnabled() {
return useSafeArea != null && useSafeArea.isValue();
}
@@ -877,6 +907,7 @@ void zoomIn() {
float nz = Math.min(8f, zoomMul * 1.5f);
if (nz != zoomMul) {
zoomMul = nz;
+ centerOnActiveRect();
repaint();
}
}
@@ -888,13 +919,35 @@ void zoomOut() {
if (zoomMul <= 1f) {
panNX = 0.5f;
panNY = 0.5f;
+ } else {
+ centerOnActiveRect();
}
repaint();
}
}
+ private void centerOnActiveRect() {
+ int cx, cy;
+ if (mode == AIM_MODE_SAFE && isSafeAreaEnabled()) {
+ cx = safeXField.getAsInt(0) + safeWField.getAsInt(screenW()) / 2;
+ cy = safeYField.getAsInt(0) + safeHField.getAsInt(screenH()) / 2;
+ } else {
+ cx = xField.getAsInt(0) + screenW() / 2;
+ cy = yField.getAsInt(0) + screenH() / 2;
+ }
+ if (imgW > 0) {
+ panNX = Math.max(0f, Math.min(1f, ((float) cx) / imgW));
+ }
+ if (imgH > 0) {
+ panNY = Math.max(0f, Math.min(1f, ((float) cy) / imgH));
+ }
+ }
+
void nudge(int dx, int dy) {
+ int savedOp = dragOp;
+ dragOp = DRAG_MOVE;
applyDelta(dx, dy);
+ dragOp = savedOp;
repaint();
}
@@ -940,85 +993,133 @@ private int clamp(int v, int lo, int hi) {
}
private void applyDelta(int dImgX, int dImgY) {
- if (dImgX == 0 && dImgY == 0) {
- return;
- }
- if (mode == AIM_MODE_PAN) {
- if (zoomMul > 1f) {
- float scale = currentScale();
- if (scale > 0f) {
- float drawW = imgW * scale;
- float drawH = imgH * scale;
- float vw = getWidth();
- float vh = getHeight();
- if (drawW > vw) {
- panNX = Math.max(0f, Math.min(1f, panNX - (dImgX * scale) / (drawW - vw)));
- }
- if (drawH > vh) {
- panNY = Math.max(0f, Math.min(1f, panNY - (dImgY * scale) / (drawH - vh)));
- }
- }
- }
+ if (dImgX == 0 && dImgY == 0 || dragOp == DRAG_NONE) {
return;
}
+ int sW = screenW();
+ int sH = screenH();
if (mode == AIM_MODE_SAFE) {
if (!isSafeAreaEnabled()) {
return;
}
- int sx = xField.getAsInt(0);
- int sy = yField.getAsInt(0);
- int saw = Math.min(safeWField.getAsInt(screenW), screenW);
- int sah = Math.min(safeHField.getAsInt(screenH), screenH);
- int curX = safeXField.getAsInt(sx);
- int curY = safeYField.getAsInt(sy);
- int newX = clamp(curX + dImgX, sx, sx + screenW - saw);
- int newY = clamp(curY + dImgY, sy, sy + screenH - sah);
- if (newX != curX) {
- safeXField.setText("" + newX);
- }
- if (newY != curY) {
- safeYField.setText("" + newY);
- }
+ int boundsX = xField.getAsInt(0);
+ int boundsY = yField.getAsInt(0);
+ int boundsW = sW;
+ int boundsH = sH;
+ int curX = safeXField.getAsInt(boundsX);
+ int curY = safeYField.getAsInt(boundsY);
+ int curW = Math.min(safeWField.getAsInt(boundsW), boundsW);
+ int curH = Math.min(safeHField.getAsInt(boundsH), boundsH);
+ int[] rect = applyResize(curX, curY, curW, curH, dImgX, dImgY,
+ boundsX, boundsY, boundsW, boundsH);
+ boolean changed = false;
+ if (rect[0] != curX) { safeXField.setText("" + rect[0]); changed = true; }
+ if (rect[1] != curY) { safeYField.setText("" + rect[1]); changed = true; }
+ if (rect[2] != curW) { safeWField.setText("" + rect[2]); changed = true; }
+ if (rect[3] != curH) { safeHField.setText("" + rect[3]); changed = true; }
+ if (changed) { notifyChanged(); }
return;
}
int curX = xField.getAsInt(0);
int curY = yField.getAsInt(0);
- int newX = clamp(curX + dImgX, 0, imgW - screenW);
- int newY = clamp(curY + dImgY, 0, imgH - screenH);
+ int newX = clamp(curX + dImgX, 0, imgW - sW);
+ int newY = clamp(curY + dImgY, 0, imgH - sH);
int actualDx = newX - curX;
int actualDy = newY - curY;
- if (actualDx != 0) {
- xField.setText("" + newX);
- }
- if (actualDy != 0) {
- yField.setText("" + newY);
- }
+ boolean changed = false;
+ if (actualDx != 0) { xField.setText("" + newX); changed = true; }
+ if (actualDy != 0) { yField.setText("" + newY); changed = true; }
if ((actualDx != 0 || actualDy != 0) && isSafeAreaEnabled()) {
- int saw = Math.min(safeWField.getAsInt(screenW), screenW);
- int sah = Math.min(safeHField.getAsInt(screenH), screenH);
+ int saw = Math.min(safeWField.getAsInt(sW), sW);
+ int sah = Math.min(safeHField.getAsInt(sH), sH);
int curSafeX = safeXField.getAsInt(curX);
int curSafeY = safeYField.getAsInt(curY);
- int newSafeX = clamp(curSafeX + actualDx, newX, newX + screenW - saw);
- int newSafeY = clamp(curSafeY + actualDy, newY, newY + screenH - sah);
- if (newSafeX != curSafeX) {
- safeXField.setText("" + newSafeX);
- }
- if (newSafeY != curSafeY) {
- safeYField.setText("" + newSafeY);
- }
+ int newSafeX = clamp(curSafeX + actualDx, newX, newX + sW - saw);
+ int newSafeY = clamp(curSafeY + actualDy, newY, newY + sH - sah);
+ if (newSafeX != curSafeX) { safeXField.setText("" + newSafeX); changed = true; }
+ if (newSafeY != curSafeY) { safeYField.setText("" + newSafeY); changed = true; }
+ }
+ if (changed) { notifyChanged(); }
+ }
+
+ private int[] applyResize(int curX, int curY, int curW, int curH, int dx, int dy,
+ int boundsX, int boundsY, int boundsW, int boundsH) {
+ boolean moveLeft = (dragOp & DRAG_W) != 0 || dragOp == DRAG_MOVE;
+ boolean moveRight = (dragOp & DRAG_E) != 0 || dragOp == DRAG_MOVE;
+ boolean moveTop = (dragOp & DRAG_N) != 0 || dragOp == DRAG_MOVE;
+ boolean moveBottom = (dragOp & DRAG_S) != 0 || dragOp == DRAG_MOVE;
+ int left = curX;
+ int right = curX + curW;
+ int top = curY;
+ int bottom = curY + curH;
+ if (moveLeft) { left += dx; }
+ if (moveRight) { right += dx; }
+ if (moveTop) { top += dy; }
+ if (moveBottom) { bottom += dy; }
+ int minX = boundsX;
+ int minY = boundsY;
+ int maxX = boundsX + boundsW;
+ int maxY = boundsY + boundsH;
+ if (left < minX) { left = minX; if (moveLeft && !moveRight && right < left + MIN_SAFE_SIZE) right = left + MIN_SAFE_SIZE; }
+ if (right > maxX) { right = maxX; if (moveRight && !moveLeft && left > right - MIN_SAFE_SIZE) left = right - MIN_SAFE_SIZE; }
+ if (top < minY) { top = minY; if (moveTop && !moveBottom && bottom < top + MIN_SAFE_SIZE) bottom = top + MIN_SAFE_SIZE; }
+ if (bottom > maxY) { bottom = maxY; if (moveBottom && !moveTop && top > bottom - MIN_SAFE_SIZE) top = bottom - MIN_SAFE_SIZE; }
+ if (right - left < MIN_SAFE_SIZE) {
+ if (moveLeft && !moveRight) { left = right - MIN_SAFE_SIZE; }
+ else if (moveRight && !moveLeft) { right = left + MIN_SAFE_SIZE; }
+ }
+ if (bottom - top < MIN_SAFE_SIZE) {
+ if (moveTop && !moveBottom) { top = bottom - MIN_SAFE_SIZE; }
+ else if (moveBottom && !moveTop) { bottom = top + MIN_SAFE_SIZE; }
}
+ if (dragOp == DRAG_MOVE) {
+ int w = right - left;
+ int h = bottom - top;
+ left = clamp(left, minX, maxX - w);
+ top = clamp(top, minY, maxY - h);
+ right = left + w;
+ bottom = top + h;
+ }
+ return new int[] { left, top, right - left, bottom - top };
}
@Override
public void pointerPressed(int x, int y) {
lastPX = x;
lastPY = y;
+ dragOp = hitTestDragOp(x, y);
}
@Override
public void pointerReleased(int x, int y) {
lastPX = -1;
lastPY = -1;
+ dragOp = DRAG_NONE;
+ }
+
+ private int hitTestDragOp(int px, int py) {
+ if (mode == AIM_MODE_SAFE && isSafeAreaEnabled()) {
+ float scale = currentScale();
+ if (scale <= 0f) { return DRAG_MOVE; }
+ int drawX = drawOriginX(scale);
+ int drawY = drawOriginY(scale);
+ int sX = drawX + Math.round(safeXField.getAsInt(0) * scale);
+ int sY = drawY + Math.round(safeYField.getAsInt(0) * scale);
+ int sW = Math.max(1, Math.round(safeWField.getAsInt(screenW()) * scale));
+ int sH = Math.max(1, Math.round(safeHField.getAsInt(screenH()) * scale));
+ int zone = Math.max(8, Display.getInstance().convertToPixels(2.5f));
+ boolean nearLeft = px >= sX - zone && px <= sX + zone;
+ boolean nearRight = px >= sX + sW - zone && px <= sX + sW + zone;
+ boolean nearTop = py >= sY - zone && py <= sY + zone;
+ boolean nearBottom = py >= sY + sH - zone && py <= sY + sH + zone;
+ int op = 0;
+ if (nearLeft) { op |= DRAG_W; }
+ if (nearRight) { op |= DRAG_E; }
+ if (nearTop) { op |= DRAG_N; }
+ if (nearBottom) { op |= DRAG_S; }
+ if (op != 0) { return op; }
+ }
+ return DRAG_MOVE;
}
@Override
@@ -1065,18 +1166,20 @@ public void paint(Graphics g) {
g.drawImage(img, drawX, drawY, drawW, drawH);
+ int sW = screenW();
+ int sH = screenH();
int screenImgX = xField.getAsInt(0);
int screenImgY = yField.getAsInt(0);
boolean safeOn = isSafeAreaEnabled();
int safeImgX = safeOn ? safeXField.getAsInt(screenImgX) : screenImgX;
int safeImgY = safeOn ? safeYField.getAsInt(screenImgY) : screenImgY;
- int safeImgW = safeOn ? safeWField.getAsInt(screenW) : screenW;
- int safeImgH = safeOn ? safeHField.getAsInt(screenH) : screenH;
+ int safeImgW = safeOn ? safeWField.getAsInt(sW) : sW;
+ int safeImgH = safeOn ? safeHField.getAsInt(sH) : sH;
int sx = drawX + Math.round(screenImgX * scale);
int sy = drawY + Math.round(screenImgY * scale);
- int sw = Math.max(1, Math.round(screenW * scale));
- int sh = Math.max(1, Math.round(screenH * scale));
+ int sw = Math.max(1, Math.round(sW * scale));
+ int sh = Math.max(1, Math.round(sH * scale));
int fx = drawX + Math.round(safeImgX * scale);
int fy = drawY + Math.round(safeImgY * scale);
int fw = Math.max(1, Math.round(safeImgW * scale));
@@ -1128,6 +1231,18 @@ public void paint(Graphics g) {
if (safeOn) {
g.setColor(mode == AIM_MODE_SAFE ? 0xffaa00 : 0xff0000);
g.drawRect(fx, fy, fw, fh);
+ if (mode == AIM_MODE_SAFE) {
+ int handle = Math.max(6, Display.getInstance().convertToPixels(2f));
+ int half = handle / 2;
+ g.fillRect(fx - half, fy - half, handle, handle);
+ g.fillRect(fx + fw - half, fy - half, handle, handle);
+ g.fillRect(fx - half, fy + fh - half, handle, handle);
+ g.fillRect(fx + fw - half, fy + fh - half, handle, handle);
+ g.fillRect(fx + fw / 2 - half, fy - half, handle, handle);
+ g.fillRect(fx + fw / 2 - half, fy + fh - half, handle, handle);
+ g.fillRect(fx - half, fy + fh / 2 - half, handle, handle);
+ g.fillRect(fx + fw - half, fy + fh / 2 - half, handle, handle);
+ }
}
g.setAlpha(oldAlpha);
@@ -1137,33 +1252,84 @@ public void paint(Graphics g) {
}
}
- void aimPosition(final Image img, final TextField x, final TextField y, final TextField safeX, final TextField safeY, final TextField safeW, final TextField safeH, final int w, final int h, final OnOffSwitch useSafeArea) {
+ void aimPosition(final Image img,
+ final TextField screenX, final TextField screenY,
+ final TextField screenW, final TextField screenH,
+ final TextField safeX, final TextField safeY,
+ final TextField safeW, final TextField safeH,
+ final OnOffSwitch useSafeArea) {
if(img == null) {
ToastBar.showErrorMessage("You need to pick a skin image first");
return;
}
- final String originalX = x.getText();
- final String originalY = y.getText();
- final String originalSafeX = safeX.getText();
- final String originalSafeY = safeY.getText();
+ final Form previousForm = Display.getInstance().getCurrent();
+ final boolean safeEnabled = useSafeArea != null && useSafeArea.isValue();
final Form editPosition = new Form("Positioning", new BorderLayout());
editPosition.setUIID("SkinDesignerForm");
- final AimView view = new AimView(img, x, y, safeX, safeY, safeW, safeH, w, h, useSafeArea);
+ final AimView view = new AimView(img, screenX, screenY, screenW, screenH,
+ safeX, safeY, safeW, safeH, useSafeArea);
+
+ final TextField aimScreenX = numericMirror(screenX);
+ final TextField aimScreenY = numericMirror(screenY);
+ final TextField aimScreenW = numericMirror(screenW);
+ final TextField aimScreenH = numericMirror(screenH);
+ final TextField aimSafeX = numericMirror(safeX);
+ final TextField aimSafeY = numericMirror(safeY);
+ final TextField aimSafeW = numericMirror(safeW);
+ final TextField aimSafeH = numericMirror(safeH);
+ if (!safeEnabled) {
+ aimSafeX.setEnabled(false);
+ aimSafeY.setEnabled(false);
+ aimSafeW.setEnabled(false);
+ aimSafeH.setEnabled(false);
+ }
+ Runnable refreshAimFields = () -> {
+ syncMirror(aimScreenX, screenX);
+ syncMirror(aimScreenY, screenY);
+ syncMirror(aimScreenW, screenW);
+ syncMirror(aimScreenH, screenH);
+ syncMirror(aimSafeX, safeX);
+ syncMirror(aimSafeY, safeY);
+ syncMirror(aimSafeW, safeW);
+ syncMirror(aimSafeH, safeH);
+ };
+ view.setOnChange(refreshAimFields);
+ bindAimToMain(aimScreenX, screenX, view);
+ bindAimToMain(aimScreenY, screenY, view);
+ bindAimToMain(aimScreenW, screenW, view);
+ bindAimToMain(aimScreenH, screenH, view);
+ bindAimToMain(aimSafeX, safeX, view);
+ bindAimToMain(aimSafeY, safeY, view);
+ bindAimToMain(aimSafeW, safeW, view);
+ bindAimToMain(aimSafeH, safeH, view);
+
+ final String originalScreenX = screenX.getText();
+ final String originalScreenY = screenY.getText();
+ final String originalScreenW = screenW.getText();
+ final String originalScreenH = screenH.getText();
+ final String originalSafeX = safeX.getText();
+ final String originalSafeY = safeY.getText();
+ final String originalSafeW = safeW.getText();
+ final String originalSafeH = safeH.getText();
Button done = new Button("Done");
styleActionButton(done, FontImage.MATERIAL_CHECK);
done.setTooltip("Keep changes and return");
- done.addActionListener(e -> editPosition.showBack());
+ done.addActionListener(e -> previousForm.showBack());
Button cancel = new Button("Cancel");
styleActionButton(cancel, FontImage.MATERIAL_CANCEL);
cancel.setTooltip("Discard changes and return");
Runnable cancelAction = () -> {
- x.setText(originalX);
- y.setText(originalY);
+ screenX.setText(originalScreenX);
+ screenY.setText(originalScreenY);
+ screenW.setText(originalScreenW);
+ screenH.setText(originalScreenH);
safeX.setText(originalSafeX);
safeY.setText(originalSafeY);
- editPosition.showBack();
+ safeW.setText(originalSafeW);
+ safeH.setText(originalSafeH);
+ previousForm.showBack();
};
cancel.addActionListener(e -> cancelAction.run());
editPosition.setBackCommand(new com.codename1.ui.Command("Cancel") {
@@ -1177,74 +1343,95 @@ public void actionPerformed(com.codename1.ui.events.ActionEvent evt) {
topActions.setUIID("SkinDesignerTabBar");
editPosition.add(BorderLayout.NORTH, topActions);
- final boolean safeEnabled = useSafeArea != null && useSafeArea.isValue();
- Button modeScreen = new Button("Screen");
- Button modeSafe = new Button("Safe");
- Button modePan = new Button("Pan");
- modeScreen.setTooltip("Drag to reposition the screen area");
- modeSafe.setTooltip(safeEnabled
- ? "Drag to adjust the safe area within the screen"
- : "Enable 'Use Safe Area' on the previous screen to edit safe area");
- modePan.setTooltip("Drag to scroll the zoomed view");
- if (!safeEnabled) {
- modeSafe.setEnabled(false);
+ Container south = new Container(new BoxLayout(BoxLayout.Y_AXIS));
+ if (safeEnabled) {
+ Button modeScreen = new Button("Screen");
+ Button modeSafe = new Button("Safe");
+ modeScreen.setTooltip("Drag to reposition the screen area");
+ modeSafe.setTooltip("Drag inside to move the safe area; drag its edges or corners to resize");
+ final Button[] modeButtons = { modeScreen, modeSafe };
+ final int[] modeValues = { AIM_MODE_SCREEN, AIM_MODE_SAFE };
+ updateModeButtons(modeButtons, view.getMode());
+ for (int i = 0; i < modeButtons.length; i++) {
+ final int target = modeValues[i];
+ modeButtons[i].addActionListener(e -> {
+ view.setMode(target);
+ updateModeButtons(modeButtons, target);
+ view.repaint();
+ });
+ }
+ Container modeBar = GridLayout.encloseIn(2, modeScreen, modeSafe);
+ modeBar.setUIID("SkinDesignerTabBar");
+ south.add(modeBar);
}
- final Button[] modeButtons = { modeScreen, modeSafe, modePan };
- final int[] modeValues = { AIM_MODE_SCREEN, AIM_MODE_SAFE, AIM_MODE_PAN };
- updateModeButtons(modeButtons, view.getMode());
- for (int i = 0; i < modeButtons.length; i++) {
- final int target = modeValues[i];
- modeButtons[i].addActionListener(e -> {
- view.setMode(target);
- updateModeButtons(modeButtons, target);
- if (target == AIM_MODE_PAN) {
- ToastBar.showMessage("Pan mode: zoom in and drag to scroll the view",
- FontImage.MATERIAL_PAN_TOOL, 2500);
- } else if (target == AIM_MODE_SAFE) {
- ToastBar.showMessage("Safe mode: drag to adjust the safe area inside the screen",
- FontImage.MATERIAL_CROP_FREE, 2500);
- } else if (target == AIM_MODE_SCREEN) {
- ToastBar.showMessage("Screen mode: drag to reposition the screen area",
- FontImage.MATERIAL_STAY_CURRENT_PORTRAIT, 2000);
- }
- view.repaint();
- });
+
+ south.add(labeledFieldTitle("Screen (X / Y / W / H)"));
+ south.add(GridLayout.encloseIn(4, aimScreenX, aimScreenY, aimScreenW, aimScreenH));
+ if (safeEnabled) {
+ south.add(labeledFieldTitle("Safe (X / Y / W / H)"));
+ south.add(GridLayout.encloseIn(4, aimSafeX, aimSafeY, aimSafeW, aimSafeH));
}
- Container modeBar = GridLayout.encloseIn(3, modeScreen, modeSafe, modePan);
- modeBar.setUIID("SkinDesignerTabBar");
Button zoomIn = new Button();
Button zoomOut = new Button();
- Button left = new Button();
- Button right = new Button();
- Button up = new Button();
- Button down = new Button();
+ Button leftBtn = new Button();
+ Button rightBtn = new Button();
+ Button upBtn = new Button();
+ Button downBtn = new Button();
styleIconActionButton(zoomIn, FontImage.MATERIAL_ZOOM_IN);
styleIconActionButton(zoomOut, FontImage.MATERIAL_ZOOM_OUT);
- styleIconActionButton(left, FontImage.MATERIAL_KEYBOARD_ARROW_LEFT);
- styleIconActionButton(right, FontImage.MATERIAL_KEYBOARD_ARROW_RIGHT);
- styleIconActionButton(up, FontImage.MATERIAL_KEYBOARD_ARROW_UP);
- styleIconActionButton(down, FontImage.MATERIAL_KEYBOARD_ARROW_DOWN);
- zoomIn.setTooltip("Zoom in");
+ styleIconActionButton(leftBtn, FontImage.MATERIAL_KEYBOARD_ARROW_LEFT);
+ styleIconActionButton(rightBtn, FontImage.MATERIAL_KEYBOARD_ARROW_RIGHT);
+ styleIconActionButton(upBtn, FontImage.MATERIAL_KEYBOARD_ARROW_UP);
+ styleIconActionButton(downBtn, FontImage.MATERIAL_KEYBOARD_ARROW_DOWN);
+ zoomIn.setTooltip("Zoom in (centers on the active rectangle)");
zoomOut.setTooltip("Zoom out");
- left.setTooltip("Nudge left (1 px)");
- right.setTooltip("Nudge right (1 px)");
- up.setTooltip("Nudge up (1 px)");
- down.setTooltip("Nudge down (1 px)");
+ leftBtn.setTooltip("Nudge the active rectangle 1 px left");
+ rightBtn.setTooltip("Nudge the active rectangle 1 px right");
+ upBtn.setTooltip("Nudge the active rectangle 1 px up");
+ downBtn.setTooltip("Nudge the active rectangle 1 px down");
zoomIn.addActionListener(e -> view.zoomIn());
zoomOut.addActionListener(e -> view.zoomOut());
- left.addActionListener(e -> view.nudge(-1, 0));
- right.addActionListener(e -> view.nudge(1, 0));
- up.addActionListener(e -> view.nudge(0, -1));
- down.addActionListener(e -> view.nudge(0, 1));
+ leftBtn.addActionListener(e -> { view.nudge(-1, 0); refreshAimFields.run(); });
+ rightBtn.addActionListener(e -> { view.nudge(1, 0); refreshAimFields.run(); });
+ upBtn.addActionListener(e -> { view.nudge(0, -1); refreshAimFields.run(); });
+ downBtn.addActionListener(e -> { view.nudge(0, 1); refreshAimFields.run(); });
+
+ Container navBar = GridLayout.encloseIn(6, zoomOut, zoomIn, leftBtn, rightBtn, upBtn, downBtn);
+ south.add(navBar);
- Container navBar = GridLayout.encloseIn(6, zoomIn, zoomOut, left, right, up, down);
- Container south = BoxLayout.encloseY(modeBar, navBar);
editPosition.add(BorderLayout.CENTER, view);
editPosition.add(BorderLayout.SOUTH, south);
applyWebsiteTheme(editPosition, websiteDarkMode);
editPosition.show();
+
+ String intro = safeEnabled
+ ? "Drag the screen or safe rectangle to move it. Drag safe-area edges or corners to resize. Use the fields below for exact values."
+ : "Drag the screen rectangle to move it. Use the X / Y / W / H fields below for exact values.";
+ ToastBar.showMessage(intro, FontImage.MATERIAL_INFO, 6000);
+ }
+
+ private TextField numericMirror(TextField source) {
+ TextField t = new TextField(source.getText(), source.getHint(), 6, TextField.NUMERIC);
+ t.setUIID("SkinDesignerField");
+ return t;
+ }
+
+ private void syncMirror(TextField mirror, TextField source) {
+ if (!source.getText().equals(mirror.getText())) {
+ mirror.setText(source.getText());
+ }
+ }
+
+ private void bindAimToMain(final TextField mirror, final TextField main, final AimView view) {
+ mirror.addActionListener(e -> {
+ String text = mirror.getText();
+ if (!main.getText().equals(text)) {
+ main.setText(text);
+ }
+ view.repaint();
+ });
}
private void updateModeButtons(Button[] buttons, int selectedMode) {
From d6e8f9503d21f8c6855f2b7682cfff25c5533bc2 Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Thu, 30 Apr 2026 17:23:47 +0300
Subject: [PATCH 30/55] Complete redesign of the app
---
.../skin-designer-devices-update.yml | 80 +
.../common/src/main/css/theme.css | 1065 ++++++-
.../tools/skindesigner/DeviceDatabase.java | 253 ++
.../tools/skindesigner/DevicePreview.java | 194 ++
.../tools/skindesigner/SkinDesigner.java | 2674 ++++++++---------
.../tools/skindesigner/SkinModel.java | 131 +
.../common/src/main/resources/devices.json | 1 +
.../skindesigner/tools/devicedb/.gitignore | 3 +
scripts/skindesigner/tools/devicedb/README.md | 102 +
.../tools/devicedb/build_devices_json.py | 435 +++
10 files changed, 3497 insertions(+), 1441 deletions(-)
create mode 100644 .github/workflows/skin-designer-devices-update.yml
create mode 100644 scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/DeviceDatabase.java
create mode 100644 scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/DevicePreview.java
create mode 100644 scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/SkinModel.java
create mode 100644 scripts/skindesigner/common/src/main/resources/devices.json
create mode 100644 scripts/skindesigner/tools/devicedb/.gitignore
create mode 100644 scripts/skindesigner/tools/devicedb/README.md
create mode 100755 scripts/skindesigner/tools/devicedb/build_devices_json.py
diff --git a/.github/workflows/skin-designer-devices-update.yml b/.github/workflows/skin-designer-devices-update.yml
new file mode 100644
index 0000000000..8bc988cec1
--- /dev/null
+++ b/.github/workflows/skin-designer-devices-update.yml
@@ -0,0 +1,80 @@
+name: Skin Designer Device DB Update
+
+on:
+ schedule:
+ # Monthly on the 1st at 06:30 UTC. The scrape walks ~18 brands × ~10
+ # listing pages × phones per page; with a 1.5 s polite delay it tends
+ # to finish in 30-60 minutes.
+ - cron: '30 6 1 * *'
+ workflow_dispatch:
+ pull_request:
+ paths:
+ - '.github/workflows/skin-designer-devices-update.yml'
+ - 'scripts/skindesigner/tools/devicedb/build_devices_json.py'
+
+permissions:
+ actions: write
+ contents: write
+ pull-requests: write
+
+jobs:
+ update-device-db:
+ runs-on: ubuntu-latest
+ timeout-minutes: 90
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.x'
+
+ - name: Restore HTML scrape cache
+ uses: actions/cache@v4
+ with:
+ path: scripts/skindesigner/tools/devicedb/cache
+ key: skin-designer-devicedb-cache-${{ github.run_id }}
+ restore-keys: |
+ skin-designer-devicedb-cache-
+
+ - name: Scrape GSMArena → devices.json
+ # --delay 1.5 keeps us at <1 req/sec averaged with jitter, which
+ # is well under GSMArena's per-IP rate limit. The cache directory
+ # is restored from the previous run so unchanged phone pages
+ # don't get re-fetched.
+ run: |
+ python3 scripts/skindesigner/tools/devicedb/build_devices_json.py \
+ --delay 1.5 --max-pages 12
+
+ - name: Show diff (PR mode)
+ if: github.event_name == 'pull_request'
+ run: |
+ if git diff --quiet -- scripts/skindesigner/common/src/main/resources/devices.json; then
+ echo "Device DB is already up to date."
+ else
+ git --no-pager diff --stat -- scripts/skindesigner/common/src/main/resources/devices.json
+ fi
+
+ - name: Create pull request
+ if: github.event_name != 'pull_request'
+ uses: peter-evans/create-pull-request@v7
+ with:
+ commit-message: "Refresh Skin Designer device database"
+ title: "Refresh Skin Designer device database"
+ body: |
+ ## Summary
+ - Regenerates `scripts/skindesigner/common/src/main/resources/devices.json`
+ by scraping GSMArena directly. The script merges fresh records
+ into the existing catalog so historical entries survive partial
+ scrapes.
+
+ This PR is created automatically by the monthly device DB update
+ workflow. Skip the merge if the diff is empty or only churn.
+ base: master
+ branch: automation/skin-designer-device-db
+ delete-branch: true
+ labels: |
+ dependencies
+ automation
diff --git a/scripts/skindesigner/common/src/main/css/theme.css b/scripts/skindesigner/common/src/main/css/theme.css
index 273bfecb08..ea4729efef 100644
--- a/scripts/skindesigner/common/src/main/css/theme.css
+++ b/scripts/skindesigner/common/src/main/css/theme.css
@@ -1,211 +1,1078 @@
+/* ==========================================================================
+ Skin Designer — wizard styling
+ Mapped from /tmp/skin-designer/project/assets/{tokens,app}.css onto the
+ Codename One CSS subset. Each design token shows up here as a UIID; the
+ *Dark variants are toggled at runtime by themedUiid() in SkinDesigner.java.
+ ========================================================================== */
+
#Constants {
includeNativeBool: true;
defaultSourceDPIInt: "0";
useLargerTextScaleBool: true;
}
+/* --- Form / shell ---------------------------------------------------- */
+
SkinDesignerForm {
- background: #eef3ff;
+ background: #f3f4f7;
}
-
SkinDesignerFormDark {
- background: #0a111d;
+ background: #071b4d;
}
-SkinDesignerTabsContainer {
- background: #eef3ff;
- border: none;
+SkinDesignerBody {
+ background: #f3f4f7;
+ padding: 0;
+}
+SkinDesignerBodyDark {
+ background: #071b4d;
}
-SkinDesignerTabsContainerDark {
- background: #0a111d;
- border: none;
+Toolbar { background: #ffffff; }
+ToolbarDark { background: #102b66; }
+Title { color: #112247; background: #ffffff; font-family: "native:MainLight"; }
+TitleDark { color: #f5f8ff; background: #102b66; font-family: "native:MainLight"; }
+Command { color: #112247; background: transparent; }
+CommandDark { color: #f5f8ff; background: transparent; }
+
+/* --- Topbar ---------------------------------------------------------- */
+
+SkinDesignerTopbar {
+ background: #ffffff;
+ border-bottom: 1px solid #d9dee8;
+ padding: 0.6mm 1.6mm;
+}
+SkinDesignerTopbarDark {
+ background: #102b66;
+ border-bottom: 1px solid #4c6ea8;
+ padding: 0.6mm 1.6mm;
}
-SkinDesignerTabBar {
- background: #eef3ff;
- border: none;
- padding: 0.4mm;
+SkinDesignerBrand { padding: 0; }
+SkinDesignerBrandDark { padding: 0; }
+
+SkinDesignerBrandLogo {
+ color: #ffffff;
+ background: #2a8a8a;
+ border-radius: 1.4mm;
+ padding: 0.6mm 0.9mm;
+ margin: 0.2mm 0.6mm;
+ font-size: 2.4mm;
+ font-family: "native:MainBold";
+ text-align: center;
+}
+SkinDesignerBrandLogoDark {
+ color: #ffffff;
+ background: #3bc5c5;
+ border-radius: 1.4mm;
+ padding: 0.6mm 0.9mm;
+ margin: 0.2mm 0.6mm;
+ font-size: 2.4mm;
+ font-family: "native:MainBold";
+ text-align: center;
}
-SkinDesignerTabBarDark {
- background: #0a111d;
- border: none;
- padding: 0.4mm;
+SkinDesignerBrandTitle {
+ color: #112247;
+ font-size: 3.4mm;
+ font-family: "native:MainBold";
+ padding: 0.6mm;
+}
+SkinDesignerBrandTitleDark {
+ color: #f5f8ff;
+ font-size: 3.4mm;
+ font-family: "native:MainBold";
+ padding: 0.6mm;
}
-SkinDesignerTabButton {
- background: #f3f4f7;
+/* --- Stepper --------------------------------------------------------- */
+
+SkinDesignerStepper {
+ background: transparent;
+ padding: 0;
+}
+SkinDesignerStepperDark { background: transparent; padding: 0; }
+
+SkinDesignerStepperItemActive,
+SkinDesignerStepperItemDone,
+SkinDesignerStepperItemPending {
+ background: transparent;
+ padding: 0.6mm 1.4mm;
+ margin: 0;
+ border-radius: 6mm;
+}
+SkinDesignerStepperItemActive { background: #e8f0ff; }
+SkinDesignerStepperItemActiveDark { background: rgba(77,134,255,0.16); padding: 0.6mm 1.4mm; margin: 0; border-radius: 6mm; }
+SkinDesignerStepperItemDoneDark { background: transparent; padding: 0.6mm 1.4mm; margin: 0; border-radius: 6mm; }
+SkinDesignerStepperItemPendingDark { background: transparent; padding: 0.6mm 1.4mm; margin: 0; border-radius: 6mm; }
+
+SkinDesignerStepperNumActive {
+ background: #2f6bff;
+ color: #ffffff;
+ border-radius: 4mm;
+ padding: 0.4mm;
+ margin: 0 0.6mm 0 0;
+ font-size: 2.2mm;
+ font-family: "native:MainBold";
+ text-align: center;
+}
+SkinDesignerStepperNumActiveDark {
+ background: #4d86ff;
+ color: #ffffff;
+ border-radius: 4mm;
+ padding: 0.4mm;
+ margin: 0 0.6mm 0 0;
+ font-size: 2.2mm;
+ font-family: "native:MainBold";
+ text-align: center;
+}
+SkinDesignerStepperNumDone {
+ background: #b8d532;
color: #112247;
- border: 1px solid #d9dee8;
- margin: 0.2mm;
- padding: 0.9mm;
+ border-radius: 4mm;
+ padding: 0.4mm;
+ margin: 0 0.6mm 0 0;
+ font-size: 2.2mm;
+ font-family: "native:MainBold";
+ text-align: center;
+}
+SkinDesignerStepperNumDoneDark {
+ background: #b8d532;
+ color: #0a2460;
+ border-radius: 4mm;
+ padding: 0.4mm;
+ margin: 0 0.6mm 0 0;
+ font-size: 2.2mm;
+ font-family: "native:MainBold";
+ text-align: center;
+}
+SkinDesignerStepperNumPending {
+ background: #d9dee8;
+ color: #ffffff;
+ border-radius: 4mm;
+ padding: 0.4mm;
+ margin: 0 0.6mm 0 0;
+ font-size: 2.2mm;
+ font-family: "native:MainLight";
+ text-align: center;
+}
+SkinDesignerStepperNumPendingDark {
+ background: #4c6ea8;
+ color: #102b66;
+ border-radius: 4mm;
+ padding: 0.4mm;
+ margin: 0 0.6mm 0 0;
+ font-size: 2.2mm;
+ font-family: "native:MainLight";
+ text-align: center;
}
-SkinDesignerTabButtonDark {
- background: #102b66;
+SkinDesignerStepperLabelActive {
+ color: #2f6bff;
+ font-size: 2.6mm;
+ font-family: "native:MainBold";
+ padding: 0;
+}
+SkinDesignerStepperLabelActiveDark {
+ color: #4d86ff;
+ font-size: 2.6mm;
+ font-family: "native:MainBold";
+ padding: 0;
+}
+SkinDesignerStepperLabelDone {
+ color: #112247;
+ font-size: 2.6mm;
+ font-family: "native:MainLight";
+ padding: 0;
+}
+SkinDesignerStepperLabelDoneDark {
color: #f5f8ff;
- border: 1px solid #4c6ea8;
- margin: 0.2mm;
- padding: 0.9mm;
+ font-size: 2.6mm;
+ font-family: "native:MainLight";
+ padding: 0;
+}
+SkinDesignerStepperLabelPending {
+ color: #7f8aa3;
+ font-size: 2.6mm;
+ font-family: "native:MainLight";
+ padding: 0;
+}
+SkinDesignerStepperLabelPendingDark {
+ color: #a8b8da;
+ font-size: 2.6mm;
+ font-family: "native:MainLight";
+ padding: 0;
}
-SkinDesignerTabButtonSelected {
+SkinDesignerStepperSep {
+ background: #d9dee8;
+ padding: 0;
+ margin: 0 0.5mm;
+ color: transparent;
+ font-size: 0.6mm;
+}
+SkinDesignerStepperSepDark { background: #4c6ea8; padding: 0; margin: 0 0.5mm; color: transparent; font-size: 0.6mm; }
+
+SkinDesignerWizardNav { background: transparent; padding: 0; }
+SkinDesignerWizardNavDark { background: transparent; padding: 0; }
+
+/* --- Status bar ------------------------------------------------------ */
+
+SkinDesignerStatusbar {
background: #ffffff;
- color: #112247;
- border: 2px solid #2f6bff;
- margin: 0.2mm;
- padding: 0.9mm;
+ border-top: 1px solid #d9dee8;
+ padding: 0.4mm 1.4mm;
+}
+SkinDesignerStatusbarDark {
+ background: #102b66;
+ border-top: 1px solid #4c6ea8;
+ padding: 0.4mm 1.4mm;
}
-SkinDesignerTabButtonSelectedDark {
- background: #163575;
- color: #f5f8ff;
- border: 2px solid #4d86ff;
- margin: 0.2mm;
- padding: 0.9mm;
+SkinDesignerStatusName,
+SkinDesignerStatusSpec {
+ color: #7f8aa3;
+ font-size: 2.4mm;
+ font-family: "native:MainLight";
+ padding: 0.2mm 0;
}
+SkinDesignerStatusNameDark,
+SkinDesignerStatusSpecDark {
+ color: #a8b8da;
+ font-size: 2.4mm;
+ font-family: "native:MainLight";
+ padding: 0.2mm 0;
+}
+
+/* --- Step container -------------------------------------------------- */
-Toolbar {
- background: #eef3ff;
+SkinDesignerStepRoot {
+ background: #f3f4f7;
+ padding: 0;
}
+SkinDesignerStepRootDark { background: #071b4d; padding: 0; }
-ToolbarDark {
- background: #0a111d;
+SkinDesignerStepHead {
+ background: transparent;
+ padding: 4mm 4mm 1mm 4mm;
}
+SkinDesignerStepHeadDark { background: transparent; padding: 4mm 4mm 1mm 4mm; }
+
+SkinDesignerStepHeadInner { background: transparent; padding: 0; }
+SkinDesignerStepHeadInnerDark { background: transparent; padding: 0; }
-Title {
+SkinDesignerStepBody {
+ background: transparent;
+ padding: 1mm 3mm;
+}
+SkinDesignerStepBodyDark { background: transparent; padding: 1mm 3mm; }
+
+SkinDesignerStepScroll {
+ background: transparent;
+ padding: 0;
+}
+SkinDesignerStepScrollDark { background: transparent; padding: 0; }
+
+SkinDesignerH1 {
+ color: #112247;
+ font-size: 5.6mm;
+ font-family: "native:MainBold";
+ padding: 0.6mm 0;
+ text-align: center;
+}
+SkinDesignerH1Dark {
+ color: #f5f8ff;
+ font-size: 5.6mm;
+ font-family: "native:MainBold";
+ padding: 0.6mm 0;
+ text-align: center;
+}
+SkinDesignerH3 {
color: #112247;
- background: #eef3ff;
+ font-size: 3.6mm;
+ font-family: "native:MainBold";
+ padding: 0.4mm 0;
+}
+SkinDesignerH3Dark {
+ color: #f5f8ff;
+ font-size: 3.6mm;
+ font-family: "native:MainBold";
+ padding: 0.4mm 0;
+}
+SkinDesignerSub {
+ color: #7f8aa3;
+ font-size: 2.8mm;
font-family: "native:MainLight";
+ padding: 0.4mm 0 1mm 0;
+ text-align: center;
}
+SkinDesignerSubDark {
+ color: #a8b8da;
+ font-size: 2.8mm;
+ font-family: "native:MainLight";
+ padding: 0.4mm 0 1mm 0;
+ text-align: center;
+}
+
+/* --- Buttons --------------------------------------------------------- */
-TitleDark {
+SkinDesignerPrimaryButton {
+ color: #ffffff;
+ background: #2f6bff;
+ border: 1px solid #2f6bff;
+ border-radius: 1.4mm;
+ padding: 1mm 2.4mm;
+ margin: 0.4mm;
+ font-family: "native:MainBold";
+ font-size: 2.8mm;
+ text-align: center;
+}
+SkinDesignerPrimaryButtonDark {
+ color: #ffffff;
+ background: #4d86ff;
+ border: 1px solid #4d86ff;
+ border-radius: 1.4mm;
+ padding: 1mm 2.4mm;
+ margin: 0.4mm;
+ font-family: "native:MainBold";
+ font-size: 2.8mm;
+ text-align: center;
+}
+SkinDesignerSecondaryButton {
+ color: #112247;
+ background: #ffffff;
+ border: 1px solid #d9dee8;
+ border-radius: 1.4mm;
+ padding: 1mm 2.4mm;
+ margin: 0.4mm;
+ font-family: "native:MainLight";
+ font-size: 2.8mm;
+ text-align: center;
+}
+SkinDesignerSecondaryButtonDark {
color: #f5f8ff;
- background: #0a111d;
+ background: #102b66;
+ border: 1px solid #4c6ea8;
+ border-radius: 1.4mm;
+ padding: 1mm 2.4mm;
+ margin: 0.4mm;
font-family: "native:MainLight";
+ font-size: 2.8mm;
+ text-align: center;
}
-
-Command {
+SkinDesignerGhostButton {
color: #112247;
background: transparent;
+ border: none;
+ padding: 1mm 2mm;
+ margin: 0.4mm;
+ font-family: "native:MainLight";
+ font-size: 2.6mm;
}
-
-CommandDark {
+SkinDesignerGhostButtonDark {
color: #f5f8ff;
background: transparent;
+ border: none;
+ padding: 1mm 2mm;
+ margin: 0.4mm;
+ font-family: "native:MainLight";
+ font-size: 2.6mm;
+}
+SkinDesignerIconButton {
+ color: #7f8aa3;
+ background: transparent;
+ border: none;
+ padding: 0.6mm;
+ margin: 0;
+ font-size: 2.4mm;
+}
+SkinDesignerIconButtonDark {
+ color: #a8b8da;
+ background: transparent;
+ border: none;
+ padding: 0.6mm;
+ margin: 0;
+ font-size: 2.4mm;
}
-Tab {
- background: #f3f4f7;
+SkinDesignerFooter {
+ background: #ffffff;
+ border-top: 1px solid #d9dee8;
+ padding: 1.2mm 2.4mm;
+}
+SkinDesignerFooterDark {
+ background: #102b66;
+ border-top: 1px solid #4c6ea8;
+ padding: 1.2mm 2.4mm;
+}
+
+/* --- Search / filter chips ------------------------------------------ */
+
+SkinDesignerSearchField {
color: #112247;
+ background: #ffffff;
border: 1px solid #d9dee8;
+ border-radius: 1.4mm;
+ padding: 1.2mm;
+ margin: 0.6mm;
font-family: "native:MainLight";
- font-size: 2.8mm;
- text-align: center;
+ font-size: 3mm;
+}
+SkinDesignerSearchFieldDark {
+ color: #f5f8ff;
+ background: #0e2a61;
+ border: 1px solid #4c6ea8;
+ border-radius: 1.4mm;
+ padding: 1.2mm;
+ margin: 0.6mm;
+ font-family: "native:MainLight";
+ font-size: 3mm;
+}
+
+SkinDesignerFilterRow {
+ background: transparent;
+ padding: 0;
margin: 0;
- padding: 0.8mm;
}
+SkinDesignerFilterRowDark { background: transparent; padding: 0; margin: 0; }
-TabDark {
+SkinDesignerFilterTag {
+ color: #7f8aa3;
+ background: #ffffff;
+ border: 1px solid #d9dee8;
+ border-radius: 6mm;
+ padding: 0.6mm 2mm;
+ margin: 0.3mm;
+ font-family: "native:MainLight";
+ font-size: 2.4mm;
+}
+SkinDesignerFilterTagDark {
+ color: #a8b8da;
background: #102b66;
- color: #f5f8ff;
border: 1px solid #4c6ea8;
+ border-radius: 6mm;
+ padding: 0.6mm 2mm;
+ margin: 0.3mm;
font-family: "native:MainLight";
- font-size: 2.8mm;
- text-align: center;
+ font-size: 2.4mm;
+}
+SkinDesignerFilterTagActive {
+ color: #ffffff;
+ background: #2f6bff;
+ border: 1px solid #2f6bff;
+ border-radius: 6mm;
+ padding: 0.6mm 2mm;
+ margin: 0.3mm;
+ font-family: "native:MainBold";
+ font-size: 2.4mm;
+}
+SkinDesignerFilterTagActiveDark {
+ color: #ffffff;
+ background: #4d86ff;
+ border: 1px solid #4d86ff;
+ border-radius: 6mm;
+ padding: 0.6mm 2mm;
+ margin: 0.3mm;
+ font-family: "native:MainBold";
+ font-size: 2.4mm;
+}
+
+SkinDesignerGroupLabel {
+ color: #7f8aa3;
+ font-size: 2.2mm;
+ font-family: "native:MainBold";
+ padding: 1.2mm 1mm 0.4mm 1mm;
+ margin: 0;
+ background: transparent;
+}
+SkinDesignerGroupLabelDark { color: #a8b8da; font-size: 2.2mm; font-family: "native:MainBold"; padding: 1.2mm 1mm 0.4mm 1mm; margin: 0; background: transparent; }
+
+SkinDesignerCardRow {
+ background: transparent;
+ padding: 0 0.5mm;
margin: 0;
- padding: 0.8mm;
}
+SkinDesignerCardRowDark { background: transparent; padding: 0 0.5mm; margin: 0; }
-TabSelected {
+/* --- Device cards --------------------------------------------------- */
+
+SkinDesignerDeviceGrid {
+ background: transparent;
+ padding: 0 2mm;
+}
+SkinDesignerDeviceGridDark { background: transparent; padding: 0 2mm; }
+
+SkinDesignerDeviceCard {
+ background: #ffffff;
+ border: 1px solid #d9dee8;
+ border-radius: 2.4mm;
+ padding: 1.6mm;
+ margin: 0.5mm;
+}
+SkinDesignerDeviceCardDark {
+ background: #102b66;
+ border: 1px solid #4c6ea8;
+ border-radius: 2.4mm;
+ padding: 1.6mm;
+ margin: 0.5mm;
+}
+SkinDesignerDeviceCardSelected {
background: #ffffff;
- color: #112247;
border: 2px solid #2f6bff;
+ border-radius: 2.4mm;
+ padding: 1.6mm;
+ margin: 0.5mm;
+}
+SkinDesignerDeviceCardSelectedDark {
+ background: #102b66;
+ border: 2px solid #4d86ff;
+ border-radius: 2.4mm;
+ padding: 1.6mm;
+ margin: 0.5mm;
}
-TabSelectedDark {
- background: #163575;
+SkinDesignerOsMark {
+ color: #112247;
+ background: #f7f8fb;
+ border-radius: 1.4mm;
+ padding: 0.6mm;
+ margin: 0 0.6mm 0 0;
+ font-size: 2.6mm;
+ text-align: center;
+}
+SkinDesignerOsMarkDark {
color: #f5f8ff;
- border: 2px solid #4d86ff;
+ background: #163575;
+ border-radius: 1.4mm;
+ padding: 0.6mm;
+ margin: 0 0.6mm 0 0;
+ font-size: 2.6mm;
+ text-align: center;
}
-TabsContainer {
- background: #eef3ff;
+SkinDesignerDeviceName {
+ color: #112247;
+ font-size: 2.8mm;
+ font-family: "native:MainBold";
+ padding: 0.4mm;
+}
+SkinDesignerDeviceNameDark {
+ color: #f5f8ff;
+ font-size: 2.8mm;
+ font-family: "native:MainBold";
+ padding: 0.4mm;
+}
+SkinDesignerDeviceSpec {
+ color: #7f8aa3;
+ font-size: 2.2mm;
+ font-family: "native:MainLight";
+ padding: 0.4mm;
}
+SkinDesignerDeviceSpecDark {
+ color: #a8b8da;
+ font-size: 2.2mm;
+ font-family: "native:MainLight";
+ padding: 0.4mm;
+}
+SkinDesignerDeviceCheck {
+ color: #2f6bff;
+ background: transparent;
+ padding: 0;
+ font-size: 3mm;
+}
+SkinDesignerDeviceCheckDark {
+ color: #4d86ff;
+ background: transparent;
+ padding: 0;
+ font-size: 3mm;
+}
+
+/* --- Source step ---------------------------------------------------- */
-TabsContainerDark {
- background: #0a111d;
+SkinDesignerSourceRow {
+ background: transparent;
+ padding: 1mm 4mm;
}
+SkinDesignerSourceRowDark { background: transparent; padding: 1mm 4mm; }
-SkinDesignerCard {
+SkinDesignerSourceCard {
background: #ffffff;
border: 1px solid #d9dee8;
- border-radius: 2mm;
+ border-radius: 3mm;
+ padding: 4mm;
margin: 1mm;
- padding: 1.2mm;
}
-
-SkinDesignerCardDark {
+SkinDesignerSourceCardDark {
background: #102b66;
border: 1px solid #4c6ea8;
- border-radius: 2mm;
+ border-radius: 3mm;
+ padding: 4mm;
margin: 1mm;
- padding: 1.2mm;
}
-SkinDesignerFieldLabel {
+SkinDesignerSourceIll {
+ background: #f7f8fb;
+ color: #7f8aa3;
+ padding: 6mm;
+ margin: 0 0 2mm 0;
+ border-radius: 2mm;
+ text-align: center;
+ font-size: 8mm;
+}
+SkinDesignerSourceIllDark {
+ background: #163575;
+ color: #a8b8da;
+ padding: 6mm;
+ margin: 0 0 2mm 0;
+ border-radius: 2mm;
+ text-align: center;
+ font-size: 8mm;
+}
+SkinDesignerSourceP {
+ color: #7f8aa3;
+ font-size: 2.6mm;
+ font-family: "native:MainLight";
+ padding: 0.4mm;
+}
+SkinDesignerSourcePDark {
+ color: #a8b8da;
+ font-size: 2.6mm;
+ font-family: "native:MainLight";
+ padding: 0.4mm;
+}
+
+/* --- Editor / stage / sidebar --------------------------------------- */
+
+SkinDesignerStage {
+ background: #fafafc;
+ padding: 0;
+}
+SkinDesignerStageDark { background: #112f70; padding: 0; }
+
+SkinDesignerStageChips {
+ background: transparent;
+ padding: 0.5mm;
+}
+SkinDesignerStageChipsDark { background: transparent; padding: 0.5mm; }
+
+SkinDesignerInfoChip {
+ background: #ffffff;
+ border: 1px solid #d9dee8;
+ border-radius: 1.4mm;
+ padding: 0.6mm 1.4mm;
+ margin: 0.5mm;
+ font-size: 2.2mm;
+}
+SkinDesignerInfoChipDark {
+ background: #102b66;
+ border: 1px solid #4c6ea8;
+ border-radius: 1.4mm;
+ padding: 0.6mm 1.4mm;
+ margin: 0.5mm;
+ font-size: 2.2mm;
+}
+SkinDesignerChipKey {
+ color: #7f8aa3;
+ font-size: 2.2mm;
+ font-family: "native:MainLight";
+ padding: 0 0.4mm 0 0;
+}
+SkinDesignerChipKeyDark { color: #a8b8da; font-size: 2.2mm; font-family: "native:MainLight"; padding: 0 0.4mm 0 0; }
+SkinDesignerChipValue {
color: #112247;
+ font-size: 2.2mm;
+ font-family: "native:MainBold";
+ padding: 0;
+}
+SkinDesignerChipValueDark { color: #f5f8ff; font-size: 2.2mm; font-family: "native:MainBold"; padding: 0; }
+
+DevicePreview {
+ background: transparent;
+ padding: 4mm;
+}
+DevicePreviewDark { background: transparent; padding: 4mm; }
+
+SkinDesignerSidebar {
+ background: #ffffff;
+ border-left: 1px solid #d9dee8;
+ padding: 0;
+}
+SkinDesignerSidebarDark {
+ background: #102b66;
+ border-left: 1px solid #4c6ea8;
+ padding: 0;
+}
+
+SkinDesignerSidebarTabs {
+ background: #ffffff;
+ border-bottom: 1px solid #d9dee8;
+ padding: 0;
+}
+SkinDesignerSidebarTabsDark {
+ background: #102b66;
+ border-bottom: 1px solid #4c6ea8;
+ padding: 0;
+}
+
+SkinDesignerSidebarTab {
+ color: #7f8aa3;
+ background: transparent;
+ border: none;
+ border-bottom: 2px solid transparent;
+ padding: 1.6mm 1mm;
+ margin: 0;
font-family: "native:MainLight";
- font-size: 2.8mm;
+ font-size: 2.6mm;
+ text-align: center;
+}
+SkinDesignerSidebarTabDark {
+ color: #a8b8da;
+ background: transparent;
+ border: none;
+ border-bottom: 2px solid transparent;
+ padding: 1.6mm 1mm;
margin: 0;
- padding: 0.8mm 0.3mm 0.2mm 0.3mm;
+ font-family: "native:MainLight";
+ font-size: 2.6mm;
+ text-align: center;
+}
+SkinDesignerSidebarTabActive {
+ color: #2f6bff;
+ background: transparent;
+ border: none;
+ border-bottom: 2px solid #2f6bff;
+ padding: 1.6mm 1mm;
+ margin: 0;
+ font-family: "native:MainBold";
+ font-size: 2.6mm;
+ text-align: center;
+}
+SkinDesignerSidebarTabActiveDark {
+ color: #4d86ff;
+ background: transparent;
+ border: none;
+ border-bottom: 2px solid #4d86ff;
+ padding: 1.6mm 1mm;
+ margin: 0;
+ font-family: "native:MainBold";
+ font-size: 2.6mm;
+ text-align: center;
+}
+
+SkinDesignerSidebarBody {
+ background: #ffffff;
+ padding: 2mm;
+}
+SkinDesignerSidebarBodyDark { background: #102b66; padding: 2mm; }
+
+SkinDesignerSidebarFoot {
+ background: #ffffff;
+ border-top: 1px solid #d9dee8;
+ padding: 1.4mm 2mm;
+}
+SkinDesignerSidebarFootDark {
+ background: #102b66;
+ border-top: 1px solid #4c6ea8;
+ padding: 1.4mm 2mm;
+}
+
+/* --- Sections / fields --------------------------------------------- */
+
+SkinDesignerSectionLabel {
+ color: #7f8aa3;
+ font-size: 2.2mm;
+ font-family: "native:MainBold";
+ padding: 1.4mm 0 0.6mm 0;
+}
+SkinDesignerSectionLabelDark {
+ color: #a8b8da;
+ font-size: 2.2mm;
+ font-family: "native:MainBold";
+ padding: 1.4mm 0 0.6mm 0;
+}
+
+SkinDesignerHelpBlock {
+ color: #a0a8ba;
+ background: #f7f8fb;
+ border-radius: 1.4mm;
+ padding: 1.4mm 1.8mm;
+ margin: 0 0 1.4mm 0;
+ font-size: 2.2mm;
+ font-family: "native:MainLight";
+}
+SkinDesignerHelpBlockDark {
+ color: #7e93bc;
+ background: #163575;
+ border-radius: 1.4mm;
+ padding: 1.4mm 1.8mm;
+ margin: 0 0 1.4mm 0;
+ font-size: 2.2mm;
+ font-family: "native:MainLight";
+}
+
+SkinDesignerEmptyHint {
+ color: #a0a8ba;
background: transparent;
+ padding: 2mm;
+ margin: 0;
+ font-size: 2.4mm;
+ font-family: "native:MainLight";
+ text-align: center;
}
+SkinDesignerEmptyHintDark { color: #7e93bc; background: transparent; padding: 2mm; margin: 0; font-size: 2.4mm; font-family: "native:MainLight"; text-align: center; }
+SkinDesignerFieldLabel {
+ color: #7f8aa3;
+ font-size: 2.2mm;
+ font-family: "native:MainLight";
+ padding: 0.4mm 0;
+ margin: 0;
+}
SkinDesignerFieldLabelDark {
- color: #f5f8ff;
+ color: #a8b8da;
+ font-size: 2.2mm;
font-family: "native:MainLight";
- font-size: 2.8mm;
+ padding: 0.4mm 0;
margin: 0;
- padding: 0.8mm 0.3mm 0.2mm 0.3mm;
- background: transparent;
}
SkinDesignerField {
color: #112247;
background: #ffffff;
border: 1px solid #d9dee8;
- padding: 1mm;
+ border-radius: 1.4mm;
+ padding: 1mm 1.4mm;
+ margin: 0.3mm;
font-family: "native:MainLight";
- font-size: 3mm;
+ font-size: 2.6mm;
}
-
SkinDesignerFieldDark {
color: #f5f8ff;
+ background: #0e2a61;
+ border: 1px solid #4c6ea8;
+ border-radius: 1.4mm;
+ padding: 1mm 1.4mm;
+ margin: 0.3mm;
+ font-family: "native:MainLight";
+ font-size: 2.6mm;
+}
+
+SkinDesignerFieldReadonly {
+ color: #7f8aa3;
+ background: #f7f8fb;
+ border: 1px solid #d9dee8;
+ border-radius: 1.4mm;
+ padding: 1mm 1.4mm;
+ margin: 0.3mm;
+ font-family: "native:MainLight";
+ font-size: 2.6mm;
+}
+SkinDesignerFieldReadonlyDark {
+ color: #a8b8da;
background: #163575;
border: 1px solid #4c6ea8;
- padding: 1mm;
+ border-radius: 1.4mm;
+ padding: 1mm 1.4mm;
+ margin: 0.3mm;
font-family: "native:MainLight";
- font-size: 3mm;
+ font-size: 2.6mm;
}
-SkinDesignerActionButton {
- color: #ffffff;
- background: #2f6bff;
- border: none;
- padding: 0.9mm 1.4mm;
+SkinDesignerFieldRow { background: transparent; padding: 0; margin: 0.4mm 0; }
+SkinDesignerFieldRowDark { background: transparent; padding: 0; margin: 0.4mm 0; }
+SkinDesignerFieldGrid { background: transparent; padding: 0; margin: 0.4mm 0; }
+SkinDesignerFieldGridDark { background: transparent; padding: 0; margin: 0.4mm 0; }
+
+/* --- Preset tiles -------------------------------------------------- */
+
+SkinDesignerPresetGrid { background: transparent; padding: 0; }
+SkinDesignerPresetGridDark { background: transparent; padding: 0; }
+
+SkinDesignerPreset {
+ background: #f7f8fb;
+ border: 1px solid #d9dee8;
+ border-radius: 2mm;
+ padding: 1.6mm 1mm;
+ margin: 0.3mm;
+}
+SkinDesignerPresetDark {
+ background: #163575;
+ border: 1px solid #4c6ea8;
+ border-radius: 2mm;
+ padding: 1.6mm 1mm;
+ margin: 0.3mm;
+}
+SkinDesignerPresetSelected {
+ background: #e8f0ff;
+ border: 2px solid #2f6bff;
+ border-radius: 2mm;
+ padding: 1.6mm 1mm;
+ margin: 0.3mm;
+}
+SkinDesignerPresetSelectedDark {
+ background: rgba(77,134,255,0.16);
+ border: 2px solid #4d86ff;
+ border-radius: 2mm;
+ padding: 1.6mm 1mm;
+ margin: 0.3mm;
+}
+SkinDesignerPresetIcon {
+ color: #112247;
+ background: transparent;
+ padding: 0.4mm;
+}
+SkinDesignerPresetIconDark { color: #f5f8ff; background: transparent; padding: 0.4mm; }
+SkinDesignerPresetLabel {
+ color: #7f8aa3;
+ font-size: 2mm;
font-family: "native:MainLight";
- font-size: 2.7mm;
+ padding: 0.4mm;
text-align: center;
}
+SkinDesignerPresetLabelDark { color: #a8b8da; font-size: 2mm; font-family: "native:MainLight"; padding: 0.4mm; text-align: center; }
-SkinDesignerActionButtonDark {
+/* --- Cutout list --------------------------------------------------- */
+
+SkinDesignerCutoutRow {
+ background: #f7f8fb;
+ border: 1px solid #d9dee8;
+ border-radius: 1.4mm;
+ padding: 1mm 1.4mm;
+ margin: 0.3mm 0;
+}
+SkinDesignerCutoutRowDark {
+ background: #163575;
+ border: 1px solid #4c6ea8;
+ border-radius: 1.4mm;
+ padding: 1mm 1.4mm;
+ margin: 0.3mm 0;
+}
+SkinDesignerCutoutRowSelected {
+ background: #e8f0ff;
+ border: 1px solid #2f6bff;
+ border-radius: 1.4mm;
+ padding: 1mm 1.4mm;
+ margin: 0.3mm 0;
+}
+SkinDesignerCutoutRowSelectedDark {
+ background: rgba(77,134,255,0.16);
+ border: 1px solid #4d86ff;
+ border-radius: 1.4mm;
+ padding: 1mm 1.4mm;
+ margin: 0.3mm 0;
+}
+SkinDesignerCutoutSwatch {
+ background: #112247;
+ color: #112247;
+ border-radius: 0.8mm;
+ padding: 0.6mm;
+ margin: 0 0.6mm 0 0;
+ font-size: 1mm;
+}
+SkinDesignerCutoutSwatchDark {
+ background: #f5f8ff;
color: #f5f8ff;
- background: #4d86ff;
- border: none;
- padding: 0.9mm 1.4mm;
+ border-radius: 0.8mm;
+ padding: 0.6mm;
+ margin: 0 0.6mm 0 0;
+ font-size: 1mm;
+}
+SkinDesignerCutoutName {
+ color: #112247;
+ font-size: 2.6mm;
+ font-family: "native:MainBold";
+ padding: 0.4mm;
+}
+SkinDesignerCutoutNameDark { color: #f5f8ff; font-size: 2.6mm; font-family: "native:MainBold"; padding: 0.4mm; }
+SkinDesignerCutoutType {
+ color: #7f8aa3;
font-family: "native:MainLight";
- font-size: 2.7mm;
+ font-size: 2mm;
+ padding: 0 0.6mm;
+}
+SkinDesignerCutoutTypeDark { color: #a8b8da; font-family: "native:MainLight"; font-size: 2mm; padding: 0 0.6mm; }
+
+SkinDesignerCutoutEditor {
+ background: #e8f0ff;
+ border-radius: 1.4mm;
+ padding: 1.4mm;
+ margin: 0.4mm 0;
+}
+SkinDesignerCutoutEditorDark {
+ background: rgba(77,134,255,0.16);
+ border-radius: 1.4mm;
+ padding: 1.4mm;
+ margin: 0.4mm 0;
+}
+
+/* --- Done step ----------------------------------------------------- */
+
+SkinDesignerDoneRoot {
+ background: #f3f4f7;
+ padding: 8mm 4mm;
+}
+SkinDesignerDoneRootDark {
+ background: #071b4d;
+ padding: 8mm 4mm;
+}
+SkinDesignerDoneCheck {
+ background: #b8d532;
+ color: #112247;
+ border-radius: 12mm;
+ padding: 4mm;
+ margin: 1mm;
+ font-size: 8mm;
text-align: center;
}
+SkinDesignerDoneCheckDark {
+ background: #b8d532;
+ color: #0a2460;
+ border-radius: 12mm;
+ padding: 4mm;
+ margin: 1mm;
+ font-size: 8mm;
+ text-align: center;
+}
+
+SkinDesignerSummary {
+ background: #ffffff;
+ border: 1px solid #d9dee8;
+ border-radius: 2.4mm;
+ padding: 4mm;
+ margin: 2mm;
+ font-size: 2.6mm;
+}
+SkinDesignerSummaryDark {
+ background: #102b66;
+ border: 1px solid #4c6ea8;
+ border-radius: 2.4mm;
+ padding: 4mm;
+ margin: 2mm;
+ font-size: 2.6mm;
+}
+SkinDesignerSummaryRow {
+ background: transparent;
+ border-bottom: 1px solid #d9dee8;
+ padding: 1mm 0;
+ margin: 0;
+}
+SkinDesignerSummaryRowDark {
+ background: transparent;
+ border-bottom: 1px solid #4c6ea8;
+ padding: 1mm 0;
+ margin: 0;
+}
+SkinDesignerSummaryKey {
+ color: #7f8aa3;
+ font-size: 2.6mm;
+ font-family: "native:MainLight";
+ padding: 0.4mm;
+}
+SkinDesignerSummaryKeyDark { color: #a8b8da; font-size: 2.6mm; font-family: "native:MainLight"; padding: 0.4mm; }
+SkinDesignerSummaryValue {
+ color: #112247;
+ font-size: 2.6mm;
+ font-family: "native:MainBold";
+ padding: 0.4mm;
+}
+SkinDesignerSummaryValueDark { color: #f5f8ff; font-size: 2.6mm; font-family: "native:MainBold"; padding: 0.4mm; }
diff --git a/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/DeviceDatabase.java b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/DeviceDatabase.java
new file mode 100644
index 0000000000..7a3b3c90a5
--- /dev/null
+++ b/scripts/skindesigner/common/src/main/java/com/codename1/tools/skindesigner/DeviceDatabase.java
@@ -0,0 +1,253 @@
+package com.codename1.tools.skindesigner;
+
+import com.codename1.io.JSONParser;
+import com.codename1.io.Log;
+import com.codename1.ui.Display;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Bundled device database loaded from {@code /devices.json} at startup.
+ *
+ * The JSON catalog is generated by
+ * {@code tools/devicedb/build_devices_json.py} from a public GSMArena dump
+ * and refreshed by CI on a schedule. Bundling it avoids any runtime fetch,
+ * which would hit CORS in the JavaScript port.
+ */
+public final class DeviceDatabase {
+ public static final String FORM_PHONE = "Phone";
+ public static final String FORM_TABLET = "Tablet";
+ public static final String FORM_FOLDABLE = "Foldable";
+
+ public static final class Device {
+ public final String id;
+ public final String brand;
+ public final String os;
+ public final int year;
+ public final String form;
+ public final String name;
+ public final int resolutionW;
+ public final int resolutionH;
+ public final double screenSize;
+ public final int ppi;
+ public final String platformName;
+ public final boolean hasNotch;
+ public final boolean hasIsland;
+ public final boolean hasHole;
+ public final boolean hasHomeIndicator;
+ public final int safeTop;
+ public final int safeBottom;
+ public final int safeLeft;
+ public final int safeRight;
+ public final String systemFont;
+ public final String proportionalFont;
+ public final String monoFont;
+ public final int fontSmall;
+ public final int fontMedium;
+ public final int fontLarge;
+ public final boolean tablet;
+
+ Device(String id, String brand, String os, int year, String form, String name,
+ int rw, int rh, double screenSize, int ppi,
+ String platformName,
+ boolean hasNotch, boolean hasIsland, boolean hasHole, boolean hasHomeIndicator,
+ int safeTop, int safeBottom, int safeLeft, int safeRight,
+ String systemFont, String proportionalFont, String monoFont,
+ int fontSmall, int fontMedium, int fontLarge,
+ boolean tablet) {
+ this.id = id;
+ this.brand = brand;
+ this.os = os;
+ this.year = year;
+ this.form = form;
+ this.name = name;
+ this.resolutionW = rw;
+ this.resolutionH = rh;
+ this.screenSize = screenSize;
+ this.ppi = ppi;
+ this.platformName = platformName;
+ this.hasNotch = hasNotch;
+ this.hasIsland = hasIsland;
+ this.hasHole = hasHole;
+ this.hasHomeIndicator = hasHomeIndicator;
+ this.safeTop = safeTop;
+ this.safeBottom = safeBottom;
+ this.safeLeft = safeLeft;
+ this.safeRight = safeRight;
+ this.systemFont = systemFont;
+ this.proportionalFont = proportionalFont;
+ this.monoFont = monoFont;
+ this.fontSmall = fontSmall;
+ this.fontMedium = fontMedium;
+ this.fontLarge = fontLarge;
+ this.tablet = tablet;
+ }
+
+ public boolean matchesFormFilter(String filter) {
+ if (filter == null || "all".equals(filter)) return true;
+ if ("phone".equals(filter)) return FORM_PHONE.equals(form);
+ if ("tablet".equals(filter)) return FORM_TABLET.equals(form);
+ if ("fold".equals(filter)) return FORM_FOLDABLE.equals(form);
+ return true;
+ }
+
+ public boolean matchesQuery(String query) {
+ if (query == null || query.isEmpty()) return true;
+ String q = query.toLowerCase();
+ return name.toLowerCase().contains(q) || brand.toLowerCase().contains(q);
+ }
+ }
+
+ private static List CACHE;
+ private static List BRANDS_CACHE;
+
+ /** Bundled JSON resource path; loaded once and cached. */
+ public static synchronized List all() {
+ if (CACHE == null) {
+ CACHE = load();
+ }
+ return CACHE;
+ }
+
+ /** Distinct brand names ordered by descending device count. */
+ public static synchronized List brands() {
+ if (BRANDS_CACHE == null) {
+ Set seen = new TreeSet<>();
+ for (Device d : all()) seen.add(d.brand);
+ BRANDS_CACHE = new ArrayList<>(seen);
+ }
+ return BRANDS_CACHE;
+ }
+
+ public static Device findById(String id) {
+ if (id == null) return null;
+ for (Device d : all()) {
+ if (id.equals(d.id)) return d;
+ }
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static List load() {
+ InputStream is = Display.getInstance().getResourceAsStream(
+ DeviceDatabase.class, "/devices.json");
+ if (is == null) {
+ Log.p("devices.json missing — falling back to empty device list");
+ return Collections.emptyList();
+ }
+ try (InputStreamReader r = new InputStreamReader(is, "UTF-8")) {
+ JSONParser p = new JSONParser();
+ Map root = p.parseJSON(r);
+ Object arr = root.get("devices");
+ if (!(arr instanceof List)) {
+ return Collections.emptyList();
+ }
+ List