GROOVY-2919 - Console as Applet support
[groovy.git] / src / main / groovy / ui / Console.groovy
blob3d362c6eae9f80235dd8aabc289cfd90e49c6ada
1 /*
2 * Copyright 2003-2007 the original author or authors.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package groovy.ui
18 import groovy.inspect.swingui.ObjectBrowser
19 import groovy.swing.SwingBuilder
20 import groovy.ui.ConsoleTextEditor
21 import groovy.ui.SystemOutputInterceptor
22 import groovy.ui.text.FindReplaceUtility
23 import java.awt.Component
24 import java.awt.EventQueue
25 import java.awt.Font
26 import java.awt.Toolkit
27 import java.awt.event.ActionEvent
28 import java.util.prefs.Preferences
29 import javax.swing.*
30 import javax.swing.event.CaretEvent
31 import javax.swing.event.CaretListener
32 import javax.swing.text.Element
33 import javax.swing.text.Style
34 import org.codehaus.groovy.runtime.InvokerHelper
35 import org.codehaus.groovy.runtime.StackTraceUtils
37 /**
38 * Groovy Swing console.
40 * Allows user to interactively enter and execute Groovy.
42 * @version $Id$
43 * @author Danno Ferrin
44 * @author Dierk Koenig, changed Layout, included Selection sensitivity, included ObjectBrowser
45 * @author Alan Green more features: history, System.out capture, bind result to _
47 class Console implements CaretListener {
49 static private prefs = Preferences.userNodeForPackage(Console)
51 // Whether or not std output should be captured to the console
52 static boolean captureStdOut = prefs.getBoolean('captureStdOut', true)
53 static consoleControllers = []
55 boolean fullStackTraces = prefs.getBoolean('fullStackTraces',
56 Boolean.valueOf(System.getProperty("groovy.full.stacktrace", "false")))
57 Action fullStackTracesAction
59 boolean showToolbar = prefs.getBoolean('showToolbar', true)
60 Component toolbar
61 Action showToolbarAction
63 // Maximum size of history
64 int maxHistory = 10
66 // Maximum number of characters to show on console at any time
67 int maxOutputChars = 20000
69 // UI
70 SwingBuilder swing
71 RootPaneContainer frame
72 ConsoleTextEditor inputEditor
73 JTextPane inputArea
74 JTextPane outputArea
75 JLabel statusLabel
76 JDialog runWaitDialog
77 JLabel rowNumAndColNum
79 // row info
80 Element rootElement
81 int cursorPos
82 int rowNum
83 int colNum
85 // Styles for output area
86 Style promptStyle
87 Style commandStyle
88 Style outputStyle
89 Style resultStyle
91 // Internal history
92 List history = []
93 int historyIndex = 1 // valid values are 0..history.length()
94 HistoryRecord pendingRecord = new HistoryRecord( allText: "", selectionStart: 0, selectionEnd: 0)
95 Action prevHistoryAction
96 Action nextHistoryAction
98 // Current editor state
99 boolean dirty
100 Action saveAction
101 int textSelectionStart // keep track of selections in inputArea
102 int textSelectionEnd
103 def scriptFile
104 File currentFileChooserDir = new File(Preferences.userNodeForPackage(Console).get('currentFileChooserDir', '.'))
105 File currentClasspathJarDir = new File(Preferences.userNodeForPackage(Console).get('currentClasspathJarDir', '.'))
106 File currentClasspathDir = new File(Preferences.userNodeForPackage(Console).get('currentClasspathDir', '.'))
108 // Running scripts
109 GroovyShell shell
110 int scriptNameCounter = 0
111 SystemOutputInterceptor systemOutInterceptor
112 Thread runThread = null
113 Closure beforeExecution
114 Closure afterExecution
116 public static String ICON_PATH = '/groovy/ui/ConsoleIcon.png' // used by ObjectBrowser too
118 static void main(args) {
119 // allow the full stack traces to bubble up to the root logger
120 java.util.logging.Logger.getLogger(StackTraceUtils.STACK_LOG_NAME).useParentHandlers = true
122 //when starting via main set the look and feel to system
123 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
125 def console = new Console(Console.class.classLoader?.getRootLoader())
126 console.run()
127 if (args.length == 1) console.loadScriptFile(args[0] as File)
130 Console() {
131 this(new Binding())
134 Console(Binding binding) {
135 this(null, binding)
138 Console(ClassLoader parent) {
139 this(parent, new Binding())
142 Console(ClassLoader parent, Binding binding) {
143 newScript(parent, binding);
144 try {
145 System.setProperty("groovy.full.stacktrace",
146 Boolean.toString(Boolean.valueOf(System.getProperty("groovy.full.stacktrace", "false"))))
147 } catch (SecurityException se) {
148 fullStackTracesAction.enabled = false;
150 consoleControllers += this
153 void newScript(ClassLoader parent, Binding binding) {
154 shell = new GroovyShell(parent, binding)
158 void run() {
159 run([
160 rootContainerDelegate:{
161 frame(
162 title: 'GroovyConsole',
163 //location: [100,100], // in groovy 2.0 use platform default location
164 iconImage: imageIcon("/groovy/ui/ConsoleIcon.png").image,
165 defaultCloseOperation: JFrame.DO_NOTHING_ON_CLOSE,
167 try {
168 current.locationByPlatform = true
169 } catch (Exception e) {
170 current.location = [100, 100] // for 1.4 compatibility
172 containingWindows += current
175 menuBarDelegate: {arg->
176 current.JMenuBar = build(arg)}
180 void run(JApplet applet) {
181 run([
182 rootContainerDelegate:{
183 containingWindows += SwingUtilities.getRoot(applet.getParent())
184 applet
186 menuBarDelegate: {arg->
187 current.JMenuBar = build(arg)}
191 void run(Map defaults) {
193 swing = new SwingBuilder()
194 defaults.each{k, v -> swing[k] = v}
196 // tweak what the stack traces filter out to be fairly broad
197 System.setProperty("groovy.sanitized.stacktraces", """org.codehaus.groovy.runtime.
198 org.codehaus.groovy.
199 groovy.lang.
200 gjdk.groovy.lang.
201 sun.
202 java.lang.reflect.
203 java.lang.Thread
204 groovy.ui.Console""")
207 // add controller to the swingBuilder bindings
208 swing.controller = this
210 // create the actions
211 swing.build(ConsoleActions)
213 // create the view
214 swing.build(ConsoleView)
216 bindResults()
218 // stitch some actions togeather
219 swing.bind(source:swing.inputEditor.undoAction, sourceProperty:'enabled', target:swing.undoAction, targetProperty:'enabled')
220 swing.bind(source:swing.inputEditor.redoAction, sourceProperty:'enabled', target:swing.redoAction, targetProperty:'enabled')
222 if (swing.consoleFrame instanceof java.awt.Window) {
223 swing.consoleFrame.pack()
224 swing.consoleFrame.show()
226 installInterceptor()
227 swing.doLater inputArea.&requestFocus
231 public void installInterceptor() {
232 systemOutInterceptor = new SystemOutputInterceptor(this.&notifySystemOut)
233 systemOutInterceptor.start()
236 void addToHistory(record) {
237 history.add(record)
238 // history.size here just retrieves method closure
239 if (history.size() > maxHistory) {
240 history.remove(0)
242 // history.size doesn't work here either
243 historyIndex = history.size()
244 updateHistoryActions()
247 // Append a string to the output area
248 void appendOutput(text, style){
249 def doc = outputArea.styledDocument
250 doc.insertString(doc.length, text, style)
252 // Ensure we don't have too much in console (takes too much memory)
253 if (doc.length > maxOutputChars) {
254 doc.remove(0, doc.length - maxOutputChars)
258 // Append a string to the output area on a new line
259 void appendOutputNl(text, style){
260 def doc = outputArea.styledDocument
261 def len = doc.length
262 if (len > 0 && doc.getText(len - 1, 1) != "\n") {
263 appendOutput("\n", style)
265 appendOutput(text, style)
268 // Return false if use elected to cancel
269 boolean askToSaveFile() {
270 if (scriptFile == null || !dirty) {
271 return true
273 switch (JOptionPane.showConfirmDialog(frame,
274 "Save changes to " + scriptFile.name + "?",
275 "GroovyConsole", JOptionPane.YES_NO_CANCEL_OPTION))
277 case JOptionPane.YES_OPTION:
278 return fileSave()
279 case JOptionPane.NO_OPTION:
280 return true
281 default:
282 return false
286 void beep() {
287 Toolkit.defaultToolkit.beep()
290 // Binds the "_" and "__" variables in the shell
291 void bindResults() {
292 shell.setVariable("_", getLastResult()) // lastResult doesn't seem to work
293 shell.setVariable("__", history.collect {it.result})
296 // Handles menu event
297 static void captureStdOut(EventObject evt) {
298 captureStdOut = evt.source.selected
299 prefs.putBoolean('captureStdOut', captureStdOut)
302 void fullStackTraces(EventObject evt) {
303 fullStackTraces = evt.source.selected
304 System.setProperty("groovy.full.stacktrace",
305 Boolean.toString(fullStackTraces))
306 prefs.putBoolean('fullStackTraces', fullStackTraces)
309 void showToolbar(EventObject evt) {
310 showToolbar = evt.source.selected
311 prefs.putBoolean('showToolbar', showToolbar)
312 toolbar.visible = showToolbar
315 void caretUpdate(CaretEvent e){
316 textSelectionStart = Math.min(e.dot,e.mark)
317 textSelectionEnd = Math.max(e.dot,e.mark)
319 setRowNumAndColNum()
322 void clearOutput(EventObject evt = null) {
323 outputArea.setText('')
326 // Confirm whether to interrupt the running thread
327 void confirmRunInterrupt(EventObject evt) {
328 def rc = JOptionPane.showConfirmDialog(frame, "Attempt to interrupt script?",
329 "GroovyConsole", JOptionPane.YES_NO_OPTION)
330 if (rc == JOptionPane.YES_OPTION) {
331 runThread?.interrupt()
335 void exit(EventObject evt = null) {
336 if (askToSaveFile()) {
337 if (frame instanceof java.awt.Window) {
338 frame.hide()
339 frame.dispose()
341 FindReplaceUtility.dispose()
342 consoleControllers.remove(this)
343 if (!consoleControllers) {
344 systemOutInterceptor.stop()
350 void fileNewFile(EventObject evt = null) {
351 if (askToSaveFile()) {
352 scriptFile = null
353 setDirty(false)
354 inputArea.text = ''
358 // Start a new window with a copy of current variables
359 void fileNewWindow(EventObject evt = null) {
360 Console consoleController = new Console(
361 new Binding(
362 new HashMap(shell.context.variables)))
363 consoleController.systemOutInterceptor = systemOutInterceptor
364 SwingBuilder swing = new SwingBuilder()
365 swing.controller = consoleController
366 swing.build(ConsoleActions)
367 swing.build(ConsoleView)
368 installInterceptor()
369 swing.consoleFrame.pack()
370 swing.consoleFrame.show()
371 swing.doLater swing.inputArea.&requestFocus
374 void fileOpen(EventObject evt = null) {
375 def scriptName = selectFilename()
376 if (scriptName != null) {
377 loadScriptFile(scriptName)
381 void loadScriptFile(File file) {
382 swing.edt {
383 inputArea.editable = false
385 swing.doOutside {
386 try {
387 consoleText = file.readLines().join('\n')
388 scriptFile = file
389 swing.edt {
390 updateTitle()
391 inputArea.text = consoleText
392 setDirty(false)
393 inputArea.caretPosition = 0
395 } finally {
396 swing.edt { inputArea.editable = true }
401 // Save file - return false if user cancelled save
402 boolean fileSave(EventObject evt = null) {
403 if (scriptFile == null) {
404 return fileSaveAs(evt)
405 } else {
406 scriptFile.write(inputArea.text)
407 setDirty(false)
408 return true
412 // Save file - return false if user cancelled save
413 boolean fileSaveAs(EventObject evt = null) {
414 scriptFile = selectFilename("Save")
415 if (scriptFile != null) {
416 scriptFile.write(inputArea.text)
417 setDirty(false)
418 return true
419 } else {
420 return false
424 def finishException(Throwable t) {
425 statusLabel.text = 'Execution terminated with exception.'
426 history[-1].exception = t
428 appendOutputNl("Exception thrown: ", promptStyle)
429 appendOutput(t.toString(), resultStyle)
431 StringWriter sw = new StringWriter()
432 new PrintWriter(sw).withWriter { pw -> StackTraceUtils.deepSanitize(t).printStackTrace(pw) }
434 appendOutputNl("\n${sw.buffer}\n", outputStyle)
435 bindResults()
438 def finishNormal(Object result) {
439 // Take down the wait/cancel dialog
440 history[-1].result = result
441 if (result != null) {
442 statusLabel.text = 'Execution complete.'
443 appendOutputNl("Result: ", promptStyle)
444 appendOutput("${InvokerHelper.inspect(result)}", resultStyle)
445 } else {
446 statusLabel.text = 'Execution complete. Result was null.'
448 bindResults()
451 // Gets the last, non-null result
452 def getLastResult() {
453 // runtime bugs in here history.reverse produces odd lookup
454 // return history.reverse.find {it != null}
455 if (!history) {
456 return
458 for (i in (history.size() - 1)..0) {
459 if (history[i].result != null) {
460 return history[i].result
463 return null
466 // Allow access to shell from outside console
467 // (useful for configuring shell before startup)
468 GroovyShell getShell() {
469 return shell
472 void historyNext(EventObject evt = null) {
473 if (historyIndex < history.size()) {
474 setInputTextFromHistory(historyIndex + 1)
475 } else {
476 statusLabel.text = "Can't go past end of history (time travel not allowed)"
477 beep()
481 void historyPrev(EventObject evt = null) {
482 if (historyIndex > 0) {
483 setInputTextFromHistory(historyIndex - 1)
484 } else {
485 statusLabel.text = "Can't go past start of history"
486 beep()
490 void inspectLast(EventObject evt = null){
491 if (null == lastResult) {
492 JOptionPane.showMessageDialog(frame, "The last result is null.",
493 "Cannot Inspect", JOptionPane.INFORMATION_MESSAGE)
494 return
496 ObjectBrowser.inspect(lastResult)
499 void inspectVariables(EventObject evt = null) {
500 ObjectBrowser.inspect(shell.context.variables)
503 void largerFont(EventObject evt = null) {
504 if (inputArea.font.size > 40) return
505 // don't worry, the fonts won't be changed to monospaced face, the styles will only derive from this
506 def newFont = new Font('Monospaced', Font.PLAIN, inputArea.font.size + 2)
507 inputArea.font = newFont
508 outputArea.font = newFont
511 static boolean notifySystemOut(String str) {
512 if (!captureStdOut) {
513 // Output as normal
514 return true
517 // Put onto GUI
518 if (EventQueue.isDispatchThread()) {
519 consoleControllers.each {it.appendOutput(str, it.outputStyle)}
521 else {
522 SwingUtilities.invokeLater {
523 consoleControllers.each {it.appendOutput(str, it.outputStyle)}
526 return false
529 // actually run the script
531 void runScript(EventObject evt = null) {
532 runScriptImpl(false)
535 void runSelectedScript(EventObject evt = null) {
536 runScriptImpl(true)
539 void addClasspathJar(EventObject evt = null) {
540 def fc = new JFileChooser(currentClasspathJarDir)
541 fc.fileSelectionMode = JFileChooser.FILES_ONLY
542 fc.acceptAllFileFilterUsed = true
543 if (fc.showDialog(frame, "Add") == JFileChooser.APPROVE_OPTION) {
544 currentClasspathJarDir = fc.currentDirectory
545 Preferences.userNodeForPackage(Console).put('currentClasspathJarDir', currentClasspathJarDir.path)
546 shell.getClassLoader().addURL(fc.selectedFile.toURL())
550 void addClasspathDir(EventObject evt = null) {
551 def fc = new JFileChooser(currentClasspathDir)
552 fc.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
553 fc.acceptAllFileFilterUsed = true
554 if (fc.showDialog(frame, "Add") == JFileChooser.APPROVE_OPTION) {
555 currentClasspathDir = fc.currentDirectory
556 Preferences.userNodeForPackage(Console).put('currentClasspathDir', currentClasspathDir.path)
557 shell.getClassLoader().addURL(fc.selectedFile.toURL())
561 void clearContext(EventObject evt = null) {
562 newScript(null, new Binding())
565 private void runScriptImpl(boolean selected) {
566 def endLine = System.getProperty('line.separator')
567 def record = new HistoryRecord( allText: inputArea.getText().replaceAll(endLine, '\n'),
568 selectionStart: textSelectionStart, selectionEnd: textSelectionEnd)
569 addToHistory(record)
570 pendingRecord = new HistoryRecord(allText:'', selectionStart:0, selectionEnd:0)
572 // Print the input text
573 for (line in record.getTextToRun(selected).tokenize("\n")) {
574 appendOutputNl('groovy> ', promptStyle)
575 appendOutput(line, commandStyle)
578 //appendOutputNl("") - with wrong number of args, causes StackOverFlowError
579 appendOutputNl("\n", promptStyle)
581 // Kick off a new thread to do the evaluation
582 statusLabel.text = 'Running Script...'
584 // Run in a thread outside of EDT, this method is usually called inside the EDT
585 runThread = Thread.start {
586 try {
587 SwingUtilities.invokeLater { showRunWaitDialog() }
588 String name = "Script${scriptNameCounter++}"
589 if(beforeExecution) {
590 beforeExecution()
592 def result = shell.evaluate(record.getTextToRun(selected), name)
593 if(afterExecution) {
594 afterExecution()
596 SwingUtilities.invokeLater { finishNormal(result) }
597 } catch (Throwable t) {
598 SwingUtilities.invokeLater { finishException(t) }
599 } finally {
600 runThread = null
603 // Use a watchdog thread to close waiting dialog
604 // apparently invokeLater paired with show/hide does not insure
605 // ordering or atomic execution, likely because of native AWT issues
606 Thread.start {
607 while (!(runWaitDialog?.visible)) {
608 sleep(10)
610 while (runThread?.alive) {
611 try {
612 runThread?.join(100)
613 } catch (InterruptedException ie) {
614 // we got interrupted, just loop again.
617 runWaitDialog.hide()
621 def selectFilename(name = "Open") {
622 def fc = new JFileChooser(currentFileChooserDir)
623 fc.fileSelectionMode = JFileChooser.FILES_ONLY
624 fc.acceptAllFileFilterUsed = true
625 if (fc.showDialog(frame, name) == JFileChooser.APPROVE_OPTION) {
626 currentFileChooserDir = fc.currentDirectory
627 Preferences.userNodeForPackage(Console).put('currentFileChooserDir', currentFileChooserDir.path)
628 return fc.selectedFile
629 } else {
630 return null
634 void setDirty(boolean newDirty) {
635 //TODO when @BoundProperty is live, this should be handled via listeners
636 dirty = newDirty
637 saveAction.enabled = newDirty
638 updateTitle()
641 private void setInputTextFromHistory(newIndex) {
642 def endLine = System.getProperty('line.separator')
643 if (historyIndex >= history.size()) {
644 pendingRecord = new HistoryRecord( allText: inputArea.getText().replaceAll(endLine, '\n'),
645 selectionStart: textSelectionStart, selectionEnd: textSelectionEnd)
647 historyIndex = newIndex
648 def record
649 if (historyIndex < history.size()) {
650 record = history[historyIndex]
651 statusLabel.text = "command history ${history.size() - historyIndex}"
652 } else {
653 record = pendingRecord
654 statusLabel.text = 'at end of history'
656 inputArea.text = record.allText
657 inputArea.selectionStart = record.selectionStart
658 inputArea.selectionEnd = record.selectionEnd
659 setDirty(true) // Should calculate dirty flag properly (hash last saved/read text in each file)
660 updateHistoryActions()
663 private void updateHistoryActions() {
664 nextHistoryAction.enabled = historyIndex < history.size()
665 prevHistoryAction.enabled = historyIndex > 0
668 // Adds a variable to the binding
669 // Useful for adding variables before openning the console
670 void setVariable(String name, Object value) {
671 shell.context.setVariable(name, value)
674 void showAbout(EventObject evt = null) {
675 def version = InvokerHelper.getVersion()
676 def pane = swing.optionPane()
677 // work around GROOVY-1048
678 pane.setMessage('Welcome to the Groovy Console for evaluating Groovy scripts\nVersion ' + version)
679 def dialog = pane.createDialog(frame, 'About GroovyConsole')
680 dialog.show()
683 void find(EventObject evt = null) {
684 FindReplaceUtility.showDialog()
687 void findNext(EventObject evt = null) {
688 FindReplaceUtility.FIND_ACTION.actionPerformed(evt)
691 void findPrevious(EventObject evt = null) {
692 def reverseEvt = new ActionEvent(
693 evt.getSource(), evt.getID(),
694 evt.getActionCommand(), evt.getWhen(),
695 ActionEvent.SHIFT_MASK) //reverse
696 FindReplaceUtility.FIND_ACTION.actionPerformed(reverseEvt)
699 void replace(EventObject evt = null) {
700 FindReplaceUtility.showDialog(true)
704 // Shows the 'wait' dialog
705 void showRunWaitDialog() {
706 runWaitDialog.pack()
707 runWaitDialog.setLocationRelativeTo(frame)
708 runWaitDialog.show()
711 void smallerFont(EventObject evt = null){
712 if (inputArea.font.size < 5) return
713 // don't worry, the fonts won't be changed to monospaced face, the styles will only derive from this
714 def newFont = new Font('Monospaced', Font.PLAIN, inputArea.font.size - 2)
715 inputArea.font = newFont
716 outputArea.font = newFont
719 void updateTitle() {
720 if (frame.properties.containsKey('title')) {
721 if (scriptFile != null) {
722 frame.title = scriptFile.name + (dirty?" * ":"") + " - GroovyConsole"
723 } else {
724 frame.title = "GroovyConsole"
729 void invokeTextAction(evt, closure) {
730 def source = evt.getSource()
731 if (source != null) {
732 closure(inputArea)
736 void cut(EventObject evt = null) {
737 invokeTextAction(evt, { source -> source.cut() })
740 void copy(EventObject evt = null) {
741 invokeTextAction(evt, { source -> source.copy() })
744 void paste(EventObject evt = null) {
745 invokeTextAction(evt, { source -> source.paste() })
748 void selectAll(EventObject evt = null) {
749 invokeTextAction(evt, { source -> source.selectAll() })
752 void setRowNumAndColNum() {
753 cursorPos = inputArea.getCaretPosition()
754 rowNum = rootElement.getElementIndex(cursorPos) + 1
756 def rowElement = rootElement.getElement(rowNum - 1)
757 colNum = cursorPos - rowElement.getStartOffset() + 1
759 rowNumAndColNum.setText("$rowNum:$colNum")
762 void print(EventObject evt = null) {
763 inputEditor.printAction.actionPerformed(evt)
766 void undo(EventObject evt = null) {
767 inputEditor.undoAction.actionPerformed(evt)
770 void redo(EventObject evt = null) {
771 inputEditor.redoAction.actionPerformed(evt)