package net.sf.fmj.gui.controlpanel;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.logging.Logger;
import javax.media.Controller;
import javax.media.ControllerClosedEvent;
import javax.media.ControllerErrorEvent;
import javax.media.ControllerEvent;
import javax.media.ControllerListener;
import javax.media.Duration;
import javax.media.MediaTimeSetEvent;
import javax.media.Player;
import javax.media.RealizeCompleteEvent;
import javax.media.ResourceUnavailableEvent;
import javax.media.RestartingEvent;
import javax.media.StartEvent;
import javax.media.StopEvent;
import javax.media.Time;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JToggleButton;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import net.sf.fmj.ejmf.toolkit.util.SourcedTimer;
import net.sf.fmj.ejmf.toolkit.util.SourcedTimerEvent;
import net.sf.fmj.ejmf.toolkit.util.SourcedTimerListener;
import net.sf.fmj.ejmf.toolkit.util.TimeSource;
import net.sf.fmj.gui.customslider.CustomSlider;
import net.sf.fmj.utility.LoggerSingleton;
/**
*
* @author Warren Bloomer
*
*/
public class SwingLookControlPanel extends JPanel implements TimeSource, SourcedTimerListener {
// TODO: the buttons need to enable/disable based on the controller state
private static final Logger logger = LoggerSingleton.logger;
public static final int USE_PLAY_CONTROL = 0x0001;
public static final int USE_STOP_CONTROL = 0x0002;
public static final int USE_BACK_CONTROL = 0x0004;
public static final int USE_FORWARD_CONTROL = 0x0008;
public static final int USE_NEXT_CONTROL = 0x0010;
public static final int USE_PREVIOUS_CONTROL = 0x0020;
public static final int USE_POSITION_CONTROL = 0x0040;
public static final int USE_POSITION_TEXT = 0x0080;
public static final int USE_LENGTH_TEXT = 0x0100;
public static final int USE_VOLUME_CONTROL = 0x0200;
public static final int USE_MUTE_CONTROL = 0x0400;
public static final int SINGLE_LINE = 0x0800;
public static final int DEFAULT_FLAGS = USE_PLAY_CONTROL | /*USE_STOP_CONTROL | USE_BACK_CONTROL | USE_FORWARD_CONTROL |*/ USE_NEXT_CONTROL | USE_PREVIOUS_CONTROL | USE_POSITION_CONTROL | USE_LENGTH_TEXT | USE_POSITION_TEXT | USE_VOLUME_CONTROL | USE_MUTE_CONTROL;
private static final boolean USE_STANDARD_SLIDER = true; // true to use a JSlider for the position slider, false to use FmjSlider
private final int flags;
private final Skin skin;
public static final Skin DEFAULT_SKIN = new DefaultSkin();
private Player player;
private SourcedTimer controlTimer;
// Timer will fire every TIMER_TICK milliseconds
final private static int TIMER_TICK = 250;
private JButton playButton = null;
private JButton stopButton = null;
private JButton backButton = null;
private JButton forwardButton = null;
private JButton nextButton = null;
private JButton previousButton = null;
private JPanel positionPanel = null;
private JSlider positionSlider = null;
private JPanel buttonPanel = null;
private JLabel positionLabel = null;
private JLabel lengthLabel = null;
private JPanel audioPanel = null;
private JSlider volumeSlider = null;
private JToggleButton muteButton = null;
public SwingLookControlPanel(int flags, Skin skin) {
super();
this.flags = flags;
this.skin = skin;
initialize();
}
public SwingLookControlPanel(int flags, Skin skin, Player player) {
super();
this.flags = flags;
this.skin = skin;
initialize();
setPlayer(player);
}
public SwingLookControlPanel() {
this(DEFAULT_FLAGS, DEFAULT_SKIN);
}
public SwingLookControlPanel(Player player) {
this(DEFAULT_FLAGS, DEFAULT_SKIN, player);
}
public void setPlayer(Player player) {
this.player = player;
final TransportControlState transportControlState = new TransportControlState();
if (player.getState() == Controller.Started)
{
transportControlState.setAllowPlay(false);
transportControlState.setAllowStop(true);
transportControlState.setAllowVolume(player.getGainControl() != null);
}
else
{
transportControlState.setAllowPlay(true);
transportControlState.setAllowStop(false);
transportControlState.setAllowVolume(false); // can't get gain control on unrealized player
}
if (getPreviousButton() != null)
getPreviousButton().setEnabled(true);
if (getNextButton() != null)
getNextButton().setEnabled(true);
if (getForwardButton() != null)
getForwardButton().setEnabled(true);
if (getBackButton() != null)
getBackButton().setEnabled(true);
onStateChange(transportControlState);
// Setup timer
controlTimer = new SourcedTimer(this, TIMER_TICK);
controlTimer.addSourcedTimerListener(this);
if (player.getState() == Controller.Started)
{
onDurationChange(player.getDuration().getNanoseconds());
controlTimer.start(); // this handles the case where it is already started before we get here, in which case controllerUpdate will never get called with the initial state
}
player.addControllerListener(controllerListener);
}
/** controller listener to listen to controller events from the player */
private ControllerListener controllerListener = new ControllerListener() {
public void controllerUpdate(ControllerEvent event) {
logger.fine("Got controller event: " + event);
final Player player = (Player) event.getSourceController();
if (player != SwingLookControlPanel.this.player)
return; // ignore messages from old players.
// TODO: handle RestartingEvent
if (event instanceof RealizeCompleteEvent) {
// controller realized
}
else if (event instanceof ResourceUnavailableEvent) {
}
else if (event instanceof StopEvent) {
final TransportControlState transportControlState = new TransportControlState();
transportControlState.setAllowPlay(true);
transportControlState.setAllowStop(false);
transportControlState.setAllowVolume(player.getGainControl() != null);
SwingLookControlPanel.this.onStateChange(transportControlState);
}
else if (event instanceof StartEvent)
{
final TransportControlState transportControlState = new TransportControlState();
transportControlState.setAllowPlay(false);
transportControlState.setAllowStop(true);
transportControlState.setAllowVolume(player.getGainControl() != null);
SwingLookControlPanel.this.onStateChange(transportControlState);
}
else if (event instanceof ControllerErrorEvent)
{
}
else if (event instanceof ControllerClosedEvent)
{
}
// Slider-related: start/stop timer, etc:
// if (isOperational())
{
if (event instanceof StartEvent || event instanceof RestartingEvent)
{
SwingLookControlPanel.this.onDurationChange(player.getDuration().getNanoseconds());
SwingLookControlPanel.this.onProgressChange(getTime());
controlTimer.start();
} else if (event instanceof StopEvent || event instanceof ControllerErrorEvent)
{
controlTimer.stop();
} else if (event instanceof MediaTimeSetEvent)
{
SwingLookControlPanel.this.onDurationChange(player.getDuration().getNanoseconds()); // just in case
// This catches any direct setting of media time
// by application. Additionally, it catches
// setMediaTime(0) by StandardStopControl.
SwingLookControlPanel.this.onProgressChange(getTime());
}
}
}
};
/**
* This method initializes this
*
*/
private void initialize() {
if ((flags & SINGLE_LINE) != 0)
{
this.setLayout(new BorderLayout());
this.setSize(new Dimension(553, 58));
if (getButtonPanel() != null)
this.add(getButtonPanel(), BorderLayout.WEST);
if (getPositionPanel() != null)
this.add(getPositionPanel(), BorderLayout.CENTER);
if (getAudioPanel() != null)
this.add(getAudioPanel(), BorderLayout.EAST);
}
else
{
this.setLayout(new BorderLayout());
this.setSize(new Dimension(553, 58));
if (getPositionPanel() != null)
this.add(getPositionPanel(), BorderLayout.NORTH);
if (getButtonPanel() != null)
this.add(getButtonPanel(), BorderLayout.WEST);
if (getAudioPanel() != null)
this.add(getAudioPanel(), BorderLayout.EAST);
}
setAudioControlEnabled(false);
}
private void start() {
if (player != null) {
player.start();
}
}
private void stop() {
if (player != null) {
player.stop();
player.setMediaTime(new Time(0));
}
}
private void pause() {
if (player != null) {
player.stop();
//player.setPosition(0);
}
}
private void setRate(float rate) {
if (player != null) {
player.setRate(rate);
}
}
private void setGain(float gain) {
if (player != null && player.getGainControl() != null)
{
player.getGainControl().setLevel(gain);
}
}
private void setMute(boolean mute) {
if (player != null && player.getGainControl() != null)
{
player.getGainControl().setMute(mute);
}
}
/**
* This method initializes playButton
*
* @return javax.swing.JButton
*/
private JButton getPlayButton() {
if ((flags & USE_PLAY_CONTROL) == 0)
return null;
if (playButton == null) {
playButton = new JButton();
//playButton.setText("play");
playButton.setOpaque(false);
playButton.setIcon(skin.getPlayIcon());
playButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent e) {
if (playButtonIsPause)
pause();
else
{
setRate(1.0f); // TODO: necessary?
start();
}
}
});
playButton.setEnabled(false);
}
return playButton;
}
/**
* This method initializes stopButton
*
* @return javax.swing.JButton
*/
private JButton getStopButton() {
if ((flags & USE_STOP_CONTROL) == 0)
return null;
if (stopButton == null) {
stopButton = new JButton();
//stopButton.setText("stop");
stopButton.setOpaque(false);
stopButton.setIcon(skin.getStopIcon());
stopButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent e) {
stop();
}
});
stopButton.setEnabled(false);
}
return stopButton;
}
/**
* This method initializes backButton
*
* @return javax.swing.JButton
*/
private JButton getBackButton() {
if ((flags & USE_BACK_CONTROL) == 0)
return null;
if (backButton == null) {
backButton = new JButton();
//backButton.setText("back");
backButton.setOpaque(false);
backButton.setIcon(skin.getRewindIcon());
backButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent e) {
setRate(-2.0f);
}
});
backButton.setEnabled(false);
}
return backButton;
}
/**
* This method initializes forwardButton
*
* @return javax.swing.JButton
*/
private JButton getForwardButton() {
if ((flags & USE_FORWARD_CONTROL) == 0)
return null;
if (forwardButton == null) {
forwardButton = new JButton();
//forwardButton.setText("forward");
forwardButton.setOpaque(false);
forwardButton.setIcon(skin.getFastForwardIcon());
forwardButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent e) {
setRate(2.0f);
}
});
forwardButton.setEnabled(false);
}
return forwardButton;
}
/**
* This method initializes nextButton
*
* @return javax.swing.JButton
*/
private JButton getNextButton() {
if ((flags & USE_NEXT_CONTROL) == 0)
return null;
if (nextButton == null) {
nextButton = new JButton();
//nextButton.setText("next");
nextButton.setIcon(skin.getStepForwardIcon());
nextButton.setOpaque(false);
nextButton.setEnabled(false);
}
return nextButton;
}
/**
* This method initializes previousButton
*
* @return javax.swing.JButton
*/
private JButton getPreviousButton() {
if ((flags & USE_PREVIOUS_CONTROL) == 0)
return null;
if (previousButton == null) {
previousButton = new JButton();
previousButton.setOpaque(false);
//previousButton.setText("previous");
previousButton.setIcon(skin.getStepBackwardIcon());
previousButton.setEnabled(false);
}
return previousButton;
}
/**
* This method initializes positionPanel
*
* @return javax.swing.JPanel
*/
private JPanel getPositionPanel() {
if ((flags & USE_POSITION_CONTROL) == 0)
return null;
if (positionPanel == null) {
if ((flags & USE_LENGTH_TEXT) != 0)
{
lengthLabel = new JLabel();
lengthLabel.setText(nanosToString(0));
lengthLabel.setOpaque(false);
}
if ((flags & USE_POSITION_TEXT) != 0)
{
positionLabel = new JLabel();
positionLabel.setText(nanosToString(0));
positionLabel.setOpaque(false);
}
positionPanel = new JPanel();
positionPanel.setOpaque(false);
positionPanel.setLayout(new BorderLayout());
positionPanel.add(getPositionSlider(), BorderLayout.CENTER);
if (positionPanel != null)
positionPanel.add(positionLabel, BorderLayout.WEST);
if (lengthLabel != null)
positionPanel.add(lengthLabel, BorderLayout.EAST);
}
return positionPanel;
}
/**
* This method initializes positionSlider
*
* @return javax.swing.JSlider
*/
private JSlider getPositionSlider() {
if ((flags & USE_POSITION_CONTROL) == 0)
return null;
if (positionSlider == null) {
positionSlider = USE_STANDARD_SLIDER ? new JSlider() : new CustomSlider();
positionSlider.setOpaque(false);
positionSlider.setValue(0);
positionSlider.setMinimum(0);
positionSlider.setMaximum(0);
positionSlider.setEnabled(false);
positionSlider.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e)
{ // note: this gets called whether the user updates it, or whether it is updated automatically.
if (suppressProgressChangeNotification)
return;
final JSlider source = (JSlider) e.getSource();
sliderDragInProgress = source.getValueIsAdjusting();
if (source.getValueIsAdjusting())
return; // we only care about "final" positions the user has adjusted to.
final int valueMillis = source.getValue();
logger.fine("User adjusted position slider to (millis): " + valueMillis + " from " + source.getMinimum() + "-" + source.getMaximum());
player.setMediaTime(new Time(((double) valueMillis) / 1000.0)); // TODO: use nanos
}
});
}
return positionSlider;
}
/**
* This method initializes buttonPanel
*
* @return javax.swing.JPanel
*/
private JPanel getButtonPanel() {
if (buttonPanel == null) {
buttonPanel = new JPanel();
buttonPanel.setLayout(new GridBagLayout());
buttonPanel.setOpaque(false);
if (getPreviousButton() != null)
buttonPanel.add(getPreviousButton(), new GridBagConstraints());
if (getBackButton() != null)
buttonPanel.add(getBackButton(), new GridBagConstraints());
if (getStopButton() != null)
buttonPanel.add(getStopButton(), new GridBagConstraints());
if (getPlayButton() != null)
buttonPanel.add(getPlayButton(), new GridBagConstraints());
if (getForwardButton() != null)
buttonPanel.add(getForwardButton(), new GridBagConstraints());
if (getNextButton() != null)
buttonPanel.add(getNextButton(), new GridBagConstraints());
}
return buttonPanel;
}
/**
* This method initializes audioPanel
*
* @return javax.swing.JPanel
*/
private JPanel getAudioPanel() {
if ((flags & USE_VOLUME_CONTROL) == 0)
return null;
if (audioPanel == null) {
GridBagConstraints gridBagConstraints = new GridBagConstraints();
gridBagConstraints.fill = GridBagConstraints.VERTICAL;
gridBagConstraints.weightx = 1.0;
audioPanel = new JPanel();
audioPanel.setLayout(new GridBagLayout());
audioPanel.setOpaque(false);
audioPanel.add(getVolumeSlider(), gridBagConstraints);
if (getMuteButton() != null)
audioPanel.add(getMuteButton(), new GridBagConstraints());
}
return audioPanel;
}
public void setAudioControlEnabled(boolean enabled) {
if (getVolumeSlider() != null)
getVolumeSlider().setEnabled(enabled);
if (getMuteButton() != null)
getMuteButton().setEnabled(enabled);
}
/**
* This method initializes volumeSlider
*
* @return javax.swing.JSlider
*/
private JSlider getVolumeSlider() {
if ((flags & USE_VOLUME_CONTROL) == 0)
return null;
if (volumeSlider == null) {
volumeSlider = new JSlider();
volumeSlider.setMinimum(0);
volumeSlider.setMaximum(100);
volumeSlider.setValue(70);
volumeSlider.setPreferredSize(new Dimension(100, 29));
volumeSlider.setOpaque(false);
volumeSlider.addChangeListener(
new ChangeListener() {
public void stateChanged(ChangeEvent e) {
if (!volumeSlider.getValueIsAdjusting()) {
float newValue = (float) volumeSlider.getValue() / 100.0f;
setGain(newValue);
}
}
}
);
}
return volumeSlider;
}
/**
* This method initializes muteButton
*
* @return javax.swing.JToggleButton
*/
private JToggleButton getMuteButton() {
if ((flags & USE_VOLUME_CONTROL) == 0)
return null;
if ((flags & USE_MUTE_CONTROL) == 0)
return null;
if (muteButton == null) {
muteButton = new JToggleButton();
muteButton.setIcon(skin.getMuteOffIcon());
muteButton.setOpaque(false);
muteButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e)
{
setMute(muteButton.isSelected());
muteButton.setIcon(muteButton.isSelected() ? skin.getMuteOnIcon() : skin.getMuteOffIcon());
}
});
}
return muteButton;
}
boolean playButtonIsPause;
public void onStateChange(TransportControlState state)
{
if (getStopButton() != null)
getStopButton().setEnabled(state.isAllowStop());
if (getPlayButton() != null)
{
if (state.isAllowPlay() || state.isAllowStop())
{
if (state.isAllowStop())
{ getPlayButton().setIcon(skin.getPauseIcon());
playButtonIsPause = true;
}
else
{ getPlayButton().setIcon(skin.getPlayIcon());
playButtonIsPause = false;
}
getPlayButton().setEnabled(state.isAllowPlay() || state.isAllowStop());
}
}
setAudioControlEnabled(state.isAllowVolume());
}
private static int nanosToMillis(long nanos)
{ return (int) (nanos / 1000000L);
}
public void onDurationChange(long nanos)
{
if (nanos == Duration.DURATION_UNKNOWN.getNanoseconds() ||
nanos == Duration.DURATION_UNBOUNDED.getNanoseconds())
{
if (positionSlider != null)
{
suppressProgressChangeNotification = true;
positionSlider.setEnabled(false);
suppressProgressChangeNotification = false;
}
if (lengthLabel != null)
lengthLabel.setText("");
}
else
{
if (positionSlider != null)
{
suppressProgressChangeNotification = true;
positionSlider.setEnabled(true);
positionSlider.setMinimum(0);
positionSlider.setMaximum(nanosToMillis(nanos)); // millis is good enough.
suppressProgressChangeNotification = false;
}
if (lengthLabel != null)
lengthLabel.setText(nanosToString(nanos));
}
}
// TODO: make sure there are no race conditions with suppressProgressChangeNotification and sliderDragInProgress
// TODO: make sure that after a drag is done we update to the correct position
// TODO: should positionLabel get updated during a slider drag?
private volatile boolean suppressProgressChangeNotification = false; // to prevent us from treating a progress change to the slider generated internally like a user one
private volatile boolean sliderDragInProgress = false; // prevent us from moving the slider while the user is dragging it
public void onProgressChange(long nanos)
{
if (positionSlider != null)
{
if (positionSlider.isEnabled())
{ if (!sliderDragInProgress)
{
suppressProgressChangeNotification = true;
positionSlider.setValue(nanosToMillis(nanos));
suppressProgressChangeNotification = false;
}
}
}
if (positionLabel != null)
positionLabel.setText(nanosToString(nanos));
}
private static String zeroPad(int i, int len)
{
String result = Integer.toString(i);
while (result.length() < len)
result = "0" + result;
return result;
}
private static String nanosToString(long nanos)
{
final long seconds = nanos / 1000000000L;
final long minutes = seconds / 60;
return "" + zeroPad((int) minutes, 2) + ":" + zeroPad((int) (seconds % 60), 2);
}
/**
* This method implements the SourcedTimerListener interface.
* Each timer tick causes slider thumbnail to move if a ProgressBar
* was built for this control panel.
*
* @see net.sf.fmj.ejmf.toolkit.util.SourcedTimer
*/
public void timerUpdate(SourcedTimerEvent e) {
// Since we are also the TimeSource, we can get
// directly from StandardControls instance.
// Normally, one would call e.getTime().
onProgressChange(getTime());
}
// For TimeSource interface
/**
* As part of TimeSource interface, getTime returns
* the current media time in nanoseconds.
*/
public long getTime() {
if (player == null)
return 0L;
return player.getMediaNanoseconds();
}
/**
* This method is used as a divisor to convert
* getTime to seconds.
*/
public long getConversionDivisor() {
return TimeSource.NANOS_PER_SEC;
}
}
class TransportControlState
{
private boolean allowStop;
private boolean allowPlay;
private boolean allowVolume;
public boolean isAllowPlay()
{
return allowPlay;
}
public void setAllowPlay(boolean allowPlay)
{
this.allowPlay = allowPlay;
}
public boolean isAllowStop()
{
return allowStop;
}
public void setAllowStop(boolean allowStop)
{
this.allowStop = allowStop;
}
public boolean isAllowVolume()
{
return allowVolume;
}
public void setAllowVolume(boolean allowVolume)
{
this.allowVolume = allowVolume;
}
}