/*
* Song.java
*
* Copyright (C) 2006-2014 Gabriel Burca (gburca dash virtmus at ebixio dot com)
*
* 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 (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 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package com.ebixio.virtmus;
import com.ebixio.util.Log;
import com.ebixio.util.NotifyUtil;
import com.ebixio.util.NumberRange;
import com.ebixio.util.PropertyChangeSupportUnique;
import com.ebixio.util.Util;
import com.ebixio.virtmus.filefilters.SongFilter;
import com.ebixio.virtmus.imgsrc.GenericImg;
import com.ebixio.virtmus.imgsrc.IcePdfImg;
import com.ebixio.virtmus.imgsrc.ImgSrc;
import com.ebixio.virtmus.imgsrc.PdfImg;
import com.ebixio.virtmus.imgsrc.PdfViewImg;
import com.ebixio.virtmus.options.Options;
import com.ebixio.virtmus.shapes.VmShape;
import com.ebixio.virtmus.stats.StatsCollector;
import com.ebixio.virtmus.xml.MusicPageConverter;
import com.ebixio.virtmus.xml.PageOrderConverter;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
import com.thoughtworks.xstream.io.xml.TraxSource;
import java.awt.Component;
import java.awt.Frame;
import java.awt.Graphics;
import java.beans.PropertyChangeListener;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import javax.swing.Icon;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.icepdf.core.exceptions.PDFException;
import org.icepdf.core.exceptions.PDFSecurityException;
import org.netbeans.spi.actions.AbstractSavable;
import org.openide.filesystems.FileObject;
import org.openide.loaders.SaveAsCapable;
import org.openide.util.Exceptions;
import org.openide.util.ImageUtilities;
import org.openide.util.NbPreferences;
import org.openide.windows.WindowManager;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
*
* @author Gabriel Burca <gburca dash virtmus at ebixio dot com>
*/
@XStreamAlias("song")
public class Song implements Comparable<Song> {
@XStreamAlias("Pages")
public final List<MusicPage> pageOrder = Collections.synchronizedList(new ArrayList<MusicPage>());
@XStreamAlias("Name")
private String name = null;
@XStreamAlias("Tags")
private String tags = null;
@XStreamAlias("Notes")
private String notes = null;
@XStreamAsAttribute
private String version = MainApp.VERSION; // Used in the XML output
// transients are not initialized when the object is deserialized !!!
private transient File sourceFile = null;
public static final String SONG_FILE_EXT = ".song.xml";
public static final String PROP_TAGS = "tagsProp";
public static final String PROP_NAME = "nameProp";
public static final String PROP_ANNOT = "annotProp"; // Annotations add/rm
public static final String PROP_DIRTY = "dirtyProp";
private transient PropertyChangeSupportUnique pcs = new PropertyChangeSupportUnique(this);
private transient final Object pcsMutex = new Object();
// Could change this to EventListenerList if we had more than 1 event type
private transient List<ChangeListener> pageListeners = Collections.synchronizedList(new LinkedList<ChangeListener>());
/* We should instantiate each song only once.
* That way when a page is added/removed from it the change will be reflected in all playlists containing the song. */
private transient static Map<String, Song> instantiated = Collections.synchronizedMap(new HashMap<String, Song>());
/** Keeps track of de-serializations that are in progress. */
private transient static Map<String, CountDownLatch> inProgress = new HashMap<>();
/** The de-serializer lock. */
private transient static final Object deserLock = new Object();
private static final Icon ICON = ImageUtilities.loadImageIcon(
"com/ebixio/virtmus/resources/SongNode.png", false);
private transient SongSavable savable = null;
private transient static Transformer songXFormer;
static {
InputStream songXform = Song.class.getResourceAsStream("/com/ebixio/virtmus/xml/SongTransform.xsl");
TransformerFactory factory = TransformerFactory.newInstance();
try {
songXFormer = factory.newTransformer(new StreamSource(songXform));
songXFormer.setOutputProperty(OutputKeys.INDENT, "yes");
songXFormer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
} catch (TransformerConfigurationException ex) {
Exceptions.printStackTrace(ex);
}
}
/** Creates a new Song instance.
* This constructor is NOT called when the object is deserialized.
*/
public Song() {
addPropertyChangeListener(PROP_ANNOT, StatsCollector.findInstance());
addPropertyChangeListener(PROP_DIRTY, StatsCollector.findInstance());
}
/** Creates a new instance of Song from a file, or a directory of files.
* This constructor is NOT called when the object is deserialized.
* @param f The file/directory to create the song from.
*/
public Song(File f) {
addPage(f);
addPropertyChangeListener(PROP_ANNOT, StatsCollector.findInstance());
addPropertyChangeListener(PROP_DIRTY, StatsCollector.findInstance());
}
/** Constructors are not called (and transients are not initialized)
* when the object is deserialized !!! */
private Object readResolve() {
pcs = new PropertyChangeSupportUnique(this);
pageListeners = Collections.synchronizedList(new LinkedList<ChangeListener>());
savable = null;
version = MainApp.VERSION;
addPropertyChangeListener(PROP_ANNOT, StatsCollector.findInstance());
addPropertyChangeListener(PROP_DIRTY, StatsCollector.findInstance());
return this;
}
public boolean isDirty() {
return savable != null;
}
public void setDirty(boolean isDirty) {
if (isDirty) {
if (savable == null) {
savable = new SongSavable(this);
VirtMusLookup.getInstance().add(savable);
notifyListeners();
fire(PROP_DIRTY, false, true);
}
} else {
if (savable != null) {
savable.saved();
VirtMusLookup.getInstance().remove(savable);
savable = null;
notifyListeners();
fire(PROP_DIRTY, true, false);
}
}
}
/**
* Present the user with a file-open dialog to select a page image to add to
* this song. The user may select more than one item. Each item is passed to
* {@link Song#addPage(java.io.File)} to be added.
*
* @return true, unless the user canceled out.
*/
public boolean addPage() {
final Frame mainWindow = WindowManager.getDefault().getMainWindow();
final JFileChooser fc = new JFileChooser();
fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
fc.setMultiSelectionEnabled(true);
File sD = this.sourceFile.getParentFile();
if (sD != null && sD.exists()) {
fc.setCurrentDirectory(sD);
} else {
String songDir = NbPreferences.forModule(MainApp.class).get(Options.OptSongDir, "");
sD = new File(songDir);
if (sD.exists()) fc.setCurrentDirectory(sD);
}
int returnVal = fc.showOpenDialog(mainWindow);
if (returnVal == JFileChooser.APPROVE_OPTION) {
File files[] = fc.getSelectedFiles();
for (File f: files) {
addPage(f);
}
return true;
} else {
return false;
}
}
/**
* Adds one or more pages to this song.
* @param f Can be a directory (of images, PDFs, etc...), or a single PDF or
* image file.
*
* @return True if the page was successfully added.
*/
public boolean addPage(File f) {
if (f == null) {
return false;
} else if (f.isDirectory()) {
File[] images = f.listFiles();
for (File image : images) {
if (image.isFile()) {
addPage(image);
}
}
} else if (f.isFile()) {
if (f.getName().toLowerCase().endsWith(".pdf")) {
int pdfPages;
org.icepdf.core.pobjects.Document doc = new org.icepdf.core.pobjects.Document();
try {
doc.setFile(f.getCanonicalPath());
pdfPages = doc.getNumberOfPages();
} catch (IOException | PDFException | PDFSecurityException e) {
pdfPages = 0;
JOptionPane.showMessageDialog(null, e.toString(), "PDF Error", JOptionPane.WARNING_MESSAGE);
}
doc.dispose();
if (pdfPages > 0) {
final Frame mainWindow = WindowManager.getDefault().getMainWindow();
// TODO: Use question icon in dialog
Object pageRangeObj = JOptionPane.showInputDialog(mainWindow,
f.getName() + "\nPage range?",
"PDF pages to add to Song", JOptionPane.QUESTION_MESSAGE,
null, null, "1-" + pdfPages);
if (pageRangeObj == null || ! (pageRangeObj instanceof String)) return false;
NumberRange range = new NumberRange((String)pageRangeObj);
for (int p : range) {
if (p > 0 && p <= pdfPages) {
pageOrder.add(new MusicPageSVG(this, f, p - 1));
}
}
}
} else {
pageOrder.add(new MusicPageSVG(this, f, null));
}
}
setDirty(true);
notifyListeners();
return true;
}
public boolean addPage(MusicPage mp) {
return addPage(mp, -1); // add it at the end
}
public boolean addPage(MusicPage mp, int index) {
if (index < 0 || index > pageOrder.size()) index = pageOrder.size();
pageOrder.add(index, mp);
setDirty(true);
notifyListeners();
return true;
}
public boolean removePage(MusicPage[] mps) {
boolean removed = false;
for (MusicPage mp: mps) {
if (pageOrder.remove(mp)) {
removed = true;
setDirty(true);
}
}
notifyListeners();
return removed;
}
public void reorder(int[] order) {
MusicPage[] mp = new MusicPage[order.length];
for (int i = 0; i < order.length; i++) {
mp[order[i]] = pageOrder.get(i);
}
pageOrder.clear();
pageOrder.addAll(Arrays.asList(mp));
setDirty(true);
notifyListeners();
}
public File getSourceFile() {
return sourceFile;
}
public void setSourceFile(File sourceFile) {
this.sourceFile = sourceFile;
}
public void setName(String name) {
if (!Util.isDifferent(this.name, name)) return;
String oldName = this.name;
this.name = name;
fire(PROP_NAME, oldName, name);
setDirty(true);
notifyListeners();
}
public String getName() {
if (name != null && name.length() > 0) {
return name;
} else if (this.sourceFile != null) {
return Utils.trimExtension(sourceFile.getName(), SONG_FILE_EXT);
} else {
return "No name";
}
}
public void setTags(String tags) {
if (!Util.isDifferent(this.tags, tags)) return;
String oldTags = this.tags;
this.tags = tags;
fire(PROP_TAGS, oldTags, tags);
setDirty(true);
notifyListeners();
}
public String getTags() {
return tags;
}
public void setNotes(String notes) {
if (!Util.isDifferent(this.notes, notes)) return;
this.notes = notes;
setDirty(true);
}
public String getNotes() {
return notes;
}
/** Keeps track of how many annotations were made to this song. Used for
* statistical reports.
* @param page The {@link MusicPage} the annotation was added to.
* @param s The annotation that was added
*/
public void addedAnnot(MusicPage page, VmShape s) {
fire(PROP_ANNOT, null, s);
}
public void removedAnnot(MusicPage page, VmShape s) {
fire(PROP_ANNOT, s, null);
}
public boolean save() {
if (sourceFile == null || !sourceFile.exists() || !sourceFile.isFile()) {
return saveAs();
} else {
return serialize();
}
}
public boolean saveAs() {
return saveAs(null);
}
public boolean saveAs(File currentSel) {
final Frame mainWindow = WindowManager.getDefault().getMainWindow();
final JFileChooser fc = new JFileChooser();
// Take a guess at the directory to save the file to
if (currentSel == null) {
String songDir = NbPreferences.forModule(MainApp.class).get(Options.OptSongDir, "");
currentSel = new File(songDir);
}
if (currentSel.exists() && currentSel.isDirectory()) {
fc.setCurrentDirectory(currentSel);
} else {
fc.setSelectedFile(currentSel);
}
fc.setDialogTitle("Enter the file name for this song.");
fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
fc.addChoosableFileFilter(new SongFilter());
int returnVal = fc.showSaveDialog(mainWindow);
if (returnVal == JFileChooser.APPROVE_OPTION) {
File file = fc.getSelectedFile();
if (! file.toString().endsWith(SONG_FILE_EXT)) {
file = new File(file.toString().concat(SONG_FILE_EXT));
}
if (file.exists()) {
returnVal = JOptionPane.showConfirmDialog(null, "Overwrite existing file?", "Overwrite?", JOptionPane.YES_NO_OPTION);
if (returnVal != JOptionPane.YES_OPTION) {
return false;
}
}
this.sourceFile = file;
return serialize();
} else {
return false;
}
}
public static Song open() {
final Frame mainWindow = WindowManager.getDefault().getMainWindow();
final JFileChooser fc = new JFileChooser();
fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
fc.setMultiSelectionEnabled(false);
String songDir = NbPreferences.forModule(MainApp.class).get(Options.OptSongDir, "");
File sD = new File(songDir);
if (sD.exists()) {
fc.setCurrentDirectory(sD);
}
fc.addChoosableFileFilter(new SongFilter());
int returnVal = fc.showOpenDialog(mainWindow);
if (returnVal == JFileChooser.APPROVE_OPTION) {
File file = fc.getSelectedFile();
return deserialize(file);
} else {
return null;
}
}
static private void configXStream(XStream xs) {
xs.setMode(XStream.NO_REFERENCES);
//Converter c = xstream.getConverterLookup().lookupConverterForType(MusicPage.class);
//c = xstream.getConverterLookup().lookupConverterForType(MusicPageSVG.class);
xs.processAnnotations(Song.class);
xs.processAnnotations(MusicPageSVG.class);
xs.processAnnotations(ImgSrc.class);
xs.processAnnotations(IcePdfImg.class);
xs.processAnnotations(PdfViewImg.class);
xs.processAnnotations(PdfImg.class);
xs.processAnnotations(GenericImg.class);
//xs.addDefaultImplementation(ArrayList.class, List.class);
xs.registerConverter(new MusicPageConverter(
xs.getConverterLookup().lookupConverterForType(MusicPageSVG.class),
xs.getReflectionProvider()));
xs.registerLocalConverter(Song.class, "pageOrder", new PageOrderConverter(xs));
xs.addDefaultImplementation(MusicPageSVG.class, MusicPage.class);
}
public boolean serialize() {
return serialize(this.sourceFile);
}
public boolean serialize(File toFile) {
version = MainApp.VERSION; // Update version
XStream xstream = new XStream();
configXStream(xstream);
// Give each page a chance to do house cleaning before being saved.
for (MusicPage mp: pageOrder) {
mp.prepareToSave();
}
boolean debug = false;
if (debug) {
try {
xstream.toXML(this, new OutputStreamWriter(new FileOutputStream(toFile), "UTF-8"));
} catch (UnsupportedEncodingException | FileNotFoundException ex) {
Exceptions.printStackTrace(ex);
}
}
try {
TraxSource traxSource = new TraxSource(this, xstream);
OutputStreamWriter buffer = new OutputStreamWriter(new FileOutputStream(toFile), "UTF-8");
synchronized (Song.class) {
songXFormer.transform(traxSource, new StreamResult(buffer));
}
//xstream.toXML(this, new OutputStreamWriter(new FileOutputStream(toFile), "UTF-8"));
} catch (FileNotFoundException | UnsupportedEncodingException | TransformerException ex) {
Log.log(ex);
return false;
}
// If this was saved using saveAs, add this file to the list of instantiated songs
try {
if (! Song.instantiated.containsKey(toFile.getCanonicalPath())) {
Song.instantiated.put(toFile.getCanonicalPath(), this);
}
} catch (IOException ex) {
Log.log(ex);
}
setDirty(false);
for (MusicPage mp: pageOrder) {
mp.isDirty = false;
}
return true;
}
/** De-serialization helper class.
* Performs the de-serialization and notifies waiting threads when it's done.
*/
private static class Deserializer implements Runnable {
CountDownLatch latch;
File file;
String canonicalPath;
public Deserializer(CountDownLatch latch, File file, String canonicalPath) {
this.latch = latch;
this.file = file;
this.canonicalPath = canonicalPath;
}
@Override
public void run() {
Song s = Song.deserializeCore(file, canonicalPath);
if (s != null) {
synchronized(deserLock) {
Song.instantiated.put(canonicalPath, s);
Song.inProgress.remove(canonicalPath);
}
}
// Notify all pending threads that the song is ready
latch.countDown();
}
}
/**
* De-serializes a song file.
*
* This function will block until the de-serialization is complete. The song
* de-serialization can be in one of 3 states:
* <li>Not started
* <li>In progress
* <li>Completed
*
* If it's completed, we just return the de-serialized song. If it's not yet
* started, we start a new Deserializer and wait for it to complete before
* returning the song (or null). If a job is already in progress we await()
* the same latch, and return the song (or null) when the job is done.
*
* @param f A song file to de-serialize
* @return The de-serialized song, or null if an error was encountered.
*/
static Song deserialize(File f) {
if (f == null || !f.getName().endsWith(SONG_FILE_EXT)) return null;
String canonicalPath;
try {
canonicalPath = f.getCanonicalPath();
} catch (IOException ex) {
//Exceptions.attachMessage(ex, "No canonical path for " + f.toString());
return null;
}
CountDownLatch latch;
Deserializer r = null;
synchronized(deserLock) {
if (instantiated.containsKey(canonicalPath)) { // deserialization completed
//Log.log("Song deserialization completed: " + canonicalPath, Level.FINEST);
return instantiated.get(canonicalPath);
} else if (!inProgress.containsKey(canonicalPath)) { // deser not started
//Log.log("Song deserialization not started: " + canonicalPath, Level.FINEST);
latch = new CountDownLatch(1);
inProgress.put(canonicalPath, latch);
r = new Deserializer(latch, f, canonicalPath);
} else { // deserialization in progress
//Log.log("Song deserialization in progress: " + canonicalPath, Level.FINEST);
latch = inProgress.get(canonicalPath);
}
}
if (r != null) {
r.run();
}
try {
latch.await();
} catch (InterruptedException ex) {
Exceptions.printStackTrace(ex);
}
return instantiated.get(canonicalPath);
}
/**
* The core de-serialization code. Callers must ensure the arguments
* are valid. No error checking is done on them.
* @param f The file to be de-serialized
* @param canonicalPath The result of f.getCanonicalPath()
* @return
*/
private static Song deserializeCore(File f, String canonicalPath) {
Song s;
XStream xs = new XStream();
configXStream(xs);
FileInputStream fis = null;
ByteArrayOutputStream xformed = null;
try {
fis = new FileInputStream(f);
xformed = new ByteArrayOutputStream();
synchronized (Song.class) {
songXFormer.transform(new StreamSource(fis), new StreamResult(xformed));
}
xformed = convertReferences(new ByteArrayInputStream(xformed.toByteArray()));
boolean debug = false;
if (debug) {
File f2 = new File(f.getName() + ".conv");
try (OutputStreamWriter w = new OutputStreamWriter(new FileOutputStream(f2), "UTF-8")) {
w.write(xformed.toString("UTF-8"));
}
}
s = (Song) xs.fromXML(xformed.toString("UTF-8"));
} catch (FileNotFoundException ex) {
//ErrorManager.getDefault().notify(ErrorManager.WARNING, ex);
NotifyUtil.error("Song file not found", canonicalPath, ex);
Log.log("Song file not found " + canonicalPath);
return null;
} catch (IOException | TransformerException ex) {
//Exceptions.attachMessage(ex, "Failed to deserialize " + canonicalPath);
NotifyUtil.error("Failed to read song", canonicalPath, ex);
Log.log("Failed to deserialize " + canonicalPath);
return null;
} finally {
if (fis != null) try {
fis.close();
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
if (xformed != null) try {
xformed.close();
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
s.sourceFile = new File(canonicalPath);
synchronized (s.pageOrder) {
for (MusicPage mp: s.pageOrder) mp.deserialize(s);
}
findPages(s);
return s;
}
static void convertReference(Document doc, String elem) {
XPath xPath = XPathFactory.newInstance().newXPath();
NodeList nodes = doc.getElementsByTagName(elem);
for (int i = 0; i < nodes.getLength(); i++) {
NamedNodeMap attrs = nodes.item(i).getAttributes();
Node ref = attrs.getNamedItem("reference");
if (ref != null) {
String refPtr = ref.getNodeValue();
if (refPtr != null) {
try {
XPathExpression xExpr = xPath.compile(refPtr);
Node refNode = (Node) xExpr.evaluate(nodes.item(i), XPathConstants.NODE);
if (refNode != null) {
nodes.item(i).setTextContent(refNode.getTextContent());
}
} catch (XPathExpressionException ex) {
Exceptions.printStackTrace(ex);
}
}
attrs.removeNamedItem("reference");
}
}
}
static ByteArrayOutputStream convertReferences(InputStream stream) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = builder.parse(stream);
convertReference(doc, "rotation");
convertReference(doc, "sourceFile");
Transformer xformer = TransformerFactory.newInstance().newTransformer();
boolean debug = false;
if (debug) {
try (FileOutputStream fos = new FileOutputStream("C:\\erase.xml")) {
StreamResult res = new StreamResult(fos);
Source src = new DOMSource(doc);
xformer.transform(src, res);
}
}
StreamResult res = new StreamResult(baos);
Source src = new DOMSource(doc);
xformer.transform(src, res);
} catch (TransformerException | SAXException | IOException | ParserConfigurationException ex) {
Exceptions.printStackTrace(ex);
}
return baos;
}
/** We store absolute path names in the song file. If the song has moved the paths
* for the page files might no longer be valid. This function attempts to fix that.
* It expects s.sourceFile to already be in canonical form.
*/
static void findPages(Song s) {
for (MusicPage mp: s.pageOrder) {
File f = mp.getSourceFile();
if (f != null && !f.exists()) {
File newFile = Utils.findFileRelative(s.getSourceFile(), f);
if (newFile != null) {
mp.setSourceFile(newFile);
//s.setDirty(true);
}
}
}
}
/**
* Clears all deserialized songs so they can be re-loaded
*/
public static void clearInstantiated() {
instantiated.clear();
}
public void addPropertyChangeListener (PropertyChangeListener pcl) {
synchronized(pcsMutex) {
pcs.addPropertyChangeListener(pcl);
}
}
public void addPropertyChangeListener (String propertyName, PropertyChangeListener pcl) {
synchronized(pcsMutex) {
pcs.addPropertyChangeListener(propertyName, pcl);
}
}
public void removePropertyChangeListener(PropertyChangeListener pcl) {
synchronized(pcsMutex) {
pcs.removePropertyChangeListener(pcl);
}
}
private void fire(String propertyName, Object old, Object nue) {
synchronized(pcsMutex) {
pcs.firePropertyChange(propertyName, old, nue);
}
}
public void addChangeListener(ChangeListener listener) {
if (!pageListeners.contains(listener)) pageListeners.add(listener);
}
public void removeChangeListener(ChangeListener listener) {
pageListeners.remove(listener);
}
public void notifyListeners() {
ChangeEvent ev = new ChangeEvent(this);
ChangeListener[] cls = pageListeners.toArray(new ChangeListener[0]);
for (ChangeListener cl : cls) {
cl.stateChanged(ev);
}
}
@Override
public int compareTo(Song other) {
return getName().compareTo(other.getName());
}
private class SongSavable extends AbstractSavable implements Icon, SaveAsCapable {
private final Song s;
public SongSavable(Song s) {
if (s == null) throw new IllegalArgumentException("Null Song not allowed");
this.s = s;
register();
}
@Override
protected String findDisplayName() {
return s.getName();
}
@Override
protected void handleSave() throws IOException {
s.save();
VirtMusLookup.getInstance().remove(this);
}
public void saved() {
unregister();
VirtMusLookup.getInstance().remove(this);
}
@Override
public boolean equals(Object other) {
if (other instanceof SongSavable) {
SongSavable ss = (SongSavable)other;
return s.equals(ss.s);
}
return false;
}
@Override
public int hashCode() {
return s.hashCode();
}
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
ICON.paintIcon(c, g, x, y);
}
@Override
public int getIconWidth() {
return ICON.getIconWidth();
}
@Override
public int getIconHeight() {
return ICON.getIconHeight();
}
@Override
public void saveAs(FileObject folder, String fileName) throws IOException {
FileObject newFile = folder.getFileObject(fileName);
s.sourceFile = new File(newFile.getNameExt());
save();
}
}
}