blob: 7ffaf1b18541f5fe5a153c35f4a39c05989a5da9 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pivot.ui;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import org.apache.pivot.beans.BeanAdapter;
import org.apache.pivot.beans.PropertyNotFoundException;
import org.apache.pivot.io.SerializationException;
import org.apache.pivot.json.JSONSerializer;
import org.apache.pivot.scene.Group;
import org.apache.pivot.scene.Node;
import org.apache.pivot.util.ObservableMap;
import org.apache.pivot.util.ObservableMapAdapter;
/**
* Abstract base class for components.
*/
public abstract class Component extends Group {
// The currently installed skin and associated style adapter
private Skin skin = null;
private BeanAdapter styles = null;
// Tooltip content and delay
private Node tooltipContent = null;
private int tooltipDelay = 1000;
// User data
private ObservableMap<String, Object> userData = ObservableMapAdapter.observableHashMap();
// The component's automation ID
private String automationID;
// TODO Event listener lists
// Typed and named styles
private static HashMap<Class<? extends Component>, Map<String, ?>> typedStyles =
new HashMap<Class<? extends Component>, Map<String,?>>();
private static HashMap<String, Map<String, ?>> namedStyles = new HashMap<String, Map<String,?>>();
/**
* Returns the currently installed skin.
*
* @return
* The currently installed skin.
*/
protected Skin getSkin() {
return skin;
}
/**
* Sets the skin, replacing any previous skin.
*
* @param skin
* The new skin.
*/
protected void setSkin(Skin skin) {
if (skin == null) {
throw new IllegalArgumentException("skin is null.");
}
if (this.skin != null) {
throw new IllegalStateException("Skin is already installed.");
}
this.skin = skin;
// TODO Use a custom inner class that ignores missing property exceptions
styles = new BeanAdapter(skin);
skin.install(this);
// Apply any defined type styles
LinkedList<Class<?>> styleTypes = new LinkedList<Class<?>>();
Class<?> type = getClass();
while (type != Object.class) {
styleTypes.add(0, type);
type = type.getSuperclass();
}
for (Class<?> styleType : styleTypes) {
Map<String, ?> styles = typedStyles.get(styleType);
if (styles != null) {
setStyles(styles);
}
}
invalidate();
repaint();
}
/**
* Installs the skin for the given component class, as defined by the current
* theme.
*
* @param componentClass
*/
@SuppressWarnings("unchecked")
protected void installSkin(Class<? extends Component> componentClass) {
// Walk up component hierarchy from this type; if we find a match
// and the super class equals the given component class, install
// the skin. Otherwise, ignore - it will be installed later by a
// subclass of the component class.
Class<?> type = getClass();
Theme theme = Theme.getTheme();
Class<? extends Skin> skinClass =
theme.getSkinClass((Class<? extends Component>)type);
while (skinClass == null
&& type != componentClass
&& type != Component.class) {
type = type.getSuperclass();
if (type != Component.class) {
skinClass = theme.getSkinClass((Class<? extends Component>)type);
}
}
if (type == Component.class) {
throw new IllegalArgumentException(componentClass.getName()
+ " is not an ancestor of " + getClass().getName());
}
if (skinClass == null) {
throw new IllegalArgumentException("No skin mapping for "
+ componentClass.getName() + " found.");
}
if (type == componentClass) {
try {
setSkin(skinClass.newInstance());
} catch(InstantiationException exception) {
throw new IllegalArgumentException(exception);
} catch(IllegalAccessException exception) {
throw new IllegalArgumentException(exception);
}
}
}
/**
* Returns the component's style map. Style names correspond to the Java
* bean properties of the skin.
* <p>
* An attempt to set a non-existent style will produce a warning. This
* allows an application to be dynamically themed without risk of failure
* due to a {@link PropertyNotFoundException}.
*/
public ObservableMap<String, Object> getStyles() {
return styles;
}
/**
* Applies a set of styles.
*
* @param styles
* A map containing the styles to apply.
*/
public void setStyles(Map<String, ?> styles) {
if (styles == null) {
throw new IllegalArgumentException("styles is null.");
}
for (Map.Entry<String, ?> entry : styles.entrySet()) {
getStyles().put(entry.getKey(), entry.getValue());
}
}
/**
* Applies a set of named styles.
*
* @param styleName
*/
public void setStyleName(String styleName) {
if (styleName == null) {
throw new IllegalArgumentException();
}
Map<String, ?> styles = namedStyles.get(styleName);
if (styles == null) {
System.err.println("Named style \"" + styleName + "\" does not exist.");
} else {
setStyles(styles);
}
}
/**
* Applies a set of named styles.
*
* @param styleNames
*/
public void setStyleNames(String styleNames) {
if (styleNames == null) {
throw new IllegalArgumentException();
}
String[] styleNameArray = styleNames.split(",");
for (int i = 0; i < styleNameArray.length; i++) {
String styleName = styleNameArray[i];
setStyleName(styleName.trim());
}
}
/**
* Returns the component's automation ID.
*
* @return
* The component's automation ID, or <tt>null</tt> if the component does not
* have an automation ID.
*/
public String getAutomationID() {
return automationID;
}
/**
* Sets the component's automation ID. This value can be used to obtain a
* reference to the component via {@link Automation#get(String)} when the
* component is attached to a component hierarchy.
*
* @param automationID
* The automation ID to use for the component, or <tt>null</tt> to clear the
* automation ID.
*/
public void setAutomationID(String automationID) {
String previousAutomationID = this.automationID;
this.automationID = automationID;
if (getStage() != null) {
if (previousAutomationID != null) {
Automation.remove(previousAutomationID);
}
if (automationID != null) {
Automation.add(automationID, this);
}
}
}
public Window getWindow() {
return (Window)getAncestor(Window.class);
}
/**
* Returns the component's tooltip content.
*
* @return
* The component's tooltip content, or <tt>null</tt> if no tooltip is
* specified.
*/
public Node getTooltipContent() {
return tooltipContent;
}
/**
* Sets the component's tooltip content.
*
* @param tooltipContents
* The component's tooltip content, or <tt>null</tt> for no tooltip.
*/
public void setTooltipContent(Node tooltipContent) {
Node previousTooltipContent = this.tooltipContent;
if (previousTooltipContent != tooltipContent) {
this.tooltipContent = tooltipContent;
// TODO Fire event
}
}
/**
* Returns the component's tooltip delay.
*
* @return
* The tooltip delay, in milliseconds.
*/
public int getTooltipDelay() {
return tooltipDelay;
}
/**
* Sets the component's tooltip delay.
*
* @param tooltipDelay
* The tooltip delay, in milliseconds.
*/
public void setTooltipDelay(int tooltipDelay) {
int previousTooltipDelay = this.tooltipDelay;
if (previousTooltipDelay != tooltipDelay) {
this.tooltipDelay = tooltipDelay;
// TODO Fire event
}
}
public ObservableMap<String, Object> getUserData() {
return userData;
}
/**
* Copies bound values from the bind context to the component. This
* functionality must be provided by the subclass; the base implementation
* is a no-op.
*
* @param context
*/
public void load(Object context) {
// TODO IMPORTANT If we don't have a Container class, we'll need to implement
// load() and store() in a lot more places
}
/**
* Copies bound values from the component to the bind context. This
* functionality must be provided by the subclass; the base implementation
* is a no-op.
*
* @param context
*/
public void store(Object context) {
}
/**
* Clears any bound values in the component.
*/
public void clear() {
}
@Override
public String toString() {
String s = super.toString();
if (automationID != null) {
s += "#" + automationID;
}
return s;
}
/**
* Returns the typed style dictionary.
*/
public static Map<Class<? extends Component>, Map<String, ?>> getTypedStyles() {
return typedStyles;
}
/**
* Returns the named style dictionary.
*/
public static Map<String, Map<String, ?>> getNamedStyles() {
return namedStyles;
}
/**
* Adds the styles from a named stylesheet to the named or typed style collections.
*
* @param resourceName
*/
@SuppressWarnings("unchecked")
public static void appendStylesheet(String resourceName) {
// TODO Use .properties files with JSON map values rather than .json files
ClassLoader classLoader = Component.class.getClassLoader();
URL stylesheetLocation = classLoader.getResource(resourceName.substring(1));
if (stylesheetLocation == null) {
throw new RuntimeException("Unable to locate style sheet resource \"" + resourceName + "\".");
}
try {
InputStream inputStream = stylesheetLocation.openStream();
try {
JSONSerializer serializer = new JSONSerializer();
Map<String, Map<String, ?>> stylesheet =
(Map<String, Map<String, ?>>)serializer.readObject(inputStream);
for (Map.Entry<String, Map<String, ?>> entry : stylesheet.entrySet()) {
String name = entry.getKey();
Map<String, ?> styles = entry.getValue();
int i = name.lastIndexOf('.') + 1;
if (Character.isUpperCase(name.charAt(i))) {
// Assume the current package if none specified
if (!name.contains(".")) {
name = Component.class.getPackage().getName() + "." + name;
}
Class<?> type = null;
try {
type = Class.forName(name);
} catch (ClassNotFoundException exception) {
// No-op
}
if (type != null
&& Component.class.isAssignableFrom(type)) {
Component.getTypedStyles().put((Class<? extends Component>)type, styles);
}
} else {
Component.getNamedStyles().put(name, styles);
}
}
} finally {
inputStream.close();
}
} catch (IOException exception) {
throw new RuntimeException(exception);
} catch (SerializationException exception) {
throw new RuntimeException(exception);
}
}
}