SwingPadを改良
griffonでアプリを作成する際に、簡単に画面周りの変更→確認をしたければ、インストールフォルダのexamples/SwingPadを利用するのがよい。
でも、このアプリも若干気になる所があって、
- viewからmodelへの入力値反映のためにbindメソッドを使用しているコードを実行するとエラー
- viewのgroovyコードをそのまま張り付けてもだめで、application(){ ... } の内側とimport文のみのこして後はコメントアウトさせて実行しないとだめ
解決できるように、SwingPadのコードをほんの少し修正してみた。
/*
* Copyright 2007-2008 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
*/
import java.awt.Color
import java.awt.Font
import java.awt.Robot
import java.awt.FlowLayout
import java.awt.event.ActionEvent
import javax.swing.JComponent
import javax.swing.JFileChooser
import javax.swing.JOptionPane
import javax.swing.SwingConstants
import static javax.swing.JSplitPane.*
import javax.imageio.ImageIO
import java.util.prefs.Preferences
import groovy.ui.text.FindReplaceUtility
import org.codehaus.groovy.runtime.StackTraceUtils
class SwingPadController {
def model
def view
def builder
private prefs = Preferences.userNodeForPackage(SwingPadController)
private File currentFileChooserDir = new File(prefs.get('currentFileChooserDir', '.'))
private File currentClasspathJarDir = new File(prefs.get('currentClasspathJarDir', '.'))
private File currentClasspathDir = new File(prefs.get('currentClasspathDir', '.'))
private File currentSnapshotDir = new File(prefs.get('currentSnapshotDir', '.'))
private runThread = null
private GroovyClassLoader groovyClassLoader
private static int scriptCounter = 0
private Set factorySet = new TreeSet()
void mvcGroupInit( Map args ) {
groovyClassLoader = new GroovyClassLoader(this.class.classLoader)
}
def updateTitle = { ->
// TODO handle undo!
if( model.scriptFile ) {
return model.scriptFile.name + (model.dirty ? " *" : "") + " - SwingPad"
}
return "SwingPad"
}
def newScript = { evt = null ->
if( askToSaveFile(evt) ) {
model.scriptFile = null
model.dirty = false
view.editor.textEditor.text = ''
view.editor.textEditor.requestFocus()
}
}
def open = { evt = null ->
model.scriptFile = selectFilename()
if( !model.scriptFile ) return
doOutside {
def scriptText = model.scriptFile.readLines().join('n')
doLater {
if( !scriptText ) return
// need 2-way binding!
view.editor.textEditor.text = scriptText
model.dirty = false
view.editor.textEditor.caretPosition = 0
view.editor.textEditor.requestFocus()
}
}
}
def save = { evt = null ->
if( !model.scriptFile ) return saveAs(evt)
model.scriptFile.write(model.content)
model.dirty = false
return true
}
def saveAs = { evt = null ->
model.scriptFile = selectFilename("Save")
if( model.scriptFile ) {
model.scriptFile.write(model.content)
model.dirty = false
return true
}
return false
}
def exit = { evt = null ->
if( askToSaveFile() ) {
FindReplaceUtility.dispose()
app.shutdown()
}
}
def snapshot = { evt ->
def fc = new JFileChooser(currentSnapshotDir)
fc.fileSelectionMode = JFileChooser.FILES_ONLY
fc.acceptAllFileFilterUsed = true
if (fc.showDialog(app.appFrames[0], "Snapshot") == JFileChooser.APPROVE_OPTION) {
currentSnapshotDir = fc.currentDirectory
prefs.put('currentSnapshotDir', currentSnapshotDir.path)
def frameBounds = app.appFrames[0].bounds
def capture = new Robot().createScreenCapture(frameBounds)
def filename = fc.selectedFile.name
def dot = filename.lastIndexOf(".")
def ext = "png"
if( dot > 0 ) {
ext = filename[dot+1..-1]
} else {
filename += ".$ext"
}
def target = new File(currentSnapshotDir,filename)
ImageIO.write( capture, ext, target )
def pane = builder.optionPane()
pane.setMessage("Successfully saved snapshot tonn${target.absolutePath}")
def dialog = pane.createDialog(app.appFrames[0], 'Snapshot')
dialog.show()
}
}
private void invokeTextAction( evt, closure ) {
if( evt.source ) closure(view.editor.textEditor)
}
def cut = { evt = null -> invokeTextAction(evt, { source -> source.cut() }) }
def copy = { evt = null -> invokeTextAction(evt, { source -> source.copy() }) }
def paste = { evt = null -> invokeTextAction(evt, { source -> source.paste() }) }
def selectAll = { evt = null -> invokeTextAction(evt, { source -> source.selectAll() }) }
// TODO yet unconnected!!
def find = { evt = null -> FindReplaceUtility.showDialog() }
def findNext = { evt = null -> FindReplaceUtility.FIND_ACTION.actionPerformed(evt) }
def findPrevious = { evt = null ->
def reverseEvt = new ActionEvent( evt.source, evt.iD,
evt.actionCommand, evt.when,
ActionEvent.SHIFT_MASK) //reverse
FindReplaceUtility.FIND_ACTION.actionPerformed(reverseEvt)
}
def replace = { evt = null -> FindReplaceUtility.showDialog(true) }
def largerFont = { evt = null ->
modifyFont(view.editor.textEditor, {it > 40}, +2)
modifyFont(view.errors, {it > 40}, +2)
}
def smallerFont = { evt = null ->
modifyFont(view.editor.textEditor, {it < 5}, -2)
modifyFont(view.errors, {it < 5}, -2)
}
def packComponents = { evt = null ->
def newLayout = evt?.source?.state ? builder.flowLayout(alignment:FlowLayout.LEFT, hgap: 0, vgap: 0) : builder.borderLayout()
if( !newLayout.class.isAssignableFrom(view.canvas.layout.class) ) {
view.canvas.layout = newLayout
if( model.successfulScript ) runScript(evt)
}
}
def showRulers = { evt = null ->
def rh = evt?.source?.state ? view.rowHeader : view.emptyRowHeader
def ch = evt?.source?.state ? view.columnHeader : view.emptyColumnHeader
if( view.scroller.rowHeader.view != rh ) {
view.scroller.rowHeaderView = rh
view.scroller.columnHeaderView = ch
view.scroller.repaint()
}
}
def runScript = { evt = null ->
if( !model.content ) return
view.tabs.selectedIndex = 0 // sourceTab
runThread = Thread.start {
try {
doLater {
model.status = "Running Script ..."
if( model.errors != "" ) {
model.errors = ""
model.caretPosition = 0
}
showDialog( "runWaitDialog" )
}
model.content = model.content.replaceAll(/(?ms)(^s+applications*(.+?)s*{)(.+)(}s*)/){ all, p1, p2, p3 ->
p1.readLines().collect{ "// " + it }.join("n") + p2 + p3.readLines().collect{ "// " + it }.join("n")
}
edt{
view.inputArea.text = model.content
}
executeScript( model.content )
} catch( Throwable t ) {
doLater { finishWithException(t) }
} finally {
doLater {
hideDialog( "runWaitDialog" )
runThread = null
}
}
}
}
def runSampleScript = { evt = null ->
if( model.currentSample ) {
def builder = model.currentSample[0..-2].toLowerCase()
if( !model.builders[builder].enabled ) {
model.status = "Enabling ${model.builders[builder].type} ..."
view."${builder}Menu".selected = true
doOutside {
if(toggleBuilder([source:view."${builder}Menu"], builder, model.builders[builder].type)) {
doLater {
model.status = "Loading Script ..."
view.editor.textEditor.text = model.samples[model.currentSample]
view.editor.textEditor.caretPosition = 0
view.runAction.enabled = true
runScript(evt)
}
}
}
} else {
model.status = "Loading Script ..."
view.editor.textEditor.text = model.samples[model.currentSample]
view.editor.textEditor.caretPosition = 0
view.runAction.enabled = true
runScript(evt)
}
}
}
def about = { evt = null ->
def pane = builder.optionPane()
// work around GROOVY-1048
pane.setMessage('Welcome to SwingPad')
def dialog = pane.createDialog(app.appFrames[0], 'About SwingPad')
dialog.show()
}
def confirmRunInterrupt = { evt = null ->
def rc = JOptionPane.showConfirmDialog( app.appFrames[0], "Attempt to interrupt script?",
"SwingPad", JOptionPane.YES_NO_OPTION)
if( rc == JOptionPane.YES_OPTION && runThread ) {
runThread.interrupt()
}
}
// the folowing 4 actions taken from groovy.ui.Console
def addClasspathJar = { evt = null ->
def fc = new JFileChooser(currentClasspathJarDir)
fc.fileSelectionMode = JFileChooser.FILES_ONLY
fc.acceptAllFileFilterUsed = true
if (fc.showDialog(app.appFrames[0], "Add") == JFileChooser.APPROVE_OPTION) {
currentClasspathJarDir = fc.currentDirectory
prefs.put('currentClasspathJarDir', currentClasspathJarDir.path)
groovyClassLoader.addURL(fc.selectedFile.toURL())
}
}
def addClasspathDir = { evt = null ->
def fc = new JFileChooser(currentClasspathDir)
fc.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
fc.acceptAllFileFilterUsed = true
if (fc.showDialog(app.appFrames[0], "Add") == JFileChooser.APPROVE_OPTION) {
currentClasspathDir = fc.currentDirectory
prefs.put('currentClasspathDir', currentClasspathDir.path)
groovyClassLoader.addURL(fc.selectedFile.toURL())
}
}
def showToolbar = { evt = null ->
def showToolbar = evt.source.selected
prefs.putBoolean('showToolbar', showToolbar)
view.toolbar.visible = showToolbar
}
def suggestNodeName = { evt = null ->
if( !model.content ) return
def editor = view.editor.textEditor
def caret = editor.caretPosition
if( !caret ) return
def document = editor.document
def target = ""
def ch = document.getText(--caret,1)
while( ch =~ /[a-zA-Z]/ ) {
target = ch + target
if( caret ) ch = document.getText(--caret,1)
else break
}
if( target.size() != document.length ) caret++
if( !factorySet ) populateFactorySet()
def suggestions = factorySet.findAll{ it.startsWith(target) }
if( !suggestions ) return
if( suggestions.size() == 1 ) {
model.suggestion = [
start: caret,
end: caret + target.size(),
offset: target.size(),
text: suggestions.iterator().next()
]
writeSuggestion()
} else {
model.suggestion = [
start: caret,
end: caret + target.size(),
offset: target.size()
]
model.suggestions.clear()
model.suggestions.addAll(suggestions)
view.popup.showPopup(SwingConstants.CENTER, app.appFrames[0])
view.suggestionList.selectedIndex = 0
}
}
def codeComplete = { evt ->
model.suggestion.text = model.suggestions[view.suggestionList.selectedIndex]
view.popup.hidePopup(true)
writeSuggestion()
}
def toggleFlamingoBuilder = { evt = null ->
doOutside {
toggleBuilder(evt, "flamingo", model.builders.flamingo.type)
}
}
def toggleTrayBuilder = { evt = null ->
doOutside {
toggleBuilder(evt, "tray", model.builders.tray.type)
}
}
def toggleMacwidgetsBuilder = { evt = null ->
doOutside {
toggleBuilder(evt, "macwidgets", model.builders.macwidgets.type)
}
}
def toggleLayout = { evt = null ->
model.horizontalLayout = !model.horizontalLayout
view.splitPane.orientation = model.horizontalLayout ? HORIZONTAL_SPLIT : VERTICAL_SPLIT
view.toggleLayoutAction.putValue("SmallIcon", model.horizontalLayout ? view.verticalLayoutIcon : view.horizontalLayoutIcon )
}
private writeSuggestion() {
if( !model.suggestion ) return
def editor = view.editor.textEditor
def document = editor.document
def s = model.suggestion
def text = s.text.substring(s.offset)
document.insertString(s.start+s.offset, text, null)
editor.requestFocus()
// clear it!
model.suggestion = [:]
}
private void finishNormal( component ) {
model.successfulScript = true
doLater {
model.status = 'Execution complete.'
view.canvas.removeAll()
view.canvas.repaint()
if( component instanceof JComponent ) {
view.canvas.add(component)
} else {
model.status = "The script did not return a JComponent!"
}
}
}
private void finishWithException( Throwable t ) {
model.successfulScript = false
model.status = 'Execution terminated with exception.'
StackTraceUtils.deepSanitize(t)
t.printStackTrace()
def baos = new ByteArrayOutputStream()
t.printStackTrace(new PrintStream(baos))
doLater {
view.canvas.removeAll()
view.canvas.repaint()
view.tabs.selectedIndex = 1 // errorsTab
model.errors = baos.toString()
model.caretPosition = 0
}
}
private void showAlert(title, message) {
doLater {
JOptionPane.showMessageDialog(app.appFrames[0], message,
title, JOptionPane.WARNING_MESSAGE)
}
}
private void showMessage(title, message) {
doLater {
JOptionPane.showMessageDialog(app.appFrames[0], message,
title, JOptionPane.INFORMATION_MESSAGE)
}
}
private void showDialog( dialogName, pack = true ) {
def dialog = view."$dialogName"
if( pack ) dialog.pack()
int x = app.appFrames[0].x + (app.appFrames[0].width - dialog.width) / 2
int y = app.appFrames[0].y + (app.appFrames[0].height - dialog.height) / 2
dialog.setLocation(x, y)
dialog.show()
}
private void hideDialog( dialogName ) {
def dialog = view."$dialogName"
dialog.hide()
}
private selectFilename( name = "Open" ) {
// should use builder.fileChooser() ?
def fc = new JFileChooser(currentFileChooserDir)
fc.fileSelectionMode = JFileChooser.FILES_ONLY
fc.acceptAllFileFilterUsed = true
if( fc.showDialog(app.appFrames[0], name ) == JFileChooser.APPROVE_OPTION ) {
currentFileChooserDir = fc.currentDirectory
prefs.put('currentFileChooserDir', currentFileChooserDir.path)
return fc.selectedFile
}
return null
}
private boolean askToSaveFile(evt) {
if( !model.scriptFile || !model.dirty ) return true
switch( JOptionPane.showConfirmDialog( app.appFrames[0],
"Save changes to " + model.scriptFile.name + "?",
"SwingPad", JOptionPane.YES_NO_CANCEL_OPTION)){
case JOptionPane.YES_OPTION: return save(evt)
case JOptionPane.NO_OPTION: return true
}
return false
}
private void executeScript( codeSource ) {
try {
codeSource = codeSource.replaceAll(
/(?ms)binds*([^)]+?)/, """"
)
def script = groovyClassLoader.parseClass(codeSource,getScriptName()).newInstance()
def b = app.builders.Script
def component = null
b.edt{ component = b.build(script)}
// if( !(component instanceof JComponent) ) {
// throw new IllegalArgumentException("The script did not return a JComponent!")
// }
doLater { finishNormal(component) }
} catch( Throwable t ) {
doLater { finishWithException(t) }
}
}
private getScriptName() {
"SwingPad_script" + (scriptCounter++)
}
private modifyFont( target, sizeFilter, sizeMod ) {
def currentFont = target.font
if( sizeFilter(currentFont.size) ) return
target.font = new Font( 'Monospaced', currentFont.style, currentFont.size + sizeMod )
}
private populateFactorySet() {
// TODO filter factories coming from SwingXBuilder that have jxclassicSwing: on their name
def ub = app.builders.Script
factorySet.clear()
ub.builderRegistration.each { ubr ->
def builder = ubr.builder
def oldProxy = builder.proxyBuilder
try {
builder.proxyBuilder = builder
factorySet.addAll(ubr.builder.factories.keySet().sort().collect(){ (ubr.prefixString?:"")+it })
} finally {
builder.proxyBuilder = oldProxy
}
}
factorySet -= factorySet.grep{ it.startsWith("jxclassicSwing:") }
}
private toggleBuilder( evt, name, builder ) {
def cname = name[0].toUpperCase() + name[1..-1]
model.builders[name].enabled = evt.source.selected
if( model.builders[name].enabled ) {
app.builderConfig.root."$builder".view = "*"
} else {
app.builderConfig.root.remove(builder)
}
try {
// With no current way to unload an URL from the rootLoader
// we have to keep track if an URL has already been added to it
if( !model.builders[name].loaded ) {
def startDir = System.getProperty("griffon.start.dir")
if( startDir.startsWith('"') && startDir.endsWith('"') ) {
startDir = startDir[1..-2]
}
def jarDir = new File(startDir,"lib/$name")
jarDir.eachFileMatch({it.endsWith(".jar")}) { jar ->
// groovyClassLoader.addURL(jar.toURI().toURL())
this.class.classLoader.addURL(jar.toURI().toURL())
}
model.builders[name].loaded = true
}
def binding = new Binding()
binding.setVariable("controller", this)
def script = """def (m, v, c) = controller.createMVCGroup("Script","Script",[:])
return v
"""
app.builders.Script = new GroovyShell(groovyClassLoader,binding).evaluate(script)
} catch( ex ) {
StackTraceUtils.deepSanitize(ex)
ex.printStackTrace()
model.builders[name].enabled != model.builders[name].enabled
evt.source.selected = !evt.source.selected
showAlert( "Enable $cname".toString(),
"Couldn't enable $cname:nn$ex".toString())
} finally {
populateFactorySet()
}
return model.builders[name].enabled
}
}
長々とコード載せたが、元ソースからl194〜199、l453〜455を変えただけ。




ディスカッション
コメント一覧
まだ、コメントがありません