/* * Copyright (c) 2008, 2009, 2010, 2011 Denis Tulskiy * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * version 3 along with this work. If not, see <http://www.gnu.org/licenses/>. */ package com.tulskiy.musique.gui.playlist; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.Point; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.DropMode; import javax.swing.ImageIcon; import javax.swing.InputMap; import javax.swing.JCheckBoxMenuItem; import javax.swing.JFrame; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.KeyStroke; import javax.swing.RepaintManager; import javax.swing.SwingUtilities; import javax.swing.TransferHandler; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.AbstractTableModel; import javax.swing.table.JTableHeader; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import com.tulskiy.musique.audio.player.Player; import com.tulskiy.musique.audio.player.PlayerEvent; import com.tulskiy.musique.audio.player.PlayerListener; import com.tulskiy.musique.gui.components.GroupTable; import com.tulskiy.musique.gui.components.Separator; import com.tulskiy.musique.gui.dialogs.ColumnDialog; import com.tulskiy.musique.gui.dialogs.ProgressDialog; import com.tulskiy.musique.gui.dialogs.Task; import com.tulskiy.musique.gui.dialogs.TracksInfoDialog; import com.tulskiy.musique.gui.dialogs.TreeFileChooser; import com.tulskiy.musique.gui.dnd.PlaylistTransferHandler; import com.tulskiy.musique.gui.dnd.SongsSelection; import com.tulskiy.musique.gui.menu.TracksMenu; import com.tulskiy.musique.playlist.PlaybackOrder; import com.tulskiy.musique.playlist.Playlist; import com.tulskiy.musique.playlist.PlaylistListener; import com.tulskiy.musique.playlist.Track; import com.tulskiy.musique.system.Application; import com.tulskiy.musique.system.configuration.Configuration; import com.tulskiy.musique.util.Util; /** * Author: Denis Tulskiy * Date: May 13, 2010 */ public class PlaylistTable extends GroupTable { private static Application app = Application.getInstance(); private static Player player = app.getPlayer(); private static Configuration config = app.getConfiguration(); private Playlist playlist; private List<PlaylistColumn> columns; private PlaylistModel model; private JScrollPane scrollPane; private JPopupMenu tablePopupMenu; private PlayerListener playerListener; private PropertyChangeListener autoResizeChangeListener; private PlaylistListener playlistListener; public PlaylistTable(Playlist playlist, List<PlaylistColumn> columns) { this.playlist = playlist; this.columns = columns; model = new PlaylistModel(); setModel(model); getTableHeader().setPreferredSize(new Dimension(10000, 20)); scrollPane = new JScrollPane(this); scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); buildActions(); buildMenus(); } public void buildActions() { ActionMap aMap = getActionMap(); InputMap iMap = getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); aMap.put("next", new AbstractAction("Next") { @Override public void actionPerformed(ActionEvent e) { player.next(); } }); aMap.put("stop", new AbstractAction("Stop") { @Override public void actionPerformed(ActionEvent e) { player.stop(); } }); aMap.put("play", new AbstractAction("Play") { @Override public void actionPerformed(ActionEvent e) { player.play(); } }); aMap.put("pause", new AbstractAction("Pause") { @Override public void actionPerformed(ActionEvent e) { player.pause(); } }); aMap.put("prev", new AbstractAction("Previous") { @Override public void actionPerformed(ActionEvent e) { player.prev(); } }); aMap.put("playSelected", new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { ArrayList<Track> tracks = getSelectedSongs(); if (!tracks.isEmpty()) { player.open(tracks.get(0)); PlaybackOrder order = player.getPlaybackOrder(); order.setLastPlayed(null); app.getPlaylistManager().setActivePlaylist(playlist); } } }); aMap.put("showProperties", new AbstractAction("Properties") { @Override public void actionPerformed(ActionEvent e) { ArrayList<Track> tracks = getSelectedSongs(); if (tracks.isEmpty()) return; showProperties(tracks); } }); aMap.put("showNowPlaying", new AbstractAction("Scroll to Now Playing") { @Override public void actionPerformed(ActionEvent e) { scrollToSong(player.getTrack()); } }); aMap.put("removeSelected", new AbstractAction("Remove") { @Override public void actionPerformed(ActionEvent e) { ArrayList<Track> songs = getSelectedSongs(); playlist.removeAll(songs); adjustLastSongAfterDelete(songs); clearSelection(); playlist.firePlaylistChanged(); model.fireTableDataChanged(); } }); aMap.put("enqueue", new AbstractAction("Add to Queue ") { @Override public void actionPerformed(ActionEvent e) { for (Track track : getSelectedSongs()) { PlaybackOrder order = player.getPlaybackOrder(); order.enqueue(track, playlist); update(); } } }); aMap.put("clearQueue", new AbstractAction("Clear Playback Queue") { @Override public void actionPerformed(ActionEvent e) { PlaybackOrder order = player.getPlaybackOrder(); order.flushQueue(); update(); } }); iMap.put(KeyStroke.getKeyStroke("B"), "next"); iMap.put(KeyStroke.getKeyStroke("V"), "stop"); iMap.put(KeyStroke.getKeyStroke("C"), "pause"); iMap.put(KeyStroke.getKeyStroke("X"), "play"); iMap.put(KeyStroke.getKeyStroke("Z"), "prev"); iMap.put(KeyStroke.getKeyStroke("ENTER"), "playSelected"); iMap.put(KeyStroke.getKeyStroke("alt ENTER"), "showProperties"); iMap.put(KeyStroke.getKeyStroke("SPACE"), "showNowPlaying"); iMap.put(KeyStroke.getKeyStroke("DELETE"), "removeSelected"); iMap.put(KeyStroke.getKeyStroke("Q"), "enqueue"); addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) { runAction("playSelected"); } } }); playerListener = new PlayerListener() { public void onEvent(PlayerEvent e) { update(); switch (e.getEventCode()) { case FILE_OPENED: if (config.getBoolean("playlists.cursorFollowsPlayback", true)) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { runAction("showNowPlaying"); } }); } if (config.getBoolean("playlists.playbackFollowsCursor", false)) { PlaybackOrder order = player.getPlaybackOrder(); order.setLastPlayed(null); } break; case STOPPED: int index = playlist.indexOf(player.getTrack()); if (index != -1) setRowSelectionInterval(index, index); break; } } }; player.addListener(playerListener); getTableHeader().addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1) { TableColumnModel model = getTableHeader().getColumnModel(); int index = model.getColumnIndexAtX(e.getX()); if (index != -1) { final int col = model.getColumn(index).getModelIndex(); final PlaylistColumn pc = columns.get(col); playlist.sort(pc.getExpression(), true); playlist.firePlaylistChanged(); } } } }); getSelectionModel().addListSelectionListener(new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent e) { ArrayList<Track> tracks = getSelectedSongs(); if (tracks.isEmpty()) return; Track track = tracks.get(0); config.put("playlist.selectedTrack", track); if (config.getBoolean("playlists.playbackFollowsCursor", false)) { PlaybackOrder order = player.getPlaybackOrder(); order.setLastPlayed(track); } } }); autoResizeChangeListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { int mode = config.getInt(evt.getPropertyName(), AUTO_RESIZE_OFF); setAutoResizeMode(mode); } }; config.addPropertyChangeListener("gui.playlist.autoResizeMode", true, autoResizeChangeListener); playlistListener = new PlaylistListener() { @Override public void playlistUpdated(Playlist playlist) { update(); } @Override public void playlistRemoved(Playlist pl) { if (pl == playlist) dispose(); } }; playlist.addChangeListener(playlistListener); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { app.getPlaylistManager().addPlaylistListener(playlistListener); } }); } public void showProperties(ArrayList<Track> tracks) { TracksInfoDialog dialog = new TracksInfoDialog(this, tracks); dialog.setVisible(true); } public void dispose() { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { config.removePropertyChangeListener(autoResizeChangeListener); playlist.removeChangeListener(playlistListener); player.removeListener(playerListener); app.getPlaylistManager().removePlaylistListener(playlistListener); } }); } public void adjustLastSongAfterDelete(ArrayList<Track> songs) { if (songs.contains(player.getTrack())) { int index = getSelectionModel().getMinSelectionIndex(); if (index < playlist.size()) { player.getPlaybackOrder().setLastPlayed(playlist.get(index)); } } } public JScrollPane getScrollPane() { return scrollPane; } @Override public void createDefaultColumnsFromModel() { super.createDefaultColumnsFromModel(); if (columns != null) { for (int i = 0; i < columns.size(); i++) { PlaylistColumn pc = columns.get(i); getColumnModel().getColumn(i).setPreferredWidth(pc.getSize()); } } } public void setUpDndCCP() { setDragEnabled(true); setDropMode(DropMode.INSERT_ROWS); setTransferHandler(new PlaylistTransferHandler(this)); ActionMap map = getActionMap(); Action cutAction = TransferHandler.getCutAction(); map.put(cutAction.getValue(Action.NAME), cutAction); Action copyAction = TransferHandler.getCopyAction(); map.put(copyAction.getValue(Action.NAME), copyAction); Action pasteAction = TransferHandler.getPasteAction(); map.put(pasteAction.getValue(Action.NAME), pasteAction); } public Playlist getPlaylist() { return playlist; } public void update() { getTableHeader().revalidate(); getTableHeader().repaint(); revalidate(); repaint(); } @Override public void updateUI() { super.updateUI(); Color text = config.getColor("gui.color.text", null); if (text != null) { setForeground(text); } Color background = config.getColor("gui.color.background", null); if (background != null) { setBackground(background); } Color selection = config.getColor("gui.color.selection", null); if (selection != null) { setSelectionBackground(selection); } Color highlight = config.getColor("gui.color.highlight", null); if (highlight != null) { setSeparatorColor(highlight); } Font defaultFont = config.getFont("gui.font.default", null); if (defaultFont != null) { setFont(defaultFont); } } public void scrollToSong(Track track) { int index = playlist.indexOf(track); if (index != -1) { scrollToRow(index); setRowSelectionInterval(index, index); } } public ArrayList<Track> getSelectedSongs() { int[] rows = getSelectedRows(); ArrayList<Track> tracks = new ArrayList<Track>(); for (int row : rows) { Track track = playlist.get(row); if (!(track instanceof Separator)) { tracks.add(track); } } return tracks; } private boolean selectSongsAt(Point p) { int index = rowAtPoint(p); if (index != -1) { if (!isRowSelected(index)) { setRowSelectionInterval(index, index); } return true; } else { clearSelection(); return false; } } //stuff for popup menu private TableColumn selectedColumn; private JFrame parentFrame; public JFrame getParentFrame() { if (parentFrame == null) { try { parentFrame = (JFrame) SwingUtilities.getWindowAncestor(this); } catch (Exception ignored) { } } return parentFrame; } public void buildMenus() { final JTableHeader header = getTableHeader(); header.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { show(e); } @Override public void mouseReleased(MouseEvent e) { show(e); } public void show(MouseEvent e) { if (e.isPopupTrigger()) { int index = header.getColumnModel().getColumnIndexAtX(e.getX()); if (index != -1) { selectedColumn = header.getColumnModel().getColumn(index); } JPopupMenu headerMenu = buildHeaderMenu(); headerMenu.show(e.getComponent(), e.getX(), e.getY()); } } }); addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { show(e); } @Override public void mouseReleased(MouseEvent e) { show(e); } public void show(MouseEvent e) { selectSongsAt(e.getPoint()); if (e.isPopupTrigger()) { // trying to fix issue 6 tablePopupMenu = buildTableMenu(); tablePopupMenu.show(e.getComponent(), e.getX(), e.getY()); } } }); } private JPopupMenu buildHeaderMenu() { ImageIcon emptyIcon = new ImageIcon(new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB)); final JPopupMenu headerMenu = new JPopupMenu(); JMenuItem menuItem = new JMenuItem("Add Column"); menuItem.setIcon(emptyIcon); headerMenu.add(menuItem).addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { PlaylistColumn column = new PlaylistColumn(); ColumnDialog dialog = new ColumnDialog(getParentFrame(), "Add Column", column); if (dialog.showDialog()) { saveColumns(); columns.add(column); createDefaultColumnsFromModel(); } } }); headerMenu.add(new JMenuItem("Edit Column")).addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (selectedColumn == null) return; PlaylistColumn column = columns.get(selectedColumn.getModelIndex()); ColumnDialog dialog = new ColumnDialog(getParentFrame(), "Edit Column", column); if (dialog.showDialog()) { selectedColumn.setHeaderValue(column.getName()); update(); } } }); headerMenu.add(new JMenuItem("Remove Column")).addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (selectedColumn == null) return; saveColumns(); getTableHeader().setDraggedColumn(null); columns.remove(selectedColumn.getModelIndex()); // trying to fix ArrayIndexOutOfBoundException RepaintManager.currentManager(getTableHeader()).markCompletelyClean(getTableHeader()); createDefaultColumnsFromModel(); } }); headerMenu.addSeparator(); final JCheckBoxMenuItem fitColumns = new JCheckBoxMenuItem("Auto-scale Columns"); headerMenu.add(fitColumns).addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { config.setInt("gui.playlist.autoResizeMode", fitColumns.isSelected() ? AUTO_RESIZE_SUBSEQUENT_COLUMNS : AUTO_RESIZE_OFF); } }); fitColumns.setSelected( AUTO_RESIZE_OFF != config.getInt("gui.playlist.autoResizeMode", AUTO_RESIZE_OFF)); Util.fixIconTextGap(headerMenu); return headerMenu; } private JPopupMenu buildTableMenu() { final ActionMap aMap = getActionMap(); final JPopupMenu tableMenu = new JPopupMenu(); final JTable owner = this; JMenuItem item; ArrayList<Track> selectedTracks = getSelectedSongs(); ActionListener listener = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { String cmd = e.getActionCommand(); Action action = aMap.get(cmd); if (action != null) action.actionPerformed(new ActionEvent(owner, ActionEvent.ACTION_PERFORMED, null)); } }; boolean selectionNotEmpty = !selectedTracks.isEmpty(); if (selectionNotEmpty) { tableMenu.add(aMap.get("removeSelected")).setAccelerator(KeyStroke.getKeyStroke("DELETE")); tableMenu.addSeparator(); } item = tableMenu.add("Cut"); item.setEnabled(selectionNotEmpty); item.setAccelerator(KeyStroke.getKeyStroke("ctrl X")); item.addActionListener(listener); item.setActionCommand((String) TransferHandler.getCutAction().getValue(Action.NAME)); item = tableMenu.add("Copy"); item.setAccelerator(KeyStroke.getKeyStroke("ctrl C")); item.setEnabled(selectionNotEmpty); item.addActionListener(listener); item.setActionCommand((String) TransferHandler.getCopyAction().getValue(Action.NAME)); item = tableMenu.add("Paste"); item.setAccelerator(KeyStroke.getKeyStroke("ctrl V")); item.setEnabled(Toolkit.getDefaultToolkit().getSystemClipboard().isDataFlavorAvailable(SongsSelection.getFlavor())); item.addActionListener(listener); item.setActionCommand((String) TransferHandler.getPasteAction().getValue(Action.NAME)); tableMenu.addSeparator(); if (selectionNotEmpty) { TracksMenu tracksMenu = new TracksMenu(); JPopupMenu menu = tracksMenu.create(this, playlist, selectedTracks); for (Component component : menu.getComponents()) { tableMenu.add(component); } } else { item = tableMenu.add("Add Files"); item.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { TreeFileChooser fc = new TreeFileChooser(PlaylistTable.this, "Add Files", true); File[] files = fc.showOpenDialog(); if (files != null) { ProgressDialog dialog = new ProgressDialog(PlaylistTable.this, "Adding Files"); dialog.show(new Task.FileAddingTask(getPlaylist(), files, getPlaylist().size())); } } }); item = tableMenu.add("Add Location"); item.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { String ret = JOptionPane.showInputDialog(getParentFrame(), "Enter URL", "Add Location", JOptionPane.QUESTION_MESSAGE); if (!Util.isEmpty(ret)) { getPlaylist().insertItem(ret, -1, false, null); } } }); } Util.fixIconTextGap(tableMenu); return tableMenu; } class PlaylistModel extends AbstractTableModel { public int getRowCount() { return playlist == null ? 0 : playlist.size(); } public int getColumnCount() { return columns.size(); } @Override public String getColumnName(int column) { return columns.get(column).getName(); } public Object getValueAt(int rowIndex, int columnIndex) { Track track = playlist.get(rowIndex); if (track instanceof Separator) return track; else return columns.get(columnIndex).getValue(track); } } public void saveColumns() { for (int i = 0; i < getColumnModel().getColumnCount(); i++) { TableColumn tc = getColumnModel().getColumn(i); PlaylistColumn pc = columns.get(tc.getModelIndex()); pc.setPosition(i); pc.setSize(tc.getWidth()); } Collections.sort(columns); } }