package com.github.czyzby.lml.uedi;
import java.util.Locale;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.scenes.scene2d.Action;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.viewport.ScreenViewport;
import com.badlogic.gdx.utils.viewport.Viewport;
import com.github.czyzby.kiwi.util.common.Strings;
import com.github.czyzby.kiwi.util.gdx.preference.ApplicationPreferences;
import com.github.czyzby.kiwi.util.gdx.viewport.LetterboxingViewport;
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.uedi.assets.AssetManagerProvider;
import com.github.czyzby.lml.uedi.assets.BitmapFontProvider;
import com.github.czyzby.lml.uedi.assets.ModelProvider;
import com.github.czyzby.lml.uedi.assets.MusicProvider;
import com.github.czyzby.lml.uedi.assets.Particle3dEffectProvider;
import com.github.czyzby.lml.uedi.assets.ParticleEffectProvider;
import com.github.czyzby.lml.uedi.assets.PixmapProvider;
import com.github.czyzby.lml.uedi.assets.SoundProvider;
import com.github.czyzby.lml.uedi.assets.TextureAtlasProvider;
import com.github.czyzby.lml.uedi.assets.TextureProvider;
import com.github.czyzby.lml.uedi.assets.impl.InjectingAssetManager;
import com.github.czyzby.lml.uedi.collections.ArrayProvider;
import com.github.czyzby.lml.uedi.collections.ListProvider;
import com.github.czyzby.lml.uedi.collections.MapProvider;
import com.github.czyzby.lml.uedi.collections.SetProvider;
import com.github.czyzby.lml.uedi.i18n.I18NBundleProvider;
import com.github.czyzby.lml.uedi.i18n.LocalePreference;
import com.github.czyzby.lml.uedi.impl.LmlContext;
import com.github.czyzby.lml.uedi.logger.LoggerProvider;
import com.github.czyzby.lml.uedi.music.MusicOnPreference;
import com.github.czyzby.lml.uedi.music.MusicService;
import com.github.czyzby.lml.uedi.music.MusicVolumePreference;
import com.github.czyzby.lml.uedi.music.SoundOnPreference;
import com.github.czyzby.lml.uedi.music.SoundVolumePreference;
import com.github.czyzby.lml.uedi.preferences.PreferencesProvider;
import com.github.czyzby.lml.uedi.ui.BatchProvider;
import com.github.czyzby.lml.uedi.ui.SkinProvider;
import com.github.czyzby.lml.uedi.ui.SpriteBatchProvider;
import com.github.czyzby.lml.uedi.ui.StageProvider;
import com.github.czyzby.lml.uedi.views.View;
import com.github.czyzby.lml.util.Lml;
import com.github.czyzby.lml.util.LmlApplicationListener;
import com.github.czyzby.lml.util.LmlUtilities;
import com.github.czyzby.uedi.Context;
import com.github.czyzby.uedi.scanner.ClassScanner;
/** Default implementation of {@link com.badlogic.gdx.ApplicationListener ApplicationListener} for UEDI-based
* applications using LML to manage their views. Additionally to managing LML view controllers, initiates UEDI context
* and registers some default components and providers.
*
* @author MJ */
public class LmlApplication extends LmlApplicationListener {
private final Class<?> root;
private final ClassScanner classScanner;
private final String preferences;
private Context context;
// GUI data:
private Stage stage;
private boolean centerCamera;
// Locale data:
private I18NBundleProvider i18nBundleProvider;
private LocalePreference localePreference;
/** @param root a class in the base package of the application. All classes implementing UEDI stereotype interfaces
* in its subpackages will be found and initiated.
* @param classScanner used to automatically scan the classpath. Has to be platform-specific, as there is no unified
* way of accessing class pool. */
public LmlApplication(final Class<?> root, final ClassScanner classScanner) {
this(root, classScanner, Lml.LOGGER_TAG);
}
/** @param root a class in the base package of the application. All classes implementing UEDI stereotype interfaces
* in its subpackages will be found and initiated.
* @param classScanner used to automatically scan the classpath. Has to be platform-specific, as there is no unified
* way of accessing class pool.
* @param preferences default path to application's preferences. Should generally match application's name. Will be
* set as default preferences in {@link LmlParser}. */
public LmlApplication(final Class<?> root, final ClassScanner classScanner, final String preferences) {
this.root = root;
this.classScanner = classScanner;
this.preferences = preferences;
}
@Override
public void create() {
ApplicationPreferences.setDefaultPreferences(getPreferences());
context = createContext(classScanner);
addDefaultComponents();
context.scan(root);
doAfterScan();
super.create();
final LmlParser parser = getParser();
parser.getData().setDefaultPreferences(ApplicationPreferences.getPreferences());
i18nBundleProvider.fill(parser);
setStageViewport();
setFirstView();
}
/** Called by the {@link #create()} method to set {@link Viewport} instance of the GUI {@link Stage}. */
protected void setStageViewport() {
if (context.isAvailable(Viewport.class)) {
stage.setViewport(context.get(Viewport.class, stage));
} else {
Gdx.app.debug(Lml.LOGGER_TAG, "No provider found for Viewport.class. Using default Stage viewport.");
}
final Viewport viewport = stage.getViewport();
centerCamera = viewport instanceof ScreenViewport || viewport instanceof LetterboxingViewport;
}
/** Used to construct {@link Context} instance in {@link #create()} method.
*
* @param classScanner will be used to scan for components.
* @return {@link LmlContext} by default. */
protected Context createContext(final ClassScanner classScanner) {
return new LmlContext(this, classScanner);
}
/** This method is invoked after the context is fully scanned and initiated. Can be safely overridden - the method
* does nothing by default. */
protected void doAfterScan() {
}
/** It is assumed that the first view was set using {@link #forceCurrentView(View)} method during context
* initiation. This method ensures smooth showing of the first view.
*
* @throws GdxRuntimeException if first view was not set. */
protected void setFirstView() {
final AbstractLmlView firstView = getCurrentView();
if (firstView == null) {
throw new GdxRuntimeException("No view marked as first. Unable to start application.");
}
setCurrentView(null);
initiateView(firstView);
setView(firstView, null);
}
@Override
protected boolean isCenteringCameraOnResize() {
return centerCamera;
}
/** Also available as "setLocale" action in LML views. Will extract locale from actor's ID.
*
* @param locale will be set as the current application's locale. If is not equal to the current locale, will hide
* current view, reload all referenced {@link com.badlogic.gdx.utils.I18NBundle i18n bundles}, recreate
* all views and reshow the current view. */
public void setLocale(final Locale locale) {
if (!localePreference.isCurrent(locale)) {
setView(getCurrentView(), new Action() {
private boolean reloaded = false;
@Override
public boolean act(final float delta) {
if (!reloaded) {
reloaded = true;
localePreference.setLocale(locale);
localePreference.save();
reloadViews();
}
return true;
}
});
}
}
@Override
protected AbstractLmlView getInstanceOf(final Class<? extends AbstractLmlView> viewClass) {
return context.get(viewClass);
}
/** Registers multiple providers and singletons that produce LibGDX-related object instances. */
protected void addDefaultComponents() {
// Creating components manually to speed up the start-up.
final boolean mapSuper = context.isMapSuperTypes();
context.setMapSuperTypes(false);
// Assets:
final AssetManagerProvider assetManagerProvider = new AssetManagerProvider();
context.addProvider(assetManagerProvider);
context.addDestructible(assetManagerProvider);
final InjectingAssetManager assetManager = assetManagerProvider.getAssetManager();
context.addProvider(new BitmapFontProvider(assetManager));
context.addProvider(new ModelProvider(assetManager));
context.addProvider(new MusicProvider(assetManager));
context.addProvider(new Particle3dEffectProvider(assetManager));
context.addProvider(new ParticleEffectProvider(assetManager));
context.addProvider(new PixmapProvider(assetManager));
context.addProvider(new SoundProvider(assetManager));
context.addProvider(new TextureAtlasProvider(assetManager));
context.addProvider(new TextureProvider(assetManager));
// Collections:
context.addProvider(new ArrayProvider<Object>());
context.addProvider(new ListProvider<Object>());
context.addProvider(new MapProvider<Object, Object>());
context.addProvider(new SetProvider<Object>());
// I18n:
i18nBundleProvider = new I18NBundleProvider(assetManager);
localePreference = new LocalePreference(i18nBundleProvider);
i18nBundleProvider.setLocalePreference(localePreference);
context.addProvider(i18nBundleProvider);
context.addProperty(localePreference);
context.addProvider(localePreference);
// Logging:
context.addProvider(new LoggerProvider());
// Preferences:
context.addProvider(new PreferencesProvider());
// UI:
final SpriteBatchProvider spriteBatchProvider = new SpriteBatchProvider();
context.addProvider(new BatchProvider(spriteBatchProvider));
context.addProvider(spriteBatchProvider);
context.addDestructible(spriteBatchProvider);
context.addProvider(new SkinProvider(assetManager));
final StageProvider stageProvider = new StageProvider(spriteBatchProvider.getBatch());
stage = stageProvider.getStage();
context.addProvider(stageProvider);
if (includeMusicService()) { // Music:
final MusicOnPreference musicOn = new MusicOnPreference();
final SoundOnPreference soundOn = new SoundOnPreference();
final MusicVolumePreference musicVolume = new MusicVolumePreference();
final SoundVolumePreference soundVolume = new SoundVolumePreference();
context.addProperty(musicOn);
context.addProperty(soundOn);
context.addProperty(musicVolume);
context.addProperty(soundVolume);
context.add(new MusicService(stage, musicOn, soundOn, musicVolume, soundVolume));
}
context.setMapSuperTypes(mapSuper);
// Application listener:
context.add(this);
}
/** {@link MusicService}, contrary to most utilities provided by default (like {@link Stage} or
* {@link com.badlogic.gdx.assets.AssetManager AssetManager}), is not a part of standard LibGDX API. It was added to
* ease the management of sound and music setting, as well as make it trivial to implement looping themes. This
* method allows to choose whether to include it in the context (along with its music settings) or not.
*
* @return true by default. Override and return false to omit {@link MusicService} in the context. */
protected boolean includeMusicService() {
return true;
}
@Override
protected void addDefaultActions() {
super.addDefaultActions();
final LmlData data = getParser().getData();
// Extracts ID from the actor, parses it as Locale instance. Tries to change current locale.
data.addActorConsumer("setLocale", new ActorConsumer<Void, Actor>() {
@Override
public Void consume(final Actor actor) {
setLocale(LocalePreference.fromString(LmlUtilities.getActorId(actor)));
return null;
}
});
// Returns current locale serialized as string.
data.addActorConsumer("getLocale", new ActorConsumer<String, Actor>() {
@Override
public String consume(final Actor actor) {
return LocalePreference.toString(localePreference.getLocale());
}
});
// Reload current view. Useful for rapid GUI prototyping.
data.addActorConsumer("reloadView", new ActorConsumer<Void, Object>() {
@Override
public Void consume(final Object actor) {
reloadView(getCurrentView());
return null;
}
});
if (includeMusicService()) {
context.get(MusicService.class).addDefaultActions(data);
}
}
/** @param view will be immediately set as the current view. Note that this should not be generally used to change
* screens - to ensure smooth transitions, use {@code setView} methods instead.
* @see #setView(AbstractLmlView)
* @see #setView(Class) */
public void forceCurrentView(final View view) {
setCurrentView(view);
}
/** @return path to application's preferences. Will be set as default preferences in {@link LmlParser}.
* @see ApplicationPreferences#getPreferences() */
protected String getPreferences() {
return preferences;
}
@Override
protected void initiateView(final AbstractLmlView view) {
getViews().put(view.getClass(), view);
final String viewId = view.getViewId();
if (Strings.isNotEmpty(viewId)) {
addClassAlias(viewId, view.getClass());
}
view.setStage(null);
final Array<Actor> actors = getParser().createView(view, view.getTemplateFile());
if (view instanceof View) {
((View) view).clearActors();
((View) view).addActors(actors);
} else {
throw new GdxRuntimeException("LmlApplication can handle only View instances.");
}
view.setStage(stage);
}
@Override
protected LmlParser createParser() {
if (!context.isAvailable(LmlParser.class)) {
throw new GdxRuntimeException(
"No singleton, provider or factory producing LmlParser. Unable to construct views.");
}
return context.get(LmlParser.class);
}
/** @return application's UEDI context, which allows to construct and inject components. */
public Context getContext() {
return context;
}
@Override
public void reloadView(final AbstractLmlView lmlView) {
final View view = (View) lmlView;
final boolean isCurrent = getCurrentView() == view;
if (isCurrent) {
view.getStage().getRoot().clearChildren();
}
view.setStage(null);
view.clearActors();
view.addActors(getParser().createView(view, view.getTemplateFile()));
view.setStage(stage);
if (isCurrent) {
view.show();
}
}
@Override
public void addClassAlias(final String alias, final Class<? extends AbstractLmlView> viewClass) {
super.addClassAlias(alias, viewClass);
}
@Override
public void dispose() {
super.dispose();
ApplicationPreferences.saveAllPreferences();
if (context != null) {
try {
context.destroy();
} catch (final Exception exception) {
Gdx.app.error(Lml.LOGGER_TAG, "Unable to destroy the context.", exception);
}
}
}
}