diff --git a/interface/src/main/java/com/wordpress/tips4java/ScrollablePanel.java b/interface/src/main/java/com/wordpress/tips4java/ScrollablePanel.java
new file mode 100644
index 0000000..cd29e38
--- /dev/null
+++ b/interface/src/main/java/com/wordpress/tips4java/ScrollablePanel.java
@@ -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:
+ *
+ * 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:
+ *
+ * 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 + "]";
+ }
+ }
+}
diff --git a/interface/src/main/java/edu/regis/universeplayer/data/LocalSongProvider.java b/interface/src/main/java/edu/regis/universeplayer/data/LocalSongProvider.java
index 503c711..c186f65 100644
--- a/interface/src/main/java/edu/regis/universeplayer/data/LocalSongProvider.java
+++ b/interface/src/main/java/edu/regis/universeplayer/data/LocalSongProvider.java
@@ -443,22 +443,25 @@ public class LocalSongProvider implements SongProvider
{
try
{
- state = getDb().createStatement();
- result = state.executeQuery("SELECT mod FROM local_songs WHERE file='" + this.file.getAbsolutePath() + "';");
- if (result.next())
+ synchronized (db)
{
- if (result.getLong(1) >= this.file.lastModified())
+ state = getDb().createStatement();
+ result = state.executeQuery("SELECT mod FROM local_songs WHERE file='" + this.file.getAbsolutePath().replaceAll("'", "''") + "';");
+ if (result.next())
{
- /*
- * No modifications needed
- */
- updatedSongs++;
- triggerUpdateListeners();
- return;
- }
- else
- {
- state.executeUpdate("UPDATE local_songs SET mod = " + this.file.lastModified() + " WHERE file='" + this.file.getAbsolutePath() + "';");
+ if (result.getLong(1) >= this.file.lastModified())
+ {
+ /*
+ * No modifications needed
+ */
+ updatedSongs++;
+ triggerUpdateListeners();
+ return;
+ }
+ else
+ {
+ state.executeUpdate("UPDATE local_songs SET mod = " + this.file.lastModified() + " WHERE file='" + this.file.getAbsolutePath().replaceAll("'", "''") + "';");
+ }
}
}
currentFolder = file.getPath();
@@ -609,147 +612,153 @@ public class LocalSongProvider implements SongProvider
/*
* Update album information.
*/
- result = state.executeQuery("SELECT album FROM local_albums WHERE album='" + albumTitle + "';");
- if (!result.next())
+ synchronized (db)
{
- state.executeUpdate("INSERT INTO local_albums (album) VALUES ('" + albumTitle + "');");
- }
- result = state.executeQuery("SELECT artists FROM local_albums WHERE album='" + albumTitle + "';");
- if (result.getString("artists") == null && albumArtist != null)
- {
- state.executeUpdate("UPDATE local_albums SET artists='" + Arrays.stream(albumArtist.split(";")).map(String::trim).collect(Collectors.joining(";")) + "' WHERE album='" + albumTitle + "';");
- }
- // TODO - Can we get year metadata?
- result = state.executeQuery("SELECT genres FROM local_albums WHERE album='" + albumTitle + "';");
- if (result.getString("genres") == null && genre != null)
- {
- state.executeUpdate("UPDATE local_albums SET genres='" + Arrays.stream(genre.split(";")).map(String::trim).collect(Collectors.joining(";")) + "' WHERE album='" + albumTitle + "';");
- }
- result = state.executeQuery("SELECT tracks FROM local_albums WHERE album='" + albumTitle + "';");
- if (result.getInt("tracks") == 0 && track != null && track[1] > 0)
- {
- state.executeUpdate("UPDATE local_albums SET tracks=" + track[1] + " WHERE album='" + albumTitle + "';");
- }
- result = state.executeQuery("SELECT discs FROM local_albums WHERE album='" + albumTitle + "';");
- if (result.getInt("discs") == 0 && disc != null && disc[1] > 0)
- {
- state.executeUpdate("UPDATE local_albums SET tracks=" + disc[1] + " WHERE album='" + albumTitle + "';");
- }
-
- /*
- * Create the song
- */
- result = state.executeQuery("SELECT title FROM local_songs WHERE file='" + file.getAbsolutePath() + "';");
- if (result.next())
- {
- logger.debug("Updating song cache for {} ({})", title, file);
- StringBuilder sql = new StringBuilder("UPDATE local_songs SET ");
- sql.append("codec='").append(codec).append("', ");
- sql.append("type='").append(codec).append("', ");
- if (title != null && !title.isEmpty())
+ /*
+ * This part in particular is prone to thread-safety issues.
+ */
+ result = state.executeQuery("SELECT album FROM local_albums WHERE album='" + albumTitle + "';");
+ if (!result.next())
{
- sql.append("title='").append(title).append("', ");
+ state.executeUpdate("INSERT INTO local_albums (album) VALUES ('" + albumTitle + "');");
}
- else
+ result = state.executeQuery("SELECT artists FROM local_albums WHERE album='" + albumTitle + "';");
+ if (result.getString("artists") == null && albumArtist != null)
{
- sql.append("title='").append(file.getName()).append("', ");
+ state.executeUpdate("UPDATE local_albums SET artists='" + Arrays.stream(albumArtist.split(";")).map(String::trim).collect(Collectors.joining(";")) + "' WHERE album='" + albumTitle + "';");
}
- if (artist != null && !artist.isEmpty())
+ // TODO - Can we get year metadata?
+ result = state.executeQuery("SELECT genres FROM local_albums WHERE album='" + albumTitle + "';");
+ if (result.getString("genres") == null && genre != null)
{
- sql.append("artists='").append(Optional.of(artist).map(s -> s.split(";")).stream().flatMap(Arrays::stream).map(String::trim).collect(Collectors.joining(";"))).append("', ");
+ state.executeUpdate("UPDATE local_albums SET genres='" + Arrays.stream(genre.split(";")).map(String::trim).collect(Collectors.joining(";")) + "' WHERE album='" + albumTitle + "';");
}
- else
+ result = state.executeQuery("SELECT tracks FROM local_albums WHERE album='" + albumTitle + "';");
+ if (result.getInt("tracks") == 0 && track != null && track[1] > 0)
{
- sql.append("artists=NULL, ");
+ state.executeUpdate("UPDATE local_albums SET tracks=" + track[1] + " WHERE album='" + albumTitle + "';");
}
- if (track != null && track[0] > 0)
+ result = state.executeQuery("SELECT discs FROM local_albums WHERE album='" + albumTitle + "';");
+ if (result.getInt("discs") == 0 && disc != null && disc[1] > 0)
{
- sql.append("track=").append(track[0]).append(", ");
- }
- else
- {
- sql.append("track=NULL, ");
- }
- if (disc != null && disc[0] > 0)
- {
- sql.append("disc=").append(disc[0]).append(", ");
- }
- else
- {
- sql.append("disc=NULL, ");
- }
- if (duration != 0)
- {
- sql.append("duration=").append(duration).append(", ");
- }
- else
- {
- sql.append("duration=NULL, ");
- }
- if (albumTitle != null && !albumTitle.isEmpty())
- {
- sql.append("album='").append(albumTitle).append("', ");
- }
- else
- {
- sql.append("album=NULL, ");
- }
- sql.append("mod=").append(file.lastModified());
- sql.append(" WHERE file='").append(file.getAbsolutePath()).append("';");
- state.executeUpdate(sql.toString());
- }
- else
- {
- logger.debug("Caching song {} ({})", title, file);
- StringBuilder sql = new StringBuilder("INSERT INTO local_songs ");
- StringBuilder columns = new StringBuilder("(");
- StringBuilder values = new StringBuilder("(");
-
- columns.append("file,");
- values.append('\'').append(file.getAbsolutePath()).append("',");
- columns.append("codec,");
- values.append('\'').append(codec).append("',");
- columns.append("type,");
- values.append('\'').append(type).append("',");
- if (title != null && !title.isEmpty())
- {
- columns.append("title,");
- values.append('\'').append(title).append("',");
- }
- if (artist != null && !artist.isEmpty())
- {
- columns.append("artists,");
- values.append('\'').append(Optional.of(artist).map(s -> s.split(";")).stream().flatMap(Arrays::stream).map(String::trim).collect(Collectors.joining(";"))).append("',");
- }
- if (track != null && track[0] > 0)
- {
- columns.append("track,");
- values.append(track[0]).append(",");
- }
- if (disc != null && disc[0] > 0)
- {
- columns.append("disc,");
- values.append(disc[0]).append(",");
- }
- if (duration > 0)
- {
- columns.append("duration,");
- values.append(duration).append(",");
- }
- if (albumTitle != null && !albumTitle.isEmpty())
- {
- columns.append("album,");
- values.append('\'').append(albumTitle).append("',");
+ state.executeUpdate("UPDATE local_albums SET tracks=" + disc[1] + " WHERE album='" + albumTitle + "';");
}
- columns.append("mod");
- values.append(file.lastModified());
-
- columns.append(") VALUES ");
- values.append(");");
- sql.append(columns);
- sql.append(values);
- state.executeUpdate(sql.toString());
+ /*
+ * Create the song
+ */
+ result = state.executeQuery("SELECT title FROM local_songs WHERE file='" + file.getAbsolutePath().replaceAll("'", "''") + "';");
+ if (result.next())
+ {
+ logger.debug("Updating song cache for {} ({})", title, file);
+ StringBuilder sql = new StringBuilder("UPDATE local_songs SET ");
+ sql.append("codec='").append(codec).append("', ");
+ sql.append("type='").append(codec).append("', ");
+ if (title != null && !title.isEmpty())
+ {
+ sql.append("title='").append(title).append("', ");
+ }
+ else
+ {
+ sql.append("title='").append(file.getName()).append("', ");
+ }
+ if (artist != null && !artist.isEmpty())
+ {
+ sql.append("artists='").append(Optional.of(artist).map(s -> s.split(";")).stream().flatMap(Arrays::stream).map(String::trim).collect(Collectors.joining(";"))).append("', ");
+ }
+ else
+ {
+ sql.append("artists=NULL, ");
+ }
+ if (track != null && track[0] > 0)
+ {
+ sql.append("track=").append(track[0]).append(", ");
+ }
+ else
+ {
+ sql.append("track=NULL, ");
+ }
+ if (disc != null && disc[0] > 0)
+ {
+ sql.append("disc=").append(disc[0]).append(", ");
+ }
+ else
+ {
+ sql.append("disc=NULL, ");
+ }
+ if (duration != 0)
+ {
+ sql.append("duration=").append(duration).append(", ");
+ }
+ else
+ {
+ sql.append("duration=NULL, ");
+ }
+ if (albumTitle != null && !albumTitle.isEmpty())
+ {
+ sql.append("album='").append(albumTitle).append("', ");
+ }
+ else
+ {
+ sql.append("album=NULL, ");
+ }
+ sql.append("mod=").append(file.lastModified());
+ sql.append(" WHERE file='").append(file.getAbsolutePath().replaceAll("'", "''")).append("';");
+ state.executeUpdate(sql.toString());
+ }
+ else
+ {
+ logger.debug("Caching song {} ({})", title, file);
+ StringBuilder sql = new StringBuilder("INSERT INTO local_songs ");
+ StringBuilder columns = new StringBuilder("(");
+ StringBuilder values = new StringBuilder("(");
+
+ columns.append("file,");
+ values.append('\'').append(file.getAbsolutePath().replaceAll("'", "''")).append("',");
+ columns.append("codec,");
+ values.append('\'').append(codec).append("',");
+ columns.append("type,");
+ values.append('\'').append(type).append("',");
+ if (title != null && !title.isEmpty())
+ {
+ columns.append("title,");
+ values.append('\'').append(title).append("',");
+ }
+ if (artist != null && !artist.isEmpty())
+ {
+ columns.append("artists,");
+ values.append('\'').append(Optional.of(artist).map(s -> s.split(";")).stream().flatMap(Arrays::stream).map(String::trim).collect(Collectors.joining(";"))).append("',");
+ }
+ if (track != null && track[0] > 0)
+ {
+ columns.append("track,");
+ values.append(track[0]).append(",");
+ }
+ if (disc != null && disc[0] > 0)
+ {
+ columns.append("disc,");
+ values.append(disc[0]).append(",");
+ }
+ if (duration > 0)
+ {
+ columns.append("duration,");
+ values.append(duration).append(",");
+ }
+ if (albumTitle != null && !albumTitle.isEmpty())
+ {
+ columns.append("album,");
+ values.append('\'').append(albumTitle).append("',");
+ }
+
+ columns.append("mod");
+ values.append(file.lastModified());
+
+ columns.append(") VALUES ");
+ values.append(");");
+ sql.append(columns);
+ sql.append(values);
+ state.executeUpdate(sql.toString());
+ }
}
updatedSongs++;
@@ -828,83 +837,86 @@ public class LocalSongProvider implements SongProvider
/*
* Check if the table exists
*/
- state = getDb().createStatement();
- result = state.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='LOCAL_ALBUMS';");
- if (!result.next())
+ synchronized (db)
{
- logger.debug("Creating album table.");
- /*
- * Create the table
- */
- state.executeUpdate("CREATE TABLE LOCAL_ALBUMS" +
- "(ALBUM TEXT PRIMARY KEY NOT NULL," +
- "ARTISTS TEXT," +
- "YEAR INTEGER," +
- "GENRES TEXT," +
- "TRACKS INTEGER," +
- "DISCS INTEGER);");
- }
- else
- {
- result = state.executeQuery("SELECT * FROM LOCAL_ALBUMS;");
- while (result.next())
+ state = getDb().createStatement();
+ result = state.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='LOCAL_ALBUMS';");
+ if (!result.next())
{
- album = albums.get(result.getString("album"));
- if (album == null)
- {
- album = new Album();
- album.name = result.getString("album");
- albums.put(album.name, album);
- }
- album.artists = Optional.ofNullable(result.getString("artists")).map(s -> s.split(";")).orElse(new String[0]);
- album.year = result.getInt("year");
- album.genres = Optional.ofNullable(result.getString("genres")).map(s -> s.split(";")).orElse(new String[0]);
- album.totalTracks = result.getInt("tracks");
- album.totalDiscs = result.getInt("discs");
+ logger.debug("Creating album table.");
+ /*
+ * Create the table
+ */
+ state.executeUpdate("CREATE TABLE LOCAL_ALBUMS" +
+ "(ALBUM TEXT PRIMARY KEY NOT NULL," +
+ "ARTISTS TEXT," +
+ "YEAR INTEGER," +
+ "GENRES TEXT," +
+ "TRACKS INTEGER," +
+ "DISCS INTEGER);");
}
- }
- result = state.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='LOCAL_SONGS';");
- if (!result.next())
- {
- logger.debug("Creating song table.");
- /*
- * Create the table
- */
- state.executeUpdate("CREATE TABLE LOCAL_SONGS" +
- "(FILE TEXT PRIMARY KEY NOT NULL," +
- "CODEC CHAR(5)," +
- "TYPE CHAR(5)," +
- "TITLE TEXT," +
- "ARTISTS TEXT," +
- "TRACK INTEGER," +
- "DISC INTEGER," +
- "DURATION BIGINT," +
- "ALBUM TEXT," +
- "MOD BIGINT);");
- }
- else
- {
- result = state.executeQuery("SELECT * FROM LOCAL_SONGS;");
- while (result.next())
+ else
{
- song = songs.get(new File(result.getString("file")));
- if (song == null)
+ result = state.executeQuery("SELECT * FROM LOCAL_ALBUMS;");
+ while (result.next())
{
- song = new LocalSong();
- song.file = new File(result.getString("file"));
- songs.put(song.file, song);
+ album = albums.get(result.getString("album"));
+ if (album == null)
+ {
+ album = new Album();
+ album.name = result.getString("album");
+ albums.put(album.name, album);
+ }
+ album.artists = Optional.ofNullable(result.getString("artists")).map(s -> s.split(";")).orElse(new String[0]);
+ album.year = result.getInt("year");
+ album.genres = Optional.ofNullable(result.getString("genres")).map(s -> s.split(";")).orElse(new String[0]);
+ album.totalTracks = result.getInt("tracks");
+ album.totalDiscs = result.getInt("discs");
}
- song.codec = result.getString("codec");
- song.type = result.getString("type");
- song.title = result.getString("title");
- song.artists = Optional.ofNullable(result.getString("artists")).map(s -> s.split(";")).orElse(new String[0]);
- song.trackNum = result.getInt("track");
- song.disc = result.getInt("disc");
- song.duration = result.getLong("duration");
- song.album = Optional.ofNullable(result.getString("album")).map(albums::get).orElse(null);
}
+ result = state.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='LOCAL_SONGS';");
+ if (!result.next())
+ {
+ logger.debug("Creating song table.");
+ /*
+ * Create the table
+ */
+ state.executeUpdate("CREATE TABLE LOCAL_SONGS" +
+ "(FILE TEXT PRIMARY KEY NOT NULL," +
+ "CODEC CHAR(5)," +
+ "TYPE CHAR(5)," +
+ "TITLE TEXT," +
+ "ARTISTS TEXT," +
+ "TRACK INTEGER," +
+ "DISC INTEGER," +
+ "DURATION BIGINT," +
+ "ALBUM TEXT," +
+ "MOD BIGINT);");
+ }
+ else
+ {
+ result = state.executeQuery("SELECT * FROM LOCAL_SONGS;");
+ while (result.next())
+ {
+ song = songs.get(new File(result.getString("file")));
+ if (song == null)
+ {
+ song = new LocalSong();
+ song.file = new File(result.getString("file"));
+ songs.put(song.file, song);
+ }
+ song.codec = result.getString("codec");
+ song.type = result.getString("type");
+ song.title = result.getString("title");
+ song.artists = Optional.ofNullable(result.getString("artists")).map(s -> s.split(";")).orElse(new String[0]);
+ song.trackNum = result.getInt("track");
+ song.disc = result.getInt("disc");
+ song.duration = result.getLong("duration");
+ song.album = Optional.ofNullable(result.getString("album")).map(albums::get).orElse(null);
+ }
+ }
+ state.close();
}
- state.close();
}
catch (SQLException e)
{
diff --git a/interface/src/main/java/edu/regis/universeplayer/data/Queue.java b/interface/src/main/java/edu/regis/universeplayer/data/Queue.java
new file mode 100644
index 0000000..1d86068
--- /dev/null
+++ b/interface/src/main/java/edu/regis/universeplayer/data/Queue.java
@@ -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
+{
+ private static Queue INSTANCE;
+
+ public static Queue getInstance()
+ {
+ if (INSTANCE == null)
+ {
+ INSTANCE = new Queue();
+ }
+ return INSTANCE;
+ }
+
+ private final ArrayList queueOrder = new ArrayList<>();
+ private int currentIndex;
+ private boolean shuffle;
+ private boolean repeat;
+ private final Random random = new Random();
+
+ private final LinkedList songListeners = new LinkedList<>();
+ private final LinkedList 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 getOrder()
+ {
+ LinkedList 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);
+ }
+}
diff --git a/interface/src/main/java/edu/regis/universeplayer/player/AlbumInfo.java b/interface/src/main/java/edu/regis/universeplayer/player/AlbumInfo.java
index 93c1fd5..33efad4 100644
--- a/interface/src/main/java/edu/regis/universeplayer/player/AlbumInfo.java
+++ b/interface/src/main/java/edu/regis/universeplayer/player/AlbumInfo.java
@@ -4,30 +4,38 @@
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);
this.albumName = new JLabel("Album");
@@ -38,7 +46,7 @@ public class AlbumInfo extends JPanel
this.add(this.genres);
this.year = new JLabel("20XX");
this.add(this.year);
-
+
/*
* Set the layout information
*/
@@ -52,27 +60,54 @@ 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)
{
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;
@@ -80,13 +115,13 @@ public class AlbumInfo extends JPanel
else
{
icon = new ImageIcon(this.getClass()
- .getResource("/gui/icons/defaultart.png"), "Default");
+ .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)
{
@@ -103,7 +138,7 @@ public class AlbumInfo extends JPanel
}
}
this.artists.setText(builder.toString());
-
+
builder = new StringBuilder();
if (album.genres != null && album.genres.length >= 1)
{
@@ -120,7 +155,7 @@ public class AlbumInfo extends JPanel
}
}
this.genres.setText(builder.toString());
-
+
this.year.setText(String.valueOf(album.year));
}
}
diff --git a/interface/src/main/java/edu/regis/universeplayer/player/CollectionList.java b/interface/src/main/java/edu/regis/universeplayer/player/CollectionList.java
index b62324c..8839d3d 100644
--- a/interface/src/main/java/edu/regis/universeplayer/player/CollectionList.java
+++ b/interface/src/main/java/edu/regis/universeplayer/player/CollectionList.java
@@ -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 labelMap = new HashMap<>();
+ private Map 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())
@@ -223,13 +238,14 @@ public class CollectionList extends JPanel
private void addYears(List years)
{
// 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.
*
diff --git a/interface/src/main/java/edu/regis/universeplayer/player/Collections.java b/interface/src/main/java/edu/regis/universeplayer/player/Collections.java
index 228e5f4..3dee1cf 100644
--- a/interface/src/main/java/edu/regis/universeplayer/player/Collections.java
+++ b/interface/src/main/java/edu/regis/universeplayer/player/Collections.java
@@ -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;
}
/**
diff --git a/interface/src/main/java/edu/regis/universeplayer/player/FocusManager.java b/interface/src/main/java/edu/regis/universeplayer/player/FocusManager.java
new file mode 100644
index 0000000..9f43d13
--- /dev/null
+++ b/interface/src/main/java/edu/regis/universeplayer/player/FocusManager.java
@@ -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()
+ {
+ }
+}
diff --git a/interface/src/main/java/edu/regis/universeplayer/player/Interface.java b/interface/src/main/java/edu/regis/universeplayer/player/Interface.java
index 33e7f02..2e036b8 100644
--- a/interface/src/main/java/edu/regis/universeplayer/player/Interface.java
+++ b/interface/src/main/java/edu/regis/universeplayer/player/Interface.java
@@ -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,8 +81,10 @@ 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
{
@@ -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)
+ {
+ }
}
\ No newline at end of file
diff --git a/interface/src/main/java/edu/regis/universeplayer/player/PlayerControls.java b/interface/src/main/java/edu/regis/universeplayer/player/PlayerControls.java
index 754db06..c75bc62 100644
--- a/interface/src/main/java/edu/regis/universeplayer/player/PlayerControls.java
+++ b/interface/src/main/java/edu/regis/universeplayer/player/PlayerControls.java
@@ -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);
@@ -82,6 +85,31 @@ public class PlayerControls extends JPanel
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);
diff --git a/interface/src/main/java/edu/regis/universeplayer/player/QueueList.java b/interface/src/main/java/edu/regis/universeplayer/player/QueueList.java
new file mode 100644
index 0000000..39b8249
--- /dev/null
+++ b/interface/src/main/java/edu/regis/universeplayer/player/QueueList.java
@@ -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();
+ }
+}
diff --git a/interface/src/main/java/edu/regis/universeplayer/player/SongList.java b/interface/src/main/java/edu/regis/universeplayer/player/SongList.java
index ea2ca0c..10b6b93 100644
--- a/interface/src/main/java/edu/regis/universeplayer/player/SongList.java
+++ b/interface/src/main/java/edu/regis/universeplayer/player/SongList.java
@@ -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> currentAlbums;
- private Map labelMap = new HashMap<>();
+ private Map labelMap = new HashMap<>();
private Map 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,15 +73,13 @@ public class SongList extends JPanel
Map> 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 songCollection;
- JPanel songList;
- JLabel songNum, songTitle;
-
+ JLabel songNum;
+ JButton songTitle;
+
this.labelMap.clear();
this.artMap.clear();
this.removeAll();
@@ -67,43 +88,276 @@ public class SongList extends JPanel
for (Album album : albums.keySet())
{
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 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 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);
+ }
+ }
}
diff --git a/interface/src/main/resources/log4j2.xml b/interface/src/main/resources/log4j2.xml
index 1fb81cc..51746ff 100644
--- a/interface/src/main/resources/log4j2.xml
+++ b/interface/src/main/resources/log4j2.xml
@@ -20,5 +20,8 @@
+
+
+
\ No newline at end of file