// JComponentWithEvents.java // By David Kosbie /* To use the JComponentWithEvents class: 1. Create a subclass with this main method: public static void main(String[] args) { launch(500, 400); } 2. Place this file in the same directory as your subclass: JComponentWithEvents.class (You may need to compile it from JComponentWithEvents.java) 3. If this fails to play sounds, you may need to install a soundbank from here: http://java.sun.com/products/java-media/sound/soundbanks.html */ import javax.swing.*; import javax.imageio.*; import java.awt.*; import java.awt.image.*; import java.awt.event.*; import java.util.*; import java.lang.reflect.*; // Additional imports for sound player import java.net.*; import java.io.*; import java.applet.*; import javax.sound.midi.*; import javax.sound.sampled.*; public class JComponentWithEvents extends JComponent implements MouseListener, MouseMotionListener, KeyListener, ComponentListener, ActionListener, // for timer Runnable // for deferredDoStart { ////////////////////////////////////// // PUBLIC INTERFACE ////////////////////////////////////// // Methods to override public void start() { } public void timerFired() { } public void paint(Graphics2D page) { } public void mousePressed(int x, int y) { } public void mouseDragged(int x, int y) { } public void mouseReleased(int x, int y) { } public void mouseClicked(int x, int y, int clickCount) { } public void mouseMoved(int x, int y) { } public void mouseEntered(int x, int y) { } public void mouseExited(int x, int y) { } public void keyPressed(char key) { } public void keyReleased(char key) { } public void keyTyped(char key) { } public void componentResized() { } // constants public static final char UP = KeyEvent.VK_UP; public static final char DOWN = KeyEvent.VK_DOWN; public static final char LEFT = KeyEvent.VK_LEFT; public static final char RIGHT = KeyEvent.VK_RIGHT; public static final char DELETE = KeyEvent.VK_DELETE; public static final char HOME = KeyEvent.VK_HOME; public static final char PAGE_UP = KeyEvent.VK_PAGE_UP; public static final char PAGE_DOWN = KeyEvent.VK_PAGE_DOWN; public static final char END = KeyEvent.VK_END; public static final char SPACE = KeyEvent.VK_SPACE; public static final char BACK_SPACE = KeyEvent.VK_BACK_SPACE; public static final char ENTER = KeyEvent.VK_ENTER; public static final char ESCAPE = KeyEvent.VK_ESCAPE; public static final char SHIFT = KeyEvent.VK_SHIFT; public static final char CAPS_LOCK = KeyEvent.VK_CAPS_LOCK; public static final char CONTROL = KeyEvent.VK_CONTROL; public static final char ALT = KeyEvent.VK_ALT; public static final char F1 = KeyEvent.VK_F1, F2 = KeyEvent.VK_F2, F3 = KeyEvent.VK_F3, F4 = KeyEvent.VK_F4, F5 = KeyEvent.VK_F5, F6 = KeyEvent.VK_F6, F7 = KeyEvent.VK_F7, F8 = KeyEvent.VK_F8, F9 = KeyEvent.VK_F9, F10 = KeyEvent.VK_F10, F11 = KeyEvent.VK_F11, F12 = KeyEvent.VK_F12; // Methods to use, but not to override: public final void beep() { doBeep(); } public final void exit() { doExit(); } public final int getTimerDelay() { return _timerDelay; } public final void setTimerDelay(int delay){ doSetTimerDelay(delay); } public final void stopTimer() { doStopTimer(); } public final boolean isPaused() { return _paused; } public final void setPaused(boolean paused) { _paused = paused; } public final char getPauseKey() { return _pauseKey; } public final void setPauseKey(char c) { _pauseKey = c; } public final char getUnpauseKey() { return _unpauseKey; } public final void setUnpauseKey(char c) { _unpauseKey = c; } public final void setIgnoreModifierKeys(boolean b) { ignoreModifierKeys = b; } public final void play(String location) { doPlay(location, false); } public final void loop(String location) { doPlay(location, true); } public final void stopSounds() { doStopSounds(); } public MouseEvent getMouseEvent() { return currentMouseEvent; } public KeyEvent getKeyEvent() { return currentKeyEvent; } ////////////////////////////////////// /// Version and semi-auto-update manager ////////////////////////////////////// // private constructor for version checker instance in another thread private static final int NO_REASON = 0; private static final int CHECK_VERSION_REASON = 1; private int privateConstructorReason = NO_REASON; private JComponentWithEvents(int privateConstructorReason) { this.privateConstructorReason = privateConstructorReason; } public static final int version = 4; public static final int getVersion() { return version; } private static boolean checkedVersion = false; private final void checkVersion() { if (checkedVersion) return; checkedVersion = true; new Thread(new JComponentWithEvents(CHECK_VERSION_REASON)).start(); } private final void runVersionCheckerInstance() { try { String url = "http://kosbie.net/cmu/JComponentWithEvents/"; Scanner scanner = null; try { scanner = new Scanner(new java.net.URL(url).openStream()); } catch (Exception e) { if (isApplet) return; } if (scanner == null) throw new RuntimeException("Cannot load version checker from " + url); int version = -1; while (scanner.hasNext()) { // contains one line: version=23 String s = scanner.next(); if (s.startsWith("version=")) { s = s.substring("version=".length()); // string can have trailing garbage, so we'll skip Integer.parseInt... int currentVersion = 0; for (int i=0; i fileToImageMap = new java.util.HashMap(); // Convert a jpg or png file into an awt Image (this is a quick way; // we really should use an ImageObserver or some other more reliable way). public final Image getImageFromFile(String filename) { Image image = fileToImageMap.get(filename); if (image != null) return image; URL url = null; if (applet != null) { try { url = new URL(applet.getCodeBase(), filename); image = applet.getImage(url); } catch (Exception e) { throw new RuntimeException(""+e); } } else { // if (!(new java.io.File(filename)).exists()) // throw new RuntimeException("File does not exist: " + filename); // image = new ImageIcon(filename).getImage(); image = loadBufferedImage(filename); } fileToImageMap.put(filename, image); return image; } ////////////////////////////////////// /// shapesIntersect Helper method ////////////////////////////////////// public final static boolean shapesIntersect(Shape s1, Shape s2) { java.awt.geom.Area area = new java.awt.geom.Area(s1); area.intersect(new java.awt.geom.Area(s2)); return !area.isEmpty(); } ////////////////////////////////////// /// getResourceAsStream Helper method ////////////////////////////////////// public final InputStream getResourceAsStream(String path) { InputStream is = getClass().getResourceAsStream(path); if (is == null) throw new RuntimeException("File " + path + " does not exist"); return is; } ////////////////////////////////////// /// loadBufferedImage and saveBufferedImage Helper methods ////////////////////////////////////// // Convert a jpg/png/etc file into an awt BufferedImage public final BufferedImage loadBufferedImage(String filename) { InputStream is = getResourceAsStream(filename); try { return ImageIO.read(is); } catch (Exception e) { throw new RuntimeException(e); } } // Save the BufferedImage into the given file -- the image type is determined // by the extension of the file (.jpg, .png, etc) public final void saveBufferedImage(BufferedImage image, String filename) { int dotIndex = filename.lastIndexOf('.'); if (dotIndex < 0) throw new RuntimeException("file must end in .jpg, .png, etc"); String format = filename.substring(dotIndex+1); String[] legalFormats = ImageIO.getReaderFormatNames(); if (java.util.Arrays.binarySearch(legalFormats, format) < 0) throw new RuntimeException("Illegal format " + format + " not in " + java.util.Arrays.toString(legalFormats)); try { ImageIO.write(image, format, new File(filename)); } catch (Exception e) { throw new RuntimeException(e); } } ////////////////////////////////////// // PRIVATE IMPLEMENTATION ////////////////////////////////////// private int _defaultWidth, _defaultHeight; private boolean _paused = false, _exited = false; private char _pauseKey = (char)0, _unpauseKey = 'p'; public final static synchronized JComponentWithEvents newInstance(Class c, int w, int h) { JComponentWithEvents result = null; if (JComponentWithEvents.width0 != -1) throw new RuntimeException("width0 is not -1"); try { JComponentWithEvents.width0 = w; JComponentWithEvents.height0 = h; result = (JComponentWithEvents)c.newInstance(); } catch (Exception e) {System.out.println("Err!");e.printStackTrace();} finally { JComponentWithEvents.width0 = -1; JComponentWithEvents.height0 = -1; } return result; } private static int width0 = -1, height0 = -1; public JComponentWithEvents() { if (width0 == -1) return; _defaultWidth = JComponentWithEvents.width0; _defaultHeight= JComponentWithEvents.height0; setPreferredSize(new Dimension(_defaultWidth,_defaultHeight)); } private void deferredDoStart() { if (!started) { start(); repaint(); started = true; } } public final void run() { if (privateConstructorReason == CHECK_VERSION_REASON) runVersionCheckerInstance(); else deferredDoStart(); } private void doStart() { SwingUtilities.invokeLater(this); } public final int getWidth() { int w = super.getWidth(); return ((w > 0) ? w : _defaultWidth); } public final int getHeight() { int h = super.getHeight(); return ((h > 0) ? h : _defaultHeight); } private boolean started = false; // set to true in start() private boolean isApplet = true; // set to false in launch() private JApplet applet = null; // set to the applet in launchApplet private boolean timerStopped = false; // to prevent paint from being inadvertently overrided public final void paint(Graphics page) { super.paint(page); } public final void paintComponent(Graphics page) { if (timer == null) { timer = new javax.swing.Timer(_timerDelay, this); if (!timerStopped) timer.start(); } if (!started) return; page.setColor(Color.white); page.fillRect(0,0,getWidth(),getHeight()); page.setColor(Color.black); if (warningMessage != null) { page.setColor(Color.red); page.setFont(new Font("SansSerif",Font.BOLD,20)); for (int i=0; i oldValuesMap = getNonLocalVariableValues(); paint((Graphics2D)page); checkNonLocalVariableValues(oldValuesMap); } } private String[] warningMessage = null; private void setWarningMessage(String... msg) { warningMessage = msg; beep(); repaint(); } private HashMap getNonLocalVariableValues() { try { HashMap valuesMap = new HashMap(); for (Field field : this.getClass().getDeclaredFields()) { field.setAccessible(true); valuesMap.put(field,get(this,field)); } return valuesMap; } catch (Exception e) { // we're in an applet and cannot access fields due to security restrictions... return null; } } private void checkNonLocalVariableValues(HashMap oldValuesMap) { if (oldValuesMap == null) return; for (Field field : this.getClass().getDeclaredFields()) { field.setAccessible(true); Object oldValue = oldValuesMap.get(field); Object newValue = get(this,field); if ((oldValue != newValue) && ((oldValue == null) || (!oldValue.equals(newValue)))) setWarningMessage("Warning: Field changed in paint method!", "Field: " + field.getName(), " Changed from: " + oldValue, " Changed to: " + newValue, "Fields should only be changed in event handlers", "and never in paint methods!"); } } private Object get(Object object, Field field) { field.setAccessible(true); Object value = null; try { value = field.get(object); } catch (Exception e) { } // System.out.println(field.getName() + ": " + value); return value; } private void doExit() { if (applet == null) System.exit(0); else { _exited = _paused = true; doStopSounds(); } } private void doBeep() { Toolkit.getDefaultToolkit().beep(); } private void doSetTimerDelay(int millis) { timerStopped = false; _timerDelay = Math.max(1,millis); if (timer != null) { timer.setDelay(_timerDelay); timer.setInitialDelay(_timerDelay); timer.restart(); } } private void doStopTimer() { timerStopped = true; if (timer != null) timer.stop(); } private javax.swing.Timer timer; private int _timerDelay = 250; // ignore these public void componentShown(ComponentEvent e) { } public void componentHidden(ComponentEvent e) { } public void componentMoved(ComponentEvent e) { } public void componentResized(ComponentEvent e) { if (!started || _paused) return; componentResized(); repaint(); } public final void actionPerformed(ActionEvent evt) { if (!started || _paused) return; timerFired(); repaint(); } private MouseEvent currentMouseEvent = null; private KeyEvent currentKeyEvent = null; public final void mousePressed(MouseEvent e) { if (!started || _paused) return; currentMouseEvent = e; mousePressed(e.getX(), e.getY()); currentMouseEvent = null; repaint(); } public final void mouseReleased(MouseEvent e) { if (!started || _paused) return; currentMouseEvent = e; mouseReleased(e.getX(), e.getY()); currentMouseEvent = null; repaint(); } public final void mouseDragged(MouseEvent e) { if (!started || _paused) return; currentMouseEvent = e; mouseDragged(e.getX(), e.getY()); currentMouseEvent = null; repaint(); } public void mouseEntered(MouseEvent e) { if (!started || _paused) return; currentMouseEvent = e; mouseEntered(e.getX(), e.getY()); currentMouseEvent = null; repaint(); } public void mouseExited(MouseEvent e) { if (!started || _paused) return; currentMouseEvent = e; mouseExited(e.getX(), e.getY()); currentMouseEvent = e; repaint(); } public void mouseClicked(MouseEvent e) { if (!started || _paused) return; currentMouseEvent = e; mouseClicked(e.getX(), e.getY(), e.getClickCount()); currentMouseEvent = e; repaint(); } public void mouseMoved(MouseEvent e) { if (!started || _paused) return; currentMouseEvent = e; mouseMoved(e.getX(), e.getY()); currentMouseEvent = e; repaint(); } public void keyReleased(KeyEvent e) { if (!started || _paused) return; char key = getKeyEventChar(e); if (key == (char)0) return; currentKeyEvent = e; keyReleased(key); currentKeyEvent = null; repaint(); } public void keyTyped(KeyEvent e) { if (!started || _paused) return; char key = getKeyEventChar(e); if (key == (char)0) return; currentKeyEvent = e; keyTyped(key); currentKeyEvent = null; repaint(); } public final void keyPressed(KeyEvent e) { if (!started || _exited) return; char key = getKeyEventChar(e); if (key == (char)0) return; currentKeyEvent = e; if (!_paused && (key == _pauseKey)) { _paused = true; doPauseSounds(); } else if (_paused && !_exited && (key == _unpauseKey)) { _paused = false; doUnpauseSounds(); } else keyPressed(key); currentKeyEvent = null; repaint(); } private boolean ignoreModifierKeys = true; private char getKeyEventChar(KeyEvent e) { int keyCode = e.getKeyCode(); char keyChar = e.getKeyChar(); char key = ((keyChar < (char)65535) ? keyChar : (char)keyCode); if (ignoreModifierKeys && ((key == SHIFT) || (key == CAPS_LOCK) || (key == CONTROL) || (key == ALT))) key = (char)0; return key; } private void addEventListeners() { addMouseListener(this); addMouseMotionListener(this); addKeyListener(this); addComponentListener(this); } public final static void launch() { launch(500,400); // default size } public final static synchronized void launch(int width, int height) { try { Class c = null; StackTraceElement[] em = new Exception().getStackTrace(); for (StackTraceElement ste : em) { Class nextC = Class.forName(ste.getClassName()); if (JComponentWithEvents.class.isAssignableFrom(nextC)) c = nextC; else break; } if (c == null) throw new RuntimeException("No main class"); JComponentWithEvents comp = JComponentWithEvents.newInstance(c,width,height); comp.isApplet = false; JFrame frame = new JFrame(comp.getClass().getName()); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JPanel cp = new JPanel(); cp.setLayout(new BorderLayout()); cp.add(comp); frame.setContentPane(cp); // comp.setPreferredSize(new Dimension(width,height)); frame.pack(); frame.setVisible(true); comp.launchApplet(null); } catch (Exception e) {System.out.println("Err!");e.printStackTrace();} } public final void launchApplet(JApplet applet) { // because when we are the JComponent of an applet, "launch" is not called try { checkVersion(); this.applet = applet; addEventListeners(); setFocusable(true); requestFocusInWindow(); // send key events to "comp" doStart(); } catch (Exception e) {System.out.println("Err!");e.printStackTrace();} } ////////////////////////// // MIDI support ////////////////////////// private URL toUrl(String location) throws Exception { if (location == null) return null; if (location .startsWith("http")) return new URL(location); return new File(location).toURI().toURL(); } private InputStream toInputStream(String location) throws Exception { byte[] data = fileCache.get(location); if (data == null) { InputStream is; if (location.startsWith("http")) is = new URL(location).openStream(); // else if (applet != null) // is = new URL(applet.getCodeBase(),location).openStream(); else { // is = new FileInputStream(new File(location)); is = getResourceAsStream(location); } ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] buf = new byte[1024]; int len; while ((len = is.read(buf)) > 0) bos.write(buf, 0, len); data = bos.toByteArray(); fileCache.put(location,data); } return new ByteArrayInputStream(data); } private java.util.HashMap fileCache = new java.util.HashMap(); private boolean inSandbox() { SecurityManager mgr = System.getSecurityManager(); if (mgr == null) return false; try { mgr.checkExit(-1); } catch (Exception e) { return true; } return false; } public final boolean isApplet() { return inSandbox(); } private void doPlay(String location, boolean loop) { if (location == null) { return; } String err = null; if (!location.endsWith("wav") && !location.endsWith("mid") && !location.endsWith("midi")) err = "Unknown file type: " + location; else { try { InputStream is = toInputStream(location); if (location.endsWith("wav")) doPlayWav(is,loop); else if (location.endsWith("midi") || location.endsWith("mid")) doPlayMidi(is,loop); } catch (Exception e) { err = e.getMessage(); if (isApplet) { if (!badAppletLocations.contains(location)) System.err.println("Applet cannot access: " + location); badAppletLocations.add(location); } else { e.printStackTrace(); throw new RuntimeException(err); } } } } // so we only report bad locations once... private HashSet badAppletLocations = new HashSet(); private void doStopSounds() { doStopWavSounds(); doStopMidiSounds(); } private void doPauseSounds() { doPauseWavSounds(); doPauseMidiSounds(); } private void doUnpauseSounds() { doUnpauseWavSounds(); doUnpauseMidiSounds(); } /////////////////// // For: midi files /////////////////// private Sequencer sequencer = null; private void doPlayMidi(InputStream is, boolean loop) { try { doStopMidiSounds(); sequencer = MidiSystem.getSequencer(); sequencer.setSequence(MidiSystem.getSequence(is)); if (loop) sequencer.setLoopCount(Sequencer.LOOP_CONTINUOUSLY); sequencer.open(); sequencer.start(); } catch (Exception e) { midiError("" + e); } } private void midiError(String msg) { System.err.println("Midi error: " + msg); sequencer = null; } private void doStopMidiSounds() { try { if ((sequencer == null) || (!sequencer.isRunning())) return; sequencer.stop(); sequencer.close(); } catch (Exception e) { midiError("" + e); } sequencer = null; } private void doPauseMidiSounds() { try { if ((sequencer == null) || (!sequencer.isRunning())) return; sequencer.stop(); // sequencer.close(); } catch (Exception e) { midiError("" + e); } // sequencer = null; } private void doUnpauseMidiSounds() { try { if (sequencer == null) return; sequencer.start(); // sequencer.close(); } catch (Exception e) { midiError("" + e); } // sequencer = null; } /////////////////////////////// // For: aif, au, and wav files /////////////////////////////// private void doPlayWav(InputStream is, boolean loop) { try { AudioInputStream stream = AudioSystem.getAudioInputStream(is); AudioFormat format = stream.getFormat(); // Create the clip DataLine.Info info = new DataLine.Info( Clip.class, stream.getFormat(), ((int)stream.getFrameLength()*format.getFrameSize())); Clip clip = (Clip) AudioSystem.getLine(info); // This method does not return until the audio file is completely loaded clip.open(stream); // Start playing clip.start(); } catch (Exception e) { throw new RuntimeException("" + e); } } private java.util.ArrayList wavClips = new java.util.ArrayList(); private void doPauseWavSounds() { // @TODO: this does not stop the wav sounds! for (Clip clip : wavClips) { clip.stop(); } } private void doUnpauseWavSounds() { for (Clip clip : wavClips) clip.start(); } private void doStopWavSounds() { for (Clip clip : wavClips) clip.stop(); wavClips.clear(); } /////////////////////////////// // For testing purposes /////////////////////////////// public static void main(String[] args) { launch(500,300); } }