/******************************************************************************
* Copyright (C) 2014 Yevgeny Krasik *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
* You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
******************************************************************************/
package com.github.ykrasik.jaci.cli.libgdx;
import com.badlogic.gdx.Application;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.*;
import com.badlogic.gdx.scenes.scene2d.ui.Label.LabelStyle;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton.TextButtonStyle;
import com.badlogic.gdx.scenes.scene2d.ui.TextField.TextFieldStyle;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import com.badlogic.gdx.utils.Array;
import com.github.ykrasik.jaci.cli.Cli;
import com.github.ykrasik.jaci.cli.CliShell;
import com.github.ykrasik.jaci.cli.commandline.CommandLineManager;
import com.github.ykrasik.jaci.cli.gui.CliGui;
import com.github.ykrasik.jaci.cli.hierarchy.CliCommandHierarchy;
import com.github.ykrasik.jaci.cli.hierarchy.CliCommandHierarchyImpl;
import com.github.ykrasik.jaci.cli.libgdx.commandline.LibGdxCommandLineManager;
import com.github.ykrasik.jaci.cli.libgdx.gui.LibGdxCliGui;
import com.github.ykrasik.jaci.cli.libgdx.log.ApplicationLoggingDecorator;
import com.github.ykrasik.jaci.cli.libgdx.output.LibGdxCliOutput;
import com.github.ykrasik.jaci.cli.libgdx.output.LibGdxCliOutputBuffer;
import com.github.ykrasik.jaci.cli.output.CliPrinter;
import com.github.ykrasik.jaci.hierarchy.CommandHierarchyDef;
import java.util.Objects;
/**
* A CLI implementation for LibGdx.<br>
* <br>
* Implemented as a {@link Table} that keeps a list of listeners that are called whenever the CLI
* is made visible or invisible with {@link #setVisible(boolean)}, to allow the game to be paused (for example).<br>
* <br>
* Built with a default skin, unless a custom skin is provided:
* A custom skin must have the following:
* <ul>
* <li>
* A {@link LabelStyle} called 'workingDirectory' that will be used to style the 'workingDirectory' label.
* </li>
* <li>
* A {@link com.badlogic.gdx.scenes.scene2d.utils.Drawable} called 'workingDirectoryBackground'
* that will be used as the background of the 'workingDirectory' label.
* </li>
* <li>
* A {@link TextFieldStyle} called 'commandLine' that will be used to style the command line text field.
* </li>
* <li>
* A {@link TextButtonStyle} called 'closeCliButton' that will be used to style the close button.
* </li>
* <li>
* A {@link com.badlogic.gdx.scenes.scene2d.utils.Drawable} called 'bottomRowBackground'
* that will be used as the background of the 'bottom row' (working directory, command line, close button).
* </li>
* <li>
* A {@link LabelStyle} called 'outputEntry' that will be used to style output buffer entry lines.
* </li>
* <li>
* A {@link com.badlogic.gdx.scenes.scene2d.utils.Drawable} called 'cliBackground'
* that will be used as the background of the whole widget.
* </li>
* </ul>
*
* Built through a concrete implementation of {@link AbstractBuilder}.
*
* @author Yevgeny Krasik
*/
public class LibGdxCli extends Table {
private final Array<VisibleListener> visibleListeners = new Array<>(2);
private final Cli cli;
/**
* @param skin Skin to use.
* @param hierarchy Command hierarchy.
* @param maxBufferEntries Maximum amount of line entries in the buffer to keep.
* @param maxCommandHistory Maximum amount of command history entries to keep.
*/
private LibGdxCli(Skin skin, CliCommandHierarchy hierarchy, int maxBufferEntries, int maxCommandHistory) {
super(Objects.requireNonNull(skin, "skin"));
// CLI GUI controller.
final Label workingDirectory = new Label("", skin, "workingDirectory");
workingDirectory.setName("workingDirectory");
final CliGui gui = new LibGdxCliGui(workingDirectory);
// Buffer for cli output.
final LibGdxCliOutputBuffer buffer = new LibGdxCliOutputBuffer(skin, maxBufferEntries);
buffer.setName("buffer");
buffer.bottom().left();
// LibGdx doesn't like '\t', so we replace it with 4 spaces.
final String tab = " ";
final CliPrinter out = new CliPrinter(new LibGdxCliOutput(buffer, Color.WHITE), tab);
final CliPrinter err = new CliPrinter(new LibGdxCliOutput(buffer, Color.SALMON), tab);
// TextField as command line.
final TextField commandLine = new TextField("", skin, "commandLine");
commandLine.setName("commandLine");
final CommandLineManager commandLineManager = new LibGdxCommandLineManager(commandLine);
// Create the shell and the actual CLI.
final CliShell shell = new CliShell.Builder(hierarchy, gui, out, err)
.setMaxCommandHistory(maxCommandHistory)
.build();
cli = new Cli(shell, commandLineManager);
// Hook input events to CLI events.
this.addListener(new LibGdxCliInputListener(cli));
// A close button.
// TODO: Make this a graphical button, not an ugly text button.
final Button closeButton = new TextButton("X", skin, "closeCliButton");
closeButton.padRight(15).padLeft(15);
closeButton.setName("closeButton");
closeButton.addListener(new ClickListener() {
@Override
public void clicked(InputEvent event, float x, float y) {
setVisible(false);
}
});
// Some layout.
final Table workingDirectoryTable = new Table(skin);
workingDirectoryTable.setName("workingDirectoryTable");
workingDirectoryTable.setBackground("workingDirectoryBackground");
workingDirectoryTable.add(workingDirectory).fill().padLeft(3).padRight(5);
// The bottom row contains the current path, command line and a close button.
final Table bottomRow = new Table(skin);
bottomRow.setName("bottomRow");
bottomRow.setBackground("bottomRowBackground");
bottomRow.add(workingDirectoryTable).fill();
bottomRow.add(commandLine).fill().expandX();
bottomRow.add(closeButton).fill();
this.setName("cli");
this.setBackground("cliBackground");
// TODO: This should operate on it's own stage.
this.addVisibleListener(new VisibleListener() {
@Override
public void onVisibleChange(boolean wasVisible, boolean isVisible) {
if (!wasVisible && isVisible) {
setKeyboardFocus(commandLine);
} else {
setKeyboardFocus(null);
}
}
});
this.pad(0);
this.add(buffer).fill().expand();
this.row();
this.add(bottomRow).fill();
this.top().left();
this.setFillParent(true);
}
private void setKeyboardFocus(TextField commandLine) {
final Stage stage = getStage();
if (stage != null) {
stage.setKeyboardFocus(commandLine);
}
}
/**
* Add a {@link VisibleListener} that will be called when this actor's visibility state changes -
* it either was visible and became invisible, or the other way.
*
* @param listener Listener to add.
*/
public void addVisibleListener(VisibleListener listener) {
visibleListeners.add(listener);
}
/**
* Remove a {@link VisibleListener} from this actor.
*
* @param listener Listener to remove.
*/
public void removeVisibleListener(VisibleListener listener) {
visibleListeners.removeValue(listener, true);
}
/**
* Toggle the visibility of this CLI on or off.
*/
public void toggleVisibility() {
this.setVisible(!this.isVisible());
}
@Override
public void setVisible(boolean visible) {
final boolean wasVisible = isVisible();
for (VisibleListener listener : visibleListeners) {
listener.onVisibleChange(wasVisible, visible);
}
super.setVisible(visible);
}
@Override
protected void setStage(Stage stage) {
super.setStage(stage);
// Call the listeners when we're first added to a stage.
final boolean isVisible = isVisible();
final boolean wasVisible = !isVisible;
for (VisibleListener listener : visibleListeners) {
listener.onVisibleChange(wasVisible, isVisible);
}
}
/**
* @return CLI stdOut.
*/
public CliPrinter getOut() {
return cli.getOut();
}
/**
* @return CLI stdErr.
*/
public CliPrinter getErr() {
return cli.getErr();
}
/**
* A builder for a {@link LibGdxCli}.
* Builds a CLI with a default skin, unless a custom skin is specified via {@link #setSkin(Skin)}.<br>
* The main methods to use are {@link #processClasses(Class[])} and {@link #process(Object[])} which process
* a class and add any annotated methods as commands to this builder.
*/
public abstract static class AbstractBuilder {
private final CommandHierarchyDef.Builder hierarchyBuilder = new CommandHierarchyDef.Builder();
private Skin skin;
private int maxBufferEntries = 1000;
private int maxCommandHistory = 30;
private boolean decorateApplicationLog = false;
/**
* Process the classes and add any commands defined through annotations to this builder.
* Each class must have a no-args constructor.
*
* @param classes Classes to process.
* @return {@code this}, for chaining.
*/
public AbstractBuilder processClasses(Class<?>... classes) {
hierarchyBuilder.processClasses(classes);
return this;
}
/**
* Process the objects' classes and add any commands defined through annotations to this builder.
*
* @param instances Objects whose classes to process.
* @return {@code this}, for chaining.
*/
public AbstractBuilder process(Object... instances) {
hierarchyBuilder.process(instances);
return this;
}
/**
* Set the maximum amount of output buffer entries to keep.
*
* @param maxBufferEntries Max output buffer entries to keep.
* @return {@code this}, for chaining.
*/
public AbstractBuilder setMaxBufferEntries(int maxBufferEntries) {
this.maxBufferEntries = maxBufferEntries;
return this;
}
/**
* Set the maximum amount of command history entries to keep.
*
* @param maxCommandHistory Max command history entries to keep.
* @return {@code this}, for chaining.
*/
public AbstractBuilder setMaxCommandHistory(int maxCommandHistory) {
this.maxCommandHistory = maxCommandHistory;
return this;
}
/**
* Set the skin to use.<br>
* A custom skin must have the following:
* <ul>
* <li>
* A {@link LabelStyle} called 'workingDirectory' that will be used to style the 'workingDirectory' label.
* </li>
* <li>
* A {@link com.badlogic.gdx.scenes.scene2d.utils.Drawable} called 'workingDirectoryBackground'
* that will be used as the background of the 'workingDirectory' label.
* </li>
* <li>
* A {@link TextFieldStyle} called 'commandLine' that will be used to style the command line text field.
* </li>
* <li>
* A {@link TextButtonStyle} called 'closeCliButton' that will be used to style the close button.
* </li>
* <li>
* A {@link com.badlogic.gdx.scenes.scene2d.utils.Drawable} called 'bottomRowBackground'
* that will be used as the background of the 'bottom row' (working directory, command line, close button).
* </li>
* <li>
* A {@link LabelStyle} called 'outputEntry' that will be used to style output buffer entry lines.
* </li>
* <li>
* A {@link com.badlogic.gdx.scenes.scene2d.utils.Drawable} called 'cliBackground'
* that will be used as the background of the whole widget.
* </li>
* </ul>
*
* @param skin Skin to use.
* @return {@code this}, for chaining.
*/
public AbstractBuilder setSkin(Skin skin) {
this.skin = skin;
return this;
}
// FIXME: JavaDoc - whether to log Gdx.App.log
public AbstractBuilder setDecorateApplicationLog(boolean decorateApplicationLog) {
this.decorateApplicationLog = decorateApplicationLog;
return this;
}
/**
* @return A {@link LibGdxCli} built out of this builder's parameters.
*/
public LibGdxCli build() {
final Skin skin = getSkin();
final CliCommandHierarchy hierarchy = CliCommandHierarchyImpl.from(hierarchyBuilder.build());
final LibGdxCli cli = new LibGdxCli(skin, hierarchy, maxBufferEntries, maxCommandHistory);
if (decorateApplicationLog) {
decorateApplication(cli);
}
return cli;
}
private Skin getSkin() {
if (skin != null) {
return skin;
}
// Default skin.
return new Skin(Gdx.files.classpath("com/github/ykrasik/jaci/cli/libgdx/default_cli.cfg"));
}
private void decorateApplication(LibGdxCli cli) {
final Application currentApplication = Objects.requireNonNull(Gdx.app, "Gdx.app is null?! This should not be called before onCreate()!");
if (currentApplication instanceof ApplicationLoggingDecorator) {
throw new IllegalStateException("Gdx.app is already decorated for logging!");
}
Gdx.app = new ApplicationLoggingDecorator(currentApplication, cli);
}
}
}