workcraft/workcraft

View on GitHub
workcraft/WorkcraftCore/src/org/workcraft/dom/visual/AbstractVisualModel.java

Summary

Maintainability
A
2 hrs
Test Coverage
package org.workcraft.dom.visual;

import org.workcraft.commands.AbstractLayoutCommand;
import org.workcraft.dom.Container;
import org.workcraft.dom.*;
import org.workcraft.dom.hierarchy.NamespaceHelper;
import org.workcraft.dom.hierarchy.NamespaceProvider;
import org.workcraft.dom.math.MathConnection;
import org.workcraft.dom.math.MathModel;
import org.workcraft.dom.math.MathNode;
import org.workcraft.dom.math.PageNode;
import org.workcraft.dom.references.FlatReferenceManager;
import org.workcraft.dom.references.Identifier;
import org.workcraft.dom.visual.connections.VisualConnection;
import org.workcraft.exceptions.InvalidConnectionException;
import org.workcraft.exceptions.NodeCreationException;
import org.workcraft.gui.properties.ModelProperties;
import org.workcraft.gui.properties.PropertyDeclaration;
import org.workcraft.gui.properties.PropertyDescriptor;
import org.workcraft.gui.tools.Decorator;
import org.workcraft.gui.tools.GraphEditorTool;
import org.workcraft.observation.*;
import org.workcraft.plugins.builtin.commands.DotLayoutCommand;
import org.workcraft.plugins.builtin.commands.RandomLayoutCommand;
import org.workcraft.serialisation.NoAutoSerialisation;
import org.workcraft.types.Func;
import org.workcraft.types.Pair;
import org.workcraft.types.Triple;
import org.workcraft.utils.Hierarchy;

import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.List;
import java.util.Queue;
import java.util.*;

public abstract class AbstractVisualModel extends AbstractModel<VisualNode, VisualConnection> implements VisualModel {

    public static final String PROPERTY_TITLE = "Title";

    private final MathModel mathModel;
    private Container currentLevel;
    private final Set<VisualNode> selection = new HashSet<>();
    private final ObservableStateImpl observableState = new ObservableStateImpl();
    private final List<GraphEditorTool> graphEditorTools = new ArrayList<>();

    public AbstractVisualModel() {
        this(null, null);
    }

    public AbstractVisualModel(MathModel mathModel) {
        this(mathModel, null);
    }

    public AbstractVisualModel(MathModel mathModel, VisualGroup root) {
        super(root);
        this.mathModel = mathModel;
        this.currentLevel = getRoot();

        registerGraphEditorTools();

        new TransformEventPropagator().attach(getRoot());
        new SelectionEventPropagator(this).attach(getRoot());
        new RemovedNodeDeselector(this).attach(getRoot());
        new DefaultHangingConnectionRemover(this).attach(getRoot());
        new DefaultMathNodeRemover().attach(getRoot());
        new DefaultReplicaRemover(this).attach(getRoot());

        new StateSupervisor() {
            @Override
            public void handleEvent(StateEvent e) {
                observableState.sendNotification(new ModelModifiedEvent(AbstractVisualModel.this));
            }
        }.attach(getRoot());
        if (generatedRoot) {
            createDefaultStructure();
        }
    }

    @Override
    public void applyRandomLayout(Point2D start, Point2D range) {
        Random r = new Random();
        Queue<Triple<Container, Point2D, Point2D>> queue = new LinkedList<>();
        queue.add(Triple.of(getRoot(), start, range));
        while (!queue.isEmpty()) {
            Triple<Container, Point2D, Point2D> item = queue.remove();
            Container container = item.getFirst();
            start = item.getSecond();
            range = item.getThird();
            for (Node node : container.getChildren()) {
                double x = start.getX() + r.nextDouble() * range.getX();
                double y = start.getY() + r.nextDouble() * range.getY();
                if (node instanceof VisualTransformableNode) {
                    VisualTransformableNode transformableNode = (VisualTransformableNode) node;
                    transformableNode.setRootSpacePosition(new Point2D.Double(x, y));
                }
                if (node instanceof Container) {
                    Point2D childrenRange = new Point2D.Double(range.getX() / 2.0, range.getY() / 2.0);
                    Point2D childrenStart = new Point2D.Double(x - childrenRange.getX() / 2.0, y - childrenRange.getY() / 2.0);
                    queue.add(Triple.of((Container) node, childrenStart, childrenRange));
                }
            }
        }
        for (VisualConnection connection : Hierarchy.getDescendantsOfType(getRoot(), VisualConnection.class)) {
            connection.setConnectionType(VisualConnection.ConnectionType.POLYLINE);
            connection.getGraphic().setDefaultControlPoints();
        }
    }

    @Override
    public VisualGroup createDefaultRoot() {
        return new VisualGroup();
    }

    @Override
    public FlatReferenceManager createDefaultReferenceManager() {
        return new FlatReferenceManager();
    }

    @Override
    public void createDefaultStructure() {
        HashMap<MathNode, VisualComponent> createdNodes = new HashMap<>();
        // Create components
        Queue<Pair<Container, Container>> containerQueue = new LinkedList<>();
        containerQueue.add(new Pair<>(getMathModel().getRoot(), getRoot()));
        while (!containerQueue.isEmpty()) {
            Pair<Container, Container> container = containerQueue.remove();
            Container mathContainer = container.getFirst();
            Container visualContainer = container.getSecond();
            for (Node node : mathContainer.getChildren()) {
                if (node instanceof MathConnection) continue;
                if (node instanceof MathNode) {
                    MathNode mathNode = (MathNode) node;
                    VisualComponent visualComponent = null;
                    try {
                        visualComponent = NodeFactory.createVisualComponent(mathNode);
                    } catch (NodeCreationException e) {
                        throw new RuntimeException(e);
                    }
                    if (visualComponent != null) {
                        visualContainer.add(visualComponent);
                        createdNodes.put(mathNode, visualComponent);
                    }
                    if ((mathNode instanceof Container) && (visualComponent instanceof Container)) {
                        containerQueue.add(new Pair<>((Container) mathNode, (Container) visualComponent));
                    }
                }
            }
        }
        // Create connections
        containerQueue.add(new Pair<>(getMathModel().getRoot(), getRoot()));
        while (!containerQueue.isEmpty()) {
            Pair<Container, Container> container = containerQueue.remove();
            Container mathContainer = container.getFirst();
            for (Node node : mathContainer.getChildren()) {
                if (node instanceof MathConnection) {
                    MathConnection mathConnection = (MathConnection) node;
                    VisualComponent firstComponent = createdNodes.get(mathConnection.getFirst());
                    VisualComponent secondComponent = createdNodes.get(mathConnection.getSecond());
                    try {
                        connect(firstComponent, secondComponent, mathConnection);
                    } catch (InvalidConnectionException e) {
                        throw new RuntimeException(e);
                    }
                } else if (node instanceof MathNode) {
                    MathNode mathNode = (MathNode) node;
                    VisualComponent visualComponent = createdNodes.get(mathNode);
                    if ((mathNode instanceof Container) && (visualComponent instanceof Container)) {
                        containerQueue.add(new Pair<>((Container) mathNode, (Container) visualComponent));
                    }
                }
            }
        }
    }

    @Override
    public void validateConnection(VisualNode first, VisualNode second) throws InvalidConnectionException {
        if (getConnection(first, second) != null) {
            throw new InvalidConnectionException("Connection already exists.");
        }

        if (!(first instanceof VisualComponent) || !(second instanceof VisualComponent)) {
            throw new InvalidConnectionException("Invalid connection.");
        }

        getMathModel().validateConnection(
                ((VisualComponent) first).getReferencedComponent(),
                ((VisualComponent) second).getReferencedComponent());
    }

    @Override
    public VisualConnection connect(VisualNode first, VisualNode second, MathConnection mConnection)
            throws InvalidConnectionException {

        validateConnection(first, second);
        if (mConnection == null) {
            mConnection = getMathModel().connect(getReferencedComponent(first), getReferencedComponent(second));
        }
        VisualConnection vConnection = new VisualConnection(mConnection, first, second);
        Container container = Hierarchy.getNearestContainer(first, second);
        container.add(vConnection);
        return vConnection;
    }

    @Override
    public VisualConnection connect(VisualNode first, VisualNode second) throws InvalidConnectionException {
        return connect(first, second, null);
    }

    @Override
    public void validateUndirectedConnection(VisualNode first, VisualNode second) throws InvalidConnectionException {
        validateConnection(first, second);
    }

    @Override
    public VisualConnection connectUndirected(VisualNode first, VisualNode second) throws InvalidConnectionException {
        validateUndirectedConnection(first, second);
        return connect(first, second, null);
    }

    public MathNode getReferencedComponent(VisualNode node) {
        VisualComponent component = null;
        if (node instanceof VisualComponent) {
            component = (VisualComponent) node;
        } else if (node instanceof VisualReplica) {
            component = ((VisualReplica) node).getMaster();
        }
        return (component == null) ? null : component.getReferencedComponent();
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T extends VisualComponent> T createVisualComponent(MathNode mathNode, Class<T> type) {
        VisualComponent component = null;
        Node mathParent = mathNode.getParent();
        if (mathParent == null) {
            mathModel.getRoot().add(mathNode);
        }
        Container container = getRoot();
        if (mathParent instanceof PageNode) {
            PageNode mathPage = (PageNode) mathParent;
            container = getVisualComponent(mathPage, VisualPage.class);
        }
        try {
            component = NodeFactory.createVisualComponent(mathNode);
            container.add(component);
        } catch (NodeCreationException e) {
            String mathName = getMathName(mathNode);
            throw new RuntimeException("Cannot create visual component for math node '" + mathName + "' of class '" + type + "'");
        }
        return (T) component;
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T extends VisualComponent> T createVisualComponent(MathNode mathNode, Class<T> type, Container container) {
        VisualComponent component = null;
        if (container == null) {
            container = getRoot();
        }
        try {
            component = NodeFactory.createVisualComponent(mathNode);
            container.add(component);
        } catch (NodeCreationException e) {
            String mathName = getMathName(mathNode);
            throw new RuntimeException("Cannot create visual component for math node '" + mathName + "' of class '" + type + "'");
        }
        return (T) component;
    }

    @Override
    public <T extends VisualComponent> T getVisualComponent(MathNode mathNode, Class<T> type) {
        T result = null;
        if (mathNode != null) {
            Collection<T> visualComponents = Hierarchy.getDescendantsOfType(getRoot(), type);
            for (T visualComponent: visualComponents) {
                if (visualComponent.getReferencedComponent() == mathNode) {
                    result = visualComponent;
                    break;
                }
            }
        }
        return result;
    }

    @Override
    public <T extends VisualComponent> T getVisualComponentByMathReference(String ref, Class<T> type) {
        T result = null;
        MathNode node = getMathModel().getNodeByReference(ref);
        if (node != null) {
            result = getVisualComponent(node, type);
        }
        return result;
    }

    @Override
    public <T extends VisualReplica> T createVisualReplica(VisualComponent masterComponent, Class<T> type, Container container) {
        T replica = null;
        try {
            replica = NodeFactory.createNode(type);
        } catch (NodeCreationException e) {
            e.printStackTrace();
        }
        if (replica != null) {
            if (container == null) {
                container = getRoot();
            }
            container.add(replica);
            replica.setMaster(masterComponent);
        }
        return replica;
    }

    @Override
    public VisualPage createVisualPage(Container container) {
        if (container == null) {
            container = getRoot();
        }
        VisualPage page = new VisualPage(new PageNode());
        container.add(page);

        Container mathContainer = NamespaceHelper.getMathContainer(this, container);
        mathContainer.add(page.getReferencedComponent());
        return page;
    }

    @Override
    public void draw(Graphics2D g, Decorator decorator) {
        DrawMan.draw(this, g, decorator, getRoot());
    }

    private Collection<VisualNode> saveSelection() {
        return new HashSet<>(selection);
    }

    private void notifySelectionChanged(Collection<? extends VisualNode> prevSelection) {
        sendNotification(new SelectionChangedEvent(this, prevSelection));
    }

    /**
     * Select all components, connections and groups from the <code>root</code> group.
     */
    @Override
    public void selectAll() {
        if (selection.size() == getCurrentLevel().getChildren().size()) {
            return;
        }
        Collection<VisualNode> s = saveSelection();
        selection.clear();
        Collection<VisualNode> nodes = NodeHelper.filterByType(getCurrentLevel().getChildren(), VisualNode.class);
        selection.addAll(nodes);
        notifySelectionChanged(s);
    }

    /**
     * Clear selection.
     */
    @Override
    public void selectNone() {
        if (!selection.isEmpty()) {
            Collection<VisualNode> s = saveSelection();
            selection.clear();
            notifySelectionChanged(s);
        }
    }

    /**
     * Invert selection.
     */
    @Override
    public void selectInverse() {
        Collection<VisualNode> s = saveSelection();
        selection.clear();
        Collection<VisualNode> nodes = NodeHelper.filterByType(getCurrentLevel().getChildren(), VisualNode.class);
        for (VisualNode node: nodes) {
            if (!s.contains(node)) {
                selection.add(node);
            }
        }
        notifySelectionChanged(nodes);
    }

    private void validateSelection(VisualNode node) {
        if (!Hierarchy.isDescendant(node, getCurrentLevel())) {
            throw new RuntimeException(
                "Cannot select a node that is not in the current editing level ("
                + node + "), parent (" + node.getParent() + ")");
        }
    }

    private void validateSelection(Collection<? extends VisualNode> nodes) {
        for (VisualNode node : nodes) {
            validateSelection(node);
        }
    }

    public boolean isSelected(Node node) {
        return selection.contains(node);
    }

    @Override
    public void select(Collection<? extends VisualNode> nodes) {
        if (nodes.isEmpty()) {
            selectNone();
            return;
        }
        Collection<VisualNode> s = saveSelection();
        validateSelection(nodes);
        selection.clear();
        selection.addAll(nodes);
        notifySelectionChanged(s);
    }

    @Override
    public void select(VisualNode node) {
        if (selection.size() == 1 && selection.contains(node)) {
            return;
        }
        Collection<VisualNode> s = saveSelection();
        validateSelection(node);
        selection.clear();
        selection.add(node);
        notifySelectionChanged(s);
    }

    @Override
    public void addToSelection(VisualNode node) {
        if (selection.contains(node)) {
            return;
        }
        Collection<VisualNode> s = saveSelection();
        validateSelection(node);
        selection.add(node);
        notifySelectionChanged(s);
    }

    @Override
    public void addToSelection(Collection<? extends VisualNode> nodes) {
        Collection<VisualNode> s = saveSelection();
        validateSelection(nodes);
        selection.addAll(nodes);
        if (s.size() != selection.size()) {
            notifySelectionChanged(s);
        }
    }

    @Override
    public void removeFromSelection(VisualNode node) {
        if (selection.contains(node)) {
            Collection<VisualNode> s = saveSelection();
            selection.remove(node);
            notifySelectionChanged(s);
        }
    }

    @Override
    public void removeFromSelection(Collection<? extends VisualNode> nodes) {
        Collection<VisualNode> s = saveSelection();
        selection.removeAll(nodes);
        if (s.size() != selection.size()) {
            notifySelectionChanged(s);
        }
    }

    @Override
    public MathModel getMathModel() {
        return mathModel;
    }

    @Override
    public String getMathReference(Node node) {
        if (node instanceof VisualComponent) {
            VisualComponent component = (VisualComponent) node;
            node = component.getReferencedComponent();
        }
        return getMathModel().getNodeReference(node);
    }

    @Override
    public String getMathName(Node node) {
        if (node instanceof VisualComponent) {
            VisualComponent component = (VisualComponent) node;
            node = component.getReferencedComponent();
        }
        return getMathModel().getName(node);
    }

    @Override
    public void setMathName(Node node, String name) {
        if (node instanceof VisualComponent) {
            VisualComponent component = (VisualComponent) node;
            node = component.getReferencedComponent();
        }
        getMathModel().setName(node, name);
    }

    @Override
    public Container getCurrentLevel() {
        return currentLevel;
    }

    @Override
    public void setCurrentLevel(Container newCurrentLevel) {
        selection.clear();
        currentLevel = newCurrentLevel;

        // manage the isInside value for all parents and children
        Collapsible collapsible = null;
        if (newCurrentLevel instanceof Collapsible) {
            collapsible = (Collapsible) newCurrentLevel;
        }

        if (collapsible != null) {
            collapsible.setIsCurrentLevelInside(true);
            Node parent = newCurrentLevel.getParent();
            while (parent != null) {
                if (parent instanceof Collapsible) {
                    ((Collapsible) parent).setIsCurrentLevelInside(true);
                }
                parent = parent.getParent();
            }

            for (Node node: newCurrentLevel.getChildren()) {
                if (node instanceof Collapsible) {
                    ((Collapsible) node).setIsCurrentLevelInside(false);
                }
            }
        }
    }

    /**
     * @return Returns selection ordered the same way as the objects are ordered in the currently active group.
     */
    @Override
    public Collection<VisualNode> getSelection() {
        return Collections.unmodifiableSet(selection);
    }

    @Override
    public boolean isGroupable(VisualNode node) {
        return true;
    }

    @Override
    public VisualGroup groupSelection() {
        VisualGroup group = null;
        Collection<VisualNode> nodes = SelectionHelper.getGroupableCurrentLevelSelection(this);
        if (!nodes.isEmpty()) {
            group = new VisualGroup();
            getCurrentLevel().add(group);
            getCurrentLevel().reparent(nodes, group);
            Point2D centre = TransformHelper.getSnappedCentre(nodes);
            VisualModelTransformer.translateNodes(nodes, -centre.getX(), -centre.getY());
            group.setPosition(centre);
            select(group);
        }
        return group;
    }

    @Override
    public VisualPage groupPageSelection() {
        VisualPage page = null;
        Collection<VisualNode> nodes = SelectionHelper.getGroupableCurrentLevelSelection(this);
        if (!nodes.isEmpty()) {
            Container container = getCurrentLevel();
            page = createVisualPage(container);
            if (reparent(page, this, container, nodes)) {
                Point2D pos = TransformHelper.getSnappedCentre(nodes);
                VisualModelTransformer.translateNodes(nodes, -pos.getX(), -pos.getY());
                page.setPosition(pos);
                select(page);
            }
        }
        return page;
    }

    @Override
    public void ungroupSelection() {
        ArrayList<VisualNode> toSelect = new ArrayList<>();
        for (VisualNode node : SelectionHelper.getOrderedCurrentLevelSelection(this)) {
            if (node instanceof VisualGroup) {
                VisualGroup group = (VisualGroup) node;
                Collection<VisualNode> nodesToReparent = NodeHelper.filterByType(group.getChildren(), VisualNode.class);
                toSelect.addAll(nodesToReparent);
                if (reparent(getCurrentLevel(), this, group, nodesToReparent)) {
                    getCurrentLevel().remove(group);
                }
            } else if (node instanceof VisualPage) {
                VisualPage page = (VisualPage) node;
                Collection<VisualNode> nodesToReparent = NodeHelper.filterByType(page.getChildren(), VisualNode.class);
                toSelect.addAll(nodesToReparent);
                if (reparent(getCurrentLevel(), this, page, nodesToReparent)) {
                    getMathModel().remove(page.getReferencedComponent());
                    getCurrentLevel().remove(page);
                }
            } else {
                toSelect.add(node);
            }
        }
        select(toSelect);
    }

    @Override
    public void deleteSelection() {
        // Note: The order of removal influences the remaining selection because
        // there are listeners that remove hanging connections and replica nodes.
        // Remove selected connections
        deleteSelection(node -> node instanceof VisualConnection);
        // Remove selected replica nodes
        deleteSelection(node -> node instanceof Replica);
        // Remove remaining selected nodes
        deleteSelection(node -> true);
    }

    private void deleteSelection(final Func<Node, Boolean> filter) {
        HashMap<Container, LinkedList<Node>> batches = new HashMap<>();
        for (Node node : selection) {
            if (node.getParent() instanceof Container) {
                Container container = (Container) node.getParent();
                if (filter.eval(node)) {
                    List<Node> batch = batches.computeIfAbsent(container, k -> new LinkedList<>());
                    batch.add(node);
                }
            }
        }
        for (Container container : batches.keySet()) {
            container.remove(batches.get(container));
        }
    }

    private Point2D transformToCurrentSpace(Point2D pointInRootSpace) {
        Point2D newPoint = new Point2D.Double();
        TransformHelper.getTransform(getRoot(), currentLevel).transform(pointInRootSpace, newPoint);
        return newPoint;
    }

    @Override
    public Collection<VisualNode> hitBox(Point2D p1, Point2D p2) {
        p1 = transformToCurrentSpace(p1);
        p2 = transformToCurrentSpace(p2);
        return HitMan.hitBox(currentLevel, p1, p2);
    }

    @Override
    public Point2D getNodeSpacePosition(Point2D rootspacePosition, VisualTransformableNode node) {
        AffineTransform rootToParentTransform = TransformHelper.getTransform(getRoot(), node);
        Point2D localPosition = rootToParentTransform.transform(rootspacePosition, null);

        AffineTransform parentToNodeTransform = node.getParentToLocalTransform();
        return parentToNodeTransform.transform(localPosition, null);
    }

    @Override
    public void addObserver(StateObserver obs) {
        observableState.addObserver(obs);
    }

    @Override
    public void removeObserver(StateObserver obs) {
        observableState.removeObserver(obs);
    }

    @Override
    public void sendNotification(StateEvent e) {
        observableState.sendNotification(e);
    }

    public Collection<MathNode> getMathChildren(Collection<? extends VisualNode> nodes) {
        Collection<MathNode> ret = new HashSet<>();
        for (Node node: nodes) {
            if ((node instanceof Dependent) && !(node instanceof Replica)) {
                ret.addAll(((Dependent) node).getMathReferences());
            } else if (node instanceof VisualGroup) {
                Collection<VisualNode> children = NodeHelper.filterByType(node.getChildren(), VisualNode.class);
                ret.addAll(getMathChildren(children));
            }
        }
        return ret;
    }

    @Override
    public boolean reparent(Container dstContainer, Model srcModel, Container srcRoot, Collection<? extends VisualNode> srcChildren) {
        if (srcModel == null) {
            srcModel = this;
        }
        if (srcChildren == null) {
            srcChildren = NodeHelper.filterByType(srcRoot.getChildren(), VisualNode.class);
        }

        Container srcMathContainer = NamespaceHelper.getMathContainer((VisualModel) srcModel, srcRoot);
        Collection<MathNode> srcMathChildren = getMathChildren(srcChildren);
        MathModel srcMathModel = ((VisualModel) srcModel).getMathModel();

        MathModel dstMathMmodel = getMathModel();
        Container dstMathContainer = NamespaceHelper.getMathContainer(this, dstContainer);
        if (!dstMathMmodel.reparent(dstMathContainer, srcMathModel, srcMathContainer, srcMathChildren)) {
            return false;
        }
        // Save root-space position of components and set connections scale mode to follow components.
        HashMap<VisualTransformableNode, Point2D> componentToPositionMap = VisualModelTransformer.getRootSpacePositions(srcChildren);
        Collection<Node> dstChildren = new LinkedList<>(srcChildren);
        srcRoot.reparent(dstChildren, dstContainer);
        VisualModelTransformer.setRootSpacePositions(componentToPositionMap);
        return true;
    }

    @Override
    @NoAutoSerialisation
    public String getTitle() {
        if (mathModel != null) {
            return mathModel.getTitle();
        } else {
            return super.getTitle();
        }
    }

    @Override
    @NoAutoSerialisation
    public void setTitle(String title) {
        if (mathModel != null) {
            mathModel.setTitle(title);
        } else {
            super.setTitle(title);
        }
        sendNotification(new ModelModifiedEvent(this));
    }

    @Override
    public AbstractLayoutCommand getBestLayouter() {
        return new DotLayoutCommand();
    }

    @Override
    public AbstractLayoutCommand getFallbackLayouter() {
        return new RandomLayoutCommand();
    }

    @Override
    public Rectangle2D getBoundingBox() {
        return BoundingBoxHelper.mergeBoundingBoxes(Hierarchy.getChildrenOfType(getRoot(), Touchable.class));
    }

    @Override
    public void registerGraphEditorTools() {
    }

    @Override
    public final void addGraphEditorTool(GraphEditorTool tool) {
        graphEditorTools.add(tool);
    }

    @Override
    public final void removeGraphEditorTool(GraphEditorTool tool) {
        graphEditorTools.remove(tool);
    }

    @Override
    public final List<GraphEditorTool> getGraphEditorTools() {
        return Collections.unmodifiableList(graphEditorTools);
    }

    @Override
    public ModelProperties getProperties(VisualNode node) {
        ModelProperties properties = new ModelProperties();
        if (node == null) {
            properties.add(getTitleProperty());
        } else {
            String name = getMathName(node);
            if ((name != null) && !Identifier.isInternal(name)) {
                properties.add(getNameProperty(node));
            }
        }
        return properties;
    }

    private PropertyDescriptor getTitleProperty() {
        return new PropertyDeclaration<>(String.class, PROPERTY_TITLE, this::setTitle, this::getTitle);
    }

    private PropertyDescriptor getNameProperty(VisualNode node) {
        String name = getMathName(node);
        return new PropertyDeclaration<>(String.class, Model.PROPERTY_NAME,
                value -> {
                    Identifier.validate(value);
                    if (node instanceof VisualComponent) {
                        VisualComponent component = (VisualComponent) node;
                        if (component.getReferencedComponent() instanceof NamespaceProvider) {
                            value = Identifier.appendNamespaceSeparator(value);
                        }
                    }
                    if (!value.equals(name)) {
                        setMathName(node, value);
                        node.sendNotification(new PropertyChangedEvent(node, Model.PROPERTY_NAME));
                    }
                },
                () -> name == null ? null : Identifier.truncateNamespaceSeparator(name));
    }

}