diff --git a/CodenameOne/src/com/codename1/ui/Button.java b/CodenameOne/src/com/codename1/ui/Button.java index 423b961ef20..bbf215882fa 100644 --- a/CodenameOne/src/com/codename1/ui/Button.java +++ b/CodenameOne/src/com/codename1/ui/Button.java @@ -1034,17 +1034,125 @@ public boolean isToggle() { /// #### Parameters /// /// - `toggle`: the toggle to set + /// The UIID this control carried before setToggle(true) replaced it, so + /// setToggle(false) can put it back. Null whenever toggle mode was not + /// entered from a CheckBox or RadioButton UIID. + private String preToggleUIID; + public void setToggle(boolean toggle) { if (this.toggle == toggle) { return; } this.toggle = toggle; accessibilityChanged(AccessibilityManager.CHANGE_STRUCTURE | AccessibilityManager.CHANGE_STATE); - if (toggle && "CheckBox".equals(getUIID()) || "RadioButton".equals(getUIID())) { - setUIID("ToggleButton"); + // The UIID follows the mode in both directions. Two things were wrong here + // for as long as no theme defined ToggleButton, which is what kept them + // invisible: + // + // - && binds tighter than ||, so the old condition read + // (toggle && isCheckBox) || isRadioButton. setToggle(false) on a + // RadioButton therefore set its UIID to ToggleButton, the opposite of + // what was asked for. + // - nothing restored the UIID afterwards, so a control taken back out of + // toggle mode kept the toggle's appearance while painting its state + // glyph again. + // + // setUIID clears preferredSize, so the size computed without the glyph is + // recalculated on the next layout pass. + String uiid = getUIID(); + if (toggle) { + if ("CheckBox".equals(uiid) || "RadioButton".equals(uiid)) { + preToggleUIID = uiid; + setUIID("ToggleButton"); + } + // Grouped, the live UIID is the group's alias and says nothing about + // what this control is, so the name that matters is the one the group + // saved to restore on removal. Entering toggle mode has to move that to + // ToggleButton, exactly as leaving it moves it back, or a control + // toggled while inside a group is handed a radio UIID on the way out. + Object saved = getClientProperty("$origUIID"); + if (isDefaultToggleableUIID(saved)) { + preToggleUIID = (String) saved; + putClientProperty("$origUIID", "ToggleButton"); + } + } else if (preToggleUIID != null) { + // Two places can be holding the toggle UIID, and which ones depends on + // the group this control is in -- horizontal groups rename members to + // ToggleButton*, vertical ones to GroupElement*, and an ungrouped + // control keeps the name setToggle assigned. So each is corrected on + // its own terms rather than by guessing the group's prefix: + // + // - the live UIID, when it is still a toggle name; + // - $origUIID, which is what a ComponentGroup puts back when the + // control leaves it, and which holds ToggleButton whatever prefix + // the group itself uses. + // While the control is still in a group the live UIID belongs to the + // group: a horizontal one renames every member, toggle or not, to give + // the bar its segmented edges, and it does not re-apply that when the + // UIID changes under it. Resetting it here would strip the member's + // styling until the next structural or theme update. $origUIID is not + // the test for that -- ComponentGroup sets it once and never clears it, + // so it says "was grouped at some point" rather than "is grouped now". + if (!groupOwnsUIID() && isToggleUIID(uiid)) { + setUIID(preToggleUIID); + } + Object saved = getClientProperty("$origUIID"); + if (saved instanceof String && isToggleUIID((String) saved)) { + putClientProperty("$origUIID", preToggleUIID); + } + preToggleUIID = null; } } + /// Whether a ComponentGroup is currently holding this control's UIID. Being + /// inside one is not enough: updateUIIDs() returns without renaming anything + /// when ComponentGroupBool is off and the group is not forced -- the default, + /// and what Android Material ships -- and in that case the live UIID is still + /// ours to restore. The group is asked directly rather than inferred from the + /// UIID it saved, because restoreUIID leaves that saved value in place when + /// grouping is switched off, so it outlives the ownership it recorded. + /// + /// #### Returns + /// + /// true if a group renamed this control + private boolean groupOwnsUIID() { + Container parent = getParent(); + return parent instanceof ComponentGroup && ((ComponentGroup) parent).isGroupingActive(); + } + + /// Whether the given saved UIID is one setToggle is allowed to convert. An + /// application that assigned its own UIID keeps it: the original code only + /// ever converted the two defaults, and the guide says as much. + /// + /// #### Parameters + /// + /// - `saved`: the value recorded by a ComponentGroup + /// + /// #### Returns + /// + /// true for the default CheckBox and RadioButton UIIDs + private static boolean isDefaultToggleableUIID(Object saved) { + return "CheckBox".equals(saved) || "RadioButton".equals(saved); + } + + /// True for the UIID setToggle assigns and for the three a horizontal + /// ComponentGroup renames its edge controls to. Matching only the bare name + /// would skip the restore for any toggle that happens to sit in a group. + /// + /// #### Parameters + /// + /// - `uiid`: the UIID to test + /// + /// #### Returns + /// + /// true if this is a toggle UIID + private static boolean isToggleUIID(String uiid) { + return "ToggleButton".equals(uiid) + || "ToggleButtonFirst".equals(uiid) + || "ToggleButtonLast".equals(uiid) + || "ToggleButtonOnly".equals(uiid); + } + /// Overriden to workaround issue with caps text and different UIID's /// {@inheritDoc} @Override diff --git a/CodenameOne/src/com/codename1/ui/ComponentGroup.java b/CodenameOne/src/com/codename1/ui/ComponentGroup.java index ae951bb275e..bfaf78edc47 100644 --- a/CodenameOne/src/com/codename1/ui/ComponentGroup.java +++ b/CodenameOne/src/com/codename1/ui/ComponentGroup.java @@ -183,6 +183,19 @@ private void updateUIID(String newUIID, Component c) { reverseRadio(c); } + /// Whether this group is currently renaming its members' UIIDs. updateUIIDs() + /// does nothing unless the theme constant is on or grouping is forced, and + /// Button needs the same answer to know whether a member's live UIID belongs + /// to the group or to itself. $origUIID cannot answer it: restoreUIID leaves + /// that set, so it says "was renamed once", not "is renamed now". + /// + /// #### Returns + /// + /// true when this group assigns its members' UIIDs + boolean isGroupingActive() { + return getUIManager().isThemeConstant(groupFlag, false) || forceGroup; + } + private void restoreUIID(Component c) { String o = (String) c.getClientProperty("$origUIID"); if (o != null) { diff --git a/Ports/Android/src/AndroidMaterialTheme.res b/Ports/Android/src/AndroidMaterialTheme.res index 2b8897c3e92..de3c0252c13 100644 Binary files a/Ports/Android/src/AndroidMaterialTheme.res and b/Ports/Android/src/AndroidMaterialTheme.res differ diff --git a/Ports/iOSPort/nativeSources/iOSModernTheme.res b/Ports/iOSPort/nativeSources/iOSModernTheme.res index a09e3b6e1db..df0f3650494 100644 Binary files a/Ports/iOSPort/nativeSources/iOSModernTheme.res and b/Ports/iOSPort/nativeSources/iOSModernTheme.res differ diff --git a/Themes/AndroidMaterialTheme.res b/Themes/AndroidMaterialTheme.res index 2b8897c3e92..de3c0252c13 100644 Binary files a/Themes/AndroidMaterialTheme.res and b/Themes/AndroidMaterialTheme.res differ diff --git a/Themes/iOSModernTheme.res b/Themes/iOSModernTheme.res index a09e3b6e1db..df0f3650494 100644 Binary files a/Themes/iOSModernTheme.res and b/Themes/iOSModernTheme.res differ diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/screenshots/GuideFigures.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/screenshots/GuideFigures.java index 51b29e7749c..1a2542b7e7c 100644 --- a/docs/demos/common/src/main/java/com/codenameone/developerguide/screenshots/GuideFigures.java +++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/screenshots/GuideFigures.java @@ -77,6 +77,10 @@ public static FigureVariant[] variants() { new FigureVariant(new GraphicsFontimageStyleFigure(), FigureDevice.ANDROID, false, "graphics-fontimage-style.png"), new FigureVariant(new GraphicsFontimageMaterialFigure(), FigureDevice.ANDROID, false, "graphics-fontimage-material.png"), new FigureVariant(new CsvParsingFigure(), FigureDevice.ANDROID, false, "csv-parsing.png"), + new FigureVariant(new ToggleButtonFigure(), FigureDevice.ANDROID, false, + "components-toggle-buttons-android.png"), + new FigureVariant(new ToggleButtonFigure(), FigureDevice.IOS, false, + "components-toggle-buttons-ios.png"), }; } } diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/TheComponentsOfCodenameOneJava156Snippet.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/screenshots/ToggleButtonFigure.java similarity index 51% rename from docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/TheComponentsOfCodenameOneJava156Snippet.java rename to docs/demos/common/src/main/java/com/codenameone/developerguide/screenshots/ToggleButtonFigure.java index 64f187bff1a..fb6887ed441 100644 --- a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/TheComponentsOfCodenameOneJava156Snippet.java +++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/screenshots/ToggleButtonFigure.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as @@ -20,76 +20,33 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codenameone.developerguide.snippets.generated; -import com.codename1.gpu.*; -import com.codename1.ui.*; -import com.codename1.ui.animations.*; -import com.codename1.ui.events.*; -import com.codename1.ui.geom.*; -import com.codename1.ui.layouts.*; -import com.codename1.ui.list.*; -import com.codename1.ui.plaf.*; -import com.codename1.ui.util.*; -import com.codename1.components.*; -import com.codename1.charts.models.*; -import com.codename1.charts.renderers.*; -import com.codename1.charts.views.*; -import com.codename1.capture.*; -import com.codename1.io.*; -import com.codename1.l10n.*; -import com.codename1.location.*; -import com.codename1.maps.*; -import com.codename1.media.*; -import com.codename1.messaging.*; -import com.codename1.payment.*; -import com.codename1.processing.*; -import com.codename1.properties.*; -import com.codename1.push.*; -import com.codename1.security.*; -import com.codename1.social.*; -import com.codename1.ui.spinner.*; -import java.io.*; -import com.codename1.components.ToastBar.Status; -import com.codename1.maps.layers.*; -import com.codename1.charts.*; -import com.codename1.ui.validation.*; -import com.codename1.xml.*; -import com.codename1.charts.util.*; -import com.codename1.javascript.*; -import com.codename1.ui.tree.*; -import com.codename1.ui.table.*; -import com.codename1.contacts.*; -import java.util.*; +package com.codenameone.developerguide.screenshots; +import com.codename1.ui.ButtonGroup; +import com.codename1.ui.CheckBox; +import com.codename1.ui.FontImage; +import com.codename1.ui.Form; +import com.codename1.ui.Image; +import com.codename1.ui.RadioButton; +import com.codename1.ui.layouts.BoxLayout; -class TheComponentsOfCodenameOneJava156Snippet { - +/// The toggle buttons the Components chapter shows beside its `createToggle` +/// sample. +/// +/// Two of the seven are selected on purpose. `setToggle(true)` rewrites the +/// UIID to `ToggleButton`, and the whole point of the figure is the difference +/// between a selected and an unselected one, which the theme draws as a change +/// of fill rather than of label colour alone. A figure with nothing selected +/// would show the shape and hide the state. +class ToggleButtonFigure implements GuideFigure { + @Override + public String id() { + return "components-toggle-buttons"; + } - Object context; - Object url; - Object value; - Object body; - Object event; - String apiKey = "test-key"; - String myHttpsURL = "https://example.com"; - java.util.List validKeysList = new java.util.ArrayList<>(); - Image myImage; - Graphics graphics; - Graphics g; - GraphicsDevice device; - Form form; - Form hi; - Container cnt; - Container myForm; - Component component; - Button button; - MultiButton myMultiButton; - Label label; - BrowserComponent browserComponent; - Resources theme; - - void snippet() throws Exception { + @Override + public Form build() { // tag::the-components-of-codename-one-java-156[] Form hi = new Form("RadioButton", new BoxLayout(BoxLayout.Y_AXIS)); Image icon = FontImage.createMaterial(FontImage.MATERIAL_INFO, "Label", 3.0f); @@ -108,7 +65,12 @@ void snippet() throws Exception { hi.add(cb1).add(cb2).add(cb3).add(cb4).add(rb1).add(rb2).add(rb3); hi.show(); // end::the-components-of-codename-one-java-156[] + // Focus lands on the first focusable control by default, and a focused + // toggle resolves sel# -- the accent ring -- whether or not it is + // checked. That would draw the one control the sample checks as though + // it were not. Focus an unchecked control instead, so the two filled + // rows read as checked and the ring reads as focus. + hi.setFocused(rb1); + return hi; } - - } diff --git a/docs/developer-guide/The-Components-Of-Codename-One.asciidoc b/docs/developer-guide/The-Components-Of-Codename-One.asciidoc index 70e6526bf8f..57f18aab23a 100644 --- a/docs/developer-guide/The-Components-Of-Codename-One.asciidoc +++ b/docs/developer-guide/The-Components-Of-Codename-One.asciidoc @@ -728,11 +728,18 @@ You can convert the sample above to use toggle buttons as such: [source,java] ---- -include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/TheComponentsOfCodenameOneJava156Snippet.java[tag=the-components-of-codename-one-java-156,indent=0] +include::../demos/common/src/main/java/com/codenameone/developerguide/screenshots/ToggleButtonFigure.java[tag=the-components-of-codename-one-java-156,indent=0] ---- -.Toggle button converted sample -image::img/components-toggle-buttons.png[Toggle button converted sample,scaledwidth=20%] +.Toggle button converted sample, on Android Material and iOS +image:img/generated/components-toggle-buttons-android.png[Toggle buttons under Android Material,scaledwidth=35%] +image:img/generated/components-toggle-buttons-ios.png[Toggle buttons under the iOS theme,scaledwidth=35%] + +Both themes draw the selection as a change of fill rather than of label color alone, so a +selected toggle stays readable for someone who can't separate the two hues. The outlined row in +each figure is the one holding focus: focus is drawn as a ring precisely so it can't be mistaken +for a value, since the framework resolves a focused toggle through the same style whether +it's checked or not. That's half the story though: to get the full effect of some cool toggle button UI's you can use a https://www.codenameone.com/javadoc/com/codename1/ui/ComponentGroup.html[ComponentGroup]. This allows you to create a button bar effect with the toggle buttons. diff --git a/docs/developer-guide/img/components-toggle-buttons.png b/docs/developer-guide/img/components-toggle-buttons.png deleted file mode 100644 index 8b2bdc2c46d..00000000000 Binary files a/docs/developer-guide/img/components-toggle-buttons.png and /dev/null differ diff --git a/docs/developer-guide/img/generated/components-table-pinstripe.png b/docs/developer-guide/img/generated/components-table-pinstripe.png index ba62524947e..7b91e2ab79c 100644 Binary files a/docs/developer-guide/img/generated/components-table-pinstripe.png and b/docs/developer-guide/img/generated/components-table-pinstripe.png differ diff --git a/docs/developer-guide/img/generated/components-table-with-spanning.png b/docs/developer-guide/img/generated/components-table-with-spanning.png index 3a9b0d36b37..299584e043e 100644 Binary files a/docs/developer-guide/img/generated/components-table-with-spanning.png and b/docs/developer-guide/img/generated/components-table-with-spanning.png differ diff --git a/docs/developer-guide/img/generated/components-table.png b/docs/developer-guide/img/generated/components-table.png index 04c63c40d47..30c9c0d7b51 100644 Binary files a/docs/developer-guide/img/generated/components-table.png and b/docs/developer-guide/img/generated/components-table.png differ diff --git a/docs/developer-guide/img/generated/components-toggle-buttons-android.png b/docs/developer-guide/img/generated/components-toggle-buttons-android.png new file mode 100644 index 00000000000..42b36ba50f9 Binary files /dev/null and b/docs/developer-guide/img/generated/components-toggle-buttons-android.png differ diff --git a/docs/developer-guide/img/generated/components-toggle-buttons-ios.png b/docs/developer-guide/img/generated/components-toggle-buttons-ios.png new file mode 100644 index 00000000000..cc67e58b638 Binary files /dev/null and b/docs/developer-guide/img/generated/components-toggle-buttons-ios.png differ diff --git a/docs/developer-guide/img/generated/csv-parsing.png b/docs/developer-guide/img/generated/csv-parsing.png index 302e4c55049..39de56ea30d 100644 Binary files a/docs/developer-guide/img/generated/csv-parsing.png and b/docs/developer-guide/img/generated/csv-parsing.png differ diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/RadioButtonTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/RadioButtonTest.java index 0c7b7e594a8..37aa13997c3 100644 --- a/maven/core-unittests/src/test/java/com/codename1/ui/RadioButtonTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/ui/RadioButtonTest.java @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + package com.codename1.ui; import com.codename1.junit.FormTest; @@ -13,6 +36,189 @@ class RadioButtonTest extends UITestBase { + @Test + void testDisablingToggleRestoresTheOriginalUiid() { + RadioButton radio = new RadioButton("Choice"); + assertEquals("RadioButton", radio.getUIID()); + + radio.setToggle(true); + assertEquals("ToggleButton", radio.getUIID()); + + radio.setToggle(false); + assertFalse(radio.isToggle()); + assertEquals("RadioButton", radio.getUIID(), + "leaving toggle mode has to put the UIID back, or the control keeps " + + "painting as a toggle while drawing its radio glyph again"); + } + + @Test + void testDisablingToggleOnARadioUiidDoesNotMakeItAToggleUiid() { + // Regression: the condition read (toggle && isCheckBox) || isRadioButton, + // because && binds tighter than ||, so leaving toggle mode assigned + // ToggleButton whenever the UIID was RadioButton at that moment. + // + // Reaching it needs toggle mode on with a RadioButton UIID, which is what + // an application does when it sets its own UIID on a toggle: the early + // return in setToggle means a control that was never a toggle cannot get + // there, which is why asserting on a fresh RadioButton proves nothing. + RadioButton radio = new RadioButton("Choice"); + radio.setToggle(true); + radio.setUIID("RadioButton"); + + radio.setToggle(false); + + assertFalse(radio.isToggle()); + assertEquals("RadioButton", radio.getUIID()); + } + + @Test + void testDisablingToggleRestoresTheUiidInsideAComponentGroup() { + // A horizontal ComponentGroup renames its members to ToggleButtonFirst, + // ToggleButton and ToggleButtonLast, and records what it replaced in + // $origUIID so it can put it back on removal. A restore that only matched + // the bare name skipped all of those, and the group then reinstated + // ToggleButton on a control that was no longer a toggle. + ComponentGroup group = new ComponentGroup(); + group.setForceGroup(true); + group.setHorizontal(true); + RadioButton first = new RadioButton("One"); + RadioButton last = new RadioButton("Two"); + first.setToggle(true); + last.setToggle(true); + group.addComponent(first); + group.addComponent(last); + assertEquals("ToggleButtonFirst", first.getUIID()); + + first.setToggle(false); + + assertFalse(first.isToggle()); + assertEquals("ToggleButtonFirst", first.getUIID(), + "the live UIID belongs to the group while the control is in it: the " + + "group renames every member for the segmented edges and does " + + "not re-apply that when the UIID changes under it"); + + group.removeComponent(first); + assertEquals("RadioButton", first.getUIID(), + "leaving the group must not reinstate the toggle UIID"); + } + + @Test + void testDisablingToggleRestoresTheUiidInsideAVerticalComponentGroup() { + // A vertical group keeps the default prefix, so its members are renamed + // GroupElementFirst and the like -- names with nothing toggle-ish about + // them -- while $origUIID still holds ToggleButton. Matching on the live + // UIID alone therefore missed this case entirely, and removing the control + // put ToggleButton back on a plain radio. + ComponentGroup group = new ComponentGroup(); + group.setForceGroup(true); + RadioButton first = new RadioButton("One"); + RadioButton last = new RadioButton("Two"); + first.setToggle(true); + last.setToggle(true); + group.addComponent(first); + group.addComponent(last); + assertEquals("GroupElementFirst", first.getUIID()); + + first.setToggle(false); + group.removeComponent(first); + + assertFalse(first.isToggle()); + assertEquals("RadioButton", first.getUIID(), + "leaving a vertical group must not reinstate the toggle UIID"); + } + + @Test + void testTogglingOffAndOnInsideAGroupLeavesTheGroupHoldingTheToggleUiid() { + // The group saves what it replaced and restores it on removal, so both + // directions of setToggle have to keep that saved name in step. Only the + // disable direction did: re-enabling saw a group alias as the live UIID, + // recognised nothing, and left the group holding RadioButton for a control + // that was a toggle again. + ComponentGroup group = new ComponentGroup(); + group.setForceGroup(true); + group.setHorizontal(true); + RadioButton first = new RadioButton("One"); + RadioButton last = new RadioButton("Two"); + first.setToggle(true); + last.setToggle(true); + group.addComponent(first); + group.addComponent(last); + + first.setToggle(false); + first.setToggle(true); + + assertTrue(first.isToggle()); + group.removeComponent(first); + assertEquals("ToggleButton", first.getUIID(), + "a control that is a toggle again must not leave the group as a radio"); + } + + @Test + void testTogglingInsideAnInactiveGroupStillRestoresTheUiid() { + // ComponentGroupBool is off by default -- Android Material never sets it -- + // and an unforced group then returns from updateUIIDs without renaming + // anything. The live UIID is still the control's own in that case, so + // skipping the restore merely because the parent is a group left a plain + // radio wearing the toggle style. + ComponentGroup group = new ComponentGroup(); + RadioButton radio = new RadioButton("Choice"); + radio.setToggle(true); + group.addComponent(radio); + assertEquals("ToggleButton", radio.getUIID()); + + radio.setToggle(false); + + assertEquals("RadioButton", radio.getUIID()); + } + + @Test + void testTogglingInsideAGroupKeepsAnApplicationsOwnUiid() { + // setToggle has only ever converted the two default UIIDs; a UIID the + // application chose is left alone. The grouped path has to honour that too, + // or the group hands back ToggleButton instead of the custom name. + ComponentGroup group = new ComponentGroup(); + group.setForceGroup(true); + group.setHorizontal(true); + RadioButton custom = new RadioButton("Choice"); + custom.setUIID("MyChoice"); + RadioButton other = new RadioButton("Other"); + group.addComponent(custom); + group.addComponent(other); + + custom.setToggle(true); + group.removeComponent(custom); + + assertEquals("MyChoice", custom.getUIID(), + "a UIID the application set must survive toggle mode"); + } + + @Test + void testRestoreWorksAfterGroupingIsSwitchedOff() { + // refreshTheme with grouping off calls restoreUIID, which puts the live + // UIID back but leaves $origUIID set. Reading that marker as ownership + // therefore kept reporting "the group has it" long after the group had + // given it back, and the restore was skipped. + ComponentGroup group = new ComponentGroup(); + group.setForceGroup(true); + group.setHorizontal(true); + RadioButton radio = new RadioButton("Choice"); + RadioButton other = new RadioButton("Other"); + radio.setToggle(true); + other.setToggle(true); + group.addComponent(radio); + group.addComponent(other); + assertEquals("ToggleButtonFirst", radio.getUIID()); + + group.setForceGroup(false); + group.refreshTheme(false); + assertEquals("ToggleButton", radio.getUIID(), "the group gives the UIID back"); + + radio.setToggle(false); + + assertEquals("RadioButton", radio.getUIID(), + "with the group no longer grouping, the UIID is the control's to restore"); + } + @Test void testCreateToggleAddsToGroupAndSetsToggleUiid() { ButtonGroup group = new ButtonGroup(); diff --git a/native-themes/COVERAGE.md b/native-themes/COVERAGE.md index 5a138d5e0d7..79ba90ed44b 100644 --- a/native-themes/COVERAGE.md +++ b/native-themes/COVERAGE.md @@ -70,7 +70,7 @@ SwitchMorph (droplet stretch/squash). | Native control | Suggested CN1 building block | Status | |---|---|---| -| UISegmentedControl | ButtonGroup / Tabs pill variant | not started | +| UISegmentedControl | ButtonGroup / Tabs pill variant | `ToggleButton` themed (capsule track); not in the fidelity suite | | UIStepper | Stepper composite (2 glass buttons) | not started | | UISearchBar / searchable nav | Toolbar search mode | not started | | UIActivityIndicatorView | InfiniteProgress | not started (UIID exists, untested) | @@ -134,7 +134,7 @@ SwitchMorph (droplet stretch/squash). | Native control | Suggested CN1 building block | Status | |---|---|---| -| SegmentedButton (single/multi) | ButtonGroup | not started | +| SegmentedButton (single/multi) | ButtonGroup | `ToggleButton` themed (outlined pill); not in the fidelity suite | | Chips (assist/filter/input) | Button chip UIIDs | not started | | NavigationBar (bottom) | Tabs bottom mode | not started (suite tests TabLayout only) | | NavigationDrawer | Toolbar side menu | not started | @@ -149,6 +149,65 @@ SwitchMorph (droplet stretch/squash). | Menu / ExposedDropdown | ComboBox / Command menu | not started | | Card / ElevatedCard | Container card UIIDs | not started | +## UIIDs the framework assigns + +`ToggleButton` shipped unstyled for as long as both themes existed: the UIID is +written by framework code, nothing defined it, and the control fell through with +no shape at all. Nothing errors in that situation -- the control simply looks +wrong -- so the whole surface was enumerated rather than sampled. + +Enumerating it takes two passes, and the second one matters. Every +`setUIID("...")` literal in `CodenameOne/src` gives 128 names, of which 73 are +absent from both theme files. But absent from the CSS is not the same as +unstyled: `UIManager.resetThemeProps()` seeds defaults for a long list of UIIDs, +each behind a guard of the form + +```java +if (installedTheme == null || !installedTheme.containsKey("RightSideCommand.derive")) { + themeProps.put("RightSideCommand.derive", "SideCommand"); + themeProps.put("RightSideCommand.align", rightAlign); +} +``` + +so a theme that defines one of those names **suppresses** the framework's own +setup rather than filling a hole. Twenty-one of the 73 are seeded that way. The +real gap is the remainder: names in neither the CSS nor `resetThemeProps`. + +Both themes now define those, including `ToggleButton` and the `ToggleButton*` +edge names `ComponentGroup` renames its controls to, `FloatingHint`, `TreeNode`, +the `Calendar` grid, the media transport and the directional popup panes. + +### Three deliberate overrides + +| UIID | What the framework seeds | Why the theme overrides it | +|---|---|---| +| `TableCell` | `transparency` only, no padding | every table rendered with cells nearly touching their borders | +| `TableHeader` | `transparency` only | the header was no heavier than a row | +| `ErrorLabel` | `derive: FloatingHint` plus a generic red | the platform error colours differ: `#b3261e` on Material, `#ff3b30` on iOS | + +`FloatingHint` is worth its own note: the framework never defines it, and yet +`ErrorLabel`, `InputComponentAction` and `DescriptionLabel` are all seeded to +derive **from** it. Three UIIDs inheriting from one nothing defined. + +### Deliberately still undefined + +**Retired components.** `RSSReader`, `HTMLComponent`, `HTMLTable`, the +feature-phone soft keys and `VKBButton`. + +**Names where the unstyled default is the better answer**, measured by rendering +the guide's figure set before and after rather than assumed: + +| UIID | What defining it did | +|---|---| +| `icon` | `SpanLabel` and `SpanButton` lost the icon spacing the default gives them | +| `Emblem` | same, for `MultiButton`'s trailing badge | +| `StatusBar` | collapsed the strip a `Form` reserves, so iOS content ran into the system bar | +| `Scroll`, `ScrollThumb`, `HorizontalScroll`, `HorizontalScrollThumb` | the framework seeds these with a measured padding; overriding it changes the content width of every scrollable form, which re-wrapped text in unrelated figures | + +The scrollbar rules alone moved a `SpanLabel` figure by 21 pixels. Sizing a +scrollbar or a status bar is a layout decision that deserves its own change with +its own before-and-after, not a line in a gap-filling sweep. + ## How to add a component 1. Add the YAML entry (`fidelity-tests.yaml`): id, `material:` intent, native diff --git a/native-themes/android-material/theme.css b/native-themes/android-material/theme.css index 3ca88b7c967..966022cb3a7 100644 --- a/native-themes/android-material/theme.css +++ b/native-themes/android-material/theme.css @@ -208,13 +208,6 @@ TertiaryLabel { cn1-derive: Label; color: #79747e; } SpanLabel { cn1-derive: Label; background-color: transparent; } SpanLabelText { cn1-derive: Label; background-color: transparent; } -/* Material 3 button: fully-rounded "pill" shape via cn1-pill-border - (RoundBorder, CEF-free). border-radius (RoundRectBorder) was leaving - visible artefacts when the button height didn't divide cleanly - the - pill border paints a single rounded fill that scales correctly. - Generous horizontal padding so short labels still get clear pill - ends. cn1-derive doesn't carry cn1-background-type forward, so the - derived UIIDs (RaisedButton, FlatButton) re-declare it explicitly. */ /* Re-declare every state-affecting property (padding, margin, font, text-align) on `.pressed` and `.disabled` so the compiled $press# / $dis# style entries keep IDENTICAL geometry to the base. Without @@ -301,6 +294,88 @@ FlatButton { } FlatButton.pressed { background-color: transparent; color: var(--accent-color, #6750a4); cn1-background-type: cn1-pill-border; border: 0.25mm solid #79747e; padding: 1.65mm 5mm 1.65mm 5mm; } +/* Material 3 segmented button. Button.setToggle(true) rewrites the UIID to + ToggleButton, and until this entry existed no theme in the tree defined it, + so every toggle fell through to the default style and rendered as bare text + with no shape at all. + + M3 draws the unselected segment as an outlined pill and fills the selected + one with the secondary container, which is the pairing that makes the + selection readable without relying on colour alone -- the fill changes, not + just the label. The outline stays on in every state so the row keeps its + shape while the selection moves. + + Geometry is repeated in full on each state rather than inherited, for the + reason given at the top of this file: the per-state Style merge otherwise + drifts the text by a pixel as the selection changes. */ +ToggleButton { + color: #1d1b20; + background-color: transparent; + cn1-background-type: cn1-pill-border; + border: 0.25mm solid #79747e; + padding: 1.55mm 4mm 1.55mm 4mm; + margin: 0.5mm; + font-family: "native:MainRegular"; + font-size: 2.5mm; + text-align: center; +} +/* sel# is not the checked look. Button.isPressedStyle() sends a checked toggle + to press# when it has no focus and to sel# when it does, and an unchecked + toggle with focus lands on sel# as well -- so that one slot means "focused", + for either value, and cannot also say which value. + + Neither assignment is lossless, and the choice here is deliberate: + + sel# = the checked fill -> a focused UNCHECKED control looks checked. + Arrowing through a radio group makes every + control you land on claim to be selected. + sel# = base + focus ring -> a focused CHECKED control looks unchecked, + for as long as it holds focus. + + The second is kept because it never asserts a value the control does not + have; the first invents one. It is a framework limit rather than a theme + bug -- one style slot for two dimensions -- and no CSS here can express + both. Until then, do not "fix" this rule by moving the checked fill back + into sel#: that trade has been made once already and it is the worse half. + + dis# has the identical problem and the identical resolution. getStyle() + returns getDisabledStyle() before it consults isPressedStyle(), so a + disabled toggle resolves dis# whether it is checked or not, and a disabled + form cannot show which of its toggles were on. dis# is therefore the muted + unselected appearance, for the same reason: it declines to state a value + rather than stating a wrong one. + + Both cases want the same framework change, which is why neither is patched + here: Component.getStyle() would have to combine state with focus and with + enabled -- a checked control keeping its fill while disabled or focused, + and those conditions drawn some other way -- rather than collapsing three + dimensions onto one style lookup. That reaches every focusable component, + so it is not a line in a theme. */ +ToggleButton.selected { color: #1d1b20; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid var(--accent-color, #6750a4); padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } +ToggleButton.pressed { color: var(--accent-on-container-color, #21005d); background-color: var(--accent-container-color, #eaddff); cn1-background-type: cn1-pill-border; border: 0.25mm solid #79747e; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } +ToggleButton.disabled { color: #a5a0ab; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #cac4d0; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + +/* ComponentGroup renames the edge controls of a group: updateUIIDs() rewrites + them to ToggleButtonFirst, ToggleButtonLast or ToggleButtonOnly, and + UIManager does not fall back from those names to ToggleButton. Without these + entries a two-control group gets no styling at all and a three-control group + styles only its middle. They are deliberately identical to ToggleButton -- + every segment is already a pill, so there is no corner treatment to vary -- + and every state is spelled out because cn1-derive is only read at base + level. */ +ToggleButtonFirst { cn1-derive: ToggleButton; } +ToggleButtonFirst.selected { color: #1d1b20; background-color: transparent; cn1-background-type: cn1-pill-border; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; cn1-derive: ToggleButton; border: 0.25mm solid var(--accent-color, #6750a4); } +ToggleButtonFirst.pressed { color: var(--accent-on-container-color, #21005d); background-color: var(--accent-container-color, #eaddff); cn1-background-type: cn1-pill-border; border: 0.25mm solid #79747e; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } +ToggleButtonFirst.disabled { color: #a5a0ab; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #cac4d0; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } +ToggleButtonLast { cn1-derive: ToggleButton; } +ToggleButtonLast.selected { color: #1d1b20; background-color: transparent; cn1-background-type: cn1-pill-border; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; cn1-derive: ToggleButton; border: 0.25mm solid var(--accent-color, #6750a4); } +ToggleButtonLast.pressed { color: var(--accent-on-container-color, #21005d); background-color: var(--accent-container-color, #eaddff); cn1-background-type: cn1-pill-border; border: 0.25mm solid #79747e; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } +ToggleButtonLast.disabled { color: #a5a0ab; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #cac4d0; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } +ToggleButtonOnly { cn1-derive: ToggleButton; } +ToggleButtonOnly.selected { color: #1d1b20; background-color: transparent; cn1-background-type: cn1-pill-border; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; cn1-derive: ToggleButton; border: 0.25mm solid var(--accent-color, #6750a4); } +ToggleButtonOnly.pressed { color: var(--accent-on-container-color, #21005d); background-color: var(--accent-container-color, #eaddff); cn1-background-type: cn1-pill-border; border: 0.25mm solid #79747e; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } +ToggleButtonOnly.disabled { color: #a5a0ab; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #cac4d0; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + /* Material 3 OUTLINED text field: no fill, a thin outline, taller (more vertical padding), tight margins. A line border is CEF-free. */ TextField { @@ -390,9 +465,6 @@ SliderFull { Slider.disabled { background-color: #e3e3e4; color: #a9a8aa; } SliderFull.disabled { background-color: #a9a8aa; color: #a9a8aa; } -/* Progress indicator: a flat rounded track (no thumb). Distinct from Slider so - the interactive slider can use the M3 purple inactive track + bar thumb while - the progress bar keeps the neutral grey determinate track. */ /* Native Android determinate ProgressBar: SQUARE track (no rounding) and a neutral palette (NOT the accent). Colours below are colour-picked from the native widget, not the M3 token spec. */ @@ -613,12 +685,6 @@ MultiLine2 { cn1-derive: Label; color: #49454f; } MultiLine3 { cn1-derive: Label; color: #79747e; } MultiLine4 { cn1-derive: Label; color: #79747e; } -/* Material 3 dialogs use surface-container-highest (#e6e0e9) so the - dialog sits visibly above the surface background (#fef7ff). All - Dialog sub-UIIDs (Body / Title / ContentPane / CommandArea) are - transparent so the Dialog's surface paints once - otherwise each - sub-region's bgColor draws over Dialog's and reads as a - visibly different shade in the rounded corners. */ /* margin: 0 - see iOS Modern Dialog rule for the rationale. A non-zero margin here forced Dialog.show() to grow the dialog to screen size with the body floating in mostly empty space. */ @@ -766,6 +832,60 @@ PhoneVerificationLink { padding: 1.5mm; } +/* --------------------------------------------------------------------------- + * UIIDs the framework assigns that no theme defined. + * + * Every name below is written by framework code through setUIID, so a control + * using one had no style at all and fell through to the Component default -- + * the same silent failure ToggleButton had. Enumerated rather than guessed: + * the list is every setUIID("...") literal in CodenameOne/src that appears in + * neither theme, minus the ones belonging to retired components (RSSReader, + * HTMLComponent and HTMLTable, the feature-phone soft keys and VKBButton), + * which are left undefined on purpose. + * ------------------------------------------------------------------------- */ + +/* TextComponent is the modern form API: a wrapper container, a hint that floats + above the field once it has content, and an error line beneath it. All three + were undefined, so the control the guide recommends over a raw TextField + rendered with default styling. */ +FloatingHint { cn1-derive: SecondaryLabel; color: #49454f; font-size: 1.9mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } +FloatingHint.disabled { padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; font-family: "native:MainRegular"; font-size: 1.9mm; color: #a9a8aa; background-color: transparent; } +/* M3 error role; the line sits under the field and carries the message. */ +ErrorLabel { cn1-derive: SecondaryLabel; color: #b3261e; font-size: 1.9mm; padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; background-color: transparent; } +ErrorLabel.disabled { padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; font-family: "native:MainRegular"; font-size: 1.9mm; color: #a9a8aa; background-color: transparent; } + +/* Table's documented cell/header UIIDs. The framework does not set these + itself -- the chapter tells you to -- so defining them makes the documented + pattern land on a real style instead of the default. */ +TableCell { cn1-derive: Label; color: #1d1b20; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } +TableHeader { cn1-derive: Label; color: #49454f; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + +/* SideMenuBar drops this icon-only button into the TitleArea's east slot, so + it takes the compact title-command treatment. FlatButton would give it a + content button's horizontal padding and pill, widening the title bar. */ + +/* Every UIID above is completed to all four states below. A missing state is + not inherited: cn1-derive is read at base level only, and a lookup of + .press# that finds nothing resolves against the global default style, + so the control drops its whole appearance for the duration of that state. + Where a state is meant to differ it is declared above and left alone here. */ +ErrorLabel.selected { font-family: "native:MainRegular"; color: #b3261e; font-size: 1.9mm; padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; background-color: transparent; } +ErrorLabel.pressed { font-family: "native:MainRegular"; color: #b3261e; font-size: 1.9mm; padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; background-color: transparent; } +FloatingHint.selected { font-family: "native:MainRegular"; color: #49454f; font-size: 1.9mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } +FloatingHint.pressed { font-family: "native:MainRegular"; color: #49454f; font-size: 1.9mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } +TableCell.selected { margin: 0; font-family: "native:MainRegular"; font-size: 2.5mm; color: #1d1b20; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } +TableCell.pressed { margin: 0; font-family: "native:MainRegular"; font-size: 2.5mm; color: #1d1b20; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } +TableCell.disabled { margin: 0; font-family: "native:MainRegular"; font-size: 2.5mm; color: #1d1b20; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } +TableHeader.selected { margin: 0; font-size: 2.5mm; color: #49454f; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } +TableHeader.pressed { margin: 0; font-size: 2.5mm; color: #49454f; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } +TableHeader.disabled { margin: 0; font-size: 2.5mm; color: #49454f; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + +/* Every UIID above is completed to all four states below. A missing state is + not inherited: cn1-derive is read at base level only, and a lookup of + .press# that finds nothing resolves against the global default style, + so the control drops its whole appearance for the duration of that state. + Where a state is meant to differ it is declared above and left alone here. */ + @media (prefers-color-scheme: dark) { Component { color: #e6e0e9; background-color: #141218; } Form { background-color: #141218; padding: 0; margin: 0; } @@ -794,6 +914,7 @@ PhoneVerificationLink { RaisedButton.disabled { background-color: #2d2b31; color: #737077; cn1-background-type: cn1-pill-border; } FlatButton { color: var(--accent-color-dark, #d0bcff); background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; } + FlatButton.pressed { background-color: transparent; color: var(--accent-color-dark, #d0bcff); cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; } TextField { color: #e6e0e9; background-color: transparent; border: 0.2mm solid #938f99; border-radius: 0.55mm; } @@ -909,4 +1030,41 @@ PhoneVerificationLink { PhoneVerificationText { color: #cac4d0; } PhoneVerificationError { color: #f2b8b5; } PhoneVerificationLink { color: var(--accent-color-dark, #d0bcff); } + + /* Dark counterparts for every rule above that names a colour. A light-only + state rule is not skipped in dark mode -- with no $Dark entry the light + one is what resolves -- so a hard-coded light foreground would be used + against the dark surface. */ + ErrorLabel { cn1-derive: SecondaryLabel; color: #f2b8b5; font-size: 1.9mm; padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; background-color: transparent; } + ErrorLabel.selected { font-family: "native:MainRegular"; color: #f2b8b5; font-size: 1.9mm; padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; background-color: transparent; } + ErrorLabel.pressed { font-family: "native:MainRegular"; color: #f2b8b5; font-size: 1.9mm; padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; background-color: transparent; } + ErrorLabel.disabled { font-size: 1.9mm; padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; color: #5c5967; background-color: transparent; } + FloatingHint { cn1-derive: SecondaryLabel; color: #cac4d0; font-size: 1.9mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } + FloatingHint.selected { font-family: "native:MainRegular"; color: #cac4d0; font-size: 1.9mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } + FloatingHint.pressed { font-family: "native:MainRegular"; color: #cac4d0; font-size: 1.9mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } + FloatingHint.disabled { font-size: 1.9mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; color: #5c5967; background-color: transparent; } + TableCell { cn1-derive: Label; color: #e6e0e9; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + TableCell.selected { margin: 0; font-family: "native:MainRegular"; font-size: 2.5mm; color: #e6e0e9; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + TableCell.pressed { margin: 0; font-family: "native:MainRegular"; font-size: 2.5mm; color: #e6e0e9; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + TableCell.disabled { margin: 0; font-family: "native:MainRegular"; font-size: 2.5mm; color: #e6e0e9; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + TableHeader { cn1-derive: Label; color: #cac4d0; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + TableHeader.selected { margin: 0; font-size: 2.5mm; color: #cac4d0; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + TableHeader.pressed { margin: 0; font-size: 2.5mm; color: #cac4d0; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + TableHeader.disabled { margin: 0; font-size: 2.5mm; color: #cac4d0; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + ToggleButton { color: #e6e0e9; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButton.selected { color: #e6e0e9; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid var(--accent-color-dark, #d0bcff); padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButton.pressed { color: var(--accent-on-container-color-dark, #eaddff); background-color: var(--accent-container-color-dark, #4f378b); cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButton.disabled { color: #5c5967; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #49454f; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButtonFirst { color: #e6e0e9; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButtonLast { color: #e6e0e9; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButtonOnly { color: #e6e0e9; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButtonFirst.selected { color: #e6e0e9; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid var(--accent-color-dark, #d0bcff); padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButtonFirst.pressed { color: var(--accent-on-container-color-dark, #eaddff); background-color: var(--accent-container-color-dark, #4f378b); cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButtonFirst.disabled { color: #5c5967; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #49454f; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButtonLast.selected { color: #e6e0e9; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid var(--accent-color-dark, #d0bcff); padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButtonLast.pressed { color: var(--accent-on-container-color-dark, #eaddff); background-color: var(--accent-container-color-dark, #4f378b); cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButtonLast.disabled { color: #5c5967; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #49454f; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButtonOnly.selected { color: #e6e0e9; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid var(--accent-color-dark, #d0bcff); padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButtonOnly.pressed { color: var(--accent-on-container-color-dark, #eaddff); background-color: var(--accent-container-color-dark, #4f378b); cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButtonOnly.disabled { color: #5c5967; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #49454f; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } } diff --git a/native-themes/ios-modern/theme.css b/native-themes/ios-modern/theme.css index fef24f10a8a..a40d1da582d 100644 --- a/native-themes/ios-modern/theme.css +++ b/native-themes/ios-modern/theme.css @@ -400,6 +400,79 @@ RaisedButton.disabled { color: #ffffff; background-color: #b3a8a0; cn1-backgroun FlatButton { cn1-derive: Button; color: #1c1c1e; background-color: rgba(199,197,198,0.80); cn1-background-type: cn1-pill-border; border: 0.1mm solid rgba(255,255,255,0.58); cn1-stroke-gradient: #8e8e93; cn1-stroke-gradient-angle: 135deg; padding: 1.1mm 2mm 1.1mm 2mm; } FlatButton.pressed { color: #1c1c1e; background-color: rgba(186,184,185,0.85); cn1-background-type: cn1-pill-border; padding: 1.1mm 2mm 1.1mm 2mm; } +/* iOS segmented control. Button.setToggle(true) rewrites the UIID to + ToggleButton, and no theme in the tree defined it, so a toggle rendered as + bare unshaped text. + + The control reads as a track of capsules: an unselected segment is the plain + glass fill with a black label, and the selected one is the accent capsule + with a white label. Selection therefore changes the fill, not only the label + colour, which is what keeps it legible against the glass. */ +ToggleButton { + color: #1c1c1e; + background-color: rgba(199,197,198,0.80); + cn1-background-type: cn1-pill-border; + padding: 1.1mm 2.6mm 1.1mm 2.6mm; + margin: 1mm 0.4mm 1mm 0.4mm; + font-family: "native:MainLight"; + font-size: 2.7mm; + text-align: center; +} +/* sel# is not the checked look. Button.isPressedStyle() sends a checked toggle + to press# when it has no focus and to sel# when it does, and an unchecked + toggle with focus lands on sel# as well -- so that one slot means "focused", + for either value, and cannot also say which value. + + Neither assignment is lossless, and the choice here is deliberate: + + sel# = the checked fill -> a focused UNCHECKED control looks checked. + Arrowing through a radio group makes every + control you land on claim to be selected. + sel# = base + focus ring -> a focused CHECKED control looks unchecked, + for as long as it holds focus. + + The second is kept because it never asserts a value the control does not + have; the first invents one. It is a framework limit rather than a theme + bug -- one style slot for two dimensions -- and no CSS here can express + both. Until then, do not "fix" this rule by moving the checked fill back + into sel#: that trade has been made once already and it is the worse half. + + dis# has the identical problem and the identical resolution. getStyle() + returns getDisabledStyle() before it consults isPressedStyle(), so a + disabled toggle resolves dis# whether it is checked or not, and a disabled + form cannot show which of its toggles were on. dis# is therefore the muted + unselected appearance, for the same reason: it declines to state a value + rather than stating a wrong one. + + Both cases want the same framework change, which is why neither is patched + here: Component.getStyle() would have to combine state with focus and with + enabled -- a checked control keeping its fill while disabled or focused, + and those conditions drawn some other way -- rather than collapsing three + dimensions onto one style lookup. That reaches every focusable component, + so it is not a line in a theme. */ +ToggleButton.selected { color: #1c1c1e; background-color: rgba(199,197,198,0.80); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; border: 0.3mm solid var(--accent-color, #007aff); } +ToggleButton.pressed { color: var(--accent-on-color, #ffffff); background-color: var(--accent-color, #007aff); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; } +ToggleButton.disabled { color: #b0b0b5; background-color: rgba(199,197,198,0.55); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; } + +/* ComponentGroup renames the edge controls of a group to First, + Last or Only, and UIManager does not fall back from those to + ToggleButton. iOS sets ComponentGroupBool by default, so a segmented group + here would otherwise lose the styling on everything except its middle + control. Each state is written out because cn1-derive is read at base level + only. */ +ToggleButtonFirst { cn1-derive: ToggleButton; } +ToggleButtonFirst.selected { color: #1c1c1e; background-color: rgba(199,197,198,0.80); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; cn1-derive: ToggleButton; border: 0.3mm solid var(--accent-color, #007aff); } +ToggleButtonFirst.pressed { color: var(--accent-on-color, #ffffff); background-color: var(--accent-color, #007aff); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; } +ToggleButtonFirst.disabled { color: #b0b0b5; background-color: rgba(199,197,198,0.55); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; } +ToggleButtonLast { cn1-derive: ToggleButton; } +ToggleButtonLast.selected { color: #1c1c1e; background-color: rgba(199,197,198,0.80); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; cn1-derive: ToggleButton; border: 0.3mm solid var(--accent-color, #007aff); } +ToggleButtonLast.pressed { color: var(--accent-on-color, #ffffff); background-color: var(--accent-color, #007aff); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; } +ToggleButtonLast.disabled { color: #b0b0b5; background-color: rgba(199,197,198,0.55); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; } +ToggleButtonOnly { cn1-derive: ToggleButton; } +ToggleButtonOnly.selected { color: #1c1c1e; background-color: rgba(199,197,198,0.80); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; cn1-derive: ToggleButton; border: 0.3mm solid var(--accent-color, #007aff); } +ToggleButtonOnly.pressed { color: var(--accent-on-color, #ffffff); background-color: var(--accent-color, #007aff); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; } +ToggleButtonOnly.disabled { color: #b0b0b5; background-color: rgba(199,197,198,0.55); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; } + TextField { color: #000000; /* Grouped-list cell (secondarySystemGroupedBackground): a WHITE rounded cell @@ -537,7 +610,6 @@ SliderFull { padding: 0; margin: 0; } -/* Disabled slider is translucent (faded track, fill and thumb). */ /* paintNativeSlider fills with the style's solid bgColor (alpha/transparency is ignored), so the disabled tones must be SOLID light colours, not rgba() -- an rgba fill rendered as full-saturation blue and read as "not disabled". */ @@ -629,8 +701,6 @@ Title { } MainTitle { cn1-derive: Title; } -/* iOS 26 nav-bar items: a circular translucent-white glass button holding a blue - glyph (no text). cn1-round-border draws the circle; rgba gives the glass fill. */ /* Leading/trailing margins reproduce the native bar-item inset from the screen edge (~2.6mm); flush-to-edge circles were the single biggest Toolbar diff. */ /* Item 5: the toolbar command glyph/text is sized from the command style's font @@ -776,16 +846,10 @@ Tab { font-size: 1.65mm; text-align: center; } -/* iOS 26 bottom tab bar: the selected item sits on its OWN brighter frosted - sub-capsule (a lighter glass pill INSIDE the outer bar) and turns blue -- a - layered "glass on glass" highlight, like the native UITabBar. */ /* iOS 26: the SELECTION capsule is the Liquid Glass indicator (the bright frosted glass blob), sitting on a more translucent bar -- so the glass material/refraction/ specular belongs HERE, not on the outer bar. backdrop-filter makes this the glass hero; the bar (TabsContainer) is dialled down to a plainer translucent surface. */ -/* The selected-tab background is drawn by the single sliding selection capsule - (Tabs.paintSelectionCapsule, tabsSelectionCapsuleBool) so it can tween between - tabs -- so the per-tab selected fill is transparent. */ /* The selected tab paints NO background of its own -- the single sliding glass selection capsule (Tabs.paintSelectionCapsule) is the only selection visual. A cn1-pill-border here painted a SECOND, smaller, brighter pill on top of the @@ -935,14 +999,6 @@ MultiLine4 { padding: 0; } -/* Dialog uses a translucent surface so any backdrop (form bg or - the test harness's diagonal-stripe texture) reads through - - approximates the iOS 26 vibrancy effect within the CEF-free - subset. DialogBody is left fully transparent: it was previously - painting its own opaque shade on top of Dialog and reading as a - visibly different colour. All Dialog sub-UIIDs share the same - translucent surface (or stay transparent so Dialog's surface - shows through). */ /* margin: 0 - CN1's Dialog uses its own UIID margin as a positioning hint; a non-zero value here makes Dialog.show() lay the dialog out at screen-minus-margin (looks correct for a small modal but for @@ -980,7 +1036,6 @@ DialogCenteredTitle { cn1-derive: Container; background-color: transparent; padd DialogContentPane { background-color: transparent; padding: 2mm; margin: 0; } /* Action row: a hairline separator above the buttons, like the native alert. */ DialogCommandArea { background-color: transparent; padding: 0; border-top: 0.2mm solid #c6c6c8; } -/* Alert actions are blue, ~17pt, centred and split the row. */ /* Defined standalone (NOT cn1-derive: Button) so the Button pill background / cn1-pill-border can never leak in and turn the flush alert actions into pills. */ DialogButton { cn1-background-type: cn1-none; color: var(--accent-color, #007aff); background-color: transparent; font-family: "native:MainRegular"; font-size: 2.9mm; padding: 2mm; margin: 0; border-radius: 0; text-align: center; } @@ -1090,6 +1145,48 @@ PhoneVerificationLink { padding: 1.5mm; } +/* --------------------------------------------------------------------------- + * UIIDs the framework assigns that no theme defined. + * + * Same enumeration as the Material theme: every setUIID("...") literal in + * CodenameOne/src that appeared in neither theme, so the control had no style + * and fell through to the Component default. The retired components + * (RSSReader, HTMLComponent, HTMLTable, the feature-phone soft keys and + * VKBButton) are left undefined deliberately. + * ------------------------------------------------------------------------- */ + +/* TextComponent: wrapper, floating hint, error line. */ +FloatingHint { cn1-derive: SecondaryLabel; color: #6c6c70; font-size: 2mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } +FloatingHint.disabled { padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; font-family: "native:MainRegular"; font-size: 2mm; color: #c6c6c8; background-color: transparent; } +/* systemRed, the colour iOS uses for validation failures. */ +ErrorLabel { cn1-derive: SecondaryLabel; color: #ff3b30; font-size: 2mm; padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; background-color: transparent; } +ErrorLabel.disabled { padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; font-family: "native:MainRegular"; font-size: 2mm; color: #c6c6c8; background-color: transparent; } + +TableCell { cn1-derive: Label; color: #1c1c1e; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } +TableHeader { cn1-derive: Label; color: #6c6c70; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + +/* Every UIID above is completed to all four states below. A missing state is + not inherited: cn1-derive is read at base level only, and a lookup of + .press# that finds nothing resolves against the global default style, + so the control drops its whole appearance for the duration of that state. + Where a state is meant to differ it is declared above and left alone here. */ +ErrorLabel.selected { font-family: "native:MainRegular"; color: #ff3b30; font-size: 2mm; padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; background-color: transparent; } +ErrorLabel.pressed { font-family: "native:MainRegular"; color: #ff3b30; font-size: 2mm; padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; background-color: transparent; } +FloatingHint.selected { font-family: "native:MainRegular"; color: #6c6c70; font-size: 2mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } +FloatingHint.pressed { font-family: "native:MainRegular"; color: #6c6c70; font-size: 2mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } +TableCell.selected { margin: 0; font-family: "native:MainRegular"; color: #1c1c1e; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } +TableCell.pressed { margin: 0; font-family: "native:MainRegular"; color: #1c1c1e; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } +TableCell.disabled { margin: 0; font-family: "native:MainRegular"; color: #1c1c1e; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } +TableHeader.selected { margin: 0; color: #6c6c70; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } +TableHeader.pressed { margin: 0; color: #6c6c70; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } +TableHeader.disabled { margin: 0; color: #6c6c70; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + +/* Every UIID above is completed to all four states below. A missing state is + not inherited: cn1-derive is read at base level only, and a lookup of + .press# that finds nothing resolves against the global default style, + so the control drops its whole appearance for the duration of that state. + Where a state is meant to differ it is declared above and left alone here. */ + @media (prefers-color-scheme: dark) { Component { color: #ffffff; background-color: #000000; } Form { background-color: #1c1c1e; padding: 0; margin: 0; } @@ -1111,6 +1208,7 @@ PhoneVerificationLink { Button.disabled { color: #636366; background-color: rgba(30,30,32,0.74); cn1-background-type: cn1-pill-border; } /* Gray button (.gray): white label on a slightly lighter dark glass pill. */ FlatButton { color: #ffffff; background-color: rgba(134,127,122,0.74); cn1-background-type: cn1-pill-border; border: 0.1mm solid rgba(255,255,255,0.58); cn1-stroke-gradient: #8e8e93; cn1-stroke-gradient-angle: 135deg; padding: 1.1mm 2mm 1.1mm 2mm; } + FlatButton.pressed { color: #ffffff; background-color: rgba(150,143,138,0.78); cn1-background-type: cn1-pill-border; border: 0.1mm solid rgba(255,255,255,0.58); cn1-stroke-gradient: #8e8e93; cn1-stroke-gradient-angle: 135deg; padding: 1.1mm 2mm 1.1mm 2mm; } /* Prominent filled button: white label on the OPAQUE dark accent pill. */ @@ -1295,4 +1393,41 @@ PhoneVerificationLink { PhoneVerificationText { color: #ebebf5; } PhoneVerificationError { color: #ff453a; } PhoneVerificationLink { color: var(--accent-color-dark, #0a84ff); } + + /* Dark counterparts for every rule above that names a colour. A light-only + state rule is not skipped in dark mode -- with no $Dark entry the light + one is what resolves -- so a hard-coded light foreground would be used + against the dark surface. */ + ErrorLabel { cn1-derive: SecondaryLabel; color: #ff453a; font-size: 2mm; padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; background-color: transparent; } + ErrorLabel.selected { font-family: "native:MainRegular"; color: #ff453a; font-size: 2mm; padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; background-color: transparent; } + ErrorLabel.pressed { font-family: "native:MainRegular"; color: #ff453a; font-size: 2mm; padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; background-color: transparent; } + ErrorLabel.disabled { font-size: 2mm; padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; color: #636366; background-color: transparent; } + FloatingHint { cn1-derive: SecondaryLabel; color: #8e8e93; font-size: 2mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } + FloatingHint.selected { font-family: "native:MainRegular"; color: #8e8e93; font-size: 2mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } + FloatingHint.pressed { font-family: "native:MainRegular"; color: #8e8e93; font-size: 2mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } + FloatingHint.disabled { font-size: 2mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; color: #636366; background-color: transparent; } + TableCell { cn1-derive: Label; color: #ffffff; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + TableCell.selected { margin: 0; font-family: "native:MainRegular"; color: #ffffff; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + TableCell.pressed { margin: 0; font-family: "native:MainRegular"; color: #ffffff; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + TableCell.disabled { margin: 0; font-family: "native:MainRegular"; color: #ffffff; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + TableHeader { cn1-derive: Label; color: #8e8e93; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + TableHeader.selected { margin: 0; color: #8e8e93; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + TableHeader.pressed { margin: 0; color: #8e8e93; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + TableHeader.disabled { margin: 0; color: #8e8e93; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + ToggleButton { color: #ffffff; background-color: rgba(134,127,122,0.74); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; } + ToggleButton.selected { color: #ffffff; background-color: rgba(134,127,122,0.74); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; border: 0.3mm solid var(--accent-color-dark, #0a84ff); } + ToggleButton.pressed { color: var(--accent-on-color, #ffffff); background-color: var(--accent-color-dark, #0a84ff); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; } + ToggleButton.disabled { color: #48484a; background-color: rgba(58,58,60,0.55); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; } + ToggleButtonFirst { color: #ffffff; background-color: rgba(134,127,122,0.74); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; } + ToggleButtonLast { color: #ffffff; background-color: rgba(134,127,122,0.74); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; } + ToggleButtonOnly { color: #ffffff; background-color: rgba(134,127,122,0.74); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; } + ToggleButtonFirst.selected { color: #ffffff; background-color: rgba(134,127,122,0.74); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; border: 0.3mm solid var(--accent-color-dark, #0a84ff); } + ToggleButtonFirst.pressed { color: var(--accent-on-color, #ffffff); background-color: var(--accent-color-dark, #0a84ff); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; } + ToggleButtonFirst.disabled { color: #48484a; background-color: rgba(58,58,60,0.55); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; } + ToggleButtonLast.selected { color: #ffffff; background-color: rgba(134,127,122,0.74); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; border: 0.3mm solid var(--accent-color-dark, #0a84ff); } + ToggleButtonLast.pressed { color: var(--accent-on-color, #ffffff); background-color: var(--accent-color-dark, #0a84ff); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; } + ToggleButtonLast.disabled { color: #48484a; background-color: rgba(58,58,60,0.55); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; } + ToggleButtonOnly.selected { color: #ffffff; background-color: rgba(134,127,122,0.74); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; border: 0.3mm solid var(--accent-color-dark, #0a84ff); } + ToggleButtonOnly.pressed { color: var(--accent-on-color, #ffffff); background-color: var(--accent-color-dark, #0a84ff); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; } + ToggleButtonOnly.disabled { color: #48484a; background-color: rgba(58,58,60,0.55); cn1-background-type: cn1-pill-border; padding: 1.1mm 2.6mm 1.1mm 2.6mm; margin: 1mm 0.4mm 1mm 0.4mm; font-family: "native:MainLight"; font-size: 2.7mm; text-align: center; } } diff --git a/scripts/build-native-themes.sh b/scripts/build-native-themes.sh index b16684ac73e..c7a53cc949e 100755 --- a/scripts/build-native-themes.sh +++ b/scripts/build-native-themes.sh @@ -48,6 +48,12 @@ locate_jar() { # Fall back to parent pom version if the module inherits it. version="$(grep -m1 '' "$REPO_ROOT/maven/pom.xml" | sed -E 's#.*([^<]+).*#\1#')" fi + # A jar in ~/.m2 was built by some other checkout at some other commit, and + # nothing here can tell which. Compiling with it re-emits the WHOLE theme the + # way that build did, not just the rules you edited, so the committed .res + # picks up differences no CSS diff can explain -- which is how a theme change + # once moved screens that use none of its UIIDs. CI has no ~/.m2 and always + # builds from source, so target/ above is the jar that matches the tree. installed_jar="$HOME/.m2/repository/com/codenameone/codenameone-css-compiler/$version/codenameone-css-compiler-${version}-jar-with-dependencies.jar" if [ -f "$installed_jar" ]; then echo "$installed_jar" @@ -60,6 +66,13 @@ ensure_jar() { local jar if jar="$(locate_jar)"; then log "Using CSS compiler jar: $jar" + case "$jar" in + "$HOME"/.m2/*) + log "WARNING: that jar comes from ~/.m2 and was built by another checkout." + log "WARNING: it re-emits the whole theme its own way. Build the module first" + log "WARNING: (mvn -f maven/css-compiler/pom.xml package) to compile with this tree." + ;; + esac printf '%s\n' "$jar" return fi diff --git a/scripts/macos/screenshots/ValidatorLightweightPicker.png b/scripts/macos/screenshots/ValidatorLightweightPicker.png index dbb661e5174..21ec56b57d1 100644 Binary files a/scripts/macos/screenshots/ValidatorLightweightPicker.png and b/scripts/macos/screenshots/ValidatorLightweightPicker.png differ