// This file is part of Penn TotalRecall <http://memory.psych.upenn.edu/TotalRecall>.
//
// TotalRecall 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, version 3 only.
//
// TotalRecall 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 TotalRecall. If not, see <http://www.gnu.org/licenses/>.
package components.audiofiles;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JList;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import components.MyFrame;
import control.CurAudio;
/**
* A <code>JList</code> for displaying the available <code>AudioFiles</code>.
*
* @author Yuvi Masory
*/
public class AudioFileList extends JList implements FocusListener {
private static AudioFileList instance;
private AudioFileListModel model;
private AudioFileListCellRenderer render;
/**
* Constructs an <code>AudioFileList</code>, initializing mouse listeners, key bindings, selection mode, cell renderer, and model.
*/
private AudioFileList() {
model = new AudioFileListModel();
setModel(model);
//set the cell renderer that will display incomplete/complete AudioFiles differently
render = new AudioFileListCellRenderer();
setCellRenderer(render);
//at this point only one audio file can be selected a time, changing to multiple selection mode would require
//a (small) rewrite of popup menus, key bindings and mouse listeners
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
setLayoutOrientation(JList.VERTICAL);
//this mouse listener handles context menus and double clicks to switch files
addMouseListener(new AudioFileListMouseAdapter(this));
//focus listener makes the containing AudioFileDisplay look focused at the appropriate times
addFocusListener(this);
//users can remove an AudioFile from the display by hitting delete or backspace (necessary for mac which conflates the two)
//technically this code is duplicated in the AudioFilePopupMenu code, but it's so simple (one line after AudioFile is identified) that it's not worth
//making a separate removal action that both will call
getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0, false), "remove file");
getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0, false), "remove file");
getActionMap().put("remove file", new AbstractAction(){
public void actionPerformed(ActionEvent e) {
Object[] objs = getSelectedValues();
int index = getSelectedIndex();
if(index < 0) {
return;
}
if(objs.length == 0) {
return;
}
if(objs.length == 1) {//in case multiple selection mode is used in the future
AudioFile ff = (AudioFile)objs[0];
if(ff == null) {
return;
}
if(CurAudio.audioOpen()) {
if(CurAudio.getCurrentAudioFileAbsolutePath().equals(ff.getAbsolutePath())) {
return;
}
}
model.removeElementAt(index);
}
}
});
//hitting enter can be used to switch to a file on the list
//again, technically this code is duplicated by double click handler in AudioFileListMouseAdapter, both the logic is too simple to justify
//writing a separate action that both will call
getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "switch");
getActionMap().put("switch", new AbstractAction(){
public void actionPerformed(ActionEvent e) {
Object[] objs = getSelectedValues();
if(objs.length == 1) {//in case multiple selection mode is used in the future
AudioFileDisplay.askToSwitchFile((AudioFile)objs[0]);
}
}
});
//overrides JScrollPane key bindings for the benefit of SeekAction's key bindings
getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "none");
getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "none");
getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.SHIFT_DOWN_MASK, false), "none");
getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.SHIFT_DOWN_MASK, false), "none");
//since the AudioFileList is a clickable area, we must write focus handling code for the event it is clicked on
addMouseListener(new MouseAdapter(){
@Override
public void mousePressed(MouseEvent e) {
if(isFocusable()) {
//automatically takes focus in this case
}
else {
MyFrame.getInstance().requestFocusInWindow();
}
}
});
}
/**
* Type-refined implementation that guarantees an <code>AudioFileListModel</code> instead of <code>ListModel</code>
*
* @return The <code>AudioFileListModel</code> associated with the <code>AudioFileList</code>
*/
@Override
public AudioFileListModel getModel() {
return model;
}
/**
* Type-refined implementation that guarantees an <code>AudioFileListCellRenderer</code> instead of <code>ListCellRenderer</code>
*
* @return The <code>AudioFileListCellRenderer</code> associated with the <code>AudioFileList</code>
*/
@Override
public AudioFileListCellRenderer getCellRenderer() {
return render;
}
/**
* Custom focusability condition that behaves in the default manner aside from rejecting focus when this <code>AudioFileList</code> has no elements.
*
* @return Whether or nut this component should accept focus
*/
@Override
public boolean isFocusable() {
return(super.isFocusable() && model.getSize() > 0);
}
/**
* Handler for the event that this <code>AudioFileList</code> gains focus.
*/
public void focusGained(FocusEvent e) {
int anchor = getAnchorSelectionIndex();
if(anchor >= 0) {
setSelectedIndex(anchor);
}
else {
setSelectedIndex(0);
}
}
/**
* Handler for event that this <code>AudioFileList</code> loses focus.
*/
public void focusLost(FocusEvent e) {
if(e.isTemporary() == false) {
clearSelection();
}
}
/**
* Gets a reference to this object for use by a custom <code>FocusTraversalPolicy</code>.
*
* <p>Unfortunately this requires a break from the encapsulation strategy of <code>AudioFileDisplay</code> containing all the <code>public</code> access.
* Please do NOT abuse this method to access the <code>AudioFileList</code> for purposes other than those intended.
* Add new public features to <code>AudioFileDisplay</code> which can then use {@linkplain #getInstance()} as needed.
*
* @return {@link #getInstance()}
*/
public static AudioFileList getFocusTraversalReference() {
return getInstance();
}
/**
* Singleton accessor.
*
* Many classes in this package require access to this object, so a singleton accessor strategy is used to avoid the need
* to pass every class a reference to this object.
*
* @return The singleton <code>AudioFileList</code>
*/
protected static AudioFileList getInstance() {
if(instance == null) {
instance = new AudioFileList();
}
return instance;
}
}