/*
* 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.client.ui.tokenpanel;
import java.awt.EventQueue;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import javax.swing.JTree;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import com.t3.client.AppUtil;
import com.t3.client.TabletopTool;
import com.t3.client.ui.zone.ZoneRenderer;
import com.t3.language.I18N;
import com.t3.model.ModelChangeEvent;
import com.t3.model.ModelChangeListener;
import com.t3.model.Token;
import com.t3.model.Zone;
import com.t3.networking.ServerPolicy;
public class TokenPanelTreeModel implements TreeModel, ModelChangeListener {
private static final String _TOKENS = "panel.MapExplorer.View.TOKENS";
private static final String _PLAYERS = "panel.MapExplorer.View.PLAYERS";
private static final String _GROUPS = "panel.MapExplorer.View.GROUPS";
private static final String _GM = "panel.MapExplorer.View.GM";
private static final String _OBJECTS = "panel.MapExplorer.View.OBJECTS";
private static final String _BACKGROUND = "panel.MapExplorer.View.BACKGROUND";
private static final String _CLIPBOARD = "panel.MapExplorer.View.CLIPBOARD";
private static final String _LIGHT_SOURCES = "panel.MapExplorer.View.LIGHT_SOURCES";
public enum View {
// @formatter:off
// I18N key Zone.Layer Req'd? isAdmin?
TOKENS(_TOKENS, Zone.Layer.TOKEN, false, false),
PLAYERS(_PLAYERS, Zone.Layer.TOKEN, false, false),
GROUPS(_GROUPS, Zone.Layer.TOKEN, false, false),
GM(_GM, Zone.Layer.GM, false, true),
OBJECTS(_OBJECTS, Zone.Layer.OBJECT, false, true),
BACKGROUND(_BACKGROUND, Zone.Layer.BACKGROUND, false, true),
CLIPBOARD(_CLIPBOARD, Zone.Layer.TOKEN, false, true),
LIGHT_SOURCES(_LIGHT_SOURCES, null, false, false);
// @formatter:on
String displayName;
String description;
boolean required;
Zone.Layer layer;
boolean isAdmin;
private View(String key, Zone.Layer layer, boolean required, boolean isAdmin) {
this.displayName = I18N.getText(key);
this.description = null; // I18N.getDescription(key); // TODO Tooltip -- not currently used
this.required = required;
this.layer = layer;
this.isAdmin = isAdmin;
}
public String getDisplayName() {
return displayName;
}
public String getDescription() {
return description;
}
public Zone.Layer getLayer() {
return layer;
}
public boolean isRequired() {
return required;
}
}
private final List<TokenFilter> filterList = new ArrayList<TokenFilter>();
private final String root = "Views";
private Zone zone;
private final JTree tree;
private volatile boolean updatePending = false;
public TokenPanelTreeModel(JTree tree) {
this.tree = tree;
update();
// It would be useful to have this list be static, but it's really not that big of a memory footprint
// TODO: refactor to more tightly couple the View enum and the corresponding filter
filterList.add(new TokenTokenFilter());
filterList.add(new PlayerTokenFilter());
filterList.add(new GMFilter());
filterList.add(new ObjectFilter());
filterList.add(new BackgroundFilter());
filterList.add(new LightSourceFilter());
}
private final List<TreeModelListener> listenerList = new ArrayList<TreeModelListener>();
private final Map<View, List<Token>> viewMap = new HashMap<View, List<Token>>();
private final List<View> currentViewList = new ArrayList<View>();
@Override
public Object getRoot() {
return root;
}
public void setZone(Zone zone) {
if (zone != null) {
zone.removeModelChangeListener(this);
}
this.zone = zone;
update();
if (zone != null) {
zone.addModelChangeListener(this);
}
}
@Override
public Object getChild(Object parent, int index) {
if (parent == root) {
return currentViewList.get(index);
}
if (parent instanceof View) {
return getViewList((View) parent).get(index);
}
return null;
}
@Override
public int getChildCount(Object parent) {
if (parent == root) {
return currentViewList.size();
}
if (parent instanceof View) {
return getViewList((View) parent).size();
}
return 0;
}
private List<Token> getViewList(View view) {
List<Token> list = viewMap.get(view);
if (list == null) {
return Collections.emptyList();
}
return list;
}
@Override
public boolean isLeaf(Object node) {
return node instanceof Token;
}
@Override
public void valueForPathChanged(TreePath path, Object newValue) {
// Nothing to do
}
@Override
public int getIndexOfChild(Object parent, Object child) {
if (parent == root) {
return currentViewList.indexOf(child);
}
if (parent instanceof View) {
getViewList((View) parent).indexOf(child);
}
return -1;
}
@Override
public void addTreeModelListener(TreeModelListener l) {
listenerList.add(l);
}
@Override
public void removeTreeModelListener(TreeModelListener l) {
listenerList.remove(l);
}
public void update() {
// better solution would be to use a timeout to invoke the internal update to give more
// token events the chance to arrive, but in this case EventQueue overload will
// manage to delay it quite nicely
if (!updatePending) {
updatePending = true;
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
updatePending = false;
updateInternal();
}
});
}
}
private void updateInternal() {
currentViewList.clear();
viewMap.clear();
// Plan to show all of the views in order to keep the order
for (TokenFilter filter : filterList) {
if (!filter.view.isAdmin || TabletopTool.getPlayer().isGM())
currentViewList.add(filter.view);
}
// Add in the appropriate views
List<Token> tokenList = new ArrayList<Token>();
if (zone != null) {
tokenList = zone.getAllTokens();
}
for (Token token : tokenList) {
for (TokenFilter filter : filterList) {
filter.filter(token);
}
}
// Clear out any view without any tokens
for (ListIterator<View> viewIter = currentViewList.listIterator(); viewIter.hasNext();) {
View view = viewIter.next();
if (!view.isRequired() && (viewMap.get(view) == null || viewMap.get(view).size() == 0)) {
viewIter.remove();
}
}
// Sort
for (List<Token> tokens : viewMap.values()) {
Collections.sort(tokens, NAME_AND_STATE_COMPARATOR);
}
// Keep the expanded branches consistent
Enumeration<TreePath> expandedPaths = tree.getExpandedDescendants(new TreePath(root));
// @formatter:off
fireStructureChangedEvent(
new TreeModelEvent(this,
new Object[] { getRoot() },
new int[] { currentViewList.size() - 1 },
new Object[] { View.TOKENS }
)
);
// @formatter:on
while (expandedPaths != null && expandedPaths.hasMoreElements()) {
tree.expandPath(expandedPaths.nextElement());
}
}
private void fireStructureChangedEvent(TreeModelEvent e) {
TreeModelListener[] listeners = listenerList.toArray(new TreeModelListener[listenerList.size()]);
for (TreeModelListener listener : listeners) {
listener.treeStructureChanged(e);
}
}
private void fireNodesInsertedEvent(TreeModelEvent e) {
TreeModelListener[] listeners = listenerList.toArray(new TreeModelListener[listenerList.size()]);
for (TreeModelListener listener : listeners) {
listener.treeNodesInserted(e);
}
}
private abstract class TokenFilter {
private final View view;
public TokenFilter(View view) {
this.view = view;
}
private void filter(Token token) {
if (accept(token)) {
List<Token> tokenList = viewMap.get(view);
if (tokenList == null) {
tokenList = new ArrayList<Token>();
viewMap.put(view, tokenList);
}
tokenList.add(token);
}
}
protected abstract boolean accept(Token token);
}
/**
* Accepts only tokens that are PCs and are owned by the current player (takes useStrictTokenManagement() into
* account).
*/
private class PlayerTokenFilter extends TokenFilter {
public PlayerTokenFilter() {
super(View.PLAYERS);
}
@Override
protected boolean accept(Token token) {
if (token.getType() != Token.Type.PC)
return false;
if (TabletopTool.getServerPolicy().isUseIndividualViews()) {
if (AppUtil.playerOwns(token)) {
return true;
} else
return false;
}
return token.isVisible();
}
}
/**
* Accepts only tokens on the Object layer and only for the GM.
*/
private class ObjectFilter extends TokenFilter {
/**
* Accepts only tokens on the Object layer and only for the GM.
*/
public ObjectFilter() {
super(View.OBJECTS);
}
@Override
protected boolean accept(Token token) {
return TabletopTool.getPlayer().isGM() && token.isObjectStamp();
}
}
/**
* Accepts only tokens on the GM (aka Hidden) layer and only for a GM.
*/
private class GMFilter extends TokenFilter {
/**
* Accepts only tokens on the GM (aka Hidden) layer and only for a GM.
*/
public GMFilter() {
super(View.GM);
}
@Override
protected boolean accept(Token token) {
return TabletopTool.getPlayer().isGM() && token.isGMStamp();
}
}
/**
* Accepts only tokens on the Background layer and only for a GM.
*/
private class BackgroundFilter extends TokenFilter {
/**
* Accepts only tokens on the Background layer and only for a GM.
*/
public BackgroundFilter() {
super(View.BACKGROUND);
}
@Override
protected boolean accept(Token token) {
return TabletopTool.getPlayer().isGM() && token.isBackgroundStamp();
}
}
/**
* Accepts only tokens with an attached light source and only when owned by the user.
*/
private class LightSourceFilter extends TokenFilter {
/**
* Accepts only tokens with an attached light source and only when owned by the user.
*/
public LightSourceFilter() {
super(View.LIGHT_SOURCES);
}
@Override
protected boolean accept(Token token) {
return token.getLightSources().isEmpty() ? false : AppUtil.playerOwns(token);
}
}
/**
* Accepts only NPC tokens (for GM) or tokens owned by the current player (takes
* {@link ServerPolicy#useStrictTokenManagement()} into account). Here's the selection process:
* <ol>
* <li>If the token is not on the Token layer, return <code>false</code>.
* <li>If the token has type PC, return <code>false</code>.
* <li>If the current player is the GM, return <code>true</code>.
* <li>If the token is owned by the current player, return <code>true</code>. (Takes into account
* StrictTokenManagement and the AllPlayers ownership flag).
* <li>If the token is visible only to the owner, return <code>false</code>. (It's already been determined that
* we're not an owner.)
* <li>Otherwise, return true.
* </ol>
*/
private class TokenTokenFilter extends TokenFilter {
public TokenTokenFilter() {
super(View.TOKENS);
}
@Override
protected boolean accept(Token token) {
ZoneRenderer renderer = TabletopTool.getFrame().getCurrentZoneRenderer();
if (renderer == null) {
return false;
}
if (token.isStamp() || token.getType() == Token.Type.PC) {
return false;
}
if (TabletopTool.getPlayer().isGM())
return true;
if (AppUtil.playerOwns(token)) { // returns true if useStrictTokenManagement()==false
return true;
}
// if (token.isVisibleOnlyToOwner())
// return false;
return false;
}
}
////
// MODEL CHANGE LISTENER
@Override
public void modelChanged(ModelChangeEvent event) {
update();
}
////
// SORTING
private static final Comparator<Token> NAME_AND_STATE_COMPARATOR = new Comparator<Token>() {
@Override
public int compare(Token o1, Token o2) {
if (o1.isVisible() != o2.isVisible()) {
return o1.isVisible() ? -1 : 1;
}
return o1.getName().compareToIgnoreCase(o2.getName());
}
};
}