/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* JavaWorld Library, Copyright 2011 Bryan Chadwick *
* *
* FILE: ./world/BigBang.java *
* *
* This file is part of JavaWorld. *
* *
* JavaWorld is free software: you can redistribute it and/or *
* modify it under the terms of the GNU General Public License *
* as published by the Free Software Foundation, either version *
* 3 of the License, or (at your option) any later version. *
* *
* JavaWorld is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with JavaWorld. If not, see . *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
package world;
import javax.swing.*;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import util.Util;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.RenderingHints;
import java.awt.event.*;
import java.lang.reflect.Method;
import java.util.Timer;
import java.util.TimerTask;
import image.*;
/** A Class representing the creation of a World/System of some type, and the
* related methods and Function
* Objects (call-backs) for drawing the world and handling various events. As
* handlers are installed, each is checked for a corresponding apply
* method with the appropriate signature.
*
*
The initial value of the World assigns a (minimum) type, which is
* used to search/check all of the handlers. Functions that produce a world
* deserve special attention, since they may return a super-type of the
* initial World (e.g., initial EmptyScene, with an tick handler that
* returns a Scene). The name and types of handlers are given in the
* table below:
*
*
*
* Event Name | BigBang Method | Handler Signature | Required? |
---|
* OnDraw | onDraw(handler) | Scene apply(World w) | yes |
* OnTick | onTick(handler) or onTick(handler, double) | World apply(World w) | no |
* OnMouse | onMouse(handler) | World apply(World w, int x, int y, String what) | no |
* OnKey | onKey(handler) | World apply(World w, String key) | no |
* OnRelease | onRelease(handler) | World apply(World w, String key) | no |
* StopWhen | stopWhen(handler) | boolean apply(World w) | no |
* LastScene | lastScene(handler) | Scene apply(World w) | no |
*
*
* If a matching method is not found when installing handlers, a RuntimeException is
* thrown, describing the offense.
*
*/
public class BigBang{
/** Default Tick rate for the world: ~33 frames per second */
public static double DEFAULT_TICK_RATE = 0.03;
/** Mouse down (button-down) event String */
public static String MOUSE_DOWN = "button-down";
/** Mouse up (button-up) event String */
public static String MOUSE_UP = "button-up";
/** Mouse window enter (enter) event String */
public static String MOUSE_ENTER = "enter";
/** Mouse window leave (leave) event String */
public static String MOUSE_LEAVE = "leave";
/** Mouse motion (move) event String */
public static String MOUSE_MOVE = "move";
/** Mouse down & move (drag) event String */
public static String MOUSE_DRAG = "drag";
/** Key arrow-up event String */
public static String KEY_ARROW_UP = "up";
/** Key arrow-down event String */
public static String KEY_ARROW_DOWN = "down";
/** Key arrow-left event String */
public static String KEY_ARROW_LEFT = "left";
/** Key arrow-right event String */
public static String KEY_ARROW_RIGHT = "right";
/** Key escape event String */
public static String KEY_ESCAPE = "escape";
private Object initial;
private Class> worldType;
private double time;
// Handlers and their corresponding selected Method
protected Object ondraw;
protected Method ondrawM;
protected Object ontick;
protected Method ontickM;
protected Object onmouse;
protected Method onmouseM;
protected Object onkey;
protected Method onkeyM;
protected Object onrelease;
protected Method onreleaseM;
protected Object stopwhen;
protected Method stopwhenM;
protected Object lastscene;
protected Method lastsceneM;
/** Create a new BigBang with a value of the initial World */
public BigBang(Object initial){
this(initial, initial.getClass(), 0.02,
null, null, null, null, null, null,
null, null, null, null, null, null,
null, null);
}
/** Install a Draw Handler into this BigBang. The Draw handler
* requires an apply method [World -> Scene], though the
* requirement is checked dynamically when this method is
* called. */
public BigBang onDraw(Object ondraw){
Method ondrawM = checkTypes(ondraw, new Class[]{this.worldType}, Scene.class, "OnDraw", false, false);
return new BigBang(this.initial, this.worldType, this.time,
ondraw, ondrawM, this.ontick, this.ontickM,
this.onmouse, this.onmouseM, this.onkey, this.onkeyM, this.onrelease, this.onreleaseM,
this.stopwhen, this.stopwhenM, this.lastscene, this.lastsceneM);
}
/** Install a Tick Handler at a tick rate of 1/20th of a second. */
public BigBang onTick(Object ontick){
return onTick(ontick, 0.05);
}
/** Install a Tick Handler into this BigBang at the given tick
* rate (per-seconds). The Tick handler requires an apply
* method [World -> World], though the requirement is
* checked dynamically when this method is called. */
public BigBang onTick(Object ontick, double time){
Method ontickM = checkTypes(ontick, new Class[]{this.worldType}, this.worldType, "OnTick", true, true);
return new BigBang(this.initial, this.worldType, time,
this.ondraw, this.ondrawM, ontick, ontickM,
this.onmouse, this.onmouseM, this.onkey, this.onkeyM, this.onrelease, this.onreleaseM,
this.stopwhen, this.stopwhenM, this.lastscene, this.lastsceneM);
}
/** Install a Mouse Handler into this BigBang. The Mouse handler
* requires an apply method [World -> World], though the
* requirement is checked dynamically when this method is
* called. */
public BigBang onMouse(Object onmouse){
Method onmouseM = checkTypes(onmouse, new Class[]{this.worldType, int.class, int.class, String.class}, this.worldType, "OnMouse", true, true);
return new BigBang(this.initial, this.worldType, this.time,
this.ondraw, this.ondrawM, this.ontick, this.ontickM,
onmouse, onmouseM, this.onkey, this.onkeyM, this.onrelease, this.onreleaseM,
this.stopwhen, this.stopwhenM, this.lastscene, this.lastsceneM);
}
/** Install a Key Handler into this BigBang. The Key handler
* requires an apply method [World String -> World], though
* the requirement is checked dynamically when this method is
* called. */
public BigBang onKey(Object onkey){
Method onkeyM = checkTypes(onkey, new Class[]{this.worldType, String.class}, this.worldType, "OnKey", true, true);
return new BigBang(this.initial, this.worldType, this.time,
this.ondraw, this.ondrawM, this.ontick, this.ontickM,
this.onmouse, this.onmouseM, onkey, onkeyM, this.onrelease, this.onreleaseM,
this.stopwhen, this.stopwhenM, this.lastscene, this.lastsceneM);
}
/** Install a Key Release Handler into this BigBang. The Key
* Release handler requires an apply method [World String ->
* World], though the requirement is checked dynamically when
* this method is called. */
public BigBang onRelease(Object onrelease){
Method onreleaseM = checkTypes(onrelease, new Class[]{this.worldType, String.class}, this.worldType, "OnRelease", true, true);
return new BigBang(this.initial, this.worldType, this.time,
this.ondraw, this.ondrawM, this.ontick, this.ontickM,
this.onmouse, this.onmouseM, this.onkey, this.onkeyM, onrelease, onreleaseM,
this.stopwhen, this.stopwhenM, this.lastscene, this.lastsceneM);
}
/** Install a StopWhen Handler into this BigBang. The StopWhen
* handler requires an apply method [World -> Boolean],
* though the requirement is checked dynamically when this
* method is called. The StopWhen handler, if installed is
* call to determine whether or not the World/animation/events
* should be stopped. When/if the handler returns true then
* all events stop being received and the LastScene handler is
* given a chance to draw the final World. */
public BigBang stopWhen(Object stopwhen){
Method stopwhenM = checkTypes(stopwhen, new Class[]{this.worldType}, Boolean.class, "StopWhen", true, false);
return new BigBang(this.initial, this.worldType, this.time,
this.ondraw, this.ondrawM, this.ontick, this.ontickM,
this.onmouse, this.onmouseM, this.onkey, this.onkeyM, this.onrelease, this.onreleaseM,
stopwhen, stopwhenM, this.lastscene, this.lastsceneM);
}
/** Install a LastScene Handler into this BigBang. The LastScene
* handler requires an apply method [World -> Scene], though
* the requirement is checked dynamically when this method is
* called. After the animation is stopped (StopWhen) the final
* World is drawn using the LstScene Handler. */
public BigBang lastScene(Object lastscene){
Method lastsceneM = checkTypes(lastscene, new Class[]{this.worldType}, Scene.class, "LastScene", true, false);
return new BigBang(this.initial, this.worldType, this.time,
this.ondraw, this.ondrawM, this.ontick, this.ontickM,
this.onmouse, this.onmouseM,
this.onkey, this.onkeyM, this.onrelease, this.onreleaseM,
this.stopwhen, this.stopwhenM, lastscene, lastsceneM);
}
// Private constructor...
private BigBang(Object init, Class> worldT, double time,
Object ondraw, Method ondrawM, Object ontick, Method ontickM,
Object onmouse, Method onmouseM, Object onkey, Method onkeyM,
Object onrelease, Method onreleaseM, Object stopwhen, Method stopwhenM,
Object lastscene, Method lastsceneM){
this.initial = init;
this.worldType = worldT;
this.time = time;
this.ondraw = ondraw;
this.ondrawM = ondrawM;
this.ontick = ontick;
this.ontickM = ontickM;
this.onmouse = onmouse;
this.onmouseM = onmouseM;
this.onkey = onkey;
this.onkeyM = onkeyM;
this.onrelease = onrelease;
this.onreleaseM = onreleaseM;
this.stopwhen = stopwhen;
this.stopwhenM = stopwhenM;
this.lastscene = lastscene;
this.lastsceneM = lastsceneM;
}
/** Gap left around the border of the Window */
private static int SPACE = 5;
/** Check/find a method compatible with the given types in the
* given function Object/Handler */
private Method checkTypes(Object f, Class>[] args, Class> ret, String what, boolean nullable, boolean wret){
Class> fClass = f.getClass();
Method[] possibles = fClass.getDeclaredMethods();
for(Method m : possibles){
if(m.getName().equals(Util.funcObjMethName) &&
Util.subtypes(args, m.getParameterTypes())){
if(Util.subtype(m.getReturnType(), ret))
return m;
if(Util.subtype(ret, m.getReturnType()) &&
!m.getReturnType().equals(Object.class)){
this.worldType = m.getReturnType();
return m;
}
}
}
throw Util.exceptionDrop(2, "\n** Function Object ("+fClass.getSimpleName()+") used for "+
what.toLowerCase()+" does not contain a method sutable for:\n "+
ret.getSimpleName()+" "+Util.funcObjMethName+"("+Util.argsString(args,0)+")");
}
/** Wrapper for the Tick Handler */
private Object doOnTick(Object w) {
return Util.applyFunc(this.ontick, this.ontickM, new Object[]{w});
}
/** Wrapper for the Mouse Handler */
private Object doOnMouseEvent(Object w, int x, int y, String me) {
return Util.applyFunc(this.onmouse, this.onmouseM, new Object[]{w,x-SPACE,y-SPACE,me});
}
/** Wrapper for the Key Handler */
private Object doOnKeyEvent(Object w, String ke){
if(ke.length() == 0)return w;
return Util.applyFunc(this.onkey, this.onkeyM, new Object[]{w,ke});
}
/** Wrapper for the Key Release Handler */
private Object doOnReleaseEvent(Object w, String ke){
if(ke.length() == 0)return w;
return Util.applyFunc(this.onrelease, this.onreleaseM, new Object[]{w,ke});
}
/** Wrapper for the Draw Handler */
private Scene doOnDraw(Object w) {
return (Scene)Util.applyFunc(this.ondraw, this.ondrawM, new Object[]{w});
}
/** Wrapper for the StopWhen Handler */
private boolean doStopWhen(Object w) {
if(this.stopwhen == null)return false;
return (Boolean)Util.applyFunc(this.stopwhen, this.stopwhenM, new Object[]{w});
}
/** Wrapper for the LastScene Handler */
private Scene doLastScene(Object w) {
if(this.lastscene == null)return doOnDraw(w);
return (Scene)Util.applyFunc(this.lastscene, this.lastsceneM, new Object[]{w});
}
/** Construct and run the animation/interaction system. For the
* Swing version the method returns the final value of the
* World after the animation has completed. The Window is
* opened as a Modal dialog, so control does not return to the
* bigband caller until the window is closed. */
public Object bigBang(){
return this.bigBang("BigBang");
}
/** Open a window and run the animation with the given title */
public Object bigBang(String title){
try{
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
}catch(Exception e){}
if(this.ondraw == null)
throw new RuntimeException("No World Draw Handler");
JDialog f = new JDialog((JFrame)null, title, true);
Scene scn = doOnDraw(this.initial);
f.setSize((int)(SPACE*2+Math.max(20, 14+scn.width())),
(int)(Math.max(20, SPACE*2+31+scn.height())));
f.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
f.setResizable(false);
final Handler handler = new Handler(this,this.initial, scn,
new BufferedImage((int)(scn.width()+2*SPACE), (int)(scn.height()+2*SPACE), BufferedImage.TYPE_INT_RGB), f);
f.getContentPane().add(handler);
f.setVisible(true);
handler.run.cancel();
return handler.w;
}
/** Handles the nitty-gritty of world updates and interfacing with Swing */
static class Handler extends javax.swing.JComponent
implements MouseListener,KeyListener,MouseMotionListener{
private static final long serialVersionUID = 1L;
BigBang world;
Object w;
Scene scnBuffer;
BufferedImage buffer;
Graphics2D graph;
Timer run;
TimerTask ticker;
boolean isRunning = false;
boolean isDone = false;
/** Create a new Handler for all the World's events */
Handler(BigBang world, Object ww, Scene scn, BufferedImage buff, JDialog dia){
this.world = world;
this.w = ww;
this.scnBuffer = null;
this.buffer = buff;
this.graph = buff.createGraphics();
this.graph.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
this.graph.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
this.run = new Timer();
addMouseListener(this);
if(world.onmouse != null){
addMouseMotionListener(this);
}
if(world.onkey != null)
dia.addKeyListener(this);
this.isRunning = true;
if(world.ontick != null){
this.run.scheduleAtFixedRate(this.ticker = new TimerTask(){
public void run(){ tickAction(); }
}, 200, (int)(world.time*1000));
}
}
/** Swing uses a paint(Graphics) method to draw the
* component (Handler) into the window. */
public void paint(java.awt.Graphics g){
Scene curr;
if(!this.isDone)
curr = this.world.doOnDraw(this.w);
else
curr = this.world.doLastScene(this.w);
if(curr != this.scnBuffer){
this.scnBuffer = curr;
this.graph.setColor(Color.white);
this.graph.fillRect(0,0, this.getWidth(), this.getHeight());
this.graph.clipRect(SPACE, SPACE, this.buffer.getWidth()-SPACE*2, this.buffer.getHeight()-SPACE*2);
this.scnBuffer.paint(this.graph,SPACE,SPACE);
}
g.drawImage(this.buffer, 0, 0, null);
}
/** Rather than Swing timers, we use to java.util.Timer to
* provide compatibility with Android (i.e., so the code
* for both versions looks the same). */
public void tickAction(){
if(!this.isRunning || this.isDone)return;
replace(this.world.doOnTick(this.w));
}
/** Support saving screenshots... */
JPopupMenu popup = new JPopupMenu("World Options");
{
addItem(this.popup, "Save Image...", new ActionListener(){
public void actionPerformed(ActionEvent e){
doSaveAs(Handler.this.scnBuffer);
synchronized(Handler.this.popup){ Handler.this.popup.notify(); }
}
});
addItem(this.popup, "Continue", new ActionListener(){
public void actionPerformed(ActionEvent e){
synchronized(Handler.this.popup){ Handler.this.popup.notify(); }
}
});
this.popup.addPopupMenuListener(new PopupMenuListener(){
public void popupMenuCanceled(PopupMenuEvent arg0){
synchronized(Handler.this.popup){ Handler.this.popup.notify(); }
}
public void popupMenuWillBecomeVisible(PopupMenuEvent arg0){}
public void popupMenuWillBecomeInvisible(PopupMenuEvent arg0){}
});
}
private static void addItem(JPopupMenu m, String s, ActionListener l){
JMenuItem item = new JMenuItem(s);
if(l != null)
item.addActionListener(l);
m.add(item);
}
boolean doSaveAs(Scene scn){
try{
JFileChooser fc = new JFileChooser();
int result = fc.showDialog(this, "SaveAs");
// Check for existence...
if(result == JFileChooser.APPROVE_OPTION){
this.scnBuffer.toFile(fc.getSelectedFile().getAbsolutePath());
return true;
}
}catch(Exception e){
System.err.println("Exception: "+e);
}
return false;
}
public void mousePressed(final MouseEvent e){
if(e.isPopupTrigger()){
// Pause the simulation...
final Handler that = this;
final boolean temp = this.isRunning;
this.isRunning = false;
// Show the context Menu
new Thread(){
public void run(){
Handler.this.popup.show(that, e.getX(), e.getY());
try{
synchronized(Handler.this.popup){
Thread.yield();
Handler.this.popup.wait();
}
}catch(Exception ee){}
// Restart the simulation if running...
that.isRunning = temp;
}
}.start();
}else{
if(this.isRunning && !this.isDone)
replace(this.world.doOnMouseEvent(this.w, e.getX(), e.getY(), MOUSE_DOWN));
}
}
/** Mouse click/move/event Methods */
public void mouseClicked(MouseEvent e){}
public void mouseEntered(MouseEvent e){ if(this.isRunning && !this.isDone)replace(this.world.doOnMouseEvent(this.w, e.getX(), e.getY(), MOUSE_ENTER)); }
public void mouseExited(MouseEvent e){ if(this.isRunning && !this.isDone)replace(this.world.doOnMouseEvent(this.w, e.getX(), e.getY(), MOUSE_LEAVE)); }
public void mouseReleased(MouseEvent e){ if(this.isRunning && !this.isDone)replace(this.world.doOnMouseEvent(this.w, e.getX(), e.getY(), MOUSE_UP)); }
public void mouseDragged(MouseEvent e){ if(this.isRunning && !this.isDone)replace(this.world.doOnMouseEvent(this.w, e.getX(), e.getY(), MOUSE_DRAG)); }
public void mouseMoved(MouseEvent e){ if(this.isRunning && !this.isDone)replace(this.world.doOnMouseEvent(this.w, e.getX(), e.getY(), MOUSE_MOVE)); }
/** Keys are converted to strings to simplify handling */
public void keyPressed(KeyEvent e){
if(this.isRunning && !this.isDone)
replace(this.world.doOnKeyEvent(this.w, convert(e.getKeyCode(), ""+e.getKeyChar())));
}
public void keyReleased(KeyEvent e){
if(this.isRunning && !this.isDone)
replace(this.world.doOnReleaseEvent(this.w, convert(e.getKeyCode(), ""+e.getKeyChar())));
}
public void keyTyped(KeyEvent e){
//if(isRunning && !isDone)replace(world.doOnKeyEvent(w, ""+e.getKeyChar()));
}
private void replace(Object w){
// This isn't enough when mutation is involved...
if(!this.isRunning || this.isDone)return;
if(this.isRunning && this.world.doStopWhen(w)){
this.isRunning = false;
this.isDone = true;
this.run.cancel();
}
boolean change = !this.w.equals(w);
this.w = w;
if(change)repaint();
}
private String convert(int code, String ch){
switch(code){
case KeyEvent.VK_UP: return KEY_ARROW_UP;
case KeyEvent.VK_DOWN: return KEY_ARROW_DOWN;
case KeyEvent.VK_LEFT: return KEY_ARROW_LEFT;
case KeyEvent.VK_RIGHT: return KEY_ARROW_RIGHT;
case KeyEvent.VK_ESCAPE: return KEY_ESCAPE;
default: return ch;
}
}
}
}