/*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later
* version. You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.aitools.programd.interfaces.graphical;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.URL;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import org.aitools.programd.Core;
import org.aitools.programd.graph.Graphmapper;
import org.aitools.programd.interfaces.Console;
import org.aitools.programd.interfaces.shell.BotListCommand;
import org.aitools.programd.interfaces.shell.HelpCommand;
import org.aitools.programd.interfaces.shell.ListBotFilesCommand;
import org.aitools.programd.interfaces.shell.NoSuchCommandException;
import org.aitools.programd.interfaces.shell.Shell;
import org.aitools.util.resource.Filesystem;
import org.aitools.util.resource.URLTools;
import org.aitools.util.runtime.DeveloperError;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
/**
* Provides a very simple GUI console for the bot.
*
* @author <a href="mailto:noel@aitools.org">Noel Bush</a>
*/
public class GUIConsole extends JPanel {
/**
* Extends OutputStream to direct all output to the display textarea.
*/
public class ConsoleDisplayStream extends OutputStream {
private boolean paused = false;
protected GUIConsole _parent;
/**
* Creates a new ConsoleDisplayStream.
*
* @param parent the GUIConsole parent to use
*/
public ConsoleDisplayStream(GUIConsole parent) {
super();
this._parent = parent;
}
protected void togglePause() {
this.paused = !this.paused;
}
/**
* @see java.io.OutputStream#write(byte[], int, int)
*/
@Override
public void write(byte[] b, int off, int len) {
while (this.paused) {
try {
Thread.sleep(500);
}
catch (InterruptedException e) {
Logger.getLogger("programd").warn("GUIConsole was interrupted; shell will not run anymore.");
}
}
GUIConsole.this.display.append(new String(b, off, len).intern());
GUIConsole.this.display.setCaretPosition(GUIConsole.this.display.getText().length());
}
/**
* @see java.io.OutputStream#write(int)
*/
@Override
public void write(int b) {
while (this.paused) {
try {
Thread.sleep(500);
}
catch (InterruptedException e) {
Logger.getLogger("programd").warn("GUIConsole was interrupted; shell will not run anymore.");
}
}
GUIConsole.this.display.append(String.valueOf((char) b));
GUIConsole.this.display.setCaretPosition(GUIConsole.this.display.getText().length());
}
}
/**
* Extends InputStream to suit our purposes in handling user input for the GUIConsole.
*
* @author <a href="mailto:noel@aitools.org">Noel Bush</a>
*/
public class ConsoleInputStream extends InputStream {
byte[] content = new byte[] {};
private int mark = 0;
/**
* Creates a new ConsoleInputStream object.
*/
public ConsoleInputStream() {
// Nothing to do.
}
/**
* @see java.io.InputStream#available()
*/
@Override
public int available() {
return this.content.length - this.mark - 1;
}
/**
* @see java.io.InputStream#markSupported()
*/
@Override
public boolean markSupported() {
return false;
}
/**
* @see java.io.InputStream#read()
*/
@Override
public int read() {
while (this.mark >= this.content.length) {
try {
Thread.sleep(100);
}
catch (InterruptedException e) {
return -1;
}
}
if (this.mark < this.content.length) {
return this.content[this.mark++];
}
// (otherwise...)
return -1;
}
/**
* @see java.io.InputStream#read(byte[], int, int)
*/
@Override
public int read(byte b[], int off, int len) {
while (this.mark >= this.content.length) {
try {
Thread.sleep(100);
}
catch (InterruptedException e) {
return -1;
}
}
if (b == null) {
throw new NullPointerException("Cannot read from console.");
}
else if (off < 0 || off > b.length || len < 0 || off + len > b.length || off + len < 0) {
throw new IndexOutOfBoundsException();
}
else if (len == 0) {
return 0;
}
else if (this.content.length == 0) {
return -1;
}
int i = 1;
b[off] = this.content[this.mark++];
for (; i < len && i < this.content.length; i++) {
b[off + i] = this.content[this.mark++];
}
return i;
}
/**
* Receives the given string.
*
* @param string the string to receive
*/
public void receive(String string) {
this.content = (string + '\n').getBytes();
this.mark = 0;
}
}
/**
* Extends OutputStream to direct all output to the prompt field.
*/
public class ConsolePromptStream extends OutputStream {
protected GUIConsole _parent;
/**
* @param parent
*/
public ConsolePromptStream(GUIConsole parent) {
super();
this._parent = parent;
}
/**
* @see java.io.OutputStream#write(byte[], int, int)
*/
@Override
public void write(byte[] b, int off, int len) {
GUIConsole.this.inputPanel.setPrompt(new String(b, off, len).intern());
}
/**
* @see java.io.OutputStream#write(int)
*/
@Override
public void write(int b) {
GUIConsole.this.inputPanel.setPrompt(String.valueOf((char) b));
}
}
class InputPanel extends JPanel {
/**
* Displays a new user input and clears the input box.
*/
public class InputSender implements ActionListener {
/**
* @see ActionListener#actionPerformed(java.awt.event.ActionEvent)
*/
@Override
public void actionPerformed(ActionEvent ae) {
String inputText = ae.getActionCommand();
GUIConsole.this.display.append(InputPanel.this.prompt.getText() + inputText + LINE_SEPARATOR);
GUIConsole.this.inStream.receive(inputText);
InputPanel.this.input.setText(null);
}
}
/**
*
*/
private static final long serialVersionUID = 1L;
/** Where the console prompt will be displayed. */
protected JLabel prompt;
/** The console input field. */
protected JTextField input;
/** The enter button. */
protected JButton enter;
protected GUIConsole _parent;
/**
* Creates a new InputPanel.
*
* @param parent the parent GUIConsole to use
*/
public InputPanel(GUIConsole parent) {
this._parent = parent;
this.setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
this.prompt = new JLabel();
this.prompt.setFont(new Font("Monospaced", Font.PLAIN, 11));
this.prompt.setForeground(Color.black);
this.prompt.setBackground(Color.white);
this.prompt.setHorizontalAlignment(SwingConstants.LEFT);
this.prompt.setAlignmentY(Component.CENTER_ALIGNMENT);
this.input = new JTextField();
this.input.setFont(new Font("Monospaced", Font.PLAIN, 11));
this.input.setForeground(Color.black);
this.input.setMinimumSize(new Dimension(50, 20));
this.input.setPreferredSize(new Dimension(200, 20));
this.input.setMaximumSize(new Dimension(Short.MAX_VALUE, 20));
this.input.setHorizontalAlignment(SwingConstants.LEFT);
this.input.setAlignmentY(Component.CENTER_ALIGNMENT);
this.input.addActionListener(new InputPanel.InputSender());
this.enter = new JButton("Enter");
// this.enter.setFont(new Font("Sans-serif", Font.PLAIN, 10));
this.enter.setForeground(Color.black);
this.enter.setMinimumSize(new Dimension(70, 20));
this.enter.setPreferredSize(new Dimension(70, 20));
this.enter.setMaximumSize(new Dimension(70, 20));
this.enter.addActionListener(new InputSender());
this.enter.setAlignmentY(Component.CENTER_ALIGNMENT);
this.add(this.prompt);
this.add(this.input);
this.add(this.enter);
}
/**
* Sets the components of this panel to the given state.
*
* @param enabled whether or not the panel should be enabled
*/
@Override
public void setEnabled(boolean enabled) {
this.input.setEnabled(enabled);
this.enter.setEnabled(enabled);
}
/**
* Sets the prompt.
*
* @param text the text of the prompt
*/
public void setPrompt(String text) {
this.prompt.setText(text);
this.prompt.revalidate();
this.input.requestFocus();
}
}
/**
*
*/
private static final long serialVersionUID = 1L;
/** The core associated with this console. */
private Core _core;
/** The underlying Console. */
private Console console;
/** The Shell that will (may) be used by the underlying console. */
protected Shell _shell;
/** Where console messages will be displayed. */
protected JTextArea display;
/** Contains the input prompt and field. */
protected InputPanel inputPanel;
protected ConsoleDisplayStream outDisplay = new ConsoleDisplayStream(this);
protected ConsoleDisplayStream errDisplay = new ConsoleDisplayStream(this);
private JFrame frame;
/** The stream to which console stdout will be directed. */
private PrintStream outStream = new PrintStream(this.outDisplay);
/** The stream to which console stdout will be directed. */
private PrintStream errStream = new PrintStream(this.errDisplay);
/** The stream to which console prompt will be directed. */
private PrintStream promptStream = new PrintStream(new ConsolePromptStream(this));
/** The stream which will receive console input. */
protected ConsoleInputStream inStream = new ConsoleInputStream();
/** For convenience, the system line separator. */
protected static final String LINE_SEPARATOR = System.getProperty("line.separator", "\n");
private static final Object[] HELP_MESSAGE = { "Simple Console for Program D" };
private static JMenuBar menuBar;
private static String LOGO_PATH = "resources/icons/logo.jpg";
private ImageIcon logo;
private static String ICON_PATH = "resources/icons/icon.jpg";
private ImageIcon icon;
/**
* Constructs a new simple console gui with a new shell.
*/
public GUIConsole() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
}
catch (ClassNotFoundException e) {
throw new DeveloperError("The LookAndFeel class could not be found.", e);
}
catch (InstantiationException e) {
throw new DeveloperError("A new instance of the LookAndFeel class couldn't be created.", e);
}
catch (IllegalAccessException e) {
throw new DeveloperError("The requested LookAndFeel class or initializer isn't accessible;", e);
}
catch (UnsupportedLookAndFeelException e) {
throw new DeveloperError("The requested LookAndFeel is not supported.", e);
}
URL logoURL;
try {
logoURL = URLTools.createValidURL(LOGO_PATH, Filesystem.getWorkingDirectory());
}
catch (FileNotFoundException e) {
throw new DeveloperError(String.format("Logo is missing from \"%s\"!", LOGO_PATH), e);
}
if (logoURL != null) {
this.logo = new ImageIcon(logoURL);
}
else {
throw new NullPointerException(String.format("Logo is missing from \"%s\"!", LOGO_PATH));
}
URL iconURL;
try {
iconURL = URLTools.createValidURL(ICON_PATH, Filesystem.getWorkingDirectory());
}
catch (FileNotFoundException e) {
throw new DeveloperError(String.format("Icon is missing from \"%s\"!", ICON_PATH), e);
}
if (iconURL != null) {
this.icon = new ImageIcon(iconURL);
}
else {
throw new NullPointerException(String.format("Icon is missing from \"%s\"!", ICON_PATH));
}
this.console = new Console(this.outStream, this.errStream);
this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
this.display = new JTextArea(30, 150);
this.display.setFont(new Font("Monospaced", Font.PLAIN, 11));
this.display.setLineWrap(true);
this.display.setWrapStyleWord(true);
this.display.setTabSize(4);
this.display.setForeground(Color.black);
this.display.setEditable(false);
JScrollPane scrollPane = new JScrollPane(this.display);
scrollPane.setAlignmentY(Component.CENTER_ALIGNMENT);
this.inputPanel = new InputPanel(this);
this.inputPanel.setEnabled(false);
this.add(scrollPane);
this.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
this.add(Box.createRigidArea(new Dimension(0, 5)));
this.add(this.inputPanel);
menuBar = new JMenuBar();
JMenu fileMenu = new JMenu("File");
// fileMenu.setFont(new Font("Sans-serif", Font.PLAIN, 12));
fileMenu.setMnemonic(KeyEvent.VK_F);
JMenuItem loadAIMLURL = new JMenuItem("Load AIML from URL...");
// loadAIMLURL.setFont(new Font("Sans-serif", Font.PLAIN, 12));
loadAIMLURL.setMnemonic(KeyEvent.VK_U);
loadAIMLURL.addActionListener(new ActionEventIgnoringActionListener() {
@Override
public void actionPerformed() {
GUIConsole.this.loadAIMLURLBox();
}
});
JMenuItem loadAIMLFilePath = new JMenuItem("Load AIML from file path...");
// loadAIMLFilePath.setFont(new Font("Sans-serif", Font.PLAIN, 12));
loadAIMLFilePath.setMnemonic(KeyEvent.VK_P);
loadAIMLFilePath.addActionListener(new ActionEventIgnoringActionListener() {
@Override
public void actionPerformed() {
GUIConsole.this.loadAIMLFilePathChooser();
}
});
JMenuItem shutdown = new JMenuItem("Shutdown Program D");
// shutdown.setFont(new Font("Sans-serif", Font.PLAIN, 12));
shutdown.setMnemonic(KeyEvent.VK_X);
shutdown.addActionListener(new ActionEventIgnoringActionListener() {
@Override
public void actionPerformed() {
GUIConsole.this.shutdown();
}
});
JMenuItem quit = new JMenuItem("Quit");
// quit.setFont(new Font("Sans-serif", Font.PLAIN, 12));
quit.setMnemonic(KeyEvent.VK_X);
quit.addActionListener(new ActionEventIgnoringActionListener() {
@Override
public void actionPerformed() {
GUIConsole.this.quit();
}
});
fileMenu.add(loadAIMLURL);
fileMenu.add(loadAIMLFilePath);
fileMenu.addSeparator();
fileMenu.add(shutdown);
fileMenu.addSeparator();
fileMenu.add(quit);
// Create the Actions menu.
JMenu actionsMenu = new JMenu("Actions");
// actionsMenu.setFont(new Font("Sans-serif", Font.PLAIN, 12));
actionsMenu.setMnemonic(KeyEvent.VK_A);
JCheckBoxMenuItem pause = new JCheckBoxMenuItem("Pause Console");
// pause.setFont(new Font("Sans-serif", Font.PLAIN, 12));
pause.setMnemonic(KeyEvent.VK_P);
pause.addActionListener(new ActionEventIgnoringActionListener() {
@Override
public void actionPerformed() {
GUIConsole.this.outDisplay.togglePause();
GUIConsole.this.errDisplay.togglePause();
}
});
JMenuItem talkToBot = new JMenuItem("Talk to bot...");
// talkToBot.setFont(new Font("Sans-serif", Font.PLAIN, 12));
talkToBot.setMnemonic(KeyEvent.VK_B);
talkToBot.addActionListener(new ActionEventIgnoringActionListener() {
@Override
public void actionPerformed() {
GUIConsole.this.chooseBot();
}
});
JMenuItem botFiles = new JMenuItem("List bot files");
// botFiles.setFont(new Font("Sans-serif", Font.PLAIN, 12));
botFiles.setMnemonic(KeyEvent.VK_F);
botFiles.addActionListener(new ActionEventIgnoringActionListener() {
@Override
public void actionPerformed() {
try {
GUIConsole.this._shell.processCommandLine(ListBotFilesCommand.COMMAND_STRING);
}
catch (NoSuchCommandException e) {
throw new DeveloperError("GUIConsole sent an invalid command string to the shell!", e);
}
}
});
JMenuItem listBots = new JMenuItem("List bots");
// listBots.setFont(new Font("Sans-serif", Font.PLAIN, 12));
listBots.setMnemonic(KeyEvent.VK_L);
listBots.addActionListener(new ActionEventIgnoringActionListener() {
@Override
public void actionPerformed() {
try {
GUIConsole.this._shell.processCommandLine(BotListCommand.COMMAND_STRING);
}
catch (NoSuchCommandException e) {
throw new DeveloperError("GUIConsole sent an invalid command string to the shell!", e);
}
}
});
actionsMenu.add(pause);
actionsMenu.addSeparator();
actionsMenu.add(talkToBot);
actionsMenu.add(listBots);
actionsMenu.add(botFiles);
actionsMenu.addSeparator();
// Create the Help menu.
JMenu helpMenu = new JMenu("Help");
// helpMenu.setFont(new Font("Sans-serif", Font.PLAIN, 12));
helpMenu.setMnemonic(KeyEvent.VK_H);
JMenuItem shellHelp = new JMenuItem("Shell Help...");
// shellHelp.setFont(new Font("Sans-serif", Font.PLAIN, 12));
shellHelp.setMnemonic(KeyEvent.VK_H);
shellHelp.addActionListener(new ActionEventIgnoringActionListener() {
@Override
public void actionPerformed() {
try {
GUIConsole.this._shell.processCommandLine(HelpCommand.COMMAND_STRING);
}
catch (NoSuchCommandException e) {
throw new DeveloperError("GUIConsole sent an invalid command string to the shell!", e);
}
}
});
JMenuItem about = new JMenuItem("About Simple GUI Console...");
// about.setFont(new Font("Sans-serif", Font.PLAIN, 12));
about.setMnemonic(KeyEvent.VK_A);
about.addActionListener(new ActionEventIgnoringActionListener() {
@Override
public void actionPerformed() {
GUIConsole.this.showAboutBox();
}
});
helpMenu.add(about);
helpMenu.add(shellHelp);
// Add menus to the menu bar.
menuBar.add(fileMenu);
menuBar.add(actionsMenu);
menuBar.add(helpMenu);
this.frame = new JFrame();
this.frame.setTitle("Program D Simple GUI Console");
this.frame.getContentPane().add(this);
this.frame.setJMenuBar(menuBar);
this.frame.setIconImage(this.icon.getImage());
this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.frame.pack();
this.frame.setLocation(50, 50);
this.frame.setVisible(true);
}
/**
* Attaches the GUIConsole to the given Core.
*
* @param core the Core to which to attach
*/
public void attachTo(Core core) {
this._core = core;
this.console.attachTo(this._core);
if (this._core.getSettings().useShell()) {
this._shell = new Shell(this.inStream, this.outStream, this.errStream, this.promptStream);
this.console.addShell(this._shell, core);
}
else {
this.outStream.println("Interactive shell disabled. Awaiting manual shut down.");
}
}
protected void chooseBot() {
String[] botIDs = this._core.getBots().keySet().toArray(new String[] {});
ListDialog.initialize(this.frame, botIDs, "Choose a bot", "Choose the bot with whom you want to talk.");
String choice = ListDialog.showDialog(null, this._shell.getCurrentBotID());
if (choice != null) {
this._shell.switchToBot(choice);
}
}
protected void loadAIMLFilePathChooser() {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle("Choose AIML File");
int action = chooser.showDialog(this, "Choose");
if (action == JFileChooser.APPROVE_OPTION) {
File chosen = chooser.getSelectedFile();
String newPath = null;
try {
newPath = chosen.getCanonicalPath();
}
catch (IOException e) {
return;
}
int categories = this._core.getGraphmapper().getCategoryCount();
Graphmapper graphmapper = this._core.getGraphmapper();
this._core.load(URLTools.contextualize(Filesystem.getWorkingDirectory(), newPath), this._shell.getCurrentBotID());
Logger.getLogger("programd").log(Level.INFO,
graphmapper.getCategoryCount() - categories + " categories loaded from \"" + newPath + "\".");
}
}
protected void loadAIMLURLBox() {
Object response = JOptionPane.showInputDialog(null, "Enter the URL from which to load.", "Load AIML from URL",
JOptionPane.PLAIN_MESSAGE, null, null, null);
if (response == null) {
return;
}
Graphmapper graphmapper = this._core.getGraphmapper();
int categories = graphmapper.getCategoryCount();
this._core.load(URLTools.contextualize(Filesystem.getWorkingDirectory(), (String) response),
this._shell.getCurrentBotID());
Logger.getLogger("programd").log(Level.INFO,
graphmapper.getCategoryCount() - categories + " categories loaded from \"" + (String) response + "\".");
}
protected void quit() {
this.frame.dispose();
}
protected void showAboutBox() {
JOptionPane.showMessageDialog(null, HELP_MESSAGE, "About", JOptionPane.INFORMATION_MESSAGE, this.logo);
}
protected void shutdown() {
if (this._core != null) {
this._core.shutdown();
}
// Let the user exit, in case termination was abnormal or messages are
// otherwise interesting.
}
/**
* Starts the attached Core.
*/
public void start() {
// this.core.start();
}
/**
* Enables the input panel and starts the underlying console's shell.
*/
public void startShell() {
this.inputPanel.setEnabled(true);
this.console.startShell();
}
}