r4fterman/pdf.forms

View on GitHub
src/main/java/org/pdf/forms/gui/hierarchy/tree/CDropTargetListener.java

Summary

Maintainability
C
1 day
Test Coverage
F
6%
package org.pdf.forms.gui.hierarchy.tree;

import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragSource;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;

import javax.swing.*;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;

import org.pdf.forms.document.Page;
import org.pdf.forms.gui.IMainFrame;
import org.pdf.forms.gui.designer.IDesigner;
import org.pdf.forms.widgets.ButtonGroup;
import org.pdf.forms.widgets.IWidget;
import org.pdf.forms.widgets.RadioButtonWidget;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class CDropTargetListener implements DropTargetListener {

    private final Logger logger = LoggerFactory.getLogger(CDropTargetListener.class);

    private final Rectangle2D raCueLine = new Rectangle2D.Float();
    private final Color colorCueLine;
    private final Timer timerHover;
    private final CTree cTree;
    private final IDesigner designer;

    private TreePath lastTreePath = null;
    private Rectangle2D raGhost = new Rectangle2D.Float();
    private Point lastDragPoint = new Point();

    CDropTargetListener(
            final CTree cTree,
            final IDesigner designer) {
        this.cTree = cTree;
        this.designer = designer;

        this.colorCueLine = new Color(
                SystemColor.controlShadow.getRed(),
                SystemColor.controlShadow.getGreen(),
                SystemColor.controlShadow.getBlue(),
                64
        );

        // Set up a hover timer, so that a node will be automatically expanded or collapsed
        // if the user lingers on it for more than a short time
        this.timerHover = new Timer(500, e -> {
            if (this.cTree.isRootPath(lastTreePath)) {
                // Do nothing if we are hovering over the root node
                return;
            }
            if (!this.cTree.isExpanded(lastTreePath)) {
                this.cTree.expandPath(lastTreePath);
            }
        });
        // Set timer to one-shot mode
        timerHover.setRepeats(false);
    }

    @Override
    public void dragEnter(final DropTargetDragEvent event) {
        acceptOrRejectDrag(event, isDragNotAcceptable(event));
    }

    @Override
    public void dragExit(final DropTargetEvent event) {
        if (!DragSource.isDragImageSupported()) {
            cTree.repaint(raGhost.getBounds());
        }
    }

    @Override
    public void dragOver(final DropTargetDragEvent event) {
        // This is where the ghost image is drawn.
        // Even if the mouse is not moving, this method is still invoked 10 times per second
        final Point point = event.getLocation();
        if (point.equals(lastDragPoint)) {
            return;
        }
        this.lastDragPoint = point;

        final Graphics2D g2 = (Graphics2D) cTree.getGraphics();

        // If a drag image is not supported by the platform, then draw my own drag image
        if (!DragSource.isDragImageSupported()) {
            // Rub out the last ghost image and cue line
            cTree.paintImmediately(raGhost.getBounds());
            // And remember where we are about to draw the new ghost image
            final int x = point.x - cTree.getDraggedImageMouseClickPoint().x;
            final int y = point.y - cTree.getDraggedImageMouseClickPoint().y;
            raGhost.setRect(x, y, cTree.getDraggedImage().getWidth(), cTree.getDraggedImage().getHeight());
            g2.drawImage(cTree.getDraggedImage(), AffineTransform.getTranslateInstance(raGhost.getX(), raGhost.getY()), null);
        } else {
            // Just rub out the last cue line
            cTree.paintImmediately(raCueLine.getBounds());
        }

        final TreePath treePath = cTree.getClosestPathForLocation(point.x, point.y);
        if (treePath != lastTreePath) {
            lastTreePath = treePath;
            timerHover.restart();
        }

        // In any case draw (over the ghost image if necessary) a cue line indicating where a drop will occur
        final Rectangle raPath = Objects.requireNonNullElse(cTree.getPathBounds(treePath), new Rectangle(0, 0, 0, 0));

        final double y = raPath.y + raPath.getHeight();
        final Rectangle2D.Double rect = new Rectangle2D.Double(0, y, cTree.getWidth(), 2);
        raCueLine.setRect(rect);

        g2.setColor(colorCueLine);
        g2.fill(raCueLine);

        // And include the cue line in the area to be rubbed out next time
        raGhost = raGhost.createUnion(raCueLine);

        final DefaultMutableTreeNode targetNode = (DefaultMutableTreeNode) treePath.getLastPathComponent();
        final DefaultMutableTreeNode sourceNode = (DefaultMutableTreeNode) cTree.getPathSource().getLastPathComponent();

        final List<TreeNode> flattenedTreeItems = new ArrayList<>();
        getFlattenedTreeNodes((TreeNode) cTree.getModel().getRoot(), flattenedTreeItems);

        final Object targetUserObject = targetNode.getUserObject();
        final Object sourceUserObject = sourceNode.getUserObject();

        // check to see if a page is being moved
        if (sourceUserObject instanceof Page) {
            dragOverPage(event, targetNode, sourceNode, flattenedTreeItems, targetUserObject);
        } else {
            dragOverWidget(event, targetNode, sourceNode, targetUserObject);
        }
    }

    private void dragOverWidget(
            final DropTargetDragEvent event,
            final DefaultMutableTreeNode targetNode,
            final DefaultMutableTreeNode sourceNode,
            final Object targetUserObject) {
        // we must be moving a widget
        if (targetUserObject instanceof Page) {
            // we are trying to drop a widget on a page which is allowed
            event.acceptDrag(event.getDropAction());
            return;
        }

        // we are not trying to drop a widget on a page, so we need to check if it is being dropped in a group
        final TreeNode targetParent = targetNode.getParent();
        if (targetParent == null) {
            event.rejectDrag();
            return;
        }

        int targetIndex = targetParent.getIndex(targetNode);
        final int sourceIndex = sourceNode.getParent().getIndex(sourceNode);

        if (sourceIndex > targetIndex && sourceNode.getParent() == targetNode.getParent()
                && (!(targetUserObject instanceof IWidget) || ((IWidget) targetUserObject).getType() != IWidget.GROUP)) {
            targetIndex++;
        }

        final boolean rejectDrag = targetIndex == sourceIndex && sourceNode.getParent() == targetNode.getParent();
        acceptOrRejectDrag(event, rejectDrag);
    }

    private void dragOverPage(
            final DropTargetDragEvent e,
            final DefaultMutableTreeNode targetNode,
            final DefaultMutableTreeNode sourceNode,
            final List<TreeNode> flattenedTreeItems,
            final Object targetUserObject) {
        // we are moving a page
        if (targetUserObject.equals("Document Root")) {
            // we are dropping it at the document root level which means it is definitely going to be dropped at index 0
            final int targetIndex = targetNode.getIndex(targetNode);
            final int sourceIndex = targetNode.getIndex(sourceNode);

            // looks like we are trying to drop the page in the same place it already is
            final boolean shouldRejectDrag = targetIndex == sourceIndex;
            acceptOrRejectDrag(e, shouldRejectDrag);
            return;
        }

        /*
         * it looks as though we are trying to drop a page inside another page,
         * and this would not be allowed, however,
         * if the targetUserObject is the last widget in its page,
         * then we want to drop the page below this widget
         */
        if (targetUserObject instanceof IWidget) {
            final int locationInFlattenedList = flattenedTreeItems.indexOf(targetNode);

            final int nextIndex = locationInFlattenedList + 1;
            if (nextIndex < flattenedTreeItems.size()) {
                /* get the item immediately below the this widgets */
                final DefaultMutableTreeNode nextItemInList = (DefaultMutableTreeNode) flattenedTreeItems.get(nextIndex);

                // we are trying to drop this page directly above another page, so this is allowed
                final boolean dropOfAPageOverAnotherPage = nextItemInList.getUserObject() instanceof Page;
                acceptOrRejectDrag(e, !dropOfAPageOverAnotherPage);
                return;
            }
        }
        e.rejectDrag();
    }

    @Override
    public void drop(final DropTargetDropEvent event) {
        // Prevent hover timer from doing an unwanted expandPath or collapsePath
        timerHover.stop();

        if (!isDropAcceptable(event)) {
            event.rejectDrop();
            return;
        }
        event.acceptDrop(event.getDropAction());

        final Transferable transferable = event.getTransferable();
        final DataFlavor[] flavors = transferable.getTransferDataFlavors();
        Arrays.stream(flavors)
                .filter(flavor -> flavor.isMimeTypeEqual(DataFlavor.javaJVMLocalObjectMimeType))
                .map(flavor -> dropTransferable(transferable, flavor, event))
                .filter(result -> !result)
                .findAny()
                .ifPresentOrElse(
                        falseResult -> event.dropComplete(false),
                        () -> event.dropComplete(true));
    }

    private boolean dropTransferable(
            final Transferable transferable,
            final DataFlavor flavor,
            final DropTargetDropEvent event) {
        try {
            final Point pt = event.getLocation();
            final TreePath pathTarget = cTree.getClosestPathForLocation(pt.x, pt.y);
            final TreePath pathSource = (TreePath) transferable.getTransferData(flavor);

            /* clear the ghost image */
            cTree.repaint(raGhost.getBounds());

            final IMainFrame mainFrame = designer.getMainFrame();

            final DefaultMutableTreeNode sourceNode = (DefaultMutableTreeNode) pathSource.getLastPathComponent();
            final DefaultMutableTreeNode targetNode = (DefaultMutableTreeNode) pathTarget.getLastPathComponent();

            final Object sourceUserObject = sourceNode.getUserObject();
            final Object targetUserObject = targetNode.getUserObject();

            // check to see if a page is being moved
            if (sourceUserObject instanceof Page) {
                dropPage(mainFrame, sourceNode, targetNode, targetUserObject);
            } else {
                if (sourceUserObject instanceof IWidget
                        && targetUserObject instanceof IWidget) {
                    dropWidget(sourceNode, (IWidget) sourceUserObject, targetNode, (IWidget) targetUserObject);
                }
            }

            mainFrame.updateHierarchy();

            designer.repaint();
            return true;
        } catch (UnsupportedFlavorException | IOException e) {
            logger.error("Error during drag and drop", e);
            return false;
        }
    }

    private void dropWidget(
            final DefaultMutableTreeNode sourceNode,
            final IWidget sourceUserObject,
            final DefaultMutableTreeNode targetNode,
            final IWidget targetUserObject) {
        // we must be moving a widget
        final Object[] sourceObjectPath = sourceNode.getUserObjectPath();
        final Object[] targetObjectPath = targetNode.getUserObjectPath();

        final List<IWidget> sourceWidgetList = getWidgetsList(sourceObjectPath, false);
        final List<IWidget> targetWidgetList = getWidgetsList(targetObjectPath, true);

        Collections.reverse(targetWidgetList);

        final int sourceIndex = sourceWidgetList.indexOf(sourceUserObject);
        final int targetIndex = getTargetIndex(targetUserObject, sourceWidgetList, targetWidgetList, sourceIndex);

        if (targetWidgetList == sourceWidgetList) {
            dragOverWidget(sourceIndex, targetIndex, sourceWidgetList);
            return;
        }

        sourceWidgetList.remove(sourceUserObject);
        targetWidgetList.add(targetIndex, sourceUserObject);

        /*
         * if a radio button widget is moving page, then we need to see if the page it is moving
         * to already has any ButtonGroup's, and if so add it to one, but if not create a
         * new ButtonGroup on that page, and add the widget to it.
         *
         * todo do this test for check boxes too
         */
        if (sourceUserObject != null
                && sourceUserObject.getType() == IWidget.RADIO_BUTTON) {
            final Object source = sourceObjectPath[1];
            final Object target = targetObjectPath[1];
            if (source instanceof Page
                    && target instanceof Page) {
                dropRadioButtonOverPage((RadioButtonWidget) sourceUserObject, (Page) source, (Page) target);
            }
        }

        removeEmptyGroupFromParent(sourceObjectPath, sourceWidgetList, targetWidgetList);
    }

    private void removeEmptyGroupFromParent(
            final Object[] sourceObjectPath,
            final List<IWidget> sourceWidgetList,
            final List<IWidget> targetWidgetList) {
        /*
         * check to see if we are moving the last widget out of a group, if we do we need to
         * remove the empty group from its parent
         */
        final Object parent = sourceObjectPath[sourceObjectPath.length - 2];
        if (sourceWidgetList.isEmpty() && parent instanceof IWidget) {
            /*
             * the widget list is empty, and the parent of the widget moved is a group widget,
             * therefore we need to remove this group widget from its parent
             */
            final IWidget groupWidget = (IWidget) parent;
            if (groupWidget.getType() == IWidget.GROUP) {

                /* go back up the tree to find teh parent of the group widget */
                final Object parentOfGroup = sourceObjectPath[sourceObjectPath.length - 3];
                if (parentOfGroup instanceof Page) {
                    // remove the group from its parent page
                    ((Page) parentOfGroup).getWidgets().remove(groupWidget);
                } else {
                    // remove the group from its parent group widget
                    ((IWidget) parentOfGroup).getWidgetsInGroup().remove(groupWidget);
                }

                // set no widgets selected
                designer.setSelectedWidgets(Collections.emptySet());
            }
        }
        Collections.reverse(targetWidgetList);
    }

    private void dropRadioButtonOverPage(
            final RadioButtonWidget sourceUserObject,
            final Page sourcePage,
            final Page targetPage) {
        if (sourcePage != targetPage) {
            // we are dropping over a page
            final List<ButtonGroup> radioButtonGroups = targetPage.getRadioButtonGroups();
            final ButtonGroup rbg;
            if (radioButtonGroups.isEmpty()) {
                /*
                 * there are no radio button groups currently on this page, so we need to
                 * create a new on
                 */
                rbg = new ButtonGroup(IWidget.RADIO_BUTTON);
                radioButtonGroups.add(rbg);
            } else {
                /* add the radio button to the last ButtonGroup in the list */
                rbg = radioButtonGroups.get(radioButtonGroups.size() - 1);
            }

            /* set the radio buttons new group */
            sourceUserObject.setRadioButtonGroupName(rbg.getName());
        }
    }

    private int getTargetIndex(
            final IWidget targetUserObject,
            final List<IWidget> sourceWidgetList,
            final List<IWidget> targetWidgetList,
            final int sourceIndex) {
        // dropping at the start of the targetPage
        // OR dropping in the middle of other widgets
        if (targetUserObject instanceof Page
                || targetUserObject.getType() == IWidget.GROUP) {
            // dropping at start of group
            return 0;
        }

        final int targetIndex = targetWidgetList.indexOf(targetUserObject);
        if (targetWidgetList == sourceWidgetList) {
            if (targetIndex < sourceIndex) {
                return targetIndex + 1;
            }
        } else {
            return targetIndex + 1;
        }
        return targetIndex;
    }

    private void dropPage(
            final IMainFrame mainFrame,
            final DefaultMutableTreeNode sourceNode,
            final DefaultMutableTreeNode targetNode,
            final Object targetUserObject) {
        // we are moving a page
        final DefaultMutableTreeNode sourceParent = (DefaultMutableTreeNode) sourceNode.getParent();
        final int sourceIndex = sourceParent.getIndex(sourceNode);
        int targeteIndex = 0;

        if (!targetUserObject.equals("Document Root")) {
            final List<TreeNode> flattenedTreeItems = new ArrayList<>();
            getFlattenedTreeNodes((TreeNode) cTree.getModel().getRoot(), flattenedTreeItems);

            final int locationInFlattenedList = flattenedTreeItems.indexOf(targetNode);

            final int nextIndex = locationInFlattenedList + 1;
            if (nextIndex < flattenedTreeItems.size()) {
                /* get the item imediately below the this widgets */
                final DefaultMutableTreeNode nextItemInList = (DefaultMutableTreeNode) flattenedTreeItems.get(nextIndex);

                targeteIndex = nextItemInList.getParent().getIndex(nextItemInList);

                if (targeteIndex > sourceIndex) {
                    targeteIndex--;
                }
            }
        }

        mainFrame.getFormsDocument().movePage(sourceIndex, targeteIndex);

        mainFrame.setCurrentPage(targeteIndex + 1);
        mainFrame.displayPage(mainFrame.getCurrentPage());
    }

    private List<IWidget> getWidgetsList(
            final Object[] objectPath,
            final boolean isTarget) {
        for (int i = objectPath.length - 1; i >= 0; i--) {
            final Object object = objectPath[i];
            if (object instanceof Page) {
                return ((Page) object).getWidgets();
            }

            final IWidget widget = (IWidget) object;
            if (widget.getType() == IWidget.GROUP
                    && (isTarget || widget != objectPath[objectPath.length - 1])) {
                return widget.getWidgetsInGroup();
            }
        }

        return Collections.emptyList();
    }

    private void dragOverWidget(
            final int fromIndex,
            final int toIndex,
            final List<IWidget> page) {
        if (fromIndex > toIndex) {
            final IWidget objectToMove = page.get(fromIndex);
            page.add(toIndex, objectToMove);

            page.remove(page.lastIndexOf(objectToMove));
        } else {
            final int idx = toIndex + 1;
            final IWidget objectToMove = page.get(fromIndex);
            page.add(idx, objectToMove);

            page.remove(objectToMove);
        }
    }

    private void getFlattenedTreeNodes(
            final TreeNode theNode,
            final List<TreeNode> items) {
        items.add(theNode);

        final Enumeration<? extends TreeNode> children = theNode.children();
        while (children.hasMoreElements()) {
            final TreeNode treeNode = children.nextElement();
            getFlattenedTreeNodes(treeNode, items);
        }
    }

    @Override
    public void dropActionChanged(final DropTargetDragEvent e) {
        acceptOrRejectDrag(e, isDragNotAcceptable(e));
    }

    private boolean isDragNotAcceptable(final DropTargetDragEvent e) {
        // Only accept COPY or MOVE gestures (ie LINK is not supported)
        if ((e.getDropAction() & DnDConstants.ACTION_COPY_OR_MOVE) == 0) {
            return true;
        }

        // Only accept this particular flavor
        return !e.isDataFlavorSupported(CTransferableTreePath.TREEPATH_FLAVOR);

    }

    private boolean isDropAcceptable(final DropTargetDropEvent e) {
        // Only accept COPY or MOVE gestures (ie LINK is not supported)
        if ((e.getDropAction() & DnDConstants.ACTION_COPY_OR_MOVE) == 0) {
            return false;
        }

        // Only accept this particular flavor
        return e.isDataFlavorSupported(CTransferableTreePath.TREEPATH_FLAVOR);
    }

    private void acceptOrRejectDrag(
            final DropTargetDragEvent e,
            final boolean shouldRejectDrag) {
        if (shouldRejectDrag) {
            e.rejectDrag();
        } else {
            e.acceptDrag(e.getDropAction());
        }
    }
}