package com.github.czyzby.lml.parser.impl.tag.macro; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.Group; import com.badlogic.gdx.scenes.scene2d.ui.Label; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.scenes.scene2d.ui.Tree; import com.badlogic.gdx.scenes.scene2d.utils.Layout; import com.badlogic.gdx.utils.Array; import com.github.czyzby.kiwi.util.common.Strings; import com.github.czyzby.kiwi.util.gdx.collection.GdxArrays; import com.github.czyzby.kiwi.util.gdx.collection.GdxMaps; import com.github.czyzby.lml.parser.LmlParser; import com.github.czyzby.lml.parser.action.ActorConsumer; import com.github.czyzby.lml.parser.impl.tag.AbstractActorLmlTag; import com.github.czyzby.lml.parser.impl.tag.AbstractMacroLmlTag; import com.github.czyzby.lml.parser.tag.LmlActorBuilder; import com.github.czyzby.lml.parser.tag.LmlTag; import com.github.czyzby.lml.parser.tag.LmlTagProvider; import com.github.czyzby.lml.util.LmlUtilities; /** Allows to register new tags from within LML templates. Normally, you have to implement {@code LmlTag} interface * (there are good abstracts for that - {@link AbstractMacroLmlTag} and {@link AbstractActorLmlTag}, but still) and add * a {@link LmlTagProvider} to syntax object - all in Java. This macro allows you to use LML templates to register new * tags; this is much less flexible solution, but a lot quicker, if you want to override a method or two in the original * class. For example, let's say you want a Table that does some extra work in the constructor: * * <blockquote> * * <pre> * public Table getMyTable() { * return new Table() { * { * columnDefaults(1).pad(4f); * } * }; * } * </pre> * * </blockquote>If you want to use this specialized Table method as a provider for a new tag, all you have to do is use * this macro: * * <blockquote> * * <pre> * <:newTag myTable getMyTable /> * <!-- Now you can use: --> * <myTable> * <label pad=3>Will be properly added.</label> * </myTable> * </pre> * * </blockquote>The first argument is an array of tag names (in this case: "myTable" tag name). The second is the method * reference that returns the actor instance that you want to assign to the tag names. If the actor extends * {@link Group}, {@link Table} or {@link Tree}, it will append children and (text converted to labels) with the most * appropriate method. Actor's attributes will be properly handled: if the actor extends a Table, for example, it will * be able to parse all Table attributes and its children can have any Cell attributes, as expected. * * <p> * But there are times when you need more data to create the widget, like its style. That's why you can create a method * that consumes {@link LmlActorBuilder}: <blockquote> * * <pre> * public Table getMyTable(LmlActorBuilder builder) { * return new Table(lmlParser.getData().getSkin(builder.getSkinName()) { * { * columnDefaults(1).pad(4f); * } * }; * } * </pre> * * </blockquote>You do need a reference to your LmlParser if you want to have multiple skins support, but this should * not be an issue. If you need a different builder (one with more data and more assigned attributes - be careful * though, as you might need to register some building attributes), you can pass a third macro argument: builder * provider method. * * <blockquote> * * <pre> * public LmlActorBuilder getMyBuilder() { * return new TextLmlActorBuilder(); * } * * public Table getMyTable(LmlActorBuilder builder) { * return new Table(lmlParser.getData().getSkin(builder.getSkinName()) { * { * add(((TextLmlActorBuilder) builder).getText(); * columnDefaults(1).pad(4f); * } * }; * } * * <!-- In template: --> * <:newTag myTable;myAlias getMyTable getMyBuilder /> * </pre> * * </blockquote>Building attributes are mapped to builder types, so if you use one of custom widget builders (like the * text actor builder in the example above), your tag will automatically handle all its attributes. * * <p> * Note that this macro supports named attributes: <blockquote> * * <pre> * <:newTag alias="myTable" method="getMyTable" builder="getMyBuilder" /> * </pre> * * </blockquote> * * @author MJ */ public class NewTagLmlMacroTag extends AbstractMacroLmlTag { /** Alias for the first macro attribute: list of tag aliases. */ public static final String ALIAS_ATTRIBUTE = "alias"; /** Alias for the second macro attribute: name of the method that returns an actor instance and optionally consumes * a {@link LmlActorBuilder}. */ public static final String METHOD_ATTRIBUTE = "method"; /** Alias for the third macro attribute: name of the method that returns a customized {@link LmlActorBuilder}. */ public static final String BUILDER_ATTRIBUTE = "builder"; public NewTagLmlMacroTag(final LmlParser parser, final LmlTag parentTag, final StringBuilder rawTagData) { super(parser, parentTag, rawTagData); } @Override public void handleDataBetweenTags(final CharSequence rawData) { if (Strings.isNotWhitespace(rawData)) { getParser().throwErrorIfStrict("New tag macro cannot parse content between tags."); } } @Override public void closeTag() { final Array<String> attributes = getAttributes(); if (GdxArrays.sizeOf(attributes) < 2) { getParser().throwError( "Cannot register a new tag without two attributes: tag names array and method ID (consuming LmlActorBuilder, providing Actor)."); } // Possible tag names: final String[] tagNames = getTagNames(); // Creates actual actor: final ActorConsumer<Actor, LmlActorBuilder> creator = getActorCreator(); final ActorConsumer<LmlActorBuilder, ?> builderCreator; if (attributes.size > 2) { // Provides builders: builderCreator = getBuilderCreator(); } else { // Using default builders: builderCreator = null; } if (creator == null) { // No actor creation method - new tag cannot be created. getParser().throwError( "Cannot register a method consuming LmlActorBuilder, providing Actor. Method consuming LmlActorBuilder and returning actor not found for attribute: " + attributes.get(1)); } // Registering provider that will create custom tags for the selected tag names: getParser().getSyntax().addTagProvider(getNewTagProvider(creator, builderCreator), tagNames); } /** @return {@link ActorConsumer} returning {@link LmlActorBuilder}. */ @SuppressWarnings("unchecked") private ActorConsumer<LmlActorBuilder, ?> getBuilderCreator() { if (hasAttribute(BUILDER_ATTRIBUTE)) { return (ActorConsumer<LmlActorBuilder, ?>) getParser().parseAction(getAttribute(BUILDER_ATTRIBUTE), getActor()); } return (ActorConsumer<LmlActorBuilder, ?>) getParser().parseAction(getAttributes().get(2), getActor()); } /** @return {@link ActorConsumer} returning an {@link Actor} and (optionally) consuming {@link LmlActorBuilder}. */ @SuppressWarnings("unchecked") protected ActorConsumer<Actor, LmlActorBuilder> getActorCreator() { if (hasAttribute(METHOD_ATTRIBUTE)) { return (ActorConsumer<Actor, LmlActorBuilder>) getParser().parseAction(getAttribute(METHOD_ATTRIBUTE), new LmlActorBuilder()); } return (ActorConsumer<Actor, LmlActorBuilder>) getParser().parseAction(getAttributes().get(1), new LmlActorBuilder()); } /** @return list of tag aliases. */ protected String[] getTagNames() { if (hasAttribute(ALIAS_ATTRIBUTE)) { return getParser().parseArray(getAttribute(ALIAS_ATTRIBUTE), getActor()); } else if (GdxMaps.isNotEmpty(getNamedAttributes())) { getParser().throwError( "When using named attributes, new tag macro needs at least two attributes: 'method' (name of the method that returns an Actor and optionally consumes LmlActorBuilder) and 'tag' (array of new tag aliases). Found attributes: " + getNamedAttributes()); } return getParser().parseArray(getAttributes().first(), getActor()); } @Override public String[] getExpectedAttributes() { return new String[] { ALIAS_ATTRIBUTE, METHOD_ATTRIBUTE, BUILDER_ATTRIBUTE }; } /** @param creator method that spawns new actors. * @param builderCreator spawns actor builders. Optional. * @return an instance of {@link LmlTagProvider} that provides custom tags. */ protected LmlTagProvider getNewTagProvider(final ActorConsumer<Actor, LmlActorBuilder> creator, final ActorConsumer<LmlActorBuilder, ?> builderCreator) { return new LmlTagProvider() { @Override public LmlTag create(final LmlParser parser, final LmlTag parentTag, final StringBuilder rawTagData) { return new CustomLmlTag(parser, parentTag, rawTagData) { @Override protected Actor getNewInstanceOfActor(final LmlActorBuilder builder) { return creator.consume(builder); // This is an abstract method, because we cannot pass the creator in the constructor. The actor // is created IN the super constructor (so it can be final), before creator is even assigned. } @Override protected LmlActorBuilder getNewInstanceOfBuilder() { if (builderCreator != null) { return builderCreator.consume(null); } return super.getNewInstanceOfBuilder(); } }; } }; } /** Custom tag created with new tag macro. If the returned actor extends {@link Group}, it can be parental: plain * text will be converted to labels and added; regular tags will be added with {@link Group#addActor(Actor)}. If the * actor implements {@link Layout}, it will be packed after its tag is closed. * * @author MJ */ public static abstract class CustomLmlTag extends AbstractActorLmlTag { public CustomLmlTag(final LmlParser parser, final LmlTag parentTag, final StringBuilder rawTagData) { super(parser, parentTag, rawTagData); } @Override protected void handlePlainTextLine(final String plainTextLine) { if (getActor() instanceof Label) { // Labels might be a pretty basic widget that sometimes needs extension, so we want to support its // unique text parsing. appendText((Label) getActor(), plainTextLine); } else if (getActor() instanceof Group) { addChild(toLabel(plainTextLine)); } } /** @param actor casted for convenience. * @param plainTextLine should be appended to label. */ protected void appendText(final Label actor, final String plainTextLine) { final String textToAppend = getParser().parseString(plainTextLine, actor); if (Strings.isEmpty(actor.getText())) { actor.setText(textToAppend); } else { if (LmlUtilities.isMultiline(actor)) { actor.getText().append('\n'); } actor.getText().append(textToAppend); } actor.invalidate(); } /** @param child will be added to the actor casted to a Group or a Table. */ protected void addChild(final Actor child) { final Actor actor = getActor(); if (actor instanceof Tree) { final Tree.Node node = LmlUtilities.getTreeNode(child); if (node != null) { ((Tree) actor).add(node); } else { ((Tree) actor).add(new Tree.Node(child)); } } else if (actor instanceof Table) { LmlUtilities.getCell(child, (Table) actor); } else { ((Group) actor).addActor(child); } } @Override protected void handleValidChild(final LmlTag childTag) { if (getActor() instanceof Group) { addChild(childTag.getActor()); } } } }