/*
* CircuitPanel.java
* (FScape)
*
* Copyright (c) 2001-2016 Hanns Holger Rutz. All rights reserved.
*
* This software is published under the GNU General Public License v3+
*
*
* For further information, please contact Hanns Holger Rutz at
* contact@sciss.de
*
*
* Changelog:
* 05-Mar-05 extends JPanel instead of Panel
*/
package de.sciss.fscape.gui;
import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* Panel for arranging symbolic block
* elements in series or parallel such
* as in the FIR filter processing module.
*/
public class CircuitPanel
extends JPanel {
// -------- public variables --------
/**
* ActionCommands, die ein registrierter (AddActionListener())
* ActionListener empfaengt. Angefuegt wird die uniqueID des
* correspondierenden Boxes
*/
public static final String ACTION_BOXSELECTED = "act";
public static final String ACTION_BOXDESELECTED = "des";
public static final String ACTION_BOXCREATED = "new";
public static final String ACTION_BOXDELETED = "rem";
public static final int TYPE_GROUND = 0;
public static final int TYPE_SERIAL = 1;
public static final int TYPE_PARALLEL = 2;
public static final int TYPE_BOX = 3;
// -------- private variables --------
// gerade angewaehlte Box; wenn keiner ausgewaehlt, muss
// er auf dummyBox gesetzt werden, damit keine Null-Pointer Fehler
// auftreten koennen!
protected CircuitPanel.Box currentBox = null;
protected CircuitPanel ground;
protected final CircuitPanel.Box protoType;
protected final java.util.List<Object> boxes = new ArrayList<Object>();
protected final Map<Object, JComponent> mapBoxToView; // only for ground!
protected int typ;
// dieser Button wird nicht angezeigt, sondern dient nur als
// Institution zum Verwalten der ActionListener und fuer
// den Event-Dispatch!
private Button actionComponent;
// Fehlermeldungen
// private static final String ERR_CORRUPTED = "Internal data corrupted. Please report bug!";
private static final int DIR_WEST = 0x01; // they add up as DIR_WEST + DIR_NORTH
private static final int DIR_EAST = 0x02;
private static final int DIR_NORTH = 0x10;
private static final int DIR_SOUTH = 0x20;
private static final Color colrConLight = new Color(0x00, 0x00, 0x00, 0x7F);
private static final Color colrConDark = new Color(0xFF, 0xFF, 0xFF, 0x7F);
private GridBagLayout lay;
private GridBagConstraints con;
protected final CircuitPanel enc_cp = this;
private final boolean isDark = UIManager.getBoolean("dark-skin");
// -------- public methods --------
/**
* @param protoType Basis-Objekt ueber dessen nichtparametrisierten Constructor
* neue Boxen erzeugt werden
*/
public CircuitPanel(CircuitPanel.Box protoType) {
super();
typ = TYPE_GROUND;
ground = this;
actionComponent = new Button();
this.protoType = protoType;
mapBoxToView = new HashMap<Object, JComponent>();
init();
setCircuit("");
}
protected CircuitPanel(CircuitPanel ground, String circuit) {
super();
typ = (int) circuit.charAt(0) - 48;
this.ground = ground;
this.protoType = ground.protoType;
mapBoxToView = null;
init();
setCircuit(circuit);
}
public void repaintBox(CircuitPanel.Box box) {
Component c = ground.mapBoxToView.get(box);
if (c != null) c.repaint();
}
protected void insertBox(Object o, int index) {
JComponent view;
int numBoxes;
Object o2;
Component c;
if (o instanceof CircuitPanel.Box) {
view = new JToggleButton(new BoxAction((CircuitPanel.Box) o));
} else if (o instanceof CircuitPanel) {
view = (CircuitPanel) o;
} else {
assert false : o.getClass().getName();
view = null;
}
con.gridx = 1;
con.gridy = 1;
switch (typ) {
case TYPE_SERIAL:
con.gridx += index;
break;
case TYPE_PARALLEL:
con.gridy += index;
break;
}
lay.setConstraints(view, con);
this.add(view, index);
boxes.add(index, o);
ground.mapBoxToView.put(o, view);
numBoxes = boxes.size();
for (int i = ++index; i < numBoxes; i++) {
switch (typ) {
case TYPE_SERIAL:
con.gridx++;
break;
case TYPE_PARALLEL:
con.gridy++;
break;
}
o2 = boxes.get(i);
if (o2 instanceof Box) {
c = ground.mapBoxToView.get(o2);
} else if (o2 instanceof CircuitPanel) {
c = (CircuitPanel) o2;
} else {
assert false : o2.getClass().getName();
c = null;
}
lay.setConstraints(c, con);
}
if (this.isVisible()) {
invalidate();
ground.validate();
}
notifyListeners(ACTION_BOXCREATED);
}
protected void removeBox(int index) {
final Object o = boxes.remove(index);
Component c;
Object o2;
int numBoxes;
if (o instanceof CircuitPanel.Box) {
c = ground.mapBoxToView.remove(o);
if (c != null) {
this.remove(c);
}
} else if (o instanceof CircuitPanel) {
((CircuitPanel) o).clear();
this.remove((CircuitPanel) o);
} else {
assert false : o.getClass().getName();
}
con.gridx = 1;
con.gridy = 1;
switch (typ) {
case TYPE_SERIAL:
con.gridx += index;
break;
case TYPE_PARALLEL:
con.gridy += index;
break;
}
numBoxes = boxes.size();
for (int i = index; i < numBoxes; i++) {
o2 = boxes.get(i);
if (o2 instanceof Box) {
c = ground.mapBoxToView.get(o2);
} else if (o2 instanceof CircuitPanel) {
c = (CircuitPanel) o2;
} else {
assert false : o2.getClass().getName();
c = null;
}
lay.setConstraints(c, con);
switch (typ) {
case TYPE_SERIAL:
con.gridx++;
break;
case TYPE_PARALLEL:
con.gridy++;
break;
}
}
if (this.isVisible()) {
invalidate();
ground.validate();
}
notifyListeners(ACTION_BOXDELETED);
}
private void clear() {
Object o;
Component c;
while (!boxes.isEmpty()) {
o = boxes.remove(0);
if (o instanceof CircuitPanel.Box) {
c = ground.mapBoxToView.remove(o);
if (c != null) {
this.remove(c);
}
} else if (o instanceof CircuitPanel) {
((CircuitPanel) o).clear();
this.remove((CircuitPanel) o);
} else {
assert false : o.getClass().getName();
}
}
}
private void init() {
this.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
CircuitPanel.Box box;
Object o;
Component c;
CircuitPanel cp;
boolean b;
int dir, i, numBoxes;
if (!enc_cp.isEnabled()) return; // not while inactive
if( e.getSource() == enc_cp ) { //-------- Panel hit -------------------
if( (e.getClickCount() == 2) && !e.isAltDown() ) {
box = ground.protoType.duplicate();
numBoxes = boxes.size();
if( numBoxes == 0 ) {
insertBox( box, 0 );
} else {
switch( typ ) {
case TYPE_GROUND:
o = boxes.get( 0 );
if( o instanceof CircuitPanel.Box ) {
c = ground.mapBoxToView.get( o );
} else if( o instanceof CircuitPanel ) {
c = (CircuitPanel) o;
} else {
assert false : o.getClass().getName();
c = null;
}
dir = calcDirection( c, e.getX(), e.getY() );
if( (dir != DIR_WEST) && (dir != DIR_EAST) &&
(dir != DIR_NORTH) && (dir != DIR_SOUTH) ) break;
b = ((dir == DIR_WEST) || (dir == DIR_NORTH));
cp = new CircuitPanel( ground, (((dir == DIR_WEST) || (dir == DIR_EAST)) ? TYPE_SERIAL : TYPE_PARALLEL) +
encodeBox( b ? box : boxes.get( 0 )) + encodeBox( b ? boxes.get( 0 ) : box ));
removeBox( 0 );
insertBox( cp, 0 );
break;
case TYPE_SERIAL:
serialLp:
for (i = 0; i < numBoxes; i++) {
o = boxes.get(i);
if (o instanceof CircuitPanel.Box) {
c = ground.mapBoxToView.get(o);
} else if (o instanceof CircuitPanel) {
c = (CircuitPanel) o;
} else {
assert false : o.getClass().getName();
c = null;
}
dir = calcDirection(c, e.getX(), e.getY());
switch (dir) {
case DIR_NORTH:
case DIR_SOUTH:
b = (dir == DIR_NORTH);
cp = new CircuitPanel(ground, TYPE_PARALLEL + encodeBox(b ? box : boxes.get(i)) + encodeBox(b ? boxes.get(i) : box));
removeBox(i);
insertBox(cp, i);
break serialLp;
case DIR_WEST:
insertBox(box, i);
break serialLp;
case DIR_EAST:
if (i == numBoxes - 1) {
insertBox(box, numBoxes);
break serialLp;
}
break;
}
}
break;
case TYPE_PARALLEL:
parallelLp:
for (i = 0; i < numBoxes; i++) {
o = boxes.get(i);
if (o instanceof CircuitPanel.Box) {
c = ground.mapBoxToView.get(o);
} else if (o instanceof CircuitPanel) {
c = (CircuitPanel) o;
} else {
assert false : o.getClass().getName();
c = null;
}
dir = calcDirection(c, e.getX(), e.getY());
switch (dir) {
case DIR_WEST:
case DIR_EAST:
b = (dir == DIR_WEST);
cp = new CircuitPanel(ground, TYPE_SERIAL + encodeBox(b ? box : boxes.get(i)) + encodeBox(b ? boxes.get(i) : box));
removeBox(i);
insertBox(cp, i);
break parallelLp;
case DIR_NORTH:
insertBox(box, i);
break parallelLp;
case DIR_SOUTH:
if (i == numBoxes - 1) {
insertBox(box, numBoxes);
break parallelLp;
}
break;
}
}
break;
}
}
AbstractButton ab = (AbstractButton) ground.mapBoxToView.get(box);
if (ab != null) ab.doClick();
}
}
}
});
javax.swing.Icon icn = ground.protoType.getIcon();
setMinimumSize(new Dimension(icn.getIconWidth() + 8, icn.getIconHeight() + 8));
final Border bd;
if (ground == this) {
bd = BorderFactory.createEmptyBorder(2, 4, 2, 4);
} else {
final Color bdColor = isDark ? Color.darkGray : Color.lightGray;
bd = BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(bdColor, 2),
BorderFactory.createEmptyBorder(0, 2, 0, 2)
);
}
setBorder(bd);
lay = new GridBagLayout();
con = new GridBagConstraints();
con.gridx = 1;
con.gridy = 1;
con.gridwidth = 1;
con.gridheight = 1;
con.weightx = 0.0;
con.weighty = 0.0;
con.fill = GridBagConstraints.NONE;
con.anchor = GridBagConstraints.CENTER;
con.insets = new Insets(3, 3, 3, 3);
setLayout( lay );
}
/**
* Blockschaltbild zuweisen
* Aufbau: <type><obj1>{<obj1settings}<obj2>{<obj2settings>} etc.
*/
public void setCircuit(String circuit) {
Object o;
int numBoxes;
int subType;
String settings;
int cIndex;
clear();
cIndex = 1;
numBoxes = parseNumBoxes(circuit, cIndex);
boxLp:
for (int i = 0; i < numBoxes; i++) {
subType = (int) circuit.charAt(cIndex++) - 48;
settings = parseSettings(circuit, cIndex);
cIndex += settings.length() + 2; // plus brackets
o = null;
switch (subType) {
case TYPE_SERIAL:
case TYPE_PARALLEL:
o = new CircuitPanel(ground, settings);
break;
case TYPE_BOX:
o = ground.protoType.fromString(settings);
break;
default:
break boxLp;
}
if (o != null) {
insertBox(o, i);
}
}
}
/**
* Blockschaltbild besorgen
*/
public String getCircuit() {
return this.toString();
}
public Iterator getElements() {
return boxes.iterator();
}
public int getType() {
return typ;
}
/**
* Registriert einen ActionListener;
* Action-Events kommen, wenn sich der Wert des ParamFieldes aendert
*/
public void addActionListener(ActionListener list) {
ground.actionComponent.addActionListener(list);
}
/**
* Entfernt einen ActionListener
*/
public void removeActionListener(ActionListener list) {
ground.actionComponent.removeActionListener(list);
}
/**
* Aktuelle Box besorgen
*
* @return null, wenn Fehler oder keine Box angewaehlt
*/
public CircuitPanel.Box getActiveBox() {
return ground.currentBox;
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
Dimension dim = getSize();
int midY = dim.height >> 1;
int cMidY;
Component c[];
Rectangle bounds;
g.setColor(isDark ? colrConDark : colrConLight);
if (typ != TYPE_SERIAL) {
g.fillRect(2, midY - 2, 2, 5);
g.fillRect(dim.width - 4, midY - 2, 2, 5);
}
if (typ == TYPE_PARALLEL) {
c = getComponents();
int maxMid = 0, minMid = dim.height;
for (int i = 0; i < c.length; i++) {
bounds = c[i].getBounds();
cMidY = bounds.y + (bounds.height >> 1);
g.drawLine(4, cMidY, dim.width - 5, cMidY);
if (cMidY < minMid) minMid = cMidY;
if (cMidY > maxMid) maxMid = cMidY;
}
g.drawLine(3, minMid, 3, midY - 3);
g.drawLine(3, midY + 3, 3, maxMid);
g.drawLine(dim.width - 4, minMid, dim.width - 4, midY - 3);
g.drawLine(dim.width - 4, midY + 3, dim.width - 4, maxMid);
} else {
g.drawLine(4, midY, dim.width - 5, midY);
}
}
// -------- private methods --------
protected void notifyListeners(String actionStr) {
ActionEvent e;
e = new ActionEvent(ground, ActionEvent.ACTION_PERFORMED, actionStr);
ground.actionComponent.dispatchEvent(e);
}
protected int calcDirection(Component c, int x, int y) {
Rectangle bounds = c.getBounds();
int dir = 0;
if( x < bounds.x ) dir = DIR_WEST;
else if( x >= bounds.x + bounds.width ) dir = DIR_EAST;
if( y < bounds.y ) dir += DIR_NORTH;
else if( y >= bounds.y + bounds.height ) dir += DIR_SOUTH;
return dir;
}
private int parseNumBoxes(String circuit, int cIndexStart) {
int numBoxes, bracketCount;
char c;
int cIndex = cIndexStart;
for( numBoxes = 0; cIndex < circuit.length(); ) {
c = circuit.charAt( cIndex++ );
if( c == '{' ) {
numBoxes++;
for( bracketCount = 1; (bracketCount > 0) && (cIndex < circuit.length()); ) {
c = circuit.charAt( cIndex++ );
if( c == '{' ) {
bracketCount++;
} else if( c == '}' ) {
bracketCount--;
}
}
if( bracketCount != 0 ) return 0; // corrupted!
}
}
return numBoxes;
}
private String parseSettings(String circuit, int cIndexStart) {
int startID, endID, bracketCount;
char ch;
int cIndex = cIndexStart;
while( cIndex < circuit.length() ) {
ch = circuit.charAt( cIndex++ );
if( ch == '{' ) {
startID = cIndex;
for( bracketCount = 1; (bracketCount > 0) && (cIndex < circuit.length()); ) {
ch = circuit.charAt( cIndex++ );
if( ch == '{' ) {
bracketCount++;
} else if( ch == '}' ) {
bracketCount--;
}
}
if( bracketCount != 0 ) return ""; // corrupted!
endID = cIndex - 1;
return( circuit.substring( startID, endID ));
}
}
return "";
}
protected String encodeBox(Object o) {
if (o instanceof CircuitPanel.Box) {
return (String.valueOf(TYPE_BOX) + '{' + o.toString() + '}');
} else if (o instanceof CircuitPanel) {
return (String.valueOf(((CircuitPanel) o).typ) + '{' + o.toString() + '}');
} else {
assert false : o.getClass().getName();
}
return null;
}
public String toString() {
StringBuffer str = new StringBuffer();
str.append(typ);
synchronized (boxes) {
for (int i = 0; i < boxes.size(); i++) {
str.append(encodeBox(boxes.get(i)));
}
}
return str.toString();
}
// ---------- internal classes / interfaces ----------
public interface Box {
public javax.swing.Icon getIcon();
public CircuitPanel.Box duplicate();
public String toString();
public CircuitPanel.Box fromString(String s);
}
private class BoxAction
extends AbstractAction {
private CircuitPanel.Box box;
protected BoxAction(CircuitPanel.Box box) {
super();
putValue(SMALL_ICON, box.getIcon());
this.box = box;
}
public void actionPerformed(ActionEvent e) {
AbstractButton button = (AbstractButton) e.getSource();
if ((e.getModifiers() & ActionEvent.ALT_MASK) != 0) {
if (ground.currentBox != null) {
AbstractButton otherButton = (AbstractButton) ground.mapBoxToView.get(ground.currentBox);
if ((otherButton) != null) {
otherButton.setSelected(false);
}
ground.currentBox = null;
notifyListeners(ACTION_BOXDESELECTED);
}
int i = boxes.indexOf(this.box);
if (i >= 0) {
removeBox(i);
if ((enc_cp != ground) && (boxes.size() < 2)) { // replace panel by box
Object o2 = null;
if (boxes.size() == 1) {
o2 = boxes.get(0);
removeBox(0);
}
CircuitPanel parent = (CircuitPanel) enc_cp.getParent();
int index = parent.boxes.indexOf(enc_cp);
parent.removeBox(index);
if (o2 != null) {
parent.insertBox(o2, index);
}
} else if (boxes.isEmpty()) { // kill panel
// CircuitPanel parent = (CircuitPanel) enc_cp.getParent();
}
}
} else {
if (button.isSelected()) {
if (ground.currentBox != null) {
AbstractButton otherButton = (AbstractButton) ground.mapBoxToView.get(ground.currentBox);
if ((otherButton) != null && (otherButton != button)) {
otherButton.setSelected(false);
}
}
ground.currentBox = this.box;
notifyListeners(ACTION_BOXSELECTED);
} else {
if (ground.currentBox == this.box) {
ground.currentBox = null;
}
notifyListeners(ACTION_BOXDESELECTED);
}
}
}
}
}