package com.github.czyzby.lml.util;
import java.io.Writer;
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.math.Interpolation;
import com.badlogic.gdx.scenes.scene2d.Action;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.actions.Actions;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.ObjectMap;
import com.badlogic.gdx.utils.reflect.ClassReflection;
import com.badlogic.gdx.utils.reflect.ReflectionException;
import com.github.czyzby.kiwi.util.common.Strings;
import com.github.czyzby.kiwi.util.gdx.GdxUtilities;
import com.github.czyzby.kiwi.util.gdx.asset.Disposables;
import com.github.czyzby.kiwi.util.gdx.collection.GdxMaps;
import com.github.czyzby.kiwi.util.gdx.reflection.Reflection;
import com.github.czyzby.lml.parser.LmlData;
import com.github.czyzby.lml.parser.LmlParser;
import com.github.czyzby.lml.parser.action.ActorConsumer;
import com.github.czyzby.lml.parser.impl.AbstractLmlView;
import com.github.czyzby.lml.parser.impl.tag.Dtd;
/** An {@link ApplicationListener} implementation that manages a list of {@link AbstractLmlView LML views}. Forces the
* user to prepare a {@link LmlParser} with {@link #createParser()} method. Ensures smooth view transitions. Adds
* default actions with {@link #addDefaultActions()} method: "exit" closes the application after smooth screen hiding,
* "close" is a no-op utility method for dialogs and "setView" changes the current view according to the actor's ID (the
* ID has to match name of a class extending {@link AbstractLmlView}). Most of its settings are customizable - go
* through protected methods API for more info.
*
* <p>
* {@link AbstractLmlView} instances managed by this class are required to properly implement
* {@link AbstractLmlView#getTemplateFile()}. Note that the views are likely to be accessed with reflection, so make
* sure to include their classes in GWT reflection mechanism.
*
* <p>
* What LibGDX {@link com.badlogic.gdx.Game Game} is to {@link com.badlogic.gdx.Screen Screen}, this class is the same
* thing to {@link AbstractLmlView}. Except it adds much more functionalities.
*
* @author MJ */
public abstract class LmlApplicationListener implements ApplicationListener {
private final ObjectMap<Class<? extends AbstractLmlView>, AbstractLmlView> views = GdxMaps.newObjectMap();
private final ObjectMap<String, Class<? extends AbstractLmlView>> aliases = GdxMaps.newObjectMap();
private final ViewChangeRunnable viewChangeRunnable = new ViewChangeRunnable();
private AbstractLmlView currentView;
private LmlParser lmlParser;
private boolean clearActorsMap;
private boolean clearMetaData;
/** @return {@link LmlParser} instance created with {@link #createParser()} method.
* @see LmlParser */
public LmlParser getParser() {
return lmlParser;
}
/** @return currently displayed {@link AbstractLmlView}. Can be null. */
public AbstractLmlView getCurrentView() {
return currentView;
}
/** @param currentView will be immediately set as {@link #getCurrentView() current view}. Note that this is not a
* part of public API: to ensure smooth view transitions, use {@link #setView(AbstractLmlView)}
* method. */
protected void setCurrentView(final AbstractLmlView currentView) {
this.currentView = currentView;
}
/** This method is automatically invoked with {@link AbstractLmlView#getViewId()} (if it returns a non-empty value)
* on each view initiated with {@link #initiateView(AbstractLmlView)}. This method can be invoked manually for lazy
* views loading: they will be accessible through the chosen alias in LML templates even when their instance is not
* created yet.
*
* <p>
* If you initiate all views eagerly (in the correct order in which they reference one another), they are likely to
* already registered by the time {@link LmlParser} parses their templates. However, calling this method for each of
* your expected views in {@link #create()} is considered a good practice if you use {@code setView} LML method to
* switch between screens.
*
* @param alias name of the view in LML templates. It will be used by {@code setView} LML method to choose the
* appropriate view class.
* @param viewClass will be mapped to the selected alias. */
protected void addClassAlias(final String alias, final Class<? extends AbstractLmlView> viewClass) {
aliases.put(alias, viewClass);
}
/** @return direct reference to {@link AbstractLmlView} instances cache. Views (map values) are mapped by their
* classes (map keys). */
protected ObjectMap<Class<? extends AbstractLmlView>, AbstractLmlView> getViews() {
return views;
}
/** Uses current {@link LmlParser} to generate a DTD schema file with all supported tags, macros and attributes.
* Should be used only during development: DTD allows to validate LML templates during creation (and add content
* assist thanks to XML support in your IDE), but is not used in any way by the {@link LmlParser} in runtime.
*
* @param file path to the file where DTD schema should be saved. Advised to be local or absolute. Note that some
* platforms (GWT) do not support file saving - this method should be used on desktop platform and only
* during development.
* @throws GdxRuntimeException when unable to save DTD schema.
* @see Dtd */
public void saveDtdSchema(final FileHandle file) {
try {
final Writer appendable = file.writer(false, "UTF-8");
final boolean strict = lmlParser.isStrict();
lmlParser.setStrict(false); // Temporary setting to non-strict to generate as much tags as possible.
createDtdSchema(lmlParser, appendable);
appendable.close();
lmlParser.setStrict(strict);
} catch (final Exception exception) {
throw new GdxRuntimeException("Unable to save DTD schema.", exception);
}
}
/** @param clearActorsMap if true, {@link LmlParser#getActorsMappedByIds() IDs to actors map} will be cleared after
* each view parsing. This prevents from injecting or modifying actors from previous views if ID
* collisions occur. When this value is set to true, you cannot access actors with their IDs using
* {@link LmlParser#getActorsMappedByIds()}. When set to false, you're advised to avoid IDs collisions in
* all views. Defaults to false.
* @see LmlParser#getActorsMappedByIds()
* @see com.github.czyzby.lml.annotation.LmlActor */
protected void setClearActorsMap(final boolean clearActorsMap) {
this.clearActorsMap = clearActorsMap;
}
/** @param clearMetaData if true, {@link LmlUtilities#clearLmlUserObjects(Iterable)} will be called with parsed
* every actor after view is created. While this limits the amount of objects assigned to each actor and
* kept at runtime, this option should be used with care, as some features rely on this mechanism. Use
* when absolutely sure that it doesn't break anything. Defaults to false.
* @see LmlUtilities#clearLmlUserObject(Actor)
* @see LmlUtilities#clearLmlUserObjects(Iterable) */
protected void setClearMetaData(final boolean clearMetaData) {
this.clearMetaData = clearMetaData;
}
/** This is a utility method that allows you to hook up into DTD generation process or even modify it completely.
* This method is called by {@link #saveDtdSchema(FileHandle)} after the parser was already set to non-strict. By
* default, this method calls standard DTD utility method: {@link Dtd#saveSchema(LmlParser, Appendable)}. By
* overriding this method, you can generate minified schema with
* {@link Dtd#saveMinifiedSchema(LmlParser, Appendable)} or manually append some customized tags and attributes
* using {@link Appendable} API.
*
* <p>
* If you want to generate DTD schema file for your LML parser, use {@link #saveDtdSchema(FileHandle)} method
* instead.
*
* @param parser its schema will be generated.
* @param appendable a reference to target file.
* @see #saveDtdSchema(FileHandle)
* @throws Exception if your saving method throws any exception, it will wrapped with {@link GdxRuntimeException}
* and rethrown. */
protected void createDtdSchema(final LmlParser parser, final Appendable appendable) throws Exception {
Dtd.saveSchema(parser, appendable);
}
/** Called when application is created.
*
* <p>
* Prepares {@link LmlParser} with {@link #createParser()} method. Adds default actions present in all LML
* templates. When overridden, make sure to call super. */
@Override
public void create() {
lmlParser = createParser();
addDefaultActions();
}
/** Called by {@link #create()} after creation of the {@link LmlParser} using {@link #createParser()} method.
* Registers default actions available in all views. */
protected void addDefaultActions() {
final LmlData data = lmlParser.getData();
// Closes the application after screen transition.
data.addActorConsumer("exit", new ActorConsumer<Void, Object>() {
@Override
public Void consume(final Object actor) {
GdxUtilities.clearInputProcessor();
exit();
return null;
}
});
// Does nothing. Utility for dialogs: <dialog> ... <textButton onResult="close"> ...
data.addActorConsumer("close", new ActorConsumer<Void, Object>() {
@Override
public Void consume(final Object actor) {
return null;
}
});
// Changes current view. Uses actor ID to determine view's class.
data.addActorConsumer("setView", new ActorConsumer<Void, Actor>() {
@Override
public Void consume(final Actor actor) {
final String viewClassName = LmlUtilities.getActorId(actor);
final Class<? extends AbstractLmlView> viewClass = LmlApplicationListener.this
.getViewClass(viewClassName);
setView(viewClass);
return null;
}
});
}
/** @param viewClassName a qualified name of the view class (including package and class name) or a class alias
* registered with {@link #addClassAlias(String, Class)}.
* @return the corresponding view class object.
* @throws GdxRuntimeException if unable to determine view class. */
@SuppressWarnings("unchecked")
protected Class<? extends AbstractLmlView> getViewClass(final String viewClassName) {
if (aliases.containsKey(viewClassName)) {
return aliases.get(viewClassName);
}
try {
return ClassReflection.forName(viewClassName);
} catch (final ReflectionException exception) {
throw new GdxRuntimeException(
"Unable to determine view class: " + viewClassName
+ ". Does a class with such name exists? Was such class alias properly registered?",
exception);
}
}
/** Smoothly hides the {@link #getCurrentView() current view} and closes the application.na */
public void exit() {
if (currentView == null) {
GdxUtilities.exit();
} else {
currentView.getStage().addAction(Actions.sequence(getViewHidingAction(currentView),
Actions.run(GdxUtilities.getApplicationClosingRunnable())));
}
}
/** @return a new customized instance of {@link LmlParser} used to process LML templates.
* @see Lml
* @see LmlParserBuilder */
protected abstract LmlParser createParser();
/** Calls {@link AbstractLmlView#resize(int, int, boolean)} on current view if it isn't empty.
*
* @param width current application width.
* @param height current application height.
* @see #isCenteringCameraOnResize() */
@Override
public void resize(final int width, final int height) {
if (currentView != null) {
currentView.resize(width, height, isCenteringCameraOnResize());
}
}
/** @return if true, camera will be centered when resize event occurs. Defaults to false. When using certain
* viewports (like {@link com.badlogic.gdx.utils.viewport.ScreenViewport screen viewport}), this method
* should return true.
* @see #resize(int, int) */
protected boolean isCenteringCameraOnResize() {
return false;
}
/** Clears the screen using {@link GdxUtilities#clearScreen()}. Calls {@link AbstractLmlView#render(float)} on
* current view (if it isn't empty) with current delta time. */
@Override
public void render() {
GdxUtilities.clearScreen();
if (currentView != null) {
currentView.render(Gdx.graphics.getDeltaTime());
}
}
/** Calls {@link AbstractLmlView#pause()} on current view if it isn't empty. */
@Override
public void pause() {
if (currentView != null) {
currentView.pause();
}
}
/** Calls {@link AbstractLmlView#resume()} on current view if it isn't empty. */
@Override
public void resume() {
if (currentView != null) {
currentView.pause();
}
}
/** Calls {@link AbstractLmlView#dispose()} on each stored view. When overriding this method, make sure to call
* super or dispose your views manually. */
@Override
public void dispose() {
Disposables.disposeOf(views.values());
}
/** @param viewClass {@link AbstractLmlView} extension that represents a single view.
* @return an instance of the view. If the instance is not currently cached, it will be created using default
* no-argument constructor with reflection.
* @see #initiateView(AbstractLmlView) */
protected AbstractLmlView getView(final Class<? extends AbstractLmlView> viewClass) {
if (!views.containsKey(viewClass)) {
// Cached version is not present - asking the parser to create and fill view:
final AbstractLmlView view = getInstanceOf(viewClass);
initiateView(view);
return view;
}
return views.get(viewClass);
}
/** Disposes of {@link AbstractLmlView} instances. Clears views cache. Note that {@link #getCurrentView() current
* view} will be set as null, so invoking this method is advised after the current view is already hidden. */
public void clearViews() {
Disposables.disposeOf(views.values());
currentView = null;
views.clear();
}
/** @param viewClass {@link AbstractLmlView} extension that represents a single view. If an instance of this view
* class is managed by the application listener, it will be {@link AbstractLmlView#dispose() disposed}
* and removed from views cache. If the view is currently displayed, {@link #getCurrentView() current
* view} will be set to null. */
public void clearView(final Class<? extends AbstractLmlView> viewClass) {
final AbstractLmlView view = views.get(viewClass);
if (view != null) {
view.dispose();
views.remove(viewClass);
validateCurrentView(view);
}
}
/** @param removedView is being removed. If it is currently displayed, current view will be set to null. */
private void validateCurrentView(final AbstractLmlView removedView) {
if (removedView == currentView) {
currentView = null;
}
}
/** @param view {@link AbstractLmlView} extension that represents a single view. Will be
* {@link AbstractLmlView#dispose() disposed} and removed from views cache. If the view is currently
* displayed, {@link #getCurrentView() current view} will be set to null. */
public void clearView(final AbstractLmlView view) {
view.dispose();
views.remove(view.getClass());
validateCurrentView(view);
}
/** All currently cached views will be reloaded using {@link #reloadView(AbstractLmlView)} method. Note that this
* method should be called when the current view is hidden, as parsing of multiple templates might cause some delays
* (especially on slower devices). Useful for reloading localized texts after i18n bundle change. */
public void reloadViews() {
for (final AbstractLmlView view : views.values()) {
reloadView(view);
}
}
/** @param view will receive {@link AbstractLmlView#clear()} call. Its actors will be removed. Its template file
* accessed by {@link AbstractLmlView#getTemplateFile()} will be parsed by the {@link LmlParser} and used
* to fill the view. */
public void reloadView(final AbstractLmlView view) {
view.clear();
view.getStage().getRoot().clearChildren();
lmlParser.createView(view, view.getTemplateFile());
}
/** @param view its instance will be cached and returned each time it is requested with {@link #getView(Class)}
* method. Its {@link AbstractLmlView#getViewId()} will be used to create a class alias with
* {@link #addClassAlias(String, Class)}. Its template file accessed by
* {@link AbstractLmlView#getTemplateFile()} will be parsed by the {@link LmlParser} and used to fill the
* view.
* @see #setClearActorsMap(boolean)
* @see #setClearMetaData(boolean) */
protected void initiateView(final AbstractLmlView view) {
views.put(view.getClass(), view);
final String viewId = view.getViewId();
if (Strings.isNotEmpty(viewId)) {
addClassAlias(viewId, view.getClass());
}
lmlParser.createView(view, view.getTemplateFile());
if (clearActorsMap) {
lmlParser.getActorsMappedByIds().clear();
}
if (clearMetaData) {
LmlUtilities.clearLmlUserObjects(view.getStage().getActors());
}
}
/** @param viewClass {@link AbstractLmlView} extension that represents a single view. Its instance is requested.
* @return a new instance of the passed class. By default, the instance is created using the default no-argument
* constructor using reflection. Override this method to change the view creation way. */
protected AbstractLmlView getInstanceOf(final Class<? extends AbstractLmlView> viewClass) {
return Reflection.newInstance(viewClass);
}
/** @param viewClass {@link AbstractLmlView} extension that represents a single view. An instance of this class will
* become the current view after view transition.
* @see #setView(AbstractLmlView) */
public void setView(final Class<? extends AbstractLmlView> viewClass) {
setView(getView(viewClass), null);
}
/** @param viewClass {@link AbstractLmlView} extension that represents a single view. An instance of this class will
* become the current view after view transition.
* @param doAfterHide will be executed after the current view is fully hidden. Is never executed if there was no
* current view.
* @see #setView(AbstractLmlView, Action) */
public void setView(final Class<? extends AbstractLmlView> viewClass, final Action doAfterHide) {
setView(getView(viewClass), doAfterHide);
}
/** @param view will be set as the current view after view transition. Current screen (if any exists) will receive a
* {@link AbstractLmlView#hide()} call. The new screen will be resized using
* {@link AbstractLmlView#resize(int, int, boolean)} and then will receive a
* {@link AbstractLmlView#show()} call.
* @see #getViewShowingAction(AbstractLmlView)
* @see #getViewHidingAction(AbstractLmlView) */
public void setView(final AbstractLmlView view) {
setView(view, null);
}
/** @param view will be set as the current view after view transition. Current screen (if any exists) will receive a
* {@link AbstractLmlView#hide()} call. The new screen will be resized using
* {@link AbstractLmlView#resize(int, int, boolean)} and then will receive a
* {@link AbstractLmlView#show()} call.
* @param doAfterHide will be executed after the current view is fully hidden. Is never executed if there was no
* current view.
* @see #getViewShowingAction(AbstractLmlView)
* @see #getViewHidingAction(AbstractLmlView) */
public void setView(final AbstractLmlView view, final Action doAfterHide) {
if (currentView != null) {
viewChangeRunnable.setView(view);
Gdx.input.setInputProcessor(null);
currentView.hide();
final Action hideAction = doAfterHide == null
? Actions.sequence(getViewHidingAction(currentView), Actions.run(viewChangeRunnable))
: Actions.sequence(getViewHidingAction(currentView), doAfterHide, Actions.run(viewChangeRunnable));
currentView.getStage().addAction(hideAction);
} else {
currentView = view;
currentView.resize(Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), isCenteringCameraOnResize());
Gdx.input.setInputProcessor(currentView.getStage());
currentView.show();
currentView.getStage().addAction(getViewShowingAction(view));
}
}
/** @param view is about to be hidden.
* @return {@link Action} instance used to hide the view. A simple fade-out action by default.
* @see #getViewTransitionDuration() */
protected Action getViewHidingAction(final AbstractLmlView view) {
return Actions.fadeOut(getViewTransitionDuration(), Interpolation.fade);
}
/** @param view is about to be shown.
* @return {@link Action} instance used to show the view. By default, makes sure that the view is transparent and
* begins a simple fade-in action.
* @see #getViewTransitionDuration() */
protected Action getViewShowingAction(final AbstractLmlView view) {
return Actions.sequence(Actions.alpha(0f), Actions.fadeIn(getViewTransitionDuration(), Interpolation.fade));
}
/** @return length of a single view hiding or showing action used by default view transition actions. In seconds.
* @see #getViewShowingAction(AbstractLmlView)
* @see #getViewHidingAction(AbstractLmlView) */
protected float getViewTransitionDuration() {
return 0.4f;
}
/** {@link Action} utility. Used to change the current view thanks to {@link Actions#run(Runnable)}.
*
* @author MJ */
protected class ViewChangeRunnable implements Runnable {
private AbstractLmlView view;
/** @param view should be shown. */
public void setView(final AbstractLmlView view) {
this.view = view;
}
@Override
public void run() {
currentView = null;
LmlApplicationListener.this.setView(view, null);
}
}
}