ActiveDrag.java

package swingtree.components;

import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import swingtree.DragAwayComponentConf;
import swingtree.layout.Bounds;
import swingtree.layout.Position;
import swingtree.layout.Size;
import swingtree.style.ComponentExtension;

import javax.swing.JComponent;
import javax.swing.JRootPane;
import java.awt.*;
import java.awt.dnd.DragSource;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.awt.image.RescaleOp;
import java.util.Objects;
import java.util.Optional;

import static javax.swing.SwingUtilities.convertPoint;
import static javax.swing.SwingUtilities.getDeepestComponentAt;

final class ActiveDrag {

    private static final Logger log = org.slf4j.LoggerFactory.getLogger(ActiveDrag.class);

    private static final ActiveDrag NO_DRAG = new ActiveDrag(
                                                        null, null, 0,
                                                        Position.origin(), Position.origin(), Position.origin(),
                                                        null
                                                    );

    public static ActiveDrag none() { return NO_DRAG; }

    private final @Nullable Component                draggedComponent;
    private final @Nullable BufferedImage            currentDragImage;
    private final int                                componentHash;
    private final Position start;
    private final Position offset;
    private final Position localOffset;
    private final @Nullable DragAwayComponentConf<?> dragConf;

    private ActiveDrag(
        @Nullable Component                draggedComponent,
        @Nullable BufferedImage            currentDragImage,
        int                                componentHash,
        Position                           start,
        Position                           offset,
        Position                           localOffset,
        @Nullable DragAwayComponentConf<?> dragConf
    ) {
        this.draggedComponent  = draggedComponent;
        this.currentDragImage  = currentDragImage;
        this.componentHash     = componentHash;
        this.start             = Objects.requireNonNull(start);
        this.offset            = Objects.requireNonNull(offset);
        this.localOffset       = Objects.requireNonNull(localOffset);
        this.dragConf          = dragConf;
    }

    public @Nullable Component draggedComponent() {
        return draggedComponent;
    }

    public @Nullable BufferedImage currentDragImage() {
        return currentDragImage;
    }

    public Optional<DragAwayComponentConf<?>> dragConf() {
        return Optional.ofNullable(dragConf);
    }

    public ActiveDrag begin(Point dragStart, @Nullable JRootPane rootPane)
    {
        if ( rootPane == null )
            return this;
        /*
           We traverse the entire component tree
           to find the deepest component that is under the mouse
           and has a non-default cursor
           if we find such a component, we store it in _toBeDragged
         */
        java.awt.Component component = getDeepestComponentAt(
                                                rootPane.getContentPane(),
                                                dragStart.x, dragStart.y
                                            );

        if ( !(component instanceof JComponent) )
            return this; // We only support JComponents for dragging

        Position mousePosition = Position.of(dragStart);

        Optional<DragAwayComponentConf<JComponent>> dragConf;
        do {
            dragConf = ComponentExtension.from((JComponent) component).getDragAwayConf(mousePosition);
            if ( !dragConf.isPresent() || dragConf.map(it->!it.enabled()).orElse(false) ) {
                Component parent = component.getParent();
                if ( parent instanceof JComponent )
                    component = parent;
                else
                    return this;
            }
        }
        while ( !dragConf.isPresent() || dragConf.map(it->!it.enabled()).orElse(false) );

        Position absoluteComponentPosition = Position.of(convertPoint(component, 0,0, rootPane.getContentPane()));

        return this.withDragConf(dragConf.get())
                    .withDraggedComponent(component)
                     .withStart(mousePosition)
                     .withOffset(Position.origin())
                     .withLocalOffset(mousePosition.minus(absoluteComponentPosition))
                     .renderComponentIntoImage();
    }


    public ActiveDrag renderComponentIntoImage()
    {
        Objects.requireNonNull(dragConf);
        BufferedImage image = dragConf.customDragImage().map(ActiveDrag::toBufferedImage).orElse(this.currentDragImage);
        Component component = Objects.requireNonNull(draggedComponent);

        int currentComponentHash = 0;
        if ( component instanceof JComponent ) {
            currentComponentHash = ComponentExtension.from((JComponent) component).viewStateHashCode();
            if ( currentComponentHash == this.componentHash && image != null )
                return this;
        }
        else return this;

        if ( dragConf.opacity() <= 0.0 )
            return this;

        boolean weNeedClearing = image != null;

        if ( image == null )
            image = new BufferedImage(
                        component.getWidth(),
                        component.getHeight(),
                        BufferedImage.TYPE_INT_ARGB
                    );

        // We wipe the image before rendering the component
        Graphics2D g = image.createGraphics();
        if ( weNeedClearing ) {
            Composite oldComp = g.getComposite();
            g.setComposite(AlphaComposite.Clear);
            g.fillRect(0, 0, image.getWidth(), image.getHeight());
            g.setComposite(oldComp);
        }
        component.paint(g);

        try {
            if ( dragConf.opacity() < 1.0 && dragConf.opacity() > 0.0 ) {
                RescaleOp makeTransparent = new RescaleOp(
                                                new float[]{1.0f, 1.0f, 1.0f, /* alpha scaleFactor */ (float) dragConf.opacity()},
                                                new float[]{0f, 0f, 0f, /* alpha offset */ 0f}, null
                                            );
                image = makeTransparent.filter(image, null);
            }
        } catch (Exception e) {
            log.error("Failed to make the rendering of dragged component transparent.", e);
        }
        g.dispose();

        return new ActiveDrag(draggedComponent, image, currentComponentHash, start, offset, localOffset, dragConf);
    }

    public ActiveDrag dragged(MouseEvent e, @Nullable JRootPane rootPane)
    {
        if ( draggedComponent != null ) {
            Point point = e.getPoint();
            ActiveDrag updatedDrag = this.withOffset(Position.of(point.x - start.x(), point.y - start.y()))
                                         .renderComponentIntoImage();
            return updatedDrag;
        }
        return this;
    }

    public boolean hasDraggedComponent() {
        return draggedComponent != null;
    }

    Size draggedComponentSize() {
        if ( draggedComponent != null ) {
            return Size.of(draggedComponent.getSize());
        }
        return Size.unknown();
    }

    public void paint(Graphics g){
        if ( !DragSource.isDragImageSupported() ) {
            /*
                If the drag image is not supported by the platform, we
                do the image rendering ourselves directly on the Graphics object
                of the glass pane of the root pane.
            */
            if (draggedComponent != null && currentDragImage != null) {
                Position whereToRender = getRenderPosition();
                g.drawImage(currentDragImage, (int) whereToRender.x(), (int) whereToRender.y(), null);
            }
        }
    }

    Position getRenderPosition() {
        return Position.of(
                (start.x() + offset.x() - localOffset.x()),
                (start.y() + offset.y() - localOffset.y())
            );
    }

    Position getStart() {
        return start;
    }

    Bounds getBounds() {
        return Bounds.of(getRenderPosition(), draggedComponentSize());
    }

    public ActiveDrag withDraggedComponent(@Nullable Component draggedComponent) {
        return new ActiveDrag(draggedComponent, currentDragImage, componentHash, start, offset, localOffset, dragConf);
    }

    public ActiveDrag withStart(Position start) {
        return new ActiveDrag(draggedComponent, currentDragImage, componentHash, start, offset, localOffset, dragConf);
    }

    public ActiveDrag withOffset(Position offset) {
        return new ActiveDrag(draggedComponent, currentDragImage, componentHash, start, offset, localOffset, dragConf);
    }

    public ActiveDrag withLocalOffset(Position localOffset) {
        return new ActiveDrag(draggedComponent, currentDragImage, componentHash, start, offset, localOffset, dragConf);
    }

    public ActiveDrag withDragConf( DragAwayComponentConf<?> dragConf ) {
        return new ActiveDrag(draggedComponent, currentDragImage, componentHash, start, offset, localOffset, dragConf);
    }

    /**
     * Converts a given Image into a BufferedImage
     *
     * @param img The Image to be converted
     * @return The converted BufferedImage
     */
    private static BufferedImage toBufferedImage(Image img)
    {
        if (img instanceof BufferedImage)
        {
            return (BufferedImage) img;
        }

        // Create a buffered image with transparency
        BufferedImage bimage = new BufferedImage(img.getWidth(null), img.getHeight(null), BufferedImage.TYPE_INT_ARGB);

        // Draw the image on to the buffered image
        Graphics2D bGr = bimage.createGraphics();
        bGr.drawImage(img, 0, 0, null);
        bGr.dispose();

        // Return the buffered image
        return bimage;
    }

}