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.
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
26 import java
.awt
.Toolkit
27 import java
.awt
.event
.ActionEvent
28 import java
.util
.prefs
.Preferences
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
38 * Groovy Swing console.
40 * Allows user to interactively enter and execute Groovy.
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)
61 Action showToolbarAction
63 // Maximum size of history
66 // Maximum number of characters to show on console at any time
67 int maxOutputChars
= 20000
71 RootPaneContainer frame
72 ConsoleTextEditor inputEditor
77 JLabel rowNumAndColNum
85 // Styles for output area
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
101 int textSelectionStart
// keep track of selections in inputArea
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', '.'))
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())
127 if (args
.length
== 1) console
.loadScriptFile(args
[0] as File
)
134 Console(Binding binding
) {
138 Console(ClassLoader parent
) {
139 this(parent
, new Binding())
142 Console(ClassLoader parent
, Binding binding
) {
143 newScript(parent
, binding
);
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
)
160 rootContainerDelegate
:{
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
,
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
) {
182 rootContainerDelegate
:{
183 containingWindows
+= SwingUtilities
.getRoot(applet
.getParent())
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.
204 groovy.ui.Console""")
207 // add controller to the swingBuilder bindings
208 swing
.controller
= this
210 // create the actions
211 swing
.build(ConsoleActions
)
214 swing
.build(ConsoleView
)
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()
227 swing
.doLater inputArea
.&requestFocus
231 public void installInterceptor() {
232 systemOutInterceptor
= new SystemOutputInterceptor(this.¬ifySystemOut
)
233 systemOutInterceptor
.start()
236 void addToHistory(record
) {
238 // history.size here just retrieves method closure
239 if (history
.size() > maxHistory
) {
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
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
) {
273 switch (JOptionPane
.showConfirmDialog(frame
,
274 "Save changes to " + scriptFile
.name
+ "?",
275 "GroovyConsole", JOptionPane
.YES_NO_CANCEL_OPTION
))
277 case JOptionPane
.YES_OPTION
:
279 case JOptionPane
.NO_OPTION
:
287 Toolkit
.defaultToolkit
.beep()
290 // Binds the "_" and "__" variables in the shell
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
)
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
) {
341 FindReplaceUtility
.dispose()
342 consoleControllers
.remove(this)
343 if (!consoleControllers
) {
344 systemOutInterceptor
.stop()
350 void fileNewFile(EventObject evt
= null) {
351 if (askToSaveFile()) {
358 // Start a new window with a copy of current variables
359 void fileNewWindow(EventObject evt
= null) {
360 Console consoleController
= new Console(
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
)
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
) {
383 inputArea
.editable
= false
387 consoleText
= file
.readLines().join('\n')
391 inputArea
.text
= consoleText
393 inputArea
.caretPosition
= 0
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
)
406 scriptFile
.write(inputArea
.text
)
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
)
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
)
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
)
446 statusLabel
.text
= 'Execution complete. Result was null.'
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}
458 for (i
in (history
.size() - 1)..0) {
459 if (history
[i
].result
!= null) {
460 return history
[i
].result
466 // Allow access to shell from outside console
467 // (useful for configuring shell before startup)
468 GroovyShell
getShell() {
472 void historyNext(EventObject evt
= null) {
473 if (historyIndex
< history
.size()) {
474 setInputTextFromHistory(historyIndex
+ 1)
476 statusLabel
.text
= "Can't go past end of history (time travel not allowed)"
481 void historyPrev(EventObject evt
= null) {
482 if (historyIndex
> 0) {
483 setInputTextFromHistory(historyIndex
- 1)
485 statusLabel
.text
= "Can't go past start of history"
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
)
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
) {
518 if (EventQueue
.isDispatchThread()) {
519 consoleControllers
.each
{it
.appendOutput(str
, it
.outputStyle
)}
522 SwingUtilities
.invokeLater
{
523 consoleControllers
.each
{it
.appendOutput(str
, it
.outputStyle
)}
529 // actually run the script
531 void runScript(EventObject evt
= null) {
535 void runSelectedScript(EventObject evt
= null) {
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
)
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
{
587 SwingUtilities
.invokeLater
{ showRunWaitDialog() }
588 String name
= "Script${scriptNameCounter++}"
589 if(beforeExecution
) {
592 def result
= shell
.evaluate(record
.getTextToRun(selected
), name
)
596 SwingUtilities
.invokeLater
{ finishNormal(result
) }
597 } catch (Throwable t
) {
598 SwingUtilities
.invokeLater
{ finishException(t
) }
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
607 while (!(runWaitDialog?
.visible
)) {
610 while (runThread?
.alive
) {
613 } catch (InterruptedException ie
) {
614 // we got interrupted, just loop again.
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
634 void setDirty(boolean newDirty
) {
635 //TODO when @BoundProperty is live, this should be handled via listeners
637 saveAction
.enabled
= newDirty
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
649 if (historyIndex
< history
.size()) {
650 record
= history
[historyIndex
]
651 statusLabel
.text
= "command history ${history.size() - historyIndex}"
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')
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() {
707 runWaitDialog
.setLocationRelativeTo(frame
)
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
720 if (frame
.properties
.containsKey('title')) {
721 if (scriptFile
!= null) {
722 frame
.title
= scriptFile
.name
+ (dirty?
" * ":"") + " - GroovyConsole"
724 frame
.title
= "GroovyConsole"
729 void invokeTextAction(evt
, closure
) {
730 def source
= evt
.getSource()
731 if (source
!= null) {
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
)