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