/* Copyright (c) 2010, Carl Burch. License information is located in the
* com.cburch.logisim.Main source code and at www.cburch.com/logisim/. */
package com.cburch.logisim.gui.main;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.dnd.DnDConstants;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.Icon;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPopupMenu;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.ToolTipManager;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeSelectionModel;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import com.cburch.logisim.circuit.Circuit;
import com.cburch.logisim.circuit.CircuitEvent;
import com.cburch.logisim.circuit.CircuitListener;
import com.cburch.logisim.circuit.SubcircuitFactory;
import com.cburch.logisim.comp.ComponentFactory;
import com.cburch.logisim.comp.ComponentDrawContext;
import com.cburch.logisim.file.LibraryEventSource;
import com.cburch.logisim.file.LogisimFile;
import com.cburch.logisim.file.LibraryEvent;
import com.cburch.logisim.file.LibraryListener;
import com.cburch.logisim.prefs.AppPreferences;
import com.cburch.logisim.proj.Project;
import com.cburch.logisim.proj.ProjectEvent;
import com.cburch.logisim.proj.ProjectListener;
import com.cburch.logisim.tools.AddTool;
import com.cburch.logisim.tools.Library;
import com.cburch.logisim.tools.Tool;
import com.cburch.logisim.util.JTreeDragController;
import com.cburch.logisim.util.JTreeUtil;
import com.cburch.logisim.util.LocaleListener;
import com.cburch.logisim.util.LocaleManager;
public class ProjectExplorer extends JTree implements LocaleListener {
private static final String DIRTY_MARKER = "*";
static final Color MAGNIFYING_INTERIOR = new Color(200, 200, 255, 64);
public static class Event {
private TreePath path;
private Event(TreePath path) {
this.path = path;
}
public TreePath getTreePath() {
return path;
}
public Object getTarget() {
return path == null ? null : path.getLastPathComponent();
}
}
public static interface Listener {
public void selectionChanged(Event event);
public void doubleClicked(Event event);
public void moveRequested(Event event, AddTool dragged, AddTool target);
public void deleteRequested(Event event);
public JPopupMenu menuRequested(Event event);
}
private class MyModel implements TreeModel {
ArrayList<TreeModelListener> listeners = new ArrayList<TreeModelListener>();
public void addTreeModelListener(TreeModelListener l) {
listeners.add(l);
}
public void removeTreeModelListener(TreeModelListener l) {
listeners.remove(l);
}
public Object getRoot() {
return proj.getLogisimFile();
}
private List<?> getChildren(Object parent) {
if (parent == proj.getLogisimFile()) {
return ((Library) parent).getElements();
} else if (parent instanceof Library) {
return ((Library) parent).getTools();
} else {
return Collections.EMPTY_LIST;
}
}
public Object getChild(Object parent, int index) {
return getChildren(parent).get(index);
}
public int getChildCount(Object parent) {
return getChildren(parent).size();
}
public int getIndexOfChild(Object parent, Object query) {
if (parent == null || query == null) return -1;
int index = -1;
for (Object child : getChildren(parent)) {
index++;
if (child == query) return index;
}
return -1;
}
public boolean isLeaf(Object node) {
return node != proj && !(node instanceof Library);
}
public void valueForPathChanged(TreePath path, Object value) {
TreeModelEvent e = new TreeModelEvent(ProjectExplorer.this, path);
fireNodesChanged(Collections.singletonList(e));
}
private void fireNodesChanged(List<TreeModelEvent> events) {
for (TreeModelEvent e : events) {
for (TreeModelListener l : listeners) {
l.treeNodesChanged(e);
}
}
}
void fireStructureChanged() {
TreeModelEvent e = new TreeModelEvent(ProjectExplorer.this,
new Object[] { model.getRoot() });
for (TreeModelListener l : listeners) {
l.treeStructureChanged(e);
}
ProjectExplorer.this.repaint();
}
private ArrayList<TreeModelEvent> findPaths(Object value) {
ArrayList<TreeModelEvent> ret = new ArrayList<TreeModelEvent>();
ArrayList<Object> stack = new ArrayList<Object>();
findPathsSub(value, getRoot(), stack, ret);
return ret;
}
private void findPathsSub(Object value, Object node,
ArrayList<Object> stack, ArrayList<TreeModelEvent> paths) {
stack.add(node);
if (node == value) {
TreePath path = new TreePath(stack.toArray());
paths.add(new TreeModelEvent(ProjectExplorer.this, path));
}
for (Object child : getChildren(node)) {
findPathsSub(value, child, stack, paths);
}
stack.remove(stack.size() - 1);
}
private ArrayList<TreeModelEvent> findPathsForTools(Library value) {
ArrayList<TreeModelEvent> ret = new ArrayList<TreeModelEvent>();
ArrayList<Object> stack = new ArrayList<Object>();
findPathsForToolsSub(value, getRoot(), stack, ret);
return ret;
}
private void findPathsForToolsSub(Library value, Object node,
ArrayList<Object> stack, ArrayList<TreeModelEvent> paths) {
stack.add(node);
if (node == value) {
TreePath path = new TreePath(stack.toArray());
List<? extends Tool> toolList = value.getTools();
int[] indices = new int[toolList.size()];
Object[] tools = new Object[indices.length];
for (int i = 0; i < indices.length; i++) {
indices[i] = i;
tools[i] = toolList.get(i);
}
paths.add(new TreeModelEvent(ProjectExplorer.this, path, indices, tools));
}
for (Object child : getChildren(node)) {
findPathsForToolsSub(value, child, stack, paths);
}
stack.remove(stack.size() - 1);
}
}
private class ToolIcon implements Icon {
Tool tool;
Circuit circ = null;
ToolIcon(Tool tool) {
this.tool = tool;
if (tool instanceof AddTool) {
ComponentFactory fact = ((AddTool) tool).getFactory(false);
if (fact instanceof SubcircuitFactory) {
circ = ((SubcircuitFactory) fact).getSubcircuit();
}
}
}
public int getIconHeight() {
return 20;
}
public int getIconWidth() {
return 20;
}
public void paintIcon(java.awt.Component c, Graphics g,
int x, int y) {
// draw halo if appropriate
if (tool == haloedTool && AppPreferences.ATTRIBUTE_HALO.getBoolean()) {
g.setColor(Canvas.HALO_COLOR);
g.fillRoundRect(x, y, 20, 20, 10, 10);
g.setColor(Color.BLACK);
}
// draw tool icon
Graphics gIcon = g.create();
ComponentDrawContext context = new ComponentDrawContext(ProjectExplorer.this, null, null, g, gIcon);
tool.paintIcon(context, x, y);
gIcon.dispose();
// draw magnifying glass if appropriate
if (circ == proj.getCurrentCircuit()) {
int tx = x + 13;
int ty = y + 13;
int[] xp = { tx - 1, x + 18, x + 20, tx + 1 };
int[] yp = { ty + 1, y + 20, y + 18, ty - 1 };
g.setColor(MAGNIFYING_INTERIOR);
g.fillOval(x + 5, y + 5, 10, 10);
g.setColor(Color.BLACK);
g.drawOval(x + 5, y + 5, 10, 10);
g.fillPolygon(xp, yp, xp.length);
}
}
}
private class MyCellRenderer extends DefaultTreeCellRenderer {
@Override
public java.awt.Component getTreeCellRendererComponent(
JTree tree, Object value, boolean selected,
boolean expanded, boolean leaf, int row,
boolean hasFocus) {
java.awt.Component ret;
ret = super.getTreeCellRendererComponent(tree, value,
selected, expanded, leaf, row, hasFocus);
if (ret instanceof JComponent) {
JComponent comp = (JComponent) ret;
comp.setToolTipText(null);
}
if (value instanceof Tool) {
Tool tool = (Tool) value;
if (ret instanceof JLabel) {
((JLabel) ret).setText(tool.getDisplayName());
((JLabel) ret).setIcon(new ToolIcon(tool));
((JLabel) ret).setToolTipText(tool.getDescription());
}
} else if (value instanceof Library) {
if (ret instanceof JLabel) {
Library lib = (Library) value;
String text = lib.getDisplayName();
if (lib.isDirty()) text += DIRTY_MARKER;
((JLabel) ret).setText(text);
}
}
return ret;
}
}
private class MySelectionModel extends DefaultTreeSelectionModel {
@Override
public void addSelectionPath(TreePath path) {
if (isPathValid(path)) super.addSelectionPath(path);
}
@Override
public void setSelectionPath(TreePath path) {
if (isPathValid(path)) super.setSelectionPath(path);
}
@Override
public void addSelectionPaths(TreePath[] paths) {
paths = getValidPaths(paths);
if (paths != null) super.addSelectionPaths(paths);
}
@Override
public void setSelectionPaths(TreePath[] paths) {
paths = getValidPaths(paths);
if (paths != null) super.setSelectionPaths(paths);
}
private TreePath[] getValidPaths(TreePath[] paths) {
int count = 0;
for (int i = 0; i < paths.length; i++) {
if (isPathValid(paths[i])) ++count;
}
if (count == 0) {
return null;
} else if (count == paths.length) {
return paths;
} else {
TreePath[] ret = new TreePath[count];
int j = 0;
for (int i = 0; i < paths.length; i++) {
if (isPathValid(paths[i])) ret[j++] = paths[i];
}
return ret;
}
}
private boolean isPathValid(TreePath path) {
if (path == null || path.getPathCount() > 3) return false;
Object last = path.getLastPathComponent();
return last instanceof Tool;
}
}
private class DragController implements JTreeDragController {
public boolean canPerformAction(JTree targetTree,
Object draggedNode, int action, Point location) {
TreePath pathTarget = targetTree.getPathForLocation(location.x, location.y);
if (pathTarget == null) {
targetTree.setSelectionPath(null);
return false;
}
targetTree.setSelectionPath(pathTarget);
if (action == DnDConstants.ACTION_COPY) {
return false;
} else if (action == DnDConstants.ACTION_MOVE) {
Object targetNode = pathTarget.getLastPathComponent();
return canMove(draggedNode, targetNode);
} else {
return false;
}
}
public boolean executeDrop(JTree targetTree, Object draggedNode,
Object targetNode, int action) {
if (action == DnDConstants.ACTION_COPY) {
return false;
} else if (action == DnDConstants.ACTION_MOVE) {
if (canMove(draggedNode, targetNode)) {
if (draggedNode == targetNode) return true;
listener.moveRequested(new Event(null), (AddTool) draggedNode, (AddTool) targetNode);
return true;
} else {
return false;
}
} else {
return false;
}
}
private boolean canMove(Object draggedNode, Object targetNode) {
if (listener == null) return false;
if (!(draggedNode instanceof AddTool) || !(targetNode instanceof AddTool)) return false;
LogisimFile file = proj.getLogisimFile();
AddTool dragged = (AddTool) draggedNode;
AddTool target = (AddTool) targetNode;
int draggedIndex = file.getTools().indexOf(dragged);
int targetIndex = file.getTools().indexOf(target);
if (targetIndex < 0 || draggedIndex < 0) return false;
return true;
}
}
private class DeleteAction extends AbstractAction {
public void actionPerformed(ActionEvent event) {
TreePath path = getSelectionPath();
if (listener != null && path != null && path.getPathCount() == 2) {
listener.deleteRequested(new Event(path));
}
ProjectExplorer.this.requestFocus();
}
}
private class MyListener
implements MouseListener, TreeSelectionListener,
ProjectListener, LibraryListener, CircuitListener, PropertyChangeListener {
//
// MouseListener methods
//
public void mouseEntered(MouseEvent e) { }
public void mouseExited(MouseEvent e) { }
public void mousePressed(MouseEvent e) {
ProjectExplorer.this.requestFocus();
checkForPopup(e);
}
public void mouseReleased(MouseEvent e) {
checkForPopup(e);
}
private void checkForPopup(MouseEvent e) {
if (e.isPopupTrigger()) {
TreePath path = getPathForLocation(e.getX(), e.getY());
if (path != null && listener != null) {
JPopupMenu menu = listener.menuRequested(new Event(path));
if (menu != null) {
menu.show(ProjectExplorer.this, e.getX(), e.getY());
}
}
}
}
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
TreePath path = getPathForLocation(e.getX(), e.getY());
if (path != null && listener != null) {
listener.doubleClicked(new Event(path));
}
}
}
//
// TreeSelectionListener methods
//
public void valueChanged(TreeSelectionEvent e) {
TreePath path = e.getNewLeadSelectionPath();
if (listener != null) {
listener.selectionChanged(new Event(path));
}
}
//
// project/library file/circuit listener methods
//
public void projectChanged(ProjectEvent event) {
int act = event.getAction();
if (act == ProjectEvent.ACTION_SET_TOOL) {
TreePath path = getSelectionPath();
if (path != null && path.getLastPathComponent() != event.getTool()) {
clearSelection();
}
} else if (act == ProjectEvent.ACTION_SET_FILE) {
setFile(event.getLogisimFile());
} else if (act == ProjectEvent.ACTION_SET_CURRENT) {
ProjectExplorer.this.repaint();
}
}
public void libraryChanged(LibraryEvent event) {
int act = event.getAction();
if (act == LibraryEvent.ADD_TOOL) {
if (event.getData() instanceof AddTool) {
AddTool tool = (AddTool) event.getData();
if (tool.getFactory() instanceof SubcircuitFactory) {
SubcircuitFactory fact = (SubcircuitFactory) tool.getFactory();
fact.getSubcircuit().addCircuitListener(this);
}
}
} else if (act == LibraryEvent.REMOVE_TOOL) {
if (event.getData() instanceof AddTool) {
AddTool tool = (AddTool) event.getData();
if (tool.getFactory() instanceof SubcircuitFactory) {
SubcircuitFactory fact = (SubcircuitFactory) tool.getFactory();
fact.getSubcircuit().removeCircuitListener(this);
}
}
} else if (act == LibraryEvent.ADD_LIBRARY) {
if (event.getData() instanceof LibraryEventSource) {
((LibraryEventSource) event.getData()).addLibraryListener(subListener);
}
} else if (act == LibraryEvent.REMOVE_LIBRARY) {
if (event.getData() instanceof LibraryEventSource) {
((LibraryEventSource) event.getData()).removeLibraryListener(subListener);
}
}
Library lib = event.getSource();
switch (act) {
case LibraryEvent.DIRTY_STATE:
case LibraryEvent.SET_NAME:
model.fireNodesChanged(model.findPaths(lib));
break;
case LibraryEvent.MOVE_TOOL:
model.fireNodesChanged(model.findPathsForTools(lib));
break;
case LibraryEvent.SET_MAIN:
break;
default:
model.fireStructureChanged();
}
}
public void circuitChanged(CircuitEvent event) {
int act = event.getAction();
if (act == CircuitEvent.ACTION_SET_NAME) {
model.fireStructureChanged();
// The following almost works - but the labels aren't made
// bigger, so you get "..." behavior with longer names.
// model.fireNodesChanged(model.findPaths(event.getCircuit()));
}
}
private void setFile(LogisimFile lib) {
model.fireStructureChanged();
expandRow(0);
for (Circuit circ : lib.getCircuits()) {
circ.addCircuitListener(this);
}
subListener = new SubListener(); // create new one so that old listeners die away
for (Library sublib : lib.getLibraries()) {
if (sublib instanceof LibraryEventSource) {
((LibraryEventSource) sublib).addLibraryListener(subListener);
}
}
}
//
// PropertyChangeListener methods
//
public void propertyChange(PropertyChangeEvent event) {
if (AppPreferences.GATE_SHAPE.isSource(event)) {
repaint();
}
}
}
private class SubListener implements LibraryListener {
public void libraryChanged(LibraryEvent event) {
model.fireStructureChanged();
}
}
private Project proj;
private MyListener myListener = new MyListener();
private SubListener subListener = new SubListener();
private MyModel model = new MyModel();
private MyCellRenderer renderer = new MyCellRenderer();
private DeleteAction deleteAction = new DeleteAction();
private Listener listener = null;
private Tool haloedTool = null;
public ProjectExplorer(Project proj) {
super();
this.proj = proj;
setModel(model);
setRootVisible(true);
addMouseListener(myListener);
ToolTipManager.sharedInstance().registerComponent(this);
MySelectionModel selector = new MySelectionModel();
selector.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
setSelectionModel(selector);
setCellRenderer(renderer);
JTreeUtil.configureDragAndDrop(this, new DragController());
addTreeSelectionListener(myListener);
InputMap imap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), deleteAction);
ActionMap amap = getActionMap();
amap.put(deleteAction, deleteAction);
proj.addProjectListener(myListener);
proj.addLibraryListener(myListener);
AppPreferences.GATE_SHAPE.addPropertyChangeListener(myListener);
myListener.setFile(proj.getLogisimFile());
LocaleManager.addLocaleListener(this);
}
public Tool getSelectedTool() {
TreePath path = getSelectionPath();
if (path == null) return null;
Object last = path.getLastPathComponent();
return last instanceof Tool ? (Tool) last : null;
}
public void setListener(Listener value) {
listener = value;
}
public void setHaloedTool(Tool t) {
if (haloedTool == t) return;
haloedTool = t;
repaint();
}
public void localeChanged() {
model.fireStructureChanged();
}
}