com.github.czyzby.lml.util.LmlUtilities Maven / Gradle / Ivy
package com.github.czyzby.lml.util;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Cell;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.scenes.scene2d.ui.Tree;
import com.badlogic.gdx.scenes.scene2d.ui.Value;
import com.badlogic.gdx.utils.ObjectMap;
import com.badlogic.gdx.utils.ObjectMap.Entry;
import com.badlogic.gdx.utils.ObjectSet;
import com.github.czyzby.kiwi.util.common.Exceptions;
import com.github.czyzby.kiwi.util.common.Nullables;
import com.github.czyzby.kiwi.util.common.Strings;
import com.github.czyzby.kiwi.util.gdx.collection.GdxMaps;
import com.github.czyzby.kiwi.util.gdx.collection.pooled.PooledList;
import com.github.czyzby.kiwi.util.gdx.scene2d.Alignment;
import com.github.czyzby.lml.parser.LmlParser;
import com.github.czyzby.lml.parser.LmlSyntax;
import com.github.czyzby.lml.parser.action.ActionContainer;
import com.github.czyzby.lml.parser.action.ActorConsumer;
import com.github.czyzby.lml.parser.action.StageAttacher;
import com.github.czyzby.lml.parser.tag.LmlAttribute;
import com.github.czyzby.lml.parser.tag.LmlTag;
import com.github.czyzby.lml.util.collection.IgnoreCaseStringMap;
/** Utility class. Contains common LML methods that might be useful during parsing or even LML actors usage.
*
* @author MJ */
public class LmlUtilities {
private static final ObjectMap STATIC_TABLE_VALUES;
static {
final ObjectMap initialValues = GdxMaps.newObjectMap("minHeight", Value.minHeight, "prefHeight",
Value.prefHeight, "maxHeight", Value.maxHeight, "minWidth", Value.minWidth, "prefWidth",
Value.prefWidth, "maxWidth", Value.maxWidth);
STATIC_TABLE_VALUES = new IgnoreCaseStringMap(initialValues);
}
/** Empty object array. Might be useful for method invocations with no arguments. */
public static final Object[] EMPTY_ARRAY = new Object[0];
private LmlUtilities() {
}
/** Registers the given {@link Value} under passed name. These values should be generic and work only on any actors,
* without parsed attributes. Where a Value object is expected (for example - in {@link Table} cell sizes), you can
* pass chosen valueName as the attribute and the corresponding Value implementation will be taken from a map.
*
* @param valueName will register value under this key.
* @param value returned each time a value-requiring attribute contains the passed name. */
public static void registerStaticTableValue(final String valueName, final Value value) {
STATIC_TABLE_VALUES.put(valueName, value);
}
/** @param fromValue ends with a marker that should be removed.
* @return passed value without last character. */
public static String stripEnding(final String fromValue) {
return fromValue.substring(0, fromValue.length() - 1);
}
/** @param fromValue ends with a marker that should be removed.
* @param ifEqualToThisMarker expected, optional ending marker.
* @return passed value without last character if it ends with the passed marker or the original value if it does
* not. */
public static String stripEnding(final String fromValue, final char ifEqualToThisMarker) {
if (Strings.endsWith(fromValue, ifEqualToThisMarker)) {
return fromValue.substring(0, fromValue.length() - 1);
}
return fromValue;
}
/** @param fromValue starts with a marker character that should be removed.
* @return passed value without the marker. */
public static String stripMarker(final String fromValue) {
return fromValue.substring(1);
}
/** @param fromValue may or may not start with a marker character that should be removed.
* @param ifEqualToThisMarker expected, optional marker.
* @return value without the initial marker. */
public static String stripMarker(final String fromValue, final char ifEqualToThisMarker) {
if (ifEqualToThisMarker == fromValue.charAt(0)) {
return stripMarker(fromValue);
}
return fromValue;
}
/** @param fromValue if it starts and ends with single or double quotation, quotation characters will be stripped.
* @return passed value without quotation, if quotation was present. */
public static String stripQuotation(final String fromValue) {
if (fromValue == null || fromValue.length() < 2) {
// If string is empty or not long enough to contain quotation, returning unchanged.
return fromValue;
}
final int lastIndex = Strings.getLastIndex(fromValue); // length - 1
if (fromValue.charAt(0) == '\'' && fromValue.charAt(lastIndex) == '\''
|| fromValue.charAt(0) == '"' && fromValue.charAt(lastIndex) == '"') {
return fromValue.substring(1, lastIndex);
}
return fromValue;
}
/** @param actor will have its ID attached using actor internal methods: ID will become actor's name.
* @param id will become actor's ID. */
public static void setActorId(final Actor actor, final String id) {
actor.setName(id);
}
/** @param actor might have an ID attached using name setter.
* @return actor's ID or null. */
public static String getActorId(final Actor actor) {
return actor.getName();
}
/** @param group will be recursively searched. Does not require loops, but in a properly structured Scene, they
* should never appear. This is a relatively expensive operations big views and other means of getting
* references to actors are preferred.
* @param actorId ID of the actor to find.
* @return instance of the actor with the selected ID (ignoring case) or null if not found. */
public static Actor getActorWithId(final Group group, final String actorId) {
final PooledList groupsToSearch = new PooledList();
groupsToSearch.add(group);
while (!groupsToSearch.isEmpty()) {
for (final Actor actor : groupsToSearch.removeFirst().getChildren()) {
if (actorId.equalsIgnoreCase(actor.getName())) {
return actor;
} else if (actor instanceof Group) {
groupsToSearch.add((Group) actor);
}
}
}
return null;
}
/** @param actors clears attached {@link LmlUserObject}s with LML meta-data if any of the actors has one. */
public static void clearLmlUserObjects(final Iterable actors) {
for (final Actor actor : actors) {
clearLmlUserObject(actor);
}
}
/** @param actor clears attached {@link LmlUserObject} with LML meta-data if it has one. If the actor is a
* {@link Group}, it will be searched - all its children will have the {@link LmlUserObject}s cleared
* recursively. */
public static void clearLmlUserObject(final Actor actor) {
if (actor.getUserObject() instanceof LmlUserObject) {
actor.setUserObject(null);
}
if (actor instanceof Group) {
final PooledList groupsToSearch = new PooledList();
groupsToSearch.add((Group) actor);
while (!groupsToSearch.isEmpty()) {
for (final Actor child : groupsToSearch.removeFirst().getChildren()) {
if (child.getUserObject() instanceof LmlUserObject) {
child.setUserObject(null);
}
if (child instanceof Group) {
groupsToSearch.add((Group) child);
}
}
}
}
}
/** @param actor might be a tree node.
* @return tree node containing the actor if it is a tree node or null. */
public static Tree.Node getTreeNode(final Actor actor) {
final LmlUserObject userObject = getOptionalLmlUserObject(actor);
if (userObject != null) {
return userObject.getNode();
}
return null;
}
/** @param actor might be a text-based widget with a multiline property set to true.
* @return false if actor has no user object attached or the multiline property is set to false. */
public static boolean isMultiline(final Actor actor) {
final LmlUserObject userObject = getOptionalLmlUserObject(actor);
if (userObject != null && userObject.getData() instanceof Boolean) {
return (Boolean) userObject.getData();
}
return false;
}
/** @param actor might have a LmlUserObject attached.
* @return previous LmlUserObject instance attached to the actor or a new instance, which will also be set as
* actor's user object. As opposed to {@link #getOptionalLmlUserObject(Actor)}, this forces the actor to
* have a LmlUserObject attached.
* @see Actor#setUserObject(Object) */
public static LmlUserObject getLmlUserObject(final Actor actor) {
LmlUserObject userObject;
if (actor.getUserObject() instanceof LmlUserObject) {
userObject = (LmlUserObject) actor.getUserObject();
} else {
userObject = new LmlUserObject();
actor.setUserObject(userObject);
}
return userObject;
}
/** @param actor might have a LmlUserObject attached.
* @return LmlUserObject instance attached to the actor or null. */
public static LmlUserObject getOptionalLmlUserObject(final Actor actor) {
if (actor.getUserObject() instanceof LmlUserObject) {
return (LmlUserObject) actor.getUserObject();
}
return null;
}
/** @param stage should contain the actors.
* @param actors will be added to the stage, honoring their {@link StageAttacher} settings. */
public static void appendActorsToStage(final Stage stage, final Iterable actors) {
if (actors == null) {
return;
} else if (stage == null) {
throw new LmlParsingException(
"Cannot append actors: the stage is null. Are you sure the correct stage has been passed? If this method was invoked by a listener, are you sure that the actor is properly added to a stage?");
}
for (final Actor actor : actors) {
stage.addActor(actor);
final StageAttacher attacher = getStageAttacher(actor);
if (attacher != null) {
attacher.attachToStage(actor, stage);
}
}
}
/** @param actor might have a stage attacher set using its internal "user object" mechanism.
* @return actor's stage attacher or null if not set. */
public static StageAttacher getStageAttacher(final Actor actor) {
final LmlUserObject userObject = getOptionalLmlUserObject(actor);
return userObject == null ? null : userObject.getStageAttacher();
}
/** @param actor will have a stage attacher set using actor's internal "user object" mechanism.
* @param stageAttacher used to attach the actor to a stage. */
public static void setStageAttacher(final Actor actor, final StageAttacher stageAttacher) {
final LmlUserObject userObject = getLmlUserObject(actor);
userObject.setStageAttacher(stageAttacher);
}
/** @param actor might be in a cell. Should be added to a cell if its parent is a Table.
* @param parent direct parent tag of the actor.
* @return actor's cell or null if not in a table. */
public static Cell> getCell(final Actor actor, final LmlTag parent) {
final LmlUserObject userObject = getLmlUserObject(actor);
if (userObject.getCell() == null) {
if (parent != null && parent.getActor() instanceof Table) {
return getCell(actor, (Table) parent.getActor());
}
return null;
}
return userObject.getCell();
}
/** @param actor might be in the passed table.
* @param table if actor is currently not in a table, he will be added to this table.
* @return a cell containing the actor. Returns null if the passed ta */
public static Cell> getCell(final Actor actor, final Table table) {
final LmlUserObject userObject = getLmlUserObject(actor);
if (userObject.getCell() == null) {
if (table == null) {
throw new IllegalArgumentException("Table cannot be null. Unable to add actor to cell.");
}
userObject.setCell(userObject.getTableTarget().add(table, actor));
final Table target = userObject.getTableTarget().extract(table);
if (isOneColumn(target)) {
target.row();
}
}
return userObject.getCell();
}
/** @param table might be set as one column table.
* @return true if the table is set as one column using user object mechanism */
public static boolean isOneColumn(final Table table) {
final LmlUserObject userObject = getOptionalLmlUserObject(table);
if (userObject != null && userObject.getData() instanceof Boolean) {
return ((Boolean) userObject.getData()).booleanValue();
}
return false;
}
/** @param parser parses an LML template.
* @param actor needs an alignment.
* @param rawAttributeData raw data to parse.
* @return alignment value from enum constant with the selected name. If invalid name is passed, strict parser will
* throw an exception; non-strict parsers return the default, centered alignment.
* @see Alignment */
public static int parseAlignment(final LmlParser parser, final Actor actor, final String rawAttributeData) {
final String parsedData = parser.parseString(rawAttributeData, actor);
if (Strings.isInt(parsedData)) {
// Attribute was an int. Someone might be trying to set alignment directly; it's OK.
return parseAlignmentFromInt(parser, Integer.parseInt(parsedData));
}
try {
final Alignment alignment = Alignment.valueOf(parsedData.toUpperCase());
if (alignment != null) {
return alignment.getAlignment();
}
} catch (final Exception exception) {
Exceptions.ignore(exception); // Somewhat expected if invalid name is passed.
}
parser.throwErrorIfStrict("Unable to parser alignment from raw data: " + rawAttributeData
+ "; no alignment value matching: " + parsedData);
return Alignment.CENTER.getAlignment();
}
private static int parseAlignmentFromInt(final LmlParser parser, final int parsedData) {
if (parser.isStrict()) {
// Validating passed int value. If it is a correct alignment, there will be an enum constant present.
final Alignment alignment = Alignment.get(parsedData);
if (alignment != null) {
return alignment.getAlignment();
}
parser.throwError("Numeric alignment setting passed: " + parsedData
+ ", but there is no alignment with this exact value.");
}
// Parser not strict: returning the value, regardless of its validity:
return parsedData;
}
/** @param parser parses an LML template.
* @param parent contains the actor.
* @param actor needs a {@link Value} for vertical setting.
* @param rawAttributeData raw data to parse. If is a simple float, fixed value will be returned. If starts with
* '%', will use {@link Value#percentHeight(float)} - will return a per cent of parsed actor's height. If
* ends with '%', will use {@link Value#percentHeight(float, Actor)} - will return a per cent of
* {@link Table} parent width. If a string value, will look for static values registered with
* {@link #registerStaticTableValue(String, Value)} (there are some default values; see this class
* sources for more info).
* @return LibGDX Scene2D float value provider parsed from the raw attribute. */
public static Value parseVerticalValue(final LmlParser parser, final LmlTag parent, final Actor actor,
final String rawAttributeData) {
final Object valueToParse = determineValueObjectName(parser, actor, rawAttributeData);
if (valueToParse instanceof Value) {
return (Value) valueToParse;
}
return determineVerticalValue(parser, parent, actor, Nullables.toString(valueToParse));
}
private static Value determineVerticalValue(final LmlParser parser, final LmlTag parent, final Actor actor,
final String valueToParse) {
if (Strings.isFloat(valueToParse)) {
return new Value.Fixed(parser.parseFloat(valueToParse, actor));
} else if (Strings.endsWith(valueToParse, '%')) {
return Value.percentHeight(parser.parseFloat(stripEnding(valueToParse), actor),
getParentActorForValueParsing(parent, actor));
} else if (Strings.startsWith(valueToParse, '%')) {
return Value.percentHeight(parser.parseFloat(stripMarker(valueToParse), actor));
}
if (!STATIC_TABLE_VALUES.containsKey(valueToParse)) {
parser.throwError(
"Unable to determine Value object. Value has to be a float, float beginning or ending with '%' or static value reference. See LmlUtilities#parseVerticalValue(LmlParser, LmlTag, Actor, String). Received: "
+ valueToParse);
}
return STATIC_TABLE_VALUES.get(valueToParse);
}
/** @param parent contains the actor. Might be a Table with multiple nested tables.
* @param actor needs a {@link Value}.
* @return direct parent widget of the passed actor. */
private static Actor getParentActorForValueParsing(final LmlTag parent, final Actor actor) {
if (parent == null || parent.getActor() == actor) {
// The value is parsed for the Table or Container itself.
return actor;
}
return parent.getActor() instanceof Table ? getCell(actor, parent).getTable() : parent.getActor();
}
/** @param parser parses an LML template.
* @param parent contains the actor.
* @param actor needs a {@link Value} for horizontal setting.
* @param rawAttributeData raw data to parse. If is a simple float, fixed value will be returned. If starts with
* '%', will use {@link Value#percentWidth(float)} - will return a per cent of parsed actor's width. If
* ends with '%', will use {@link Value#percentWidth(float, Actor)} - will return a per cent of
* {@link Table} parent width. If a string value, will look for static values registered with
* {@link #registerStaticTableValue(String, Value)} (there are some default values; see this class
* sources for more info).
* @return LibGDX Scene2D float value provider parsed from the raw attribute. */
public static Value parseHorizontalValue(final LmlParser parser, final LmlTag parent, final Actor actor,
final String rawAttributeData) {
final Object valueToParse = determineValueObjectName(parser, actor, rawAttributeData);
if (valueToParse instanceof Value) {
return (Value) valueToParse;
}
return determineHorizontalValue(parser, parent, actor, Nullables.toString(valueToParse));
}
private static Object determineValueObjectName(final LmlParser parser, final Actor actor,
final String rawAttributeData) {
if (Strings.startsWith(rawAttributeData, parser.getSyntax().getMethodInvocationMarker())) {
final ActorConsumer, Actor> action = parser.parseAction(rawAttributeData, actor);
if (action == null) {
parser.throwErrorIfStrict("Cannot determine Value object with invalid action ID: " + rawAttributeData
+ " for actor: " + actor);
}
return action.consume(actor);
}
return parser.parseString(rawAttributeData, actor);
}
private static Value determineHorizontalValue(final LmlParser parser, final LmlTag parent, final Actor actor,
final String valueToParse) {
if (Strings.isFloat(valueToParse)) {
return new Value.Fixed(parser.parseFloat(valueToParse, actor));
} else if (Strings.endsWith(valueToParse, '%')) {
return Value.percentWidth(parser.parseFloat(stripEnding(valueToParse), actor),
getParentActorForValueParsing(parent, actor));
} else if (Strings.startsWith(valueToParse, '%')) {
return Value.percentWidth(parser.parseFloat(stripMarker(valueToParse), actor));
}
if (!STATIC_TABLE_VALUES.containsKey(valueToParse)) {
parser.throwError(
"Unable to determine Value object. Value has to be a float, float beginning or ending with '%' or static value reference. See LmlUtilities#parseHorizontalValue(LmlParser, LmlTag, Actor, String). Received: "
+ valueToParse);
}
return STATIC_TABLE_VALUES.get(valueToParse);
}
/** Utility method that processes all named attributes of the selected type.
*
* @param widget widget (validator, actor - processed element) that will be used to process attributes.
* @param tag contains attributes.
* @param parser will be used to parse attributes.
* @param processedAttributes already processed attribute names. These attributes will be ignored. Optional, can be
* null.
* @param throwExceptionIfAttributeUnknown if true and unknown attribute is found, a strict parser will throw an
* exception.
* @param type of processed widget. Its class tree will be used to retrieve attributes. */
public static void processAttributes(final Type widget, final LmlTag tag, final LmlParser parser,
final ObjectSet processedAttributes, final boolean throwExceptionIfAttributeUnknown) {
if (GdxMaps.isEmpty(tag.getNamedAttributes())) {
return;
}
final LmlSyntax syntax = parser.getSyntax();
final boolean hasProcessedAttributes = processedAttributes != null;
for (final Entry attribute : tag.getNamedAttributes()) {
if (attribute == null || hasProcessedAttributes && processedAttributes.contains(attribute.key)) {
continue;
}
final LmlAttribute attributeProcessor = syntax.getAttributeProcessor(widget, attribute.key);
if (attributeProcessor == null) {
if (throwExceptionIfAttributeUnknown) {
parser.throwErrorIfStrict("Unknown attribute: \"" + attribute.key + "\" for widget type: "
+ widget.getClass().getName());
}
continue;
}
attributeProcessor.process(parser, tag, widget, attribute.value);
if (hasProcessedAttributes) {
processedAttributes.add(attribute.key);
}
}
}
// Syntax helpers:
/** Warning: uses default LML syntax. Will not work if you modified any LML markers.
*
* @param array will be converted to an LML array argument using default syntax.
* @return unparsed LML array. */
public static String toArrayArgument(final Object... array) {
return Strings.join(";", array);
}
/** Warning: uses default LML syntax. Will not work if you modified any LML markers.
*
* @param iterable will be converted to an LML array argument using default syntax.
* @return unparsed LML array. */
public static String toArrayArgument(final Iterable> iterable) {
return Strings.join(";", iterable);
}
/** Warning: uses default LML syntax. Will not work if you modified any LML markers.
*
* @param base base of the range. Can be null - range will not have a base and will iterate solely over numbers.
* @param rangeStart start of range. Can be negative. Does not have to be lower than end - if start is bigger, range
* is iterating from bigger to lower values.
* @param rangeEnd end of range. Can be negative.
* @return range is format: base + rangeOpening + start + separator + end + rangeClosing. For example,
* "base[4,2]". */
public static String toRangeArrayArgument(final Object base, final int rangeStart, final int rangeEnd) {
return Nullables.toString(base, Strings.EMPTY_STRING) + '[' + rangeStart + ',' + rangeEnd + ']';
}
/** Warning: uses default LML syntax. Will not work if you modified any LML markers.
*
* @param bundleLineId name of a bundle line in default i18n bundle.
* @return converted LML bundle line argument using default syntax. */
public static String toBundleLine(final String bundleLineId) {
return '@' + bundleLineId;
}
/** Warning: uses default LML syntax. Will not work if you modified any LML markers.
*
* @param bundleId name of the bundle, as registered to LML data container.
* @param bundleLineId name of a bundle line in the specified i18n bundle.
* @return converted LML bundle line argument using default syntax. */
public static String toBundleLine(final String bundleId, final String bundleLineId) {
return '@' + bundleId + '.' + bundleLineId;
}
/** Warning: uses default LML syntax. Will not work if you modified any LML markers.
*
* @param preferenceId name of a preference in default preferences object.
* @return converted LML preference argument using default syntax. */
public static String toPreference(final String preferenceId) {
return '#' + preferenceId;
}
/** Warning: uses default LML syntax. Will not work if you modified any LML markers.
*
* @param preferencesId name of the preferences object, as registered to LML data container.
* @param preferenceId name of a preference in the specified i18n bundle.
* @return converted LML preference argument using default syntax. */
public static String toPreference(final String preferencesId, final String preferenceId) {
return '#' + preferencesId + '.' + preferenceId;
}
/** Warning: uses default LML syntax. Will not work if you modified any LML markers.
*
* @param methodId name of a registered {@link ActorConsumer} or method name of a registered {@link ActionContainer}
* .
* @return converted LML action argument using default syntax. */
public static String toAction(final String methodId) {
return '$' + methodId;
}
/** Warning: uses default LML syntax. Will not work if you modified any LML markers.
*
* @param actionContainerId name of an ActionContainer, as registered to LML data container.
* @param methodId name of a method of an {@link ActionContainer} with the specified ID.
* @return converted LML action argument using default syntax. */
public static String toAction(final String actionContainerId, final String methodId) {
return '$' + actionContainerId + '.' + methodId;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy