| 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.¬ifySystemOut) |
| 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 |
| } |
| } |