org.shaman.rpg.editor.objects.ui.ElementPropertyVisualPanel1.java Source code

Java tutorial

Introduction

Here is the source code for org.shaman.rpg.editor.objects.ui.ElementPropertyVisualPanel1.java

Source

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package org.shaman.rpg.editor.objects.ui;

import com.jme3.gde.core.assets.ProjectAssetManager;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.AffineTransform;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.*;
import javax.swing.border.LineBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.apache.commons.lang3.ArrayUtils;
import org.netbeans.api.annotations.common.StaticResource;
import org.netbeans.api.visual.action.*;
import org.netbeans.api.visual.anchor.*;
import org.netbeans.api.visual.graph.GraphPinScene;
import org.netbeans.api.visual.layout.LayoutFactory;
import org.netbeans.api.visual.widget.*;
import org.netbeans.api.visual.widget.general.IconNodeWidget;
import org.openide.awt.UndoRedo;
import org.openide.explorer.propertysheet.PropertySheet;
import org.openide.nodes.Node;
import org.openide.util.Exceptions;
import org.openide.util.ImageUtilities;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.shaman.database.IndexedRecord;
import org.shaman.database.Record;
import org.shaman.rpg.editor.objects.*;
import org.shaman.rpg.editor.objects.undo.ElementEditUndoableEdit;
import org.shaman.rpg.engine.core.math.Point2i;
import org.shaman.rpg.engine.core.obj.*;
import org.shaman.rpg.engine.core.obj.controller.AbstractController;
import org.shaman.rpg.engine.core.obj.controller.AbstractMultiController;
import org.shaman.rpg.engine.core.obj.model.AbstractModel;
import org.shaman.rpg.engine.core.obj.model.AbstractMultiModel;

public final class ElementPropertyVisualPanel1 extends JPanel implements ChangeListener {
    private static final Logger LOG = Logger.getLogger(ElementPropertyVisualPanel1.class.getName());
    @StaticResource
    private static final String LOCK_ICON = "org/shaman/rpg/editor/objects/ui/lock24.png";
    @StaticResource
    private static final String HIDDEN_ICON = "org/shaman/rpg/editor/objects/ui/hidden24.png";

    private final PropertySheet propertySheet;
    private final GraphSceneImpl scene;
    private final ElementPropertyWizardPanel1 wizardPanel;

    private EditorLevelElement<BaseView, BaseModel<BaseView>, BaseController<BaseModel<BaseView>>> element;
    private ElementEditUndoableEdit edit;
    private MVCContainer container;
    private Collection<EditorLevelElement<?, ?, ?>> allElements;
    private ProjectAssetManager assetManager;
    private UndoRedo.Manager undoRedoManager;
    private ObjectPositions objectPositions;
    private ObjectNode selectedObject;

    {
        //force registering of property editors
        Installer.registerEditors();
    }

    /**
     * Creates new form ElementPropertyVisualPanel1
     */
    public ElementPropertyVisualPanel1(ElementPropertyWizardPanel1 wizardPanel) {
        this.wizardPanel = wizardPanel;
        initComponents();

        propertySheet = new PropertySheet();
        scene = new GraphSceneImpl();
        JScrollPane scenePane = new JScrollPane(scene.createView());
        JButton addNew = new JButton(
                NbBundle.getMessage(ElementPropertyVisualPanel1.class, "ElementPropertyVisualPanel1.addNew"));
        JButton addExisting = new JButton(
                NbBundle.getMessage(ElementPropertyVisualPanel1.class, "ElementPropertyVisualPanel1.addExisting"));
        addNew.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                addNewObjectAction();
            }
        });
        addExisting.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                addExistingObjectAction();
            }
        });

        setLayout(new BorderLayout());
        JPanel mainPanel = new JPanel(new GridLayout(1, 2, 5, 5));
        mainPanel.add(scenePane);
        mainPanel.add(propertySheet);
        JPanel buttonRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
        buttonRow.add(addNew);
        buttonRow.add(addExisting);
        add(mainPanel, BorderLayout.CENTER);
        add(buttonRow, BorderLayout.SOUTH);
    }

    @SuppressWarnings("unchecked")
    void setElement(EditorLevelElement<?, ?, ?> element, MVCContainer container,
            Collection<EditorLevelElement<?, ?, ?>> allElements, ProjectAssetManager assetManager,
            UndoRedo.Manager undoRedoManager) {
        this.element = (EditorLevelElement<BaseView, BaseModel<BaseView>, BaseController<BaseModel<BaseView>>>) element;
        this.container = container;
        this.allElements = allElements;
        this.assetManager = assetManager;
        this.undoRedoManager = undoRedoManager;
        this.objectPositions = (ObjectPositions) element.get(ObjectPositions.KEY);
        if (this.objectPositions == null) {
            this.objectPositions = new ObjectPositions();
            int i = 0;
            for (BaseView v : element.getViews()) {
                this.objectPositions.put(v, new Point2i(i * 50, 0));
                i++;
            }
            i = 0;
            for (BaseModel m : element.getModels()) {
                this.objectPositions.put(m, new Point2i(i * 50, 100));
                i++;
            }
            i = 0;
            for (BaseController c : element.getControllers()) {
                this.objectPositions.put(c, new Point2i(i * 50, 200));
                i++;
            }
            element.put(ObjectPositions.KEY, this.objectPositions);
        }
        scene.setElement();
        edit = new ElementEditUndoableEdit(allElements, container);
        edit.setOriginalElement(element);
        fireElementChanged();
    }

    boolean isElementValid() {
        Collection<String> messages;
        //first check selected object
        if (selectedObject != null) {
            messages = ElementValidator.validate(selectedObject.getObject());
            if (!messages.isEmpty()) {
                //set error message
                String msg = messages.iterator().next();
                wizardPanel.setErrorMessage(selectedObject.getName() + ": " + msg);
                return false;
            }
        }
        //check all other elements
        for (ObjectInfo info : scene.getNodes()) {
            messages = ElementValidator.validate(info.node.getObject());
            if (!messages.isEmpty()) {
                //set error message
                String msg = messages.iterator().next();
                wizardPanel.setErrorMessage(info.node.getName() + ": " + msg);
                return false;
            }
        }
        //check additional validators
        for (LevelElementValidator<BaseView, BaseModel<BaseView>, BaseController<BaseModel<BaseView>>> v : element
                .getValidators()) {
            String message = v.validate(element);
            if (message != null) {
                wizardPanel.setErrorMessage(message);
                return false;
            }
        }
        return true;
    }

    void apply() {
        //Applies all changes, called when the OK-Button is pressed
        //notify object action providers
        for (ObjectInfo i : scene.getNodes()) {
            if (i.actionProvider != null) {
                try {
                    i.actionProvider.onApprove();
                } catch (Exception e) {
                    LOG.log(Level.SEVERE,
                            "error while executing onApprove() on action provider " + i.actionProvider, e);
                }
            }
        }
        //write positions (changes in the positions are not undoable)
        objectPositions.clear();
        for (ObjectInfo i : scene.getNodes()) {
            Point p = i.widget.getLocation();
            objectPositions.put((IndexedRecord) i.object, new Point2i(p.x, p.y));
        }

        //Add undoable edit
        if (edit.setChangedElement(element)) {
            //add to UndoRedoManager
            undoRedoManager.addEdit(edit);
        }
    }

    void cancel() {
        //Cancles the changes (undo it), called when the Cancle-Button is pressed
        //notify object action providers
        for (ObjectInfo i : scene.getNodes()) {
            if (i.actionProvider != null) {
                i.actionProvider.onCancel();
            }
        }
        //Undo edits
        if (edit.setChangedElement(element)) {
            edit.undo();
        }
    }

    private void fireElementChanged() {
        System.out.println("fire element changed");
        wizardPanel.onElementChanged();
    }

    @Override
    public void stateChanged(ChangeEvent e) {
        fireElementChanged();
    }

    public void addNewObjectAction() {
        Class<?> clazz = ChooseNewObject.show(null, element);
        if (clazz == null) {
            return;
        }
        //create an instance of the object
        Object obj = null;
        try {
            obj = clazz.newInstance();
        } catch (InstantiationException | IllegalAccessException ex) {
            Exceptions.printStackTrace(ex);
            return;
        }
        addObject(obj, false);
    }

    private boolean addObject(Object obj, boolean hidden) {
        //check if it can be added
        int type;
        if (obj instanceof BaseView) {
            if (!hidden) {
                if (!element.canAdd(EditorLevelElement.ObjectType.VIEW, obj)) {
                    return false;
                }
                BaseView[] views = Arrays.copyOf(element.getViews(), element.getViews().length + 1);
                views[views.length - 1] = (BaseView) obj;
                element.setViews(views);
                container.addView((BaseView) obj);
            }
            type = 1;
        } else if (obj instanceof BaseModel) {
            if (!hidden) {
                if (!element.canAdd(EditorLevelElement.ObjectType.MODEL, obj)) {
                    return false;
                }
                BaseModel[] models = Arrays.copyOf(element.getModels(), element.getModels().length + 1);
                models[models.length - 1] = (BaseModel) obj;
                element.setModels(models);
                container.addModel((BaseModel) obj);
            }
            type = 2;
        } else if (obj instanceof BaseController) {
            if (!hidden) {
                if (!element.canAdd(EditorLevelElement.ObjectType.CONTROLLER, obj)) {
                    return false;
                }
                BaseController[] controller = Arrays.copyOf(element.getControllers(),
                        element.getControllers().length + 1);
                controller[controller.length - 1] = (BaseController) obj;
                element.setControllers(controller);
                container.addController((BaseController) obj);
            }
            type = 3;
        } else {
            throw new IllegalArgumentException("object is not a view, model or controller: " + obj);
        }
        //add it
        ObjectInfo info = new ObjectInfo(obj, type, hidden);
        scene.addObject(info);
        scene.repaint();
        fireElementChanged();
        return true;
    }

    public void addExistingObjectAction() {
        Object obj = ChooseExistingObject.show(null, element, allElements);
        if (obj != null) {
            addObject(obj, true);
        }
    }

    //<editor-fold defaultstate="collapsed" desc="Graph Element Structures">
    private class ObjectInfo {
        private Object object;
        private ObjectNode node;
        private NodeWidget widget;
        private @Deprecated int type; //1=View, 2=Model, 3=Controller
        private EditorLevelElement.ObjectType typeEnum;
        private boolean hidden;
        private PinInfo inputPin;
        private ArrayList<PinInfo> outputPins;
        private ObjectNode.Links links;
        private ObjectActionProvider actionProvider;

        /**
         * Creates a new object info describing a view, model or controller
         * @param object the object
         * @param type 1: view, 2: model, 3: controller
         * @param hidden {@code true} if the object is hidden, meaning it is
         * refernced by a link from another higher-level object, but not
         * controller by the EditorLevelElement.
         */
        private ObjectInfo(Object object, int type, boolean hidden) {
            this.object = object;
            this.type = type;
            if (type == 1) {
                typeEnum = EditorLevelElement.ObjectType.VIEW;
            } else if (type == 2) {
                typeEnum = EditorLevelElement.ObjectType.MODEL;
            } else {
                typeEnum = EditorLevelElement.ObjectType.CONTROLLER;
            }
            this.hidden = hidden;
            this.node = new ObjectNode(object, assetManager);
            //add pins
            inputPin = new PinInfo(this, true, 0);
            links = new ObjectNode.Links(object);
            outputPins = new ArrayList<>();
            if (type != 1) { //views have no output pins
                int index = 0;
                for (int i = 0; i < links.getFixedSlotCount(); ++i) {
                    PinInfo pin = new PinInfo(this, false, index++, links.getClassesAtSlot(i));
                    outputPins.add(pin);
                }
                //flexible pins, add minimum count
                for (int i = 0; i < Math.min(Math.max(links.getMinFlexible(), 1), links.getMaxFlexible()); ++i) {
                    PinInfo pin = new PinInfo(this, false, index++, links.getFlexibleClasses());
                    outputPins.add(pin);
                }
            }
            //action provider
            for (ObjectActionProvider.Factory f : Lookup.getDefault()
                    .lookupAll(ObjectActionProvider.Factory.class)) {
                try {
                    ObjectActionProvider p = f.createActionProvider(element, object, typeEnum);
                    if (p != null) {
                        actionProvider = p;
                        break;
                    }
                } catch (Exception e) {
                    LOG.log(Level.SEVERE, "error while initializing action provider", e);
                }
            }
        }

        /**
         * Requestes the specified count of pins. If the object contains a
         * flexible count of pins, this count is provided.
         * All outgoing edges and pins are removed from the scene.
         * @param count the count of available pins.
         */
        private long requestPins(long count) {
            //remove old edges
            for (PinInfo pin : outputPins) {
                EdgeInfo edge = pin.edge;
                if (edge != null) {
                    edge.source.edge = null;
                    edge.target.edge = null;
                    scene.removeEdge(edge);
                }
                scene.removePin(pin);
            }
            //minimal and maximal count of pins
            long minCount = links.getFixedSlotCount() + links.getMinFlexible();
            long maxCount = links.getFixedSlotCount() + links.getMaxFlexible();
            if (maxCount < 0) {
                maxCount = Integer.MAX_VALUE; //overflow because max flexible already Integer.MAX_VALUE
            }
            count = Math.max(minCount, count);
            count = Math.min(maxCount, count);
            //create pins
            outputPins.clear();
            int index = 0;
            for (int i = 0; i < links.getFixedSlotCount(); ++i) {
                PinInfo pin = new PinInfo(this, false, index++, links.getClassesAtSlot(i));
                outputPins.add(pin);
            }
            long flexibleCount = Math.min(links.getMaxFlexible(), count - links.getFixedSlotCount() + 1); //add one extra flexible slot
            for (int i = 0; i < flexibleCount; ++i) {
                PinInfo pin = new PinInfo(this, false, index++, links.getFlexibleClasses());
                outputPins.add(pin);
            }
            //update slot names
            updateSlotNames();
            //return count of created pins
            return count;
        }

        @Override
        public int hashCode() {
            int hash = 3;
            hash = 37 * hash + Objects.hashCode(this.object);
            hash = 37 * hash + this.type;
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final ObjectInfo other = (ObjectInfo) obj;
            if (!Objects.equals(this.object, other.object)) {
                return false;
            }
            if (this.type != other.type) {
                return false;
            }
            return true;
        }

        @Override
        public String toString() {
            return "ObjectInfo{" + "object=" + object + ", type=" + type + ", hidden=" + hidden + ", inputPin="
                    + inputPin + '}';
        }

        private void updateSlotNames() {
            if (object instanceof NamedSlots) {
                NamedSlots ns = (NamedSlots) object;
                int slotCount;
                if (typeEnum == EditorLevelElement.ObjectType.CONTROLLER) {
                    slotCount = ((BaseController) object).getModels().size();
                } else if (typeEnum == EditorLevelElement.ObjectType.MODEL) {
                    slotCount = ((BaseModel) object).getViews().size();
                } else {
                    LOG.severe("a view implements NamedSlots, this is not possible");
                    return;
                }
                String[] names;
                if (ns.getSlotNames() == null) {
                    names = new String[slotCount];
                } else {
                    names = Arrays.copyOf(ns.getSlotNames(), slotCount);
                }
                if (slotCount > outputPins.size()) {
                    return; //still in configuration
                }
                for (int i = 0; i < slotCount; ++i) {
                    PinInfo pin = outputPins.get(i);
                    if (pin.edge == null) {
                        //names[i] = null;
                    } else {
                        String n = pin.edge.name;
                        if (n == null || n.isEmpty()) {
                            if (names[i] == null) {
                                n = "null";
                            } else {
                                n = names[i];
                            }
                            pin.edge.name = n;
                            scene.repaint();
                        }
                        names[i] = n;
                    }
                }
                ns.setSlotNames(names);
            }
        }

    }

    private class PinInfo {
        private ObjectInfo object;
        private boolean input; //true: input, false: output
        private int index;
        private Class<?>[] allowedClasses;
        private EdgeInfo edge;

        public PinInfo(ObjectInfo object, boolean input, int index, Class<?>... allowedClasses) {
            this.object = object;
            this.input = input;
            this.index = index;
            this.allowedClasses = allowedClasses;
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 97 * hash + Objects.hashCode(this.object);
            hash = 97 * hash + (this.input ? 1 : 0);
            hash = 97 * hash + this.index;
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final PinInfo other = (PinInfo) obj;
            if (!Objects.equals(this.object, other.object)) {
                return false;
            }
            if (this.input != other.input) {
                return false;
            }
            if (this.index != other.index) {
                return false;
            }
            return true;
        }

        private void connectTo(ObjectInfo target) {
            Object obj = this.object.object;
            if (obj instanceof AbstractModel) {
                @SuppressWarnings("unchecked")
                AbstractModel<BaseView> m = (AbstractModel<BaseView>) obj;
                assert (index == 0);
                m.setLinkedView((BaseView) target.object);
            } else if (obj instanceof AbstractMultiModel) {
                @SuppressWarnings("unchecked")
                AbstractMultiModel<BaseView> m = (AbstractMultiModel<BaseView>) obj;
                BaseView[] views = m.getLinkedViews();
                if (views == null) {
                    views = new BaseView[index + 1];
                }
                if (views.length <= index) {
                    views = Arrays.copyOf(views, index + 1);
                }
                views[index] = (BaseView) target.object;
                m.setLinkedViews(views);
            } else if (obj instanceof AbstractController) {
                @SuppressWarnings("unchecked")
                AbstractController<BaseModel<?>> c = (AbstractController<BaseModel<?>>) obj;
                assert (index == 0);
                c.setModel((BaseModel<?>) target.object);
            } else if (obj instanceof AbstractMultiController) {
                @SuppressWarnings("unchecked")
                AbstractMultiController<BaseModel<?>> c = (AbstractMultiController<BaseModel<?>>) obj;
                BaseModel[] models = c.getLinkedModels();
                if (models == null) {
                    models = new BaseModel[index + 1];
                }
                if (models.length <= index) {
                    models = Arrays.copyOf(models, index + 1);
                }
                models[index] = (BaseModel) target.object;
                c.setLinkedModels(models);
            } else {
                LOG.warning("unable to link the two objects " + obj.getClass() + " with " + target.object.getClass()
                        + "\n, source is no AbstractModel, AbstractMultiModel, AbstractController or AbstractMultiController");
                return;
            }
            if (obj instanceof NamedSlots) {
                NamedSlots ns = (NamedSlots) obj;
                String[] names = ns.getSlotNames();
                names = Arrays.copyOf(names, Math.max(names.length, index + 1));
                if (names[index] == null) {
                    names[index] = "null";
                }
                ns.setSlotNames(names);
            }
            LOG.info("successfully linked " + obj.getClass() + " with " + target.object.getClass() + " at pin "
                    + index);
        }

    }

    private class EdgeInfo {
        private PinInfo source;
        private PinInfo target;
        private String name;

        public EdgeInfo(PinInfo source, PinInfo target) {
            this.source = source;
            this.target = target;
            source.edge = this;
            target.edge = this;
        }

        @Override
        public int hashCode() {
            int hash = 5;
            hash = 97 * hash + Objects.hashCode(this.source);
            hash = 97 * hash + Objects.hashCode(this.target);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final EdgeInfo other = (EdgeInfo) obj;
            if (!Objects.equals(this.source, other.source)) {
                return false;
            }
            if (!Objects.equals(this.target, other.target)) {
                return false;
            }
            return true;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
            source.object.updateSlotNames();
        }

    }
    //</editor-fold>

    private class SelectProviderImpl implements SelectProvider {
        private Widget oldSelected = null;

        @Override
        public boolean isAimingAllowed(Widget widget, Point localLocation, boolean invertSelection) {
            return true;
        }

        @Override
        public boolean isSelectionAllowed(Widget widget, Point localLocation, boolean invertSelection) {
            return true;
        }

        @Override
        public void select(Widget widget, Point localLocation, boolean invertSelection) {
            if (oldSelected == widget) {
                return;
            }
            if (oldSelected != null) {
                setSelected(oldSelected, false);
            }
            setSelected(widget, true);
            oldSelected = widget;
            LOG.info("widget selected: " + widget);
        }

        private void setSelected(Widget w, boolean selected) {
            if (w instanceof ConnectionWidget) {
                ((ConnectionWidget) w).setStroke(new BasicStroke(selected ? 3 : 1));
            } else if (w instanceof PinWidget) {
                ((PinWidget) w).setSelected(selected);
            } else if (w instanceof NodeWidget) {
                ((NodeWidget) w).setSelected(selected);
            }
        }
    }

    private class ConnectProviderImpl implements ConnectProvider {
        private final GraphSceneImpl scene;

        private ConnectProviderImpl(GraphSceneImpl scene) {
            this.scene = scene;
        }

        @Override
        public boolean isSourceWidget(Widget sourceWidget) {
            return sourceWidget instanceof PinWidget;
        }

        @Override
        public ConnectorState isTargetWidget(Widget sourceWidget, Widget targetWidget) {
            if (!(sourceWidget instanceof PinWidget)) {
                return ConnectorState.REJECT_AND_STOP;
            }
            if (!(targetWidget instanceof PinWidget)) {
                return ConnectorState.REJECT;
            }
            PinWidget s = (PinWidget) sourceWidget;
            PinWidget t = (PinWidget) targetWidget;
            //first test: check that input/output flag is different
            if (s.info.input == t.info.input) {
                return ConnectorState.REJECT;
            }
            //second test: source pin must not contain connections
            PinWidget target = s.info.input ? s : t;
            PinWidget source = t.info.input ? s : t;
            for (EdgeInfo e : scene.getEdges()) {
                if (e.source == source.info) {
                    return ConnectorState.REJECT_AND_STOP; //already a connection
                }
            }
            //third test: check that the layers are correct
            if (source.info.object.type - target.info.object.type != 1) {
                return ConnectorState.REJECT;
            }
            //forth test: valid target against allowed source classes
            Class<?> targetClass = target.info.object.object.getClass();
            Class<?>[] allowedClasses = source.info.allowedClasses;
            boolean allowed = false;
            for (Class<?> ac : allowedClasses) {
                if (ac.isAssignableFrom(targetClass)) {
                    allowed = true;
                    break;
                }
            }
            if (!allowed) {
                return ConnectorState.REJECT;
            }
            //all tests passed
            return ConnectorState.ACCEPT;
        }

        @Override
        public boolean hasCustomTargetWidgetResolver(Scene scene) {
            return false;
        }

        @Override
        public Widget resolveTargetWidget(Scene scene, Point sceneLocation) {
            throw new UnsupportedOperationException("Not supported");
            //not needed because hasCustomTargetWidgetResolver returns false
        }

        @Override
        public void createConnection(Widget sourceWidget, Widget targetWidget) {
            PinWidget s = (PinWidget) sourceWidget;
            PinWidget t = (PinWidget) targetWidget;
            PinWidget target = s.info.input ? s : t;
            PinWidget source = t.info.input ? s : t;
            //connect them in real
            int index = source.info.index;
            source.info.connectTo(target.info.object);
            //add edge
            scene.addEdge(new EdgeInfo(source.info, target.info));
            //if allowed, add new pin
            if (index == source.info.object.outputPins.size() - 1) { //it was the last pin
                long maxCount = source.info.object.links.getFixedSlotCount()
                        + source.info.object.links.getMaxFlexible();
                if (maxCount > index + 1) {
                    //add next flexible pin
                    PinInfo pin = new PinInfo(source.info.object, false, index + 1,
                            source.info.object.links.getFlexibleClasses());
                    source.info.object.outputPins.add(pin);
                    scene.addPin(source.info.object, pin);
                }
            }

            fireElementChanged();
        }

    }

    private class PinWidget extends Widget {
        private final PinInfo info;
        private boolean selected;

        private PinWidget(GraphSceneImpl scene, PinInfo info) {
            super(scene);
            this.info = info;
            this.setPreferredSize(new Dimension(20, 20));

            WidgetAction.Chain actions = getActions();
            actions.addAction(scene.createObjectHoverAction());
            actions.addAction(scene.createSelectAction());
            actions.addAction(ActionFactory.createConnectAction(scene.interactionLayer, scene.connectProvider));
            actions.addAction(ActionFactory.createSelectAction(scene.selectProvider));

            if (!info.input) {
                StringBuilder tt = new StringBuilder();
                tt.append("<html>");
                for (int i = 0; i < info.allowedClasses.length; ++i) {
                    tt.append(info.allowedClasses[i].getSimpleName());
                    if (i < info.allowedClasses.length - 1) {
                        tt.append("<br>");
                    }
                }
                tt.append("</html>");
                setToolTipText(tt.toString());
            }
        }

        @Override
        protected void paintWidget() {
            Graphics2D g = getGraphics();
            int w = 20;
            int h = 20;
            g.setColor(Color.BLACK);
            int strength = selected ? 8 : 4;
            g.fillRect((w - strength) / 2, 0, strength, h);
        }

        private void setSelected(boolean selected) {
            this.selected = selected;
            repaint();
        }
    }

    private class NodeWidget extends Widget {
        private final IconNodeWidget body;
        private final Widget upperPinContainer;
        private final Widget lowerPinContainer;
        private Color color;

        private NodeWidget(final GraphSceneImpl scene, final ObjectInfo node) {
            super(scene);
            node.widget = this;

            //create body widget
            body = new IconNodeWidget(scene);
            if (node.hidden) {
                body.setBackground(Color.GRAY);
                LOG.info(node + " is hidden");
            }
            if (!element.canDelete(node.typeEnum, node.object)) {
                body.setImage(ImageUtilities.loadImage(LOCK_ICON));
            }
            if (node.hidden) {
                body.setImage(ImageUtilities.loadImage(HIDDEN_ICON));
            }
            body.setLabel(node.object.toString());
            Point2i p = objectPositions.get((IndexedRecord) node.object);
            switch (node.type) {
            case 1: //VIEW
                color = Color.RED;
                if (node.hidden)
                    color.brighter();
                body.setBorder(new LineBorder(color));
                if (p != null) {
                    this.setPreferredLocation(new Point(p.x, p.y));
                } else {
                    this.setPreferredLocation(new Point((scene.viewCount++) * 50, 0));
                }
                break;
            case 2: //MODEL
                color = Color.BLUE;
                if (node.hidden)
                    color.brighter();
                body.setBorder(new LineBorder(color));
                if (p != null) {
                    this.setPreferredLocation(new Point(p.x, p.y));
                } else {
                    this.setPreferredLocation(new Point((scene.viewCount++) * 50, 100));
                }
                break;
            case 3: //CONTROLLER
                color = Color.GREEN;
                if (node.hidden)
                    color.brighter();
                body.setBorder(new LineBorder(color));
                if (p != null) {
                    this.setPreferredLocation(new Point(p.x, p.y));
                } else {
                    this.setPreferredLocation(new Point((scene.viewCount++) * 50, 200));
                }
                break;
            }

            //actions
            WidgetAction.Chain actions = super.getActions();
            actions.addAction(scene.createObjectHoverAction());
            actions.addAction(scene.createSelectAction());
            actions.addAction(ActionFactory.createMoveAction());
            //TODO: add actions for popup actions and double-click actions, depending on the ObjectActionProvider

            //         actions.addAction (ActionFactory.createSelectAction(scene.selectProvider));
            InputMap inputMap = new InputMap();
            ActionMap actionMap = new ActionMap();
            String key = "delete";
            actionMap.put(key, new AbstractAction("delete") {

                @Override
                public void actionPerformed(ActionEvent e) {
                    if (scene.getFocusedWidget() == NodeWidget.this) {
                        System.out.println("delete node");
                        scene.mainLayer.removeChild(NodeWidget.this);
                        scene.removeNode(node);
                        scene.setFocusedWidget(null);
                    }
                }
            });
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), key);
            actions.addAction(ActionFactory.createActionMapAction(inputMap, actionMap));

            //create upper pin container
            upperPinContainer = new Widget(scene);
            upperPinContainer.setLayout(LayoutFactory.createHorizontalFlowLayout());

            //create lower pin container
            lowerPinContainer = new Widget(scene);
            lowerPinContainer.setLayout(LayoutFactory.createHorizontalFlowLayout());

            //add them to this parent widget
            this.setLayout(LayoutFactory.createVerticalFlowLayout());
            this.addChild(upperPinContainer);
            this.addChild(body);
            this.addChild(lowerPinContainer);

            //tooltip: implemented interfaces and superclasses
            LinkedHashSet<String> lines = new LinkedHashSet<>();
            Class<?> clazz = node.object.getClass();
            lines.add(clazz.getCanonicalName());
            addClasses(clazz, lines);
            StringBuilder tt = new StringBuilder();
            tt.append("<html>");
            String[] lineArray = lines.toArray(new String[lines.size()]);
            for (int i = 0; i < lineArray.length; ++i) {
                tt.append(lineArray[i]);
                if (i < lineArray.length - 1) {
                    tt.append("<br>");
                }
            }
            tt.append("</html>");
            body.setToolTipText(tt.toString());
        }

        private void addClasses(Class<?> clazz, LinkedHashSet<String> lines) {
            if (clazz.getSuperclass() != null) {
                lines.add(clazz.getSuperclass().getSimpleName());
            }
            for (Class<?> c : clazz.getInterfaces()) {
                lines.add(c.getSimpleName());
            }
            for (Class<?> c : clazz.getInterfaces()) {
                addClasses(c, lines);
            }
            if (clazz.getSuperclass() != null) {
                addClasses(clazz.getSuperclass(), lines);
            }
        }

        private void addPin(PinWidget pin) {
            if (pin.info.input) {
                lowerPinContainer.addChild(pin);
            } else {
                upperPinContainer.addChild(pin);
            }
        }

        private void setSelected(boolean selected) {
            body.setBorder(new LineBorder(color, selected ? 3 : 1));
        }
    }

    /**
     * Extends {@link ConnectionWidget} by adding a name to the link
     */
    private class EdgeWidget extends ConnectionWidget {
        private final EdgeInfo info;

        public EdgeWidget(final Scene scene, final EdgeInfo info) {
            super(scene);
            this.info = info;
            assert (info != null);
            //this action reacts on double clicks and activates the name editing
            getActions().addAction(new WidgetAction.Adapter() {

                @Override
                public WidgetAction.State mouseClicked(Widget widget, WidgetAction.WidgetMouseEvent event) {
                    if (event.getClickCount() == 2 && event.getButton() == MouseEvent.BUTTON1
                            && info.getName() != null) {
                        //edit name
                        String n = JOptionPane.showInputDialog(ElementPropertyVisualPanel1.this, "Set edge name",
                                info.getName());
                        if (n != null && !n.isEmpty()) {
                            info.setName(n);
                            EdgeWidget.this.repaint();
                        }
                        return WidgetAction.State.CONSUMED;
                    }
                    return super.mouseClicked(widget, event);
                }

            });
        }

        @Override
        protected void paintWidget() {
            super.paintWidget();

            if (info.getName() != null) {
                Graphics2D g = getGraphics();
                AffineTransform trafo = new AffineTransform(g.getTransform());
                Point ps = getSourceAnchor().getRelatedSceneLocation();
                Point pt = getTargetAnchor().getRelatedSceneLocation();
                trafo.translate((ps.x + pt.x) / 2, (ps.y + pt.y) / 2);
                trafo.rotate(pt.x - ps.x, pt.y - ps.y);
                if (Math.signum(pt.x - ps.x) + Math.signum(pt.y - ps.y) != 0) {
                    //text stands on the head -> rotate around 180
                    trafo.rotate(Math.PI);
                }
                g.setColor(getForeground());
                AffineTransform oldTrafo = g.getTransform();
                g.setTransform(trafo);
                FontMetrics fm = g.getFontMetrics();
                float sw = fm.stringWidth(info.getName());
                float sh = fm.getDescent();
                g.drawString(info.getName(), -sw / 2, -sh);
                g.setTransform(oldTrafo);
            }
        }

    }

    private class GraphSceneImpl extends GraphPinScene<ObjectInfo, EdgeInfo, PinInfo> {
        private LayerWidget mainLayer;
        private LayerWidget connectionLayer;
        private LayerWidget interactionLayer;
        private WidgetAction moveAction = ActionFactory.createMoveAction();
        private final ConnectProviderImpl connectProvider;
        private final SelectProviderImpl selectProvider;

        //very simple layout
        private int viewCount = 0;
        private int modelCount = 0;
        private int controllerCount = 0;

        private GraphSceneImpl() {
            mainLayer = new LayerWidget(this);
            addChild(mainLayer);
            connectionLayer = new LayerWidget(this);
            addChild(connectionLayer);
            interactionLayer = new LayerWidget(this);
            addChild(interactionLayer);

            connectProvider = new ConnectProviderImpl(this);
            selectProvider = new SelectProviderImpl();
        }

        /**
         * Adds a new object to the scene.
         * The pins are created according to the ObjectInfo
         * @param info 
         */
        private void addObject(ObjectInfo info) {
            info.node.addChangeListener(ElementPropertyVisualPanel1.this);
            addNode(info);
            //add pins
            for (PinInfo p : info.outputPins) {
                addPin(info, p);
            }
            if (info.type != 3) { //no controller
                PinInfo pin = new PinInfo(info, true, info.type);
                info.inputPin = pin;
                addPin(info, pin);
            }
        }

        /**
         * Connects the object with the specified targets.
         * The targets are instances of View[], Model[] or Controller[] as returned
         * by getViews(), getModels() or getControllers().
         * Warning: It is not checked if the connection is allowed (no validation).
         * All previous edges are removed.
         * @param info the source info
         * @param targets the target info
         */
        private void connectPins(ObjectInfo info, Collection<? extends Object> targets) {
            long count = info.requestPins(targets.size());
            assert (count >= targets.size()) : "Unable to create enough pins for " + info;
            for (PinInfo p : info.outputPins) {
                addPin(info, p);
            }
            int index = 0;
            for (Object t : targets) {
                boolean found = false;
                for (ObjectInfo i : getNodes()) {
                    if (i.object == t) { //reference equality
                        //add edge
                        addEdge(new EdgeInfo(info.outputPins.get(index), i.inputPin));
                        found = true;
                        break;
                    }
                }
                if (found == false) {
                    LOG.warning("no target widget found for object " + t);
                }
                index++;
            }
            info.updateSlotNames();
        }

        private void setElement() {
            LOG.log(Level.INFO, "element views: {0}", Arrays.toString(element.getViews()));
            LOG.log(Level.INFO, "element models: {0}", Arrays.toString(element.getModels()));
            LOG.log(Level.INFO, "element controllers: {0}", Arrays.toString(element.getControllers()));
            //collect hidden objects
            Set<BaseView> hiddenViews = new HashSet<>();
            Set<BaseModel<? extends BaseView>> hiddenModels = new HashSet<>();
            for (BaseModel<? extends BaseView> m : element.getModels()) {
                if (m != null) {
                    hiddenViews.addAll(m.getViews());
                }
            }
            for (BaseController<? extends BaseModel<? extends BaseView>> c : element.getControllers()) {
                if (c != null) {
                    hiddenModels.addAll(c.getModels());
                }
            }
            hiddenViews.removeAll(Arrays.asList(element.getViews()));
            hiddenModels.removeAll(Arrays.asList(element.getModels()));
            LOG.log(Level.INFO, "hidden views: {0}", hiddenViews);
            LOG.log(Level.INFO, "hidden models: {0}", hiddenModels);

            for (BaseView v : element.getViews()) {
                addObject(new ObjectInfo(v, 1, false));
            }
            for (BaseView v : hiddenViews) {
                if (v == null)
                    continue;
                addObject(new ObjectInfo(v, 1, true));
            }
            for (BaseModel<? extends BaseView> m : element.getModels()) {
                ObjectInfo info = new ObjectInfo(m, 2, false);
                addObject(info);
                connectPins(info, m.getViews());
            }
            for (BaseModel<? extends BaseView> m : hiddenModels) {
                if (m == null)
                    continue;
                ObjectInfo info = new ObjectInfo(m, 2, true);
                addObject(info);
                connectPins(info, m.getViews());
            }
            for (BaseController<? extends BaseModel<?>> c : element.getControllers()) {
                ObjectInfo info = new ObjectInfo(c, 3, false);
                addObject(info);
                connectPins(info, c.getModels());
            }
        }

        @Override
        protected Widget attachNodeWidget(ObjectInfo node) {
            node.updateSlotNames();
            NodeWidget widget = new NodeWidget(this, node);
            mainLayer.addChild(widget);
            return widget;
        }

        @Override
        protected Widget attachEdgeWidget(final EdgeInfo edge) {

            //final ConnectionWidget widget = new ConnectionWidget (this);
            final EdgeWidget widget = new EdgeWidget(this, edge);
            widget.setTargetAnchorShape(AnchorShape.TRIANGLE_FILLED);
            Widget sourceWidget = findWidget(edge.source);
            Widget targetWidget = findWidget(edge.target);
            widget.setSourceAnchor(AnchorFactory.createRectangularAnchor(sourceWidget));
            widget.setTargetAnchor(AnchorFactory.createRectangularAnchor(targetWidget));

            WidgetAction.Chain actions = widget.getActions();
            InputMap inputMap = new InputMap();
            ActionMap actionMap = new ActionMap();
            String key = "delete";
            actionMap.put(key, new AbstractAction("delete") {

                @Override
                public void actionPerformed(ActionEvent e) {
                    if (getFocusedWidget() == widget) {
                        System.out.println("delete connection");
                        widget.setSourceAnchor(null);
                        widget.setTargetAnchor(null);
                        connectionLayer.removeChild(widget);
                        removeEdge(edge);
                        setFocusedWidget(null);
                    }
                }
            });
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), key);
            actions.addAction(ActionFactory.createActionMapAction(inputMap, actionMap));
            actions.addAction(createObjectHoverAction());
            actions.addAction(createSelectAction());

            connectionLayer.addChild(widget);

            //force name update
            edge.setName(edge.getName());

            return widget;
        }

        @Override
        protected Widget attachPinWidget(ObjectInfo node, PinInfo pin) {
            PinWidget widget = new PinWidget(scene, pin);
            NodeWidget objectWidget = (NodeWidget) findWidget(node);
            objectWidget.addPin(widget);
            return widget;
        }

        @Override
        protected void attachEdgeSourceAnchor(EdgeInfo edge, PinInfo oldSourcePin, PinInfo sourcePin) {
            ConnectionWidget edgeWidget = (ConnectionWidget) findWidget(edge);
            Widget sourcePinWidget = findWidget(sourcePin);
            Anchor sourceAnchor = AnchorFactory.createRectangularAnchor(sourcePinWidget);
            edgeWidget.setSourceAnchor(sourceAnchor);
        }

        @Override
        protected void attachEdgeTargetAnchor(EdgeInfo edge, PinInfo oldTargetPin, PinInfo targetPin) {
            ConnectionWidget edgeWidget = (ConnectionWidget) findWidget(edge);
            Widget targetPinWidget = findWidget(targetPin);
            Anchor targetAnchor = AnchorFactory.createRectangularAnchor(targetPinWidget);
            edgeWidget.setTargetAnchor(targetAnchor);
        }

        @Override
        public void userSelectionSuggested(Set<?> suggestedSelectedObjects, boolean invertSelection) {
            System.out.println("user selection: " + suggestedSelectedObjects);
            Object first = suggestedSelectedObjects.iterator().next();
            if (first instanceof ObjectInfo) {
                ObjectNode node = ((ObjectInfo) first).node;
                node.setReadOnlyProperties(((ObjectInfo) first).hidden);
                propertySheet.setNodes(new Node[] { node });
                selectedObject = ((ObjectInfo) first).node;
            }
            setFocusedObject(first);
            setFocusedWidget(findWidget(first));
            fireElementChanged();
        }

    }

    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        setLayout(new java.awt.GridLayout(1, 2, 0, 5));
    }// </editor-fold>//GEN-END:initComponents

    @Override
    public String getName() {
        return "";
    }

    // Variables declaration - do not modify//GEN-BEGIN:variables
    // End of variables declaration//GEN-END:variables

}