/* * Copyright (c) 2021 William Hubbard. All Rights Reserved. */ package edu.regis.universeplayer.player; import edu.regis.universeplayer.Player; import edu.regis.universeplayer.browser.Browser; import edu.regis.universeplayer.browser.BrowserPlayer; import edu.regis.universeplayer.data.InternetSong; import edu.regis.universeplayer.data.Queue; import edu.regis.universeplayer.data.*; import edu.regis.universeplayer.localPlayer.LocalPlayer; import net.harawata.appdirs.AppDirsFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Locale; import java.util.ResourceBundle; import java.util.Set; /** * The Interface class serves as the primary GUI that the player interacts with. * * @author William Hubbard * @version 0.1 */ public class Interface extends JFrame implements SongDisplayListener, ComponentListener, WindowListener, UpdateListener, FocusListener { // static { // Locale.setDefault(new Locale("es", "ES")); // } private static final Logger logger = LoggerFactory.getLogger(Interface.class); private static final ResourceBundle langs = ResourceBundle .getBundle("lang.interface", Locale.getDefault()); private static Interface INSTANCE; private static File dataDir; private static File commDir; private static File configDir; public final ActionMap actions = new ActionMap(); /** * A reference to the panel containing links to different collection views. */ private final Collections collectionTypes; /** * A reference to the panel showing the queue. */ private final QueueList queueList; /** * A reference to the central view showing a list of songs. */ private final SongList songList; /** * A reference to the central view showing a list of collections. */ private final CollectionList collectionList; /** * A reference to the central view scroll pane. */ private final JScrollPane centerView; /** * A reference to the player controls pane. */ private final PlayerControls controls; /** * A link to the browser. */ private final ArrayList> players = new ArrayList<>(); private int currentPlayer = -1; public static Interface getInstance() { return INSTANCE; } public static void main(String[] args) { Thread browserThread; Interface inter = null; Browser browser; BrowserPlayer browserPlayer; try { /* * Add this just in case of a crash or something. It won't work if the * program is forcibly terminated by the OS, but it could be helpful * otherwise. */ logger.info("Starting application"); inter = new Interface(); inter.setSize(700, 500); SongProvider.INSTANCE.addUpdateListener(inter); inter.setVisible(true); browser = Browser.createBrowser(); browserThread = new Thread(browser); browserThread.start(); Player.REGISTERED_PLAYERS.put(LocalSong.class, new LocalPlayer()); try { inter.players.add(browserPlayer = new BrowserPlayer()); logger.debug("Sending ping"); browser.sendObject("ping"); Runtime.getRuntime().addShutdownHook(new Thread(browserPlayer::close)); Player.REGISTERED_PLAYERS.put(InternetSong.class, browserPlayer); // LinkedList> pingRequests = new LinkedList<>(); // for (int i = 0; i < 20; i++) // { // logger.info("Sending ping"); // pingRequests.add(browser.sendObject("ping")); // } // // LinkedList> toRemove = new LinkedList<>(); // while (pingRequests.size() > 0) // { // for (Future future : pingRequests) // { // if (future.isDone()) // { // logger.info("Receiving {}", future.get()); // toRemove.add(future); // } // } // pingRequests.removeAll(toRemove); // toRemove.clear(); // } } catch (IOException e) { logger.error("Could not open browser background", e); JOptionPane.showMessageDialog(inter, e, langs .getString("error.browser.launch"), JOptionPane.ERROR_MESSAGE); } } catch (Throwable e) { logger.error("Could not open browser background", e); JOptionPane .showMessageDialog(inter != null && inter.isVisible() ? inter : null, e, langs .getString("error.generic"), JOptionPane.ERROR_MESSAGE); } } /** * Obtains the data storage directory for the application, creating it if * needed. * * @return The data storage directory. */ public static File getDataDir() { if (dataDir == null) { dataDir = new File(AppDirsFactory.getInstance() .getUserDataDir("universalmusic", null, null, true)); if (!dataDir.exists()) { if (!dataDir.mkdir()) { logger.error("Could not create data directory {}", dataDir); } } } return dataDir; } /** * Obtains the configuration directory for the application, creating it if * needed. * * @return The configuration directory. */ public static File getConfigDir() { if (configDir == null) { configDir = new File(AppDirsFactory.getInstance() .getUserConfigDir("universalmusic", null, null, true)); if (!configDir.exists()) { if (!configDir.mkdir()) { logger.error("Could not create configuration directory {}", configDir); } } } return configDir; } /** * Obtains the directory for memory mapped files * * @return The communications storage directory. */ public static File getCommDir() { if (commDir == null) { commDir = new File(AppDirsFactory.getInstance() .getSharedDir("universalmusic", null, null), "comm"); if (!commDir.getParentFile().exists()) { if (!commDir.getParentFile().mkdir()) { logger.error("Could not create shared directory {}", commDir.getParent()); } } if (!commDir.exists()) { if (!commDir.mkdir()) { logger.error("Could not create data directory {}", commDir); } } } return commDir; } /** * Creates an interface */ public Interface() { super(); INSTANCE = this; this.initActions(); this.collectionTypes = new Collections(); this.controls = new PlayerControls(); this.queueList = new QueueList(); this.songList = new SongList(); this.collectionList = new CollectionList(); this.centerView = new JScrollPane(this.songList); this.constructWindow(); this.setFocusManager(); this.updateSongs(SongProvider.INSTANCE.getSongs()); } protected void initActions() { AbstractAction action; this.actions.put("refresh", action = new AbstractAction(langs.getString("actions.refresh")) { @Override public void actionPerformed(ActionEvent e) { if (this.isEnabled()) { } } }); action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_R); this.actions.put("addExternal", action = new AbstractAction(langs .getString("actions.addExternal")) { @Override public void actionPerformed(ActionEvent e) { if (this.isEnabled()) { InternetSongDialog dialog = new InternetSongDialog(Interface.this); dialog.addWindowListener(new WindowAdapter() { /** * Invoked when a window has been closed. * * @param e */ @Override public void windowClosed(WindowEvent e) { } }); dialog.setVisible(true); } } }); action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_E); this.actions.put("exit", action = new AbstractAction(langs.getString("actions.exit")) { @Override public void actionPerformed(ActionEvent e) { if (this.isEnabled()) { Interface.this .dispatchEvent(new WindowEvent(Interface.this, WindowEvent.WINDOW_CLOSING)); } } }); action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_X); this.actions.put("logs", action = new AbstractAction(langs.getString("actions.logs")) { @Override public void actionPerformed(ActionEvent e) { if (this.isEnabled()) { } } }); this.actions.put("about", action = new AbstractAction(langs.getString("actions.about")) { @Override public void actionPerformed(ActionEvent e) { if (this.isEnabled()) { } } }); this.actions .put("view.all", action = new AbstractAction(langs.getString("actions.view.all")) { @Override public void actionPerformed(ActionEvent e) { if (this.isEnabled()) { collectionTypes .triggerSongDisplayListeners(new ArrayList<>(SongProvider.INSTANCE .getSongs())); } } }); action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_A); this.actions.put("view.artists", action = new AbstractAction(langs .getString("actions.view.artists")) { @Override public void actionPerformed(ActionEvent e) { if (this.isEnabled()) { collectionTypes .triggerCollectionDisplayListeners(CollectionType.albumArtist, SongProvider.INSTANCE .getAlbumArtists()); } } }); action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_T); this.actions.put("view.albums", action = new AbstractAction(langs .getString("actions.view.albums")) { @Override public void actionPerformed(ActionEvent e) { if (this.isEnabled()) { collectionTypes .triggerCollectionDisplayListeners(CollectionType.album, SongProvider.INSTANCE .getAlbums()); } } }); action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_B); this.actions.put("view.genres", action = new AbstractAction(langs .getString("actions.view.genres")) { @Override public void actionPerformed(ActionEvent e) { if (this.isEnabled()) { collectionTypes .triggerCollectionDisplayListeners(CollectionType.genre, SongProvider.INSTANCE .getGenres()); } } }); action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_G); this.actions.put("view.years", action = new AbstractAction(langs .getString("actions.view.years")) { @Override public void actionPerformed(ActionEvent e) { if (this.isEnabled()) { collectionTypes .triggerCollectionDisplayListeners(CollectionType.year, SongProvider.INSTANCE .getYears()); } } }); action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_Y); this.actions.put("view.playlists", action = new AbstractAction(langs .getString("actions.view.playlists")) { @Override public void actionPerformed(ActionEvent e) { if (this.isEnabled()) { } } }); action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_L); this.actions.put("playback.clear", action = new AbstractAction(langs .getString("actions.playback.clear")) { @Override public void actionPerformed(ActionEvent e) { if (this.isEnabled()) { Queue.getInstance().clear(); } } }); action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_R); this.actions.put("playback.play", action = new AbstractAction(langs .getString("actions.playback.play")) { @Override public void actionPerformed(ActionEvent e) { if (this.isEnabled()) { controls.play(); } } }); this.actions.put("playback.pause", action = new AbstractAction(langs .getString("actions.playback.pause")) { @Override public void actionPerformed(ActionEvent e) { if (this.isEnabled()) { controls.pause(); } } }); this.actions.put("playback.toggle", action = new AbstractAction(langs .getString("actions.playback.toggle")) { @Override public void actionPerformed(ActionEvent e) { if (this.isEnabled()) { controls.togglePlayback(); } } }); this.actions.put("playback.skipPrev", action = new AbstractAction(langs .getString("actions.playback.skipPrev")) { @Override public void actionPerformed(ActionEvent e) { if (this.isEnabled()) { controls.previousSong(); } } }); this.actions.put("playback.skipNext", action = new AbstractAction(langs .getString("actions.playback.skipNext")) { @Override public void actionPerformed(ActionEvent e) { if (this.isEnabled()) { controls.nextSong(); } } }); } protected void constructWindow() { JMenuBar toolbar; JMenu fileMenu, viewMenu, collectionsMenu, playbackMenu, helpMenu; this.setTitle(langs.getString("title")); this.getContentPane().setLayout(new BorderLayout()); this.setFocusable(true); this.setFocusCycleRoot(true); this.getContentPane() .add(this.collectionTypes, BorderLayout.LINE_START); this.collectionTypes.addFocusListener(this); this.collectionTypes.addSongDisplayListener(this); this.controls.addFocusListener(this); this.getContentPane().add(controls, BorderLayout.PAGE_END); this.queueList.addFocusListener(this); Queue.getInstance().addQueueChangeListener(this.queueList); Queue.getInstance().addSongChangeListener(this.queueList); this.getContentPane().add(queueList, BorderLayout.LINE_END); this.songList.addFocusListener(this); this.collectionList.addSongDisplayListener(this); this.getContentPane().add(this.centerView, BorderLayout.CENTER); this.componentResized(null); toolbar = new JMenuBar(); fileMenu = new JMenu(langs.getString("menu.file.title")); fileMenu.setMnemonic(KeyEvent.VK_F); toolbar.add(fileMenu); viewMenu = new JMenu(langs.getString("menu.view.title")); viewMenu.setMnemonic(KeyEvent.VK_V); toolbar.add(viewMenu); collectionsMenu = new JMenu(langs.getString("menu.view.collections")); collectionsMenu.setMnemonic(KeyEvent.VK_C); viewMenu.add(collectionsMenu); playbackMenu = new JMenu(langs.getString("menu.playback.title")); playbackMenu.setMnemonic(KeyEvent.VK_P); toolbar.add(playbackMenu); helpMenu = new JMenu(langs.getString("menu.help.title")); helpMenu.setMnemonic(KeyEvent.VK_H); toolbar.add(helpMenu); fileMenu.add(new JMenuItem(actions.get("refresh"))); fileMenu.add(actions.get("addExternal")); fileMenu.add(new JMenuItem(actions.get("exit"))); collectionsMenu.add(new JMenuItem(actions.get("view.all"))); collectionsMenu.add(new JMenuItem(actions.get("view.artists"))); collectionsMenu.add(new JMenuItem(actions.get("view.albums"))); collectionsMenu.add(new JMenuItem(actions.get("view.genres"))); collectionsMenu.add(new JMenuItem(actions.get("view.years"))); collectionsMenu.add(new JMenuItem(actions.get("view.playlists"))); playbackMenu.add(new JMenuItem(actions.get("playback.clear"))); playbackMenu.addSeparator(); playbackMenu.add(new JMenuItem(actions.get("playback.toggle"))); playbackMenu.add(new JMenuItem(actions.get("playback.skipPrev"))); playbackMenu.add(new JMenuItem(actions.get("playback.skipNext"))); helpMenu.add(new JMenuItem(actions.get("logs"))); helpMenu.add(new JMenuItem(actions.get("about"))); this.setJMenuBar(toolbar); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.addComponentListener(this); this.addWindowListener(this); this.addFocusListener(this); } protected void setFocusManager() { ((SortingFocusTraversalPolicy) this.getFocusTraversalPolicy()) .setImplicitDownCycleTraversal(true); this.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, Set .of(AWTKeyStroke.getAWTKeyStroke(KeyEvent.VK_DOWN, 0), AWTKeyStroke .getAWTKeyStroke(KeyEvent.VK_RIGHT, 0))); this.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, Set .of(AWTKeyStroke.getAWTKeyStroke(KeyEvent.VK_UP, 0), AWTKeyStroke .getAWTKeyStroke(KeyEvent.VK_LEFT, 0))); this.setFocusTraversalKeys(KeyboardFocusManager.UP_CYCLE_TRAVERSAL_KEYS, Set .of(AWTKeyStroke.getAWTKeyStroke(KeyEvent.VK_TAB, 0))); this.setFocusTraversalKeys(KeyboardFocusManager.DOWN_CYCLE_TRAVERSAL_KEYS, Set .of(AWTKeyStroke.getAWTKeyStroke(KeyEvent.VK_TAB, KeyEvent.SHIFT_DOWN_MASK))); } @Override public void updateSongs(Collection songs) { this.songList.listAlbums(songs); this.centerView.setViewportView(this.songList); this.songList.revalidate(); this.centerView.revalidate(); } @Override public void updateCollections(CollectionType type, Collection collections) { this.collectionList.listCollection(type, collections); this.centerView.setViewportView(this.collectionList); this.collectionList.revalidate(); this.centerView.revalidate(); } @Override public void componentResized(ComponentEvent event) { } @Override public void componentMoved(ComponentEvent event) { } @Override public void componentShown(ComponentEvent event) { } @Override public void componentHidden(ComponentEvent event) { } @Override public void windowOpened(WindowEvent windowEvent) { logger.info("Interface opened"); } @Override public void windowClosing(WindowEvent windowEvent) { this.players.forEach(Player::close); } @Override public void windowClosed(WindowEvent windowEvent) { } @Override public void windowIconified(WindowEvent windowEvent) { } @Override public void windowDeiconified(WindowEvent windowEvent) { } @Override public void windowActivated(WindowEvent windowEvent) { } @Override public void windowDeactivated(WindowEvent windowEvent) { } @Override public void onUpdate(int updated, int totalUpdate, String updating) { this.controls.setUpdateProgress(updated, totalUpdate, updating); if (updated == totalUpdate || totalUpdate == 0) { logger.debug("Resetting the song provider."); /* * TODO - Add some way to get back to the current view, just updated */ 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) { } }