/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2017 Neil C Smith.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3 only, as
* published by the Free Software Foundation.
*
* This code 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
* version 3 for more details.
*
* You should have received a copy of the GNU General Public License version 3
* along with this work; if not, see http://www.gnu.org/licenses/
*
*
* Please visit http://neilcsmith.net if you need additional information or
* have any questions.
*/
package net.neilcsmith.praxis.live.pxr.graph;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyVetoException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.*;
import javax.swing.border.LineBorder;
import javax.swing.text.DefaultEditorKit;
import net.neilcsmith.praxis.core.CallArguments;
import net.neilcsmith.praxis.core.ComponentType;
import net.neilcsmith.praxis.core.info.ComponentInfo;
import net.neilcsmith.praxis.core.info.PortInfo;
import net.neilcsmith.praxis.core.interfaces.ComponentInterface;
import net.neilcsmith.praxis.core.interfaces.ContainerInterface;
import net.neilcsmith.praxis.live.core.api.Callback;
import net.neilcsmith.praxis.live.core.api.Syncable;
import net.neilcsmith.praxis.live.graph.Alignment;
import net.neilcsmith.praxis.live.graph.EdgeID;
import net.neilcsmith.praxis.live.graph.EdgeWidget;
import net.neilcsmith.praxis.live.graph.NodeWidget;
import net.neilcsmith.praxis.live.graph.ObjectSceneAdaptor;
import net.neilcsmith.praxis.live.graph.PinID;
import net.neilcsmith.praxis.live.graph.PinWidget;
import net.neilcsmith.praxis.live.graph.PraxisGraphScene;
import net.neilcsmith.praxis.live.model.ComponentProxy;
import net.neilcsmith.praxis.live.model.Connection;
import net.neilcsmith.praxis.live.model.ContainerProxy;
import net.neilcsmith.praxis.live.model.ProxyException;
import net.neilcsmith.praxis.live.model.RootProxy;
import net.neilcsmith.praxis.live.pxr.api.ActionSupport;
import net.neilcsmith.praxis.live.pxr.api.EditorUtils;
import net.neilcsmith.praxis.live.pxr.api.PaletteUtils;
import net.neilcsmith.praxis.live.pxr.api.RootEditor;
import org.netbeans.api.visual.action.AcceptProvider;
import org.netbeans.api.visual.action.ActionFactory;
import org.netbeans.api.visual.action.ConnectProvider;
import org.netbeans.api.visual.action.ConnectorState;
import org.netbeans.api.visual.action.EditProvider;
import org.netbeans.api.visual.action.PopupMenuProvider;
import org.netbeans.api.visual.model.ObjectSceneEvent;
import org.netbeans.api.visual.model.ObjectSceneEventType;
import org.netbeans.api.visual.widget.Scene;
import org.netbeans.api.visual.widget.Widget;
import org.netbeans.spi.palette.PaletteController;
import org.openide.DialogDescriptor;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.explorer.ExplorerManager;
import org.openide.explorer.ExplorerUtils;
import org.openide.explorer.view.MenuView;
import org.openide.filesystems.FileObject;
import org.openide.nodes.Node;
import org.openide.nodes.NodeAcceptor;
import org.openide.nodes.NodeTransfer;
import org.openide.util.Exceptions;
import org.openide.util.ImageUtilities;
import org.openide.util.Lookup;
import org.openide.util.actions.Presenter;
import org.openide.util.lookup.Lookups;
import org.openide.util.lookup.ProxyLookup;
/**
*
* @author Neil C Smith (http://neilcsmith.net)
*/
public class GraphEditor extends RootEditor {
private final static Logger LOG = Logger.getLogger(GraphEditor.class.getName());
final static String ATTR_GRAPH_X = "graph.x";
final static String ATTR_GRAPH_Y = "graph.y";
final static String ATTR_GRAPH_MINIMIZED = "graph.minimized";
final static String ATTR_GRAPH_COLORS = "graph.colors";
final static String ATTR_GRAPH_COMMENT = "graph.comment";
private final RootProxy root;
private final Map<String, ComponentProxy> knownChildren;
private final Set<Connection> knownConnections;
private final ContainerListener containerListener;
private final InfoListener infoListener;
private final PraxisGraphScene<String> scene;
private final ExplorerManager manager;
private final Lookup lookup;
private final LocationAction location;
private final Action goUpAction;
private final Action deleteAction;
private final Action sceneCommentAction;
private final Action exportAction;
private final JMenuItem addMenu;
private JComponent panel;
private ContainerProxy container;
private ActionSupport actionSupport;
private final Point activePoint = new Point();
private boolean sync;
private final ColorsAction[] colorsActions;
public GraphEditor(RootProxy root, String category) {
this.root = root;
knownChildren = new LinkedHashMap<>();
knownConnections = new LinkedHashSet<>();
scene = new PraxisGraphScene<>(new ConnectProviderImpl(), new MenuProviderImpl());
manager = new ExplorerManager();
manager.setRootContext(root.getNodeDelegate());
if (root instanceof ContainerProxy) {
container = (ContainerProxy) root;
}
deleteAction = new DeleteAction();
PaletteController palette = PaletteUtils.getPalette("core", category);
lookup = new ProxyLookup(ExplorerUtils.createLookup(manager, buildActionMap(manager)),
Lookups.fixed(palette));
addMenu = new MenuView.Menu(
palette.getRoot().lookup(Node.class),
new NodeAcceptor() {
@Override
public boolean acceptNodes(Node[] nodes) {
if (nodes.length == 1) {
ComponentType type = nodes[0].getLookup().lookup(ComponentType.class);
if (type != null) {
EventQueue.invokeLater(() -> acceptComponentType(type));
return true;
}
FileObject fo = nodes[0].getLookup().lookup(FileObject.class);
if (fo != null) {
EventQueue.invokeLater(() -> acceptImport(fo));
return true;
}
}
return false;
}
});
addMenu.setIcon(null);
addMenu.setText("Add");
scene.addObjectSceneListener(new SelectionListener(),
ObjectSceneEventType.OBJECT_SELECTION_CHANGED);
goUpAction = new GoUpAction();
location = new LocationAction();
containerListener = new ContainerListener();
infoListener = new InfoListener();
Colors[] colorsValues = Colors.values();
colorsActions = new ColorsAction[colorsValues.length];
for (int i = 0; i < colorsValues.length; i++) {
colorsActions[i] = new ColorsAction(colorsValues[i]);
}
sceneCommentAction = new CommentAction(scene);
exportAction = new ExportAction(this, manager);
setupSceneActions();
}
private ActionMap buildActionMap(ExplorerManager em) {
ActionMap am = new ActionMap();
deleteAction.setEnabled(false);
am.put("delete", deleteAction);
CopyActionPerformer copyAction = new CopyActionPerformer(this, em);
am.put(DefaultEditorKit.copyAction, copyAction);
PasteActionPerformer pasteAction = new PasteActionPerformer(this, em);
am.put(DefaultEditorKit.pasteAction, pasteAction);
return am;
}
private void setupSceneActions() {
scene.getActions().addAction(ActionFactory.createAcceptAction(new AcceptProviderImpl()));
scene.getCommentWidget().getActions().addAction(ActionFactory.createEditAction(new EditProvider() {
@Override
public void edit(Widget widget) {
sceneCommentAction.actionPerformed(new ActionEvent(scene, ActionEvent.ACTION_PERFORMED, "edit"));
}
}));
}
private JPopupMenu getComponentPopup(NodeWidget widget) {
JPopupMenu menu = new JPopupMenu();
Object obj = scene.findObject(widget);
if (obj instanceof String) {
ComponentProxy cmp = container.getChild(obj.toString());
if (cmp instanceof ContainerProxy) {
menu.add(new ContainerOpenAction((ContainerProxy) cmp));
menu.add(new JSeparator());
}
if (cmp != null) {
boolean addSep = false;
for (Action a : cmp.getNodeDelegate().getActions(false)) {
if (a == null) {
menu.add(new JSeparator());
addSep = false;
} else {
menu.add(a);
addSep = true;
}
}
if (addSep) {
menu.add(new JSeparator());
}
}
}
menu.add(deleteAction);
menu.addSeparator();
menu.add(exportAction);
menu.addSeparator();
JMenu colorsMenu = new JMenu("Colors");
for (ColorsAction action : colorsActions) {
colorsMenu.add(action);
}
menu.add(colorsMenu);
menu.add(new CommentAction(widget));
return menu;
}
private JPopupMenu getConnectionPopup() {
JPopupMenu menu = new JPopupMenu();
menu.add(deleteAction);
return menu;
}
private JPopupMenu getScenePopup() {
JPopupMenu menu = new JPopupMenu();
// menu.add(deleteAction);
menu.add(addMenu);
menu.addSeparator();
JMenu colorsMenu = new JMenu("Colors");
for (ColorsAction action : colorsActions) {
colorsMenu.add(action);
}
menu.add(colorsMenu);
menu.add(new CommentAction(scene));
return menu;
}
ActionSupport getActionSupport() {
if (actionSupport == null) {
actionSupport = new ActionSupport(this);
}
return actionSupport;
}
ContainerProxy getContainer() {
return container;
}
Point getActivePoint() {
return new Point(activePoint);
}
@Override
public void componentActivated() {
if (panel == null) {
return;
}
scene.getView().requestFocusInWindow();
}
@Override
public JComponent getEditorComponent() {
if (panel == null) {
JPanel viewPanel = new JPanel(new BorderLayout());
JComponent view = scene.createView();
view.addMouseListener(new ActivePointListener());
JScrollPane scroll = new JScrollPane(
view,
JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
viewPanel.add(scroll, BorderLayout.CENTER);
JPanel satellitePanel = new JPanel();
satellitePanel.setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.weightx = 1;
gbc.weighty = 1;
gbc.insets = new Insets(0, 0, 25, 25);
gbc.anchor = GridBagConstraints.SOUTHEAST;
view = scene.createSatelliteView();
JPanel holder = new JPanel(new BorderLayout());
holder.setBorder(new LineBorder(Color.LIGHT_GRAY, 1));
holder.add(view);
satellitePanel.add(holder, gbc);
satellitePanel.setOpaque(false);
JLayeredPane layered = new JLayeredPane();
layered.setLayout(new OverlayLayout(layered));
layered.add(viewPanel, JLayeredPane.DEFAULT_LAYER);
layered.add(satellitePanel, JLayeredPane.PALETTE_LAYER);
panel = new JPanel(new BorderLayout());
panel.add(layered, BorderLayout.CENTER);
if (container != null) {
buildScene();
}
}
return panel;
}
@Override
public Lookup getLookup() {
return lookup;
}
@Override
public Action[] getActions() {
return new Action[]{goUpAction, location};
}
private void clearScene() {
container.removePropertyChangeListener(containerListener);
Syncable syncable = container.getLookup().lookup(Syncable.class);
if (syncable != null) {
syncable.removeKey(this);
}
for (Map.Entry<String, ComponentProxy> child : knownChildren.entrySet()) {
removeChild(child.getKey(), child.getValue());
}
activePoint.setLocation(0, 0);
knownChildren.clear();
knownConnections.clear();
location.address.setText("");
}
private void buildScene() {
container.addPropertyChangeListener(containerListener);
Syncable syncable = container.getLookup().lookup(Syncable.class);
if (syncable != null) {
syncable.addKey(this);
}
container.getNodeDelegate().getChildren().getNodes();
sync(true);
goUpAction.setEnabled(container.getParent() != null);
location.address.setText(container.getAddress().toString());
scene.setSchemeColors(getColorsFromAttribute(container).getSchemeColors());
scene.setComment(Utils.getAttr(container, ATTR_GRAPH_COMMENT));
scene.validate();
}
private void buildChild(String id, final ComponentProxy cmp) {
if (LOG.isLoggable(Level.FINEST)) {
LOG.finest("Adding " + cmp.getAddress() + " to graph.");
}
String name = cmp instanceof ContainerProxy ? id + "/.." : id;
NodeWidget widget = scene.addNode(id, name);
widget.setSchemeColors(getColorsFromAttribute(cmp).getSchemeColors());
widget.setToolTipText(cmp.getType().toString());
widget.setPreferredLocation(resolveLocation(id, cmp));
if ("true".equals(Utils.getAttr(cmp, ATTR_GRAPH_MINIMIZED))) {
widget.setMinimized(true);
}
updateWidgetComment(widget,
Utils.getAttr(cmp, ATTR_GRAPH_COMMENT, ""),
cmp instanceof ContainerProxy);
widget.getActions().addAction(ActionFactory.createEditAction(new EditProvider() {
@Override
public void edit(Widget widget) {
if (cmp instanceof ContainerProxy) {
new ContainerOpenAction((ContainerProxy) cmp).actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "edit"));
} else {
cmp.getNodeDelegate().getPreferredAction().actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "edit"));
}
}
}));
final CommentAction commentAction = new CommentAction(widget);
widget.getCommentWidget().getActions().addAction(ActionFactory.createEditAction(new EditProvider() {
@Override
public void edit(Widget widget) {
commentAction.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "edit"));
}
}));
ComponentInfo info = cmp.getInfo();
for (String portID : info.getPorts()) {
PortInfo pi = info.getPortInfo(portID);
if (LOG.isLoggable(Level.FINEST)) {
LOG.finest("Building port " + portID);
}
buildPin(id, portID, pi);
}
cmp.addPropertyChangeListener(infoListener);
}
private void rebuildChild(String id, ComponentProxy cmp) {
if (LOG.isLoggable(Level.FINEST)) {
LOG.finest("Rebuilding " + cmp.getAddress() + " in graph.");
}
// remove all connections to this component from known connections list
Iterator<Connection> itr = knownConnections.iterator();
while (itr.hasNext()) {
Connection con = itr.next();
if (con.getChild1().equals(id) || con.getChild2().equals(id)) {
LOG.finest("Removing connection : " + con);
itr.remove();
}
}
// match visual state by removing all pins and edges from graph node
List<PinID<String>> pins = new ArrayList<>(scene.getNodePins(id));
LOG.finest(pins.toString());
for (PinID<String> pin : pins) {
LOG.finest("Removing pin : " + pin);
scene.removePinWithEdges(pin);
}
ComponentInfo info = cmp.getInfo();
for (String portID : info.getPorts()) {
PortInfo pi = info.getPortInfo(portID);
if (LOG.isLoggable(Level.FINEST)) {
LOG.finest("Building port " + portID);
}
buildPin(id, portID, pi);
}
syncConnections();
}
private void removeChild(String id, ComponentProxy cmp) {
cmp.removePropertyChangeListener(infoListener);
scene.removeNodeWithEdges(id);
// @TODO temporary fix for moving dynamic components?
activePoint.x = 0;
activePoint.y = 0;
}
private Point resolveLocation(String id, ComponentProxy cmp) {
int x = activePoint.x;
int y = activePoint.y;
try {
String xStr = Utils.getAttr(cmp, ATTR_GRAPH_X);
String yStr = Utils.getAttr(cmp, ATTR_GRAPH_Y);
if (xStr != null) {
x = Integer.parseInt(xStr) + x;
}
if (yStr != null) {
y = Integer.parseInt(yStr) + y;
}
LOG.log(Level.FINEST, "Resolved location for {0} : {1} x {2}", new Object[]{id, x, y});
} catch (Exception ex) {
LOG.log(Level.FINEST, "Cannot resolve location for " + id, ex);
}
// @TODO what to do about importing components without positions?
// if point not set, check for widget at point?
return new Point(x, y);
}
private Colors getColorsFromAttribute(ComponentProxy cmp) {
String colorsAttr = Utils.getAttr(cmp, ATTR_GRAPH_COLORS);
if (colorsAttr != null) {
try {
return Colors.valueOf(colorsAttr);
} catch (Exception e) {
Exceptions.printStackTrace(e);
}
}
return Colors.Default;
}
private void buildPin(String cmpID, String pinID, PortInfo info) {
boolean primary = info.getType().getSimpleName().startsWith("Audio")
|| info.getType().getSimpleName().startsWith("Video");
PinWidget pin = scene.addPin(cmpID, pinID, "", getPinAlignment(info));
if (primary) {
pin.setFont(scene.getDefaultFont().deriveFont(Font.BOLD, 11.0f));
} else {
pin.setFont(scene.getDefaultFont().deriveFont(11.0f));
}
pin.setToolTipText(pinID + " : " + info.getType().getSimpleName());
}
private Alignment getPinAlignment(PortInfo info) {
switch (info.getDirection()) {
case IN:
return Alignment.Left;
case OUT:
return Alignment.Right;
default:
return Alignment.Center;
}
}
private boolean buildConnection(Connection connection) {
PinID<String> p1 = new PinID<>(connection.getChild1(), connection.getPort1());
PinID<String> p2 = new PinID<>(connection.getChild2(), connection.getPort2());
if (scene.isPin(p1) && scene.isPin(p2)) {
EdgeWidget widget = scene.connect(connection.getChild1(), connection.getPort1(),
connection.getChild2(), connection.getPort2());
widget.setToolTipText(connection.getChild1() + "!" + connection.getPort1() + " -> "
+ connection.getChild2() + "!" + connection.getPort2());
return true;
} else {
return false;
}
}
private boolean removeConnection(Connection connection) {
EdgeID<String> edge = new EdgeID<>(new PinID<>(connection.getChild1(), connection.getPort1()),
new PinID<>(connection.getChild2(), connection.getPort2()));
if (scene.isEdge(edge)) {
scene.disconnect(connection.getChild1(), connection.getPort1(),
connection.getChild2(), connection.getPort2());
return true;
} else {
return false;
}
}
private void syncChildren() {
if (container == null) {
return;
}
List<String> ch = Arrays.asList(container.getChildIDs());
Set<String> tmp = new LinkedHashSet<String>(knownChildren.keySet());
tmp.removeAll(ch);
// tmp now contains children that have been removed from model
for (String id : tmp) {
ComponentProxy cmp = knownChildren.remove(id);
removeChild(id, cmp);
}
tmp.clear();
tmp.addAll(ch);
tmp.removeAll(knownChildren.keySet());
// tmp now contains children that have been added to model
for (String id : tmp) {
ComponentProxy cmp = container.getChild(id);
if (cmp != null) {
buildChild(id, cmp);
knownChildren.put(id, cmp);
}
}
scene.validate();
}
private void syncConnections() {
if (container == null) {
return;
}
List<Connection> cons = Arrays.asList(container.getConnections());
Set<Connection> tmp = new LinkedHashSet<Connection>(knownConnections);
tmp.removeAll(cons);
// tmp now contains connections that have been removed from model
for (Connection con : tmp) {
removeConnection(con);
knownConnections.remove(con);
}
tmp.clear();
tmp.addAll(cons);
tmp.removeAll(knownConnections);
// tmp now contains connections that have been added to model
for (Connection con : tmp) {
if (buildConnection(con)) {
knownConnections.add(con);
} else {
// leave for later?
}
}
scene.validate();
}
void sync(boolean sync) {
if (sync) {
this.sync = true;
syncChildren();
syncConnections();
} else {
this.sync = false;
}
}
private void updateWidgetComment(final NodeWidget widget, final String text, final boolean container) {
if (!container) {
widget.setComment(text);
return;
}
// OK, we have a container, trim text
int delim = text.indexOf("\n\n");
if (delim >= 0) {
widget.setComment(text.substring(0, delim) + "...");
} else {
widget.setComment(text);
}
scene.revalidate();
}
private ComponentProxy findComponent(Widget widget) {
if (widget == scene) {
return container;
}
return findComponent(scene.findObject(widget));
}
private ComponentProxy findComponent(Object obj) {
if (obj instanceof Widget) {
return findComponent((Widget) obj);
}
if (obj instanceof String) {
return container.getChild(obj.toString());
}
return null;
}
private void acceptComponentType(final ComponentType type) {
NotifyDescriptor.InputLine dlg = new NotifyDescriptor.InputLine(
"ID:", "Enter an ID for " + type);
dlg.setInputText(getFreeID(type));
Object retval = DialogDisplayer.getDefault().notify(dlg);
if (retval == NotifyDescriptor.OK_OPTION) {
final String id = dlg.getInputText();
try {
container.addChild(id, type, new Callback() {
@Override
public void onReturn(CallArguments args) {
// nothing wait for sync
}
@Override
public void onError(CallArguments args) {
// pointMap.remove(id);
DialogDisplayer.getDefault().notifyLater(new NotifyDescriptor.Message("Error creating component", NotifyDescriptor.ERROR_MESSAGE));
}
});
} catch (ProxyException ex) {
Exceptions.printStackTrace(ex);
}
// pointMap.put(id, point);
}
}
private void acceptImport(FileObject file) {
if (getActionSupport().importSubgraph(container, file, new Callback() {
@Override
public void onReturn(CallArguments args) {
sync(true);
}
@Override
public void onError(CallArguments args) {
sync(true);
}
})) {
sync(false);
}
}
private String getFreeID(ComponentType type) {
Set<String> existing = new HashSet<String>(Arrays.asList(container.getChildIDs()));
return EditorUtils.findFreeID(existing, EditorUtils.extractBaseID(type), true);
}
private class ContainerListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (sync) {
if (ContainerInterface.CHILDREN.equals(evt.getPropertyName())) {
syncChildren();
} else if (ContainerInterface.CONNECTIONS.equals(evt.getPropertyName())) {
syncConnections();
}
}
}
}
private class InfoListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (ComponentInterface.INFO.equals(evt.getPropertyName())) {
Object src = evt.getSource();
assert src instanceof ComponentProxy;
if (src instanceof ComponentProxy) {
ComponentProxy cmp = (ComponentProxy) src;
String id = cmp.getAddress().getID();
rebuildChild(id, cmp);
}
}
}
}
private class ConnectProviderImpl implements ConnectProvider {
@Override
public boolean isSourceWidget(Widget sourceWidget) {
return sourceWidget instanceof PinWidget;
}
@Override
public ConnectorState isTargetWidget(Widget sourceWidget, Widget targetWidget) {
if (sourceWidget instanceof PinWidget && targetWidget instanceof PinWidget) {
return ConnectorState.ACCEPT;
} else {
return ConnectorState.REJECT;
}
}
@Override
public boolean hasCustomTargetWidgetResolver(Scene scene) {
return false;
}
@Override
public Widget resolveTargetWidget(Scene scene, Point sceneLocation) {
return null;
}
@Override
public void createConnection(Widget sourceWidget, Widget targetWidget) {
PinID<String> p1 = (PinID<String>) scene.findObject(sourceWidget);
PinID<String> p2 = (PinID<String>) scene.findObject(targetWidget);
try {
container.connect(new Connection(p1.getParent(), p1.getName(), p2.getParent(), p2.getName()), new Callback() {
@Override
public void onReturn(CallArguments args) {
}
@Override
public void onError(CallArguments args) {
}
});
} catch (ProxyException ex) {
Exceptions.printStackTrace(ex);
}
}
}
private class MenuProviderImpl implements PopupMenuProvider {
@Override
public JPopupMenu getPopupMenu(Widget widget, Point localLocation) {
if (widget instanceof NodeWidget) {
return getComponentPopup((NodeWidget) widget);
} else if (widget instanceof EdgeWidget) {
return getConnectionPopup();
} else if (widget == scene) {
return getScenePopup();
} else {
return null;
}
}
}
private class ActivePointListener extends MouseAdapter {
@Override
public void mouseClicked(MouseEvent e) {
updateActivePoint(e);
}
@Override
public void mousePressed(MouseEvent e) {
updateActivePoint(e);
}
@Override
public void mouseReleased(MouseEvent e) {
updateActivePoint(e);
}
private void updateActivePoint(MouseEvent e) {
activePoint.setLocation(scene.convertViewToScene(e.getPoint()));
LOG.log(Level.FINEST, "Updated active point : {0}", activePoint);
}
}
private class SelectionListener extends ObjectSceneAdaptor {
@Override
public void selectionChanged(ObjectSceneEvent event, Set<Object> previousSelection, Set<Object> newSelection) {
for (Object obj : previousSelection) {
if (newSelection.contains(obj)) {
continue;
}
if (obj instanceof String) {
ComponentProxy cmp = container.getChild((String) obj);
if (cmp != null) {
syncAttributes(cmp, (String) obj);
}
}
}
try {
//inSelection = true; // not needed now not listening to EM?
if (newSelection.isEmpty()) {
LOG.log(Level.FINEST, "newSelection is empty");
if (container != null) {
manager.setSelectedNodes(new Node[]{container.getNodeDelegate()});
} else {
manager.setSelectedNodes(new Node[]{manager.getRootContext()});
}
deleteAction.setEnabled(false);
} else {
ArrayList<Node> sel = new ArrayList<Node>();
for (Object obj : newSelection) {
// if (obj instanceof Node) {
// sel.add((Node) obj);
// }
if (obj instanceof String) {
ComponentProxy cmp = container.getChild((String) obj);
if (cmp != null) {
sel.add(cmp.getNodeDelegate());
syncAttributes(cmp, (String) obj);
}
}
}
LOG.log(Level.FINEST, "newSelection size is " + newSelection.size() + " and node selection size is " + sel.size());
if (sel.isEmpty()) {
manager.setSelectedNodes(new Node[]{manager.getRootContext()});
} else {
manager.setSelectedNodes(sel.toArray(new Node[sel.size()]));
}
deleteAction.setEnabled(true);
}
} catch (PropertyVetoException ex) {
LOG.log(Level.FINEST, "Received PropertyVetoException trying to set selected nodes", ex);
} finally {
//inSelection = false;
}
}
private void syncAttributes(ComponentProxy cmp, String id) {
Widget widget = scene.findWidget(id);
if (widget instanceof NodeWidget) {
NodeWidget nodeWidget = (NodeWidget) widget;
String x = Integer.toString((int) nodeWidget.getLocation().getX());
String y = Integer.toString((int) nodeWidget.getLocation().getY());
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "Setting position attributes of {0} to x:{1} y:{2}",
new Object[]{cmp.getAddress(), x, y});
}
Utils.setAttr(cmp, ATTR_GRAPH_X, x);
Utils.setAttr(cmp, ATTR_GRAPH_Y, y);
Utils.setAttr(cmp, ATTR_GRAPH_MINIMIZED,
nodeWidget.isMinimized() ? "true" : null);
}
}
}
private class DeleteAction extends AbstractAction {
private DeleteAction() {
super("Delete");
}
@Override
public void actionPerformed(final ActionEvent e) {
// GRRR! Built in delete action is asynchronous - replace?
if (!EventQueue.isDispatchThread()) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
actionPerformed(e);
}
});
return;
}
assert EventQueue.isDispatchThread();
Set<?> sel = scene.getSelectedObjects();
if (sel.isEmpty()) {
return;
}
if (!checkDeletion(sel)) {
return;
}
for (Object obj : sel) {
if (obj instanceof String) {
try {
container.removeChild((String) obj, new Callback() {
@Override
public void onReturn(CallArguments args) {
}
@Override
public void onError(CallArguments args) {
}
});
} catch (ProxyException ex) {
Exceptions.printStackTrace(ex);
}
} else if (obj instanceof EdgeID) {
EdgeID edge = (EdgeID) obj;
PinID p1 = edge.getPin1();
PinID p2 = edge.getPin2();
Connection con = new Connection(p1.getParent().toString(), p1.getName(),
p2.getParent().toString(), p2.getName());
try {
container.disconnect(con, new Callback() {
@Override
public void onReturn(CallArguments args) {
}
@Override
public void onError(CallArguments args) {
}
});
} catch (ProxyException ex) {
Exceptions.printStackTrace(ex);
}
}
}
}
private boolean checkDeletion(Set<?> selected) {
List<String> components = new ArrayList<String>();
for (Object o : selected) {
if (o instanceof String) {
components.add((String) o);
}
}
if (components.isEmpty()) {
return true;
}
int count = components.size();
String msg = count > 1
? "Delete " + count + " components?"
: "Delete " + components.get(0) + "?";
String title = "Confirm deletion";
NotifyDescriptor desc = new NotifyDescriptor.Confirmation(msg, title, NotifyDescriptor.YES_NO_OPTION);
return NotifyDescriptor.YES_OPTION.equals(DialogDisplayer.getDefault().notify(desc));
}
}
private class ColorsAction extends AbstractAction {
private final Colors colors;
private ColorsAction(Colors colors) {
super(colors.name());
this.colors = colors;
}
@Override
public void actionPerformed(ActionEvent e) {
boolean foundNode = false;
for (Object obj : scene.getSelectedObjects()) {
if (obj instanceof String) {
ComponentProxy cmp = container.getChild(obj.toString());
NodeWidget widget = (NodeWidget) scene.findWidget(obj);
widget.setSchemeColors(colors.getSchemeColors());
setColorsAttr(cmp);
foundNode = true;
}
}
if (!foundNode) {
scene.setSchemeColors(colors.getSchemeColors());
setColorsAttr(container);
}
scene.revalidate();
}
private void setColorsAttr(ComponentProxy cmp) {
if (colors == Colors.Default) {
Utils.setAttr(cmp, ATTR_GRAPH_COLORS, null);
} else {
Utils.setAttr(cmp, ATTR_GRAPH_COLORS, colors.name());
}
}
}
private class CommentAction extends AbstractAction {
private final Widget widget;
private CommentAction(Widget widget) {
super("Comment...");
this.widget = widget;
}
@Override
public void actionPerformed(ActionEvent e) {
Runnable runnable = new Runnable() {
@Override
public void run() {
String comment = findInitialText(widget);
JTextArea editor = new JTextArea(comment);
JPanel panel = new JPanel(new BorderLayout());
panel.add(new JScrollPane(editor));
panel.setPreferredSize(new Dimension(400, 300));
DialogDescriptor dlg = new DialogDescriptor(panel, "Comment");
editor.selectAll();
editor.requestFocusInWindow();
Object result = DialogDisplayer.getDefault().notify(dlg);
if (result != NotifyDescriptor.OK_OPTION) {
return;
}
comment = editor.getText();
if (widget == scene) {
scene.setComment(comment);
Utils.setAttr(container, ATTR_GRAPH_COMMENT, comment.isEmpty() ? null : comment);
} else if (widget instanceof NodeWidget) {
ComponentProxy cmp = findComponent(widget);
updateWidgetComment((NodeWidget) widget, comment, cmp instanceof ContainerProxy);
Utils.setAttr(cmp, ATTR_GRAPH_COMMENT, comment.isEmpty() ? null : comment);
for (Object obj : scene.getSelectedObjects()) {
ComponentProxy additional = findComponent(obj);
if (additional != null) {
NodeWidget n = (NodeWidget) scene.findWidget(obj);
if (n != widget) {
updateWidgetComment(n, comment, cmp instanceof ContainerProxy);
Utils.setAttr(additional, ATTR_GRAPH_COMMENT, comment.isEmpty() ? null : comment);
}
}
}
}
scene.validate();
}
};
EventQueue.invokeLater(runnable);
}
private String findInitialText(Widget widget) {
ComponentProxy cmp = findComponent(widget);
if (cmp != null) {
String comment = Utils.getAttr(cmp, ATTR_GRAPH_COMMENT);
return comment == null ? "" : comment;
} else {
return "";
}
}
}
private class GoUpAction extends AbstractAction {
private GoUpAction() {
super("Go Up",
ImageUtilities.loadImageIcon(
"net/neilcsmith/praxis/live/pxr/graph/resources/go-up.png",
true));
}
@Override
public void actionPerformed(ActionEvent e) {
ContainerProxy parent = container.getParent();
if (parent != null) {
clearScene();
container = parent;
buildScene();
}
}
}
private class LocationAction extends AbstractAction implements Presenter.Toolbar {
private final JLabel address = new JLabel();
@Override
public void actionPerformed(ActionEvent e) {
}
@Override
public Component getToolbarPresenter() {
return address;
}
}
private class ContainerOpenAction extends AbstractAction {
private final ContainerProxy container;
private ContainerOpenAction(ContainerProxy container) {
super("Open");
this.container = container;
}
@Override
public void actionPerformed(ActionEvent e) {
clearScene();
GraphEditor.this.container = container;
buildScene();
}
}
private class AcceptProviderImpl implements AcceptProvider {
@Override
public ConnectorState isAcceptable(Widget widget, Point point, Transferable transferable) {
if (extractType(transferable) != null || extractFile(transferable) != null) {
return ConnectorState.ACCEPT;
} else {
return ConnectorState.REJECT;
}
}
@Override
public void accept(Widget widget, final Point point, Transferable transferable) {
activePoint.setLocation(point);
ComponentType type = extractType(transferable);
if (type != null) {
EventQueue.invokeLater(() -> acceptComponentType(type));
return;
}
FileObject file = extractFile(transferable);
if (file != null) {
EventQueue.invokeLater(() -> acceptImport(file));
}
}
private ComponentType extractType(Transferable transferable) {
Node n = NodeTransfer.node(transferable, NodeTransfer.DND_COPY_OR_MOVE);
if (n != null) {
ComponentType t = n.getLookup().lookup(ComponentType.class);
if (t != null) {
return t;
}
}
return null;
}
private FileObject extractFile(Transferable transferable) {
Node n = NodeTransfer.node(transferable, NodeTransfer.DND_COPY_OR_MOVE);
if (n != null) {
FileObject dob = n.getLookup().lookup(FileObject.class);
if (dob != null) {
return dob;
}
}
return null;
}
}
}