blob: 06dd23342b52558db7ddf342af62aa3fd68aeea4 [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.inspect.swingui.ObjectBrowser
import groovy.swing.SwingBuilder
import groovy.ui.ConsoleTextEditor
import groovy.ui.SystemOutputInterceptor
import groovy.ui.text.FindReplaceUtility
import java.awt.Component
import java.awt.EventQueue
import java.awt.Font
import java.awt.Toolkit
import java.awt.event.ActionEvent
import java.util.prefs.Preferences
import javax.swing.*
import javax.swing.event.CaretEvent
import javax.swing.event.CaretListener
import javax.swing.text.Element
import javax.swing.text.Style
import org.codehaus.groovy.runtime.InvokerHelper
import org.codehaus.groovy.runtime.StackTraceUtils
/**
* 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 {
private prefs = Preferences.userNodeForPackage(Console)
// Whether or not std output should be captured to the console
boolean captureStdOut = prefs.getBoolean('captureStdOut', true)
boolean fullStackTraces = prefs.getBoolean('fullStackTraces',
Boolean.valueOf(System.getProperty("groovy.full.stacktrace", "false")))
Action fullStackTracesAction
boolean showToolbar = prefs.getBoolean('showToolbar', true)
Component toolbar
Action showToolbarAction
// 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
ConsoleTextEditor inputEditor
JTextPane inputArea
JTextPane outputArea
JLabel statusLabel
JDialog runWaitDialog
JLabel rowNumAndColNum
// row info
Element rootElement
int cursorPos
int rowNum
int colNum
// 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()
HistoryRecord pendingRecord = new HistoryRecord( allText: "", selectionStart: 0, selectionEnd: 0)
Action prevHistoryAction
Action nextHistoryAction
// Current editor state
boolean dirty
Action saveAction
int textSelectionStart // keep track of selections in inputArea
int textSelectionEnd
def scriptFile
File currentFileChooserDir = new File(Preferences.userNodeForPackage(Console).get('currentFileChooserDir', '.'))
// Running scripts
GroovyShell shell
int scriptNameCounter = 0
SystemOutputInterceptor systemOutInterceptor
def runThread = null
Closure beforeExecution
Closure afterExecution
public static String ICON_PATH = '/groovy/ui/ConsoleIcon.png' // used by ObjectBrowser too
static void main(args) {
// allow the full stack traces to bubble up to the root logger
java.util.logging.Logger.getLogger(StackTraceUtils.STACK_LOG_NAME).useParentHandlers = true
//when starting via main set the look and feel to system
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
def console = new Console()
console.run()
}
Console() {
this(new Binding())
}
Console(Binding binding) {
this(null, binding)
}
Console(ClassLoader parent, Binding binding) {
shell = new GroovyShell(parent,binding)
try {
System.setProperty("groovy.full.stacktrace",
Boolean.toString(Boolean.valueOf(System.getProperty("groovy.full.stacktrace", "false"))))
} catch (SecurityException se) {
fullStackTracesAction.enabled = false;
}
}
void run() {
swing = new SwingBuilder()
// tweak what the stack traces filter out to be fairly broad
System.setProperty("groovy.sanitized.stacktraces", """org.codehaus.groovy.runtime.
org.codehaus.groovy.
groovy.lang.
gjdk.groovy.lang.
sun.
java.lang.reflect.
java.lang.Thread
groovy.ui.Console""")
// add controller to the swingBuilder bindings
swing.controller = this
// create the actions
swing.build(ConsoleActions)
// create the view
swing.build(ConsoleView)
bindResults()
// stitch some actions togeather
swing.bind(source:swing.inputEditor.undoAction, sourceProperty:'enabled', target:swing.undoAction, targetProperty:'enabled')
swing.bind(source:swing.inputEditor.redoAction, sourceProperty:'enabled', target:swing.redoAction, targetProperty:'enabled')
swing.consoleFrame.pack()
swing.consoleFrame.show()
installInterceptor()
swing.doLater inputArea.&requestFocus
}
public void installInterceptor() {
systemOutInterceptor = new SystemOutputInterceptor(this.&notifySystemOut)
systemOutInterceptor.start()
}
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()
updateHistoryActions()
}
// 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
prefs.putBoolean('captureStdOut', captureStdOut)
}
void fullStackTraces(EventObject evt) {
fullStackTraces = evt.source.selected
System.setProperty("groovy.full.stacktrace",
Boolean.toString(fullStackTraces))
prefs.putBoolean('fullStackTraces', fullStackTraces)
}
void showToolbar(EventObject evt) {
showToolbar = evt.source.selected
prefs.putBoolean('showToolbar', showToolbar)
toolbar.visible = showToolbar
}
void caretUpdate(CaretEvent e){
textSelectionStart = Math.min(e.dot,e.mark)
textSelectionEnd = Math.max(e.dot,e.mark)
setRowNumAndColNum()
}
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()
FindReplaceUtility.dispose()
}
systemOutInterceptor.stop()
}
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) {
Console consoleController = new Console(
new Binding(
new HashMap(shell.context.variables)))
consoleController.systemOutInterceptor = systemOutInterceptor
SwingBuilder swing = new SwingBuilder()
swing.controller = consoleController
swing.build(ConsoleActions)
swing.build(ConsoleView)
installInterceptor()
swing.consoleFrame.pack()
swing.consoleFrame.show()
}
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 -> StackTraceUtils.deepSanitize(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()) {
setInputTextFromHistory(historyIndex + 1)
} else {
statusLabel.text = "Can't go past end of history (time travel not allowed)"
beep()
}
}
void historyPrev(EventObject evt = null) {
if (historyIndex > 0) {
setInputTextFromHistory(historyIndex - 1)
} 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
// don't worry, the fonts won't be changed to monospaced face, the styles will only derive from this
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 script
void runScript(EventObject evt = null) {
runScriptImpl(false)
}
void runSelectedScript(EventObject evt = null) {
runScriptImpl(true)
}
private void runScriptImpl(boolean selected) {
def endLine = System.getProperty('line.separator')
def record = new HistoryRecord( allText: inputArea.getText().replaceAll(endLine, '\n'),
selectionStart: textSelectionStart, selectionEnd: textSelectionEnd)
addToHistory(record)
pendingRecord = new HistoryRecord(allText:'', selectionStart:0, selectionEnd:0)
// Print the input text
for (line in record.getTextToRun(selected).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.getTextToRun(selected), 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(currentFileChooserDir)
fc.fileSelectionMode = JFileChooser.FILES_ONLY
fc.acceptAllFileFilterUsed = true
if (fc.showDialog(frame, name) == JFileChooser.APPROVE_OPTION) {
currentFileChooserDir = fc.currentDirectory
Preferences.userNodeForPackage(Console).put('currentFileChooserDir', currentFileChooserDir.path)
return fc.selectedFile
} else {
return null
}
}
void setDirty(boolean newDirty) {
//TODO when @BoundProperty is live, this should be handled via listeners
dirty = newDirty
saveAction.enabled = newDirty
updateTitle()
}
private void setInputTextFromHistory(newIndex) {
def endLine = System.getProperty('line.separator')
if (historyIndex >= history.size()) {
pendingRecord = new HistoryRecord( allText: inputArea.getText().replaceAll(endLine, '\n'),
selectionStart: textSelectionStart, selectionEnd: textSelectionEnd)
}
historyIndex = newIndex
def record
if (historyIndex < history.size()) {
record = history[historyIndex]
statusLabel.text = "command history ${history.size() - historyIndex}"
} else {
record = pendingRecord
statusLabel.text = 'at end of history'
}
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)
updateHistoryActions()
}
private void updateHistoryActions() {
nextHistoryAction.enabled = historyIndex < history.size()
prevHistoryAction.enabled = historyIndex > 0
}
// 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()
}
void find(EventObject evt = null) {
FindReplaceUtility.showDialog()
}
void findNext(EventObject evt = null) {
FindReplaceUtility.FIND_ACTION.actionPerformed(evt)
}
void findPrevious(EventObject evt = null) {
def reverseEvt = new ActionEvent(
evt.getSource(), evt.getID(),
evt.getActionCommand(), evt.getWhen(),
ActionEvent.SHIFT_MASK) //reverse
FindReplaceUtility.FIND_ACTION.actionPerformed(reverseEvt)
}
void replace(EventObject evt = null) {
FindReplaceUtility.showDialog(true)
}
// 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
// don't worry, the fonts won't be changed to monospaced face, the styles will only derive from this
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() })
}
void setRowNumAndColNum() {
cursorPos = inputArea.getCaretPosition()
rowNum = rootElement.getElementIndex(cursorPos) + 1
def rowElement = rootElement.getElement(rowNum - 1)
colNum = cursorPos - rowElement.getStartOffset() + 1
rowNumAndColNum.setText("$rowNum:$colNum")
}
void print(EventObject evt = null) {
inputEditor.printAction.actionPerformed(evt)
}
void undo(EventObject evt = null) {
inputEditor.undoAction.actionPerformed(evt)
}
void redo(EventObject evt = null) {
inputEditor.redoAction.actionPerformed(evt)
}
}