All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.marid.bd.schema.SchemaEditor Maven / Gradle / Ivy

/*
 * Copyright (C) 2014 Dmitry Ovchinnikov
 * Marid, the free data acquisition and visualization software
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see .
 */

package org.marid.bd.schema;

import org.apache.commons.lang3.tuple.Pair;
import org.marid.bd.Block;
import org.marid.bd.BlockComponent;
import org.marid.bd.BlockLink;
import org.marid.bd.common.DndBlockSource;
import org.marid.bd.shapes.Link;
import org.marid.bd.shapes.LinkShape;
import org.marid.bd.shapes.LinkShapeEvent;
import org.marid.concurrent.MaridTimerTask;
import org.marid.ide.components.SchemaFrameConfiguration;
import org.marid.ide.frames.schema.SchemaFrame;
import org.marid.spring.annotation.PrototypeComponent;
import org.marid.swing.InputMaskType;
import org.marid.swing.SwingUtil;
import org.marid.swing.dnd.DndTarget;
import org.marid.swing.dnd.MaridTransferHandler;
import org.marid.swing.geom.ShapeUtils;
import org.springframework.beans.factory.annotation.Autowired;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.AffineTransform;
import java.rmi.server.UID;
import java.util.*;
import java.util.List;
import java.util.Timer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Predicate;

import static java.awt.AWTEvent.MOUSE_EVENT_MASK;
import static java.awt.AWTEvent.MOUSE_MOTION_EVENT_MASK;
import static java.awt.AWTEvent.MOUSE_WHEEL_EVENT_MASK;
import static java.awt.EventQueue.invokeLater;
import static java.awt.RenderingHints.KEY_ANTIALIASING;
import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON;
import static java.awt.event.InputEvent.ALT_DOWN_MASK;
import static java.awt.event.InputEvent.META_DOWN_MASK;
import static java.awt.event.InputEvent.SHIFT_DOWN_MASK;
import static java.awt.event.MouseEvent.*;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang3.tuple.Pair.of;
import static org.marid.bd.BlockComponent.Input;
import static org.marid.bd.BlockComponent.Output;
import static org.marid.concurrent.AtomicUtils.processDirty;
import static org.marid.swing.SwingUtil.componentStream;
import static org.marid.swing.geom.ShapeUtils.mouseEvent;
import static org.marid.swing.geom.ShapeUtils.ptAdd;

/**
 * @author Dmitry Ovchinnikov
 */
@PrototypeComponent
public class SchemaEditor extends JComponent implements DndTarget, DndBlockSource {

    protected final AffineTransform transform = new AffineTransform();
    private final Point mousePoint = new Point();
    private final Rectangle clip = new Rectangle();
    private final AffineTransform mouseTransform = new AffineTransform();
    private Component curComponent;
    private final ComponentGroup selection = new ComponentGroup();
    private LinkShape currentLink;
    private volatile InputMaskType panType, dragType;
    private final AtomicBoolean dirty = new AtomicBoolean();
    private final Timer timer = new Timer(true);
    private Block selectedBlock;
    private final Set links = Collections.newSetFromMap(new ConcurrentHashMap<>());
    private boolean selectionMode;
    private final SchemaFrameConfiguration configuration;

    @Autowired
    public SchemaEditor(SchemaFrameConfiguration configuration) {
        this.configuration = configuration;
        panType = configuration.pan.get();
        dragType = configuration.drag.get();
        setFont(UIManager.getFont("Label.font"));
        setBackground(SystemColor.controlLtHighlight);
        setDoubleBuffered(true);
        setName(new UID().toString());
        setTransferHandler(new MaridTransferHandler());
        setForeground(SystemColor.controlDkShadow);
        enableEvents(MOUSE_EVENT_MASK | MOUSE_MOTION_EVENT_MASK | MOUSE_WHEEL_EVENT_MASK);
        configuration.pan.addListener(this, n -> panType = n);
        configuration.drag.addListener(this, n -> dragType = n);
        configuration.link.addListener(this, n -> EventQueue.invokeLater(() -> {
            final List> pairs = links.stream().map(l -> of(l.output, l.input)).collect(toList());
            links.clear();
            pairs.forEach(p -> addLink(p.getLeft(), p.getRight()));
            repaint();
        }));
    }

    public void load(SchemaModel schemaModel) {
        selection.clear();
        removeAll();
        links.clear();
        final Map blockMap = new IdentityHashMap<>();
        schemaModel.getBlocks().forEach(i -> {
            final BlockComponent blockComponent = i.getBlock().createComponent();
            blockComponent.setLocation(i.getLocation());
            blockComponent.updateBlock();
            add(blockComponent.getComponent());
            blockMap.put(blockComponent.getBlock(), blockComponent);
            blockComponent.setVisible(false);
        });
        schemaModel.getLinks().forEach(i -> {
            final BlockLink link = i.getBlockLink();
            final BlockComponent source = blockMap.get(link.getSource());
            final BlockComponent target = blockMap.get(link.getTarget());
            addLink(source.outputFor(link.getOutput()), target.inputFor(link.getInput()));
        });
        repaint();
    }

    public void start() {
        timer.schedule(new MaridTimerTask(() -> {
            if (!dirty.get()) {
                final Point mousePoint = MouseInfo.getPointerInfo().getLocation();
                final Point topLeft = new Point(0, 0);
                SwingUtilities.convertPointToScreen(topLeft, this);
                final Rectangle screenBounds = new Rectangle(topLeft, getSize());
                if (screenBounds.contains(mousePoint)) {
                    SwingUtilities.convertPointFromScreen(mousePoint, this);
                    final BlockComponent blockComponent = findBlockComponent(bc -> bc.getBounds().contains(mousePoint));
                    if (blockComponent != null) {
                        links.stream().filter(link -> link.isAssociatedWith(blockComponent)).forEach(link -> {
                            link.update();
                            dirty.compareAndSet(false, true);
                        });
                    }
                    final LinkShape clink = currentLink;
                    if (clink != null) {
                        clink.update();
                        dirty.compareAndSet(false, true);
                    }
                }
                if (dirty.getAndSet(false)) {
                    super.repaint();
                }
            } else {
                processDirty(dirty, this::update);
            }
        }), 50L, 50L);
    }

    public void stop() {
        timer.cancel();
    }

    public void update() {
        links.forEach(LinkShape::update);
        invokeLater(super::repaint);
    }

    public void setSelectionMode(boolean selectionMode) {
        if (this.selectionMode != selectionMode) {
            firePropertyChange("selectionMode", this.selectionMode, selectionMode);
        }
        this.selectionMode = selectionMode;
        if (!selectionMode) {
            selection.clear();
            repaint();
        }
    }

    public boolean isSelectionMode() {
        return selectionMode;
    }

    public LinkShape addLink(Output output, Input input) {
        final LinkShape link = configuration.link.get().linkShapeFor(output, input);
        links.add(link);
        info("Added link: {0}", link);
        return link;
    }

    public List removeAllLinks(BlockComponent component) {
        final List removed = new ArrayList<>();
        for (final Iterator i = links.iterator(); i.hasNext(); ) {
            final LinkShape link = i.next();
            if (link.output.getBlockComponent() == component || link.input.getBlockComponent() == component) {
                i.remove();
                removed.add(new Link(link));
            }
        }
        return removed;
    }

    public Set getLinkShapes() {
        return links;
    }

    public void removeLink(LinkShape link) {
        links.remove(link);
        repaint();
    }

    public void createLinks(List links) {
        for (final Link link : links) {
            final Output output = link.outputComponent.outputFor(link.output);
            final Input input = link.inputComponent.inputFor(link.input);
            if (output != null && input != null) {
                addLink(output, input);
            }
        }
        repaint();
    }

    @Override
    protected void processMouseWheelEvent(MouseWheelEvent e) {
        zoom(1.0 + e.getPreciseWheelRotation() / 10.0, e.getPoint());
    }

    @Override
    protected void processMouseMotionEvent(MouseEvent e) {
        switch (e.getID()) {
            case MouseEvent.MOUSE_DRAGGED:
                if (panType.isEnabled(e)) {
                    final Point p = SwingUtil.transform(mouseTransform::inverseTransform, e.getPoint());
                    transform.setTransform(mouseTransform);
                    transform.translate(p.getX() - mousePoint.getX(), p.getY() - mousePoint.getY());
                    repaint();
                    return;
                } else if (isSelectionMode() && !selection.isEmpty() && !e.isControlDown()) {
                    final Point mp = SwingUtil.transform(transform::inverseTransform, e.getPoint());
                    selection.move(mp);
                    repaint();
                    currentLink = null;
                    return;
                } else if (!selection.isEmptySelection()) {
                    final Point mp = SwingUtil.transform(transform::inverseTransform, e.getPoint());
                    selection.updateSelection(mp);
                    repaint();
                }
                break;
        }
        processMouseEvent(e);
    }

    @Override
    protected void processMouseEvent(MouseEvent e) {
        mousePoint.setLocation(SwingUtil.transform(transform::inverseTransform, e.getPoint()));
        mouseTransform.setTransform(transform);
        if (isSelectionMode() && dispatchSelection(e)) {
            return;
        }
        if (!isSelectionMode() && dispatchBlocks(e)) {
            return;
        }
        if (!isSelectionMode() && dispatchLinks(e)) {
            return;
        }
        if (e.isPopupTrigger()) {
            new SchemaEditorPopupMenu().show(this, e.getX(), e.getY());
        }
    }

    private boolean dispatchBlocks(MouseEvent e) {
        for (int i = getComponentCount() - 1; i >= 0; i--) {
            final Component component = getComponent(i);
            final Point p = ptAdd(1, mousePoint, -1, component.getLocation());
            if (component.contains(p)) {
                if (component instanceof BlockComponent) {
                    if (e.isPopupTrigger()) {
                        final JPopupMenu popupMenu = ((BlockComponent) component).popupMenu();
                        popupMenu.show(this, e.getX(), e.getY());
                        return true;
                    } else if (e.getID() == MOUSE_DRAGGED && dragType.isEnabled(e)) {
                        selectedBlock = ((BlockComponent) component).getBlock();
                        getTransferHandler().exportAsDrag(this, e, DND_COPY);
                        return true;
                    }
                }
                final Component s = ShapeUtils.findChild(component, p);
                SwingUtil.dispatchEvent(mouseEvent(s, e, e.getID(), p));
                changeCurrentComponent(s, e, p);
                changeCurrentLink(null, e);
                return true;
            }
        }
        changeCurrentComponent(null, e, mousePoint);
        return false;
    }

    private boolean dispatchLinks(MouseEvent e) {
        for (final LinkShape linkShape : links) {
            final Shape shape = linkShape.getShape();
            if (ShapeUtils.contains(shape, mousePoint, 3.0)) {
                if (e.isPopupTrigger()) {
                    linkShape.popupMenu().show(this, e.getX(), e.getY());
                }
                changeCurrentLink(linkShape, e);
                return true;
            }
        }
        changeCurrentLink(null, e);
        return false;
    }

    private boolean dispatchSelection(MouseEvent e) {
        switch (e.getID()) {
            case MOUSE_CLICKED:
                if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 1) {
                    final BlockComponent component = findBlockComponent(c -> c.getBounds().contains(mousePoint));
                    if (component != null) {
                        if (!e.isShiftDown()) {
                            selection.clear();
                        }
                        selection.addComponent(component.getComponent());
                        repaint();
                        return true;
                    }
                }
                return false;
            case MOUSE_PRESSED:
            case MOUSE_RELEASED:
                if (e.isPopupTrigger() || e.getButton() != MouseEvent.BUTTON1) {
                    return false;
                } else if ((e.getModifiers() & (ALT_DOWN_MASK | SHIFT_DOWN_MASK | META_DOWN_MASK)) != 0) {
                    return false;
                } else if (selection.contains(mousePoint)) {
                    selection.reset();
                    return false;
                }
                switch (e.getID()) {
                    case MOUSE_PRESSED:
                        if (!e.isShiftDown() && !e.isControlDown()) {
                            selection.clear();
                        }
                        selection.startSelection(mousePoint);
                        break;
                    case MOUSE_RELEASED:
                        if (!selection.isEmptySelection()) {
                            selection.endSelection(mousePoint, Arrays.asList(getComponents()));
                        } else {
                            selection.reset();
                        }
                        break;
                }
                repaint();
                return true;
            default:
                return false;
        }
    }

    private void changeCurrentComponent(Component component, MouseEvent e, Point point) {
        if (component != curComponent) {
            if (component == null) {
                curComponent.dispatchEvent(mouseEvent(curComponent, e, MOUSE_EXITED, point));
            } else {
                if (curComponent == null) {
                    component.dispatchEvent(mouseEvent(component, e, MOUSE_ENTERED, point));
                } else {
                    curComponent.dispatchEvent(mouseEvent(curComponent, e, MOUSE_EXITED, point));
                    component.dispatchEvent(mouseEvent(component, e, MOUSE_ENTERED, point));
                }
            }
            curComponent = component;
            repaint();
        }
    }

    private void changeCurrentLink(LinkShape newLink, MouseEvent e) {
        if (newLink != currentLink) {
            if (newLink == null) {
                fireLinkEvent(currentLink, LinkShapeEvent.MOUSE_EXITED, e);
            } else {
                if (currentLink == null) {
                    fireLinkEvent(newLink, LinkShapeEvent.MOUSE_ENTERED, e);
                } else {
                    fireLinkEvent(currentLink, LinkShapeEvent.MOUSE_EXITED, e);
                    fireLinkEvent(newLink, LinkShapeEvent.MOUSE_ENTERED, e);
                }
            }
            currentLink = newLink;
            repaint();
        }
    }

    private void fireLinkEvent(LinkShape link, int id, MouseEvent e) {
        final SchemaFrame schemaFrame = (SchemaFrame) SwingUtilities.windowForComponent(this);
        schemaFrame.fireEvent(new LinkShapeEvent(link, id, e));
        final AbstractButton outputButton = link.output.getButton();
        final AbstractButton inputButton = link.input.getButton();
        switch (id) {
            case LinkShapeEvent.MOUSE_ENTERED:
                outputButton.dispatchEvent(mouseEvent(outputButton, e, MOUSE_ENTERED, new Point()));
                inputButton.dispatchEvent(mouseEvent(inputButton, e, MOUSE_ENTERED, new Point()));
                break;
            case LinkShapeEvent.MOUSE_EXITED:
                outputButton.dispatchEvent(mouseEvent(outputButton, e, MOUSE_EXITED, new Point()));
                inputButton.dispatchEvent(mouseEvent(inputButton, e, MOUSE_EXITED, new Point()));
                break;
        }
    }

    @Override
    protected void paintComponent(Graphics graphics) {
        final Graphics2D g = (Graphics2D) graphics;
        g.getClipBounds(clip);
        g.setBackground(getBackground());
        g.clearRect(clip.x, clip.y, clip.width, clip.height);
        g.transform(transform);
        g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
        links.forEach(l -> l.paint(g, l == currentLink));
        for (int i = getComponentCount() - 1; i >= 0; i--) {
            final Component c = getComponent(i);
            g.translate(c.getX(), c.getY());
            c.print(g);
            g.translate(-c.getX(), -c.getY());
        }
        selection.paint(g);
    }

    @Override
    protected void paintChildren(Graphics g) {
    }

    @Override
    public void repaint() {
        dirty.set(true);
    }

    public void alignToLeft(ActionEvent actionEvent) {
        selection.toLeft();
        repaint();
    }

    public void alignToRight(ActionEvent actionEvent) {
        selection.toRight();
        repaint();
    }

    public void resetInputOutputSelection(ActionEvent actionEvent) {
        visitBlockComponents(bc -> {
            bc.getOutputs().forEach(o -> o.getButton().setSelected(false));
            bc.getInputs().forEach(i -> i.getButton().setSelected(false));
        });
        repaint();
    }

    public void visitBlockComponents(Consumer componentConsumer) {
        synchronized (getTreeLock()) {
            for (int i = getComponentCount() - 1; i >= 0; i--) {
                final Component c = getComponent(i);
                if (c instanceof BlockComponent) {
                    componentConsumer.accept((BlockComponent) c);
                }
            }
        }
    }

    public BlockComponent findBlockComponent(Predicate predicate) {
        synchronized (getTreeLock()) {
            for (int i = getComponentCount() - 1; i >= 0; i--) {
                final Component c = getComponent(i);
                if (c instanceof BlockComponent) {
                    final BlockComponent blockComponent = (BlockComponent) c;
                    if (predicate.test(blockComponent)) {
                        return blockComponent;
                    }
                }
            }
        }
        return null;
    }

    public void zoom(double scale, Point point) {
        final Point mp = SwingUtil.transform(transform::inverseTransform, point);
        transform.translate(mp.getX(), mp.getY());
        transform.scale(scale, scale);
        transform.translate(-mp.getX(), -mp.getY());
        repaint();
    }

    public void zoomIn() {
        zoom(1.1, new Point(getWidth() / 2, getHeight() / 2));
    }

    public void zoomOut() {
        zoom(0.9, new Point(getWidth() / 2, getHeight() / 2));
    }

    public void resetZoom() {
        transform.setToIdentity();
        final int minX = componentStream(this).mapToInt(Component::getX).min().orElse(0);
        final int minY = componentStream(this).mapToInt(Component::getY).min().orElse(0);
        componentStream(this).forEach(c -> c.setLocation(c.getX() - minX + 10, c.getY() - minY + 10));
        repaint();
    }

    @Override
    public boolean dropDndObject(Block object, TransferHandler.TransferSupport support) {
        return dropBlock(object, support.getDropLocation().getDropPoint(), support.getDropAction());
    }

    public boolean dropBlock(Block object, Point dropPoint, int action) {
        try {
            transform.inverseTransform(new Point(dropPoint), dropPoint);
            final BlockComponent blockComponent = object.createComponent();
            blockComponent.setBounds(new Rectangle(dropPoint, blockComponent.getPreferredSize()));
            add(blockComponent.getComponent());
            blockComponent.setVisible(false);
            repaint();
            return true;
        } catch (Exception x) {
            warning("Unable to transform coordinates", x);
            return false;
        }
    }

    @Override
    public Block getDndObject() {
        return selectedBlock;
    }

    @Override
    public void dndObjectExportDone(Block dndObject, int action) {
        selectedBlock = null;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy