/* * Jajuk * Copyright (C) The Jajuk Team * http://jajuk.info * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * */ package org.jajuk.ui.views; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Properties; import java.util.Set; import javax.swing.BorderFactory; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JToolBar; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.event.ListSelectionEvent; import net.miginfocom.swing.MigLayout; import org.jajuk.base.File; import org.jajuk.base.FileManager; import org.jajuk.base.Playlist; import org.jajuk.base.SmartPlaylist; import org.jajuk.events.JajukEvent; import org.jajuk.events.JajukEvents; import org.jajuk.events.ObservationManager; import org.jajuk.services.players.QueueModel; import org.jajuk.services.players.StackItem; import org.jajuk.ui.actions.ActionManager; import org.jajuk.ui.actions.JajukActions; import org.jajuk.ui.helpers.ILaunchCommand; import org.jajuk.ui.helpers.IndexHighlighterPredicate; import org.jajuk.ui.helpers.JajukTableModel; import org.jajuk.ui.helpers.PlaylistEditorTransferHandler; import org.jajuk.ui.helpers.PlaylistTableModel; import org.jajuk.ui.widgets.JajukButton; import org.jajuk.ui.widgets.JajukJToolbar; import org.jajuk.ui.widgets.JajukTable; import org.jajuk.ui.widgets.JajukToggleButton; import org.jajuk.util.Conf; import org.jajuk.util.Const; import org.jajuk.util.IconLoader; import org.jajuk.util.JajukIcons; import org.jajuk.util.Messages; import org.jajuk.util.UtilFeatures; import org.jajuk.util.error.JajukException; import org.jajuk.util.log.Log; import org.jdesktop.swingx.decorator.ColorHighlighter; import org.jdesktop.swingx.decorator.ComponentAdapter; import org.jdesktop.swingx.decorator.HighlightPredicate; import org.pushingpixels.substance.api.DecorationAreaType; import org.pushingpixels.substance.api.SubstanceColorScheme; import org.pushingpixels.substance.api.SubstanceLookAndFeel; import org.pushingpixels.substance.api.SubstanceSkin; /** * Adapter for playlists editors *. */ public class QueueView extends PlaylistView { /** Generated serialVersionUID. */ private static final long serialVersionUID = -2851288035506442507L; private JScrollPane jsp; private JajukToggleButton jtbAutoScroll; /** Last scrolled-item *. */ private StackItem lastScrolledItem; /** Stop after button. */ private JajukToggleButton jtbStopAfter; /** Action when user clicks on stop after. */ private ActionListener alStopAfter = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { QueueModel.setStopAfter(jtbStopAfter.isSelected()); } }; /** Action for auto scrolling. */ private ActionListener alAutoScroll = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { Conf.setProperty(Const.CONF_AUTO_SCROLL, Boolean.toString(jtbAutoScroll.isSelected())); if (jtbAutoScroll.isSelected()) { autoScroll(); } } }; /* * (non-Javadoc) * * @see org.jajuk.ui.IView#display() */ @Override public void initUI() { plf = new SmartPlaylist(Playlist.Type.QUEUE, Integer.toString(Playlist.Type.QUEUE.ordinal()), null, null); // Control panel jpEditorControl = new JPanel(); jpEditorControl.setBorder(BorderFactory.createEtchedBorder()); jbSave = new JajukButton(IconLoader.getIcon(JajukIcons.SAVE)); jbSave.setToolTipText(Messages.getString("AbstractPlaylistEditorView.3")); jbSave.addActionListener(this); jbRemove = new JajukButton(IconLoader.getIcon(JajukIcons.REMOVE)); jbRemove.setToolTipText(Messages.getString("AbstractPlaylistEditorView.5")); jbRemove.addActionListener(this); jbUp = new JajukButton(IconLoader.getIcon(JajukIcons.UP)); jbUp.setToolTipText(Messages.getString("AbstractPlaylistEditorView.6")); jbUp.addActionListener(this); jbDown = new JajukButton(IconLoader.getIcon(JajukIcons.DOWN)); jbDown.setToolTipText(Messages.getString("AbstractPlaylistEditorView.7")); jbDown.addActionListener(this); jbAddShuffle = new JajukButton(IconLoader.getIcon(JajukIcons.ADD_SHUFFLE)); jbAddShuffle.setToolTipText(Messages.getString("AbstractPlaylistEditorView.10")); jbAddShuffle.addActionListener(this); jlTitle = new JLabel(" [" + QueueModel.getQueue().size() + "]"); jbClear = new JajukButton(IconLoader.getIcon(JajukIcons.CLEAR)); jbClear.setToolTipText(Messages.getString("QueueView.1")); jbClear.addActionListener(this); jtbStopAfter = new JajukToggleButton(IconLoader.getIcon(JajukIcons.STOP_AFTER)); jtbStopAfter.setToolTipText(Messages.getString("QueueView.3")); jtbStopAfter.addActionListener(alStopAfter); jtbAutoScroll = new JajukToggleButton(IconLoader.getIcon(JajukIcons.AUTOSCROLL)); jtbAutoScroll.setToolTipText(Messages.getString("QueueView.2")); jtbAutoScroll.setSelected(Conf.getBoolean(Const.CONF_AUTO_SCROLL)); jtbAutoScroll.addActionListener(alAutoScroll); JToolBar jtb = new JajukJToolbar(); jtb.add(jbSave); jtb.add(jbRemove); jtb.add(jbAddShuffle); jtb.add(jbUp); jtb.add(jbDown); jtb.add(jbClear); // Add items jpEditorControl.setLayout(new MigLayout("insets 5", "[][grow][]")); jpEditorControl.add(jtb, "left,gapright 15::"); jpEditorControl.add(jlTitle, "center,gapright 5,grow"); jpEditorControl.add(jtbStopAfter, "right"); jpEditorControl.add(jtbAutoScroll, "right"); editorModel = new PlaylistTableModel(true); editorTable = new JajukTable(editorModel, CONF_QUEUE_COLUMNS); editorModel.populateModel(editorTable.getColumnsConf()); editorTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); // multi-row // selection editorTable.setSortable(false); editorTable.setTransferHandler(new PlaylistEditorTransferHandler(editorTable)); setRenderers(); // just an icon editorTable.getColumnModel().getColumn(0).setPreferredWidth(20); editorTable.getColumnModel().getColumn(0).setMaxWidth(20); editorTable.getTableHeader().setPreferredSize(new Dimension(0, 20)); editorTable.showColumns(editorTable.getColumnsConf()); setLayout(new BorderLayout()); add(jpEditorControl, BorderLayout.NORTH); jsp = new JScrollPane(editorTable); jsp.setBorder(BorderFactory.createEmptyBorder(0, 1, 0, 0)); add(jsp, BorderLayout.CENTER); // menu items jmiFilePlay = new JMenuItem(Messages.getString("TracksTableView.7"), IconLoader.getIcon(JajukIcons.PLAY_16X16)); // We don't use regular action for the play because it has very special // behavior here in the queue view : it must go to selection without // keeping previous FIFO jmiFilePlay.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { goToSelection(); } }); initMenuItems(); //handle track removing for the queue when deleting it jmiDelete.addActionListener(this); SubstanceSkin theme = SubstanceLookAndFeel.getCurrentSkin(); SubstanceColorScheme scheme = theme.getActiveColorScheme(DecorationAreaType.NONE); Color queueHighlighterColor = null; if (scheme.isDark()) { queueHighlighterColor = scheme.getUltraLightColor(); } else { queueHighlighterColor = scheme.getLineColor(); } ColorHighlighter colorHighlighter = new ColorHighlighter(new IndexHighlighterPredicate(), queueHighlighterColor, null); editorTable.addHighlighter(colorHighlighter); HighlightPredicate hpPlaying = new HighlightPredicate() { @Override public boolean isHighlighted(Component renderer, ComponentAdapter adapter) { int convertedRow = editorTable.convertRowIndexToModel(adapter.row); return QueueModel.isPlayingTrack() && convertedRow == QueueModel.getIndex(); } }; ColorHighlighter playHighlighter = new ColorHighlighter(hpPlaying, Color.ORANGE, null); editorTable.addHighlighter(playHighlighter); // register events ObservationManager.register(this); // Add key listener to enable row suppression using SUPR key editorTable.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { // The fact that a selection can be removed or not is // in the jbRemove state if (e.getKeyCode() == KeyEvent.VK_DELETE && jbRemove.isEnabled()) { removeAction(); } } }); // Add specific behavior on left click editorTable.setCommand(new ILaunchCommand() { @Override public void launch(int nbClicks) { int iSelectedCol = editorTable.getSelectedColumn(); // Convert column selection as columns may have been moved iSelectedCol = editorTable.convertColumnIndexToModel(iSelectedCol); // double click, launches selected track and all after if (nbClicks == 2 // click on play icon || (nbClicks == 1 && iSelectedCol == 0)) { StackItem item = editorModel.getStackItem(editorTable.getSelectedRow()); if (item.isPlanned()) { item.setPlanned(false); item.setRepeat(Conf.getBoolean(Const.CONF_STATE_REPEAT)); item.setUserLaunch(true); QueueModel.push(item, Conf.getBoolean(Const.CONF_OPTIONS_PUSH_ON_CLICK)); } else { // non planned items goToSelection(); } } } }); // see JajukTable code, all the event code is centralized over there editorTable.addListSelectionListener(this); // Register keystrokes over table super.setKeystrokes(); // Force a first need refresh event update(new JajukEvent(JajukEvents.QUEUE_NEED_REFRESH)); } /** * Go to selected row, do it asynchronously because FIFO.goTO() can freeze * the GUI */ private void goToSelection() { new Thread("Queue Selection Thread") { @Override public void run() { try { QueueModel.goTo(editorTable.getSelectedRow()); // remove selection for planned tracks ListSelectionModel lsm = editorTable.getSelectionModel(); editorModel.setRefreshing(true); editorTable.getSelectionModel().removeSelectionInterval(lsm.getMinSelectionIndex(), lsm.getMaxSelectionIndex()); } catch (Exception e) { Log.error(e); } finally { editorModel.setRefreshing(false); } } }.start(); } /* * (non-Javadoc) * * @see org.jajuk.ui.views.PlaylistView#getRegistrationKeys() */ @Override public Set<JajukEvents> getRegistrationKeys() { Set<JajukEvents> eventSubjectSet = new HashSet<JajukEvents>(); eventSubjectSet.add(JajukEvents.QUEUE_NEED_REFRESH); eventSubjectSet.add(JajukEvents.DEVICE_REFRESH); eventSubjectSet.add(JajukEvents.DEVICE_MOUNT); eventSubjectSet.add(JajukEvents.DEVICE_UNMOUNT); eventSubjectSet.add(JajukEvents.CUSTOM_PROPERTIES_ADD); eventSubjectSet.add(JajukEvents.CUSTOM_PROPERTIES_REMOVE); eventSubjectSet.add(JajukEvents.VIEW_REFRESH_REQUEST); eventSubjectSet.add(JajukEvents.RATE_CHANGED); eventSubjectSet.add(JajukEvents.PARAMETERS_CHANGE); eventSubjectSet.add(JajukEvents.PLAYER_STOP); return eventSubjectSet; } /* * (non-Javadoc) * * @see org.jajuk.ui.IView#getDesc() */ @Override public String getDesc() { return Messages.getString("PlaylistFileItem.5"); } /* * (non-Javadoc) * * @see org.jajuk.ui.Observer#update(java.lang.String) */ @Override public void update(final JajukEvent event) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { try { JajukEvents subject = event.getSubject(); editorTable.setAcceptColumnsEvents(false); // flag reloading if (JajukEvents.QUEUE_NEED_REFRESH.equals(subject) || JajukEvents.DEVICE_REFRESH.equals(subject) || JajukEvents.DEVICE_MOUNT.equals(subject) || JajukEvents.DEVICE_UNMOUNT.equals(subject) || JajukEvents.RATE_CHANGED.equals(subject) || JajukEvents.PARAMETERS_CHANGE.equals(subject)) { refreshQueue(); // Only scroll if song actually changed, otherwise, any // queue refresh // would scroll and annoy users if (Conf.getBoolean(CONF_AUTO_SCROLL) && QueueModel.getCurrentItem() != null && !QueueModel.getCurrentItem().equals(lastScrolledItem)) { autoScroll(); lastScrolledItem = QueueModel.getCurrentItem(); } } else if (JajukEvents.CUSTOM_PROPERTIES_ADD.equals(subject)) { Properties properties = event.getDetails(); if (properties == null) { // can be null at view populate return; } // create a new model editorModel = new PlaylistTableModel(true); editorModel.populateModel(editorTable.getColumnsConf()); editorTable.setModel(editorModel); setRenderers(); editorTable.addColumnIntoConf((String) properties.get(Const.DETAIL_CONTENT)); editorTable.showColumns(editorTable.getColumnsConf()); editorModel.getItems().clear(); editorModel.getPlanned().clear(); refreshQueue(); } else if (JajukEvents.CUSTOM_PROPERTIES_REMOVE.equals(subject)) { Properties properties = event.getDetails(); if (properties == null) { // can be null at view // populate return; } editorModel = new PlaylistTableModel(true); editorModel.populateModel(editorTable.getColumnsConf()); editorTable.setModel(editorModel); setRenderers(); // remove item from configuration cols editorTable.removeColumnFromConf((String) properties.get(Const.DETAIL_CONTENT)); editorTable.showColumns(editorTable.getColumnsConf()); editorModel.getItems().clear(); editorModel.getPlanned().clear(); refreshQueue(); } else if (JajukEvents.VIEW_REFRESH_REQUEST.equals(subject)) { // force filter to refresh if the events has been // triggered by the table itself after a column change JTable table = (JTable) event.getDetails().get(Const.DETAIL_CONTENT); if (table.equals(editorTable)) { editorModel.getItems().clear(); editorModel.getPlanned().clear(); refreshQueue(); } } else if (JajukEvents.PLAYER_STOP.equals(subject)) { // Reset stop after button on a player stop jtbStopAfter.setSelected(false); } } catch (Exception e) { Log.error(e); } finally { editorTable.setAcceptColumnsEvents(true); // Update number of tracks remaining jlTitle.setText(" [" + QueueModel.getQueue().size() + "]"); } } }); } /** * Auto scroll to played track if option is enabled. */ private void autoScroll() { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (QueueModel.getQueueSize() > 0) { double index = QueueModel.getIndex(); double size = QueueModel.getQueueSize() + QueueModel.getPlanned().size(); double factor = (index / size); int value = (int) (factor * jsp.getVerticalScrollBar().getMaximum()); // 'center' played track value -= (jsp.getVerticalScrollBar().getHeight() / 2) - (editorTable.getRowHeight() / 2); if (value < 0) { value = 0; } if (value >= jsp.getVerticalScrollBar().getMinimum() && value <= jsp.getVerticalScrollBar().getMaximum()) { jsp.getVerticalScrollBar().setValue(value); } } } }); } /** * Refresh queue. */ private synchronized void refreshQueue() { // when nothing is selected, set default button state if (editorTable.getSelectionModel().getMinSelectionIndex() == -1) { setDefaultButtonState(); } editorModel.setItems(QueueModel.getQueue()); editorModel.setPlanned(QueueModel.getPlanned()); ((JajukTableModel) editorTable.getModel()).populateModel(editorTable.getColumnsConf()); // save selection to avoid reseting selection the user is doing int[] rows = editorTable.getSelectedRows(); try { editorModel.setRefreshing(true); // force table refresh editorModel.fireTableDataChanged(); for (int element : rows) { // set saved selection after a refresh editorTable.getSelectionModel().addSelectionInterval(element, element); } // Refresh menu item along with preference icon. This is only useful when // a user change the preference and display again the menu (right click) again // without changing the selection. pjmFilesEditor.resetUI(editorTable.getSelection()); } finally { editorModel.setRefreshing(false); } } /** * Set default button state. */ private void setDefaultButtonState() { // set buttons jbUp.setEnabled(false); // set it to false just for startup // because nothing is selected jbDown.setEnabled(false); // set it to false just for startup // because nothing is selected jbAddShuffle.setEnabled(true);// add at the FIFO end by // default even with no // selection jbRemove.setEnabled(false); // set it to false just for startup // because cursor is over first track // and it can't be removed in queue mode // disable prepare party for queue playlist } /* * (non-Javadoc) * * @see * org.jajuk.ui.views.PlaylistView#actionPerformed(java.awt.event.ActionEvent * ) */ @Override public void actionPerformed(ActionEvent ae) { try { if (ae.getSource() == jbSave) { // special playlist, same behavior than a save as // (to be done in a thread because saveAs() uses invokeAndWait()) new Thread("SaveAsAction") { @Override public void run() { try { plf.saveAs(); } catch (JajukException je) { Log.error(je); Messages.showErrorMessage(je.getCode()); } catch (Exception ex) { Log.error(ex); } } }.start(); // notify playlist repository to refresh ObservationManager.notify(new JajukEvent(JajukEvents.DEVICE_REFRESH)); } else if (ae.getSource() == jbDown || ae.getSource() == jbUp || ae.getSource() == jmiFileDown || ae.getSource() == jmiFileUp) { int iRow = editorTable.getSelectedRow(); if (iRow != -1) { // -1 means nothing is selected if (ae.getSource() == jbDown || ae.getSource() == jmiFileDown) { plf.down(iRow); if (iRow < editorTable.getModel().getRowCount() - 1) { // force immediate table refresh refreshQueue(); editorTable.getSelectionModel().setSelectionInterval(iRow + 1, iRow + 1); } } else if (ae.getSource() == jbUp || ae.getSource() == jmiFileUp) { plf.up(iRow); if (iRow > 0) { // force immediate table refresh refreshQueue(); editorTable.getSelectionModel().setSelectionInterval(iRow - 1, iRow - 1); } } } } else if (ae.getSource() == jbRemove || ae.getSource() == jmiFileRemove) { removeAction(); } else if (ae.getSource() == jbAddShuffle) { int iRow = editorTable.getSelectedRow(); if (iRow < 0 // no row is selected, add to the end || iRow > QueueModel.getQueue().size()) { // row can be on planned track if user select a planned // track and if // fifo is reduced after tracks have been played iRow = QueueModel.getQueue().size(); } File file = FileManager.getInstance().getShuffleFile(); List<File> files = new ArrayList<File>(); files.add(file); QueueModel.insert( UtilFeatures.createStackItems(files, Conf.getBoolean(Const.CONF_STATE_REPEAT), true), iRow); refreshQueue(); } else if (ae.getSource() == jbClear) { // Reset the FIFO QueueModel.reset(); // reinit all variables try { ActionManager.getAction(JajukActions.STOP_TRACK).perform(null); ObservationManager.notify(new JajukEvent(JajukEvents.ZERO)); } catch (Exception e) { Log.error(e); } } else if (ae.getSource() == jmiDelete) { // We remove the track. It has already been deleted in the DELETE action. removeAction(); } } catch (Exception e2) { Log.error(e2); } } private void removeAction() { int[] selectedRows = editorTable.getSelectedRows(); removeItems(selectedRows); // Update selection : if multiple selection, remove selection, for multiple selection, we disable buttons after the actions if (selectedRows.length > 1) { editorTable.getSelectionModel().removeIndexInterval(0, editorTable.getRowCount() - 1); setDefaultButtonState(); } // No queue refresh as it will be triggered indirectly by the QUEUE_NEED_REFRESH events updateInformationPanel(getSelectedFiles()); } protected void removeItems(int[] selectedRows) { // Remove items HashSet<Integer> rows = new HashSet<Integer>(); for (Integer i : selectedRows) { rows.add(i); } plf.remove(rows); // Refresh queue asynchronously to serialize refresh due to remove() call. This is necessary only for the case where // we drop a non-playing track. ObservationManager.notify(new JajukEvent(JajukEvents.QUEUE_NEED_REFRESH)); } /** * Called when table selection changed. * * @param e * */ @Override public void valueChanged(ListSelectionEvent e) { ListSelectionModel selection = (ListSelectionModel) e.getSource(); updateButtonsStateForSelection(selection.getMinSelectionIndex(), selection.getMaxSelectionIndex()); updateInformationPanel(getSelectedFiles()); } private void updateButtonsStateForSelection(int min, int max) { // Refresh the preference menu according to the selection pjmFilesEditor.resetUI(editorTable.getSelection()); int selectedRow = max; // true if selected line is a planned track boolean bPlanned = false; if (selectedRow > editorModel.getItems().size() - 1) { // means it is a planned track bPlanned = true; } // -- now analyze each button -- // Remove button jbRemove.setEnabled(!bPlanned); jmiFileRemove.setEnabled(!bPlanned); // Add shuffle button // No adding for planned track jbAddShuffle.setEnabled(!bPlanned); // Up button if (min != max) { // check if several rows have been selected jbUp.setEnabled(false); jmiFileUp.setEnabled(false); } else { // still here ? if (bPlanned) { // No up/down buttons for planned tracks jbUp.setEnabled(false); jmiFileUp.setEnabled(false); } else { // normal item if (min == 0) { // already at the top jbUp.setEnabled(false); jmiFileUp.setEnabled(false); } else { jbUp.setEnabled(true); jmiFileUp.setEnabled(true); } } } // Down button if (min != max) { // check if several rows have been selected : // doesn't supported yet jbDown.setEnabled(false); jmiFileDown.setEnabled(false); } else { // yet here ? if (bPlanned) { // No up/down buttons for planned tracks jbDown.setEnabled(false); jmiFileDown.setEnabled(false); } else { // normal item if (max < editorModel.getItems().size() - 1) { // a normal item can't go in the planned items jbDown.setEnabled(true); jmiFileDown.setEnabled(true); } else { jbDown.setEnabled(false); jmiFileDown.setEnabled(false); } } } } }