package com.github.czyzby.lml.parser.impl.tag;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.Tree;
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.Strings;
import com.github.czyzby.kiwi.util.gdx.collection.GdxMaps;
import com.github.czyzby.kiwi.util.gdx.collection.GdxSets;
import com.github.czyzby.lml.parser.LmlParser;
import com.github.czyzby.lml.parser.LmlSyntax;
import com.github.czyzby.lml.parser.tag.LmlActorBuilder;
import com.github.czyzby.lml.parser.tag.LmlBuildingAttribute;
import com.github.czyzby.lml.parser.tag.LmlTag;
import com.github.czyzby.lml.util.Lml;
import com.github.czyzby.lml.util.LmlUserObject;
import com.github.czyzby.lml.util.LmlUtilities;
/**
* Common base class for all tags that spawn and manage an actor.
* @author MJ
*/
public abstract class AbstractActorLmlTag extends AbstractLmlTag {
private final Actor actor;
public AbstractActorLmlTag(final LmlParser parser, final LmlTag parentTag, final StringBuilder rawTagData) {
super(parser, parentTag, rawTagData);
actor = prepareActor();
}
/**
* Warning: invoked by the constructor.
* @return a fully initiated instance of the actor, with its tag attributes processed.
*/
protected Actor prepareActor() {
final LmlActorBuilder builder = getNewInstanceOfBuilder();
final ObjectSet<String> processedAttributes = GdxSets.newSet();
addDefaultAttributes();
processBuildingAttributes(builder, processedAttributes);
final Actor actor;
try {
actor = getNewInstanceOfActor(builder);
} catch (final Exception exception) {
getParser().throwError("Unable to create a new instance of actor with tag: " + getTagName(), exception);
return null;
}
builder.finishBuilding(actor);
processTagAttributes(processedAttributes, actor, getManagedObject());
invokeOnCreateActions(actor);
return actor;
}
@Override
protected boolean hasDefaultAttributes(final String tagName) {
return GdxMaps.isNotEmpty(getParser().getStyleSheet().getStyles(tagName));
}
private void addDefaultAttributes() {
final ObjectMap<String, String> defaultAttributes = getParser().getStyleSheet().getStyles(getTagName());
final ObjectMap<String, String> namedAttributes = getNamedAttributes();
if (defaultAttributes != null) {
for (Entry<String, String> attribute : defaultAttributes) {
if (!namedAttributes.containsKey(attribute.key)) {
namedAttributes.put(attribute.key, attribute.value);
}
}
}
}
private void processBuildingAttributes(final LmlActorBuilder builder, final ObjectSet<String> processedAttributes) {
if (getNamedAttributes() == null) {
return;
}
final LmlSyntax syntax = getParser().getSyntax();
for (final Entry<String, String> attribute : getNamedAttributes()) {
// Processing building attributes:
final LmlBuildingAttribute<LmlActorBuilder> buildingAttributeProcessor = syntax
.getBuildingAttributeProcessor(builder, attribute.key);
if (buildingAttributeProcessor != null // This is the actual processing method:
&& buildingAttributeProcessor.process(getParser(), this, builder, attribute.value)) {
// If processing returns true, the attribute is fully parsed and can be omitted during attribute parsing
// after the actor is initiated. If it returns false, it is expected that the attribute will be
// eventually parsed by a second processor, after the widget is created.
processedAttributes.add(attribute.key);
}
}
}
private void processTagAttributes(final ObjectSet<String> processedAttributes, final Actor actor,
final Object managedObject) {
if (hasComponentActors() && !Lml.DISABLE_COMPONENT_ACTORS_ATTRIBUTE_PARSING) {
// Processing own attributes first, ignoring unknowns:
LmlUtilities.processAttributes(actor, this, getParser(), processedAttributes, false);
// Processing leftover attributes for component children:
processComponentAttributes(processedAttributes, actor);
// Processing leftover attributes, after the widget is fully constructed; not throwing errors for unknown
// attributes. After we're done with components, we parse original attributes again for meaningful
// exceptions - "attribute for Window not found" is a lot better than "attribute for Label not found", just
// because we were parsing Label component of Window last. Continuing even for non-strict parser to ensure
// the same parsing behavior.
}
boolean hasDistinctManagedObject = managedObject != null && managedObject != actor;
if (hasDistinctManagedObject) {
// Throws errors if actor is null:
LmlUtilities.processAttributes(managedObject, this, getParser(), processedAttributes, actor == null);
}
if (actor != null) {
// Processing own attributes. Throwing errors for the unknown attributes:
LmlUtilities.processAttributes(actor, this, getParser(), processedAttributes, true);
}
}
private void processComponentAttributes(final ObjectSet<String> processedAttributes, final Actor actor) {
if (hasComponentActors()) {
final Actor[] components = getComponentActors(actor);
if (components == null || components.length == 0) {
return;
}
for (final Actor component : components) {
LmlUtilities.processAttributes(component, this, getParser(), processedAttributes, false);
}
}
}
/**
* @return true if the widget consists of multiple widgets that should have their attributes parsed separately. If
* this method returns true, {@link #getComponentActors(Actor)} cannot return null.
*/
protected boolean hasComponentActors() {
return false;
}
/**
* @param actor instance of the actor. Component widgets should be extracted from this.
* @return components that are used to create the widget. If {@link #hasComponentActors()} returns true, this method
* cannot return false.
*/
protected Actor[] getComponentActors(final Actor actor) {
return null;
}
/**
* @param actor will have its specialized user object extracted (if present) and will invoke all its referenced on
* create actions.
*/
protected void invokeOnCreateActions(final Actor actor) {
final LmlUserObject userObject = LmlUtilities.getOptionalLmlUserObject(actor);
if (userObject != null) {
userObject.invokeOnCreateActions(actor);
}
}
/**
* @param actor will have its specialized user object extracted (if present) and will invoke all its referenced on
* tag close actions.
*/
protected void invokeOnCloseActions(final Actor actor) {
final LmlUserObject userObject = LmlUtilities.getOptionalLmlUserObject(actor);
if (userObject != null) {
userObject.invokeOnCloseActions(actor);
}
}
/**
* @param builder fully initiated builder object with all building attributes already processed.
* @return a new instance of handled actor.
*/
protected abstract Actor getNewInstanceOfActor(LmlActorBuilder builder);
/** @return specialized builder needed to construct the widget. */
protected LmlActorBuilder getNewInstanceOfBuilder() {
return new LmlActorBuilder();
}
@Override
public Actor getActor() {
return actor;
}
@Override
public Object getManagedObject() {
return actor;
}
@Override
protected boolean supportsNamedAttributes() {
return true;
}
@Override
public final void handleDataBetweenTags(final CharSequence rawData) {
if (Strings.isWhitespace(rawData)) {
return;
}
final String[] lines = Strings.split(rawData, '\n');
final Tree.Node node = LmlUtilities.getTreeNode(actor);
if (node != null) {
appendTreeNodes(lines, node);
return;
}
for (String line : lines) {
if (Strings.isNotWhitespace(line)) {
line = line.trim();
handlePlainTextLine(line);
}
}
}
private void appendTreeNodes(final String[] lines, final Tree.Node node) {
for (String line : lines) {
if (Strings.isNotWhitespace(line)) {
line = line.trim();
node.add(new Tree.Node(toLabel(line)));
}
}
}
/** @param plainTextLine trimmed line of data between tags. Is not empty. Should be handled by the tag. */
protected abstract void handlePlainTextLine(String plainTextLine);
/**
* @param rawData unparsed LML data.
* @return parsed LML data as a new label widget.
*/
protected Label toLabel(final String rawData) {
final LmlParser parser = getParser();
return new Label(parser.parseString(rawData, actor), parser.getData().getDefaultSkin());
}
@Override
public void handleChild(final LmlTag childTag) {
if (childTag.isAttachable()) {
// Child tag is an attachable object that can be appended to any widget, even if it is not prepared to
// handle children. For example, tooltip tag can be nested inside a label and will be properly attached,
// even though label wouldn't know how to handle a normal child, like a button or another label.
if (actor != null) {
childTag.attachTo(this);
}
} else if (childTag.getActor() != null) {
final Tree.Node node = LmlUtilities.getTreeNode(actor);
if (node != null) {
// This actor is a tree node. Adding its child as a leaf.
Tree.Node childNode = LmlUtilities.getTreeNode(childTag.getActor());
if (childNode == null) {
childNode = new Tree.Node(childTag.getActor());
}
node.add(childNode);
} else {
handleValidChild(childTag);
}
}
}
/**
* @param childTag is validated and fully initiated. Contains a non-null actor. Should be appended to the stored
* widget.
*/
protected abstract void handleValidChild(LmlTag childTag);
@Override
public final void closeTag() {
doOnTagClose();
closeComponentActors();
invokeOnCloseActions(actor);
}
private void closeComponentActors() {
if (hasComponentActors()) {
for (final Actor component : getComponentActors(actor)) {
invokeOnCloseActions(component);
}
}
}
/** Callback method, safe to override. Invoked by {@link #closeTag()} before on close actions are invoked. */
protected void doOnTagClose() {
// Most actors do nothing upon tag closing. This is reserved for the few widgets that might need additional
// packing, preparing, etc.
}
}