blob: 550715e1b5ce49d9da7687a8fefe6fd84d973d09 [file] [log] [blame]
package groovy.ui
import groovy.swing.SwingBuilder
import groovy.inspect.swingui.ObjectBrowser
import java.awt.BorderLayout
import java.awt.EventQueue
import java.awt.Color
import java.awt.Font
import java.awt.Insets
import java.awt.Toolkit
import java.awt.event.KeyEvent
import java.io.PrintWriter
import java.io.StringWriter
import java.util.EventObject
import javax.swing.*
import javax.swing.text.*
import javax.swing.event.*
import org.codehaus.groovy.runtime.InvokerHelper
/**
* Groovy Swing console.
*
* Allows user to interactively enter and execute Groovy.
*
* @author Danno Ferrin
* @author Dierk Koenig, changed Layout, included Selection sensitivity, included ObjectBrowser
* @author Alan Green more features: history, System.out capture, bind result to _
*/
class Console implements CaretListener {
// Whether or not std output should be captured to the console
def captureStdOut = true
// Maximum size of history
int maxHistory = 10
// Maximum number of characters to show on console at any time
int maxOutputChars = 10000
// UI
SwingBuilder swing
JFrame frame
JTextArea inputArea
JTextPane outputArea
JLabel statusLabel
JDialog runWaitDialog
// Styles for output area
Style promptStyle;
Style commandStyle;
Style outputStyle;
Style resultStyle;
// Internal history
List history = []
int historyIndex = 1 // valid values are 0..history.length()
// Current editor state
boolean dirty
int textSelectionStart // keep track of selections in inputArea
int textSelectionEnd
def scriptFile
// Running scripts
GroovyShell shell
int scriptNameCounter = 0
def systemOutInterceptor
def runThread = null
static void main(args) {
def console = new Console()
console.run()
}
Console() {
shell = new GroovyShell()
}
Console(Binding binding) {
shell = new GroovyShell(binding)
}
Console(ClassLoader parent, Binding binding) {
shell = new GroovyShell(parent,binding)
}
void run() {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
// if menu modifier is two keys we are out of luck as the javadocs
// indicates it returns "Control+Shift" instead of "Control Shift"
def menuModifier = KeyEvent.getKeyModifiersText(
Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()).toLowerCase() + ' '
swing = new SwingBuilder()
frame = swing.frame(
title:'GroovyConsole',
location:[100,100],
size:[500,400],
defaultCloseOperation:javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE) {
def newFileAction = action(
name:'New File', closure: this.&fileNewFile, mnemonic: 'N',
accelerator: menuModifier + 'Q'
)
def newWindowAction = action(
name:'New Window', closure: this.&fileNewWindow, mnemonic: 'W'
)
def openAction = action(
name:'Open', closure: this.&fileOpen, mnemonic: 'O', accelerator: menuModifier + 'O'
)
def saveAction = action(
name:'Save', closure: this.&fileSave, mnemonic: 'S', accelerator: menuModifier + 'S'
)
def exitAction = action(
name:'Exit', closure: this.&exit, mnemonic: 'x', accelerator: 'alt F4'
)
def historyPrevAction = action(
name:'Previous', closure: this.&historyPrev, mnemonic: 'P', accelerator: 'ctrl P'
)
def historyNextAction = action(
name: 'Next', closure: this.&historyNext, mnemonic: 'N', accelerator: 'ctrl N'
)
def clearOutputAction = action(
name:'Clear Output', closure: this.&clearOutput, mnemonic: 'l', keyStroke: 'ctrl W',
accelerator: 'ctrl W'
)
def runAction = action(
name:'Run', closure: this.&runScript, mnemonic: 'R', keyStroke: 'ctrl ENTER',
accelerator: 'ctrl R'
)
def inspectLastAction = action(
name:'Inspect Last', closure: this.&inspectLast, mnemonic: 'I', keyStroke: 'ctrl I',
accelerator: 'ctrl I'
)
def inspectVariablesAction = action(
name:'Inspect Variables', closure: this.&inspectVariables, mnemonic: 'V', keyStroke: 'ctrl J',
accelerator: 'ctrl J'
)
def captureStdOutAction = action(
name:'Capture Standard Output', closure: this.&captureStdOut, mnemonic: 'C'
)
def largerFontAction = action(
name:'Larger Font', closure: this.&largerFont, mnemonic: 'L', keyStroke: 'alt shift L',
accelerator: 'alt shift L'
)
def smallerFontAction = action(
name:'Smaller Font', closure: this.&smallerFont, mnemonic: 'S', keyStroke: 'alt shift S',
accelerator: 'alt shift S'
)
def aboutAction = action(name:'About', closure: this.&showAbout, mnemonic: 'A')
menuBar {
menu(text:'File', mnemonic: 'F') {
menuItem() { action(newFileAction) }
menuItem() { action(newWindowAction) }
menuItem() { action(openAction) }
separator()
menuItem() { action(saveAction) }
separator()
menuItem() { action(exitAction) }
}
menu(text:'Edit', mnemonic: 'E') {
menuItem() { action(historyNextAction) }
menuItem() { action(historyPrevAction) }
separator()
menuItem() { action(clearOutputAction) }
}
menu(text:'Actions', mnemonic: 'A') {
menuItem() { action(runAction) }
menuItem() { action(inspectLastAction) }
menuItem() { action(inspectVariablesAction) }
separator()
checkBoxMenuItem(selected: captureStdOut) { action(captureStdOutAction) }
separator()
menuItem() { action(largerFontAction) }
menuItem() { action(smallerFontAction) }
}
menu(text:'Help', mnemonic: 'H') {
menuItem() { action(aboutAction) }
}
}
borderLayout()
splitPane(id:'splitPane', resizeWeight:0.50F,
orientation:JSplitPane.VERTICAL_SPLIT, constraints: BorderLayout.CENTER)
{
scrollPane {
inputArea = textArea(
margin: new Insets(3,3,3,3), font: new Font('Monospaced',Font.PLAIN,12)
) { action(runAction) }
}
scrollPane {
outputArea = textPane(editable:false, background: new Color(255,255,218))
addStylesToDocument(outputArea)
}
}
statusLabel = label(id:'status', text: 'Welcome to the Groovy.', constraints: BorderLayout.SOUTH,
border: BorderFactory.createLoweredBevelBorder())
} // end of frame
runWaitDialog = swing.dialog(title: 'Groovy executing', owner: frame, modal: true) {
//boxLayout(axis: BoxLayout.Y_AXIS) // todo mittie: dialog.setLayout -> dialog.contentPane.setLayout()
label(text: "Groovy is now executing. Please wait.",
border: BorderFactory.createEmptyBorder(10, 10, 10, 10), alignmentX: 0.5f)
button(action: action(name: 'Interrupt', closure: this.&confirmRunInterrupt),
border: BorderFactory.createEmptyBorder(10, 10, 10, 10), alignmentX: 0.5f)
} // end of runWaitDialog
// add listeners
frame.windowClosing = this.&exit
inputArea.addCaretListener(this)
inputArea.document.undoableEditHappened = { setDirty(true) }
systemOutInterceptor = new SystemOutputInterceptor(this.&notifySystemOut)
systemOutInterceptor.start();
bindResults()
frame.show()
SwingUtilities.invokeLater({inputArea.requestFocus()});
}
void addStylesToDocument(JTextPane outputArea) {
StyledDocument doc = outputArea.getStyledDocument();
Style defStyle = StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE);
Style regular = doc.addStyle("regular", defStyle);
StyleConstants.setFontFamily(regular, "Monospaced")
promptStyle = doc.addStyle("prompt", regular)
StyleConstants.setForeground(promptStyle, Color.BLUE)
commandStyle = doc.addStyle("command", regular);
StyleConstants.setForeground(commandStyle, Color.MAGENTA)
outputStyle = regular
resultStyle = doc.addStyle("result", regular)
StyleConstants.setBackground(resultStyle, Color.BLUE)
StyleConstants.setBackground(resultStyle, Color.YELLOW)
}
void addToHistory(record) {
history.add(record)
// history.size here just retrieves method closure
if (history.size() > maxHistory) {
history.remove(0)
}
// history.size doesn't work here either
historyIndex = history.size()
}
// Append a string to the output area
void appendOutput(text, style){
def doc = outputArea.styledDocument
doc.insertString(doc.length, text, style)
// Ensure we don't have too much in console (takes too much memory)
if (doc.length > maxOutputChars) {
doc.remove(0, doc.length - maxOutputChars)
}
}
// Append a string to the output area on a new line
void appendOutputNl(text, style){
def doc = outputArea.styledDocument
def len = doc.length
if (len > 0 && doc.getText(len - 1, 1) != "\n") {
appendOutput("\n", style);
}
appendOutput(text, style)
}
// Return false if use elected to cancel
boolean askToSaveFile() {
if (scriptFile == null || !dirty) {
return true
}
switch (JOptionPane.showConfirmDialog(frame,
"Save changes to " + scriptFile.name + "?",
"GroovyConsole", JOptionPane.YES_NO_CANCEL_OPTION))
{
case JOptionPane.YES_OPTION:
return fileSave()
case JOptionPane.NO_OPTION:
return true
default:
return false
}
}
private static void beep() {
Toolkit.defaultToolkit.beep()
}
// Binds the "_" and "__" variables in the shell
void bindResults() {
shell.setVariable("_", getLastResult()) // lastResult doesn't seem to work
shell.setVariable("__", history.collect {it.result})
}
// Handles menu event
void captureStdOut(EventObject evt) {
captureStdOut = evt.source.selected
}
void caretUpdate(CaretEvent e){
textSelectionStart = Math.min(e.dot,e.mark)
textSelectionEnd = Math.max(e.dot,e.mark)
}
void clearOutput(EventObject evt = null) {
outputArea.setText('')
}
// Confirm whether to interrupt the running thread
void confirmRunInterrupt(EventObject evt) {
def rc = JOptionPane.showConfirmDialog(frame, "Attempt to interrupt script?",
"GroovyConsole", JOptionPane.YES_NO_OPTION)
if (rc == JOptionPane.YES_OPTION && runThread != null) {
runThread.interrupt()
}
}
void exit(EventObject evt = null) {
if (askToSaveFile()) {
frame.hide()
frame.dispose()
}
}
void fileNewFile(EventObject evt = null) {
if (askToSaveFile()) {
scriptFile = null
setDirty(false)
inputArea.text = ''
}
}
// Start a new window with a copy of current variables
void fileNewWindow(EventObject evt = null) {
(new Console(new Binding(new HashMap(shell.context.variables)))).run()
}
void fileOpen(EventObject evt = null) {
scriptFile = selectFilename();
if (scriptFile != null) {
inputArea.text = scriptFile.readLines().join('\n');
setDirty(false)
inputArea.caretPosition = 0
}
}
// Save file - return false if user cancelled save
boolean fileSave(EventObject evt = null) {
if (scriptFile == null) {
scriptFile = selectFilename("Save");
}
if (scriptFile != null) {
scriptFile.write(inputArea.text)
setDirty(false);
return true
} else {
return false
}
}
def finishException(Throwable t) {
statusLabel.text = 'Execution terminated with exception.'
history[-1].exception = t
appendOutputNl("Exception thrown: ", promptStyle)
appendOutput(t.toString(), resultStyle)
StringWriter sw = new StringWriter()
new PrintWriter(sw).withWriter { pw -> t.printStackTrace(pw) }
appendOutputNl("\n${sw.buffer}\n", outputStyle)
bindResults()
}
def finishNormal(Object result) {
// Take down the wait/cancel dialog
history[-1].result = result
if (result != null) {
statusLabel.text = 'Execution complete.'
appendOutputNl("Result: ", promptStyle)
appendOutput("${InvokerHelper.inspect(result)}", resultStyle)
} else {
statusLabel.text = 'Execution complete. Result was null.'
}
bindResults()
}
// Gets the last, non-null result
def getLastResult() {
// runtime bugs in here history.reverse produces odd lookup
// return history.reverse.find {it != null}
if (!history) {
return
}
for (i in (history.size() - 1)..0) {
if (history[i].result != null) {
return history[i].result
}
}
return null
}
// Allow access to shell from outside console
// (useful for configuring shell before startup)
GroovyShell getShell() {
return shell
}
void historyNext(EventObject evt = null) {
if (historyIndex < history.size()) {
historyIndex++;
setInputTextFromHistory()
} else {
statusLabel.text = "Can't go past end of history (time travel not allowed)"
beep()
}
}
void historyPrev(EventObject evt = null) {
if (historyIndex > 0) {
historyIndex--;
setInputTextFromHistory()
} else {
statusLabel.text = "Can't go past start of history"
beep()
}
}
void inspectLast(EventObject evt = null){
if (null == lastResult) {
JOptionPane.showMessageDialog(frame, "The last result is null.",
"Cannot Inspect", JOptionPane.INFORMATION_MESSAGE)
return
}
ObjectBrowser.inspect(lastResult)
}
void inspectVariables(EventObject evt = null) {
ObjectBrowser.inspect(shell.context.variables)
}
void largerFont(EventObject evt = null) {
if (inputArea.font.size > 40) return
def newFont = new Font('Monospaced', Font.PLAIN, inputArea.font.size + 2)
inputArea.font = newFont
outputArea.font = newFont
}
Boolean notifySystemOut(String str) {
if (!captureStdOut) {
// Output as normal
return true
}
// Put onto GUI
if (EventQueue.isDispatchThread()) {
appendOutput(str, outputStyle)
}
else {
SwingUtilities.invokeLater {
appendOutput(str, outputStyle)
}
}
return false
}
// actually run the
void runScript(EventObject evt = null) {
def record = new HistoryRecord( allText: inputArea.getText(),
selectionStart: textSelectionStart, selectionEnd: textSelectionEnd)
addToHistory(record)
// Print the input text
for (line in record.textToRun.tokenize("\n")) {
appendOutputNl('groovy> ', promptStyle)
appendOutput(line, commandStyle)
}
//appendOutputNl("") - with wrong number of args, causes StackOverFlowError;
appendOutputNl("\n", promptStyle)
// Kick off a new thread to do the evaluation
statusLabel.text = 'Running Script...'
// Run in separate thread, so that System.out can be captured
runThread = Thread.start {
try {
SwingUtilities.invokeLater { showRunWaitDialog() }
String name = "Script${scriptNameCounter++}"
def result = shell.evaluate(record.textToRun, name);
SwingUtilities.invokeLater { finishNormal(result) }
} catch (Throwable t) {
SwingUtilities.invokeLater { finishException(t) }
} finally {
SwingUtilities.invokeLater {
runWaitDialog.hide();
runThread = null
}
}
}
}
def selectFilename(name = "Open") {
def fc = new JFileChooser()
fc.fileSelectionMode = JFileChooser.FILES_ONLY
fc.acceptAllFileFilterUsed = true
if (fc.showDialog(frame, name) == JFileChooser.APPROVE_OPTION) {
return fc.selectedFile
} else {
return null
}
}
void setDirty(boolean newDirty) {
dirty = newDirty
updateTitle()
}
private void setInputTextFromHistory() {
if (historyIndex < history.size()) {
def record = history[historyIndex]
inputArea.text = record.allText
inputArea.selectionStart = record.selectionStart
inputArea.selectionEnd = record.selectionEnd
setDirty(true) // Should calculate dirty flag properly (hash last saved/read text in each file)
statusLabel.text = "command history ${history.size() - historyIndex}"
} else {
inputArea.text = ""
statusLabel.text = 'at end of history'
}
}
// Adds a variable to the binding
// Useful for adding variables before openning the console
void setVariable(String name, Object value) {
shell.context.setVariable(name, value)
}
void showAbout(EventObject evt = null) {
def version = InvokerHelper.getVersion()
def pane = swing.optionPane()
// work around GROOVY-1048
pane.setMessage('Welcome to the Groovy Console for evaluating Groovy scripts\nVersion ' + version)
def dialog = pane.createDialog(frame, 'About GroovyConsole')
dialog.show()
}
// Shows the 'wait' dialog
void showRunWaitDialog() {
runWaitDialog.pack()
int x = frame.x + (frame.width - runWaitDialog.width) / 2
int y = frame.y + (frame.height - runWaitDialog.height) / 2
runWaitDialog.setLocation(x, y)
runWaitDialog.show()
}
void smallerFont(EventObject evt = null){
if (inputArea.font.size < 5) return
def newFont = new Font('Monospaced', Font.PLAIN, inputArea.font.size - 2)
inputArea.font = newFont
outputArea.font = newFont
}
void updateTitle() {
if (scriptFile != null) {
frame.title = scriptFile.name + (dirty?" * ":"") + " - GroovyConsole"
} else {
frame.title = "GroovyConsole"
}
}
}
/** A single time when the user selected "run" */
class HistoryRecord {
def allText
def selectionStart
def selectionEnd
def scriptName
def result
def exception
def getTextToRun() {
if (selectionStart != selectionEnd) {
return allText[selectionStart ..< selectionEnd]
}
return allText
}
def getValue() {
return exception ? exception : result
}
}