package au.net.causal.projo.prefs.transform;
import java.awt.Font;
import java.awt.font.TextAttribute;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import au.net.causal.projo.prefs.DataTypeSupport;
import au.net.causal.projo.prefs.PreferenceKeyMetadata;
import au.net.causal.projo.prefs.PreferencesException;
import au.net.causal.projo.prefs.TransformDataTypeSupportChain;
import au.net.causal.projo.prefs.TransformGetChain;
import au.net.causal.projo.prefs.TransformPutChain;
import au.net.causal.projo.prefs.TransformRemoveChain;
import au.net.causal.projo.prefs.TransformResult;
import com.google.common.collect.ImmutableList;
import com.google.common.reflect.TypeToken;
/**
* Provides ability to store and load {@link Font} values from stores.
* <p>
*
* When constructing the font transformer, a set of attributes are supplied which control which attributes of the font are stored and loaded.
* For ease-of-use, the {@link #createSimpleFontTransformer()} and {@link #createStandardFontTransformer()} methods can be used for common use-cases.
* <p>
*
* Font attributes are stored under multiple keys with appropriate data types (other transformers are used where necessary, such as when the store does not
* have native support for an attribute's data type). The key name will be composed of the base name, a '.' character and the attribute name. For example,
* with a base key name of 'menuFont', the keys 'menuFont.family' and 'menuFont.size' will be used to store the actual font information.
*
* @author prunge
*/
public class FontTransformer implements PreferenceTransformer
{
private final Collection<? extends Attribute> attributes;
/**
* Creates a <code>FontTransformer</code> that stores and loads using the specified attributes.
*
* @param attributes the attributes to use.
*
* @throws NullPointerException if <code>attributes</code> is null.
*/
public FontTransformer(Collection<? extends Attribute> attributes)
{
this.attributes = ImmutableList.copyOf(attributes);
}
/**
* Creates a simple font transformer with common attributes. These attributes support fonts or varying names and sizes and support bold/italic fonts.
*
* @return the created font transformer.
*/
public static FontTransformer createSimpleFontTransformer()
{
List<Attribute> attributes = ImmutableList.of(
Attributes.FAMILY,
Attributes.SIZE,
Attributes.WEIGHT,
Attributes.POSTURE
);
return(new FontTransformer(attributes));
}
/**
* Creates a simple font transformer with typical attributes. These attributes support fonts of varying names, sizes, bold/italic fonts, as well as
* underline and strikethrough properties.
*
* @return the created font transformer.
*/
public static FontTransformer createStandardFontTransformer()
{
List<Attribute> attributes = ImmutableList.of(
Attributes.FAMILY,
Attributes.SIZE,
Attributes.WEIGHT,
Attributes.POSTURE,
Attributes.UNDERLINE,
Attributes.STRIKETHROUGH
);
return(new FontTransformer(attributes));
}
@Override
public <T> TransformResult<T> applyGet(String key, PreferenceKeyMetadata<T> keyMetadata, TransformGetChain chain)
throws PreferencesException
{
if (!isSupported(keyMetadata, chain))
return(null);
//Look up the component values
Map<TextAttribute, Object> fontAttributeMap = new HashMap<>();
for (Attribute attribute : attributes)
{
Object attributeValue = chain.getValue(key + attribute.getKeySuffix(), keyMetadata.withDataType(attribute.getStoredValueType()));
if (attributeValue != null)
fontAttributeMap.put(attribute.getTextAttribute(), attributeValue);
}
if (fontAttributeMap.isEmpty())
return(new TransformResult<>(null));
Font font = Font.getFont(fontAttributeMap);
@SuppressWarnings("unchecked")
TransformResult<T> result = new TransformResult<>((T)font);
return(result);
}
@Override
public <T> boolean applyPut(String key, T value, PreferenceKeyMetadata<T> keyMetadata, TransformPutChain chain)
throws PreferencesException
{
if (!isSupported(keyMetadata, chain))
return(false);
//Store component values
Font fValue = (Font)value;
if (fValue == null)
{
for (Attribute attribute : attributes)
{
chain.putValue(key + attribute.getKeySuffix(), null, keyMetadata.withDataType(attribute.getStoredValueType()));
}
}
else
{
for (Attribute attribute : attributes)
{
typeSafePutValue(chain, key + attribute.getKeySuffix(), attribute, attribute.getStoredValueType(), fValue, keyMetadata);
}
}
return(true);
}
private <V> void typeSafePutValue(TransformPutChain chain, String key, Attribute attribute, Class<V> storedValueType, Font font, PreferenceKeyMetadata<?> baseKeyMetadata)
throws PreferencesException
{
PreferenceKeyMetadata<V> componentKeyMetadata = baseKeyMetadata.withDataType(storedValueType);
Object oValue = font.getAttributes().get(attribute.getTextAttribute());
//Special handling - cases where font attribute might be Number but we need subclass type
V value;
if (oValue == null)
value = null;
else if (storedValueType.equals(attribute.getAllowedValueType()))
value = storedValueType.cast(oValue);
else if (attribute.getAllowedValueType().equals(Number.class))
{
Number nValue = (Number)oValue;
if (storedValueType.equals(Float.class))
value = storedValueType.cast(nValue.floatValue());
else if (storedValueType.equals(Double.class))
value = storedValueType.cast(nValue.doubleValue());
else if (storedValueType.equals(Long.class))
value = storedValueType.cast(nValue.longValue());
else if (storedValueType.equals(Integer.class))
value = storedValueType.cast(nValue.intValue());
else if (storedValueType.equals(Short.class))
value = storedValueType.cast(nValue.shortValue());
else if (storedValueType.equals(Byte.class))
value = storedValueType.cast(nValue.byteValue());
else
throw new PreferencesException("Cannot handle storedType=" + storedValueType.getCanonicalName() + ", allowedType=" + attribute.getAllowedValueType().getCanonicalName());
}
else
throw new PreferencesException("Cannot handle storedType=" + storedValueType.getCanonicalName() + ", allowedType=" + attribute.getAllowedValueType().getCanonicalName());
chain.putValue(key, value, componentKeyMetadata);
}
@Override
public <T> boolean applyRemove(String key, PreferenceKeyMetadata<T> keyMetadata, TransformRemoveChain chain)
throws PreferencesException
{
if (!isSupported(keyMetadata, chain))
return(false);
for (Attribute attribute : attributes)
{
PreferenceKeyMetadata<?> componentMetadata = keyMetadata.withDataType(attribute.getStoredValueType());
chain.removeValue(key + attribute.getKeySuffix(), componentMetadata);
}
return(true);
}
@Override
public DataTypeSupport applyDataTypeSupport(PreferenceKeyMetadata<?> keyMetadata, TransformDataTypeSupportChain chain)
throws PreferencesException
{
//Only for Font keys
if (!keyMetadata.getDataType().equals(TypeToken.of(Font.class)))
return(null);
//If underlying store supports integers then add support, otherwise we can't touch it
for (Attribute attribute : attributes)
{
if (!chain.isDataTypeSupported(keyMetadata.withDataType(attribute.getStoredValueType())))
return(null);
}
//If we get here the transform will work
return(DataTypeSupport.ADD_SUPPORT);
}
/**
* Only use this transformer if:
*
* <ul>
* <li>Store does not support {@link Font} data type natively</li>
* <li>Store supports all the attribute data types of the font</li>
* <li>Key is a Font data type</li>
* </ul>
*
* @throws PreferencesException if the underlying store fails to retrieve data type support information.
*/
private boolean isSupported(PreferenceKeyMetadata<?> keyMetadata, TransformDataTypeSupportChain chain)
throws PreferencesException
{
if (!TypeToken.of(Font.class).equals(keyMetadata.getDataType()))
return(false);
if (chain.isDataTypeSupportedNatively(keyMetadata.withDataType(Font.class)))
return(false);
for (Attribute attribute : attributes)
{
if (!chain.isDataTypeSupported(keyMetadata.withDataType(attribute.getStoredValueType())))
return(false);
}
return(true);
}
/**
* A single font attribute that wraps a {@link TextAttribute} and also defines the key suffix to apply to the base key when storing or loading in stores.
*
* @author prunge
*/
public static class Attribute
{
private final String keySuffix;
private final TextAttribute textAttribute;
private final Class<?> storedValueType;
private final Class<?> allowedValueType;
/**
* Creates an <code>Attribute</code>.
* <p>
*
* Typically the <code>storedValueType</code> and <code>allowedValueType</code> will be the same. The only exception will be when a text attribute
* declares its data type as {@link Number}. In this case, the <code>allowedValueType</code> will be Number and the <code>storedValueType</code>
* will be some subclass of number that all number values will be converted to before storing.
*
* @param keySuffix the suffix to apply to the base key when storing or loading in a store.
* @param textAttribute the text attribute to wrap.
* @param storedValueType the value type to use for storing in the preference store. Must be a sub-type of <code>allowedValueType</code>. Use wrapper
* types instead of primitive types where applicable.
* @param allowedValueType all value types that are allowed for the text attribute.
*
* @throws NullPointerException if any parameter is null.
*/
public <V> Attribute(String keySuffix, TextAttribute textAttribute, Class<? extends V> storedValueType, Class<V> allowedValueType)
{
if (keySuffix == null)
throw new NullPointerException("keySuffix == null");
if (textAttribute == null)
throw new NullPointerException("textAttribute == null");
if (storedValueType == null)
throw new NullPointerException("storedValueType == null");
if (allowedValueType == null)
throw new NullPointerException("allowedValueType == null");
this.keySuffix = keySuffix;
this.textAttribute = textAttribute;
this.storedValueType = storedValueType;
this.allowedValueType = allowedValueType;
}
/**
* @return the suffix to apply to the base key when storing values of this attribute in preference stores.
*/
public String getKeySuffix()
{
return(keySuffix);
}
/**
* @return the text attribute of the font.
*/
public TextAttribute getTextAttribute()
{
return(textAttribute);
}
/**
* @return the value type for this attribute that is being stored in the preference store.
*/
public Class<?> getStoredValueType()
{
return(storedValueType);
}
/**
* @return the value type for this attribute that may be present for attribute values coming from {@link Font} objects. Will be a supertype
* of {@linkplain #storedValueType stored value type}.
*/
public Class<?> getAllowedValueType()
{
return(allowedValueType);
}
}
/**
* Common font attributes.
*
* @author prunge
*/
public static final class Attributes
{
private static final String KEY_SEPARATOR = ".";
/**
* Font family.
*
* @see TextAttribute#FAMILY
*/
public static final Attribute FAMILY = new Attribute(KEY_SEPARATOR + "family", TextAttribute.FAMILY, String.class, String.class);
/**
* Font size in points, stored as a floating point value.
*
* @see TextAttribute#SIZE
*/
public static final Attribute SIZE = new Attribute(KEY_SEPARATOR + "size", TextAttribute.SIZE, Float.class, Number.class);
/**
* Font weight (boldness), stored as a floating point value.
*
* @see TextAttribute#WEIGHT
*/
public static final Attribute WEIGHT = new Attribute(KEY_SEPARATOR + "weight", TextAttribute.WEIGHT, Float.class, Number.class);
/**
* Font posture (italic-ness), stored as a floating point value.
*
* @see TextAttribute#POSTURE
*/
public static final Attribute POSTURE = new Attribute(KEY_SEPARATOR + "posture", TextAttribute.POSTURE, Float.class, Number.class);
/**
* Font underline style, stored as an integer.
*
* @see TextAttribute#UNDERLINE
*/
public static final Attribute UNDERLINE = new Attribute(KEY_SEPARATOR + "underline", TextAttribute.UNDERLINE, Integer.class, Integer.class);
/**
* Font striketrhough option, stored as a boolean value.
*
* @see TextAttribute#STRIKETHROUGH
*/
public static final Attribute STRIKETHROUGH = new Attribute(KEY_SEPARATOR + "strikethrough", TextAttribute.STRIKETHROUGH, Boolean.class, Boolean.class);
}
}