// Near Infinity - An Infinity Engine Browser and Editor
// Copyright (C) 2001 - 2005 Jon Olav Hauglid
// See LICENSE.txt for license information
package org.infinity.gui.hexview;
import java.awt.Color;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.infinity.datatype.SectionCount;
import org.infinity.datatype.SectionOffset;
import org.infinity.resource.AbstractStruct;
import org.infinity.resource.StructEntry;
import org.infinity.resource.dlg.AbstractCode;
import org.infinity.util.MapEntry;
import tv.porst.jhexview.IColormap;
/**
* Defines color schemes for specific resource types to be used in JHexView components.
*/
public class BasicColorMap implements IColormap
{
/**
* Use one of the defined color values for background colors of specific structure types.
* Note: Colors are duplicated to provide coloring support for more than 7 structure types.
*/
public enum Coloring {
BLUE, GREEN, RED, CYAN, MAGENTA, YELLOW, LIGHT_GRAY, VIOLET, AQUAMARINE, PEACH, SKY, PINK, LIME,
BLUE2, GREEN2, RED2, CYAN2, MAGENTA2, YELLOW2, LIGHT_GRAY2, VIOLET2, AQUAMARINE2, PEACH2, SKY2, PINK2, LIME2,
BLUE3, GREEN3, RED3, CYAN3, MAGENTA3, YELLOW3, LIGHT_GRAY3, VIOLET3, AQUAMARINE3, PEACH3, SKY3, PINK3, LIME3,
}
// Color definitions. Each entry consists of two slightly different color tones
// that will be used alternately.
private static final EnumMap<Coloring, Color[]> colorMap = new EnumMap<Coloring, Color[]>(Coloring.class);
static {
// Populating color map
colorMap.put(Coloring.BLUE, new Color[]{new Color(0xd8d8ff), new Color(0xe8e8ff)});
colorMap.put(Coloring.GREEN, new Color[]{new Color(0xb8ffb8), new Color(0xd8ffd8)});
colorMap.put(Coloring.RED, new Color[]{new Color(0xffd0d0), new Color(0xffe8e8)});
colorMap.put(Coloring.CYAN, new Color[]{new Color(0xb8ffff), new Color(0xe0ffff)});
colorMap.put(Coloring.MAGENTA, new Color[]{new Color(0xffc8ff), new Color(0xffe0ff)});
colorMap.put(Coloring.YELLOW, new Color[]{new Color(0xffffa0), new Color(0xffffe0)});
colorMap.put(Coloring.LIGHT_GRAY, new Color[]{new Color(0xe0e0e0), new Color(0xf0f0f0)});
colorMap.put(Coloring.VIOLET, new Color[]{new Color(0xdfbfff), new Color(0xf0e1ff)});
colorMap.put(Coloring.AQUAMARINE, new Color[]{new Color(0x85ffc2), new Color(0xc2ffe1)});
colorMap.put(Coloring.PEACH, new Color[]{new Color(0xffd3a6), new Color(0xffe8d1)});
colorMap.put(Coloring.SKY, new Color[]{new Color(0x99ccff), new Color(0xbfe0ff)});
colorMap.put(Coloring.PINK, new Color[]{new Color(0xffa3d1), new Color(0xffd1e8)});
colorMap.put(Coloring.LIME, new Color[]{new Color(0xb9ff73), new Color(0xdcffb8)});
colorMap.put(Coloring.BLUE2, colorMap.get(Coloring.BLUE));
colorMap.put(Coloring.GREEN2, colorMap.get(Coloring.GREEN));
colorMap.put(Coloring.RED2, colorMap.get(Coloring.RED));
colorMap.put(Coloring.CYAN2, colorMap.get(Coloring.CYAN));
colorMap.put(Coloring.MAGENTA2, colorMap.get(Coloring.MAGENTA));
colorMap.put(Coloring.YELLOW2, colorMap.get(Coloring.YELLOW));
colorMap.put(Coloring.LIGHT_GRAY2, colorMap.get(Coloring.LIGHT_GRAY));
colorMap.put(Coloring.VIOLET2, colorMap.get(Coloring.VIOLET));
colorMap.put(Coloring.AQUAMARINE2, colorMap.get(Coloring.AQUAMARINE));
colorMap.put(Coloring.PEACH2, colorMap.get(Coloring.PEACH));
colorMap.put(Coloring.SKY2, colorMap.get(Coloring.SKY));
colorMap.put(Coloring.PINK2, colorMap.get(Coloring.PINK));
colorMap.put(Coloring.LIME2, colorMap.get(Coloring.LIME));
colorMap.put(Coloring.BLUE3, colorMap.get(Coloring.BLUE));
colorMap.put(Coloring.GREEN3, colorMap.get(Coloring.GREEN));
colorMap.put(Coloring.RED3, colorMap.get(Coloring.RED));
colorMap.put(Coloring.CYAN3, colorMap.get(Coloring.CYAN));
colorMap.put(Coloring.MAGENTA3, colorMap.get(Coloring.MAGENTA));
colorMap.put(Coloring.YELLOW3, colorMap.get(Coloring.YELLOW));
colorMap.put(Coloring.LIGHT_GRAY3, colorMap.get(Coloring.LIGHT_GRAY));
colorMap.put(Coloring.VIOLET3, colorMap.get(Coloring.VIOLET));
colorMap.put(Coloring.AQUAMARINE3, colorMap.get(Coloring.AQUAMARINE));
colorMap.put(Coloring.PEACH3, colorMap.get(Coloring.PEACH));
colorMap.put(Coloring.SKY3, colorMap.get(Coloring.SKY));
colorMap.put(Coloring.PINK3, colorMap.get(Coloring.PINK));
colorMap.put(Coloring.LIME3, colorMap.get(Coloring.LIME));
}
// Contains color definitions for specific data types.
// Works only on top-level datatypes that are preferably described by a section offset and count.
private final EnumMap<Coloring, Structure> typeMap = new EnumMap<Coloring, Structure>(Coloring.class);
private final MapEntry<Long, Color> cachedColor = new MapEntry<Long, Color>();
private final AbstractStruct struct;
private List<ColoredBlock> listBlocks;
/**
* Constructs a new color map and attempts to initialize structures automatically.
* @param struct The associated resource structure.
*/
public BasicColorMap(AbstractStruct struct)
{
this(struct, true);
}
/**
* Constructs a new color map and optionally attempts to initialize structures automatically.
* @param struct The associated resource structure.
* @param autoInit If true, attempts to initialize structures automatically.
*/
public BasicColorMap(AbstractStruct struct, boolean autoInit)
{
if (struct == null) {
throw new NullPointerException("struct is null");
}
this.struct = struct;
if (autoInit) {
autoInitColoredEntries();
}
}
//--------------------- Begin Interface IColormap ---------------------
@Override
public boolean colorize(byte value, long currentOffset)
{
return (getCachedColor(currentOffset) != null);
}
@Override
public Color getBackgroundColor(byte value, long currentOffset)
{
return getCachedColor(currentOffset);
}
@Override
public Color getForegroundColor(byte value, long currentOffset)
{
// Use component's foreground colors
return null;
}
//--------------------- End Interface IColormap ---------------------
/** Cleans up resources. */
public void close()
{
if (listBlocks != null) {
listBlocks.clear();
listBlocks = null;
}
}
/** Re-initializes data cache. */
public void reset()
{
close();
if (listBlocks == null) {
listBlocks = new ArrayList<ColoredBlock>();
}
if (!listBlocks.isEmpty()) {
listBlocks.clear();
}
List<StructEntry> flatList = getStruct().getFlatList();
for (final StructEntry curEntry: flatList) {
List<StructEntry> chain = curEntry.getStructChain();
boolean found = false;
for (int i = 0; !found && i < chain.size(); i++) {
final StructEntry e = chain.get(i);
Iterator<Map.Entry<Coloring, Structure>> iter = typeMap.entrySet().iterator();
while (!found && iter.hasNext()) {
Map.Entry<Coloring, Structure> entry = iter.next();
Structure s = entry.getValue();
if (s.getStructureClass().isInstance(e)) {
int index = s.getStructureIndex(e.getOffset());
if (index >= 0) {
ColoredBlock cb = new ColoredBlock(curEntry.getOffset(), curEntry.getSize(),
entry.getKey(), index);
listBlocks.add(cb);
if (curEntry instanceof AbstractCode) {
AbstractCode ac = (AbstractCode)curEntry;
cb = new ColoredBlock(ac.getTextOffset(), ac.getTextLength(), entry.getKey(), index);
listBlocks.add(cb);
}
found = true;
}
}
}
if (found) break;
}
chain.clear();
chain = null;
}
flatList.clear();
flatList = null;
combineColoredBlocks();
}
/**
* Attempts to find and add all top-level structures in the associated resource structure.
* Old entries in the color map will be removed.
*/
public void autoInitColoredEntries()
{
typeMap.clear();
Coloring[] colors = Coloring.values();
int colIdx = 0;
List<StructEntry> list = getStruct().getList();
Collections.sort(list);
for (final StructEntry entry: list) {
if (entry instanceof SectionOffset) {
setColoredEntry(colors[colIdx], ((SectionOffset)entry).getSection());
colIdx++;
}
if (colIdx >= colors.length) {
// no use overwriting already initialized color entries
break;
}
}
}
/**
* Removes the specified color entry from the map.
* @param color The color entry to remove.
*/
public void clearColoredEntry(Coloring color)
{
if (color != null) {
typeMap.remove(color);
}
}
/**
* Returns the class type associated with the specified color entry.
* @param color The color entry of the class.
* @return The class type associated with the specified color entry,
* or null if no entry found.
*/
public Class<? extends StructEntry> getColoredEntry(Coloring color)
{
if (color != null) {
Structure s = typeMap.get(color);
if (s != null) {
return s.getStructureClass();
}
}
return null;
}
/** Returns the associated resource structure. */
public AbstractStruct getStruct()
{
return struct;
}
/**
* Adds a new color entry to the map. Previous entries using the same color will be overwritten.
* @param color The coloring value to use.
* @param classType The class type that should be colorized.
*/
public void setColoredEntry(Coloring color, Class<? extends StructEntry> classType)
{
if (color != null && classType != null) {
typeMap.put(color, new Structure(getStruct(), classType));
}
}
// Returns the cached color for a specific offset.
private Color getCachedColor(long offset)
{
if (cachedColor.getKey() != Long.valueOf(offset)) {
cachedColor.setKey(Long.valueOf(offset));
ColoredBlock cb = findColoredBlock((int)offset);
if (cb != null) {
cachedColor.setValue(colorMap.get(cb.getColoring())[cb.getColorIndex()]);
} else {
cachedColor.setValue(Color.WHITE);
}
}
return cachedColor.getValue();
}
private boolean isCacheInitialized()
{
return (listBlocks != null);
}
// Minimizes size of block list by combining adjacent blocks into a single block
private void combineColoredBlocks()
{
if (listBlocks != null && !listBlocks.isEmpty()) {
Collections.sort(listBlocks);
int idx = 0;
while (idx < listBlocks.size()) {
ColoredBlock cb = listBlocks.get(idx);
while (idx+1 < listBlocks.size()) {
ColoredBlock cb2 = listBlocks.get(idx+1);
if (cb.getColoring() == cb2.getColoring() &&
cb.getColorIndex() == cb2.getColorIndex() &&
cb2.getOffset() <= cb.getOffset()+cb.getSize()) {
int minOfs = Math.min(cb.getOffset(), cb2.getOffset());
int maxOfs = Math.max(cb.getOffset()+cb.getSize(), cb2.getOffset()+cb2.getSize());
cb.setOffset(minOfs);
cb.setSize(maxOfs-minOfs);
listBlocks.remove(idx+1);
} else {
break;
}
}
idx++;
}
}
}
// Attempts to find and return the ColoredBlock object containing the specified offset
private ColoredBlock findColoredBlock(int offset)
{
if (!isCacheInitialized()) {
reset();
}
ColoredBlock cb = ColoredBlock.getSearchBlock(offset);
int index = Collections.binarySearch(listBlocks, cb, new Comparator<ColoredBlock>() {
@Override
public int compare(ColoredBlock obj, ColoredBlock key)
{
if (key.getOffset() < obj.getOffset()) {
return 1;
} else if (key.getOffset() >= obj.getOffset()+obj.getSize()) {
return -1;
} else {
return 0;
}
}
});
if (index >= 0 && index < listBlocks.size()) {
return listBlocks.get(index);
}
return null;
}
//-------------------------- INNER CLASSES --------------------------
private class Structure
{
// only used if isTable = true
private final List<StructEntry> structures = new ArrayList<StructEntry>();
private final Class<? extends StructEntry> classType;
private boolean isTable;
private SectionOffset so;
private SectionCount sc;
private int structureSize;
public Structure(AbstractStruct struct, Class<? extends StructEntry> classType)
{
if (struct == null || classType == null) {
throw new NullPointerException();
}
this.classType = classType;
// caches the size of the specified structure for faster index calculation
structureSize = -1;
// check if structure is defined by section offset and count fields
isTable = false;
so = null; sc = null;
for (final StructEntry entry: struct.getList()) {
if (so == null &&
entry instanceof SectionOffset &&
((SectionOffset)entry).getSection() == classType) {
so = (SectionOffset)entry;
}
if (sc == null &&
entry instanceof SectionCount &&
((SectionCount)entry).getSection() == classType) {
sc = (SectionCount)entry;
}
if (so != null && sc != null) {
break;
}
}
if (so == null || sc == null) {
// no section offset and count -> use static table lookup instead
isTable = true;
for (final StructEntry entry: struct.getList()) {
if (entry.getClass() == classType) {
structures.add(entry);
}
}
Collections.sort(structures);
}
}
/** Returns the structure type for this object. */
public Class<? extends StructEntry> getStructureClass()
{
return classType;
}
/** Returns the index of the structure located at the given offset. Index starts at 0. */
public int getStructureIndex(int offset)
{
if (isTable) {
for (int i = 0; i < structures.size(); i++) {
if (offset >= structures.get(i).getOffset() &&
offset < structures.get(i).getOffset() + structures.get(i).getSize()) {
return i;
}
}
} else if (sc.getValue() > 0) {
// structure size not yet cached?
if (structureSize < 0) {
for (final StructEntry entry: getStruct().getList()) {
if(entry.getClass() == classType) {
structureSize = entry.getSize();
break;
}
}
}
// AbstractCode instances consist of two separate data blocks
if (AbstractCode.class.isAssignableFrom(classType)) {
int curIndex = 0;
for (final StructEntry entry: getStruct().getList()) {
if (entry.getClass() == classType) {
AbstractCode ac = (AbstractCode)entry;
if ((offset >= ac.getTextOffset() && offset < ac.getTextOffset()+ac.getTextLength())) {
return curIndex;
}
curIndex++;
}
}
}
// calculating index only on valid structure size
if (structureSize > 0) {
if (offset >= so.getValue() && offset < so.getValue() + sc.getValue()*structureSize) {
int relOfs = offset - so.getValue();
if (relOfs >= 0) {
return relOfs / structureSize;
}
}
}
}
return -1;
}
}
private static class ColoredBlock implements Comparable<ColoredBlock>, Comparator<ColoredBlock>
{
private int offset, size, index;
private Coloring color;
/** Returns a dummy block that can be used as key for search operations. */
public static ColoredBlock getSearchBlock(int offset)
{
return new ColoredBlock(offset, 0, null, 0);
}
public ColoredBlock(int offset, int size, Coloring color, int index)
{
this.offset = offset;
this.size = size;
this.color = color;
this.index = index & 1;
}
public int getOffset() { return offset; }
public void setOffset(int offset) { this.offset = offset; }
public int getSize() { return size; }
public void setSize(int size) { this.size = size; }
public Coloring getColoring() { return color; }
// public void setColoring(Coloring color) { this.color = color; }
public int getColorIndex() { return index; }
// public void setColorIndex(int index) { this.index = index & 1; }
@Override
public boolean equals(Object o)
{
if (o instanceof ColoredBlock) {
return ((ColoredBlock)o).getOffset() == getOffset();
} else {
return false;
}
}
@Override
public int compare(ColoredBlock o1, ColoredBlock o2)
{
return o2.getOffset() - o1.getOffset();
}
@Override
public int compareTo(ColoredBlock o)
{
return (getOffset() - o.getOffset());
}
}
}