Added a queue system.

This commit is contained in:
Markil3
2021-08-03 13:11:36 -06:00
parent 8386c9575c
commit 52262b5668
12 changed files with 1912 additions and 352 deletions

View File

@@ -0,0 +1,351 @@
/*
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
*/
package com.wordpress.tips4java;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import java.awt.*;
/**
* A JPanel containing default options for scrolling.
*
* @author Rob Camick, https://tips4java.wordpress.com/2009/12/20/scrollable-panel/
*/
public class ScrollablePanel extends JPanel implements Scrollable, SwingConstants
{
public enum ScrollableSizeHint
{
NONE,
FIT,
STRETCH;
}
public enum IncrementType
{
PERCENT,
PIXELS;
}
private ScrollableSizeHint scrollableHeight = ScrollableSizeHint.NONE;
private ScrollableSizeHint scrollableWidth = ScrollableSizeHint.NONE;
private IncrementInfo horizontalBlock;
private IncrementInfo horizontalUnit;
private IncrementInfo verticalBlock;
private IncrementInfo verticalUnit;
/**
* Default constructor that uses a FlowLayout
*/
public ScrollablePanel()
{
this(new FlowLayout());
}
/**
* Constuctor for specifying the LayoutManager of the panel.
*
* @param layout the LayountManger for the panel
*/
public ScrollablePanel(LayoutManager layout)
{
super(layout);
IncrementInfo block = new IncrementInfo(IncrementType.PERCENT, 100);
IncrementInfo unit = new IncrementInfo(IncrementType.PERCENT, 10);
setScrollableBlockIncrement(HORIZONTAL, block);
setScrollableBlockIncrement(VERTICAL, block);
setScrollableUnitIncrement(HORIZONTAL, unit);
setScrollableUnitIncrement(VERTICAL, unit);
}
/**
* Get the height ScrollableSizeHint enum
*
* @return the ScrollableSizeHint enum for the height
*/
public ScrollableSizeHint getScrollableHeight()
{
return scrollableHeight;
}
/**
* Set the ScrollableSizeHint enum for the height. The enum is used to
* determine the boolean value that is returned by the
* getScrollableTracksViewportHeight() method. The valid values are:
* <p>
* ScrollableSizeHint.NONE - return "false", which causes the height
* of the panel to be used when laying out the children
* ScrollableSizeHint.FIT - return "true", which causes the height of
* the viewport to be used when laying out the children
* ScrollableSizeHint.STRETCH - return "true" when the viewport height
* is greater than the height of the panel, "false" otherwise.
*
* @param scrollableHeight as represented by the ScrollableSizeHint enum.
*/
public void setScrollableHeight(ScrollableSizeHint scrollableHeight)
{
this.scrollableHeight = scrollableHeight;
revalidate();
}
/**
* Get the width ScrollableSizeHint enum
*
* @return the ScrollableSizeHint enum for the width
*/
public ScrollableSizeHint getScrollableWidth()
{
return scrollableWidth;
}
/**
* Set the ScrollableSizeHint enum for the width. The enum is used to
* determine the boolean value that is returned by the
* getScrollableTracksViewportWidth() method. The valid values are:
* <p>
* ScrollableSizeHint.NONE - return "false", which causes the width
* of the panel to be used when laying out the children
* ScrollableSizeHint.FIT - return "true", which causes the width of
* the viewport to be used when laying out the children
* ScrollableSizeHint.STRETCH - return "true" when the viewport width
* is greater than the width of the panel, "false" otherwise.
*
* @param scrollableWidth as represented by the ScrollableSizeHint enum.
*/
public void setScrollableWidth(ScrollableSizeHint scrollableWidth)
{
this.scrollableWidth = scrollableWidth;
revalidate();
}
/**
* Get the block IncrementInfo for the specified orientation
*
* @return the block IncrementInfo for the specified orientation
*/
public IncrementInfo getScrollableBlockIncrement(int orientation)
{
return orientation == SwingConstants.HORIZONTAL ? horizontalBlock : verticalBlock;
}
/**
* Specify the information needed to do block scrolling.
*
* @param orientation specify the scrolling orientation. Must be either:
* SwingContants.HORIZONTAL or SwingContants.VERTICAL.
* @param amount a value used with the IncrementType to determine the
* scrollable amount
* @paran type specify how the amount parameter in the calculation of
* the scrollable amount. Valid values are:
* IncrementType.PERCENT - treat the amount as a % of the viewport size
* IncrementType.PIXEL - treat the amount as the scrollable amount
*/
public void setScrollableBlockIncrement(int orientation, IncrementType type, int amount)
{
IncrementInfo info = new IncrementInfo(type, amount);
setScrollableBlockIncrement(orientation, info);
}
/**
* Specify the information needed to do block scrolling.
*
* @param orientation specify the scrolling orientation. Must be either:
* SwingContants.HORIZONTAL or SwingContants.VERTICAL.
* @param info An IncrementInfo object containing information of how to
* calculate the scrollable amount.
*/
public void setScrollableBlockIncrement(int orientation, IncrementInfo info)
{
switch (orientation)
{
case SwingConstants.HORIZONTAL:
horizontalBlock = info;
break;
case SwingConstants.VERTICAL:
verticalBlock = info;
break;
default:
throw new IllegalArgumentException("Invalid orientation: " + orientation);
}
}
/**
* Get the unit IncrementInfo for the specified orientation
*
* @return the unit IncrementInfo for the specified orientation
*/
public IncrementInfo getScrollableUnitIncrement(int orientation)
{
return orientation == SwingConstants.HORIZONTAL ? horizontalUnit : verticalUnit;
}
/**
* Specify the information needed to do unit scrolling.
*
* @param orientation specify the scrolling orientation. Must be either:
* SwingContants.HORIZONTAL or SwingContants.VERTICAL.
* @param amount a value used with the IncrementType to determine the
* scrollable amount
* @paran type specify how the amount parameter in the calculation of
* the scrollable amount. Valid values are:
* IncrementType.PERCENT - treat the amount as a % of the viewport size
* IncrementType.PIXEL - treat the amount as the scrollable amount
*/
public void setScrollableUnitIncrement(int orientation, IncrementType type, int amount)
{
IncrementInfo info = new IncrementInfo(type, amount);
setScrollableUnitIncrement(orientation, info);
}
/**
* Specify the information needed to do unit scrolling.
*
* @param orientation specify the scrolling orientation. Must be either:
* SwingContants.HORIZONTAL or SwingContants.VERTICAL.
* @param info An IncrementInfo object containing information of how to
* calculate the scrollable amount.
*/
public void setScrollableUnitIncrement(int orientation, IncrementInfo info)
{
switch (orientation)
{
case SwingConstants.HORIZONTAL:
horizontalUnit = info;
break;
case SwingConstants.VERTICAL:
verticalUnit = info;
break;
default:
throw new IllegalArgumentException("Invalid orientation: " + orientation);
}
}
// Implement Scrollable interface
public Dimension getPreferredScrollableViewportSize()
{
return getPreferredSize();
}
public int getScrollableUnitIncrement(
Rectangle visible, int orientation, int direction)
{
switch (orientation)
{
case SwingConstants.HORIZONTAL:
return getScrollableIncrement(horizontalUnit, visible.width);
case SwingConstants.VERTICAL:
return getScrollableIncrement(verticalUnit, visible.height);
default:
throw new IllegalArgumentException("Invalid orientation: " + orientation);
}
}
public int getScrollableBlockIncrement(
Rectangle visible, int orientation, int direction)
{
switch (orientation)
{
case SwingConstants.HORIZONTAL:
return getScrollableIncrement(horizontalBlock, visible.width);
case SwingConstants.VERTICAL:
return getScrollableIncrement(verticalBlock, visible.height);
default:
throw new IllegalArgumentException("Invalid orientation: " + orientation);
}
}
protected int getScrollableIncrement(IncrementInfo info, int distance)
{
if (info.getIncrement() == IncrementType.PIXELS)
{
return info.getAmount();
}
else
{
return distance * info.getAmount() / 100;
}
}
public boolean getScrollableTracksViewportWidth()
{
if (scrollableWidth == ScrollableSizeHint.NONE)
{
return false;
}
if (scrollableWidth == ScrollableSizeHint.FIT)
{
return true;
}
// STRETCH sizing, use the greater of the panel or viewport width
if (getParent() instanceof JViewport)
{
return (((JViewport) getParent()).getWidth() > getPreferredSize().width);
}
return false;
}
public boolean getScrollableTracksViewportHeight()
{
if (scrollableHeight == ScrollableSizeHint.NONE)
{
return false;
}
if (scrollableHeight == ScrollableSizeHint.FIT)
{
return true;
}
// STRETCH sizing, use the greater of the panel or viewport height
if (getParent() instanceof JViewport)
{
return (((JViewport) getParent()).getHeight() > getPreferredSize().height);
}
return false;
}
/**
* Helper class to hold the information required to calculate the scroll amount.
*/
static class IncrementInfo
{
private IncrementType type;
private int amount;
public IncrementInfo(IncrementType type, int amount)
{
this.type = type;
this.amount = amount;
}
public IncrementType getIncrement()
{
return type;
}
public int getAmount()
{
return amount;
}
public String toString()
{
return
"com.ScrollablePanel[" +
type + ", " +
amount + "]";
}
}
}

View File

@@ -442,9 +442,11 @@ public class LocalSongProvider implements SongProvider<LocalSong>
if (getFormats().contains(type))
{
try
{
synchronized (db)
{
state = getDb().createStatement();
result = state.executeQuery("SELECT mod FROM local_songs WHERE file='" + this.file.getAbsolutePath() + "';");
result = state.executeQuery("SELECT mod FROM local_songs WHERE file='" + this.file.getAbsolutePath().replaceAll("'", "''") + "';");
if (result.next())
{
if (result.getLong(1) >= this.file.lastModified())
@@ -458,7 +460,8 @@ public class LocalSongProvider implements SongProvider<LocalSong>
}
else
{
state.executeUpdate("UPDATE local_songs SET mod = " + this.file.lastModified() + " WHERE file='" + this.file.getAbsolutePath() + "';");
state.executeUpdate("UPDATE local_songs SET mod = " + this.file.lastModified() + " WHERE file='" + this.file.getAbsolutePath().replaceAll("'", "''") + "';");
}
}
}
currentFolder = file.getPath();
@@ -609,6 +612,11 @@ public class LocalSongProvider implements SongProvider<LocalSong>
/*
* Update album information.
*/
synchronized (db)
{
/*
* This part in particular is prone to thread-safety issues.
*/
result = state.executeQuery("SELECT album FROM local_albums WHERE album='" + albumTitle + "';");
if (!result.next())
{
@@ -639,7 +647,7 @@ public class LocalSongProvider implements SongProvider<LocalSong>
/*
* Create the song
*/
result = state.executeQuery("SELECT title FROM local_songs WHERE file='" + file.getAbsolutePath() + "';");
result = state.executeQuery("SELECT title FROM local_songs WHERE file='" + file.getAbsolutePath().replaceAll("'", "''") + "';");
if (result.next())
{
logger.debug("Updating song cache for {} ({})", title, file);
@@ -695,7 +703,7 @@ public class LocalSongProvider implements SongProvider<LocalSong>
sql.append("album=NULL, ");
}
sql.append("mod=").append(file.lastModified());
sql.append(" WHERE file='").append(file.getAbsolutePath()).append("';");
sql.append(" WHERE file='").append(file.getAbsolutePath().replaceAll("'", "''")).append("';");
state.executeUpdate(sql.toString());
}
else
@@ -706,7 +714,7 @@ public class LocalSongProvider implements SongProvider<LocalSong>
StringBuilder values = new StringBuilder("(");
columns.append("file,");
values.append('\'').append(file.getAbsolutePath()).append("',");
values.append('\'').append(file.getAbsolutePath().replaceAll("'", "''")).append("',");
columns.append("codec,");
values.append('\'').append(codec).append("',");
columns.append("type,");
@@ -751,6 +759,7 @@ public class LocalSongProvider implements SongProvider<LocalSong>
sql.append(values);
state.executeUpdate(sql.toString());
}
}
updatedSongs++;
triggerUpdateListeners();
@@ -828,6 +837,8 @@ public class LocalSongProvider implements SongProvider<LocalSong>
/*
* Check if the table exists
*/
synchronized (db)
{
state = getDb().createStatement();
result = state.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='LOCAL_ALBUMS';");
if (!result.next())
@@ -906,6 +917,7 @@ public class LocalSongProvider implements SongProvider<LocalSong>
}
state.close();
}
}
catch (SQLException e)
{
logger.error("Could not query SQL database.", e);

View File

@@ -0,0 +1,469 @@
/*
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
*/
package edu.regis.universeplayer.data;
import java.util.*;
/**
* A song queue contains a list of songs that are to play, along with a way to
* order them
*
* @author William Hubbard
* @version 0.1
*/
public class Queue extends ArrayList<Song>
{
private static Queue INSTANCE;
public static Queue getInstance()
{
if (INSTANCE == null)
{
INSTANCE = new Queue();
}
return INSTANCE;
}
private final ArrayList<Song> queueOrder = new ArrayList<>();
private int currentIndex;
private boolean shuffle;
private boolean repeat;
private final Random random = new Random();
private final LinkedList<SongChangeListener> songListeners = new LinkedList<>();
private final LinkedList<QueueChangeListener> queueListeners = new LinkedList<>();
public Queue()
{
}
/**
* Obtains the song scheduled to play.
*
* @return The currently playing song.
*/
public Song getCurrentSong()
{
if (this.currentIndex < 0 || this.currentIndex >= this.queueOrder.size())
{
return null;
}
return this.queueOrder.get(currentIndex);
}
/**
* Obtains the song index scheduled to play.
*
* @return The currently playing song index.
*/
public int getCurrentIndex()
{
return this.indexOf(this.getCurrentSong());
}
/**
* Checks whether this queue will loop after the end of the song.
*
* @return True if the queue loops, false otherwise.
*/
public boolean isRepeating()
{
return this.repeat;
}
/**
* Sets whether this queue should loop after the last song plays.
*
* @param repeat - Whether the queue loops.
*/
public void setRepeat(boolean repeat)
{
this.repeat = repeat;
}
/**
* Checks whether this queue is in shuffle mode.
*
* @return True if the queue shuffles, false otherwise.
*/
public boolean isShuffling()
{
return this.shuffle;
}
/**
* Sets whether this queue should be shuffled or not.
*
* @param shuffle - Whether or not the queue shuffles.
*/
public void setShuffle(boolean shuffle)
{
this.shuffle = shuffle;
this.queueOrder.clear();
}
/**
* Skips to the next song.
*
* @return The new song, or null if we finished the list.
*/
public Song skipNext()
{
if (++this.currentIndex >= this.queueOrder.size())
{
if (this.repeat)
{
this.currentIndex = 0;
/*
* Reshuffle the queue as needed.
*/
this.queueOrder.clear();
this.getOrder();
this.triggerSongChangeListeners();
return this.getCurrentSong();
}
else
{
/*
* Make sure we aren't infinitely increasing the current index.
*/
this.currentIndex = this.queueOrder.size() - 1;
this.triggerSongChangeListeners();
return null;
}
}
this.triggerSongChangeListeners();
return this.getCurrentSong();
}
/**
* Skips to the previous song.
*
* @return The previous song, or the first one if we go underboard.
*/
public Song skipPrev()
{
if (--this.currentIndex < 0)
{
if (this.repeat)
{
this.currentIndex = this.queueOrder.size() - 1;
}
else
{
this.currentIndex = 0;
}
}
this.triggerSongChangeListeners();
return this.getCurrentSong();
}
/**
* Skips to the song at the provided song index.
*
* @param index - The new index. This is not relational to the queue.
* @return The new song, or null if we finished the list.
*/
public Song skipToSong(int index)
{
Song song = this.get(index);
this.currentIndex = this.queueOrder.indexOf(song);
this.triggerSongChangeListeners();
return this.getCurrentSong();
}
private ArrayList<Song> getOrder()
{
LinkedList<Song> indexesRemaining;
if (this.queueOrder.isEmpty())
{
this.queueOrder.addAll(this);
if (this.shuffle)
{
indexesRemaining = new LinkedList<>(this.queueOrder);
this.queueOrder.clear();
while (indexesRemaining.size() > 0)
{
this.queueOrder.add(indexesRemaining.remove(random.nextInt(indexesRemaining.size())));
}
}
}
return this.queueOrder;
}
@Override
public boolean add(Song song)
{
int newIndex;
if (super.add(song))
{
if (this.shuffle)
{
newIndex = random.nextInt(this.queueOrder.size());
if (newIndex <= this.currentIndex)
{
this.currentIndex++;
}
this.queueOrder.add(newIndex, song);
}
else
{
this.queueOrder.add(song);
}
this.triggerQueueChangeListeners();
return true;
}
return false;
}
@Override
public void add(int index, Song song)
{
int newIndex;
super.add(index, song);
if (this.shuffle)
{
newIndex = this.random.nextInt(this.queueOrder.size());
}
else
{
newIndex = index;
}
if (newIndex <= this.currentIndex)
{
this.currentIndex++;
}
this.queueOrder.add(newIndex, song);
this.triggerQueueChangeListeners();
}
@Override
public Song remove(int index)
{
Song removed = super.remove(index);
if (removed != null)
{
index = this.queueOrder.indexOf(removed);
if (index <= this.currentIndex)
{
this.currentIndex--;
}
this.queueOrder.remove(removed);
this.triggerQueueChangeListeners();
}
return removed;
}
@Override
public boolean remove(Object o)
{
int index = this.indexOf(o);
if (super.remove(o))
{
if (index <= this.currentIndex)
{
this.currentIndex--;
}
this.queueOrder.remove(o);
this.triggerQueueChangeListeners();
return true;
}
return false;
}
@Override
public void clear()
{
super.clear();
this.queueOrder.clear();
this.currentIndex = 0;
this.triggerQueueChangeListeners();
this.triggerSongChangeListeners();
}
@Override
public boolean addAll(Collection<? extends Song> c)
{
int newIndex;
int length = this.size();
if (super.addAll(c))
{
for (Song song : c)
{
if (this.shuffle)
{
newIndex = random.nextInt(this.queueOrder.size());
if (newIndex <= this.currentIndex)
{
this.currentIndex++;
}
this.queueOrder.add(newIndex, song);
}
else
{
this.queueOrder.add(song);
}
}
this.triggerQueueChangeListeners();
return true;
}
return false;
}
@Override
public boolean addAll(int index, Collection<? extends Song> c)
{
int newIndex;
int i = 0;
if (super.addAll(index, c))
{
for (Song song : c)
{
if (this.shuffle)
{
newIndex = random.nextInt(this.queueOrder.size());
}
else
{
newIndex = index + i++;
}
if (newIndex <= this.currentIndex)
{
this.currentIndex++;
}
this.queueOrder.add(newIndex, song);
}
this.triggerQueueChangeListeners();
return true;
}
return false;
}
@Override
protected void removeRange(int fromIndex, int toIndex)
{
for (int i = fromIndex; i < toIndex; i++)
{
this.queueOrder.remove(this.get(i));
if (i <= this.currentIndex)
{
this.currentIndex--;
}
}
super.removeRange(fromIndex, toIndex);
this.triggerQueueChangeListeners();
}
@Override
public boolean removeAll(Collection<?> c)
{
boolean success;
for (int i = 0, removed = 0; i <= this.currentIndex && i < this.size(); i++)
{
if (c.contains(this.get(i)))
{
this.currentIndex--;
}
}
this.queueOrder.removeAll(c);
success = super.removeAll(c);
this.triggerQueueChangeListeners();
return success;
}
@Override
public boolean retainAll(Collection<?> c)
{
boolean success;
for (int i = 0, removed = 0; i <= this.currentIndex && i < this.size(); i++)
{
if (!c.contains(this.get(i)))
{
this.currentIndex--;
}
}
this.queueOrder.retainAll(c);
success = super.retainAll(c);
this.triggerQueueChangeListeners();
return success;
}
/**
* Adds a listener for when the current song changes.
*
* @param listener - The listener to add.
*/
public void addSongChangeListener(SongChangeListener listener)
{
this.songListeners.add(listener);
}
/**
* Removes a listener for when the current song changes.
*
* @param listener - The listener to remove.
*/
public void removeSongChangeListener(SongChangeListener listener)
{
this.songListeners.remove(listener);
}
/**
* Tells all listeners that the song changed.
*/
protected void triggerSongChangeListeners()
{
this.songListeners.forEach(listener -> listener.onSongChange(this));
}
/**
* Adds a listener for when the queue contents changes.
*
* @param listener - The listener to add.
*/
public void addQueueChangeListener(QueueChangeListener listener)
{
this.queueListeners.add(listener);
}
/**
* Removes a listener for when the queue contents changes.
*
* @param listener - The listener to remove.
*/
public void removeQueueChangeListener(QueueChangeListener listener)
{
this.queueListeners.remove(listener);
}
/**
* Tells all listeners that the queue changed.
*/
protected void triggerQueueChangeListeners()
{
this.queueListeners.forEach(listener -> listener.onQueueChange(this));
}
public interface SongChangeListener extends EventListener
{
/**
* Called when the song of a queue changes.
*
* @param queue - The queue whose song changed.
*/
void onSongChange(Queue queue);
}
public interface QueueChangeListener extends EventListener
{
/**
* Called when the queue contents change.
*
* @param queue - The queue that changed.
*/
void onQueueChange(Queue queue);
}
}

View File

@@ -4,29 +4,37 @@
package edu.regis.universeplayer.player;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SpringLayout;
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;
/**
* This panel will display information on an album.
*/
public class AlbumInfo extends JPanel
public class AlbumInfo extends JButton
{
private Album album;
private JLabel artLabel;
private JLabel albumName;
private JLabel artists;
private JLabel genres;
private JLabel year;
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);
@@ -52,11 +60,38 @@ public class AlbumInfo extends JPanel
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);
// infoLayout.putConstraint(SpringLayout.EAST, this, 0, SpringLayout.EAST, albumName);
infoLayout.putConstraint(SpringLayout.EAST, this, 0, SpringLayout.EAST, artists);
// infoLayout.putConstraint(SpringLayout.EAST, this, 0, SpringLayout.EAST, genres);
// infoLayout.putConstraint(SpringLayout.EAST, this, 0, SpringLayout.EAST, year);
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)

View File

@@ -4,14 +4,16 @@
package edu.regis.universeplayer.player;
import java.awt.FlowLayout;
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.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.*;
import com.wordpress.tips4java.ScrollablePanel;
import edu.regis.universeplayer.ClickListener;
import edu.regis.universeplayer.data.Album;
import edu.regis.universeplayer.data.CollectionType;
@@ -24,7 +26,7 @@ import edu.regis.universeplayer.data.SongProvider;
* @author William Hubbard
* @version 0.1
*/
public class CollectionList extends JPanel
public class CollectionList extends ScrollablePanel
{
/**
* The type of collections being displayed.
@@ -33,7 +35,7 @@ public class CollectionList extends JPanel
/**
* A link between the JLabel and the object they point towards.
*/
private Map<JLabel, Object> labelMap = new HashMap<>();
private Map<JButton, Object> labelMap = new HashMap<>();
/**
* A list of all things interested in knowing when we click a collection.
@@ -49,6 +51,16 @@ public class CollectionList extends JPanel
FlowLayout layout = new FlowLayout();
this.setLayout(layout);
this.setFocusCycleRoot(true);
// this.setFocusable(true);
this.addFocusListener(new FocusAdapter()
{
@Override
public void focusGained(FocusEvent e)
{
labelMap.keySet().stream().findFirst().ifPresent(JButton::requestFocusInWindow);
}
});
}
/**
@@ -105,12 +117,13 @@ public class CollectionList extends JPanel
{
final int ART_SIZE = 128;
JLabel artistLabel;
JButton artistLabel;
ImageIcon icon;
for (String artist : artists)
{
artistLabel = new JLabel();
artistLabel = new JButton();
setButtonLook(artistLabel);
// TODO - Maybe add some sort of artist image lookup?
// if (album.art != null)
// {
@@ -126,7 +139,7 @@ public class CollectionList extends JPanel
artistLabel.setText(artist);
artistLabel.setHorizontalTextPosition(JLabel.CENTER);
artistLabel.setVerticalTextPosition(JLabel.BOTTOM);
artistLabel.addMouseListener((ClickListener) mouseEvent -> {
artistLabel.addActionListener(mouseEvent -> {
if (album)
{
this.triggerSongDisplayListeners(SongProvider.INSTANCE
@@ -155,12 +168,13 @@ public class CollectionList extends JPanel
{
final int ART_SIZE = 128;
JLabel albumLabel;
JButton albumLabel;
ImageIcon icon;
for (Album album : albums)
{
albumLabel = new JLabel();
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));
@@ -168,7 +182,7 @@ public class CollectionList extends JPanel
albumLabel.setText(album.name);
albumLabel.setHorizontalTextPosition(JLabel.CENTER);
albumLabel.setVerticalTextPosition(JLabel.BOTTOM);
albumLabel.addMouseListener((ClickListener) mouseEvent -> this.triggerSongDisplayListeners(new ArrayList<>(SongProvider.INSTANCE
albumLabel.addActionListener(mouseEvent -> this.triggerSongDisplayListeners(new ArrayList<>(SongProvider.INSTANCE
.getSongsFromAlbum(album))));
this.add(albumLabel);
this.labelMap.put(albumLabel, album);
@@ -184,12 +198,13 @@ public class CollectionList extends JPanel
{
// final int ART_SIZE = 128;
JLabel genreLabel;
JButton genreLabel;
// ImageIcon icon;
for (String genre : genres)
{
genreLabel = new JLabel();
genreLabel = new JButton();
setButtonLook(genreLabel);
// TODO - Maybe add some sort of artist image lookup?
// if (album.art != null)
// {
@@ -205,7 +220,7 @@ public class CollectionList extends JPanel
genreLabel.setText(genre);
genreLabel.setHorizontalTextPosition(JLabel.CENTER);
genreLabel.setVerticalTextPosition(JLabel.BOTTOM);
genreLabel.addMouseListener((ClickListener) mouseEvent -> this.triggerSongDisplayListeners(SongProvider.INSTANCE
genreLabel.addActionListener(mouseEvent -> this.triggerSongDisplayListeners(SongProvider.INSTANCE
.getAlbumsFromGenre(genre).stream()
.flatMap(album2 -> SongProvider.INSTANCE.getSongsFromAlbum(album2)
.stream())
@@ -224,12 +239,13 @@ public class CollectionList extends JPanel
{
// final int ART_SIZE = 128;
JLabel yearLabel;
JButton yearLabel;
// ImageIcon icon;
for (Integer year : years)
{
yearLabel = new JLabel();
yearLabel = new JButton();
setButtonLook(yearLabel);
// TODO - Maybe add some sort of artist image lookup?
// if (album.art != null)
// {
@@ -245,7 +261,7 @@ public class CollectionList extends JPanel
yearLabel.setText(year.toString());
yearLabel.setHorizontalTextPosition(JLabel.CENTER);
yearLabel.setVerticalTextPosition(JLabel.BOTTOM);
yearLabel.addMouseListener((ClickListener) mouseEvent -> this.triggerSongDisplayListeners(SongProvider.INSTANCE
yearLabel.addActionListener(mouseEvent -> this.triggerSongDisplayListeners(SongProvider.INSTANCE
.getAlbumsFromYear(year).stream()
.flatMap(album2 -> SongProvider.INSTANCE.getSongsFromAlbum(album2)
.stream())
@@ -255,6 +271,15 @@ public class CollectionList extends JPanel
}
}
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.
*

View File

@@ -4,14 +4,15 @@
package edu.regis.universeplayer.player;
import java.awt.Color;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import javax.swing.BoxLayout;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.*;
import javax.swing.border.LineBorder;
import javax.swing.plaf.LabelUI;
import edu.regis.universeplayer.ClickListener;
import edu.regis.universeplayer.data.CollectionType;
@@ -36,37 +37,85 @@ public class Collections extends JPanel
*/
public Collections()
{
JLabel label;
JButton defaultLabel;
JButton label;
BoxLayout layout = new BoxLayout(this, BoxLayout.Y_AXIS);
this.setLayout(layout);
this.setFocusable(true);
this.setFocusCycleRoot(true);
this.add(label = new JLabel("All Songs"));
label.setForeground(Color.BLUE);
label.addMouseListener((ClickListener) mouseEvent -> this
this.add(defaultLabel = label = this.createButton("All Songs"));
label.setMnemonic('A');
label.addActionListener(mouseEvent -> this
.triggerSongDisplayListeners(new ArrayList<>(SongProvider.INSTANCE.getSongs())));
this.add(label = new JLabel("Artists"));
label.setForeground(Color.BLUE);
label.addMouseListener((ClickListener) mouseEvent -> this
this.add(label = this.createButton("Artists"));
label.setMnemonic('T');
label.addActionListener(mouseEvent -> this
.triggerCollectionDisplayListeners(CollectionType.albumArtist, SongProvider.INSTANCE
.getAlbumArtists()));
this.add(label = new JLabel("Albums"));
label.setForeground(Color.BLUE);
label.addMouseListener((ClickListener) mouseEvent -> this
this.add(label = this.createButton("Albums"));
label.setMnemonic('B');
label.addActionListener(mouseEvent -> this
.triggerCollectionDisplayListeners(CollectionType.album, SongProvider.INSTANCE
.getAlbums()));
this.add(label = new JLabel("Genres"));
label.setForeground(Color.BLUE);
label.addMouseListener((ClickListener) mouseEvent -> this
this.add(label = this.createButton("Genres"));
label.setMnemonic('G');
label.addActionListener(mouseEvent -> this
.triggerCollectionDisplayListeners(CollectionType.genre, SongProvider.INSTANCE
.getGenres()));
this.add(label = new JLabel("Years"));
label.setForeground(Color.BLUE);
label.addMouseListener((ClickListener) mouseEvent -> this
this.add(label = this.createButton("Years"));
label.setMnemonic('Y');
label.addActionListener(mouseEvent -> this
.triggerCollectionDisplayListeners(CollectionType.year, SongProvider.INSTANCE
.getYears()));
this.add(new JLabel("\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF"));
this.add(label = new JLabel("Playlists"));
this.add(new JLabel("\u23AF".repeat(6)));
this.add(label = this.createButton("Playlists"));
label.setMnemonic('P');
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 JButton createButton(String text)
{
JButton label = new JButton(text);
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;
}
/**

View File

@@ -0,0 +1,14 @@
/*
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
*/
package edu.regis.universeplayer.player;
import java.awt.*;
public class FocusManager extends DefaultKeyboardFocusManager
{
public FocusManager()
{
}
}

View File

@@ -6,25 +6,21 @@ package edu.regis.universeplayer.player;
import edu.regis.universeplayer.Player;
import edu.regis.universeplayer.browser.Browser;
import edu.regis.universeplayer.data.CollectionType;
import edu.regis.universeplayer.data.Song;
import edu.regis.universeplayer.data.SongProvider;
import edu.regis.universeplayer.data.UpdateListener;
import edu.regis.universeplayer.data.Queue;
import edu.regis.universeplayer.data.*;
import net.harawata.appdirs.AppDirsFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.awt.event.*;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Set;
import java.util.concurrent.Future;
/**
@@ -33,7 +29,7 @@ import java.util.concurrent.Future;
* @author William Hubbard
* @version 0.1
*/
public class Interface extends JFrame implements SongDisplayListener, ComponentListener, WindowListener, PlaybackCommandListener, UpdateListener
public class Interface extends JFrame implements SongDisplayListener, ComponentListener, WindowListener, PlaybackCommandListener, UpdateListener, FocusListener
{
private static final Logger logger = LoggerFactory.getLogger(Interface.class);
@@ -44,6 +40,10 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
* 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.
*/
@@ -81,9 +81,11 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
*/
logger.info("Starting application");
inter = new Interface();
inter.pack();
inter.setSize(700, 500);
SongProvider.INSTANCE.addUpdateListener(inter);
inter.setVisible(true);
try
{
inter.players.add(browser = Browser.createBrowser());
@@ -118,8 +120,6 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
logger.error("Could not open browser background", e);
JOptionPane.showMessageDialog(inter, e, "Could not open browser background", JOptionPane.ERROR_MESSAGE);
}
SongProvider.INSTANCE.addUpdateListener(inter);
}
catch (Throwable e)
{
@@ -179,14 +179,27 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
this.setTitle("Universal Music Player");
this.getContentPane().setLayout(new BorderLayout());
this.setFocusable(true);
this.setFocusCycleRoot(true);
this.getContentPane()
.add(this.collectionTypes = new Collections(), BorderLayout.LINE_START);
this.collectionTypes.addFocusListener(this);
this.collectionTypes.addSongDisplayListener(this);
this.controls = new PlayerControls();
this.controls.addFocusListener(this);
controls.addCommandListener(this);
this.getContentPane().add(controls, BorderLayout.PAGE_END);
this.queueList = new QueueList();
this.queueList.addFocusListener(this);
Queue.getInstance().addQueueChangeListener(this.queueList);
Queue.getInstance().addSongChangeListener(this.queueList);
this.getContentPane().add(queueList, BorderLayout.LINE_END);
this.songList = new SongList();
this.songList.addFocusListener(this);
this.collectionList = new CollectionList();
this.collectionList.addSongDisplayListener(this);
@@ -196,7 +209,13 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
this.addComponentListener(this);
this.addWindowListener(this);
this.addFocusListener(this);
((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)));
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.updateSongs(SongProvider.INSTANCE.getSongs());
@@ -207,11 +226,12 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
{
this.songList.listAlbums(songs);
this.centerView.setViewportView(this.songList);
this.songList.setPreferredSize(new Dimension(this.centerView.getViewport()
.getExtentSize().width, Integer.MAX_VALUE));
// this.songList.setPreferredSize(new Dimension(this.centerView.getViewport()
// .getExtentSize().width, Integer.MAX_VALUE));
this.songList.setPreferredSize(new Dimension(this.centerView.getViewport()
.getExtentSize().width, this.songList
.getMinimumSize().height));
this.centerView.validate();
}
@Override
@@ -219,11 +239,12 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
{
this.collectionList.listCollection(type, collections);
this.centerView.setViewportView(this.collectionList);
this.collectionList.setPreferredSize(new Dimension(this.centerView.getViewport()
.getExtentSize().width, Integer.MAX_VALUE));
// this.collectionList.setPreferredSize(new Dimension(this.centerView.getViewport()
// .getExtentSize().width, Integer.MAX_VALUE));
this.collectionList.setPreferredSize(new Dimension(this.centerView.getViewport()
.getExtentSize().width, this.collectionList
.getMinimumSize().height));
this.centerView.validate();
}
@Override
@@ -309,15 +330,11 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
@Override
public void onCommand(PlaybackCommand command, Object data)
{
Player player;
Player player = null;
if (this.currentPlayer >= 0 && this.currentPlayer < this.players.size())
{
player = this.players.get(this.currentPlayer);
}
else
{
throw new NullPointerException("No player available");
}
switch (command)
{
case PLAY -> {
@@ -328,10 +345,8 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
}
case PAUSE -> {
}
case NEXT -> {
}
case PREVIOUS -> {
}
case NEXT -> Queue.getInstance().skipNext();
case PREVIOUS -> Queue.getInstance().skipPrev();
case SEEK -> {
}
}
@@ -350,4 +365,85 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
this.updateSongs(SongProvider.INSTANCE.getSongs());
}
}
@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

@@ -4,8 +4,9 @@
package edu.regis.universeplayer.player;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.*;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.util.LinkedList;
import javax.swing.*;
@@ -40,6 +41,8 @@ public class PlayerControls extends JPanel
SpringLayout layout = new SpringLayout();
this.setLayout(layout);
this.setFocusable(true);
this.setFocusCycleRoot(false);
buttonLayout = new FlowLayout();
buttonCont = new JPanel(buttonLayout);
@@ -83,6 +86,31 @@ public class PlayerControls extends JPanel
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);

View File

@@ -0,0 +1,224 @@
/*
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
*/
package edu.regis.universeplayer.player;
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 javax.swing.border.BevelBorder;
import javax.swing.border.LineBorder;
import java.awt.*;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.util.Formatter;
public class QueueList extends JPanel implements Queue.SongChangeListener, Queue.QueueChangeListener
{
private static final Logger logger = LoggerFactory.getLogger(QueueList.class);
private final JScrollPane scroll;
final JPanel header;
final ScrollablePanel songList;
private int currentHighlight = 0;
private 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) + "Queue" + " ".repeat(20)), c);
this.clearButton = new JButton("Clear");
this.clearButton.addActionListener(e -> {
Queue.getInstance().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);
this.scroll = new JScrollPane(this.songList);
// this.scroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
// this.scroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
this.add(this.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;
JButton songLabel;
JLabel durationLabel;
GridBagConstraints c;
this.songList.removeAll();
int i = 0;
for (Song song : queue)
{
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.getClickCount() == 2)
{
queue.skipToSong(songIndex);
}
});
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();
}
}

View File

@@ -4,29 +4,25 @@
package edu.regis.universeplayer.player;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.util.Collection;
import java.util.HashMap;
import com.wordpress.tips4java.ScrollablePanel;
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.Map;
import java.util.*;
import java.util.stream.Collectors;
import javax.swing.JLabel;
import javax.swing.JPanel;
import edu.regis.universeplayer.data.Album;
import edu.regis.universeplayer.data.Song;
import edu.regis.universeplayer.data.SongProvider;
/**
* This panel will list all the songs that are to be currently displayed.
*/
public class SongList extends JPanel
public class SongList extends ScrollablePanel
{
private Map<Album, List<Song>> currentAlbums;
private Map<JLabel, Song> labelMap = new HashMap<>();
private Map<JComponent, Song> labelMap = new HashMap<>();
private Map<AlbumInfo, Album> artMap = new HashMap<>();
public SongList()
@@ -34,10 +30,37 @@ public class SongList extends JPanel
super();
GridBagLayout layout = new GridBagLayout();
this.setFocusTraversalPolicyProvider(true);
this.setLayout(layout);
SongProvider<?> provider = SongProvider.INSTANCE;
this.listAlbums(provider.getSongs());
this.setScrollableWidth(ScrollableSizeHint.FIT);
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());
});
}
}
});
}
/**
@@ -50,14 +73,12 @@ public class SongList extends JPanel
Map<Album, List<Song>> albums = songs.stream().sorted().collect(Collectors
.groupingBy(song -> song.album, Collectors
.mapping(song -> (Song) song, Collectors.toList())));
GridBagConstraints c = new GridBagConstraints(), c2 = new GridBagConstraints();
c.fill = GridBagConstraints.NONE;
c.insets = new Insets(0, 0, 20, 0);
int i = 0, j;
AlbumInfo albumInfo;
GridBagConstraints c = new GridBagConstraints();
c.fill = GridBagConstraints.HORIZONTAL;
int i = 0;
List<Song> songCollection;
JPanel songList;
JLabel songNum, songTitle;
JLabel songNum;
JButton songTitle;
this.labelMap.clear();
this.artMap.clear();
@@ -68,42 +89,275 @@ public class SongList extends JPanel
{
songCollection = albums.get(album);
albumInfo = new AlbumInfo(album);
AlbumInfo albumInfo = new AlbumInfo(album);
c.gridx = 0;
c.gridy = i;
c.anchor = GridBagConstraints.WEST;
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);
songList = new JPanel(new GridBagLayout());
j = 0;
JButton firstSong = null;
for (Song song : songCollection)
{
songNum = new JLabel(String.valueOf(song.trackNum));
c2.gridx = 0;
c2.gridy = j;
c2.anchor = GridBagConstraints.EAST;
c2.insets = new Insets(0, 0, 0, 0);
songList.add(songNum, c2);
songNum.setFocusable(false);
c.gridx = 1;
c.gridy = i;
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 JLabel(song.title);
c2.gridx = 1;
c2.gridy = j;
c2.anchor = GridBagConstraints.WEST;
c2.insets = new Insets(0, 10, 0, 0);
songList.add(songTitle, c2);
songTitle = new JButton(song.title);
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;
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
j++;
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++;
}
c.gridx = 1;
c.anchor = GridBagConstraints.WEST;
this.add(songList, c);
// this.add(new JLabel(new ImageIcon(this.getClass().getResource("/gui/icons/defaultart.png"), "Default")), c);
c.gridx = 0;
c.gridy = i++;
c.gridwidth = 3;
c.anchor = GridBagConstraints.NORTH;
this.add(new JSeparator(SwingConstants.HORIZONTAL), c);
i++;
}
}
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);
}
}
}

View File

@@ -20,5 +20,8 @@
<Logger name="edu.regis.universeplayer.data.LocalSongProvider" level="debug">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="edu.regis.universeplayer.player.Interface" level="debug">
<AppenderRef ref="Console"/>
</Logger>
</Loggers>
</Configuration>