com.t3.model.initiative.InitiativeList.java Source code

Java tutorial

Introduction

Here is the source code for com.t3.model.initiative.InitiativeList.java

Source

/*
 * Copyright (c) 2014 tabletoptool.com team.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl.html
 * 
 * Contributors:
 *     rptools.com team - initial implementation
 *     tabletoptool.com team - further development
 */
package com.t3.model.initiative;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;

import javax.swing.Icon;

import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;

import com.t3.client.AppPreferences;
import com.t3.client.TabletopTool;
import com.t3.guid.GUID;
import com.t3.model.Token;
import com.t3.model.Zone;
import com.t3.util.guidreference.NullHelper;
import com.t3.util.guidreference.TokenReference;
import com.t3.util.guidreference.ZoneReference;
import com.t3.xstreamversioned.version.SerializationVersion;

/**
 * All of the tokens currently being shown in the initiative list. It includes a reference to all
 * the tokens in order, a reference to the current token, a displayable initiative value and a
 * hold state for each token.
 * 
 * @author Jay
 */
@SerializationVersion(1)
public class InitiativeList implements Serializable {

    /*---------------------------------------------------------------------------------------------
     * Instance Variables 
     *-------------------------------------------------------------------------------------------*/

    /**
     * The tokens and their order within the initiative
     */
    private List<TokenInitiative> tokens = new ArrayList<TokenInitiative>();

    /**
     * The token in the list which currently has initiative.
     */
    private int current = -1;

    /**
     * The current round for initiative.
     */
    private int round = -1;

    /**
     * Used to add property change support to the round and current values.
     */
    private transient PropertyChangeSupport pcs = new PropertyChangeSupport(this);

    /**
     * The zone that owns this initiative list, used for persistence
     */
    private ZoneReference zone;

    /**
     * Hold the update when this variable is greater than 0. Some methods need to call 
     * {@link #updateServer()} when they are called, but they also get called by other 
     * methods that update the server. This keeps it from happening multiple times.
     */
    private transient int holdUpdate;

    /**
     * Flag indicating that a full update is needed.
     */
    private boolean fullUpdate;

    /**
     * Hide all of the NPC's from the players.
     */
    private boolean hideNPC = AppPreferences.getInitHideNpcs();

    /*---------------------------------------------------------------------------------------------
     * Class Variables
     *-------------------------------------------------------------------------------------------*/

    /**
     * Name of the tokens property passed in {@link PropertyChangeEvent}s.
     */
    public static final String TOKENS_PROP = "tokens";

    /**
     * Name of the round property passed in {@link PropertyChangeEvent}s.
     */
    public static final String ROUND_PROP = "round";

    /**
     * Name of the current property passed in {@link PropertyChangeEvent}s.
     */
    public static final String CURRENT_PROP = "current";

    /**
     * Name of the hide NPCs property passed in {@link PropertyChangeEvent}s.
     */
    public static final String HIDE_NPCS_PROP = "hideNPCs";

    /**
     * Name of the owner permission property passed in {@link PropertyChangeEvent}s.
     */
    public static final String OWNER_PERMISSIONS_PROP = "ownerPermissions";

    /**
     * Logger for this class
     */
    private static final Logger LOGGER = Logger.getLogger(InitiativeList.class);

    /*---------------------------------------------------------------------------------------------
     * Constructor
     *-------------------------------------------------------------------------------------------*/

    /**
     * Create an initiative list for a zone.
     * 
     * @param aZone The zone that owns this initiative list.
     */
    public InitiativeList(Zone aZone) {
        setZone(aZone);
    }

    /*---------------------------------------------------------------------------------------------
     * Instance Methods 
     *-------------------------------------------------------------------------------------------*/

    /**
     * Get the token initiative data at the passed index. Allows the other state to be set.
     * 
     * @param index Index of the token initiative data needed. 
     * @return The token initiative data for the passed index.
     */
    public TokenInitiative getTokenInitiative(int index) {
        return index < tokens.size() && index >= 0 ? tokens.get(index) : null;
    }

    /**
     * Get the number of tokens in this list.
     * 
     * @return Number of tokens
     */
    public int getSize() {
        return tokens.size();
    }

    /**
     * Get the token at the passed index.
     * 
     * @param index Index of the token needed. 
     * @return The token for the passed index.
     */
    public Token getToken(int index) {
        return index >= 0 && index < tokens.size() ? tokens.get(index).getToken() : null;
    }

    /**
     * Insert a new token into the initiative.
     * 
     * @param index Insert the token here.
     * @param token Insert this token.
     * @return The token initiative value that holds the token.
     */
    public TokenInitiative insertToken(int index, Token token) {
        startUnitOfWork();
        TokenInitiative currentInitiative = getTokenInitiative(getCurrent()); // Save the currently selected initiative
        if (index == -1) {
            index = tokens.size();
        }
        TokenInitiative ti = new TokenInitiative(token);
        ti.setInitiativeList(this);
        tokens.add(index, ti);
        getPCS().fireIndexedPropertyChange(TOKENS_PROP, index, null, ti);
        setCurrent(indexOf(currentInitiative)); // Restore current initiative
        finishUnitOfWork();
        return ti;
    }

    /**
     * Insert a new token into the initiative.
     * 
     * @param tokens Insert these tokens.
     */
    public void insertTokens(List<Token> tokens) {
        startUnitOfWork();
        for (Token token : tokens)
            insertToken(-1, token);
        finishUnitOfWork();
    }

    /**
     * Find the index of the passed token.
     * 
     * @param token Search for this token.
     * @return A list of the indexes found for the listed token
     */
    public List<Integer> indexOf(Token token) {
        List<Integer> list = new ArrayList<Integer>();
        for (int i = 0; i < tokens.size(); i++)
            if (token.equals(tokens.get(i).getToken()))
                list.add(i);
        return list;
    }

    /**
     * Searches for the passed token in the list.
     * 
     * @param token Search for this token.
     * @return if this token is contained in the list
     */
    public boolean contains(Token token) {
        for (int i = 0; i < tokens.size(); i++)
            if (token.equals(tokens.get(i).getToken()))
                return true;
        return false;
    }

    /**
     * Find the index of the passed token initiative.
     * 
     * @param ti Search for this token initiative instance
     * @return The index of the token initiative that was found or -1 if the token initiative was not found;
     */
    public int indexOf(TokenInitiative ti) {
        for (int i = 0; i < tokens.size(); i++)
            if (tokens.get(i).equals(ti))
                return i;
        return -1;
    }

    /**
     * Remove a token from the initiative.
     * 
     * @param index Remove the token at this index.
     * @return The token that was removed.
     */
    public Token removeToken(int index) {

        // If we are deleting the token with initiative, drop back to the previous token, if we're at the beginning, clear current
        startUnitOfWork();
        TokenInitiative currentInitiative = getTokenInitiative(getCurrent()); // Save the currently selected initiative
        int currentInitIndex = indexOf(currentInitiative);
        if (currentInitIndex == index) {
            if (tokens.size() == 1) {
                currentInitiative = null;
            }
            if (index == 0) {
                currentInitiative = getTokenInitiative(1);
            } else {
                currentInitiative = getTokenInitiative(currentInitIndex - 1);
            } // endif
        } // endif

        TokenInitiative ti = tokens.remove(index);
        Token old = ti.getToken();
        getPCS().fireIndexedPropertyChange(TOKENS_PROP, index, ti, null);
        setCurrent(indexOf(currentInitiative)); // Restore current initiative
        finishUnitOfWork();
        return old;
    }

    /** @return Getter for current */
    public int getCurrent() {
        return current;
    }

    /** @param aCurrent Setter for the current to set */
    public void setCurrent(int aCurrent) {
        if (current == aCurrent)
            return;
        startUnitOfWork();
        if (aCurrent < 0 || aCurrent >= tokens.size())
            aCurrent = -1; // Don't allow bad values
        int old = current;
        current = aCurrent;
        getPCS().firePropertyChange(CURRENT_PROP, old, current);
        finishUnitOfWork();
    }

    /**
     * Go to the next token in initiative order.
     */
    public void nextInitiative() {
        if (tokens.isEmpty())
            return;
        startUnitOfWork();
        int newRound = (round < 0) ? 1 : (current + 1 >= tokens.size()) ? round + 1 : round;
        int newCurrent = (current < 0 || current + 1 >= tokens.size()) ? 0 : current + 1;
        setCurrent(newCurrent);
        setRound(newRound);
        finishUnitOfWork();
    }

    /**
     * Go to the previous token in initiative order.
     */
    public void prevInitiative() {
        if (tokens.isEmpty())
            return;
        startUnitOfWork();
        int newRound = (round < 2) ? 1 : (current - 1 < 0) ? round - 1 : round;
        int newCurrent = (current < 1) ? (round < 2 ? 0 : tokens.size() - 1) : current - 1;
        setCurrent(newCurrent);
        setRound(newRound);
        finishUnitOfWork();
    }

    /** @return Getter for round */
    public int getRound() {
        return round;
    }

    /** @param aRound Setter for the round to set */
    public void setRound(int aRound) {
        if (round == aRound)
            return;
        startUnitOfWork();
        int old = round;
        round = aRound;
        getPCS().firePropertyChange(ROUND_PROP, old, aRound);
        finishUnitOfWork();
    }

    /**
     * Add a listener to any property change.
     * 
     * @param listener The listener to be added.
     */
    public void addPropertyChangeListener(PropertyChangeListener listener) {
        getPCS().addPropertyChangeListener(listener);
    }

    /**
     * Add a listener to the given property name
     * 
     * @param propertyName Add the listener to this property name.
     * @param listener The listener to be added.
     */
    public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
        getPCS().addPropertyChangeListener(propertyName, listener);
    }

    /**
     * Remove a listener for all property changes.
     * 
     * @param listener The listener to be removed.
     */
    public void removePropertyChangeListener(PropertyChangeListener listener) {
        getPCS().removePropertyChangeListener(listener);
    }

    /**
     * Remove a listener from a given property name
     * 
     * @param propertyName Remove the listener from this property name.
     * @param listener The listener to be removed.
     */
    public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
        getPCS().removePropertyChangeListener(propertyName, listener);
    }

    /**
     * Start a new unit of work.
     */
    public void startUnitOfWork() {
        holdUpdate += 1;
        if (holdUpdate == 1)
            fullUpdate = false;
        LOGGER.debug("startUnitOfWork(): " + holdUpdate + " full: " + fullUpdate);
    }

    /**
     * Finish the current unit of work and update the server.
     */
    public void finishUnitOfWork() {
        fullUpdate = true;
        finishUnitOfWork(null);
    }

    /**
     * Finish the current unit of work on a single initiative item and update the server.
     * 
     * @param ti Only need to update this token initiative.
     */
    public void finishUnitOfWork(TokenInitiative ti) {
        assert holdUpdate > 0 : "Trying to close unit of work when one is not open.";
        holdUpdate -= 1;
        LOGGER.debug("finishUnitOfWork(" + (ti == null ? "" : ti.getId().toString()) + "): = " + holdUpdate
                + " full: " + fullUpdate);
        if (holdUpdate == 0) {
            if (fullUpdate || ti == null) {
                updateServer();
            } else {
                updateServer(ti);
            } // endif
        } // endif
    }

    /**
     * Remove all of the tokens from the model and clear round and current 
     */
    public void clearModel() {
        if (current == -1 && round == -1 && tokens.isEmpty())
            return;
        startUnitOfWork();
        setCurrent(-1);
        setRound(-1);
        if (!tokens.isEmpty()) {
            List<TokenInitiative> old = tokens;
            tokens = new ArrayList<TokenInitiative>();
            getPCS().firePropertyChange(TOKENS_PROP, old, tokens);
        } // endif
        finishUnitOfWork();
    }

    /**
     * Updates occurred to the tokens. 
     */
    public void update() {

        // No zone, no tokens
        if (getZone() == null) {
            clearModel();
            return;
        } // endif

        // Remove deleted tokens
        startUnitOfWork();
        boolean updateNeeded = false;
        ListIterator<TokenInitiative> i = tokens.listIterator();
        while (i.hasNext()) {
            TokenInitiative ti = i.next();
            if (!ti.tokenExists()) {
                int index = tokens.indexOf(ti);
                if (index <= current)
                    setCurrent(current - 1);
                i.remove();
                updateNeeded = true;
                getPCS().fireIndexedPropertyChange(TOKENS_PROP, index, ti, null);
            } // endif
        } // endwhile
        if (updateNeeded) {
            finishUnitOfWork();
        } else if (holdUpdate == 1) {
            holdUpdate -= 1; // Do no updates.
            LOGGER.debug("finishUnitOfWork() - no update");
        } // endif
    }

    /**
     * Sort the tokens by their initiative state from largest to smallest. If the initiative state string can be converted into a 
     * {@link Double} that is done first. All values converted to {@link Double}s are always considered bigger than the {@link String}
     * values. The {@link String} values are considered bigger than any <code>null</code> values.
     */
    public void sort() {
        startUnitOfWork();
        TokenInitiative currentInitiative = getTokenInitiative(getCurrent()); // Save the currently selected initiative
        Collections.sort(tokens);
        getPCS().firePropertyChange(TOKENS_PROP, null, tokens);
        setCurrent(indexOf(currentInitiative)); // Restore current initiative
        finishUnitOfWork();
    }

    /** @return Getter for zone */
    public Zone getZone() {
        return NullHelper.value(zone);
    }

    /** @return Getter for pcs */
    private PropertyChangeSupport getPCS() {
        if (pcs == null)
            pcs = new PropertyChangeSupport(this);
        return pcs;
    }

    /**
     * Move a token from it's current position to the new one.
     * 
     * @param oldIndex Move the token at this index
     * @param index To here.
     */
    public void moveToken(int oldIndex, int index) {

        // Bad index, same index, oldIndex->oldindex+1, or moving the last token to the end of the list do nothing.
        if (oldIndex < 0 || oldIndex == index || (oldIndex == tokens.size() - 1 && index == tokens.size())
                || oldIndex == (index - 1))
            return;

        // Save the current position, the token moves but the initiative does not.
        TokenInitiative newInitiative = null;
        TokenInitiative currentInitiative = getTokenInitiative(getCurrent()); // Save the current initiative
        if (oldIndex == current) {
            newInitiative = getTokenInitiative(oldIndex != 0 ? oldIndex - 1 : 1);
            current = (oldIndex != 0 ? oldIndex - 1 : 1);
        }

        startUnitOfWork();
        current = -1;
        TokenInitiative ti = tokens.remove(oldIndex);
        ti.setInitiativeList(this);
        getPCS().fireIndexedPropertyChange(TOKENS_PROP, oldIndex, ti, null);

        // Add it at it's new position
        index -= index > oldIndex ? 1 : 0;
        tokens.add(index, ti);
        getPCS().fireIndexedPropertyChange(TOKENS_PROP, index, null, ti);

        // Set/restore proper initiative
        if (newInitiative == null)
            current = indexOf(currentInitiative);
        else
            setCurrent(indexOf(newInitiative));
        finishUnitOfWork();
    }

    /**
     * Update the server with the new list
     */
    public void updateServer() {
        if (zone == null)
            return;
        LOGGER.debug("Full update");
        TabletopTool.serverCommand().updateInitiative(this, null);
    }

    /**
     * Update the server with the new Token Initiative
     * 
     * @param ti Item to update
     */
    public void updateServer(TokenInitiative ti) {
        if (zone == null)
            return;
        LOGGER.debug("Token Init update: " + ti.getId());
        TabletopTool.serverCommand().updateTokenInitiative(zone.getId(), ti.getId(), ti.isHolding(),
                ti.getRawState(), indexOf(ti));
    }

    /** @param aZone Setter for the zone */
    public void setZone(Zone aZone) {
        zone = NullHelper.referenceZone(aZone);
    }

    /** @return Getter for hideNPC */
    public boolean isHideNPC() {
        return hideNPC;
    }

    /** @param hide Setter for hideNPC */
    public void setHideNPC(boolean hide) {
        if (hide == hideNPC)
            return;
        startUnitOfWork();
        boolean old = hideNPC;
        hideNPC = hide;
        getPCS().firePropertyChange(HIDE_NPCS_PROP, old, hide);
        finishUnitOfWork();
    }

    /** @return Getter for tokens */
    public List<TokenInitiative> getTokens() {
        return Collections.unmodifiableList(tokens);
    }

    /*---------------------------------------------------------------------------------------------
     * TokenInitiative Inner Class
     *-------------------------------------------------------------------------------------------*/

    /**
     * This class holds all of the data to describe a token w/in initiative.
     * 
     * @author Jay
     */
    @SerializationVersion(1)
    public static class TokenInitiative implements Comparable<TokenInitiative> {

        /*---------------------------------------------------------------------------------------------
         * Instance Variables 
         *-------------------------------------------------------------------------------------------*/

        /**
         * The token which is needed for persistence. It is immutable.
         */
        private TokenReference token;

        /**
         * Flag indicating that the token is holding it's initiative.
         */
        private boolean holding;

        /**
         * Optional state that can be displayed in the initiative panel. 
         */
        private InitiativeValue state;

        /**
         * Save off the icon so that it can be displayed as needed.
         */
        private transient Icon displayIcon;

        private InitiativeList initiativeList;

        /*---------------------------------------------------------------------------------------------
         * Constructors
         *-------------------------------------------------------------------------------------------*/

        /**
         * Create the token initiative for the passed token.
         * 
         * @param aToken Add this token to the initiative.
         */
        public TokenInitiative(Token aToken) {
            token = NullHelper.referenceToken(aToken);
        }

        /*---------------------------------------------------------------------------------------------
         * Instance Methods 
         *-------------------------------------------------------------------------------------------*/

        public boolean tokenExists() {
            return token.isValid();
        }

        /** @return Getter for token */
        public Token getToken() {
            return NullHelper.value(token);
        }

        /** @return Getter for id */
        public GUID getId() {
            return NullHelper.getId(token);
        }

        /** @return Getter for holding */
        public boolean isHolding() {
            return holding;
        }

        /** @param isHolding Setter for the holding to set */
        public void setHolding(boolean isHolding) {
            if (holding == isHolding)
                return;
            initiativeList.startUnitOfWork();
            boolean old = holding;
            holding = isHolding;
            initiativeList.getPCS().fireIndexedPropertyChange(TOKENS_PROP, initiativeList.tokens.indexOf(this), old,
                    isHolding);
            initiativeList.finishUnitOfWork(this);
        }

        /** @return Getter for state */
        public Object getState() {
            if (state == null)
                return null;
            else
                return state.getValue();
        }

        /** @return Getter for state */
        public InitiativeValue getRawState() {
            return state;
        }

        public void setState(String state) {
            this.setState(InitiativeValue.create(state));
        }

        public void setState(Number state) {
            this.setState(InitiativeValue.create(state));
        }

        /** This method accepts a string as the new initiative value but it will try to convert it into a number first*/
        public void setUnparsedState(String state) {
            try {
                this.setState(Integer.valueOf(state));
            } catch (NumberFormatException e) {
                try {
                    this.setState(Double.valueOf(state));
                } catch (NumberFormatException e2) {
                    if (StringUtils.isBlank(state))
                        this.setState((InitiativeValue) null);
                    else
                        this.setState(state);
                }
            }
        }

        /** @param aState Setter for the state to set */
        public void setState(InitiativeValue aState) {
            if (state == aState || (state != null && state.equals(aState)))
                return;
            initiativeList.startUnitOfWork();
            Object old = state;
            state = aState;
            initiativeList.getPCS().fireIndexedPropertyChange(TOKENS_PROP, initiativeList.tokens.indexOf(this), old,
                    aState);
            initiativeList.finishUnitOfWork(this);
        }

        /** @return Getter for displayIcon */
        public Icon getDisplayIcon() {
            return displayIcon;
        }

        /** @param displayIcon Setter for the displayIcon to set */
        public void setDisplayIcon(Icon displayIcon) {
            this.displayIcon = displayIcon;
        }

        public void update(boolean isHolding, String aState) {
            this.update(isHolding, InitiativeValue.create(aState));
        }

        public void update(boolean isHolding, Number aState) {
            this.update(isHolding, InitiativeValue.create(aState));
        }

        /**
         * Update the internal state w/o firing events. Needed for single token 
         * init updates. 
         * 
         * @param isHolding New holding state
         * @param aState New state
         */
        public void update(boolean isHolding, InitiativeValue aState) {
            boolean old = holding;
            holding = isHolding;
            Object oldState = state;
            state = aState;
            initiativeList.getPCS().fireIndexedPropertyChange(TOKENS_PROP, initiativeList.tokens.indexOf(this), old,
                    isHolding);
            initiativeList.getPCS().fireIndexedPropertyChange(TOKENS_PROP, initiativeList.tokens.indexOf(this),
                    oldState, aState);
        }

        public void setInitiativeList(InitiativeList initiativeList) {
            this.initiativeList = initiativeList;
        }

        @Override
        public int compareTo(TokenInitiative o) {
            if (o == null)
                return 1;
            else
                return state.compareTo(o.state);
        }
    }
}