/*
* LiveWindowJOGL.java
*
* Copyright (C) 2006-2007 Gabriel Burca (gburca dash virtmus at ebixio dot com)
*
* This program 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 2
* of the License, or (at your option) any later version.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package com.ebixio.virtmus;
import com.ebixio.util.Log;
import com.ebixio.virtmus.options.Options;
import com.sun.opengl.util.j2d.TextRenderer;
import com.sun.opengl.util.texture.Texture;
import com.sun.opengl.util.texture.TextureCoords;
import com.sun.opengl.util.texture.TextureIO;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Rectangle;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.text.DecimalFormat;
import java.util.Hashtable;
import java.util.Vector;
import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCanvas;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.glu.GLU;
import javax.swing.WindowConstants;
import org.jdesktop.animation.timing.*;
/**
*
* @author Gabriel Burca <gburca dash virtmus at ebixio dot com>
*/
public class LiveWindowJOGL extends javax.swing.JFrame
implements GLEventListener, Renderer.JobRequester {
private static final GLU glu = new GLU();
final com.sun.opengl.util.Animator animator;
private TextRenderer textRenderer;
private String fpsText;
private int fpsWidth;
private long startTime;
private int frameCount;
private DecimalFormat format = new DecimalFormat("####.00");
Rectangle displaySize = new Rectangle(Utils.getScreenSize()); // Physical display size
Dimension windowSize = new Dimension(0, 0); // OpenGL window size
Dimension windowSizeR = new Dimension(0, 0); // OpenGL window size once rotated as needed
final double cameraDist = 1000d; // Camera distance from pages
private Song song = null;
boolean waitingForImage = false;
boolean fullyPainted = false;
private int page = 0;
final int maxPrevCache = 3;
final int maxNextCache = 4;
Hashtable<Integer, TexturePage> pageCache = new Hashtable<Integer, TexturePage>(3);
Vector<Integer> toBeRendered = new Vector<Integer>(3);
private Animator pageShiftAnim;
private float pageShift = 0;
private float pageShiftAmount = 0;
/** Creates a new instance of LiveWindowJOGL */
public LiveWindowJOGL() {
GLCapabilities caps = new GLCapabilities();
Log.log("JOGL caps: " + caps.toString());
GLCanvas canvas = new GLCanvas(caps);
canvas.addGLEventListener(this);
this.add(canvas, BorderLayout.CENTER);
setCursor(Utils.getInvisibleCursor());
setResizable(false);
setUndecorated(true);
setSize(displaySize.getSize());
//this.setSize(displaySize.width - 200, displaySize.height - 300);
this.setLocationRelativeTo(null);
this.setLocation(0, 0);
this.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
KeyAdapter ka = new KeyAdapter() {
@Override
public void keyPressed(KeyEvent evt) {
formKeyPressed(evt);
}
};
addKeyListener(ka);
canvas.addKeyListener(ka);
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) {
KeyEvent evt = new KeyEvent(null, KeyEvent.KEY_PRESSED, 0, 0, KeyEvent.VK_PAGE_DOWN, KeyEvent.CHAR_UNDEFINED);
formKeyPressed(evt);
} else {
KeyEvent evt = new KeyEvent(null, KeyEvent.KEY_PRESSED, 0, 0, KeyEvent.VK_PAGE_UP, KeyEvent.CHAR_UNDEFINED);
formKeyPressed(evt);
}
}
});
initTimers();
animator = new com.sun.opengl.util.Animator(canvas);
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
// Run this on another thread than the AWT event queue to
// make sure the call to Animator.stop() completes before
// exiting
new Thread(new Runnable() {
public void run() {
animator.stop();
}
}).start();
}
});
animator.start();
}
public static void main(String[] args) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
new LiveWindowJOGL().setVisible(true);
}
});
}
// <editor-fold desc=" GLEventListener interface ">
/** One time call when OpenGL is initialized */
public void init(GLAutoDrawable drawable) {
GL gl = drawable.getGL();
gl.setSwapInterval(1);
gl.glEnable(GL.GL_DEPTH_TEST);
gl.glEnable(GL.GL_TEXTURE_2D);
gl.glShadeModel(GL.GL_SMOOTH);
gl.glHint(GL.GL_PERSPECTIVE_CORRECTION_HINT, GL.GL_NICEST);
// Set erase color
gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
// Set drawing color
gl.glColor3f(1.0f, 1.0f, 1.0f);
initTextRenderer();
windowSize = new Dimension(drawable.getWidth(), drawable.getHeight());
windowSizeR = Options.findInstance().screenRot.getSize(windowSize);
//IntBuffer i = IntBuffer.wrap(new int[1]);
//gl.glGetIntegerv(GL.GL_MAX_TEXTURE_SIZE, i);
showPage(page);
}
/** Requests component to draw itself */
public void display(GLAutoDrawable drawable) {
GL gl = drawable.getGL();
gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
gl.glMatrixMode(GL.GL_MODELVIEW);
gl.glLoadIdentity();
// gluLookAt (from, to, up-direction)
//glu.gluLookAt(0d, 0d, cameraDist, 0d, 0d, 0d, 0d, 1d, 0d);
glu.gluLookAt(pageShift, 0, cameraDist, pageShift, 0, 0, 0, 1d, 0);
if (Options.findInstance().scrollDir == Options.ScrollDir.Vertical) {
renderPageVertical(drawable);
} else {
renderPageHorizontal(drawable);
}
if (gl.glGetError() != GL.GL_NO_ERROR) {
Log.log("OpenGL error: " + gl.glGetError());
}
displayFPSText(drawable);
gl.glFlush();
}
/** Signals us that the component's location or size has been changed.
* Also called after init(). */
public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
GL gl = drawable.getGL();
gl.glViewport(0, 0, width, height);
gl.glMatrixMode(GL.GL_PROJECTION);
gl.glLoadIdentity();
boolean ortho = false;
if (ortho) {
//glu.gluOrtho2D(-width, width, -height, height);
gl.glOrtho(0, width, -height/2, height/2, 500d, 1200d);
} else {
double aspectRatio = (double)width / (double)height;
double angle = Math.atan((double)height / 2d / cameraDist) * 2;
angle *= 180d / Math.PI; // Convert radians to degrees
glu.gluPerspective(angle, aspectRatio, 500.0, 1200.0);
}
gl.glMatrixMode(GL.GL_MODELVIEW);
gl.glLoadIdentity();
}
/** Signals us that the display mode or device has changed (color depth, etc...).
* Also, that the window has been dragged from one monitor (a "device") to another. */
public void displayChanged(GLAutoDrawable arg0, boolean arg1, boolean arg2) {
//throw new UnsupportedOperationException("Not supported yet.");
}
// </editor-fold>
// <editor-fold defaultstate="collapsed" desc=" FPS Text ">
private void initTextRenderer() {
// Create the text renderer
Font font = new Font("SansSerif", Font.BOLD, 24);
textRenderer = new TextRenderer(font, true, false);
}
private void displayFPSText(GLAutoDrawable drawable) {
if (++frameCount == 100) {
long endTime = System.currentTimeMillis();
float fps = 100.0f / (float) (endTime - startTime) * 1000;
frameCount = 0;
startTime = System.currentTimeMillis();
fpsText = "FPS: " + format.format(fps);
if (fpsWidth == 0) {
// Place it at a fixed offset wrt the upper right corner
fpsWidth = (int)
textRenderer.getBounds("FPS: 10000.00").getWidth();
}
}
if (fpsWidth == 0) {
return;
}
// Calculate text location and color
int x = drawable.getWidth() - fpsWidth - 5;
int y = drawable.getHeight() - 30;
float c = 0.55f;
// Render the text
textRenderer.beginRendering(drawable.getWidth(), drawable.getHeight());
textRenderer.setColor(c, c, c, c);
textRenderer.draw(fpsText, x, y);
textRenderer.endRendering();
}
// </editor-fold>
private void renderPageVertical(GLAutoDrawable drawable) {
GL gl = drawable.getGL();
gl.glPushMatrix();
if (Options.findInstance().screenRot != Options.Rotation.Clockwise_0) {
gl.glRotatef(-Options.findInstance().screenRot.degrees(), 0, 0, 1);
}
TexturePage tp1, tp2, tp3;
synchronized(pageCache) { tp1 = pageCache.get(page); }
if (tp1 != null && tp1.getTexture() != null) {
//gl.glTranslatef(-tp1.dim.width / 2, (windowSizeR.height - tp1.dim.height) / 2 + (pageShift * tp1.dim.height), 0);
gl.glTranslatef(-tp1.dim.width / 2, (windowSizeR.height - tp1.dim.height) / 2, 0);
renderPage(drawable, tp1);
synchronized(pageCache) { tp2 = pageCache.get(page + 1); }
if (tp2 != null && tp2.getTexture() != null) {
gl.glTranslatef(0, -tp2.dim.height, 0);
renderPage(drawable, tp2);
synchronized(pageCache) { tp3 = pageCache.get(page + 2); }
if (tp3 != null && tp3.getTexture() != null) {
gl.glTranslatef(0, -tp3.dim.height, 0);
renderPage(drawable, tp3);
}
}
}
gl.glPopMatrix();
}
private void renderPageHorizontal(GLAutoDrawable drawable) {
GL gl = drawable.getGL();
gl.glPushMatrix();
if (Options.findInstance().screenRot != Options.Rotation.Clockwise_0) {
gl.glRotatef(-Options.findInstance().screenRot.degrees(), 0, 0, 1);
}
TexturePage tp1, tp2, tp3, tp4;
synchronized(pageCache) { tp1 = pageCache.get(page); }
if (tp1 != null && tp1.getTexture() != null) {
//gl.glTranslatef(-windowSizeR.width / 2 - (pageShift * tp1.dim.width), 0, 0);
gl.glTranslatef(-windowSizeR.width / 2, 0, 0);
renderPage(drawable, tp1);
synchronized(pageCache) { tp2 = pageCache.get(page + 1); }
if (tp2 != null && tp2.getTexture() != null) {
gl.glTranslatef(tp1.dim.width, 0, 0);
renderPage(drawable, tp2);
synchronized(pageCache) { tp3 = pageCache.get(page + 2); }
if (tp3 != null && tp3.getTexture() != null) {
gl.glTranslatef(tp2.dim.width, 0, 0);
renderPage(drawable, tp3);
synchronized(pageCache) { tp4 = pageCache.get(page + 3); }
if (tp4 != null && tp4.getTexture() != null) {
gl.glTranslatef(tp3.dim.width, 0, 0);
renderPage(drawable, tp4);
}
}
}
}
gl.glPopMatrix();
}
/** All pages are placed on the XY plane with Z=0.
* What moves, the model (music pages) or the view/camera (the laptop display)?
* We can place the first page with its lower-left corner at 0,0 and string the rest of the
* pages to the right. If the user decides to start off with page 7, we'll have to figure
* out the width of pages 1-6 so we know at what X-coordinate to start page 7 at.
* Alternatively, we can keep the view fixed at 0,0 and move the model so that the current
* page shows up at the left of the screen. This way we don't need to know the width of all
* preceeding pages. We always place the current page at the same X,Y.
* */
private float renderPage(GLAutoDrawable drawable, TexturePage tp) {
GL gl = drawable.getGL();
Texture texture = tp.getTexture();
if (texture == null) return 0;
TextureCoords tc = texture.getImageTexCoords();
float tx1 = tc.left();
float ty1 = tc.top();
float tx2 = tc.right();
float ty2 = tc.bottom();
float w = tp.dim.width;
float h2 = (float)tp.dim.height / 2;
texture.enable();
texture.bind();
gl.glBegin(GL.GL_QUADS);
gl.glColor4f(1f, 1f, 1f, 1f);
gl.glTexCoord2f(tx1, ty1); gl.glVertex3f(0, h2, 0f);
gl.glTexCoord2f(tx2, ty1); gl.glVertex3f(w, h2, 0f);
gl.glTexCoord2f(tx2, ty2); gl.glVertex3f(w, -h2, 0f);
gl.glTexCoord2f(tx1, ty2); gl.glVertex3f(0, -h2, 0f);
gl.glEnd();
texture.disable();
return w;
}
protected void initTimers() {
//pageShiftAnim = PropertySetter.createAnimator(1000, this, "pageShift", 0f, 1f);
pageShiftAnim = new Animator(500, new TimingTargetAdapter() {
@Override
public void timingEvent(float fraction) { setPageShift(pageShiftAmount * fraction); }
@Override
public void end() {
setPageShift(pageShiftAmount);
page++;
setPageShift(0);
java.awt.EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException ex) {
}
showPage(page);
}
});
}
});
}
public float getPageShift() { return pageShift; }
public void setPageShift(float shift) { this.pageShift = shift; Log.log("Shift: " + shift);}
// <editor-fold defaultstate="collapsed" desc=" Rendering Cache ">
private void renderNext() {
if (song == null || waitingForImage) return;
if (toBeRendered.size() > 0) {
int newPage = toBeRendered.remove(0);
if (newPage < 0 || newPage >= song.pageOrder.size()) return;
Renderer.JobRequest request = new Renderer.JobRequest(
this, newPage, Math.abs(page - newPage),
Options.findInstance().screenRot.getSize(windowSize));
Renderer.requestRendering(request, song.pageOrder.get(newPage));
this.waitingForImage = true;
}
}
@Override
public void renderingComplete(MusicPage mp, Renderer.JobRequest request) {
waitingForImage = false;
if (song == null) return;
if (request.pageNr >= 0 && request.requester == this) {
synchronized(pageCache) {
pageCache.put(request.pageNr, new TexturePage(Renderer.getRenderedImage(this)));
}
//if (request.pageNr == page || !fullyPainted) repaint();
}
cleanCache();
renderNext();
}
private void repopulateCache(int page) {
if (song == null) return;
int range = Math.max(maxPrevCache, maxNextCache);
toBeRendered.clear();
synchronized(pageCache) {
if (!pageCache.containsKey(page)) toBeRendered.add(page);
// Render closest pages first, then next removed, etc...
for (int i = 1; i <= range; i++) {
if (i <= maxNextCache && !pageCache.containsKey(page + i)) {
if (page + i < song.pageOrder.size()) toBeRendered.add(page + i);
}
if (i <= maxPrevCache && !pageCache.containsKey(page - i)) {
if (page - i >= 0) toBeRendered.add(page - i);
}
}
}
renderNext();
}
private void cleanCache() {
cleanCache(page);
}
private void cleanCache(int currentPage) {
int lastPage = 0;
synchronized(pageCache) {
for (int i = 0; i < currentPage - maxPrevCache; i++) {
if (pageCache.containsKey(i)) {
pageCache.remove(i);
Log.log("LiveWindow: removed page " + i);
}
}
for (int i: pageCache.keySet()) {
if (i > lastPage) lastPage = i;
}
for (int i = currentPage + maxNextCache + 1; i < lastPage; i++) {
if (pageCache.containsKey(i)) {
pageCache.remove(i);
Log.log("LiveWindow: removed page " + i);
}
}
}
}
// </editor-fold>
protected void formKeyPressed(java.awt.event.KeyEvent evt) {
switch(evt.getKeyCode()) {
case KeyEvent.VK_ESCAPE:
animator.stop();
this.dispose();
break;
case KeyEvent.VK_1:
showPage(page = 0);
break;
case KeyEvent.VK_PAGE_UP:
showPage(--page);
break;
case KeyEvent.VK_PAGE_DOWN:
case KeyEvent.VK_SPACE:
TexturePage tp;
synchronized(pageCache) { tp = pageCache.get(page); }
if (tp != null) {
pageShiftAmount = tp.dim.width;
}
this.pageShiftAnim.start();
//showPage(++page);
break;
default:
break;
}
}
private void showPage(int page) {
cleanCache(page);
repopulateCache(page);
//setPageShift(0);
//this.repaint();
}
// <editor-fold defaultstate="collapsed" desc=" Set song/playlist ">
public void setSong(Song song) {
if (song == null || song.pageOrder.size() == 0) return;
setSong(song, song.pageOrder.get(0));
}
public void setSong(Song song, MusicPage startingPage) {
this.song = song;
page = song.pageOrder.indexOf(startingPage);
//showPage(page);
}
public void setPlayList(PlayList playList) {
Song s = new Song();
synchronized (playList.songs) {
for (Song plSong: playList.songs) {
synchronized(plSong.pageOrder) {
for (MusicPage mp: plSong.pageOrder) s.pageOrder.add(mp);
}
}
}
setSong(s);
}
// </editor-fold>
// <editor-fold defaultstate="collapsed" desc=" TexturePage class ">
private class TexturePage {
protected Texture texture = null;
public Dimension dim;
protected BufferedImage img;
protected boolean converted = false;
public TexturePage(BufferedImage img) {
this.img = img;
this.dim = new Dimension(img.getWidth(), img.getHeight());
}
/** This can only be called from a thread that has an OpenGL context or it will throw an exception. */
public Texture getTexture() {
if (!converted) {
converted = true;
texture = TextureIO.newTexture(img, true);
img = null; // no longer needed
if (texture != null) {
texture.setTexParameteri(GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);
texture.setTexParameteri(GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
}
}
return texture;
}
}
// </editor-fold>
}