/*******************************************************************************
* Copyright (c) 2007, 2010 Innoopract Informationssysteme GmbH.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Innoopract Informationssysteme GmbH - initial API and implementation
* EclipseSource - ongoing development
******************************************************************************/
package org.eclipse.rwt.internal.theme;
import java.io.*;
import java.text.MessageFormat;
import java.util.*;
import org.eclipse.rwt.internal.lifecycle.HtmlResponseWriter;
import org.eclipse.rwt.internal.lifecycle.LifeCycleAdapterUtil;
import org.eclipse.rwt.internal.resources.ResourceManager;
import org.eclipse.rwt.internal.resources.ResourceManagerImpl;
import org.eclipse.rwt.internal.service.ContextProvider;
import org.eclipse.rwt.internal.service.IServiceStateInfo;
import org.eclipse.rwt.internal.theme.css.*;
import org.eclipse.rwt.resources.IResourceManager;
import org.eclipse.rwt.resources.IResourceManager.RegisterOptions;
import org.eclipse.swt.widgets.Widget;
/**
* The ThemeManager maintains information about the themeable widgets and the
* installed themes.
*/
public final class ThemeManager {
public static final String DEFAULT_THEME_ID
= "org.eclipse.rap.rwt.theme.Default";
private static final String DEFAULT_THEME_NAME = "RAP Default Theme";
private static final ResourceLoader STANDARD_RESOURCE_LOADER
= new ResourceLoader()
{
ClassLoader classLoader = getClass().getClassLoader();
public InputStream getResourceAsStream( final String resourceName )
throws IOException
{
return classLoader.getResourceAsStream( resourceName );
}
};
/** Expected character set of JS files. */
private static final String CHARSET = "UTF-8";
private static final String LOG_SYSTEM_PROPERTY
= System.getProperty( ThemeManager.class.getName() + ".log" );
private static final boolean DEBUG
= "true".equals( LOG_SYSTEM_PROPERTY );
private static final String CLIENT_LIBRARY_VARIANT
= "org.eclipse.rwt.clientLibraryVariant";
private static final String DEBUG_CLIENT_LIBRARY_VARIANT = "DEBUG";
private static final String WIDGET_THEME_PATH = "resource/widget/rap";
static final String IMAGE_DEST_PATH = "themes/images";
private static final String CURSOR_DEST_PATH = "themes/cursors";
private static final Class[] THEMEABLE_WIDGETS = new Class[]{
org.eclipse.swt.widgets.Widget.class,
org.eclipse.swt.widgets.Control.class,
org.eclipse.swt.widgets.Composite.class,
org.eclipse.swt.widgets.Button.class,
org.eclipse.swt.widgets.Combo.class,
org.eclipse.swt.widgets.CoolBar.class,
org.eclipse.swt.custom.CTabFolder.class,
org.eclipse.swt.widgets.Group.class,
org.eclipse.swt.widgets.Label.class,
org.eclipse.swt.widgets.Link.class,
org.eclipse.swt.widgets.List.class,
org.eclipse.swt.widgets.Menu.class,
org.eclipse.swt.widgets.ProgressBar.class,
org.eclipse.swt.widgets.Shell.class,
org.eclipse.swt.widgets.Spinner.class,
org.eclipse.swt.widgets.TabFolder.class,
org.eclipse.swt.widgets.Table.class,
org.eclipse.swt.widgets.Text.class,
org.eclipse.swt.widgets.ToolBar.class,
org.eclipse.swt.widgets.Tree.class,
org.eclipse.swt.widgets.Scale.class,
org.eclipse.swt.widgets.DateTime.class,
org.eclipse.swt.widgets.ExpandBar.class,
org.eclipse.swt.widgets.Sash.class,
org.eclipse.swt.widgets.Slider.class,
org.eclipse.swt.custom.CCombo.class,
org.eclipse.swt.custom.CLabel.class,
org.eclipse.swt.browser.Browser.class
};
private static ThemeManager instance;
private final Set customAppearances;
private final Map themes;
private final Set registeredThemeFiles;
private boolean initialized;
private boolean widgetsInitialized;
private Theme defaultTheme;
private ThemeableWidgetHolder themeableWidgets;
private final CssElementHolder registeredCssElements;
private ThemeManager() {
// prevent instantiation from outside
initialized = false;
widgetsInitialized = false;
themeableWidgets = new ThemeableWidgetHolder();
customAppearances = new HashSet();
registeredThemeFiles = new HashSet();
registeredCssElements = new CssElementHolder();
defaultTheme = new Theme( DEFAULT_THEME_ID, DEFAULT_THEME_NAME, null );
themes = new HashMap();
themes.put( DEFAULT_THEME_ID, defaultTheme );
}
/**
* Returns the sole instance of the ThemeManager.
*/
public static ThemeManager getInstance() {
if( instance == null ) {
instance = new ThemeManager();
}
return instance;
}
/**
* Clears the current ThemeManager instance, forcing a subsequent getInstance
* call to create a new instance.
*/
public static void resetInstance() {
instance = null;
}
/**
* Initializes the ThemeManager. Theming-relevant files are loaded for all
* themeable widgets, resources are registered. If the ThemeManager has
* already been initialized, no action is taken.
*/
public void initialize() {
if( !initialized ) {
initializeThemeableWidgets();
Collection allThemes = themes.values();
Iterator iterator = allThemes.iterator();
ThemeableWidget[] allThemeableWidgets = themeableWidgets.getAll();
while( iterator.hasNext() ) {
Theme theme = ( Theme )iterator.next();
theme.initialize( allThemeableWidgets );
}
initialized = true;
}
}
public void initializeThemeableWidgets() {
if( !widgetsInitialized ) {
addDefaultThemableWidgets();
ThemeableWidget[] widgets = themeableWidgets.getAll();
for( int i = 0; i < widgets.length; i++ ) {
processThemeableWidget( widgets[ i ] );
}
widgetsInitialized = true;
}
}
/**
* Adds a custom widget to the list of themeable widgets. Note that this
* method must be called before <code>initialize</code>.
*
* @param widget the themeable widget to add, must not be <code>null</code>
* @param loader the resource loader used to load theme resources like theme
* definitions etc. The resources to load follow a naming convention
* and must be resolved by the class loader. This argument must not
* be <code>null</code>.
* @throws IllegalStateException if the ThemeManager is already initialized
* @throws NullPointerException if a parameter is null
* @throws IllegalArgumentException if the given widget is not a subtype of
* {@link Widget}
*/
public void addThemeableWidget( final Class widget,
final ResourceLoader loader )
{
if( initialized ) {
throw new IllegalStateException( "ThemeManager is already initialized" );
}
if( widget == null ) {
throw new NullPointerException( "widget" );
}
if( loader == null ) {
throw new NullPointerException( "loader" );
}
if( !Widget.class.isAssignableFrom( widget ) ) {
String message = "Themeable widget is not a subtype of Widget: "
+ widget.getName();
throw new IllegalArgumentException( message );
}
themeableWidgets.add( new ThemeableWidget( widget, loader ) );
}
/**
* Registers a theme. Must be called before <code>initialize()</code>.
*
* @param theme the theme to register
* @throws IllegalStateException if already initialized
* @throws IllegalArgumentException if a theme with the same id is already
* registered
*/
public void registerTheme( final Theme theme ) {
if( initialized ) {
throw new IllegalStateException( "ThemeManager is already initialized" );
}
String id = theme.getId();
if( themes.containsKey( id ) ) {
String pattern = "Theme with id ''{0}'' exists already";
Object[] arguments = new Object[]{ id };
String msg = MessageFormat.format( pattern, arguments );
throw new IllegalArgumentException( msg );
}
themes.put( id, theme );
}
/**
* Determines whether a theme with the specified id has been registered.
*
* @param themeId the id to check for
* @return <code>true</code> if a theme has been registered with the given
* id
*/
public boolean hasTheme( final String themeId ) {
return themes.containsKey( themeId );
}
/**
* Returns the theme registered with the given id.
*
* @param themeId the id of the theme to retrieve
* @return the theme registered with the given id or <code>null</code> if
* there is no theme registered with this id
*/
public Theme getTheme( final String themeId ) {
Theme result = null;
if( themes.containsKey( themeId ) ) {
result = ( Theme )themes.get( themeId );
}
return result;
}
/**
* Returns a list of all registered themes.
*
* @return an array that contains the ids of all registered themes, never
* <code>null</code>
*/
public String[] getRegisteredThemeIds() {
String[] result = new String[ themes.size() ];
return ( String[] )themes.keySet().toArray( result );
}
/**
* Generates and registers JavaScript code that installs the registered themes
* on the client.
*
* @throws IllegalStateException if not initialized
*/
public void registerResources() {
checkInitialized();
Iterator iterator = themes.keySet().iterator();
// default theme must be rendered first
registerThemeFiles( defaultTheme );
while( iterator.hasNext() ) {
String key = ( String )iterator.next();
Theme theme = ( Theme )themes.get( key );
if( theme != defaultTheme ) {
registerThemeFiles( theme );
}
}
}
ThemeableWidget getThemeableWidget( final Class widget ) {
return themeableWidgets.get( widget );
}
private void checkInitialized() {
if( !initialized ) {
throw new IllegalStateException( "ThemeManager not initialized" );
}
}
private void addDefaultThemableWidgets() {
for( int i = 0; i < THEMEABLE_WIDGETS.length; i++ ) {
addThemeableWidget( THEMEABLE_WIDGETS[ i ], STANDARD_RESOURCE_LOADER );
}
}
/**
* Loads and processes all theme-relevant resources for a given widget.
*/
private void processThemeableWidget( final ThemeableWidget themeWidget )
{
String packageName = themeWidget.widget.getPackage().getName();
String[] variants = LifeCycleAdapterUtil.getPackageVariants( packageName );
String className
= LifeCycleAdapterUtil.getSimpleClassName( themeWidget.widget );
boolean found = false;
try {
for( int i = 0; i < variants.length && !found ; i++ ) {
String pkgName = variants[ i ] + "." + className.toLowerCase() + "kit";
found |= loadThemeDef( themeWidget, pkgName, className );
found |= loadAppearanceJs( themeWidget, pkgName, className );
found |= loadDefaultCss( themeWidget, pkgName, className );
}
if( themeWidget.elements == null ) {
log( "WARNING: No elements defined for themeable widget: "
+ themeWidget.widget.getName() );
}
if( themeWidget.defaultStyleSheet != null ) {
defaultTheme.addStyleSheet( themeWidget.defaultStyleSheet );
}
} catch( final IOException e ) {
String message = "Failed to initialize themeable widget "
+ themeWidget.widget.getName();
throw new ThemeManagerException( message, e );
}
}
private boolean loadThemeDef( final ThemeableWidget themeWidget,
final String pkgName,
final String className ) throws IOException
{
boolean result = false;
String resPkgName = pkgName.replace( '.', '/' );
String fileName = resPkgName + "/" + className + ".theme.xml";
InputStream inStream = themeWidget.loader.getResourceAsStream( fileName );
if( inStream != null ) {
log( "Found theme definition file: " + fileName );
result = true;
try {
ThemeDefinitionReader reader
= new ThemeDefinitionReader( inStream, fileName );
reader.read();
themeWidget.elements = reader.getThemeCssElements();
for( int i = 0; i < themeWidget.elements.length; i++ ) {
registeredCssElements.addElement( themeWidget.elements[ i ] );
}
} catch( final Exception e ) {
String message = "Failed to parse theme definition file " + fileName;
throw new ThemeManagerException( message, e );
} finally {
inStream.close();
}
}
return result;
}
private boolean loadAppearanceJs( final ThemeableWidget themeWidget,
final String pkgName,
final String className )
throws IOException
{
boolean result = false;
String resPkgName = pkgName.replace( '.', '/' );
String fileName = resPkgName + "/" + className + ".appearances.js";
InputStream inStream = themeWidget.loader.getResourceAsStream( fileName );
if( inStream != null ) {
log( "Found appearance js file: " + fileName );
try {
String content = AppearancesUtil.readAppearanceFile( inStream );
customAppearances.add( content );
result = true;
} finally {
inStream.close();
}
}
return result;
}
private boolean loadDefaultCss( final ThemeableWidget themeWidget,
final String pkgName,
final String className ) throws IOException
{
boolean result = false;
String resPkgName = pkgName.replace( '.', '/' );
String fileName = resPkgName + "/" + className + ".default.css";
ResourceLoader resLoader = themeWidget.loader;
InputStream inStream = resLoader.getResourceAsStream( fileName );
if( inStream != null ) {
log( "Found default css file: " + fileName );
try {
// TODO [rst] Check for illegal element names in selector list
themeWidget.defaultStyleSheet
= CssFileReader.readStyleSheet( inStream, fileName, resLoader );
result = true;
} finally {
inStream.close();
}
}
return result;
}
/**
* Creates and registers all JavaScript theme files and images for a given
* theme.
*/
private void registerThemeFiles( final Theme theme ) {
boolean compress = !isDebugVariant();
synchronized( registeredThemeFiles ) {
String themeId = theme.getId();
if( !registeredThemeFiles.contains( themeId ) ) {
String jsId = theme.getJsId();
registerThemeableWidgetImages( theme );
registerThemeableWidgetCursors( theme );
StringBuffer sb = new StringBuffer();
sb.append( createQxThemes( theme ) );
// TODO [rst] Optimize: create only one ThemeStoreWriter for all themes
IThemeCssElement[] elements = registeredCssElements.getAllElements();
ThemeStoreWriter storeWriter = new ThemeStoreWriter( elements );
storeWriter.addTheme( theme, theme == defaultTheme );
sb.append( storeWriter.createJs() );
String themeCode = sb.toString();
log( "-- REGISTERED THEME CODE FOR " + themeId + " ( " + themeCode.length() + " )--" );
log( themeCode );
log( "-- END REGISTERED THEME CODE --" );
String name = jsId.replace( '.', '/' ) + ".js";
registerJsLibrary( name, themeCode, compress );
registeredThemeFiles.add( themeId );
}
}
}
private void registerThemeableWidgetImages( final Theme theme ) {
QxType[] values = theme.getValuesMap().getAllValues();
for( int i = 0; i < values.length; i++ ) {
QxType value = values[ i ];
if( value instanceof QxImage ) {
QxImage image = ( QxImage )value;
if( !image.none ) {
InputStream inputStream;
try {
inputStream = image.loader.getResourceAsStream( image.path );
} catch( IOException e ) {
String message = "Failed to load resource " + image.path;
throw new ThemeManagerException( message, e );
}
if( inputStream == null ) {
String pattern = "Resource ''{0}'' not found for theme ''{1}''";
Object[] arguments = new Object[]{ image.path, theme.getName() };
String mesg = MessageFormat.format( pattern, arguments );
throw new IllegalArgumentException( mesg );
}
try {
String key = Theme.createCssKey( value );
String registerPath = IMAGE_DEST_PATH + "/" + key;
IResourceManager resourceMgr = ResourceManager.getInstance();
resourceMgr.register( registerPath, inputStream );
} finally {
try {
inputStream.close();
} catch( final IOException e ) {
throw new RuntimeException( e );
}
}
}
}
}
}
private void registerThemeableWidgetCursors( final Theme theme ) {
QxType[] values = theme.getValuesMap().getAllValues();
for( int i = 0; i < values.length; i++ ) {
QxType value = values[ i ];
if( value instanceof QxCursor ) {
QxCursor cursor = ( QxCursor )value;
if( cursor.isCustomCursor() ) {
String key = Theme.createCssKey( value );
String path = cursor.value;
log( " register theme cursor " + key + ", path=" + path );
InputStream inputStream;
try {
inputStream = cursor.loader.getResourceAsStream( path );
} catch( IOException e ) {
String message = "Failed to load resource " + path;
throw new ThemeManagerException( message, e );
}
if( inputStream == null ) {
String pattern = "Resource ''{0}'' not found for theme ''{1}''";
Object[] arguments = new Object[]{ path, theme.getName() };
String mesg = MessageFormat.format( pattern, arguments );
throw new IllegalArgumentException( mesg );
}
try {
String widgetDestPath = CURSOR_DEST_PATH;
String registerPath = widgetDestPath + "/" + key;
IResourceManager resMgr = ResourceManager.getInstance();
resMgr.register( registerPath, inputStream );
String location = resMgr.getLocation( registerPath );
log( " theme cursor registered @ " + location );
} finally {
try {
inputStream.close();
} catch( final IOException e ) {
throw new RuntimeException( e );
}
}
}
}
}
}
private static void registerJsLibrary( final String name,
final String code,
final boolean compress )
{
IResourceManager manager = ResourceManager.getInstance();
RegisterOptions option = RegisterOptions.VERSION;
if( compress ) {
option = RegisterOptions.VERSION_AND_COMPRESS;
}
if( code != null ) {
byte[] buffer;
try {
buffer = code.getBytes( CHARSET );
} catch( final UnsupportedEncodingException shouldNotHappen ) {
throw new RuntimeException( shouldNotHappen );
}
ByteArrayInputStream inputStream = new ByteArrayInputStream( buffer );
manager.register( name, inputStream, CHARSET, option );
} else {
manager.register( name, CHARSET, option );
}
IServiceStateInfo stateInfo = ContextProvider.getStateInfo();
HtmlResponseWriter responseWriter = stateInfo.getResponseWriter();
responseWriter.useJSLibrary( name );
}
private String createQxThemes( final Theme theme ) {
StringBuffer buffer = new StringBuffer();
buffer.append( createQxTheme( theme, QxTheme.COLOR ) );
buffer.append( createQxTheme( theme, QxTheme.BORDER ) );
buffer.append( createQxTheme( theme, QxTheme.FONT ) );
buffer.append( createQxTheme( theme, QxTheme.ICON ) );
buffer.append( createQxTheme( theme, QxTheme.WIDGET ) );
buffer.append( createQxTheme( theme, QxTheme.APPEARANCE ) );
buffer.append( createQxTheme( theme, QxTheme.META ) );
return buffer.toString();
}
private String createQxTheme( final Theme theme, final int type ) {
String jsId = theme.getJsId();
String base = null;
if( type == QxTheme.BORDER ) {
base = "org.eclipse.swt.theme.BordersBase";
} else if( type == QxTheme.APPEARANCE ) {
base = "org.eclipse.swt.theme.AppearancesBase";
}
QxTheme qxTheme = new QxTheme( jsId, theme.getName(), type, base );
if( type == QxTheme.WIDGET || type == QxTheme.ICON ) {
// TODO [rh] remove hard-coded resource-manager-path-prefix
String uri = ResourceManagerImpl.RESOURCES + "/" + WIDGET_THEME_PATH;
qxTheme.appendUri( uri );
} else if( type == QxTheme.APPEARANCE ) {
Iterator iterator = customAppearances.iterator();
while( iterator.hasNext() ) {
String appearance = ( String )iterator.next();
qxTheme.appendValues( appearance );
}
} else if( type == QxTheme.META ) {
qxTheme.appendTheme( "color", jsId + "Colors" );
qxTheme.appendTheme( "border", jsId + "Borders" );
qxTheme.appendTheme( "font", jsId + "Fonts" );
qxTheme.appendTheme( "icon", jsId + "Icons" );
qxTheme.appendTheme( "widget", jsId + "Widgets" );
qxTheme.appendTheme( "appearance", jsId + "Appearances" );
}
return qxTheme.getJsCode();
}
private static boolean isDebugVariant() {
String libraryVariant = System.getProperty( CLIENT_LIBRARY_VARIANT );
return DEBUG_CLIENT_LIBRARY_VARIANT.equals( libraryVariant );
}
private static void log( final String mesg ) {
if( DEBUG ) {
System.out.println( mesg );
}
}
}