blob: 9fa997c6b83b4bafdf05d0013bc0f293d40672b9 [file] [log] [blame]
/*
* Copyright 2003-2007 the original author or authors.
*
* 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 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
import groovy.ui.text.FindReplaceUtility
/**
* Groovy Swing console.
*
* Allows user to interactively enter and execute Groovy.
*
* @version $Id$
* @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 = 20000
// UI
SwingBuilder swing
JFrame frame
JTextPane 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
Closure beforeExecution
Closure afterExecution
static String ICON_PATH = 'groovy/ui/ConsoleIcon.png' // used by ObjectBrowser too
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())
System.setProperty("apple.laf.useScreenMenuBar", "true")
System.setProperty("com.apple.mrj.application.apple.menu.about.name", "GroovyConsole")
swing = new SwingBuilder()
def inputEditor = new ConsoleTextEditor()
swing.actions {
action(id: 'newFileAction',
name: 'New File',
closure: this.&fileNewFile,
mnemonic: 'N',
accelerator: shortcut('N')
)
action(id: 'newWindowAction',
name: 'New Window',
closure: this.&fileNewWindow,
mnemonic: 'W',
accelerator: shortcut('shift N')
)
action(id: 'openAction',
name: 'Open',
closure: this.&fileOpen,
mnemonic: 'O',
accelerator: shortcut('O')
)
action(id: 'saveAction',
name: 'Save',
closure: this.&fileSave,
mnemonic: 'S',
accelerator: shortcut('S')
)
action(id: 'saveAsAction',
name: 'Save As...',
closure: this.&fileSaveAs,
mnemonic: 'A',
)
action(inputEditor.printAction,
id: 'printAction',
name: 'Print...',
mnemonic: 'P',
accelerator: shortcut('P'))
action(id: 'exitAction',
name: 'Exit',
closure: this.&exit,
mnemonic: 'X'
)
// whether or not application exit should have an
// accellerator is debatable in usability circles
// at the very least a confirm dialog should dhow up
//accelerator: shortcut('Q')
action(inputEditor.undoAction,
id: 'undoAction',
name: 'Undo',
mnemonic: 'U',
accelerator: shortcut('Z')
)
action(inputEditor.redoAction,
id: 'redoAction',
name: 'Redo',
closure: this.&redo,
mnemonic: 'R',
accelerator: shortcut('shift Z') // is control-shift-Z or control-Y more common?
)
action(FindReplaceUtility.FIND_ACTION,
id: 'findAction',
name: 'Find/Replace...',
mnemonic: 'F',
accelerator: shortcut('F')
)
action(id: 'cutAction',
name: 'Cut',
closure: this.&cut,
mnemonic: 't',
accelerator: shortcut('X')
)
action(id: 'copyAction',
name: 'Copy',
closure: this.&copy,
mnemonic: 'C',
accelerator: shortcut('C')
)
action(id: 'pasteAction',
name: 'Paste',
closure: this.&paste,
mnemonic: 'P',
accelerator: shortcut('V')
)
action(id: 'selectAllAction',
name: 'Select All',
closure: this.&selectAll,
mnemonic: 'A',
accelerator: shortcut('A')
)
action(id: 'historyPrevAction',
name: 'Previous',
closure: this.&historyPrev,
mnemonic: 'P',
accelerator: shortcut(KeyEvent.VK_COMMA)
)
action(id: 'historyNextAction',
name: 'Next',
closure: this.&historyNext,
mnemonic: 'N',
accelerator: shortcut(KeyEvent.VK_PERIOD)
)
action(id: 'clearOutputAction',
name: 'Clear Output',
closure: this.&clearOutput,
mnemonic: 'l',
accelerator: shortcut('W')
)
action(id: 'runAction',
name: 'Run',
closure: this.&runScript,
mnemonic: 'R',
keyStroke: 'ctrl ENTER', // does this need to be shortcutted or explicitly ctrl?
accelerator: shortcut('R')
)
action(id: 'inspectLastAction',
name: 'Inspect Last',
closure: this.&inspectLast,
mnemonic: 'I',
accelerator: shortcut('I')
)
action(id: 'inspectVariablesAction',
name: 'Inspect Variables',
closure: this.&inspectVariables,
mnemonic: 'V',
accelerator: shortcut('J')
)
action(id: 'captureStdOutAction',
name: 'Capture Standard Output',
closure: this.&captureStdOut,
mnemonic: 'C'
)
action(id: 'largerFontAction',
name: 'Larger Font',
closure: this.&largerFont,
mnemonic: 'L',
accelerator: shortcut('shift L')
)
action(id: 'smallerFontAction',
name: 'Smaller Font',
closure: this.&smallerFont,
mnemonic: 'S',
accelerator: shortcut('shift S')
)
action(id: 'aboutAction',
name: 'About',
closure: this.&showAbout,
mnemonic: 'A'
)
action(id: 'interruptAction',
name: 'Interrupt',
closure: this.&confirmRunInterrupt
)
}
frame = swing.frame(
title: 'GroovyConsole',
location: [100,100],
size: [500,400],
defaultCloseOperation: WindowConstants.DO_NOTHING_ON_CLOSE
) {
menuBar {
menu(text: 'File', mnemonic: 'F') {
menuItem(newFileAction)
menuItem(newWindowAction)
menuItem(openAction)
separator()
menuItem(saveAction)
menuItem(saveAsAction)
separator()
menuItem(printAction)
separator()
menuItem(exitAction)
}
menu(text: 'Edit', mnemonic: 'E') {
menuItem(undoAction)
menuItem(redoAction)
separator()
menuItem(cutAction)
menuItem(copyAction)
menuItem(pasteAction)
separator()
//menuItem(findAction)
//separator()
menuItem(selectAllAction)
}
menu(text: 'View', mnemonic: 'V') {
menuItem(clearOutputAction)
separator()
menuItem(largerFontAction)
menuItem(smallerFontAction)
separator()
checkBoxMenuItem(captureStdOutAction, selected: captureStdOut)
}
menu(text: 'History', mnemonic: 'I') {
menuItem(historyPrevAction)
menuItem(historyNextAction)
}
menu(text: 'Script', mnemonic: 'S') {
menuItem(runAction)
separator()
menuItem(inspectLastAction)
menuItem(inspectVariablesAction)
}
menu(text: 'Help', mnemonic: 'H') {
menuItem(aboutAction)
}
}
borderLayout()
splitPane(id: 'splitPane', resizeWeight: 0.50F,
orientation: JSplitPane.VERTICAL_SPLIT, constraints: BorderLayout.CENTER)
{
widget(widget: inputEditor)
scrollPane {
textPane(id: 'outputArea',
editable: false,
background: new Color(255,255,218)
)
}
}
label(id: 'status',
text: 'Welcome to the Groovy.',
constraints: BorderLayout.SOUTH,
border: BorderFactory.createLoweredBevelBorder()
)
} // end of frame
inputArea = inputEditor.textEditor
// attach ctrl-enter to input area
swing.widget(inputArea) {
action(runAction)
}
outputArea = swing.outputArea
addStylesToDocument(outputArea)
statusLabel = swing.status
runWaitDialog = swing.dialog(title: 'Groovy executing',
owner: frame,
modal: true
) {
vbox(border: BorderFactory.createEmptyBorder(6, 6, 6, 6)) {
label(text: "Groovy is now executing. Please wait.", alignmentX: 0.5f)
vstrut()
button(interruptAction,
margin: new Insets(10, 20, 10, 20),
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()
// add icon
def icon = new ImageIcon(getClass().classLoader.getResource(ICON_PATH))
frame.iconImage = icon.image
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
}
}
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) {
return fileSaveAs(evt)
} else {
scriptFile.write(inputArea.text)
setDirty(false);
return true
}
}
// Save file - return false if user cancelled save
boolean fileSaveAs(EventObject evt = 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++}"
if(beforeExecution) {
beforeExecution()
}
def result = shell.evaluate(record.textToRun, name);
if(afterExecution) {
afterExecution()
}
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"
}
}
void invokeTextAction(evt, closure) {
def source = evt.getSource()
if (source != null) {
closure(inputArea)
}
}
void cut(EventObject evt = null) {
invokeTextAction(evt, { source -> source.cut() })
}
void copy(EventObject evt = null) {
invokeTextAction(evt, { source -> source.copy() })
}
void paste(EventObject evt = null) {
invokeTextAction(evt, { source -> source.paste() })
}
void selectAll(EventObject evt = null) {
invokeTextAction(evt, { source -> source.selectAll() })
}
}
/** 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
}
}