All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 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