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:
Markil3
2021-09-14 16:39:40 -06:00
parent 92ebb75e9d
commit 999f3345cc
24 changed files with 791 additions and 519 deletions

View File

@@ -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));
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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()
{
}
}

View File

@@ -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)
{
}
}

View File

@@ -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);
}
}
}
});
}
}

View File

@@ -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);
}
}

View File

@@ -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
}

View File

@@ -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);
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}