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

com.github.czyzby.lml.vis.ui.FixedSizeGridGroup Maven / Gradle / Ivy


package com.github.czyzby.lml.vis.ui;

import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.IntSet;
import com.badlogic.gdx.utils.Pools;
import com.github.czyzby.lml.vis.ui.reflected.MockUpActor;
import com.kotcrab.vis.ui.layout.DragPane;
import com.kotcrab.vis.ui.layout.DragPane.DefaultDragListener;
import com.kotcrab.vis.ui.layout.GridGroup;
import com.kotcrab.vis.ui.widget.Draggable;

/** A specialized {@link GridGroup} with fixed amount of children. Contains mock-up actors in "empty" cells. Created as
 * a {@link GridGroup} replacement for {@link DragPane} usage - this might be very useful to display RPG-style
 * inventories, for example. Instead of simply allowing to move the actors around the group, its drag listener swaps
 * dragged cells, allowing to fully customize actor layout.
 *
 * @author MJ
 * @see #getDraggable(FixedSizeGridGroup)
 * @see GridDragListener */
public class FixedSizeGridGroup extends GridGroup {
    private final IntSet blockedIndexes = new IntSet();
    private SwapListener swapListener;
    private int itemsAmount;

    /** @param itemsAmount amount of cells expected in the grid. All these cells will be filled with mock-up actors.
     * @param itemSize width and height of individual cells. */
    public FixedSizeGridGroup(final int itemsAmount, final int itemSize) {
        this(itemsAmount, itemSize, itemSize);
    }

    /** @param itemsAmount amount of cells expected in the grid. All these cells will be filled with mock-up actors.
     * @param itemWidth width of individual cells.
     * @param itemHeight height of individual cells. */
    public FixedSizeGridGroup(final int itemsAmount, final int itemWidth, final int itemHeight) {
        setItemWidth(itemWidth);
        setItemHeight(itemHeight);
        setSpacing(0f);
        this.itemsAmount = itemsAmount;
        fillCellsWithMockUps();
    }

    private void fillCellsWithMockUps() {
        for (int index = 0; index < itemsAmount; index++) {
            super.addActor(getMockUpActor());
        }
    }

    /** @param index child cell with this index will reject dragged actors. */
    public void setBlockedIndex(final int index) {
        blockedIndexes.add(index);
    }

    /** @param indexes child cells with this indexes will reject dragged actors. */
    public void setBlockedIndexes(final int... indexes) {
        blockedIndexes.addAll(indexes);
    }

    /** @param index a child index in the grid.
     * @return true if index cannot be filled with a non-mock-up actor. */
    public boolean isIndexBlocked(final int index) {
        return blockedIndexes.contains(index);
    }

    /** @return direct reference to internal set of blocked indexes. */
    public IntSet getBlockedIndexes() {
        return blockedIndexes;
    }

    /** @return fixed amount of grid's cells. */
    public int getItemsAmount() {
        return itemsAmount;
    }

    /** @param itemsAmount fixed amount of grid's cells. If smaller than current items amount, some cells will be
     *            removed. If higher than current items amount, new cells with mock-up actors will be added. */
    public void setItemsAmount(final int itemsAmount) {
        if (this.itemsAmount < itemsAmount) {
            for (; this.itemsAmount < itemsAmount; this.itemsAmount++) {
                super.addActor(getMockUpActor());
            }
        } else if (this.itemsAmount > itemsAmount) {
            for (; this.itemsAmount > itemsAmount; this.itemsAmount--) {
                super.removeActor(getChildren().get(this.itemsAmount - 1), true);
            }
        }
    }

    /** @param result will contain all non-mock-up actors stored in the group.
     * @return passed array with non-mock-up items. */
    public Array getItems(final Array result) {
        for (final Actor actor : getChildren()) {
            if (!isMockUp(actor)) {
                result.add(actor);
            }
        }
        return result;
    }

    /** @return true if grid stores no mock-up actors (all its cells are filled). */
    public boolean isFull() {
        final Array children = getChildren();
        for (int index = 0, size = children.size; index < size; index++) {
            if (!isIndexBlocked(index) && isMockUp(children.get(index))) {
                return false;
            }
        }
        return true;
    }

    @Override
    public void clear() {
        super.clear();
        fillCellsWithMockUps();
    }

    @Override
    public boolean removeActor(final Actor actor, final boolean unfocus) {
        final int index = getChildren().indexOf(actor, true);
        if (index >= 0) {
            super.removeActor(actor, unfocus);
            if (getChildren().size < itemsAmount) {
                super.addActorAt(index, getMockUpActor());
            }
            return true;
        }
        return false;
    }

    /** @param actor will be added to the first cell which currently contains a mock-up actor and is not blocked. Be
     *            careful: if no mock-ups are found, will replace last actor in the group.
     * @see #setBlockedIndex(int)
     * @see #isFull() */
    @Override
    public void addActor(final Actor actor) {
        super.addActor(actor); // Has to be done to set parent and stage.
        final Object[] children = getChildren().items;
        children[itemsAmount] = null; // Removing the added actor.
        getChildren().size--;
        for (int index = 0; index < itemsAmount; index++) {
            if (!isIndexBlocked(index) && isMockUp(children[index])) {
                free(children[index]);
                children[index] = actor;
                childrenChanged();
                return;
            }
        }
        // No mock-ups found - replacing last cell:
        children[itemsAmount - 1] = actor;
        childrenChanged();
    }

    @Override
    @Deprecated
    public void addActorAfter(final Actor actorAfter, final Actor actor) {
        super.addActorAfter(actorAfter, actor);
        getChildren().get(itemsAmount).remove(); // Removing last cell.
    }

    @Override
    @Deprecated
    public void addActorBefore(final Actor actorBefore, final Actor actor) {
        super.addActorBefore(actorBefore, actor);
        getChildren().get(itemsAmount).remove(); // Removing last cell.
    }

    @Override
    public void addActorAt(final int index, final Actor actor) {
        if (isIndexBlocked(index)) {
            throw new IllegalStateException("Cannot add actor to a cell with blocked index: " + index);
        }
        super.addActor(actor); // Has to be done to set parent and stage.
        final Object[] children = getChildren().items;
        children[itemsAmount] = null; // Removing the added actor.
        getChildren().size--;
        if (isMockUp(children[index])) {
            free(children[index]);
        }
        children[index] = actor;
        childrenChanged();
    }

    /** @return a new (or pooled) instance of mock-up actor. */
    protected Actor getMockUpActor() {
        return Pools.obtain(MockUpActor.class);
    }

    /** @param mockUpActor is no longer needed. If actor instances are pooled, it should be returned to the pool. */
    protected void free(final Object mockUpActor) {
        Pools.free(mockUpActor);
    }

    /** @param actor a possible mock-up actor stored in a cell.
     * @return true if passed actor is a mock-up. */
    public boolean isMockUp(final Object actor) {
        return actor instanceof MockUpActor;
    }

    /** @return listener notified of actor swaps. Can be null. */
    public SwapListener getSwapListener() {
        return swapListener;
    }

    /** @param swapListener will be notified of actor swaps. Pass null to remove. */
    public void setSwapListener(final SwapListener swapListener) {
        this.swapListener = swapListener;
    }

    /** @param group is about to be added to a {@link DragPane} and needs a {@link Draggable} for its children.
     * @return a specialized {@link Draggable} for {@link FixedSizeGridGroup}. */
    public static Draggable getDraggable(final FixedSizeGridGroup group) {
        return new Draggable(new GridDragListener(group));
    }

    /** {@link DefaultDragListener} customized for {@link FixedSizeGridGroup}. Allows to move children only to other
     * {@link DragPane}s storing instances of {@link FixedSizeGridGroup}s. Instead of simply moving the actors around
     * the group, this listener swaps children positions.
     *
     * @author MJ */
    public static class GridDragListener extends DefaultDragListener {
        private final FixedSizeGridGroup group;

        /** @param group will be managed by this listener. */
        public GridDragListener(final FixedSizeGridGroup group) {
            this.group = group;
        }

        @Override
        public boolean onStart(final Draggable draggable, final Actor actor, final float stageX, final float stageY) {
            if (group.isMockUp(actor)) {
                return CANCEL;
            }
            return super.onStart(draggable, actor, stageX, stageY);
        }

        @Override
        protected boolean addDirectlyToPane(final Draggable draggable, final Actor actor, final DragPane dragPane) {
            return CANCEL; // Prohibited. Can only swap children.
        }

        @Override
        protected boolean accept(final Actor actor, final DragPane dragPane) {
            return dragPane != null && dragPane.getGroup() instanceof FixedSizeGridGroup
                    && super.accept(actor, dragPane);
        }

        @Override
        protected boolean addToHorizontalGroup(final Actor actor, final DragPane dragPane,
                final Actor directPaneChild) {
            return CANCEL;
        }

        @Override
        protected boolean addToVerticalGroup(final Actor actor, final DragPane dragPane, final Actor directPaneChild) {
            return CANCEL;
        }

        @Override
        protected boolean addToFloatingGroup(final Draggable draggable, final Actor actor, final DragPane dragPane) {
            return CANCEL;
        }

        @Override
        protected boolean addToOtherGroup(final Actor actor, final DragPane dragPane, final Actor directPaneChild) {
            final int actorIndex = group.getChildren().indexOf(actor, true);
            int childIndex = group.getChildren().indexOf(directPaneChild, true);
            if (childIndex >= 0) { // Same group:
                if (group.isIndexBlocked(childIndex)
                        || !hasSwapListenerApproval(group, actor, group, directPaneChild)) {
                    return CANCEL;
                }
                final Object[] children = group.getChildren().items;
                children[actorIndex] = directPaneChild;
                children[childIndex] = actor;
                group.childrenChanged();
            } else { // Dragged into a different group:
                if (dragPane.getGroup() instanceof FixedSizeGridGroup) {
                    childIndex = dragPane.getGroup().getChildren().indexOf(directPaneChild, true);
                    final FixedSizeGridGroup targetGroup = (FixedSizeGridGroup) dragPane.getGroup();
                    if (targetGroup.isIndexBlocked(childIndex)
                            || !isSwapApproved(group, actor, targetGroup, directPaneChild)) {
                        return CANCEL;
                    }
                    dragPane.addActorAt(childIndex, actor);
                    group.getParent().addActorAt(actorIndex, directPaneChild); // Replaces Draggable listener.
                } else { // Not expected, dragged into a non-GridGroup:
                    return CANCEL;
                }
            }
            return APPROVE;
        }

        private static boolean isSwapApproved(final FixedSizeGridGroup fromGroup, final Actor removedActor,
                final FixedSizeGridGroup toGroup, final Actor addedActor) {
            return hasSwapListenerApproval(fromGroup, removedActor, toGroup, addedActor)
                    && hasSwapListenerApproval(toGroup, addedActor, fromGroup, removedActor);
        }

        private static boolean hasSwapListenerApproval(final FixedSizeGridGroup fromGroup, final Actor removedActor,
                final FixedSizeGridGroup toGroup, final Actor addedActor) {
            return fromGroup.getSwapListener() == null
                    || fromGroup.getSwapListener().onSwap(fromGroup, removedActor, toGroup, addedActor);
        }

        /** @param previousParent previous parent of the dragged actor. Can be null.
         * @param previousIndex previous cell index of the dragged actor. -1 if has no parent.
         * @param removedCell this actor was previously in a cell to which another actor was dragged and dropped. By
         *            default, this method will replace the previous cell of the other group with the removed actor, if
         *            the group is also a {@link FixedSizeGridGroup}. */
        protected void onNonEmptyCellReplacement(final Group previousParent, final int previousIndex,
                final Actor removedCell) {
            if (previousParent instanceof FixedSizeGridGroup) {
                previousParent.addActorAt(previousIndex, removedCell);
            }
        }
    }

    /** Allows to manage swapping events.
     *
     * @author MJ */
    public static interface SwapListener {
        /** Use in listener's method for code clarity. */
        boolean CANCEL = false, APPROVE = true;

        /** Note that if an item is moved from one group to another and both groups have listeners, both of them will be
         * invoked and both of them have to approve. If an item is moved around internally (in the same group, moved
         * from one cell to another), listener will be invoked once.
         *
         * @param fromGroup has the listener attached.
         * @param removedActor will be removed from the fromGroup and added to toGroup.
         * @param toGroup will contain the removed actor. Might be the same object as fromGroup.
         * @param addedActor will be added in place of removedActor to fromGroup. Can be a mock-up object.
         * @return true if you want to accept the swap. False if you want to cancel it.
         * @see #CANCEL
         * @see #APPROVE
         * @see FixedSizeGridGroup#isMockUp(Object) */
        boolean onSwap(FixedSizeGridGroup fromGroup, Actor removedActor, FixedSizeGridGroup toGroup, Actor addedActor);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy