// 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 info.Constants;
import info.GUIConstants;
import info.MyShapes;
import info.UserPrefs;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.util.ArrayList;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.KeyStroke;
import util.GiveMessage;
import components.MyFrame;
import components.audiofiles.AudioFile.AudioFilePathException;
import control.CurAudio;
/**
* A custom interface component for displaying the available audio files to the user.
*
* <p>Note: Access to this component from outside the package is limited to the public static methods provided in this class.
* Code outside the package cannot and should not try to access the internal list, model, or other components directly.
*
* @author Yuvi Masory
*/
public class AudioFileDisplay extends JScrollPane {
private static final String title = "Audio Files";
private static AudioFileDisplay instance;
private static AudioFileList list;
/**
* Creates a new instance of the component, initializing internal components, key bindings, listeners, and various aspects of appearance.
*/
private AudioFileDisplay() {
list = AudioFileList.getInstance();
getViewport().setView(list);
setPreferredSize(GUIConstants.soundFileDisplayDimension);
setMaximumSize(GUIConstants.soundFileDisplayDimension);
setBorder(MyShapes.createMyUnfocusedTitledBorder(title));
//overrides JScrollPane key bindings for the benefit of SeekAction's key bindings
getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "none");
getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "none");
//since AudioFileDisplay is a clickable area, we must write focus handling code for the event it is clicked on
//this case is rare, since only a very small amount of this component is exposed (the area around the border title), the rest being obscured by the AudioFileList
//JScrollPane passes focus to JList if focusable, and to the frame otherwise
addMouseListener(new MouseAdapter(){
@Override
public void mousePressed(MouseEvent e) {
if(list.isFocusable()) {
list.requestFocusInWindow();
}
else {
MyFrame.getInstance().requestFocusInWindow();
}
}
});
}
/**
* Adds files to the <code>AudioFileList</code>, but only if ones that are regular files with supported file extensions.
*
* <code>AudioFiles</code> that don't exist, or are already displayed in this component are automatically filtered out, so this does not need to be checked in advance by the caller.
*
* @param files Candidate files to be added to the <code>AudioFileList</code>
* @return <code>true</code> if any of the files were ultimately added
*/
public static boolean addFilesIfSupported(File[] files) {
ArrayList<AudioFile> supportedFiles = new ArrayList<AudioFile>();
for(File f: files) {
if(f.isFile()) { //this also filters files that don't actually exist, filtering of duplicate files is handled by the AudioFileListModel
if(extensionSupported(f.getName())) {
AudioFile af;
try {
af = new AudioFile(f.getAbsolutePath());
}
catch (AudioFilePathException e) {
e.printStackTrace();
GiveMessage.errorMessage(e.getMessage());
continue;
}
supportedFiles.add(af);
}
}
}
if(supportedFiles.size() > 0) {
list.getModel().addElements(supportedFiles);
return true;
}
else {
return false;
}
}
/**
* Singleton accessor.
*
* @return The singleton <code>AudioFileDisplay</code>
*/
public static AudioFileDisplay getInstance() {
if (instance == null) {
instance = new AudioFileDisplay();
}
return instance;
}
/**
* Switches to the provided <code>File</code>, but only after asking the user for confirmation if the current user's preferences demand such a warning.
*
* Keep in mind the user may decline to switch file.
* Also, if the provided file is already done being annotated, the switch will automatically be rejected without user being prompted.
*
* @param file The file that may be switched to
* @return <code>true</code> iff the file switch took place
*/
protected static boolean askToSwitchFile(AudioFile file) {
if(file.isDone()) {
return false;
}
if(CurAudio.audioOpen()) {
if(file.getAbsolutePath().equals(CurAudio.getCurrentAudioFileAbsolutePath())) {
return true;
}
boolean shouldWarn = UserPrefs.prefs.getBoolean(UserPrefs.warnFileSwitch, UserPrefs.defaultWarnFileSwitch);
if(shouldWarn) {
JCheckBox checkbox = new JCheckBox(GUIConstants.dontShowAgainString);
String message = "Switch to file " + file.toString() + "?\nYour changes to the current file will not be lost.";
Object[] params = {message, checkbox};
int response = JOptionPane.showConfirmDialog(MyFrame.getInstance(), params,
GUIConstants.yesNoDialogTitle, JOptionPane.YES_NO_OPTION);
boolean dontShow = checkbox.isSelected();
if(dontShow && response != JOptionPane.CLOSED_OPTION) {
UserPrefs.prefs.putBoolean(UserPrefs.warnFileSwitch, false);
}
if(response != JOptionPane.YES_OPTION) {
return false;
}
}
}
CurAudio.switchFile(file);
return true;
}
/**
* Decides whether a <code>File</code> is supported by comparing its extension to the collection of supported extensions in {@link Constants#audioFormatsLowerCase}, ignoring case.
*
* @param name The name of the file, from <code>File.getName()</code>
* @return <code>true</code> iff the the <code>name</code> parameter ends with a supported extension
*/
private static boolean extensionSupported(String name) {
for(String ext: Constants.audioFormatsLowerCase) {
if(name.toLowerCase().endsWith(ext)) {
return true;
}
}
return false;
}
}