Moves player logic to its own class, away from Interface
In the future, this will make testing and a CLI environment easier.
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package edu.regis.universeplayer.gui;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.LineBorder;
|
||||
|
||||
import edu.regis.universeplayer.data.Album;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
/**
|
||||
* This panel will display information on an album.
|
||||
*/
|
||||
public class AlbumInfo extends JButton
|
||||
{
|
||||
private static final ResourceBundle langs = ResourceBundle.getBundle("lang.interface", Locale.getDefault());
|
||||
|
||||
public Album album;
|
||||
public final JLabel artLabel;
|
||||
public final JLabel albumName;
|
||||
public final JLabel artists;
|
||||
public final JLabel genres;
|
||||
public final JLabel year;
|
||||
|
||||
public AlbumInfo()
|
||||
{
|
||||
this.removeAll();
|
||||
this.setContentAreaFilled(false);
|
||||
this.setBorder(null);
|
||||
SpringLayout infoLayout = new SpringLayout();
|
||||
this.setLayout(infoLayout);
|
||||
this.setFocusable(true);
|
||||
|
||||
this.setModel(new DefaultButtonModel());
|
||||
|
||||
this.artLabel = new JLabel();
|
||||
this.add(this.artLabel);
|
||||
this.albumName = new JLabel(langs.getString("albumInfo.album"));
|
||||
this.add(this.albumName);
|
||||
this.artists = new JLabel(langs.getString("albumInfo.artists"));
|
||||
this.add(this.artists);
|
||||
this.genres = new JLabel(langs.getString("albumInfo.genres"));
|
||||
this.add(this.genres);
|
||||
this.year = new JLabel(langs.getString("albumInfo.year"));
|
||||
this.add(this.year);
|
||||
|
||||
/*
|
||||
* Set the layout information
|
||||
*/
|
||||
infoLayout.putConstraint(SpringLayout.NORTH, artLabel, 5, SpringLayout.NORTH, this);
|
||||
infoLayout.putConstraint(SpringLayout.WEST, artLabel, 5, SpringLayout.WEST, this);
|
||||
infoLayout.putConstraint(SpringLayout.NORTH, albumName, 5, SpringLayout.NORTH, this);
|
||||
infoLayout.putConstraint(SpringLayout.WEST, albumName, 5, SpringLayout.EAST, artLabel);
|
||||
infoLayout.putConstraint(SpringLayout.NORTH, artists, 5, SpringLayout.SOUTH, albumName);
|
||||
infoLayout.putConstraint(SpringLayout.WEST, artists, 5, SpringLayout.EAST, artLabel);
|
||||
infoLayout.putConstraint(SpringLayout.NORTH, genres, 5, SpringLayout.SOUTH, artists);
|
||||
infoLayout.putConstraint(SpringLayout.WEST, genres, 5, SpringLayout.EAST, artLabel);
|
||||
infoLayout.putConstraint(SpringLayout.NORTH, year, 5, SpringLayout.SOUTH, genres);
|
||||
infoLayout.putConstraint(SpringLayout.WEST, year, 5, SpringLayout.EAST, artLabel);
|
||||
|
||||
int maxLength = -1;
|
||||
JLabel longest = null;
|
||||
for (JLabel label : Arrays.asList(albumName, artists, genres, year))
|
||||
{
|
||||
if (label.getWidth() > maxLength)
|
||||
{
|
||||
longest = label;
|
||||
maxLength = longest.getWidth();
|
||||
}
|
||||
}
|
||||
infoLayout.putConstraint(SpringLayout.EAST, this, 0, SpringLayout.EAST, longest);
|
||||
infoLayout.putConstraint(SpringLayout.SOUTH, this, 5, SpringLayout.EAST, artLabel);
|
||||
|
||||
while (this.getMouseListeners().length > 0)
|
||||
{
|
||||
this.removeMouseListener(this.getMouseListeners()[0]);
|
||||
}
|
||||
this.addFocusListener(new FocusAdapter()
|
||||
{
|
||||
@Override
|
||||
public void focusGained(FocusEvent e)
|
||||
{
|
||||
((AlbumInfo) e.getComponent()).setBorder(new LineBorder(Color.GRAY, 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void focusLost(FocusEvent e)
|
||||
{
|
||||
((AlbumInfo) e.getComponent()).setBorder(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public AlbumInfo(Album album)
|
||||
{
|
||||
this();
|
||||
this.updateInfo(album);
|
||||
}
|
||||
|
||||
public void updateInfo(Album album)
|
||||
{
|
||||
final int ART_SIZE = 128;
|
||||
ImageIcon icon;
|
||||
StringBuilder builder;
|
||||
|
||||
this.album = album;
|
||||
|
||||
if (album.art != null)
|
||||
{
|
||||
icon = album.art;
|
||||
}
|
||||
else
|
||||
{
|
||||
icon = new ImageIcon(this.getClass()
|
||||
.getResource("/gui/icons/defaultart.png"), "Default");
|
||||
}
|
||||
icon.setImage(icon.getImage().getScaledInstance(ART_SIZE, ART_SIZE, 0));
|
||||
this.artLabel.setIcon(icon);
|
||||
|
||||
this.albumName.setText(album.name);
|
||||
|
||||
builder = new StringBuilder();
|
||||
if (album.artists != null && album.artists.length >= 1)
|
||||
{
|
||||
builder.append(album.artists[0]);
|
||||
for (int i = 1, l = album.artists.length - 1; i < l; i++)
|
||||
{
|
||||
builder.append(", ");
|
||||
builder.append(album.artists[i]);
|
||||
}
|
||||
if (album.artists.length > 1)
|
||||
{
|
||||
builder.append(" & ");
|
||||
builder.append(album.artists[album.artists.length - 1]);
|
||||
}
|
||||
}
|
||||
this.artists.setText(builder.toString());
|
||||
|
||||
builder = new StringBuilder();
|
||||
if (album.genres != null && album.genres.length >= 1)
|
||||
{
|
||||
builder.append(album.genres[0]);
|
||||
for (int i = 1, l = album.genres.length - 1; i < l; i++)
|
||||
{
|
||||
builder.append(", ");
|
||||
builder.append(album.genres[i]);
|
||||
}
|
||||
if (album.genres.length > 1)
|
||||
{
|
||||
builder.append(" & ");
|
||||
builder.append(album.genres[album.genres.length - 1]);
|
||||
}
|
||||
}
|
||||
this.genres.setText(builder.toString());
|
||||
|
||||
this.year.setText(String.valueOf(album.year));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package edu.regis.universeplayer.gui;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.FocusAdapter;
|
||||
import java.awt.event.FocusEvent;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
import com.wordpress.tips4java.ScrollablePanel;
|
||||
|
||||
import edu.regis.universeplayer.data.Album;
|
||||
import edu.regis.universeplayer.data.CollectionType;
|
||||
import edu.regis.universeplayer.data.Song;
|
||||
import edu.regis.universeplayer.data.SongProvider;
|
||||
|
||||
/**
|
||||
* This panel will list all the song collections on display (albums, artists, genres, etc.)
|
||||
*
|
||||
* @author William Hubbard
|
||||
* @version 0.1
|
||||
*/
|
||||
public class CollectionList extends ScrollablePanel
|
||||
{
|
||||
/**
|
||||
* The type of collections being displayed.
|
||||
*/
|
||||
private CollectionType type;
|
||||
/**
|
||||
* A link between the JLabel and the object they point towards.
|
||||
*/
|
||||
private Map<JButton, Object> labelMap = new HashMap<>();
|
||||
|
||||
/**
|
||||
* A list of all things interested in knowing when we click a collection.
|
||||
*/
|
||||
private LinkedList<SongDisplayListener> listeners = new LinkedList<>();
|
||||
|
||||
/**
|
||||
* Creates a collections list view.
|
||||
*/
|
||||
public CollectionList()
|
||||
{
|
||||
super();
|
||||
|
||||
FlowLayout layout = new FlowLayout();
|
||||
this.setLayout(layout);
|
||||
this.setFocusCycleRoot(true);
|
||||
// this.setFocusable(true);
|
||||
this.setScrollableWidth(ScrollableSizeHint.FIT);
|
||||
this.setScrollableHeight(ScrollableSizeHint.STRETCH);
|
||||
this.addFocusListener(new FocusAdapter()
|
||||
{
|
||||
@Override
|
||||
public void focusGained(FocusEvent e)
|
||||
{
|
||||
labelMap.keySet().stream().findFirst().ifPresent(JButton::requestFocusInWindow);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the collections currently listed, sorted by album.
|
||||
*
|
||||
* @param type - The type of collection we are displaying.
|
||||
* @param objects - The collection to display.
|
||||
*/
|
||||
public void listCollection(CollectionType type, Collection<?> objects)
|
||||
{
|
||||
Class<?> fType = objects.stream().filter(Objects::nonNull).map(Object::getClass).findFirst()
|
||||
.orElse(null);
|
||||
if (objects.isEmpty() || fType == null)
|
||||
{
|
||||
/*
|
||||
* We really don't need error checking here, and we couldn't get it working anyway.
|
||||
*/
|
||||
this.labelMap.clear();
|
||||
this.removeAll();
|
||||
return;
|
||||
}
|
||||
if (!type.objectType.isAssignableFrom(fType))
|
||||
{
|
||||
throw new ClassCastException("Can't assign " + type.objectType
|
||||
.getName() + " from " + fType
|
||||
.getName());
|
||||
}
|
||||
|
||||
this.labelMap.clear();
|
||||
this.removeAll();
|
||||
this.type = type;
|
||||
switch (type)
|
||||
{
|
||||
case album -> this.addAlbums(objects.stream().sorted().map(ob -> (Album) ob)
|
||||
.collect(Collectors.toList()));
|
||||
case artist -> this.addArtists(objects.stream().sorted().map(ob -> (String) ob)
|
||||
.collect(Collectors.toList()), false);
|
||||
case albumArtist -> this.addArtists(objects.stream().sorted().map(ob -> (String) ob)
|
||||
.collect(Collectors.toList()), true);
|
||||
case genre -> this.addGenres(objects.stream().sorted().map(ob -> (String) ob)
|
||||
.collect(Collectors.toList()));
|
||||
case year -> this.addYears(objects.stream().sorted().map(ob -> (Integer) ob)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the display to show a list of artists
|
||||
*
|
||||
* @param artists - The list of artists to display.
|
||||
* @param album - Whether the list should be treated as song artists or album artists.
|
||||
*/
|
||||
private void addArtists(List<String> artists, boolean album)
|
||||
{
|
||||
final int ART_SIZE = 128;
|
||||
|
||||
JButton artistLabel;
|
||||
ImageIcon icon;
|
||||
|
||||
for (String artist : artists)
|
||||
{
|
||||
artistLabel = new JButton();
|
||||
setButtonLook(artistLabel);
|
||||
// TODO - Maybe add some sort of artist image lookup?
|
||||
// if (album.art != null)
|
||||
// {
|
||||
// icon = album.art;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
icon = new ImageIcon(this.getClass()
|
||||
.getResource("/gui/icons/artist.png"), "Default");
|
||||
// }
|
||||
icon.setImage(icon.getImage().getScaledInstance(ART_SIZE, ART_SIZE, 0));
|
||||
artistLabel.setIcon(icon);
|
||||
artistLabel.setText(artist);
|
||||
artistLabel.setHorizontalTextPosition(JLabel.CENTER);
|
||||
artistLabel.setVerticalTextPosition(JLabel.BOTTOM);
|
||||
artistLabel.addActionListener(mouseEvent -> {
|
||||
if (album)
|
||||
{
|
||||
this.triggerSongDisplayListeners(SongProvider.INSTANCE
|
||||
.getAlbumsFromArtist(artist).stream()
|
||||
.flatMap(album2 -> SongProvider.INSTANCE.getSongsFromAlbum(album2)
|
||||
.stream())
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.triggerSongDisplayListeners(new ArrayList<>(SongProvider.INSTANCE
|
||||
.getSongsFromArtist(artist)));
|
||||
}
|
||||
});
|
||||
this.add(artistLabel);
|
||||
this.labelMap.put(artistLabel, artist);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the display to show a list of albums.
|
||||
*
|
||||
* @param albums - The list of albums to display.
|
||||
*/
|
||||
private void addAlbums(List<Album> albums)
|
||||
{
|
||||
final int ART_SIZE = 128;
|
||||
|
||||
JButton albumLabel;
|
||||
ImageIcon icon;
|
||||
|
||||
for (Album album : albums)
|
||||
{
|
||||
albumLabel = new JButton();
|
||||
setButtonLook(albumLabel);
|
||||
icon = Objects.requireNonNullElseGet(album.art, () -> new ImageIcon(this.getClass()
|
||||
.getResource("/gui/icons/defaultart.png"), "Default"));
|
||||
icon.setImage(icon.getImage().getScaledInstance(ART_SIZE, ART_SIZE, 0));
|
||||
albumLabel.setIcon(icon);
|
||||
albumLabel.setText(album.name);
|
||||
albumLabel.setHorizontalTextPosition(JLabel.CENTER);
|
||||
albumLabel.setVerticalTextPosition(JLabel.BOTTOM);
|
||||
albumLabel.addActionListener(mouseEvent -> this.triggerSongDisplayListeners(new ArrayList<>(SongProvider.INSTANCE
|
||||
.getSongsFromAlbum(album))));
|
||||
this.add(albumLabel);
|
||||
this.labelMap.put(albumLabel, album);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the display to show a list of genres.
|
||||
*
|
||||
* @param genres - The list of genres to display.
|
||||
*/
|
||||
private void addGenres(List<String> genres)
|
||||
{
|
||||
// final int ART_SIZE = 128;
|
||||
|
||||
JButton genreLabel;
|
||||
// ImageIcon icon;
|
||||
|
||||
for (String genre : genres)
|
||||
{
|
||||
genreLabel = new JButton();
|
||||
setButtonLook(genreLabel);
|
||||
// TODO - Maybe add some sort of artist image lookup?
|
||||
// if (album.art != null)
|
||||
// {
|
||||
// icon = album.art;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// icon = new ImageIcon(this.getClass()
|
||||
// .getResource("/gui/icons/defaultart.png"), "Default");
|
||||
// }
|
||||
// icon.setImage(icon.getImage().getScaledInstance(ART_SIZE, ART_SIZE, 0));
|
||||
// albumLabel.setIcon(icon);
|
||||
genreLabel.setText(genre);
|
||||
genreLabel.setHorizontalTextPosition(JLabel.CENTER);
|
||||
genreLabel.setVerticalTextPosition(JLabel.BOTTOM);
|
||||
genreLabel.addActionListener(mouseEvent -> this.triggerSongDisplayListeners(SongProvider.INSTANCE
|
||||
.getAlbumsFromGenre(genre).stream()
|
||||
.flatMap(album2 -> SongProvider.INSTANCE.getSongsFromAlbum(album2)
|
||||
.stream())
|
||||
.collect(Collectors.toList())));
|
||||
this.add(genreLabel);
|
||||
this.labelMap.put(genreLabel, genre);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the display to show a list of release years.
|
||||
*
|
||||
* @param years - The list of genres to display.
|
||||
*/
|
||||
private void addYears(List<Integer> years)
|
||||
{
|
||||
// final int ART_SIZE = 128;
|
||||
|
||||
JButton yearLabel;
|
||||
// ImageIcon icon;
|
||||
|
||||
for (Integer year : years)
|
||||
{
|
||||
yearLabel = new JButton();
|
||||
setButtonLook(yearLabel);
|
||||
// TODO - Maybe add some sort of artist image lookup?
|
||||
// if (album.art != null)
|
||||
// {
|
||||
// icon = album.art;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// icon = new ImageIcon(this.getClass()
|
||||
// .getResource("/gui/icons/defaultart.png"), "Default");
|
||||
// }
|
||||
// icon.setImage(icon.getImage().getScaledInstance(ART_SIZE, ART_SIZE, 0));
|
||||
// albumLabel.setIcon(icon);
|
||||
yearLabel.setText(year.toString());
|
||||
yearLabel.setHorizontalTextPosition(JLabel.CENTER);
|
||||
yearLabel.setVerticalTextPosition(JLabel.BOTTOM);
|
||||
yearLabel.addActionListener(mouseEvent -> this.triggerSongDisplayListeners(SongProvider.INSTANCE
|
||||
.getAlbumsFromYear(year).stream()
|
||||
.flatMap(album2 -> SongProvider.INSTANCE.getSongsFromAlbum(album2)
|
||||
.stream())
|
||||
.collect(Collectors.toList())));
|
||||
this.add(yearLabel);
|
||||
this.labelMap.put(yearLabel, year);
|
||||
}
|
||||
}
|
||||
|
||||
private void setButtonLook(JButton button)
|
||||
{
|
||||
button.setFocusPainted(true);
|
||||
button.setMargin(new Insets(0, 0, 0, 0));
|
||||
button.setContentAreaFilled(false);
|
||||
button.setBorderPainted(false);
|
||||
button.setOpaque(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener for when the displayed songs should change.
|
||||
*
|
||||
* @param listener - The listener to add.
|
||||
*/
|
||||
public void addSongDisplayListener(SongDisplayListener listener)
|
||||
{
|
||||
this.listeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener for when the displayed songs should change.
|
||||
*
|
||||
* @param listener - The listener to add.
|
||||
*/
|
||||
public void removeSongDisplayListener(SongDisplayListener listener)
|
||||
{
|
||||
this.listeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers all the song display listeners.
|
||||
*/
|
||||
protected void triggerSongDisplayListeners(Collection<Song> songs)
|
||||
{
|
||||
for (SongDisplayListener listener : this.listeners)
|
||||
{
|
||||
listener.updateSongs(songs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package edu.regis.universeplayer.gui;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Locale;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
import edu.regis.universeplayer.data.CollectionType;
|
||||
import edu.regis.universeplayer.data.Song;
|
||||
|
||||
/**
|
||||
* This panel links to various song collections the player has set up.
|
||||
*
|
||||
* @author William Hubbard
|
||||
* @version 0.1
|
||||
*/
|
||||
public class Collections extends JPanel
|
||||
{
|
||||
private static final ResourceBundle langs = ResourceBundle.getBundle("lang.interface", Locale.getDefault());
|
||||
|
||||
/**
|
||||
* A list of all things interested in knowing when we click a collection.
|
||||
*/
|
||||
private LinkedList<SongDisplayListener> listeners = new LinkedList<>();
|
||||
|
||||
/**
|
||||
* Creates a collections list view.
|
||||
*/
|
||||
public Collections()
|
||||
{
|
||||
JButton defaultLabel;
|
||||
JButton label;
|
||||
BoxLayout layout = new BoxLayout(this, BoxLayout.Y_AXIS);
|
||||
this.setLayout(layout);
|
||||
this.setFocusable(true);
|
||||
this.setFocusCycleRoot(true);
|
||||
|
||||
this.add(defaultLabel = label = this.createButton(Interface.getInstance().actions.get("view.all")));
|
||||
this.add(label = this.createButton(Interface.getInstance().actions.get("view.artists")));
|
||||
this.add(label = this.createButton(Interface.getInstance().actions.get("view.albums")));
|
||||
this.add(label = this.createButton(Interface.getInstance().actions.get("view.genres")));
|
||||
this.add(label = this.createButton(Interface.getInstance().actions.get("view.years")));
|
||||
this.add(new JLabel("\u23AF".repeat(6)));
|
||||
this.add(label = this.createButton(Interface.getInstance().actions.get("view.playlists")));
|
||||
this.add(new JLabel("\u23AF".repeat(6)));
|
||||
this.add(label = this.createButton(Interface.getInstance().actions.get("addExternal")));
|
||||
|
||||
addMouseListener(new MouseAdapter()
|
||||
{
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e)
|
||||
{
|
||||
defaultLabel.requestFocusInWindow();
|
||||
}
|
||||
});
|
||||
addFocusListener(new FocusAdapter()
|
||||
{
|
||||
@Override
|
||||
public void focusGained(FocusEvent e)
|
||||
{
|
||||
int index = -1;
|
||||
Component[] children = ((Container) e.getComponent()).getComponents();
|
||||
for (int i = 0, l = children.length; index == -1 && i < l; i++)
|
||||
{
|
||||
if (children[i] == e.getOppositeComponent())
|
||||
{
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Only auto-switch focus if the previous component was not from
|
||||
* within.
|
||||
*/
|
||||
if (index == -1)
|
||||
{
|
||||
defaultLabel.requestFocusInWindow();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void addNewSong()
|
||||
{
|
||||
Container parent = this.getParent();
|
||||
while (!(parent instanceof JFrame) && parent != null)
|
||||
{
|
||||
parent = parent.getParent();
|
||||
}
|
||||
new InternetSongDialog((JFrame) parent).setVisible(true);
|
||||
}
|
||||
|
||||
private JButton createButton(String text)
|
||||
{
|
||||
return setButton(new JButton(text));
|
||||
}
|
||||
|
||||
private JButton createButton(Action action)
|
||||
{
|
||||
return setButton(new JButton(action));
|
||||
}
|
||||
|
||||
private JButton setButton(JButton label)
|
||||
{
|
||||
label.setFocusPainted(true);
|
||||
label.setMargin(new Insets(0, 0, 0, 0));
|
||||
label.setContentAreaFilled(false);
|
||||
label.setBorderPainted(false);
|
||||
label.setOpaque(false);
|
||||
label.setForeground(Color.BLUE);
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener for when the displayed songs should change.
|
||||
*
|
||||
* @param listener - The listener to add.
|
||||
*/
|
||||
public void addSongDisplayListener(SongDisplayListener listener)
|
||||
{
|
||||
this.listeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener for when the displayed songs should change.
|
||||
*
|
||||
* @param listener - The listener to add.
|
||||
*/
|
||||
public void removeSongDisplayListener(SongDisplayListener listener)
|
||||
{
|
||||
this.listeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers all the song display listeners.
|
||||
*/
|
||||
protected void triggerSongDisplayListeners(Collection<Song> songs)
|
||||
{
|
||||
for (SongDisplayListener listener : this.listeners)
|
||||
{
|
||||
listener.updateSongs(songs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers all the song display listeners.
|
||||
*/
|
||||
protected void triggerCollectionDisplayListeners(CollectionType type, Collection<?> collection)
|
||||
{
|
||||
for (SongDisplayListener listener : this.listeners)
|
||||
{
|
||||
listener.updateCollections(type, collection);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package edu.regis.universeplayer.gui;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
public class FocusManager extends DefaultKeyboardFocusManager
|
||||
{
|
||||
public FocusManager()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,779 @@
|
||||
/*
|
||||
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package edu.regis.universeplayer.gui;
|
||||
|
||||
import edu.regis.universeplayer.player.Player;
|
||||
import edu.regis.universeplayer.browser.Browser;
|
||||
import edu.regis.universeplayer.player.BrowserPlayer;
|
||||
import edu.regis.universeplayer.browserCommands.QueryFuture;
|
||||
import edu.regis.universeplayer.data.InternetSong;
|
||||
import edu.regis.universeplayer.data.Queue;
|
||||
import edu.regis.universeplayer.data.*;
|
||||
import edu.regis.universeplayer.player.LocalPlayer;
|
||||
import edu.regis.universeplayer.player.PlayerManager;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Locale;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* The Interface class serves as the primary GUI that the player interacts
|
||||
* with.
|
||||
*
|
||||
* @author William Hubbard
|
||||
* @version 0.1
|
||||
*/
|
||||
public class Interface extends JFrame implements SongDisplayListener, ComponentListener, WindowListener, UpdateListener, FocusListener
|
||||
{
|
||||
// static {
|
||||
// Locale.setDefault(new Locale("es", "ES"));
|
||||
// }
|
||||
private static final Logger logger = LoggerFactory
|
||||
.getLogger(Interface.class);
|
||||
private static final ResourceBundle langs = ResourceBundle
|
||||
.getBundle("lang.interface", Locale.getDefault());
|
||||
|
||||
private static Interface INSTANCE;
|
||||
|
||||
public final ActionMap actions = new ActionMap();
|
||||
|
||||
/**
|
||||
* A reference to the panel containing links to different collection views.
|
||||
*/
|
||||
private final Collections collectionTypes;
|
||||
/**
|
||||
* A reference to the panel showing the queue.
|
||||
*/
|
||||
private final QueueList queueList;
|
||||
/**
|
||||
* A reference to the central view showing a list of songs.
|
||||
*/
|
||||
private final SongList songList;
|
||||
/**
|
||||
* A reference to the central view showing a list of collections.
|
||||
*/
|
||||
private final CollectionList collectionList;
|
||||
/**
|
||||
* A reference to the central view scroll pane.
|
||||
*/
|
||||
private final JScrollPane centerView;
|
||||
/**
|
||||
* A reference to the player controls pane.
|
||||
*/
|
||||
private final PlayerControls controls;
|
||||
|
||||
private int currentPlayer = -1;
|
||||
|
||||
public static Interface getInstance()
|
||||
{
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
Interface inter = null;
|
||||
PlayerManager.getPlayers();
|
||||
try
|
||||
{
|
||||
/*
|
||||
* Add this just in case of a crash or something. It won't work if the
|
||||
* program is forcibly terminated by the OS, but it could be helpful
|
||||
* otherwise.
|
||||
*/
|
||||
logger.info("Starting application");
|
||||
|
||||
inter = new Interface();
|
||||
inter.setSize(700, 500);
|
||||
SongProvider.INSTANCE.addUpdateListener(inter);
|
||||
inter.setVisible(true);
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
logger.error("Could not open browser background", e);
|
||||
JOptionPane
|
||||
.showMessageDialog(inter != null && inter
|
||||
.isVisible() ? inter : null, e, langs
|
||||
.getString("error.generic"), JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an interface
|
||||
*/
|
||||
public Interface()
|
||||
{
|
||||
super();
|
||||
INSTANCE = this;
|
||||
|
||||
this.initActions();
|
||||
|
||||
this.collectionTypes = new Collections();
|
||||
this.controls = new PlayerControls();
|
||||
this.queueList = new QueueList();
|
||||
this.songList = new SongList();
|
||||
this.collectionList = new CollectionList();
|
||||
this.centerView = new JScrollPane(this.songList);
|
||||
|
||||
this.constructWindow();
|
||||
this.setFocusManager();
|
||||
|
||||
this.updateSongs(SongProvider.INSTANCE.getSongs());
|
||||
}
|
||||
|
||||
protected void initActions()
|
||||
{
|
||||
AbstractAction action;
|
||||
|
||||
this.actions.put("refresh", action = new AbstractAction(langs
|
||||
.getString("actions.refresh"))
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
if (this.isEnabled())
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_R);
|
||||
|
||||
this.actions.put("addExternal", action = new AbstractAction(langs
|
||||
.getString("actions.addExternal"))
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
if (this.isEnabled())
|
||||
{
|
||||
InternetSongDialog dialog = new InternetSongDialog(Interface.this);
|
||||
dialog.addWindowListener(new WindowAdapter()
|
||||
{
|
||||
/**
|
||||
* Invoked when a window has been closed.
|
||||
*
|
||||
* @param e
|
||||
*/
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e)
|
||||
{
|
||||
}
|
||||
});
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_E);
|
||||
|
||||
this.actions.put("exit", action = new AbstractAction(langs
|
||||
.getString("actions.exit"))
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
if (this.isEnabled())
|
||||
{
|
||||
Interface.this
|
||||
.dispatchEvent(new WindowEvent(Interface.this, WindowEvent.WINDOW_CLOSING));
|
||||
}
|
||||
}
|
||||
});
|
||||
action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_X);
|
||||
|
||||
this.actions.put("logs", action = new AbstractAction(langs
|
||||
.getString("actions.logs"))
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
if (this.isEnabled())
|
||||
{
|
||||
LogWindow logs = new LogWindow();
|
||||
logs.pack();
|
||||
logs.setSize(600, 400);
|
||||
logs.setVisible(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
action.putValue(Action.ACCELERATOR_KEY, KeyStroke
|
||||
.getKeyStroke(KeyEvent.VK_F12, 0));
|
||||
|
||||
this.actions.put("debug.error", action =
|
||||
new AbstractAction(langs.getString("actions.debug.error"))
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
if (this.isEnabled())
|
||||
{
|
||||
new SwingWorker<Void, Void>()
|
||||
{
|
||||
/**
|
||||
* Computes
|
||||
* a
|
||||
* result,
|
||||
* or
|
||||
* throws
|
||||
* an
|
||||
* exception
|
||||
* if
|
||||
* unable
|
||||
* to
|
||||
* do
|
||||
* so.
|
||||
*
|
||||
* <p>
|
||||
* Note
|
||||
* that
|
||||
* this
|
||||
* method
|
||||
* is
|
||||
* executed
|
||||
* only
|
||||
* once.
|
||||
*
|
||||
* <p>
|
||||
* Note:
|
||||
* this
|
||||
* method
|
||||
* is
|
||||
* executed
|
||||
* in
|
||||
* a
|
||||
* background
|
||||
* thread.
|
||||
*
|
||||
* @return the computed result
|
||||
* @throws Exception if unable to compute a result
|
||||
*/
|
||||
@Override
|
||||
protected Void doInBackground() throws Exception
|
||||
{
|
||||
QueryFuture<Void> command = PlayerManager.getPlayers().throwError(false);
|
||||
try
|
||||
{
|
||||
if (!command.getConfirmation().wasSuccessful())
|
||||
{
|
||||
logger.error("Could not run command",
|
||||
command.getConfirmation()
|
||||
.getError());
|
||||
JOptionPane
|
||||
.showMessageDialog(Interface.this,
|
||||
command.getConfirmation()
|
||||
.getError(),
|
||||
command.getConfirmation()
|
||||
.getMessage(),
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
catch (ExecutionException | InterruptedException executionException)
|
||||
{
|
||||
logger.error("Error" +
|
||||
" creating " +
|
||||
"debug " +
|
||||
"message", executionException);
|
||||
JOptionPane
|
||||
.showMessageDialog(Interface.this, executionException
|
||||
.getMessage(), langs
|
||||
.getString(
|
||||
"error.command"), JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
}
|
||||
});
|
||||
action.putValue(Action.ACCELERATOR_KEY, KeyStroke
|
||||
.getKeyStroke(KeyEvent.VK_F12, 0));
|
||||
|
||||
this.actions.put("about", action = new AbstractAction(langs
|
||||
.getString("actions.about"))
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
if (this.isEnabled())
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.actions
|
||||
.put("view.all", action = new AbstractAction(langs
|
||||
.getString("actions.view.all"))
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
if (this.isEnabled())
|
||||
{
|
||||
collectionTypes
|
||||
.triggerSongDisplayListeners(new ArrayList<>(SongProvider.INSTANCE
|
||||
.getSongs()));
|
||||
}
|
||||
}
|
||||
});
|
||||
action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_A);
|
||||
|
||||
this.actions.put("view.artists", action = new AbstractAction(langs
|
||||
.getString("actions.view.artists"))
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
if (this.isEnabled())
|
||||
{
|
||||
collectionTypes
|
||||
.triggerCollectionDisplayListeners(CollectionType.albumArtist, SongProvider.INSTANCE
|
||||
.getAlbumArtists());
|
||||
}
|
||||
}
|
||||
});
|
||||
action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_T);
|
||||
|
||||
this.actions.put("view.albums", action = new AbstractAction(langs
|
||||
.getString("actions.view.albums"))
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
if (this.isEnabled())
|
||||
{
|
||||
collectionTypes
|
||||
.triggerCollectionDisplayListeners(CollectionType.album, SongProvider.INSTANCE
|
||||
.getAlbums());
|
||||
}
|
||||
}
|
||||
});
|
||||
action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_B);
|
||||
|
||||
this.actions.put("view.genres", action = new AbstractAction(langs
|
||||
.getString("actions.view.genres"))
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
if (this.isEnabled())
|
||||
{
|
||||
collectionTypes
|
||||
.triggerCollectionDisplayListeners(CollectionType.genre, SongProvider.INSTANCE
|
||||
.getGenres());
|
||||
}
|
||||
}
|
||||
});
|
||||
action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_G);
|
||||
|
||||
this.actions.put("view.years", action = new AbstractAction(langs
|
||||
.getString("actions.view.years"))
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
if (this.isEnabled())
|
||||
{
|
||||
collectionTypes
|
||||
.triggerCollectionDisplayListeners(CollectionType.year, SongProvider.INSTANCE
|
||||
.getYears());
|
||||
}
|
||||
}
|
||||
});
|
||||
action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_Y);
|
||||
|
||||
this.actions.put("view.playlists", action = new AbstractAction(langs
|
||||
.getString("actions.view.playlists"))
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
if (this.isEnabled())
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_L);
|
||||
|
||||
this.actions.put("playback.clear", action = new AbstractAction(langs
|
||||
.getString("actions.playback.clear"))
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
if (this.isEnabled())
|
||||
{
|
||||
Queue.getInstance().clear();
|
||||
}
|
||||
}
|
||||
});
|
||||
action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_R);
|
||||
|
||||
this.actions.put("playback.play", action = new AbstractAction(langs
|
||||
.getString("actions.playback.play"))
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
if (this.isEnabled())
|
||||
{
|
||||
PlayerManager.getPlayers().play();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.actions.put("playback.pause", action = new AbstractAction(langs
|
||||
.getString("actions.playback.pause"))
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
if (this.isEnabled())
|
||||
{
|
||||
PlayerManager.getPlayers().pause();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.actions.put("playback.toggle", action = new AbstractAction(langs
|
||||
.getString("actions.playback.toggle"))
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
if (this.isEnabled())
|
||||
{
|
||||
PlayerManager.getPlayers().toggle();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.actions.put("playback.skipPrev", action = new AbstractAction(langs
|
||||
.getString("actions.playback.skipPrev"))
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
if (this.isEnabled())
|
||||
{
|
||||
controls.previousSong();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.actions.put("playback.skipNext", action = new AbstractAction(langs
|
||||
.getString("actions.playback.skipNext"))
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
if (this.isEnabled())
|
||||
{
|
||||
controls.nextSong();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void constructWindow()
|
||||
{
|
||||
JMenuBar toolbar;
|
||||
JMenu fileMenu, viewMenu, collectionsMenu, playbackMenu, helpMenu,
|
||||
debugMenu;
|
||||
|
||||
this.setTitle(langs.getString("title"));
|
||||
this.getContentPane().setLayout(new BorderLayout());
|
||||
this.setFocusable(true);
|
||||
this.setFocusCycleRoot(true);
|
||||
|
||||
this.getContentPane()
|
||||
.add(this.collectionTypes, BorderLayout.LINE_START);
|
||||
this.collectionTypes.addFocusListener(this);
|
||||
this.collectionTypes.addSongDisplayListener(this);
|
||||
|
||||
this.controls.addFocusListener(this);
|
||||
this.getContentPane().add(controls, BorderLayout.PAGE_END);
|
||||
|
||||
this.queueList.addFocusListener(this);
|
||||
Queue.getInstance().addQueueChangeListener(this.queueList);
|
||||
Queue.getInstance().addSongChangeListener(this.queueList);
|
||||
this.getContentPane().add(queueList, BorderLayout.LINE_END);
|
||||
|
||||
this.songList.addFocusListener(this);
|
||||
this.collectionList.addSongDisplayListener(this);
|
||||
|
||||
this.getContentPane().add(this.centerView, BorderLayout.CENTER);
|
||||
this.componentResized(null);
|
||||
|
||||
toolbar = new JMenuBar();
|
||||
|
||||
fileMenu = new JMenu(langs.getString("menu.file.title"));
|
||||
fileMenu.setMnemonic(KeyEvent.VK_F);
|
||||
toolbar.add(fileMenu);
|
||||
|
||||
viewMenu = new JMenu(langs.getString("menu.view.title"));
|
||||
viewMenu.setMnemonic(KeyEvent.VK_V);
|
||||
toolbar.add(viewMenu);
|
||||
|
||||
collectionsMenu = new JMenu(langs.getString("menu.view.collections"));
|
||||
collectionsMenu.setMnemonic(KeyEvent.VK_C);
|
||||
viewMenu.add(collectionsMenu);
|
||||
|
||||
playbackMenu = new JMenu(langs.getString("menu.playback.title"));
|
||||
playbackMenu.setMnemonic(KeyEvent.VK_P);
|
||||
toolbar.add(playbackMenu);
|
||||
|
||||
helpMenu = new JMenu(langs.getString("menu.help.title"));
|
||||
helpMenu.setMnemonic(KeyEvent.VK_H);
|
||||
toolbar.add(helpMenu);
|
||||
|
||||
debugMenu = new JMenu(langs.getString("menu.debug.title"));
|
||||
debugMenu.setMnemonic(KeyEvent.VK_D);
|
||||
helpMenu.add(debugMenu);
|
||||
|
||||
fileMenu.add(new JMenuItem(actions.get("refresh")));
|
||||
|
||||
fileMenu.add(actions.get("addExternal"));
|
||||
|
||||
fileMenu.add(new JMenuItem(actions.get("exit")));
|
||||
|
||||
collectionsMenu.add(new JMenuItem(actions.get("view.all")));
|
||||
collectionsMenu.add(new JMenuItem(actions.get("view.artists")));
|
||||
collectionsMenu.add(new JMenuItem(actions.get("view.albums")));
|
||||
collectionsMenu.add(new JMenuItem(actions.get("view.genres")));
|
||||
collectionsMenu.add(new JMenuItem(actions.get("view.years")));
|
||||
collectionsMenu.add(new JMenuItem(actions.get("view.playlists")));
|
||||
|
||||
playbackMenu.add(new JMenuItem(actions.get("playback.clear")));
|
||||
playbackMenu.addSeparator();
|
||||
playbackMenu.add(new JMenuItem(actions.get("playback.toggle")));
|
||||
playbackMenu.add(new JMenuItem(actions.get("playback.skipPrev")));
|
||||
playbackMenu.add(new JMenuItem(actions.get("playback.skipNext")));
|
||||
|
||||
helpMenu.add(new JMenuItem(actions.get("logs")));
|
||||
|
||||
helpMenu.add(debugMenu);
|
||||
debugMenu.add(new JMenuItem(actions.get("debug.error")));
|
||||
|
||||
helpMenu.add(new JMenuItem(actions.get("about")));
|
||||
|
||||
this.setJMenuBar(toolbar);
|
||||
|
||||
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
|
||||
this.addComponentListener(this);
|
||||
this.addWindowListener(this);
|
||||
this.addFocusListener(this);
|
||||
}
|
||||
|
||||
protected void setFocusManager()
|
||||
{
|
||||
((SortingFocusTraversalPolicy) this.getFocusTraversalPolicy())
|
||||
.setImplicitDownCycleTraversal(true);
|
||||
this.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, Set
|
||||
.of(AWTKeyStroke
|
||||
.getAWTKeyStroke(KeyEvent.VK_DOWN, 0), AWTKeyStroke
|
||||
.getAWTKeyStroke(KeyEvent.VK_RIGHT, 0)));
|
||||
this.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, Set
|
||||
.of(AWTKeyStroke
|
||||
.getAWTKeyStroke(KeyEvent.VK_UP, 0), AWTKeyStroke
|
||||
.getAWTKeyStroke(KeyEvent.VK_LEFT, 0)));
|
||||
this.setFocusTraversalKeys(KeyboardFocusManager.UP_CYCLE_TRAVERSAL_KEYS, Set
|
||||
.of(AWTKeyStroke.getAWTKeyStroke(KeyEvent.VK_TAB, 0)));
|
||||
this.setFocusTraversalKeys(KeyboardFocusManager.DOWN_CYCLE_TRAVERSAL_KEYS, Set
|
||||
.of(AWTKeyStroke
|
||||
.getAWTKeyStroke(KeyEvent.VK_TAB, KeyEvent.SHIFT_DOWN_MASK)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSongs(Collection<? extends Song> songs)
|
||||
{
|
||||
this.songList.listAlbums(songs);
|
||||
this.centerView.setViewportView(this.songList);
|
||||
this.songList.revalidate();
|
||||
this.centerView.revalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateCollections(CollectionType type, Collection<?> collections)
|
||||
{
|
||||
this.collectionList.listCollection(type, collections);
|
||||
this.centerView.setViewportView(this.collectionList);
|
||||
this.collectionList.revalidate();
|
||||
this.centerView.revalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void componentResized(ComponentEvent event)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void componentMoved(ComponentEvent event)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void componentShown(ComponentEvent event)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void componentHidden(ComponentEvent event)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windowOpened(WindowEvent windowEvent)
|
||||
{
|
||||
logger.info("Interface opened");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windowClosing(WindowEvent windowEvent)
|
||||
{
|
||||
PlayerManager.getPlayers().shutdownPlayers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windowClosed(WindowEvent windowEvent)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windowIconified(WindowEvent windowEvent)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windowDeiconified(WindowEvent windowEvent)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windowActivated(WindowEvent windowEvent)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windowDeactivated(WindowEvent windowEvent)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate(int updated, int totalUpdate, String updating)
|
||||
{
|
||||
this.controls.setUpdateProgress(updated, totalUpdate, updating);
|
||||
if (updated == totalUpdate || totalUpdate == 0)
|
||||
{
|
||||
Collection<Song> songs = SongProvider.INSTANCE.getSongs();
|
||||
logger.debug("Resetting the song provider with {} songs.",
|
||||
songs.size());
|
||||
/*
|
||||
* TODO - Add some way to get back to the current view, just updated
|
||||
*/
|
||||
this.updateSongs(songs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void focusGained(FocusEvent e)
|
||||
{
|
||||
Component parent;
|
||||
if (e.getOppositeComponent() == null)
|
||||
{
|
||||
collectionTypes.requestFocusInWindow();
|
||||
return;
|
||||
}
|
||||
parent = e.getOppositeComponent().getParent();
|
||||
if (e.getComponent() == this)
|
||||
{
|
||||
if (parent == collectionTypes)
|
||||
{
|
||||
centerView.getViewport().getView().requestFocusInWindow();
|
||||
}
|
||||
else if (parent == songList || parent == collectionList)
|
||||
{
|
||||
queueList.requestFocusInWindow();
|
||||
}
|
||||
else if (parent == queueList.songList || parent == queueList.header)
|
||||
{
|
||||
controls.requestFocusInWindow();
|
||||
}
|
||||
else if (parent == controls)
|
||||
{
|
||||
collectionTypes.requestFocusInWindow();
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.warn("Unrecognized parent {}", parent);
|
||||
collectionTypes.requestFocusInWindow();
|
||||
}
|
||||
}
|
||||
/*
|
||||
* We are transfering backwards from an inner element
|
||||
*/
|
||||
else
|
||||
{
|
||||
int index = -1;
|
||||
Component[] children = ((Container) e.getComponent())
|
||||
.getComponents();
|
||||
for (int i = 0, l = children.length; index == -1 && i < l; i++)
|
||||
{
|
||||
if (children[i] == e.getOppositeComponent())
|
||||
{
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
if (index != -1)
|
||||
{
|
||||
parent = e.getComponent();
|
||||
if (parent == collectionTypes)
|
||||
{
|
||||
centerView.getViewport().getView().requestFocusInWindow();
|
||||
}
|
||||
else if (parent == songList || parent == collectionList)
|
||||
{
|
||||
queueList.requestFocusInWindow();
|
||||
}
|
||||
else if (parent == controls)
|
||||
{
|
||||
controls.requestFocusInWindow();
|
||||
}
|
||||
else if (parent == queueList.songList || parent == queueList.header)
|
||||
{
|
||||
collectionTypes.requestFocusInWindow();
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.warn("Unrecognized parent {}", parent);
|
||||
collectionTypes.requestFocusInWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void focusLost(FocusEvent e)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package edu.regis.universeplayer.gui;
|
||||
|
||||
import edu.regis.universeplayer.data.InternetSong;
|
||||
import edu.regis.universeplayer.data.InternetSongProvider;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.FocusAdapter;
|
||||
import java.awt.event.FocusEvent;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Locale;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* This dialog is used to add songs
|
||||
*/
|
||||
public class InternetSongDialog extends JDialog
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(InternetSongDialog.class);
|
||||
private static final ResourceBundle langs = ResourceBundle.getBundle("lang.interface", Locale.getDefault());
|
||||
|
||||
private final JTextField urlBox;
|
||||
private final JTextField titleBox;
|
||||
private final JTextField albumBox;
|
||||
private final JTextField artistBox;
|
||||
private final JTextField genreBox;
|
||||
|
||||
public InternetSongDialog(Frame owner)
|
||||
{
|
||||
super(owner, true);
|
||||
|
||||
JLabel label;
|
||||
GridBagConstraints c = new GridBagConstraints();
|
||||
this.getContentPane().setLayout(new GridBagLayout());
|
||||
|
||||
int y = 0;
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = y;
|
||||
c.gridwidth = 2;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.anchor = GridBagConstraints.WEST;
|
||||
c.insets = new Insets(0, 2, 5, 2);
|
||||
|
||||
this.getContentPane().add(label = new JLabel(langs.getString("actions.addExternal.url")), c);
|
||||
this.urlBox = new JTextField();
|
||||
this.urlBox.addFocusListener(new FocusAdapter()
|
||||
{
|
||||
@Override
|
||||
public void focusLost(FocusEvent e)
|
||||
{
|
||||
try
|
||||
{
|
||||
new URL(((JTextField) e.getSource()).getText());
|
||||
((JTextField) e.getSource()).setForeground(Color.BLACK);
|
||||
}
|
||||
catch (MalformedURLException ex)
|
||||
{
|
||||
((JTextField) e.getSource()).requestFocusInWindow();
|
||||
((JTextField) e.getSource()).setForeground(Color.RED);
|
||||
}
|
||||
}
|
||||
});
|
||||
c.gridy = ++y;
|
||||
this.getContentPane().add(this.urlBox, c);
|
||||
label.setLabelFor(this.urlBox);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = ++y;
|
||||
this.getContentPane().add(label = new JLabel(langs.getString("actions.addExternal.song")), c);
|
||||
this.titleBox = new JTextField();
|
||||
c.gridy = ++y;
|
||||
this.getContentPane().add(this.titleBox, c);
|
||||
label.setLabelFor(this.titleBox);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = ++y;
|
||||
this.getContentPane().add(label = new JLabel(langs.getString("actions.addExternal.album")), c);
|
||||
this.albumBox = new JTextField();
|
||||
c.gridy = ++y;
|
||||
this.getContentPane().add(this.albumBox, c);
|
||||
label.setLabelFor(this.albumBox);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = ++y;
|
||||
this.getContentPane().add(label = new JLabel(langs.getString("actions.addExternal.artists")), c);
|
||||
this.artistBox = new JTextField();
|
||||
c.gridy = ++y;
|
||||
this.getContentPane().add(this.artistBox, c);
|
||||
label.setLabelFor(this.artistBox);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = ++y;
|
||||
this.getContentPane().add(label = new JLabel(langs.getString("actions.addExternal.genre")), c);
|
||||
this.genreBox = new JTextField();
|
||||
c.gridy = ++y;
|
||||
this.getContentPane().add(this.genreBox, c);
|
||||
label.setLabelFor(this.genreBox);
|
||||
|
||||
c.gridwidth = 1;
|
||||
c.gridx = 0;
|
||||
c.gridy = ++y;
|
||||
JButton cancelButton = new JButton(langs.getString("actions.cancel"));
|
||||
cancelButton.addActionListener(e -> this.dispose());
|
||||
this.getContentPane().add(cancelButton, c);
|
||||
c.gridx = 1;
|
||||
JButton addButton = new JButton(langs.getString("actions.add"));
|
||||
addButton.addActionListener(e -> this.confirm());
|
||||
this.getContentPane().add(addButton, c);
|
||||
|
||||
this.pack();
|
||||
}
|
||||
|
||||
private void confirm()
|
||||
{
|
||||
URL url;
|
||||
if (this.urlBox.getText().isEmpty())
|
||||
{
|
||||
this.urlBox.requestFocusInWindow();
|
||||
this.urlBox.setForeground(Color.RED);
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
url = new URL(this.urlBox.getText());
|
||||
}
|
||||
catch (MalformedURLException e)
|
||||
{
|
||||
this.urlBox.requestFocusInWindow();
|
||||
return;
|
||||
}
|
||||
|
||||
for (Component comp: this.getContentPane().getComponents())
|
||||
{
|
||||
comp.setEnabled(false);
|
||||
}
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
Future<InternetSong> future = InternetSongProvider.getInstance().addSong(url, this.titleBox.getText(), this.albumBox.getText(), this.artistBox.getText(), this.genreBox.getText());
|
||||
InternetSong song = null;
|
||||
try
|
||||
{
|
||||
song = future.get();
|
||||
}
|
||||
catch (InterruptedException | ExecutionException e)
|
||||
{
|
||||
logger.error("Could not evaluate song", e);
|
||||
JOptionPane.showMessageDialog(InternetSongDialog.this, e.getMessage(), langs.getString("error.generic"), JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (song != null)
|
||||
{
|
||||
dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
for (Component comp: this.getContentPane().getComponents())
|
||||
{
|
||||
comp.setEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
package edu.regis.universeplayer.gui;
|
||||
|
||||
import org.apache.logging.log4j.core.LogEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Color;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.util.Comparator;
|
||||
import java.util.Formatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.Box;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JMenu;
|
||||
import javax.swing.JMenuItem;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JPopupMenu;
|
||||
import javax.swing.JScrollBar;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.SwingUtilities;
|
||||
|
||||
import edu.regis.universeplayer.ListAppender;
|
||||
|
||||
public class LogWindow extends JFrame
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(LogWindow.class);
|
||||
private static final ResourceBundle langs = ResourceBundle
|
||||
.getBundle("lang.interface", Locale.getDefault());
|
||||
|
||||
private final HashMap<String, JComponent> filters = new HashMap<>();
|
||||
private final Box filterBox;
|
||||
private final Box logBox;
|
||||
private final JScrollPane centerView;
|
||||
private final JMenu loggers;
|
||||
private int lastLogRecord = 0;
|
||||
private Thread logUpdaterThread;
|
||||
private boolean active = true;
|
||||
|
||||
public LogWindow()
|
||||
{
|
||||
super();
|
||||
|
||||
this.setTitle(langs.getString("logs.title"));
|
||||
|
||||
FlowLayout layout = new FlowLayout();
|
||||
JPanel header = new JPanel();
|
||||
header.setLayout(layout);
|
||||
|
||||
this.filterBox = Box.createHorizontalBox();
|
||||
header.add(filterBox);
|
||||
|
||||
JPopupMenu filterMenu = new JPopupMenu("Filters");
|
||||
JButton filterButton = new JButton("\u25BC");
|
||||
filterButton.addMouseListener(new MouseAdapter()
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @param e
|
||||
*/
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e)
|
||||
{
|
||||
filterMenu.show(e.getComponent(), e.getX(), e.getY());
|
||||
}
|
||||
});
|
||||
header.add(filterButton);
|
||||
|
||||
JMenu levels = new JMenu(langs.getString("logs.levels"));
|
||||
filterMenu.add(levels);
|
||||
JMenuItem levelTrace = new JMenuItem(langs.getString("logs.levels.trace"));
|
||||
levelTrace.addActionListener(e -> addFilter("levels.trace"));
|
||||
levels.add(levelTrace);
|
||||
JMenuItem levelDebug = new JMenuItem(langs.getString("logs.levels.debug"));
|
||||
levelDebug.addActionListener(e -> addFilter("levels.debug"));
|
||||
levels.add(levelDebug);
|
||||
JMenuItem levelInfo = new JMenuItem(langs.getString("logs.levels.info"));
|
||||
levelInfo.addActionListener(e -> addFilter("levels.info"));
|
||||
levels.add(levelInfo);
|
||||
JMenuItem levelWarning = new JMenuItem(langs.getString("logs.levels.warning"));
|
||||
levelWarning.addActionListener(e -> addFilter("levels.warning"));
|
||||
levels.add(levelWarning);
|
||||
JMenuItem levelError = new JMenuItem(langs.getString("logs.levels.error"));
|
||||
levelError.addActionListener(e -> addFilter("levels.error"));
|
||||
levels.add(levelError);
|
||||
|
||||
this.loggers = new JMenu(langs.getString("logs.loggers"));
|
||||
filterMenu.add(this.loggers);
|
||||
|
||||
this.getContentPane().add(header, BorderLayout.PAGE_START);
|
||||
|
||||
this.logBox = Box.createVerticalBox();
|
||||
|
||||
this.centerView = new JScrollPane(this.logBox);
|
||||
this.getContentPane().add(centerView, BorderLayout.CENTER);
|
||||
|
||||
this.addWindowListener(new WindowAdapter()
|
||||
{
|
||||
/**
|
||||
* Invoked when a window is in the process of being closed.
|
||||
* The close operation can be overridden at this point.
|
||||
*
|
||||
* @param e
|
||||
*/
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e)
|
||||
{
|
||||
active = false;
|
||||
}
|
||||
});
|
||||
|
||||
this.logUpdaterThread = new Thread(() -> {
|
||||
while (active)
|
||||
{
|
||||
if (ListAppender.getLogEvents().size() > this.lastLogRecord)
|
||||
{
|
||||
this.lastLogRecord = ListAppender.getLogEvents().size();
|
||||
SwingUtilities.invokeLater(() -> this.resetLogs());
|
||||
try
|
||||
{
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
catch (InterruptedException e)
|
||||
{
|
||||
logger.error("Could not sleep", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Stop
|
||||
*/
|
||||
});
|
||||
this.logUpdaterThread.start();
|
||||
}
|
||||
|
||||
private void addFilter(String filter)
|
||||
{
|
||||
if (!this.filters.containsKey(filter))
|
||||
{
|
||||
Box filterBox = Box.createHorizontalBox();
|
||||
if (langs.containsKey("logs." + filter))
|
||||
{
|
||||
filterBox.add(new JLabel(langs.getString("logs." + filter)));
|
||||
}
|
||||
else
|
||||
{
|
||||
filterBox.add(new JLabel(filter.substring(filter.indexOf('.') + 1)));
|
||||
}
|
||||
|
||||
filterBox.add(new JButton(new AbstractAction("X")
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
removeFilter(filter);
|
||||
}
|
||||
}));
|
||||
this.filters.put(filter, filterBox);
|
||||
this.filterBox.add(filterBox);
|
||||
this.resetLogs();
|
||||
this.filterBox.revalidate();
|
||||
this.filterBox.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
private void removeFilter(String filter)
|
||||
{
|
||||
JComponent component = this.filters.remove(filter);
|
||||
if (component != null)
|
||||
{
|
||||
component.getParent().remove(component);
|
||||
this.resetLogs();
|
||||
this.filterBox.revalidate();
|
||||
this.filterBox.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void resetLogs()
|
||||
{
|
||||
JScrollBar scroll = this.centerView.getVerticalScrollBar();
|
||||
boolean bottom = scroll.getModel().getValue() >= scroll.getModel().getMaximum() - scroll
|
||||
.getModel().getExtent();
|
||||
this.logBox.removeAll();
|
||||
this.loggers.removeAll();
|
||||
ListAppender.getLogEvents().stream().map(LogEvent::getLoggerName).distinct().forEach(name -> {
|
||||
JMenuItem loggerItem = new JMenuItem(name);
|
||||
loggerItem.addActionListener(e -> addFilter("loggers." + name));
|
||||
this.loggers.add(loggerItem);
|
||||
});
|
||||
ListAppender.getLogEvents().stream().filter(event -> {
|
||||
Set<String> filters = this.filters.keySet();
|
||||
if (filters.stream().anyMatch(filter -> filter.startsWith("levels")))
|
||||
{
|
||||
switch (event.getLevel().name())
|
||||
{
|
||||
case "TRACE":
|
||||
if (!filters.stream().anyMatch(f -> f.equals("levels.trace")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case "DEBUG":
|
||||
if (!filters.stream().anyMatch(f -> f.equals("levels.debug")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case "WARN":
|
||||
if (!filters.stream().anyMatch(f -> f.equals("levels.warning")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case "ERROR":
|
||||
case "FATAL":
|
||||
if (!filters.stream().anyMatch(f -> f.equals("levels.error")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!filters.stream().anyMatch(f -> f.equals("levels.info")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (filters.stream().anyMatch(f -> f.startsWith("loggers")))
|
||||
{
|
||||
String loggerName = "loggers." + event.getLoggerName();
|
||||
if (!filters.stream().anyMatch(f -> f.equals(loggerName)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).sorted(Comparator.comparingLong(LogEvent::getNanoTime)).forEach(event -> {
|
||||
Formatter form = new Formatter();
|
||||
JLabel label = new JLabel(form
|
||||
.format("%tT - %s - %s", event.getNanoTime(), event.getLoggerName(), event
|
||||
.getMessage().getFormattedMessage()).toString());
|
||||
form = new Formatter();
|
||||
label.setToolTipText(form.format("(%s#%s:%d)", Optional
|
||||
.ofNullable(event.getSource()).map(StackTraceElement::getClassName)
|
||||
.orElse(""), Optional
|
||||
.ofNullable(event.getSource()).map(StackTraceElement::getMethodName)
|
||||
.orElse(""), Optional
|
||||
.ofNullable(event.getSource()).map(StackTraceElement::getLineNumber).orElse(-1))
|
||||
.toString());
|
||||
switch (event.getLevel().name())
|
||||
{
|
||||
case "TRACE":
|
||||
label.setForeground(Color.GRAY);
|
||||
break;
|
||||
case "DEBUG":
|
||||
label.setForeground(Color.BLUE);
|
||||
break;
|
||||
case "WARN":
|
||||
label.setForeground(Color.ORANGE);
|
||||
break;
|
||||
case "ERROR":
|
||||
case "FATAL":
|
||||
label.setForeground(Color.RED);
|
||||
break;
|
||||
default:
|
||||
label.setForeground(Color.BLACK);
|
||||
break;
|
||||
}
|
||||
logBox.add(label);
|
||||
});
|
||||
this.logBox.revalidate();
|
||||
this.logBox.repaint();
|
||||
if (bottom)
|
||||
{
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
scroll.setValue(scroll.getMaximum());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
LogWindow window = new LogWindow();
|
||||
window.pack();
|
||||
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
window.setVisible(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package edu.regis.universeplayer.gui;
|
||||
|
||||
/**
|
||||
* Contains possible playback commands that the play can send.
|
||||
*
|
||||
* @author William Hubbard
|
||||
* @version 0.1
|
||||
*/
|
||||
public enum PlaybackCommand
|
||||
{
|
||||
PLAY, PAUSE, NEXT, PREVIOUS, SEEK
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package edu.regis.universeplayer.gui;
|
||||
|
||||
import java.util.EventListener;
|
||||
|
||||
/**
|
||||
* A listener that allows objects to observe when playback commands are triggered.
|
||||
*
|
||||
* @author William Hubbard
|
||||
* @version 0.1
|
||||
*/
|
||||
public interface PlaybackCommandListener extends EventListener
|
||||
{
|
||||
/**
|
||||
* Called when a playback command is issued.
|
||||
*
|
||||
* @param command - The command issued.
|
||||
* @param data - Additional data relevent to the command.
|
||||
*/
|
||||
void onCommand(PlaybackCommand command, Object data);
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
/*
|
||||
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package edu.regis.universeplayer.gui;
|
||||
|
||||
import edu.regis.universeplayer.PlaybackListener;
|
||||
import edu.regis.universeplayer.PlaybackStatus;
|
||||
import edu.regis.universeplayer.player.Player;
|
||||
import edu.regis.universeplayer.browserCommands.CommandConfirmation;
|
||||
import edu.regis.universeplayer.browserCommands.QueryFuture;
|
||||
import edu.regis.universeplayer.data.PlaybackEvent;
|
||||
import edu.regis.universeplayer.data.Queue;
|
||||
import edu.regis.universeplayer.data.Song;
|
||||
import edu.regis.universeplayer.player.PlayerManager;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.FocusAdapter;
|
||||
import java.awt.event.FocusEvent;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Locale;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
|
||||
/**
|
||||
* This panel contains the buttons necessary for controlling the playback of
|
||||
* audio.
|
||||
*
|
||||
* @author William Hubbard
|
||||
* @version 0.1
|
||||
*/
|
||||
public class PlayerControls extends JPanel implements Queue.SongChangeListener, PlaybackListener
|
||||
{
|
||||
private static final Logger logger = LoggerFactory
|
||||
.getLogger(PlayerControls.class);
|
||||
private static final ResourceBundle langs = ResourceBundle
|
||||
.getBundle("lang.interface", Locale.getDefault());
|
||||
private final ImageIcon PLAY_ICON, PAUSE_ICON;
|
||||
|
||||
private final JButton playButton;
|
||||
private final JButton nextButton;
|
||||
private final JButton prevButton;
|
||||
private final JProgressBar progress;
|
||||
private final JProgressBar updateProgress;
|
||||
|
||||
private final ForkJoinPool service = new ForkJoinPool();
|
||||
|
||||
/**
|
||||
* A list of all things interested in knowing when we trigger a command.
|
||||
*/
|
||||
private final LinkedList<PlaybackCommandListener> listeners = new LinkedList<>();
|
||||
|
||||
public PlayerControls()
|
||||
{
|
||||
final Dimension BUTTON_SIZE = new Dimension(32, 32);
|
||||
final Dimension ICON_SIZE = new Dimension(16, 16);
|
||||
ImageIcon icon;
|
||||
JPanel buttonCont, progressCont;
|
||||
FlowLayout buttonLayout;
|
||||
SpringLayout progressLayout;
|
||||
|
||||
SpringLayout layout = new SpringLayout();
|
||||
this.setLayout(layout);
|
||||
this.setFocusable(true);
|
||||
this.setFocusCycleRoot(false);
|
||||
|
||||
buttonLayout = new FlowLayout();
|
||||
buttonCont = new JPanel(buttonLayout);
|
||||
this.add(buttonCont);
|
||||
|
||||
this.prevButton = new JButton(Interface.getInstance().actions
|
||||
.get("playback.skipPrev"));
|
||||
this.prevButton.setText("");
|
||||
icon = new ImageIcon(this.getClass()
|
||||
.getResource("/gui/icons/skipPrev.png"), "Previous Button");
|
||||
icon.setImage(icon.getImage()
|
||||
.getScaledInstance(ICON_SIZE.width, ICON_SIZE.height, 0));
|
||||
this.prevButton.setIcon(icon);
|
||||
this.prevButton.setPreferredSize(BUTTON_SIZE);
|
||||
buttonCont.add(this.prevButton);
|
||||
|
||||
this.playButton = new JButton(Interface.getInstance().actions
|
||||
.get("playback.toggle"));
|
||||
this.playButton.setText("");
|
||||
PAUSE_ICON = icon = new ImageIcon(this.getClass()
|
||||
.getResource("/gui/icons/pause.png"), "Pause Button");
|
||||
icon.setImage(icon.getImage()
|
||||
.getScaledInstance(ICON_SIZE.width, ICON_SIZE.height, 0));
|
||||
PLAY_ICON = icon = new ImageIcon(this.getClass()
|
||||
.getResource("/gui/icons/play.png"), "Play Button");
|
||||
icon.setImage(icon.getImage()
|
||||
.getScaledInstance(ICON_SIZE.width, ICON_SIZE.height, 0));
|
||||
this.playButton.setIcon(icon);
|
||||
this.playButton.setPreferredSize(BUTTON_SIZE);
|
||||
buttonCont.add(this.playButton);
|
||||
|
||||
this.nextButton = new JButton(Interface.getInstance().actions
|
||||
.get("playback.skipNext"));
|
||||
this.nextButton.setText("");
|
||||
icon = new ImageIcon(this.getClass()
|
||||
.getResource("/gui/icons/skipNext.png"), "Next Button");
|
||||
icon.setImage(icon.getImage()
|
||||
.getScaledInstance(ICON_SIZE.width, ICON_SIZE.height, 0));
|
||||
this.nextButton.setIcon(icon);
|
||||
this.nextButton.setPreferredSize(BUTTON_SIZE);
|
||||
buttonCont.add(this.nextButton);
|
||||
|
||||
progressLayout = new SpringLayout();
|
||||
progressCont = new JPanel(progressLayout);
|
||||
this.add(progressCont);
|
||||
|
||||
this.progress = new JProgressBar();
|
||||
this.progress.addMouseListener(new MouseAdapter()
|
||||
{
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e)
|
||||
{
|
||||
seek((float) e.getX() / (float) e.getComponent()
|
||||
.getWidth() * ((JProgressBar) e
|
||||
.getComponent()).getMaximum());
|
||||
logger.debug("Changing time");
|
||||
}
|
||||
});
|
||||
this.add(this.progress);
|
||||
|
||||
this.updateProgress = new JProgressBar();
|
||||
this.updateProgress.setStringPainted(true);
|
||||
this.setUpdateProgress(0, 0, null);
|
||||
this.add(this.updateProgress);
|
||||
|
||||
this.addFocusListener(new FocusAdapter()
|
||||
{
|
||||
@Override
|
||||
public void focusGained(FocusEvent e)
|
||||
{
|
||||
int index = -1;
|
||||
Component[] children = ((Container) e.getComponent())
|
||||
.getComponents();
|
||||
for (int i = 0, l = children.length; index == -1 && i < l; i++)
|
||||
{
|
||||
if (children[i] == e.getOppositeComponent())
|
||||
{
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Only auto-switch focus if the previous component was not from
|
||||
* within.
|
||||
*/
|
||||
if (index == -1)
|
||||
{
|
||||
prevButton.requestFocusInWindow();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
layout.putConstraint(SpringLayout.NORTH, buttonCont, 0, SpringLayout.NORTH, this);
|
||||
layout.putConstraint(SpringLayout.WEST, buttonCont, 0, SpringLayout.WEST, this);
|
||||
layout.putConstraint(SpringLayout.EAST, buttonCont, 0, SpringLayout.EAST, this);
|
||||
layout.putConstraint(SpringLayout.NORTH, this.progress, 5, SpringLayout.SOUTH, buttonCont);
|
||||
layout.putConstraint(SpringLayout.WEST, this.progress, 5, SpringLayout.WEST, this);
|
||||
layout.putConstraint(SpringLayout.EAST, this.progress, 5, SpringLayout.EAST, this);
|
||||
layout.putConstraint(SpringLayout.NORTH, this.updateProgress, 5, SpringLayout.SOUTH, this.progress);
|
||||
layout.putConstraint(SpringLayout.WEST, this.updateProgress, 5, SpringLayout.WEST, this);
|
||||
|
||||
layout.putConstraint(SpringLayout.EAST, this, 5, SpringLayout.EAST, this.progress);
|
||||
layout.putConstraint(SpringLayout.EAST, this, 5, SpringLayout.EAST, this.updateProgress);
|
||||
layout.putConstraint(SpringLayout.SOUTH, this, 5, SpringLayout.SOUTH, this.updateProgress);
|
||||
|
||||
Queue.getInstance().addSongChangeListener(this);
|
||||
PlayerManager.getPlayers().addPlaybackListener(this);
|
||||
}
|
||||
|
||||
private void seek(float value)
|
||||
{
|
||||
this.service.execute(() -> {
|
||||
QueryFuture<Void> command = PlayerManager.getPlayers().seek(value);
|
||||
try
|
||||
{
|
||||
if (command != null && !command.getConfirmation()
|
||||
.wasSuccessful())
|
||||
{
|
||||
logger.error("Could not run command",
|
||||
command.getConfirmation().getError());
|
||||
JOptionPane.showMessageDialog(this,
|
||||
command.getConfirmation().getError(),
|
||||
command.getConfirmation().getMessage(),
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
catch (ExecutionException | InterruptedException e)
|
||||
{
|
||||
logger.error("Could not run command",
|
||||
e);
|
||||
JOptionPane.showMessageDialog(this,
|
||||
e,
|
||||
e.getMessage(),
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
this.triggerCommandListeners(PlaybackCommand.SEEK, value);
|
||||
}
|
||||
|
||||
protected void play()
|
||||
{
|
||||
this.service.execute(() -> {
|
||||
try
|
||||
{
|
||||
QueryFuture<PlaybackStatus> status =
|
||||
PlayerManager.getPlayers().getStatus();
|
||||
CommandConfirmation confirmation = status.getConfirmation();
|
||||
if (status.getConfirmation().wasSuccessful())
|
||||
{
|
||||
switch (status.get())
|
||||
{
|
||||
case PAUSED -> confirmation = PlayerManager.getPlayers()
|
||||
.play()
|
||||
.getConfirmation();
|
||||
case STOPPED, EMPTY -> {
|
||||
if (Queue.getInstance().size() > 0)
|
||||
{
|
||||
if (Queue.getInstance()
|
||||
.getCurrentSong() == null)
|
||||
{
|
||||
Queue.getInstance().skipToSong(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
confirmation = PlayerManager.getPlayers().play()
|
||||
.getConfirmation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (confirmation != null && !confirmation.wasSuccessful())
|
||||
{
|
||||
logger.error("Could not run command",
|
||||
confirmation.getError());
|
||||
JOptionPane.showMessageDialog(this,
|
||||
confirmation.getError(),
|
||||
confirmation.getMessage(),
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
catch (ExecutionException | InterruptedException e)
|
||||
{
|
||||
logger.error("Could not get current playback status", e);
|
||||
JOptionPane.showMessageDialog(this, e.getMessage(), langs
|
||||
.getString(
|
||||
"error.command"), JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
this.triggerCommandListeners(PlaybackCommand.PLAY, null);
|
||||
}
|
||||
|
||||
protected void pause()
|
||||
{
|
||||
this.service.execute(() -> {
|
||||
try
|
||||
{
|
||||
QueryFuture<PlaybackStatus> status =
|
||||
PlayerManager.getPlayers().getStatus();
|
||||
CommandConfirmation confirmation = status.getConfirmation();
|
||||
if (status.getConfirmation().wasSuccessful())
|
||||
{
|
||||
switch (status.get())
|
||||
{
|
||||
case PLAYING -> confirmation =
|
||||
PlayerManager.getPlayers().pause()
|
||||
.getConfirmation();
|
||||
}
|
||||
}
|
||||
if (confirmation != null && !confirmation.wasSuccessful())
|
||||
{
|
||||
logger.error("Could not run command",
|
||||
confirmation.getError());
|
||||
JOptionPane.showMessageDialog(this,
|
||||
confirmation.getError(),
|
||||
confirmation.getMessage(),
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
catch (ExecutionException | InterruptedException e)
|
||||
{
|
||||
logger.error("Could not get current playback status", e);
|
||||
JOptionPane.showMessageDialog(this, e.getMessage(), langs
|
||||
.getString(
|
||||
"error.command"), JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
this.triggerCommandListeners(PlaybackCommand.PAUSE, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the playback of the current song.
|
||||
*/
|
||||
protected void togglePlayback()
|
||||
{
|
||||
this.service.execute(() -> {
|
||||
try
|
||||
{
|
||||
QueryFuture<PlaybackStatus> status =
|
||||
PlayerManager.getPlayers().getStatus();
|
||||
CommandConfirmation confirmation = status.getConfirmation();
|
||||
if (status.getConfirmation().wasSuccessful())
|
||||
{
|
||||
switch (status.get())
|
||||
{
|
||||
case PAUSED -> confirmation = PlayerManager.getPlayers()
|
||||
.play()
|
||||
.getConfirmation();
|
||||
case PLAYING -> confirmation = PlayerManager.getPlayers()
|
||||
.pause()
|
||||
.getConfirmation();
|
||||
case STOPPED, EMPTY -> {
|
||||
if (Queue.getInstance().size() > 0)
|
||||
{
|
||||
if (Queue.getInstance()
|
||||
.getCurrentSong() == null)
|
||||
{
|
||||
Queue.getInstance().skipToSong(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
confirmation = PlayerManager.getPlayers().play()
|
||||
.getConfirmation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (confirmation != null && !confirmation.wasSuccessful())
|
||||
{
|
||||
JOptionPane.showMessageDialog(this,
|
||||
confirmation.getError(),
|
||||
confirmation.getMessage(),
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
logger.error("Could not run command",
|
||||
confirmation.getError());
|
||||
}
|
||||
}
|
||||
catch (ExecutionException | InterruptedException e)
|
||||
{
|
||||
logger.error("Could not get current playback status", e);
|
||||
JOptionPane.showMessageDialog(this, e.getMessage(), langs
|
||||
.getString(
|
||||
"error.command"), JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
this.triggerCommandListeners(PlaybackCommand.PLAY, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips to the next song.
|
||||
*/
|
||||
protected void previousSong()
|
||||
{
|
||||
Queue.getInstance().skipPrev();
|
||||
this.triggerCommandListeners(PlaybackCommand.PREVIOUS, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips to the next song.
|
||||
*/
|
||||
protected void nextSong()
|
||||
{
|
||||
Queue.getInstance().skipNext();
|
||||
this.triggerCommandListeners(PlaybackCommand.NEXT, null);
|
||||
}
|
||||
|
||||
void setUpdateProgress(int updated, int toUpdate, String updating)
|
||||
{
|
||||
this.updateProgress.setString(updating);
|
||||
if (toUpdate == 0)
|
||||
{
|
||||
this.updateProgress.setVisible(false);
|
||||
// this.updateProgress.setPreferredSize(new Dimension(this.updateProgress.getPreferredSize().width, 0));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.updateProgress.setVisible(true);
|
||||
// this.updateProgress.setSize(new Dimension(this.updateProgress.getPreferredSize().width, this.updateSize));
|
||||
if (toUpdate < 0)
|
||||
{
|
||||
this.updateProgress.setIndeterminate(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.updateProgress.setIndeterminate(false);
|
||||
this.updateProgress.setMaximum(toUpdate);
|
||||
this.updateProgress.setValue(updated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener for playback commands.
|
||||
*
|
||||
* @param listener - The listener to add.
|
||||
*/
|
||||
public void addCommandListener(PlaybackCommandListener listener)
|
||||
{
|
||||
this.listeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a playback listener.
|
||||
*
|
||||
* @param listener - The listener to remove.
|
||||
*/
|
||||
public void removeCommandListener(PlaybackCommandListener listener)
|
||||
{
|
||||
this.listeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers all the command listeners.
|
||||
*
|
||||
* @param command - The command to trigger.
|
||||
* @param data - Extra command data.
|
||||
*/
|
||||
protected void triggerCommandListeners(PlaybackCommand command, Object data)
|
||||
{
|
||||
for (PlaybackCommandListener listener : this.listeners)
|
||||
{
|
||||
listener.onCommand(command, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSongChange(Queue queue)
|
||||
{
|
||||
this.service.execute(() -> {
|
||||
QueryFuture<Void> command = PlayerManager.getPlayers().stopSong();
|
||||
try
|
||||
{
|
||||
if (command != null && !command.getConfirmation().wasSuccessful())
|
||||
{
|
||||
JOptionPane.showMessageDialog(this,
|
||||
command.getConfirmation().getError(),
|
||||
command.getConfirmation().getMessage(),
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
logger.error("Could not run command",
|
||||
command.getConfirmation().getError());
|
||||
}
|
||||
else
|
||||
{
|
||||
command =
|
||||
PlayerManager.getPlayers()
|
||||
.playSong(queue.getCurrentSong());
|
||||
if (!command.getConfirmation().wasSuccessful())
|
||||
{
|
||||
JOptionPane.showMessageDialog(this,
|
||||
command.getConfirmation().getError(),
|
||||
command.getConfirmation().getMessage(),
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
logger.error("Could not run command",
|
||||
command.getConfirmation().getError());
|
||||
}
|
||||
else
|
||||
{
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
this.playButton.setIcon(PLAY_ICON);
|
||||
this.progress.setMaximum((int) (PlayerManager
|
||||
.getPlayers()
|
||||
.getCurrentSong().duration / 1000));
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ExecutionException | InterruptedException e)
|
||||
{
|
||||
logger.error("Could not get current playback status", e);
|
||||
JOptionPane
|
||||
.showMessageDialog(this, e.getMessage(), langs
|
||||
.getString(
|
||||
"error.command"), JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackChanged(PlaybackEvent status)
|
||||
{
|
||||
switch (status.getInfo().getStatus())
|
||||
{
|
||||
case PLAYING -> this.playButton.setIcon(PAUSE_ICON);
|
||||
case FINISHED -> Queue.getInstance().skipNext();
|
||||
case PAUSED, STOPPED, EMPTY -> this.playButton.setIcon(PLAY_ICON);
|
||||
}
|
||||
this.progress.setValue((int) status.getInfo().getPlayTime());
|
||||
this.progress.setMaximum((int) (status.getInfo()
|
||||
.getSong().duration / 1000));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package edu.regis.universeplayer.gui;
|
||||
|
||||
import com.wordpress.tips4java.ScrollablePanel;
|
||||
import edu.regis.universeplayer.ClickListener;
|
||||
import edu.regis.universeplayer.data.Queue;
|
||||
import edu.regis.universeplayer.data.Song;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.FocusAdapter;
|
||||
import java.awt.event.FocusEvent;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.Formatter;
|
||||
import java.util.Locale;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
public class QueueList extends JPanel implements Queue.SongChangeListener, Queue.QueueChangeListener
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(QueueList.class);
|
||||
private static final ResourceBundle langs = ResourceBundle.getBundle("lang.interface", Locale.getDefault());
|
||||
|
||||
final JPanel header;
|
||||
final ScrollablePanel songList;
|
||||
private int currentHighlight = 0;
|
||||
|
||||
private final JButton clearButton;
|
||||
|
||||
public QueueList()
|
||||
{
|
||||
GridBagConstraints c = new GridBagConstraints();
|
||||
|
||||
this.setLayout(new BorderLayout());
|
||||
this.setFocusable(true);
|
||||
// this.setFocusCycleRoot(true);
|
||||
|
||||
header = new JPanel(new GridBagLayout());
|
||||
// header.setFocusCycleRoot(true);
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
c.gridx = 0;
|
||||
c.gridy = 0;
|
||||
c.gridwidth = 1;
|
||||
header.add(new JLabel(" ".repeat(20) + langs.getString("interface.queue.title") + " ".repeat(20)), c);
|
||||
this.clearButton = new JButton(Interface.getInstance().actions.get("playback.clear"));
|
||||
c.gridy = 1;
|
||||
c.gridwidth = 1;
|
||||
header.add(clearButton, c);
|
||||
header.addFocusListener(new FocusAdapter()
|
||||
{
|
||||
@Override
|
||||
public void focusGained(FocusEvent e)
|
||||
{
|
||||
int index = -1;
|
||||
Component[] children = ((Container) e.getComponent()).getComponents();
|
||||
for (int i = 0, l = children.length; index == -1 && i < l; i++)
|
||||
{
|
||||
if (children[i] == e.getOppositeComponent())
|
||||
{
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Only auto-switch focus if the previous component was not from
|
||||
* within.
|
||||
*/
|
||||
if (index == -1)
|
||||
{
|
||||
clearButton.requestFocusInWindow();
|
||||
}
|
||||
}
|
||||
});
|
||||
this.add(header, BorderLayout.NORTH);
|
||||
|
||||
this.songList = new ScrollablePanel(new GridLayout(0, 2));
|
||||
/*
|
||||
* Don't focus on here until we have elements.
|
||||
*/
|
||||
this.songList.setFocusable(false);
|
||||
this.songList.setFocusCycleRoot(true);
|
||||
this.songList.addFocusListener(new FocusAdapter()
|
||||
{
|
||||
@Override
|
||||
public void focusGained(FocusEvent e)
|
||||
{
|
||||
int index = -1;
|
||||
Component[] children = ((Container) e.getComponent()).getComponents();
|
||||
for (int i = 0, l = children.length; index == -1 && i < l; i++)
|
||||
{
|
||||
if (children[i] == e.getOppositeComponent())
|
||||
{
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Only auto-switch focus if the previous component was not from
|
||||
* within.
|
||||
*/
|
||||
if (index == -1)
|
||||
{
|
||||
songList.getComponents()[0].requestFocusInWindow();
|
||||
}
|
||||
else
|
||||
{
|
||||
header.requestFocusInWindow();
|
||||
}
|
||||
}
|
||||
});
|
||||
this.songList.setScrollableWidth(ScrollablePanel.ScrollableSizeHint.FIT);
|
||||
|
||||
JScrollPane scroll = new JScrollPane(this.songList);
|
||||
// this.scroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
|
||||
// this.scroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
this.add(scroll, BorderLayout.CENTER);
|
||||
|
||||
this.addFocusListener(new FocusAdapter()
|
||||
{
|
||||
@Override
|
||||
public void focusGained(FocusEvent e)
|
||||
{
|
||||
int index = -1;
|
||||
Component[] children = ((Container) e.getComponent()).getComponents();
|
||||
for (int i = 0, l = children.length; index == -1 && i < l; i++)
|
||||
{
|
||||
if (children[i] == e.getOppositeComponent())
|
||||
{
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Only auto-switch focus if the previous component was not from
|
||||
* within.
|
||||
*/
|
||||
if (index == -1)
|
||||
{
|
||||
header.requestFocusInWindow();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSongChange(Queue queue)
|
||||
{
|
||||
if (this.currentHighlight >= 0)
|
||||
{
|
||||
this.songList.getComponent(this.currentHighlight * 2).setForeground(Color.BLACK);
|
||||
}
|
||||
this.currentHighlight = queue.getCurrentIndex();
|
||||
if (this.currentHighlight >= 0)
|
||||
{
|
||||
this.songList.getComponent(this.currentHighlight * 2).setForeground(Color.BLUE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQueueChange(Queue queue)
|
||||
{
|
||||
Formatter dateForm;
|
||||
JLabel durationLabel;
|
||||
GridBagConstraints c;
|
||||
this.songList.removeAll();
|
||||
int i = 0;
|
||||
for (Song song : queue)
|
||||
{
|
||||
SongMenu menu = new SongMenu(song, queue);
|
||||
JButton songLabel = new JButton(song.title);
|
||||
songLabel.setFocusPainted(true);
|
||||
songLabel.setMargin(new Insets(0, 0, 0, 0));
|
||||
songLabel.setContentAreaFilled(false);
|
||||
songLabel.setBorderPainted(false);
|
||||
songLabel.setOpaque(false);
|
||||
songLabel.setHorizontalAlignment(JLabel.LEFT);
|
||||
int songIndex = i;
|
||||
songLabel.addMouseListener((ClickListener) e -> {
|
||||
if (e.getButton() == MouseEvent.BUTTON1)
|
||||
{
|
||||
if (e.getClickCount() == 2)
|
||||
{
|
||||
queue.skipToSong(songIndex);
|
||||
}
|
||||
}
|
||||
else if (e.getButton() == MouseEvent.BUTTON3)
|
||||
{
|
||||
menu.show(songLabel, e.getX(), e.getY());
|
||||
}
|
||||
});
|
||||
dateForm = new Formatter();
|
||||
durationLabel = new JLabel(dateForm.format("%1$tM:%1$tS", song.duration).toString());
|
||||
durationLabel.setHorizontalAlignment(JLabel.RIGHT);
|
||||
durationLabel.addMouseListener((ClickListener) e -> {
|
||||
if (e.getClickCount() == 2)
|
||||
{
|
||||
queue.skipToSong(songIndex);
|
||||
}
|
||||
});
|
||||
dateForm.close();
|
||||
|
||||
c = new GridBagConstraints();
|
||||
c.gridx = 0;
|
||||
c.gridy = i;
|
||||
c.anchor = GridBagConstraints.WEST;
|
||||
c.insets = new Insets(0, 0, 0, 5);
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
this.songList.add(songLabel);
|
||||
|
||||
c = new GridBagConstraints();
|
||||
c.gridx = 1;
|
||||
c.gridy = i;
|
||||
c.anchor = GridBagConstraints.EAST;
|
||||
c.insets = new Insets(0, 0, 0, 0);
|
||||
this.songList.add(durationLabel);
|
||||
|
||||
songLabel.setForeground(Color.BLACK);
|
||||
i++;
|
||||
}
|
||||
this.songList.setFocusable(queue.size() > 0);
|
||||
this.currentHighlight = queue.getCurrentIndex();
|
||||
if (this.currentHighlight >= 0)
|
||||
{
|
||||
this.songList.getComponent(queue.getCurrentIndex() * 2).setForeground(Color.BLUE);
|
||||
}
|
||||
this.validate();
|
||||
}
|
||||
|
||||
private static class SongMenu extends JPopupMenu
|
||||
{
|
||||
public SongMenu(Song song, Queue queue)
|
||||
{
|
||||
final JMenuItem play = new JMenuItem(langs.getString("actions.play"));
|
||||
final JMenuItem remove = new JMenuItem(langs.getString("actions.remove"));
|
||||
|
||||
play.addActionListener(e -> queue.skipToSong(queue.indexOf(song)));
|
||||
remove.addActionListener(e -> queue.remove(song));
|
||||
|
||||
this.add(play);
|
||||
this.add(remove);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package edu.regis.universeplayer.gui;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.EventListener;
|
||||
|
||||
import edu.regis.universeplayer.data.CollectionType;
|
||||
import edu.regis.universeplayer.data.Song;
|
||||
|
||||
/**
|
||||
* A callback that is triggered whenever one element believes that the interface center display needs to be updated.
|
||||
*
|
||||
* @author William Hubbard
|
||||
* @version 0.1
|
||||
*/
|
||||
public interface SongDisplayListener extends EventListener
|
||||
{
|
||||
/**
|
||||
* Called to display a list of songs, sorted by album.
|
||||
*
|
||||
* @param songs - The songs to display.
|
||||
*/
|
||||
void updateSongs(Collection<? extends Song> songs);
|
||||
|
||||
/**
|
||||
* Called to display a list of collections
|
||||
*
|
||||
* @param type - The type of collections to display.
|
||||
* @param collections - The collections to display
|
||||
*/
|
||||
void updateCollections(CollectionType type, Collection<?> collections);
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
/*
|
||||
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package edu.regis.universeplayer.gui;
|
||||
|
||||
import com.wordpress.tips4java.ScrollablePanel;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import edu.regis.universeplayer.ClickListener;
|
||||
import edu.regis.universeplayer.data.Queue;
|
||||
import edu.regis.universeplayer.data.*;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* This panel will list all the songs that are to be currently displayed.
|
||||
*/
|
||||
public class SongList extends ScrollablePanel
|
||||
{
|
||||
private static final Logger logger = LoggerFactory
|
||||
.getLogger(SongList.class);
|
||||
private static final ResourceBundle langs = ResourceBundle
|
||||
.getBundle("lang.interface", Locale.getDefault());
|
||||
|
||||
private Map<Album, List<Song>> currentAlbums;
|
||||
private Map<JComponent, Song> labelMap = new HashMap<>();
|
||||
private Map<AlbumInfo, Album> artMap = new HashMap<>();
|
||||
|
||||
public SongList()
|
||||
{
|
||||
super();
|
||||
|
||||
GridBagLayout layout = new GridBagLayout();
|
||||
this.setFocusTraversalPolicyProvider(true);
|
||||
this.setLayout(layout);
|
||||
|
||||
SongProvider<?> provider = SongProvider.INSTANCE;
|
||||
this.listAlbums(provider.getSongs());
|
||||
|
||||
this.setScrollableWidth(ScrollableSizeHint.FIT);
|
||||
this.setScrollableHeight(ScrollableSizeHint.STRETCH);
|
||||
this.setFocusTraversalPolicy(new SongListPolicy());
|
||||
this.addFocusListener(new FocusAdapter()
|
||||
{
|
||||
@Override
|
||||
public void focusGained(FocusEvent e)
|
||||
{
|
||||
int index = -1;
|
||||
Component[] children = ((Container) e.getComponent())
|
||||
.getComponents();
|
||||
for (int i = 0, l = children.length; index == -1 && i < l; i++)
|
||||
{
|
||||
if (children[i] == e.getOppositeComponent())
|
||||
{
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
if (index == -1)
|
||||
{
|
||||
artMap.keySet().stream().findFirst()
|
||||
.ifPresent(albumInfo -> {
|
||||
albumInfo.requestFocusInWindow();
|
||||
scrollRectToVisible(albumInfo.getBounds());
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the songs currently listed, sorted by album.
|
||||
*
|
||||
* @param songs - The songs to display.
|
||||
*/
|
||||
public void listAlbums(Collection<? extends Song> songs)
|
||||
{
|
||||
logger.debug("Sorting {} songs...", songs.size());
|
||||
Map<Album, List<Song>> albums = songs.stream().sorted().collect(Collectors
|
||||
.groupingBy(song -> song.album, Collectors
|
||||
.mapping(song -> (Song) song, Collectors.toList())));
|
||||
logger.debug("Listing {} albums ({} songs)",
|
||||
albums.size(), songs.size());
|
||||
GridBagConstraints c = new GridBagConstraints();
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
AtomicInteger i = new AtomicInteger(0);
|
||||
|
||||
this.labelMap.clear();
|
||||
this.artMap.clear();
|
||||
this.removeAll();
|
||||
this.currentAlbums = albums;
|
||||
|
||||
albums.keySet().stream().sorted().forEach((album) -> {
|
||||
List<Song> songCollection = albums.get(album);
|
||||
|
||||
AlbumInfo albumInfo = new AlbumInfo(album);
|
||||
c.gridx = 0;
|
||||
c.gridy = i.get();
|
||||
c.gridwidth = 1;
|
||||
c.gridheight = songCollection.size();
|
||||
c.weightx = 0;
|
||||
c.anchor = GridBagConstraints.NORTHWEST;
|
||||
c.insets = new Insets(0, 0, 20, 10);
|
||||
List<Song> finalSongCollection = songCollection;
|
||||
albumInfo.addMouseListener((ClickListener) e -> {
|
||||
if (e.getClickCount() == 2)
|
||||
{
|
||||
Queue.getInstance().addAll(finalSongCollection);
|
||||
}
|
||||
});
|
||||
albumInfo.albumName.addMouseListener((ClickListener) e -> {
|
||||
if (e.getClickCount() == 2)
|
||||
{
|
||||
Container inter = this;
|
||||
do
|
||||
{
|
||||
inter = inter.getParent();
|
||||
}
|
||||
while (!(inter instanceof Interface) && inter
|
||||
.getParent() != null);
|
||||
if (inter instanceof Interface)
|
||||
{
|
||||
((Interface) inter).updateSongs(SongProvider.INSTANCE
|
||||
.getSongsFromAlbum(albumInfo.album));
|
||||
}
|
||||
}
|
||||
});
|
||||
albumInfo.artists.addMouseListener((ClickListener) e -> {
|
||||
if (e.getClickCount() == 2)
|
||||
{
|
||||
Container inter = this;
|
||||
do
|
||||
{
|
||||
inter = inter.getParent();
|
||||
}
|
||||
while (!(inter instanceof Interface) && inter
|
||||
.getParent() != null);
|
||||
if (inter instanceof Interface)
|
||||
{
|
||||
((Interface) inter)
|
||||
.updateCollections(CollectionType.album, Arrays
|
||||
.stream(albumInfo.album.artists)
|
||||
.flatMap(s -> SongProvider.INSTANCE
|
||||
.getAlbumsFromArtist(s)
|
||||
.stream())
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
});
|
||||
albumInfo.genres.addMouseListener((ClickListener) e -> {
|
||||
if (e.getClickCount() == 2)
|
||||
{
|
||||
Container inter = this;
|
||||
do
|
||||
{
|
||||
inter = inter.getParent();
|
||||
}
|
||||
while (!(inter instanceof Interface) && inter
|
||||
.getParent() != null);
|
||||
if (inter instanceof Interface)
|
||||
{
|
||||
((Interface) inter)
|
||||
.updateCollections(CollectionType.album, Arrays
|
||||
.stream(albumInfo.album.genres)
|
||||
.flatMap(s -> SongProvider.INSTANCE
|
||||
.getAlbumsFromGenre(s).stream())
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
});
|
||||
albumInfo.year.addMouseListener((ClickListener) e -> {
|
||||
if (e.getClickCount() == 2)
|
||||
{
|
||||
Container inter = this;
|
||||
do
|
||||
{
|
||||
inter = inter.getParent();
|
||||
}
|
||||
while (!(inter instanceof Interface) && inter
|
||||
.getParent() != null);
|
||||
if (inter instanceof Interface)
|
||||
{
|
||||
((Interface) inter)
|
||||
.updateCollections(CollectionType.album, SongProvider.INSTANCE
|
||||
.getAlbumsFromYear(albumInfo.album.year));
|
||||
}
|
||||
}
|
||||
});
|
||||
this.add(albumInfo, c);
|
||||
this.artMap.put(albumInfo, album);
|
||||
|
||||
JButton firstSong = null;
|
||||
|
||||
JLabel songNum;
|
||||
JButton songTitle;
|
||||
for (Song song : songCollection)
|
||||
{
|
||||
songNum = new JLabel(String.valueOf(song.trackNum));
|
||||
songNum.setFocusable(false);
|
||||
c.gridx = 1;
|
||||
c.gridy = i.get();
|
||||
c.gridheight = 1;
|
||||
c.weightx = 0;
|
||||
c.anchor = GridBagConstraints.NORTHEAST;
|
||||
c.insets = new Insets(0, 0, 0, 0);
|
||||
this.add(songNum, c);
|
||||
this.labelMap.put(songNum, song);
|
||||
|
||||
songTitle = new JButton(song.title);
|
||||
if (song.title == null || song.title.isEmpty())
|
||||
{
|
||||
if (song instanceof LocalSong)
|
||||
{
|
||||
songTitle.setText(((LocalSong) song).file.getName());
|
||||
}
|
||||
}
|
||||
songTitle.setHorizontalAlignment(JButton.LEFT);
|
||||
songTitle.setFocusPainted(true);
|
||||
songTitle.setMargin(new Insets(0, 0, 0, 0));
|
||||
songTitle.setContentAreaFilled(false);
|
||||
songTitle.setBorderPainted(false);
|
||||
songTitle.setOpaque(false);
|
||||
songTitle.addActionListener(new AbstractAction()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
Queue.getInstance().add(song);
|
||||
Queue.getInstance()
|
||||
.skipToSong(Queue.getInstance().size() - 1);
|
||||
}
|
||||
});
|
||||
c.gridx = 2;
|
||||
c.gridy = i.get();
|
||||
c.weightx = 1.0;
|
||||
c.anchor = GridBagConstraints.NORTHWEST;
|
||||
c.insets = new Insets(0, 10, 0, 0);
|
||||
this.add(songTitle, c);
|
||||
this.labelMap.put(songTitle, song);
|
||||
// TODO - Add song length or something
|
||||
|
||||
if (firstSong == null)
|
||||
{
|
||||
firstSong = songTitle;
|
||||
JButton finalFirstSong = firstSong;
|
||||
albumInfo.setAction(new AbstractAction()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
finalFirstSong.requestFocusInWindow();
|
||||
}
|
||||
});
|
||||
List<Song> finalSongCollection1 = songCollection;
|
||||
albumInfo.addKeyListener(new KeyAdapter()
|
||||
{
|
||||
@Override
|
||||
public void keyTyped(KeyEvent e)
|
||||
{
|
||||
if (e.getKeyCode() == KeyEvent.VK_ENTER)
|
||||
{
|
||||
Queue.getInstance()
|
||||
.addAll(finalSongCollection1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
i.getAndIncrement();
|
||||
}
|
||||
// this.add(new JLabel(new ImageIcon(this.getClass().getResource("/gui/icons/defaultart.png"), "Default")), c);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = i.getAndIncrement();
|
||||
c.gridwidth = 3;
|
||||
c.anchor = GridBagConstraints.NORTH;
|
||||
this.add(new JSeparator(SwingConstants.HORIZONTAL), c);
|
||||
|
||||
i.getAndIncrement();
|
||||
});
|
||||
logger.debug("Song list built");
|
||||
}
|
||||
|
||||
private class SongListPolicy extends FocusTraversalPolicy
|
||||
{
|
||||
@Override
|
||||
public Component getComponentAfter(Container aContainer, Component aComponent)
|
||||
{
|
||||
Component[] children = aContainer.getComponents();
|
||||
int index = -1;
|
||||
for (int i = 0, l = aContainer
|
||||
.getComponentCount(); index == -1 && i < l; i++)
|
||||
{
|
||||
if (children[i] == aComponent)
|
||||
{
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
if (index != -1)
|
||||
{
|
||||
if (aComponent instanceof AlbumInfo)
|
||||
{
|
||||
for (int i = index + 1, l = aContainer
|
||||
.getComponentCount(); i < l; i++)
|
||||
{
|
||||
if (children[i] instanceof AlbumInfo)
|
||||
{
|
||||
scrollRectToVisible(children[i].getBounds());
|
||||
return children[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (aComponent instanceof JButton)
|
||||
{
|
||||
for (int i = index + 1, l = aContainer
|
||||
.getComponentCount(); i < l; i++)
|
||||
{
|
||||
if (children[i].isFocusable())
|
||||
{
|
||||
scrollRectToVisible(children[i].getBounds());
|
||||
return children[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getComponentBefore(Container aContainer, Component aComponent)
|
||||
{
|
||||
Component[] children = aContainer.getComponents();
|
||||
int index = -1;
|
||||
for (int i = 0, l = aContainer
|
||||
.getComponentCount(); index == -1 && i < l; i++)
|
||||
{
|
||||
if (children[i] == aComponent)
|
||||
{
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
if (index != -1)
|
||||
{
|
||||
if (aComponent instanceof AlbumInfo)
|
||||
{
|
||||
for (int i = index - 1; i >= 0; i--)
|
||||
{
|
||||
if (children[i] instanceof AlbumInfo)
|
||||
{
|
||||
scrollRectToVisible(children[i].getBounds());
|
||||
return children[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (aComponent instanceof JButton)
|
||||
{
|
||||
for (int i = index - 1; i >= 0; i--)
|
||||
{
|
||||
if (children[i].isFocusable())
|
||||
{
|
||||
scrollRectToVisible(children[i].getBounds());
|
||||
return children[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getFirstComponent(Container aContainer)
|
||||
{
|
||||
Component comp = Arrays.stream(aContainer.getComponents())
|
||||
.filter(a -> a instanceof AlbumInfo)
|
||||
.findFirst().orElse(null);
|
||||
if (comp != null)
|
||||
{
|
||||
scrollRectToVisible(comp.getBounds());
|
||||
}
|
||||
return comp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getLastComponent(Container aContainer)
|
||||
{
|
||||
Component[] matching = Arrays.stream(aContainer.getComponents())
|
||||
.filter(a -> a instanceof JButton)
|
||||
.toArray(Component[]::new);
|
||||
if (matching.length > 0)
|
||||
{
|
||||
scrollRectToVisible(matching[matching.length - 1].getBounds());
|
||||
return matching[matching.length - 1];
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getDefaultComponent(Container aContainer)
|
||||
{
|
||||
return this.getFirstComponent(aContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user