/*
* License: source-license.txt
* If this code is used independently, copy the license here.
*/
package wombat.gui.frames;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.GridLayout;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.text.*;
import net.infonode.docking.*;
import net.infonode.docking.View; // explicit because of import conflict with javax.swing
import net.infonode.docking.util.*;
import wombat.Wombat;
import wombat.gui.icons.*;
import wombat.gui.text.*;
import wombat.scheme.*;
import wombat.util.*;
import wombat.util.errors.*;
import wombat.util.files.*;
/**
* Main frame for the program. This does pretty much everything.
*
* TODO: Break this apart a little better. It's overlarge.
*/
public class MainFrame extends JFrame {
private static final long serialVersionUID = 2574330949324570164L;
// Self-reference for singleton access.
static MainFrame Me;
static boolean MeBuilding;
// Display components.
RootWindow Root;
Petite Petite;
StringViewMap ViewMap;
// Toolbar.
JToolBar ToolBar;
JButton UpdateButton;
public JButton ToolBarRun;
public JButton ToolBarStop;
public JLabel RowColumn;
// Unique code components.
NonEditableTextArea History;
NonEditableTextArea Debug;
REPLTextArea REPL;
public NonEditableTextArea DebugLogs;
/**
* Singleton access.
* @return The main frame.
*/
public static MainFrame Singleton() {
if (Me == null && !MeBuilding) {
MeBuilding = true;
Me = new MainFrame();
MeBuilding = false;
}
return Me;
}
/**
* Don't directly create this, use me().
* Use this method to set it up though.
*/
private MainFrame() {
// Set frame options.
setTitle("Wombat - Build " + Wombat.VERSION);
setSize(Options.DisplayWidth, Options.DisplayHeight);
setLocation(Options.DisplayLeft, Options.DisplayTop);
setLayout(new BorderLayout(5, 5));
setDefaultCloseOperation(EXIT_ON_CLOSE);
try {
setIconImage(IconManager.icon("Wombat.png").getImage());
} catch(NullPointerException ex) {
}
// Wait for the program to end.
final MainFrame me = this;
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
Options.DisplayTop = Math.max(0, e.getWindow().getLocation().y);
Options.DisplayLeft = Math.max(0, e.getWindow().getLocation().x);
Options.DisplayWidth = Math.max(400, e.getWindow().getWidth());
Options.DisplayHeight = Math.max(400, e.getWindow().getHeight());
Options.save();
DocumentManager.CloseAll();
stopAllThreads(true, false);
me.dispose();
}
});
// Set up the menus using the above definitions.
MenuManager.Singleton(this);
setJMenuBar(MenuManager.getMenu());
// Create a display for any open documents.
Root = DockingUtil.createRootWindow(new ViewMap(), true);
TabWindow documents = new TabWindow();
ViewMap = new StringViewMap();
DocumentManager.init(this, Root, ViewMap, documents);
DocumentManager.New();
// Create displays for a split REPL.
History = new NonEditableTextArea();
REPL = new REPLTextArea();
ViewMap.addView("REPL - Execute", new View("REPL - Execute", null, REPL));
ViewMap.addView("REPL - History", new View("REPL - History", null, History));
SplitWindow replSplit = new SplitWindow(true, 0.5f, ViewMap.getView("REPL - Execute"), ViewMap.getView("REPL - History"));
ViewMap.getView("REPL - Execute").getWindowProperties().setCloseEnabled(false);
ViewMap.getView("REPL - History").getWindowProperties().setCloseEnabled(false);
ViewMap.getView("REPL - Execute").getWindowProperties().setUndockEnabled(false);
ViewMap.getView("REPL - History").getWindowProperties().setUndockEnabled(false);
// Create the error/debug/display views.
JPanel debugPanel = new JPanel();
debugPanel.setLayout(new GridLayout(2, 1));
Debug = new NonEditableTextArea();
debugPanel.add(Debug);
DebugLogs = new NonEditableTextArea();
debugPanel.add(DebugLogs);
ViewMap.addView("Debug", new View("Debug", null, debugPanel));
// Listen and report new error messages.
ErrorManager.addErrorListener(new ErrorListener() {
@Override
public void logError(String msg) {
Debug.append(msg + "\n");
}
});
// Put everything together into the actual dockable display.
SplitWindow fullSplit = new SplitWindow(false, 0.6f, documents, replSplit);
Root.setWindow(fullSplit);
add(Root);
// Add a toolbar.
ToolBar = new JToolBar();
ToolBarRun = new JButton(MenuManager.itemForName("Run").getAction());
ToolBarStop = new JButton(MenuManager.itemForName("Stop").getAction());
ToolBar.setFloatable(false);
for (Action a : new Action[]{
MenuManager.itemForName("New").getAction(),
MenuManager.itemForName("Open").getAction(),
MenuManager.itemForName("Save").getAction(),
MenuManager.itemForName("Close").getAction(),
})
ToolBar.add(new JButton(a));
ToolBar.addSeparator();
for (Action a : new Action[]{
MenuManager.itemForName("Connect").getAction(),
MenuManager.itemForName("Upload").getAction(),
})
ToolBar.add(new JButton(a));
ToolBar.addSeparator();
for (Action a : new Action[]{
MenuManager.itemForName("Cut").getAction(),
MenuManager.itemForName("Copy").getAction(),
MenuManager.itemForName("Paste").getAction(),
MenuManager.itemForName("Undo").getAction(),
MenuManager.itemForName("Redo").getAction(),
MenuManager.itemForName("Find/Replace").getAction(),
})
ToolBar.add(new JButton(a));
ToolBar.addSeparator();
ToolBar.add(ToolBarRun);
ToolBar.add(ToolBarStop);
for (Action a : new Action[]{
MenuManager.itemForName("Format").getAction(),
MenuManager.itemForName("Reset").getAction(),
})
ToolBar.add(new JButton(a));
add(ToolBar, BorderLayout.PAGE_START);
ToolBar.setVisible(Options.DisplayToolbar);
// Disable items by default.
setRunning(false);
// Remove text on toolbar buttons.
for (Component c : ToolBar.getComponents())
if (c instanceof JButton)
((JButton) c).setText("");
// Add a tool to show the current row and column.
RowColumn = new JLabel("row:column");
ToolBar.addSeparator();
ToolBar.add(RowColumn);
// Set up options specifically for OS X.
if (OS.IsOSX) {
try {
com.apple.eawt.Application app = com.apple.eawt.Application.getApplication();
app.setDockIconImage(IconManager.icon("Wombat.png").getImage());
} catch (Exception e) {
System.err.println("Error setting up OSX specific features:");
e.printStackTrace();
}
}
// Finally, intialize petite.
initPetite();
}
static Border REPLOriginalBorder;
static Border REPLRunningBorder;
/**
* Set if the system should report itself as running.
* @param running True to display run and enable stop, false for the opposite.
*/
public void setRunning(boolean toRunning) {
MenuManager.itemForName("Run").setEnabled(!toRunning);
ToolBarRun.setEnabled(!toRunning);
MenuManager.itemForName("Stop").setEnabled(toRunning);
ToolBarStop.setEnabled(toRunning);
if (REPLOriginalBorder == null) {
REPLOriginalBorder = REPL.code.getBorder();
REPLRunningBorder = BorderFactory.createBevelBorder(javax.swing.border.BevelBorder.RAISED, Color.RED, Color.RED);
}
REPL.code.setBorder(toRunning ? REPLRunningBorder : REPLOriginalBorder);
}
/**
* Start up Petite or restart it on a reset.
*/
private void initPetite() {
// Connect to Petite
// This thread also takes the output from Petite and relays it to the GUI
try {
Petite = new Petite();
// Add a listener to reset the running state when Petite reports it is ready.
Petite.addPetiteListener(new PetiteListener() {
@Override public void onReady() {
setRunning(false);
}
@Override public void onOutput(String output) {
if (History != null && output != null) {
if (Options.LambdaMode)
output = output.replace("lambda", "\u03BB");
if (Options.GreekMode)
for (String[] pair : Options.GreekModeCharacters)
output = output.replace(pair[1], pair[0]);
History.append(output);
History.goToEnd();
}
}
@Override public void onError(Exception ex) {}
@Override public void onStop() {}
@Override public void onReset() {
History.setText(">>> Environment reset <<<\n");
}
});
}
// This will come up if we cannot connect to Petite. This is a pretty critical error.
catch (Exception e1) {
JOptionPane.showMessageDialog(
this,
"Unable to start Petite process:\n"
+ e1.getMessage()
+ "\n\nPlease report this error to the developers.",
"Error starting Petite",
JOptionPane.ERROR_MESSAGE);
ErrorManager.logError(e1.getMessage());
e1.printStackTrace();
}
}
/**
* Run a line of Scheme code.
* @param command The command to run.
*/
public void doCommand(String command) {
// Don't allow multiple things to run at once.
setRunning(true);
// Clean up the input and don't run empty content.
final String cmd = command.trim();
if (cmd.length() == 0)
return;
// Add the prompt and indent to match it.
History.append("\n~ " + cmd.replace("\n", "\n ") + "\n");
History.goToEnd();
// Actually execute the command (async, the petite thread above will capture any output).
Petite.sendCommand(cmd);
}
/**
* Update the display.
*/
public boolean updateDisplay() {
boolean reloaded = true;
for (SchemeTextArea ss : new SchemeTextArea[]{History, Debug, REPL}) {
try {
((SchemeDocument) ss.code.getDocument()).processChangedLines(0, ss.getText().length());
ss.updateUI();
} catch (BadLocationException e) {
reloaded = false;
ErrorManager.logError("Unable to format view: " + e.getMessage());
}
}
return reloaded;
}
/**
* Focus the REPL.
*/
public void focusREPL() {
REPL.code.requestFocusInWindow();
}
/**
* Set the toolbar's display mode.
* @param displayToolbar If the toolbar should be visible.
*/
public void toggleToolbar(boolean displayToolbar) {
ToolBar.setVisible(displayToolbar);
}
/**
* Reset Kawa.
*/
public void resetScheme() {
Petite.reset();
}
/**
* Show the debug view.
*/
public void showDebug() {
View view = ViewMap.getView("Debug");
if (!view.isShowing()) {
if (view.getSize().width == 0 || view.getSize().height == 0)
view.setSize(500, 500);
FloatingWindow win = Root.createFloatingWindow(getLocation(), view.getSize(), view);
win.getTopLevelAncestor().setVisible(true);
}
}
/**
* Show the given view.
* @param which Which display we are writing to.
*/
public void showView(String which) {
View view = ViewMap.getView(which);
if (!view.isShowing()) {
if (view.getSize().width == 0 || view.getSize().height == 0)
view.setSize(500, 500);
FloatingWindow win = Root.createFloatingWindow(getLocation(), view.getSize(), view);
win.getTopLevelAncestor().setVisible(true);
}
}
/**
* Stop all running worker threads.
*/
public void stopAllThreads(final boolean silent, final boolean andRestart) {
if (silent || JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(
this,
"Stopping will reset the current Petite process.\nAre you sure you want to do this?",
"Confirm Stop", JOptionPane.YES_NO_OPTION)) {
// If we want to restart Petite, add a listener to do just that.
if (andRestart) {
Petite.addPetiteListener(new PetiteListener() {
@Override public void onStop() {
// Create the new Petite.
initPetite();
// Add add a listener to reset the display state when the new Petite is ready.
Petite.addPetiteListener(new PetiteListener() {
@Override public void onReady() {
// Tell the user it worked.
History.setText(">>> Execution halted <<<<\n\n");
History.goToEnd();
// Don't stack these up.
Petite.removePetiteListener(this);
// Reenable running code.
setRunning(false);
}
@Override public void onOutput(String output) {}
@Override public void onError(Exception ex) {}
@Override public void onStop() {};
@Override public void onReset() {};
});
}
@Override public void onReset() {}
@Override public void onReady() {}
@Override public void onOutput(String output) {}
@Override public void onError(Exception ex) {}
});
}
// Now, actually stop Petite.
Petite.stop();
}
}
/**
* Set if the update dialog should be visible or not.
* @param b The new value.
*/
public void setUpdateVisible(boolean b) {
UpdateButton.setVisible(b);
}
/**
* Save recent command history.
* @return The current history, separated by null bytes.
*/
public static String getHistory() {
return Me.REPL.getHistory(Options.SavedHistoryCount);
}
}