package org.limewire.ui.swing.player;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.font.FontRenderContext;
import java.util.List;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.plaf.basic.BasicSliderUI;
import net.miginfocom.swing.MigLayout;
import org.jdesktop.application.Resource;
import org.jdesktop.swingx.JXPanel;
import org.jdesktop.swingx.painter.Painter;
import org.limewire.core.api.library.LocalFileItem;
import org.limewire.player.api.PlayerState;
import org.limewire.setting.evt.SettingEvent;
import org.limewire.setting.evt.SettingListener;
import org.limewire.ui.swing.components.Disposable;
import org.limewire.ui.swing.components.IconButton;
import org.limewire.ui.swing.components.LimeSliderBar;
import org.limewire.ui.swing.components.MarqueeButton;
import org.limewire.ui.swing.components.VolumeSlider;
import org.limewire.ui.swing.components.decorators.SliderBarDecorator;
import org.limewire.ui.swing.library.LibraryMediator;
import org.limewire.ui.swing.painter.ComponentBackgroundPainter;
import org.limewire.ui.swing.painter.BorderPainter.AccentType;
import org.limewire.ui.swing.settings.SwingUiSettings;
import org.limewire.ui.swing.util.GuiUtils;
import org.limewire.ui.swing.util.I18n;
import org.limewire.ui.swing.util.ResizeUtils;
import com.google.inject.Provider;
/**
* Main UI container for the media player.
*/
class PlayerControlPanel extends JXPanel implements PlayerMediatorListener, Disposable {
@Resource private int arcWidth;
@Resource private int arcHeight;
@Resource private Color innerBorder;
@Resource private Color innerBackground;
@Resource private Color bevelLeft;
@Resource private Color bevelTop1;
@Resource private Color bevelTop2;
@Resource private Color bevelRight;
@Resource private Color bevelBottom;
@Resource private Icon backIcon;
@Resource private Icon backIconPressed;
@Resource private Icon backIconRollover;
@Resource private Icon backIconDisabled;
@Resource private Icon forwardIcon;
@Resource private Icon forwardIconPressed;
@Resource private Icon forwardIconRollover;
@Resource private Icon forwardIconDisabled;
@Resource private Icon playIcon;
@Resource private Icon playIconPressed;
@Resource private Icon playIconRollover;
@Resource private Icon playIconDisabled;
@Resource private Icon pauseIcon;
@Resource private Icon pauseIconPressed;
@Resource private Icon pauseIconRollover;
@Resource private Icon volumeIcon;
@Resource private Icon volumeIconPressed;
@Resource private Icon volumeIconRollover;
@Resource private Icon volumeIconDisabled;
@Resource private Icon shuffleIcon;
@Resource private Icon shuffleIconPressed;
@Resource private Icon shuffleIconRollover;
@Resource private Icon shuffleIconActive;
@Resource private Font font;
private final JXPanel innerPanel;
private final JButton backButton;
private final JButton playButton;
private final JButton pauseButton;
private final JButton forwardButton;
private final LimeSliderBar progressSlider;
private final JPanel statusPanel;
private final JButton volumeButton;
private JButton shuffleButton;
private int numberOfPlayerTimeValuesToIgnore = 0;
private final JPopupMenu volumeControlPopup;
private final VolumeSlider volumeSlider;
private final MarqueeButton titleLabel;
private static final String BACK = "BACK";
private static final String PLAY = "PLAY";
private static final String PAUSE = "PAUSE";
private static final String FORWARD = "FORWARD";
private static final String VOLUME = "VOLUME";
private static final String SHUFFLE = "SHUFFLE";
private final Provider<PlayerMediator> playerProvider;
private final LibraryMediator libraryMediator;
private final boolean isAudio;
private SettingListener settingListener;
/**
* Constructs a PlayerPanel with the specified component providers and
* decorators.
*/
public PlayerControlPanel(Provider<PlayerMediator> playerProvider,
LibraryMediator libraryMediator,
SliderBarDecorator sliderBarDecorator,
boolean isAudio) {
this.playerProvider = playerProvider;
this.libraryMediator = libraryMediator;
this.isAudio = isAudio;
GuiUtils.assignResources(this);
setLayout(new MigLayout("insets 0, filly, alignx center"));
setOpaque(false);
final ButtonListener playerListener = new ButtonListener();
backButton = new IconButton(backIcon, backIconRollover, backIconPressed);
backButton.addActionListener(playerListener);
backButton.setActionCommand(BACK);
backButton.setDisabledIcon(backIconDisabled);
backButton.setToolTipText(I18n.tr("Skip Back"));
backButton.setFocusable(false);
playButton = new IconButton(playIcon, playIconRollover, playIconPressed);
playButton.addActionListener(playerListener);
playButton.setActionCommand(PLAY);
playButton.setDisabledIcon(playIconDisabled);
playButton.setToolTipText(I18n.tr("Play"));
playButton.setFocusable(false);
pauseButton = new IconButton(pauseIcon, pauseIconRollover, pauseIconPressed);
pauseButton.addActionListener(playerListener);
pauseButton.setActionCommand(PAUSE);
pauseButton.setVisible(false);
pauseButton.setToolTipText(I18n.tr("Pause"));
pauseButton.setFocusable(false);
pauseButton.setMinimumSize(playButton.getMinimumSize());
pauseButton.setPreferredSize(playButton.getPreferredSize());
forwardButton = new IconButton(forwardIcon, forwardIconRollover, forwardIconPressed);
forwardButton.addActionListener(playerListener);
forwardButton.setActionCommand(FORWARD);
forwardButton.setDisabledIcon(forwardIconDisabled);
forwardButton.setToolTipText(I18n.tr("Skip Forward"));
forwardButton.setFocusable(false);
volumeButton = new IconButton(volumeIcon, volumeIconRollover, volumeIconPressed);
volumeButton.addActionListener(playerListener);
volumeButton.setActionCommand(VOLUME);
volumeButton.setDisabledIcon(volumeIconDisabled);
volumeButton.setToolTipText(I18n.tr("Volume"));
volumeButton.setFocusable(false);
volumeSlider = new VolumeSlider(0, 100);
volumeSlider.setValue((int)(SwingUiSettings.PLAYER_VOLUME.getValue() * volumeSlider.getMaximum()));
volumeControlPopup = volumeSlider.createPopup();
if (isAudio) {
shuffleButton = new IconButton(shuffleIcon, shuffleIconRollover, shuffleIconPressed, shuffleIconActive);
shuffleButton.addActionListener(playerListener);
shuffleButton.setActionCommand(SHUFFLE);
shuffleButton.setRolloverSelectedIcon(shuffleIconActive);
shuffleButton.setToolTipText(I18n.tr("Shuffle"));
shuffleButton.setFocusable(false);
}
progressSlider = new LimeSliderBar();
sliderBarDecorator.decoratePlain(progressSlider);
initProgressControl();
statusPanel = new JPanel(new MigLayout());
titleLabel = new MarqueeButton("", 150);
titleLabel.setFont(font);
ResizeUtils.forceSize(titleLabel, new Dimension(206, (int)
font.getMaxCharBounds(new FontRenderContext(null, false, false)).getHeight()));
statusPanel.add(titleLabel);
statusPanel.add(progressSlider, "dock south");
statusPanel.setOpaque(false);
int buttonWidth = backButton.getPreferredSize().width +
playButton.getPreferredSize().width + forwardButton.getPreferredSize().width;
Dimension statusSize = new Dimension(buttonWidth, statusPanel.getPreferredSize().height);
statusPanel.setPreferredSize(statusSize);
innerPanel = new JXPanel(new MigLayout("insets 4 10 4 10, filly, gapy 5, alignx center"));
innerPanel.setOpaque(false);
innerPanel.setBackgroundPainter(createStatusBackgroundPainter());
if (isAudio) {
innerPanel.add(backButton, "gapright 1");
}
innerPanel.add(pauseButton, "hidemode 3");
innerPanel.add(playButton, "hidemode 3");
if (isAudio) {
innerPanel.add(forwardButton, "gapright 3");
}
innerPanel.add(statusPanel, "gapbottom 2, hidemode 2");
innerPanel.add(volumeButton, "gapleft 2");
if (isAudio) {
innerPanel.add(shuffleButton, "gapleft 2");
}
innerPanel.setVisible(SwingUiSettings.PLAYER_ENABLED.getValue());
add(innerPanel, "gaptop 2, gapbottom 2");
VolumeController volumeController = new VolumeController();
volumeSlider.addChangeListener(volumeController);
volumeControlPopup.addPopupMenuListener(new PopupMenuListener() {
@Override
public void popupMenuCanceled(PopupMenuEvent e) {
playerListener.clearMenu();
}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
playerListener.clearMenu();
}
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
}
});
register();
}
/**
* Registers listeners for player events.
*/
private void register() {
getPlayerMediator().addMediatorListener(this);
// Stop player if disabled, and show/hide player.
settingListener = new SettingListener(){
@Override
public void settingChanged(final SettingEvent evt) {
SwingUtilities.invokeLater(new Runnable(){
public void run() {
boolean enabled = SwingUiSettings.PLAYER_ENABLED.getValue();
if (!enabled) {
getPlayerMediator().stop();
resetPlayer();
}
PlayerControlPanel.this.innerPanel.setVisible(enabled);
}
});
}
};
SwingUiSettings.PLAYER_ENABLED.addSettingListener(settingListener);
}
/**
* Clears all of the progress/song fields.
*/
private void resetPlayer() {
if (isAudio) {
titleLabel.stop();
}
progressSlider.setValue(0);
progressSlider.setEnabled(false);
titleLabel.setText("");
titleLabel.setToolTipText("");
}
public void dispose(){
getPlayerMediator().removeMediatorListener(this);
SwingUiSettings.PLAYER_ENABLED.removeSettingListener(settingListener);
if (isAudio) {
titleLabel.stop();
}
}
/**
* Initializes the progress component.
*/
private void initProgressControl() {
// The progress slider UI has been offsetting the value of the slider to account for
// the size of the position knob, but since we're overriding the foreground painter to not
// paint a knob, this is causing the slider to be positioned incorrectly.
// So, let's override the BasicSliderUI to make the position knob small enough to not
// distort the mapping but bit enough to still be dragable.
progressSlider.setUI(new BasicSliderUI(progressSlider) {
@Override
protected Dimension getThumbSize() {
return new Dimension(5,8);
}
});
progressSlider.addChangeListener(new ProgressListener());
progressSlider.setMaximum(Integer.MAX_VALUE);
progressSlider.setMaximumSize(new Dimension(206, 8));
progressSlider.setMinimumSize(new Dimension(206, 8));
progressSlider.setPreferredSize(new Dimension(206, 8));
progressSlider.setSize(new Dimension(206, 6));
progressSlider.setEnabled(false);
progressSlider.setMinorTickSpacing(1);
progressSlider.addMouseListener(new MouseAdapter() {
/**
* Reposition the thumb on the jslider to the location of the mouse
* click
*/
@Override
public void mousePressed(MouseEvent e) {
if (!progressSlider.isEnabled())
return;
mouseSkip(e.getX());
}
/**
* Overrides the mouse press increment when a mouse click occurs in the
* jslider. Repositions the jslider directly to the location the mouse
* click avoiding the standard step increment with each click
* @param x - location of mouse click
*/
protected void mouseSkip(int x) {
if (progressSlider.getUI() instanceof BasicSliderUI) {
progressSlider.setValue(((BasicSliderUI)progressSlider.getUI()).valueForXPosition(x));
numberOfPlayerTimeValuesToIgnore = 2;
}
}
});
}
/**
* Creates a background painter for the container.
*/
private Painter<JXPanel> createStatusBackgroundPainter() {
return new ComponentBackgroundPainter<JXPanel>(innerBackground, innerBorder,
bevelLeft, bevelTop1, bevelTop2, bevelRight, bevelBottom,
arcWidth, arcHeight, AccentType.SHADOW);
}
/**
* Returns the mediator component that controls the player.
*/
private PlayerMediator getPlayerMediator() {
return playerProvider.get();
}
/**
* Handles update to the specified progress value to adjust the visual
* position of the slider.
*/
@Override
public void progressUpdated(float progress) {
if (!(progressSlider.getValueIsAdjusting() || getPlayerMediator().getStatus() == PlayerState.SEEKING)) {
// When the slider value is updated by the mouse, there can be some slider updates with the old values still
// queued and waiting to run. So, after the user moves the slider with his mouse to the new play location,
// an old update can cause it to return to the old play location before a fresh update causes it to jump
// forward to the new location again. To prevent this odd behavior, after the user changes the play position
// with his mouse, we ignore a few of the subsequent updates.
if (numberOfPlayerTimeValuesToIgnore > 0) {
numberOfPlayerTimeValuesToIgnore--;
} else {
progressSlider.setValue((int) (progressSlider.getMaximum() * progress));
}
}
}
/**
* Handles song change to the specified song name.
*/
@Override
public void mediaChanged(String name) {
initializeVolumeSettings();
// Enable progress slider.
progressSlider.setEnabled(getPlayerMediator().isSeekable());
// Set song text.
titleLabel.setText(name);
titleLabel.setToolTipText(name);
if (isAudio) {
titleLabel.start();
}
if (!innerPanel.isVisible()) {
innerPanel.setVisible(true);
}
}
private void initializeVolumeSettings() {
// Update volume.
updateVolumeFromSetting();
//enable volume control
volumeButton.setEnabled(getPlayerMediator().hasVolumeControl());
}
/**
* Handles state change in the player to the specified state.
*/
@Override
public void stateChanged(PlayerState playerState) {
progressSlider.setEnabled(getPlayerMediator().isSeekable());
if ((playerState == PlayerState.OPENED) || (playerState == PlayerState.SEEKED)) {
initializeVolumeSettings();
} else if (playerState == PlayerState.GAIN) {
// Exit on volume change.
return;
}
// Update buttons based on player status.
if ((playerState == PlayerState.PLAYING) || (playerState == PlayerState.SEEKING_PLAY)) {
playButton.setVisible(false);
pauseButton.setVisible(true);
if (isAudio) {
titleLabel.start();
}
} else if (playerState == PlayerState.PAUSED || playerState == PlayerState.SEEKING_PAUSED ||
playerState == PlayerState.EOM || playerState == PlayerState.STOPPED ||
playerState == PlayerState.UNKNOWN || playerState == PlayerState.NO_SOUND_DEVICE) {
playButton.setVisible(true);
pauseButton.setVisible(false);
if (isAudio) {
titleLabel.stop();
}
}
}
private void setVolume(float volume){
SwingUiSettings.PLAYER_VOLUME.setValue(volume);
updateVolumeFromSetting();
}
/**
* Updates the volume in the player.
*/
private void updateVolumeFromSetting() {
if (getPlayerMediator().hasVolumeControl()) {
getPlayerMediator().setVolume(SwingUiSettings.PLAYER_VOLUME.getValue());
}
}
/**
* Listener to handle user actions on various player buttons.
*/
private class ButtonListener implements ActionListener {
private long menuInvizTime = -1;
public void clearMenu() {
menuInvizTime = System.currentTimeMillis();
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand() == PLAY){
// If the player already has a file, then let's resume play on that file.
// Otherwise, let's start playing whatever file is selected in the library.
if (getPlayerMediator().getCurrentMediaFile() != null) {
getPlayerMediator().resume();
} else {
List<LocalFileItem> selectedItems = libraryMediator.getSelectedItems();
if (selectedItems.size() > 0) {
playerProvider.get().play(selectedItems.get(0));
}
}
} else if (e.getActionCommand() == PAUSE){
getPlayerMediator().pause();
} else if (e.getActionCommand() == FORWARD) {
getPlayerMediator().nextSong();
} else if (e.getActionCommand() == BACK) {
getPlayerMediator().prevSong();
} else if (e.getActionCommand() == VOLUME) {
if (System.currentTimeMillis() - menuInvizTime > 250f) {
volumeControlPopup.show(volumeButton, 0, 14);
volumeControlPopup.requestFocus();
}
} else if (e.getActionCommand() == SHUFFLE) {
// Toggle shuffle mode.
getPlayerMediator().setShuffle(!getPlayerMediator().isShuffle());
// Update button state.
shuffleButton.setSelected(getPlayerMediator().isShuffle());
shuffleButton.setPressedIcon(getPlayerMediator().isShuffle() ?
shuffleIconActive : shuffleIconPressed);
}
}
}
/**
* Listener to handle change to progress bar to skip to a new position in
* the song.
*/
private class ProgressListener implements ChangeListener {
private boolean waiting = false;
@Override
public void stateChanged(ChangeEvent e) {
if (progressSlider.getMaximum() != 0 && getPlayerMediator().isSeekable() && progressSlider.getValueIsAdjusting()) {
if (!waiting) {
waiting = true;
}
} else if (waiting) {
waiting = false;
double percent = (double)progressSlider.getValue() / (double)progressSlider.getMaximum();
getPlayerMediator().seek(percent);
progressSlider.setValue((int)(percent * progressSlider.getMaximum()));
}
}
}
/**
* Listener to update volume when volume slider is adjusted.
*/
private class VolumeController implements ChangeListener {
@Override
public void stateChanged(ChangeEvent e) {
setVolume(((float) volumeSlider.getValue()) / volumeSlider.getMaximum());
}
}
}