package com.github.czyzby.lml.util;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.ui.Cell;
import com.badlogic.gdx.scenes.scene2d.ui.Dialog;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.scenes.scene2d.ui.Tree;
import com.badlogic.gdx.scenes.scene2d.ui.Window;
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.lml.parser.LmlParser;
import com.github.czyzby.lml.parser.action.ActorConsumer;
import com.github.czyzby.lml.parser.action.StageAttacher;
import com.github.czyzby.lml.parser.impl.action.DefaultStageAttacher;
import com.github.czyzby.lml.parser.impl.action.DefaultStageAttacher.StandardPositionConverter;
import com.github.czyzby.lml.parser.tag.LmlTag;
/** Custom user object set to LML actors when additional data needs to be stored.
*
* @author MJ */
public class LmlUserObject {
private Cell<?> cell;
private Tree.Node node;
private StageAttacher stageAttacher;
private Object data;
private Array<ActorConsumer<?, Object>> onCreateActions;
private Array<ActorConsumer<?, Object>> onCloseActions;
private TableTarget tableTarget = StandardTableTarget.MAIN;
/** @return cell of a table in which the actor is stored. */
public Cell<?> getCell() {
return cell;
}
/** @param cell cell of a table in which the actor is stored. */
public void setCell(final Cell<?> cell) {
this.cell = cell;
}
/** @return custom widget data that would be too specific to include in all widgets. Most actors - if they do need a
* value like this - usually require one such property (and can use a custom container, if they need more),
* so only 1 such field is provided. Null for most widgets. */
public Object getData() {
return data;
}
/** @param data custom widget data. */
public void setData(final Object data) {
this.data = data;
}
/** @return optional stage attacher. */
public StageAttacher getStageAttacher() {
return stageAttacher;
}
/** @param stageAttacher will attach the widget to stage. */
public void setStageAttacher(final StageAttacher stageAttacher) {
this.stageAttacher = stageAttacher;
}
/** Creates a default stage attacher if the widget does not have one already. Use on widgets that are usually
* template roots, like windows or dialogs. */
public void initiateStageAttacher() {
if (stageAttacher == null) {
stageAttacher = new DefaultStageAttacher();
}
}
/** @param onCreateAction stores this action to be invoked when the actor is fully initiated. */
public void addOnCreateAction(final ActorConsumer<?, Object> onCreateAction) {
if (onCreateActions == null) {
onCreateActions = GdxArrays.newArray();
}
if (onCreateAction != null) {
onCreateActions.add(onCreateAction);
}
}
/** @param onActor will invoke all currently stored on create actions on this actor and clear the actions queue. */
public void invokeOnCreateActions(final Actor onActor) {
if (onCreateActions == null) {
return;
}
for (final ActorConsumer<?, Object> onCreateAction : onCreateActions) {
onCreateAction.consume(onActor);
}
onCreateActions = null;
}
/** @param onCloseAction stores this action to be invoked when the actor's tag is closed. */
public void addOnCloseAction(final ActorConsumer<?, Object> onCloseAction) {
if (onCloseActions == null) {
onCloseActions = GdxArrays.newArray();
}
if (onCloseAction != null) {
onCloseActions.add(onCloseAction);
}
}
/** @param onActor will invoke all currently stored on close actions on this actor and clear the actions queue. */
public void invokeOnCloseActions(final Actor onActor) {
if (onCloseActions == null) {
return;
}
for (final ActorConsumer<?, Object> onCloseAction : onCloseActions) {
onCloseAction.consume(onActor);
}
onCloseActions = null;
}
/** @param actor is supposed to become a tree node.
* @param parent optional actor's parent, used to validate if the actor can be a tree node (it has to have a tree
* parent in the structure).
* @param parser used to parse the actor. */
public void prepareTreeNode(final Actor actor, final LmlTag parent, final LmlParser parser) {
if (parent == null) {
parser.throwErrorIfStrict("Actor cannot be tree node if it has no parent.");
return;
} else if (!hasTreeParent(parent)) {
parser.throwErrorIfStrict("Actor cannot be a tree node if it has no tree parent in the structure.");
return;
}
node = new Tree.Node(actor);
}
/** @return non-null tree node containing the actor or null if the actor is not a tree node. */
public Tree.Node getNode() {
return node;
}
private static boolean hasTreeParent(LmlTag parent) {
while (parent != null) {
if (parent.getActor() instanceof Tree) {
return true;
}
parent = parent.getParent();
}
return false;
}
/** @param parser parses an attribute.
* @param actor wants to set its position.
* @param rawAttributeData will be parsed. */
public void parseXPosition(final LmlParser parser, final Actor actor, final String rawAttributeData) {
initiateStageAttacher();
final String parsedValue = parser.parseString(rawAttributeData).trim();
try {
final DefaultStageAttacher stageAttacher = (DefaultStageAttacher) this.stageAttacher;
if (Strings.endsWith(parsedValue, '%')) {
stageAttacher.setXConverter(StandardPositionConverter.PERCENT);
stageAttacher.setX(Float.parseFloat(LmlUtilities.stripEnding(parsedValue)));
} else {
stageAttacher.setXConverter(StandardPositionConverter.ABSOLUTE);
stageAttacher.setX(Float.parseFloat(parsedValue));
}
} catch (final Exception parsingException) {
// Might happen due to invalid number format or class cast.
parser.throwError(
"Unable to parse position: " + rawAttributeData
+ ". Is it a valid float or a float ending with %? Did you use a custom stage attacher that cannot parse position attribute?",
parsingException);
}
}
/** @param parser parses an attribute.
* @param actor wants to set its position.
* @param rawAttributeData will be parsed. */
public void parseYPosition(final LmlParser parser, final Actor actor, final String rawAttributeData) {
initiateStageAttacher();
final String parsedValue = parser.parseString(rawAttributeData, actor).trim();
try {
final DefaultStageAttacher stageAttacher = (DefaultStageAttacher) this.stageAttacher;
if (Strings.endsWith(parsedValue, '%')) {
stageAttacher.setYConverter(StandardPositionConverter.PERCENT);
stageAttacher.setY(Float.parseFloat(LmlUtilities.stripEnding(parsedValue)));
} else {
stageAttacher.setYConverter(StandardPositionConverter.ABSOLUTE);
stageAttacher.setY(Float.parseFloat(parsedValue));
}
} catch (final Exception parsingException) {
// Might happen due to invalid number format or class cast.
parser.throwError(
"Unable to parse position: " + rawAttributeData
+ ". Is it a valid float or a float ending with %? Did you use a custom stage attacher that cannot parse position attribute?",
parsingException);
}
}
/** @return object that determines how the actor is added to a table. */
public TableTarget getTableTarget() {
return tableTarget;
}
/** @param tableTarget determines how the actor is added to a table. */
public void setTableTarget(final TableTarget tableTarget) {
if (tableTarget == null) {
throw new IllegalArgumentException("Table target cannot be null.");
}
this.tableTarget = tableTarget;
}
/** Determines how the actor is added to a table. If a table-extending actor contains multiple tables (for example:
* Window also has a title table, Dialog has additional content and buttons tables), these objects allow to choose
* which table is actually used.
*
* @author MJ */
public static interface TableTarget {
/** @param table may consist of multiple tables.
* @return table that should be chosen with this target. */
Table extract(Table table);
/** @param table will contain the actor.
* @param actor will be added to the table.
* @return cell of the table with the actor. */
Cell<?> add(Table table, Actor actor);
}
/** Used to extract a {@link Table} instance from another table. Used for complex, multipart widgets that consist of
* multiple tables.
*
* @author MJ */
public static interface TableExtractor {
/** @param table may consist of multiple tables.
* @return table chosen by this extractor. */
Table extract(Table table);
}
/** Determines how the actor is added to a table. If a table-extending actor contains multiple tables (for example:
* Window also has a title table, Dialog has additional content and buttons tables), this enum allows to choose
* which table is actually used.
*
* @author MJ */
public static enum StandardTableTarget implements TableTarget {
/** Adds actors directly to the main table of the widget. For most tables, actor will be appended to the table
* itself; if the actor is a dialog, actor is appended to the content table. */
MAIN(new TableExtractor() {
@Override
public Table extract(final Table table) {
return table instanceof Dialog ? ((Dialog) table).getContentTable() : table;
}
}),
/** Adds actors to the title table. Available only for windows. */
TITLE(new TableExtractor() {
@Override
public Table extract(final Table table) {
return table instanceof Window ? ((Window) table).getTitleTable() : table;
}
}),
/** Adds actors to the buttons table. Available only for dialogs. */
BUTTON(new TableExtractor() {
@Override
public Table extract(final Table table) {
return table instanceof Dialog ? ((Dialog) table).getButtonTable() : table;
}
}),
/** Adds actors directly to the table, even if actor has another main table (for example, {@link Dialog} has the
* {@link Dialog#getContentTable()}, which would be ignored using this attacher). */
DIRECT(new TableExtractor() {
@Override
public Table extract(final Table table) {
return table;
}
});
private TableExtractor tableExtractor;
private StandardTableTarget(final TableExtractor tableExteractor) {
tableExtractor = tableExteractor;
}
@Override
public final Cell<?> add(final Table table, final Actor actor) {
return extract(table).add(actor);
}
@Override
public final Table extract(final Table table) {
return tableExtractor.extract(table);
}
/** @param tableExtractor will change behavior of the chosen table target. Use with care! */
public void setTableExtractor(final TableExtractor tableExtractor) {
this.tableExtractor = tableExtractor;
}
}
}