/* Copyright (C) 2006 Christian Schneider
*
* This file is part of Nomad.
*
* Nomad 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.
*
* Nomad 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 Nomad; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
/*
* Created on Jan 21, 2007
*/
package net.sf.nmedit.jtheme.component;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.LayoutManager2;
import java.awt.Point;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTargetDropEvent;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.JMenu;
import javax.swing.JPopupMenu;
import javax.swing.border.Border;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import net.sf.nmedit.jpatch.CopyOperation;
import net.sf.nmedit.jpatch.InvalidDescriptorException;
import net.sf.nmedit.jpatch.ModuleDescriptions;
import net.sf.nmedit.jpatch.MoveOperation;
import net.sf.nmedit.jpatch.PConnection;
import net.sf.nmedit.jpatch.PConnectionManager;
import net.sf.nmedit.jpatch.PConnector;
import net.sf.nmedit.jpatch.PModule;
import net.sf.nmedit.jpatch.PModuleContainer;
import net.sf.nmedit.jpatch.PModuleDescriptor;
import net.sf.nmedit.jpatch.PModuleMetrics;
import net.sf.nmedit.jpatch.PPatch;
import net.sf.nmedit.jpatch.dnd.PDragDrop;
import net.sf.nmedit.jpatch.dnd.PModuleTransferData;
import net.sf.nmedit.jpatch.event.PConnectionEvent;
import net.sf.nmedit.jpatch.event.PConnectionListener;
import net.sf.nmedit.jpatch.event.PModuleContainerEvent;
import net.sf.nmedit.jpatch.event.PModuleContainerListener;
import net.sf.nmedit.jpatch.history.PUndoableEditSupport;
import net.sf.nmedit.jpatch.util.DescriptorNameComparator;
import net.sf.nmedit.jtheme.JTContext;
import net.sf.nmedit.jtheme.JTException;
import net.sf.nmedit.jtheme.cable.Cable;
import net.sf.nmedit.jtheme.cable.JTCableManager;
import net.sf.nmedit.jtheme.component.plaf.mcui.InsertModuleAction;
import net.sf.nmedit.jtheme.component.plaf.mcui.JTModuleContainerUI;
import net.sf.nmedit.jtheme.help.HelpHandler;
import net.sf.nmedit.jtheme.store2.ModuleElement;
import net.sf.nmedit.nmutils.dnd.FileDnd;
import net.sf.nmedit.nmutils.swing.NmSwingUtilities;
public class JTModuleContainer extends JTBaseComponent
{
private static final Log log = LogFactory.getLog(JTModuleContainer.class);
/**
*
*/
private static final long serialVersionUID = 4287282305262880549L;
public static final String uiClassId = "ModuleContainerUI";
private boolean optimizedDrawing;
private JTPatch patchContainer;
private PModuleContainer moduleContainer;
private ContentSynchronisation cs;
//private CableOverlay overlay;
private JTLayerRoot cableLayer;
public JTModuleContainer(JTContext context, JTCableManager cableManager)
{
super(context);
setOpaque(true);
setFocusable(true);
optimizedDrawing = false;// 'true' does not work: !context.hasModuleContainerOverlay();
// we have to overwrite boolean isPaintingOrigin() which is package private
this.cableLayer = new JTLayerRoot(context);
add(cableLayer);
setCableManager(cableManager);
cs = createContentSynchronisation();
if (cs != null)
cs.install();
setJTFlag(FLAG_INVALIDATE, true);
setJTFlag(FLAG_VALIDATE, true);
setJTFlag(FLAG_REVALIDATE, true);
setJTFlag(FLAG_VALIDATE_TREE, true);
setLayout(new ModuleContainerLayout());
}
public HelpHandler getHelpHandler()
{
return patchContainer != null ? patchContainer.getHelpHandler() : null;
}
protected boolean isRepaintOrigin()
{
return true;
}
/**
* Returns a mutable list of all modules in this container.
* @return modules in this container
*/
public List<JTModule> getModules()
{
return getComponents(JTModule.class);
}
private Set<JTModule> selectionSet = new HashSet<JTModule>();
public int getSelectionSize()
{
return selectionSet.size();
}
public JTLayerRoot getLayerRoot() {
return cableLayer;
}
public boolean isOnlyThisSelected(JTModule module)
{
return getSelectionSize() == 1 && isInSelection(module);
}
public void selectOnly(JTModule module)
{
if (!isOnlyThisSelected(module))
{
if (!selectionSet.isEmpty())
{
for (JTModule m: selectionSet)
m.setSelected(false);
selectionSet.clear();
}
addSelection(module);
}
}
public void addSelection(JTModule module)
{
JTPatch patch = getPatchContainer();
for (JTModuleContainer c : patch.getModuleContainers()) {
if (c != null && c != this) {
c.clearSelection();
}
}
selectionSet.add(module);
module.setSelected(true);
}
public void removeSelection(JTModule module)
{
selectionSet.remove(module);
module.setSelected(false);
}
public boolean isInSelection(JTModule module)
{
return selectionSet.contains(module);
}
public boolean isSelectionEmpty()
{
return selectionSet.isEmpty();
}
public Collection<? extends JTModule> getSelectedModules()
{
return new HashSet<JTModule>(selectionSet);
}
public Collection<? extends PModule> getSelectedPModules()
{
HashSet<PModule> result = new HashSet<PModule>();
for (JTModule m : selectionSet) {
result.add(m.getModule());
}
return result;
}
public void clearSelection()
{
if (selectionSet.isEmpty())
return;
for (JTModule module: selectionSet)
module.setSelected(false);
selectionSet.clear();
}
private JTCableManager cableManager;
/*
public Component findComponentAt(int x, int y) {
if (!(contains(x, y) && isVisible() && isEnabled())) {
return null;
}
synchronized (getTreeLock())
{
for (int i=getComponentCount()-1;i>=0;i--)
{
Component comp = getComponent(i);
if (comp != overlay)
{
if (comp instanceof Container)
{
comp = ((Container)comp).findComponentAt(x-comp.getX(), y-comp.getY());
}
else
{
comp = comp.getComponentAt(x, y);
}
if (comp != null && comp.isVisible() && comp.isEnabled())
{
return comp;
}
}
}
}
return this;
}*/
protected ContentSynchronisation createContentSynchronisation()
{
return new ContentSynchronisation();
}
protected class ContentSynchronisation implements PModuleContainerListener, PConnectionListener
{
private PModuleContainer mc;
private PConnectionManager pc;
//private boolean oneUpdate = false;
public void install()
{
// no op
}
public void update()
{
setInstalledContainer(moduleContainer);
}
private void setInstalledContainer(PModuleContainer moduleContainer)
{
if (this.mc != null)
uninstall(this.mc);
this.mc = moduleContainer;
if (this.mc != null)
install(this.mc);
}
protected void install(PModuleContainer mc)
{
mc.addModuleContainerListener(this);
pc = mc.getConnectionManager();
if (pc != null)
pc.addConnectionListener(this);
}
protected void uninstall(PModuleContainer mc)
{
mc.removeModuleContainerListener(this);
if (pc != null)
pc.removeConnectionListener(this);
pc = null;
}
public void moduleAdded(PModuleContainerEvent e)
{
createUIFor(e.getModule());
}
public void moduleRemoved(PModuleContainerEvent e)
{
removeUI(e.getModule());
}
private void createUIFor(PModule module)
{
ModuleElement ms = getContext().getStorageContext().getModuleStoreById(module.getComponentId());
try
{
JTModule mui = ms.createModule(getContext(), module);
mui.setLocation(module.getScreenLocation());
add(mui);
JTModuleContainer.this.revalidate();
mui.repaint();
}
catch (JTException e)
{
if (log.isErrorEnabled())
{
log.error("createUIFor(PModule) failed for module: "+module, e);
}
}
}
public void removeUI(PModule module)
{
for (JTModule mui: JTModuleContainer.this.getModules())
{
if (mui.getModule() == module)
{
int x = mui.getX();
int y = mui.getY();
int w = mui.getWidth();
int h = mui.getHeight();
remove(mui);
JTModuleContainer.this.revalidate();
JTModuleContainer.this.repaint(x, y, w, h);
break;
}
}
}
public void connectionAdded(PConnectionEvent e)
{
colorCables(e, true);
}
public void connectionRemoved(PConnectionEvent e)
{
colorCables(e, false);
}
public void colorCables(PConnectionEvent e, boolean joined)
{
if (joined)
{
Color color = null;
PConnector graph = null;
if (e.getDestination().getOutputConnector() != null)
{
graph = e.getSource();
color = e.getDestination().getOutputConnector().getSignalType().getColor();
}
else if (e.getSource().getOutputConnector() != null)
{
graph = e.getDestination();
color = e.getSource().getOutputConnector().getSignalType().getColor();
}
if (graph != null)
{
Collection<Cable> cables = getCables(graph);
for (Cable cable: cables)
{
cable.setColor(color);
}
getCableManager().update(cables);
}
}
else
{
PConnector graph = null;
if (e.getDestination().getOutputConnector() != null)
{
graph = e.getSource();
}
else if (e.getSource().getOutputConnector() != null)
{
graph = e.getDestination();
}
// make grey
if (graph != null)
{
Collection<Cable> cables = getCables(graph);
for (Cable cable: cables)
{
// TODO lookup correct color in patch
cable.setColor(Color.WHITE);
}
getCableManager().update(cables);
}
}
}
private Collection<Cable> getCables(PConnector c)
{
Collection<Cable> cables = new ArrayList<Cable>(getCableManager().size());
Collection<PConnection> connections = c.getGraphConnections();
for (Cable cable: getCableManager())
{
for (Iterator<PConnection> iter=connections.iterator();iter.hasNext();)
{
if (isSameConnection(iter.next(), cable))
cables.add(cable);
}
}
return cables;
}
private boolean isSameConnection(PConnection c, Cable cable)
{
// compare connection ((a1, b1) with (a2, b2)) or ((a1, b1) with (b2, a2))
return isSameConnection(c.getA(), c.getB(), cable.getSource(), cable.getDestination())
|| isSameConnection(c.getA(), c.getB(), cable.getDestination(), cable.getSource());
}
private boolean isSameConnection(PConnector a1, PConnector b1,
PConnector a2, PConnector b2)
{
// compare connection (a1, b1) with (a2, b2)
return a1==a2 && b1==b2;
}
}
public void setModuleContainer(PModuleContainer mc)
{
this.moduleContainer = mc;
setName(moduleContainer == null ? null : moduleContainer.getName());
PModuleMetrics metrics = null;
if (mc != null) metrics = mc.getModuleMetrics();
if (metrics == null) setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE));
else setMaximumSize(new Dimension(metrics.getMaxScreenX(), metrics.getMaxScreenY()));
if (cs != null)
cs.update();
}
public PModuleContainer getModuleContainer()
{
return moduleContainer;
}
public JTPatch getPatchContainer()
{
return patchContainer;
}
public void setPatchContainer(JTPatch patchContainer)
{
this.patchContainer = patchContainer;
}
public JTModuleContainerUI getUI()
{
return (JTModuleContainerUI) ui;
}
public void setUI(JTModuleContainerUI ui)
{
super.setUI(ui);
}
public String getUIClassID()
{
return uiClassId;
}
protected void setCableManager(JTCableManager cableManager)
{
this.cableManager = cableManager;
cableLayer.setCableManager(cableManager);
}
public JTCableManager getCableManager()
{
return cableManager;
}
public boolean isOptimizedDrawingEnabled()
{
return optimizedDrawing;
}
/**
* Calls {@link #paint(Graphics)}.
*/
public void update(Graphics g)
{
paint(g);
}
public void setBorder(Border border)
{
if (border != null)
throw new UnsupportedOperationException("border not supported");
}
protected final void paintBorder(Graphics g)
{
// no op
}
public boolean isDnDAllowed()
{
return getContext().isDnDAllowed();
}
private class ModuleContainerLayout implements LayoutManager2
{
private boolean valid = false;
private Dimension cachedSize = new Dimension();
private Insets cachedInsets = new Insets(0,0,0,0);
public void invalidateLayout(Container target)
{
valid = false;
}
public void layoutContainer(Container parent)
{
JTModuleContainer mc = JTModuleContainer.this;
// only layout cable layer
JTLayerRoot cl = mc.cableLayer;
if (cl != null)
{
synchronized(mc.getTreeLock())
{
Insets insets = mc.getInsets();
cl.setBounds(0, 0, mc.getWidth()-insets.left-insets.right,
mc.getHeight()-insets.top-insets.bottom);
}
}
}
public Dimension minimumLayoutSize(Container parent)
{
if (valid) return new Dimension(cachedSize);
Dimension dim = new Dimension(0, 0);
JTModuleContainer mc = JTModuleContainer.this;
synchronized (mc.getTreeLock())
{
for (int i=mc.getComponentCount()-1;i>=0;i--)
{
Component c = mc.getComponent(i);
if (mc.cableLayer == c)
continue;
dim.width = Math.max(dim.width, c.getX()+c.getWidth());
dim.height = Math.max(dim.height, c.getY()+c.getHeight());
}
}
boolean keepSize = false;
JTLayerRoot cl = mc.cableLayer;
if (cl != null)
{
final int EXTRA = 200;
// also search through layers
synchronized (cl.getTreeLock())
{
for (int i=cl.getComponentCount()-1;i>=0;i--)
{
Component c = cl.getComponent(i);
dim.width = Math.max(dim.width, c.getX()+c.getWidth()+EXTRA);
dim.height = Math.max(dim.height, c.getY()+c.getHeight()+EXTRA);
keepSize = true;
}
}
}
Insets insets = mc.getInsets(cachedInsets);
dim.width += insets.left + insets.right;
dim.height += insets.top + insets.bottom;
if (keepSize)
{
cachedSize.width = Math.max(cachedSize.width, dim.width);
cachedSize.height = Math.max(cachedSize.height, dim.height);
}
else
{
cachedSize.setSize(dim);
}
valid = true;
return dim;
}
public Dimension maximumLayoutSize(Container target)
{
return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
}
public Dimension preferredLayoutSize(Container parent)
{
return minimumLayoutSize(parent);
}
public float getLayoutAlignmentX(Container target)
{
return 0;
}
public float getLayoutAlignmentY(Container target)
{
return 0;
}
public void addLayoutComponent(String name, Component comp)
{
// no op
}
public void addLayoutComponent(Component comp, Object constraints)
{
// no op
}
public void removeLayoutComponent(Component comp)
{
// no op
}
}
public boolean dropPatch(PPatch newPatch, Point location) {
PModuleContainer newMc = null;
for (int i = 0; i < newPatch.getModuleContainerCount(); i++) {
newMc = newPatch.getModuleContainer(i);
if (newMc.getModuleCount() > 0)
break;
else
newMc = null;
}
if (newMc == null) {
return false;
}
CopyOperation op = newMc.createCopyOperation();
op.setDestination(getModuleContainer());
for (PModule module: newMc){
op.add(module);
}
op.setScreenOffset(location.x, location.y);
op.copy();
return true;
}
public void dropPatchFile(DropTargetDropEvent dtde) {
Transferable transfer = dtde.getTransferable();
PPatch patch = getModuleContainer().getPatch();
DataFlavor fileFlavor = FileDnd.getFileFlavor(transfer.getTransferDataFlavors());
List<File> files = FileDnd.getTransferableFiles(fileFlavor, transfer);
if (files.size() == 1) {
PPatch newPatch;
try
{
newPatch = patch.createFromFile(files.get(0));
}
catch (Exception e)
{
newPatch = null;
}
if (newPatch != null) {
if (dropPatch(newPatch, dtde.getLocation())) {
dtde.dropComplete(true);
} else {
dtde.rejectDrop();
dtde.dropComplete(false);
}
} else {
dtde.rejectDrop();
dtde.dropComplete(false);
}
} else {
dtde.rejectDrop();
dtde.dropComplete(false);
}
}
public void copyMoveModules(DropTargetDropEvent dtde) {
DataFlavor chosen = PDragDrop.ModuleSelectionFlavor;
Transferable transfer = dtde.getTransferable();
Object data = null;
try {
// Get the data
data = transfer.getTransferData(chosen);
dtde.acceptDrop(dtde.getDropAction() & (DnDConstants.ACTION_MOVE | DnDConstants.ACTION_COPY | DnDConstants.ACTION_LINK));
} catch (Throwable t) {
if (log.isWarnEnabled())
{
log.warn("copyMoveModules(DropTargetDropEvent="+dtde+") failed", t);
}
dtde.dropComplete(false);
return;
}
if (data!=null && data instanceof PModuleTransferData)
{
// Cast the data and create a nice module.
PModuleTransferData tdata = ((PModuleTransferData)data);
boolean isSamePatch = false;
if (tdata.getSourcePatch() == getModuleContainer().getPatch())
isSamePatch = true;
//Point p = dtde.getLocation();
int action = dtde.getDropAction();
if ((action&DnDConstants.ACTION_MOVE)!=0 && isSamePatch)
{
MoveOperation op = tdata.getSourceModuleContainer().createMoveOperation();
op.setDestination(getModuleContainer());
executeOperationOnSelection(tdata, dtde.getLocation(), op);
}
else
{
copyModules(tdata, dtde.getLocation(), ((dtde.getDropAction() & DnDConstants.ACTION_LINK) != 0));
}
}
dtde.dropComplete(true);
}
public void copyModules(PModuleTransferData tdata, Point p, boolean link) {
if (tdata != null && tdata.getSourceModuleContainer() != null) {
CopyOperation op = tdata.getSourceModuleContainer().createCopyOperation();
// check for shift pressed to create links XXX
if (tdata.getSourcePatch() == getPatchContainer().getPatch() && link) {
op.setDuplicate(true);
}
op.setDestination(getModuleContainer());
executeOperationOnSelection(tdata, p, op);
}
}
public void dropNewModule(DropTargetDropEvent dtde) {
PModuleContainer mc = getModuleContainer();
PModuleDescriptor md = PDragDrop.getModuleDescriptor(dtde.getTransferable());
if (md == null || mc == null)
{
dtde.rejectDrop();
return;
}
Point l = dtde.getLocation();
PModule module;
try
{
module = mc.createModule(md);
module.setScreenLocation(l.x, l.y);
}
catch (InvalidDescriptorException e)
{
if (log.isErrorEnabled())
{
log.error("could not create module: "+md, e);
}
dtde.rejectDrop();
return;
}
boolean moduleAdded = mc.add(module);
if (!moduleAdded)
{
dtde.rejectDrop();
return;
}
// TODO short after dropping a new module and then moving it
// causes a NullPointerException in the next line
PModuleContainer parent = module.getParentComponent();
if (parent != null) {
JTCableManager cm = getCableManager();
try
{
cm.setAutoRepaintDisabled();
MoveOperation move = parent.createMoveOperation();
move.setScreenOffset(0, 0);
move.add(module);
move.move();
}
finally
{
cm.clearAutoRepaintDisabled();
}
} else {
// XXX concurrency problems probably ?!
throw new RuntimeException("Drop problem on illegal modules: for example 2 midi globals");
}
dtde.acceptDrop(DnDConstants.ACTION_COPY);
// compute dimensions of container
revalidate();
repaint();
dtde.dropComplete(true);
}
public void executeOperationOnSelection(PModuleTransferData tdata, Point p, MoveOperation op)
{
Point o = tdata.getDragStartLocation();
p.x = p.x-o.x;
p.y = p.y-o.y;
JTModuleContainer jtmc = this;
PUndoableEditSupport ues = jtmc.getModuleContainer().getEditSupport();
JTCableManager cm = jtmc.getCableManager();
PModuleContainer mc = jtmc.getModuleContainer();
for (PModule module: tdata.getModules()) {
op.add(module);
}
op.setScreenOffset(p.x, p.y);
try
{
cm.setAutoRepaintDisabled();
String name = (op instanceof CopyOperation ? "copy modules" : "move modules");
if (tdata.getModules().size() > 1)
ues.beginUpdate(name);
else
ues.beginUpdate();
try {
op.move();
} finally {
ues.endUpdate();
}
Collection<? extends PModule> moved = op.getMovedModules();
int maxx = 0;
int maxy = 0;
for (JTModule jtmodule: NmSwingUtilities.getChildren(JTModule.class, jtmc))
{
PModule module = jtmodule.getModule();
if (moved.contains(module))
{
jtmodule.setLocation(module.getScreenLocation());
maxx = Math.max(jtmodule.getX(), maxx)+jtmodule.getWidth();
maxy = Math.max(jtmodule.getY(), maxy)+jtmodule.getHeight();
}
}
Collection<Cable> cables = new ArrayList<Cable>(20);
cm.getCables(cables, moved);
for (Cable cable: cables)
{
cm.update(cable);
cable.updateEndPoints();
cm.update(cable);
}
jtmc.revalidate();
jtmc.repaint();
}
finally
{
cm.clearAutoRepaintDisabled();
}
}
public void installModulesMenu(JPopupMenu menu) {
PModuleContainer container = getModuleContainer();
ModuleDescriptions modules = getModuleContainer().getPatch().getModuleDescriptions();
Map<String, List<PModuleDescriptor>> categoryMap = new HashMap<String, List<PModuleDescriptor>>();
for (PModuleDescriptor module : modules)
{
if (module.isInstanciable() && container.canAdd(module))
{
String cat = module.getCategory();
List<PModuleDescriptor> catList = categoryMap.get(cat);
if (catList == null)
{
catList = new ArrayList<PModuleDescriptor>();
categoryMap.put(cat, catList);
}
catList.add(module);
}
}
Comparator<PModuleDescriptor> order = new DescriptorNameComparator<PModuleDescriptor>();
List<String> categories = new ArrayList<String>();
categories.addAll(categoryMap.keySet());
Collections.sort(categories);
for (String cat: categories)
{
List<PModuleDescriptor> catList = categoryMap.get(cat);
Collections.sort(catList, order);
JMenu catMenu = new JMenu(cat);
for (PModuleDescriptor m: catList)
{
catMenu.add(new InsertModuleAction(m, this));
}
menu.add(catMenu);
}
}
}