Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4fd32a4
Give ToggleButton a style: neither native theme defined it
shaiblah Sep 9, 2026
343700d
Style the rest of the UIIDs no theme defined, and prove which ones to…
shaiblah Sep 10, 2026
8a0a0fe
Reseed the figures the new TableCell and TableHeader styles changed
shaiblah Sep 10, 2026
c54d4f8
Absent from the CSS is not the same as unstyled
shaiblah Sep 10, 2026
0d5050a
Give the derived aliases their states, and the popup panes a dark fill
shaiblah Sep 10, 2026
7db8dce
Complete every state on every UIID this change adds
shaiblah Sep 10, 2026
96a15ee
Complete the dark side of the audit too
shaiblah Sep 10, 2026
ba41ec1
Every rule that names a colour now has a dark counterpart
shaiblah Sep 10, 2026
818615a
Regenerate the states I deleted, and match TODAY to the row it replaces
shaiblah Sep 10, 2026
6d65ade
Expand a derived state from the base's matching state, not its base
shaiblah Sep 10, 2026
0263790
The light-to-dark map was applied sequentially, so a swap pair cancel…
shaiblah Sep 10, 2026
7995898
Bind every foreground over an accent fill to its on-accent token
shaiblah Sep 10, 2026
04abd0a
Narrow the sweep to the UIIDs whose gap is provable
shaiblah Sep 10, 2026
92631e4
An alias needs a dark base of its own
shaiblah Sep 10, 2026
a8fd83e
sel# means focused, not checked, so stop painting it as checked
shaiblah Sep 10, 2026
1f1fb16
Reseed the toggle baselines from CI after the focus-ring change
shaiblah Sep 10, 2026
8791d4f
Record why sel# cannot carry both focus and value
shaiblah Sep 10, 2026
bb639d7
A state rule that names only colours is built from the default, not i…
shaiblah Sep 10, 2026
e9038b1
dis# collapses the same two dimensions that sel# does
shaiblah Sep 10, 2026
5becde8
The disabled hint was mapped to a separator grey, not a text grey
shaiblah Sep 10, 2026
cdf0dd0
setToggle leaves the UIID behind, and sets the wrong one on the way out
shaiblah Sep 10, 2026
ee20edb
Add the copyright header the diff-scoped gate now asks of this test
shaiblah Sep 10, 2026
7637135
The restore guard missed every toggle that sits in a group
shaiblah Sep 10, 2026
819720d
Stop guessing the group's prefix and correct each holder on its own t…
shaiblah Sep 10, 2026
f524d50
While a control is in a group, the live UIID is the group's to set
shaiblah Sep 10, 2026
a47c56b
Make entering toggle mode keep the group's saved name in step too
shaiblah Sep 10, 2026
49946b1
The themes were compiled by a jar from another checkout
shaiblah Sep 10, 2026
5128dcd
Being inside a group is not the same as the group owning your UIID
shaiblah Sep 11, 2026
21d75d6
Ask the group whether it is grouping, and reseed the one golden this …
shaiblah Sep 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 110 additions & 2 deletions CodenameOne/src/com/codename1/ui/Button.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Comment thread
shai-almog marked this conversation as resolved.
}
// 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
Expand Down
13 changes: 13 additions & 0 deletions CodenameOne/src/com/codename1/ui/ComponentGroup.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +195 to +196

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Track actual group UIID ownership

When setForceGroup(true) is called after a toggle has already been added to an inactive group, the setter only flips forceGroup and never calls updateUIIDs(), so the child still owns its live ToggleButton UIID and has no $origUIID; this method nevertheless reports that the group owns it. A subsequent setToggle(false) therefore skips restoring RadioButton/CheckBox, and removing the component leaves the non-toggle control permanently styled as ToggleButton. Fresh evidence beyond the earlier ownership comment is that the final setForceGroup() implementation does not apply the group aliases, so ownership must track an actual rename (or activation must update the members), not just the configuration flag.

Useful? React with 👍 / 👎.

}

private void restoreUIID(Component c) {
String o = (String) c.getClientProperty("$origUIID");
if (o != null) {
Expand Down
Binary file modified Ports/Android/src/AndroidMaterialTheme.res
Binary file not shown.
Binary file modified Ports/iOSPort/nativeSources/iOSModernTheme.res
Binary file not shown.
Binary file modified Themes/AndroidMaterialTheme.res
Binary file not shown.
Binary file modified Themes/iOSModernTheme.res
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
};
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<String> 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);
Expand All @@ -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;
}


}
13 changes: 10 additions & 3 deletions docs/developer-guide/The-Components-Of-Codename-One.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/developer-guide/img/generated/components-table.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/developer-guide/img/generated/csv-parsing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading