/*
* Created on Mar 2, 2006
*
* Copyright (c) 2006 P.J.Leonard
*
* http://www.frinika.com
*
* This file is part of Frinika.
*
* Frinika 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.
* Frinika 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 Frinika; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.frinika.sequencer.model;
import static com.frinika.localization.CurrentLocale.getMessage;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.PrintStream;
import java.io.Serializable;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import com.frinika.gui.OptionsDialog;
import com.frinika.gui.OptionsEditor;
import com.frinika.project.MultiPart;
import com.frinika.project.ProjectContainer;
import com.frinika.project.gui.ProjectFrame;
import com.frinika.sequencer.gui.Item;
import com.frinika.sequencer.gui.TimeFormat;
import com.frinika.sequencer.gui.TimeSelector;
import com.frinika.sequencer.gui.partview.PartView;
import java.util.Vector;
/**
* A Part encapsulates what can be displayed in the partview.
* The startTick and endTick define the range in the display.
* (These do not need to correspond to the range of any contained items)
* It's Lane defines the row of display.
*
* @author Paul
*
*/
public abstract class Part implements Item, Selectable, EditHistoryRecordable, Serializable, MenuPlugable {
private static final long serialVersionUID = -7282369887900349287L;
protected Lane lane;
// Default tick based limits. Subclasses can override this scheme with it's own fields and methods
private long startTick = 0;
private long endTick = 0;
// protected int colorID=0;
Color color;
transient Color transColor;
transient protected boolean selected = false;
private MultiPart multiPart;
// transient boolean attached;
Long partResourceId; // The database id for my resource (null then not saved yet)
Part rootPart; // the part that I was copied from.
transient Part editParent; // the part I was edited from.
public Part getEditParent() {
return editParent;
}
public void setEditParent(Part editParent) {
this.editParent = editParent;
}
public Part getRootPart() {
return rootPart;
}
public void setRootPart(Part rootPart) {
this.rootPart = rootPart;
}
public Long getPartResourceId() {
return partResourceId;
}
public void setPartResourceId(Long partResourceId) {
this.partResourceId = partResourceId;
}
protected Part() {
color = Color.lightGray;
rootPart = this;
editParent= null;
}
/**
* Construct a new Part and add it to it's lane.
*
* @param lane
*/
public Part(Lane lane) {
rootPart = this; // default to not being a copy !!!
this.lane = lane;
if (lane != null) {
this.lane.add(this);
color = lane.color;
}
}
/**
*
* @return the Lane that contains the Part
*/
public Lane getLane() {
return lane;
}
/**
*
* @return length of the part in ticks
*/
public long getDurationInTicks() {
if (startTick > endTick) {
return 0;
}
return endTick - startTick;
}
/**
*
* @return start of the part in ticks
*/
public long getStartTick() {
//if (startTick > endTick ) return 0;
return startTick;
}
/**
*
* @return end tick of the part
*/
public long getEndTick() {
//if (startTick > endTick ) return 0;
return endTick;
}
/**
*
* @return length of the part in samples
*/
public double getDurationInSecs() {
if (startTick > endTick) {
return 0;
}
return (lane.project.getTempoList().getTimeAtTick(endTick) - lane.project.getTempoList().getTimeAtTick(startTick));
}
/**
*
* @return start of the part in samples
*/
public double getStartInSecs() {
//if (startTick > endTick ) return 0;
return lane.project.getTempoList().getTimeAtTick(startTick);
}
/**
*
* @return end samples of the part
*/
public double getEndInSecs() {
//if (startTick > endTick ) return 0;
return lane.project.getTempoList().getTimeAtTick(endTick);
}
/**
*
* @return length of the part
*/
public double getDuration(boolean sampleBased) {
if (sampleBased) {
return getDurationInSecs();
} else {
return getDurationInTicks();
}
}
/**
*
* @return start of the part
*/
public double getStart(boolean sampleBased) {
if (sampleBased) {
return getStartInSecs();
} else {
return getStartTick();
}
}
/**
*
* @return end of the part
*/
public double getEnd(boolean sampleBased) {
if (sampleBased) {
return getEndInSecs();
} else {
return getEndTick();
}
}
/**
* used by the GUI
*/
public boolean isSelected() {
return selected;
}
/**
* used by the GUI
*/
public void setSelected(boolean b) {
selected = b;
}
/**
*
* NOTE AudioPert overrides these methods
*
* Set the start tick.
* This does not effect any items contained in the part.
* Purely for display.
*
* @param tick new start tick
*/
public void setStartTick(double tick) {
startTick = (long) tick; // XXX
}
/**
*
* @param tick new end tick for display purpose only
*/
public void setEndTick(double tick) {
endTick = (long) tick; // XXX
}
public void setStartInSecs(double start) {
startTick = (long) lane.getProject().getTempoList().getTickAtTime(start);
}
public void setEndInSecs(double end) {
endTick = (long) lane.getProject().getTempoList().getTickAtTime(end);
}
/**
* Moves the part and all it contains by deltaTick.
* Concrete subclasses implement moveItemsBy method to move the contents of the Part.
*
* @param tick
* @deprecated
*/
public void moveBy(long deltaTick) {
startTick += deltaTick;
endTick += deltaTick;
moveItemsBy(deltaTick);
}
/**
* @deprecated
*/
abstract protected void moveItemsBy(long deltaTick);
public abstract Object clone() throws CloneNotSupportedException;
/*
*
* Called when part is inserted into the model
*
*/
public abstract void commitEventsAdd();
/**
*
* Called when part is removed from the model
*
*/
public abstract void commitEventsRemove();
abstract public void copyBy(double tick, Lane dst);
/**
* move the contents by tick into dstLane
* @param tick
*/
public abstract void moveContentsBy(double tick, Lane dstLane);
public void removeFromModel() {
lane.remove(this);
}
/**
*
* @return if part is part of the model.
*
*/
public boolean isAttached() {
if (lane == null) {
return false;
}
synchronized (lane) {
return lane.getParts().contains(this);
}
}
public void addToModel() {
lane.add(this);
}
public long leftTickForMove() {
return getStartTick();
}
public long rightTickForMove() {
return getEndTick();
}
public Rectangle getEventBounds() {
// TODO Auto-generated method stub
return null;
}
// public int getColorID() {
// return colorID;
// }
@SuppressWarnings("unchecked")
private void readObject(ObjectInputStream in)
throws ClassNotFoundException, IOException {
in.defaultReadObject();
if (rootPart == null) {
rootPart = this;
}
}
public abstract void onLoad() throws Exception;
public abstract void drawThumbNail(Graphics2D g, Rectangle rect, PartView partView);
public void displayStructure(String prefix, PrintStream out) {
out.println(prefix + toString());
}
/**
* Override to customize the right button popup
*
* @param invoker
* @param x
* @param y
* @return
*/
public boolean showRightButtonMenu(Component invoker, int x, int y) {
return false;
}
static Vector<MenuPlugin> menuPlugins = new Vector<MenuPlugin>();
/**
* Allow custom menus to be added.
*
* This will appear at the top of the right button popup menu.
*
* @param menuPlugin
*/
public static void addPluginRightButtonMenu(MenuPlugin menuPlugin) {
Part.menuPlugins.add(menuPlugin);
}
/**
* Shows the right-click context menu of the current component.
*
* @param frame
* @param invoker
* @param x
* @param y
*/
public void showContextMenu(final ProjectFrame frame, Component invoker, int x, int y) {
// build popup-menu from menuPrefix and own items
JPopupMenu popup = new JPopupMenu();
for (MenuPlugin plugin : menuPlugins) {
plugin.initContextMenu(popup, this);
}
initContextMenu(frame, popup);
popup.show(invoker, x, y);
}
/**
* Fills the context menu with part-type specific (or possibly even instance-specific) items.
* TO BE EXTENDED BY SUBCLASSES.
*
* @param popup
*/
protected void initContextMenu(final ProjectFrame frame, JPopupMenu popup) {
// ... subclass overwrite and do something, then call super....() ...
if (popup.getComponentCount() > 0) {
popup.addSeparator();
}
// "Properties..."
JMenuItem item = new JMenuItem(getMessage("project.menu.properties") + "...");
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
showPropertiesDialog(frame);
}
});
popup.add(item);
}
public Color getTransparentColor() {
Color c = getColor();
if (transColor == null) {
transColor = new Color(c.getRed(), c.getGreen(), c.getBlue(), (int) (0.8f * 255));
}
return transColor;
}
public Color getColor() {
if (color == null) {
color = Color.RED;
}
return color;
}
public void setColor(Color col) {
color = col;
transColor = null;
}
public void showPropertiesDialog(ProjectFrame frame) {
createPropertiesDialog(frame).show();
frame.repaintPartView();
}
protected JDialog createPropertiesDialog(ProjectFrame frame) {
final OptionsEditor contentEditor = createPropertiesPanel(frame);
final OptionsEditor backup = createPropertiesPanel(frame); // a second one which keeps initial values (and will not be displayed)
backup.refresh();
OptionsDialog dialog = new OptionsDialog(frame, (JComponent) contentEditor, "Part Properties") {
/**
* Called when Ok is chosen.
*/
public void ok() {
super.ok();
// commit as undoable action
ProjectContainer project = frame.getProjectContainer();
project.getEditHistoryContainer().mark(getMessage("project.menu.edit_properties"));
EditHistoryAction action = new EditHistoryAction() {
public void redo() {
contentEditor.update();
frame.repaintPartView();
}
public void undo() {
backup.update();
frame.repaintPartView();
}
};
project.getEditHistoryContainer().push(action);
project.getEditHistoryContainer().notifyEditHistoryListeners();
}
/**
* Special handling of cancel, because values changed in the dialog will directly be applied
* and thus must explicitly be restored if cancel is chosen.
*/
public void cancel() {
backup.update();
super.cancel();
frame.repaintPartView();
}
};
return dialog;
}
/**
* Create PropertiesPanel.
*
* Optionally overwritten by subclass, returning a subclass-instance of PropertiesPanel.
*
* @param frame
* @return
*/
protected OptionsEditor createPropertiesPanel(ProjectFrame frame) {
return new PropertiesPanel(frame); // default, may be overwritten
}
public MultiPart getMultiPart() {
return multiPart;
}
public void setMultiPart(MultiPart multiPart) {
this.multiPart = multiPart;
}
// --- inner class ---
/**
* Optionally to be extended by subclass and returned via createProperitesPanel().
*/
protected class PropertiesPanel extends JPanel implements OptionsEditor {
protected ProjectFrame frame;
protected TimeSelector startTimeSelector;
protected TimeSelector endTimeSelector;
protected TimeSelector lengthTimeSelector;
/**
* Constructor.
*
* @param frame
*/
protected PropertiesPanel(ProjectFrame frame) {
super();
this.frame = frame;
setLayout(new GridBagLayout());
initComponents();
}
/**
* Fills the panel with gui elements for editing the part's properties.
*
* TO BE EXTENDED BY SUBCLASS.
*/
protected void initComponents() {
JLabel startLabel = new JLabel("Start:");
startTimeSelector = new TimeSelector(startTick, frame.getProjectContainer(), TimeFormat.BAR_BEAT_TICK);
JLabel endLabel = new JLabel("End:");
JLabel lengthLabel = new JLabel("Duration:");
endTimeSelector = new TimeSelector(Part.this.endTick, frame.getProjectContainer(), TimeFormat.BAR_BEAT_TICK);
lengthTimeSelector = new TimeSelector(Part.this.getDurationInTicks(), frame.getProjectContainer(), TimeFormat.BEAT_TICK);
// special: directly apply change and make visible
startTimeSelector.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
long d = getDurationInTicks();
startTick = startTimeSelector.getTicks();
endTick = startTick + d;
endTimeSelector.setTicks(endTick);
frame.repaintPartView();
}
});
// special: directly apply change and make visible
endTimeSelector.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
endTick = endTimeSelector.getTicks();
lengthTimeSelector.setTicks(getDurationInTicks());
frame.repaintPartView();
}
});
// special: directly apply change and make visible
lengthTimeSelector.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
long t = lengthTimeSelector.getTicks();
endTick = startTick + t;
endTimeSelector.setTicks(endTick);
frame.repaintPartView();
}
});
GridBagConstraints gc = new GridBagConstraints();
gc.insets = new Insets(5, 5, 5, 5);
gc.anchor = GridBagConstraints.WEST;
this.add(startLabel, gc);
this.add(startTimeSelector, gc);
this.add(endLabel, gc);
gc.gridwidth = GridBagConstraints.REMAINDER;
gc.anchor = GridBagConstraints.EAST;
this.add(endTimeSelector, gc);
gc.anchor = GridBagConstraints.WEST;
gc.gridwidth = 2;
this.add(new JPanel(), gc); // spacer
gc.gridwidth = 1;
this.add(lengthLabel, gc);
gc.gridwidth = GridBagConstraints.REMAINDER;
gc.anchor = GridBagConstraints.EAST;
this.add(lengthTimeSelector, gc);
gc.anchor = GridBagConstraints.WEST;
gc.gridwidth = 1;
}
/**
* Refreshes the GUI so that it reflects the model's current state.
*
* TO BE EXTENDED BY SUBCLASS.
*/
public void refresh() {
startTimeSelector.setTicks(startTick);
endTimeSelector.setTicks(endTick);
lengthTimeSelector.setTicks(getDurationInTicks());
}
/**
* Updates the model so that it contains the values set by the user.
*
* TO BE EXTENDED BY SUBCLASS.
*/
public void update() {
startTick = startTimeSelector.getTicks();
endTick = endTimeSelector.getTicks();
}
}
/**
*
* Must be called if structure is changed.
* - the database partResourceID is set to null (flag for new)
* - rootPart is set to this.
* - if we have edited a copied part then
*/
public void setChanged() {
if (rootPart != this) {
// this part is currently a copy of a different part
System.out.println(" Detaching a copied part ");
editParent = rootPart; // make the original the parent.
partResourceId = null; // not in the data base yet
rootPart = this; // this means it is/will be an original
} else {
// we are an original.
// so disassociate from any saved part resource.
if (partResourceId != null) {
System.out.println(" Detaching a root part from "+partResourceId);
partResourceId=null;
}
}
if (partResourceId != null) {
// should not happen
try {
throw new Throwable(" Part with resourceID!=null changed ");
} catch (Throwable ex) {
Logger.getLogger(Part.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}