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を変えただけ。
ディスカッション
コメント一覧
まだ、コメントがありません