/*******************************************************************************
* Copyright 2013 Geoscience Australia
*
* Licensed 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 au.gov.ga.earthsci.application.theme;
import java.net.URI;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.e4.core.di.annotations.Creatable;
import org.eclipse.e4.core.di.annotations.Optional;
import org.eclipse.e4.ui.css.swt.theme.IThemeEngine;
import org.eclipse.e4.ui.model.application.MApplication;
import org.eclipse.e4.ui.model.application.descriptor.basic.MPartDescriptor;
import org.eclipse.e4.ui.model.application.ui.MUIElement;
import org.eclipse.e4.ui.model.application.ui.MUILabel;
import org.eclipse.e4.ui.model.application.ui.basic.MPart;
import org.eclipse.e4.ui.model.application.ui.menu.MMenu;
import org.eclipse.e4.ui.workbench.modeling.EModelService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import au.gov.ga.earthsci.application.Activator;
import au.gov.ga.earthsci.worldwind.common.util.Util;
/**
* Sets the theme on the current theme engine to that specified in the
* {@value #DEFAULT_THEME_EXTENSION_POINT_ID} extension, and loads icon
* overrides specified in the {@value #ICON_PROVIDER_EXTENSION_POINT_ID}
* extension.
* <p/>
* Allows plugins to provide a complete theme replacement for the application
* without the need for user-controlled switching. If no contributions are found
* in the extension points, the default platform look-and-feel is used.
* <p/>
* Uses extension points:
* <ul>
* <li>{@value #ICON_PROVIDER_EXTENSION_POINT_ID}
* <li>{@value #DEFAULT_THEME_EXTENSION_POINT_ID}
* </ul>
*
* @author James Navin (james.navin@ga.gov.au)
*/
@Creatable
@Singleton
public class ThemeLoader
{
public static final String DEFAULT_ICONS_PROPERTIES = "icons/icons.properties"; //$NON-NLS-1$
public static final String DEFAULT_THEME_EXTENSION_POINT_ID = "au.gov.ga.earthsci.application.defaultTheme"; //$NON-NLS-1$
public static final String DEFAULT_THEME_ID_ATTRIBUTE = "id"; //$NON-NLS-1$
public static final String ICON_PROVIDER_EXTENSION_POINT_ID =
"au.gov.ga.earthsci.application.iconReplacementProviders"; //$NON-NLS-1$
public static final String ID_TO_ICON_MAPPING_ELEMENT = "idToIconMapping"; //$NON-NLS-1$
public static final String ID_ATTRIBUTE = "id"; //$NON-NLS-1$
public static final String ICON_ATTRIBUTE = "icon"; //$NON-NLS-1$
public static final String ID_TO_ICON_PROPERTIES_ELEMENT = "idToIconProperties"; //$NON-NLS-1$
public static final String PROPERTIES_ATTRIBUTE = "properties"; //$NON-NLS-1$
private static final Logger logger = LoggerFactory.getLogger(ThemeLoader.class);
@Inject
private IExtensionRegistry registry;
@Optional
@Inject
public void setupTheme(IThemeEngine engine)
{
if (engine == null)
{
return;
}
// This is done in the setter to ensure that the theme engine is available
// (the engine only becomes available once the part renderer is instantiated)
// The DI engine invokes the setter only once the engine is available
// (vs. a PostConstruct method which is invoked immediately after the object is instantiated).
logger.debug("Loading plugin contributed theme"); //$NON-NLS-1$
IConfigurationElement[] config = registry.getConfigurationElementsFor(DEFAULT_THEME_EXTENSION_POINT_ID);
if (config.length == 0)
{
logger.debug("No plugin contributed theme found"); //$NON-NLS-1$
return;
}
String themeId = config[0].getAttribute(DEFAULT_THEME_ID_ATTRIBUTE);
engine.setTheme(themeId, true);
logger.debug("Switched to theme {} from plugin {}", themeId, config[0].getContributor().getName()); //$NON-NLS-1$
}
@Optional
@Inject
public void setupIcons(MApplication application, EModelService modelService)
{
if (application == null)
{
return;
}
logger.debug("Loading plugin contributed icons"); //$NON-NLS-1$
IConfigurationElement[] config = registry.getConfigurationElementsFor(ICON_PROVIDER_EXTENSION_POINT_ID);
Set<MUILabel> elements = findAllLabels(application, modelService);
Map<String, URI> iconOverrides = findIdToIconMappings(config);
applyIconOverrides(elements, iconOverrides);
}
/**
* Apply the provided icon mappings to the set of {@link MUILabel}s
*/
private void applyIconOverrides(Set<MUILabel> elements, Map<String, URI> iconOverrides)
{
// Apply the icon overrides
for (MUILabel l : elements)
{
String elementId = ((MUIElement) l).getElementId();
URI icon = iconOverrides.get(elementId);
// A mapping to null means no icon
// Absence of a mapping means leave as-is
if (icon != null)
{
l.setIconURI(icon.toString());
}
else if (iconOverrides.containsKey(elementId))
{
l.setIconURI(null);
}
}
}
/**
* Extract all of the {@code elementID->Icon} mappings from the provided
* configuration elements
*/
private Map<String, URI> findIdToIconMappings(IConfigurationElement[] config)
{
Map<String, URI> elementMappings = new HashMap<String, URI>();
// Load the mappings from the config elements
for (IConfigurationElement element : config)
{
String contributingPlugin = element.getContributor().getName();
logger.debug("Loading icons from plugin {}", contributingPlugin); //$NON-NLS-1$
if (ID_TO_ICON_MAPPING_ELEMENT.equals(element.getName()))
{
String iconRelativePath = element.getAttribute(ICON_ATTRIBUTE);
String elementID = element.getAttribute(ID_ATTRIBUTE);
addMapping(elementMappings, elementID, contributingPlugin, iconRelativePath);
}
if (ID_TO_ICON_PROPERTIES_ELEMENT.equals(element.getName()))
{
String propertiesRelativePath = element.getAttribute(PROPERTIES_ATTRIBUTE);
addMappingsFromProperties(elementMappings, contributingPlugin, propertiesRelativePath);
}
}
if (config.length == 0)
{
logger.debug("Loading default icons"); //$NON-NLS-1$
addMappingsFromProperties(elementMappings, Activator.getBundleName(), DEFAULT_ICONS_PROPERTIES);
}
return elementMappings;
}
/**
* Add a single mapping to the provided map
*/
private void addMapping(Map<String, URI> elementMappings, String elementID, String contributingPlugin,
String iconRelativePath)
{
try
{
URI iconURI = makePluginUri(contributingPlugin, iconRelativePath);
elementMappings.put(elementID, iconURI);
}
catch (Exception e)
{
logger.error("Invalid icon URI " + iconRelativePath, e); //$NON-NLS-1$
}
}
/**
* Add all mappings from the provided properties file
*/
private void addMappingsFromProperties(Map<String, URI> elementMappings, String contributingPlugin,
String propertiesRelativePath)
{
URI propertiesURI = null;
try
{
propertiesURI = makePluginUri(contributingPlugin, propertiesRelativePath);
}
catch (Exception e)
{
logger.error("Invalid properties URI " + propertiesRelativePath); //$NON-NLS-1$
return;
}
Properties properties = new Properties();
try
{
properties.load(propertiesURI.toURL().openStream());
}
catch (Exception e)
{
logger.error("Unable to open properties file " + propertiesURI.toString(), e); //$NON-NLS-1$
return;
}
for (String key : properties.stringPropertyNames())
{
try
{
String iconPath = properties.getProperty(key);
if (Util.isBlank(iconPath))
{
elementMappings.put(key, null);
}
else
{
URI iconURI = makePluginUri(contributingPlugin, iconPath);
elementMappings.put(key, iconURI);
}
}
catch (Exception e)
{
logger.error("Invalid icon URI " + properties.getProperty(key)); //$NON-NLS-1$
}
}
}
/**
* Search the application model from the top-level application and find all
* elements that can have icons set on them.
*/
private Set<MUILabel> findAllLabels(MApplication application, EModelService modelService)
{
Set<MUILabel> labels = new HashSet<MUILabel>();
labels.addAll(modelService.findElements(application, null, MUILabel.class, null));
// Model service does not include part descriptors
for (MPartDescriptor d : application.getDescriptors())
{
for (MMenu menu : d.getMenus())
{
if (menu != null)
{
labels.addAll(modelService.findElements(menu, null, MUILabel.class, null));
}
}
if (d.getToolbar() != null)
{
labels.addAll(modelService.findElements(d.getToolbar(), null, MUILabel.class, null));
}
}
// Model service does not include menus and toolbars - need to find them ourselves
List<MPart> parts = modelService.findElements(application, null, MPart.class, null);
for (MPart part : parts)
{
for (MMenu menu : part.getMenus())
{
if (menu != null)
{
labels.addAll(modelService.findElements(menu, null, MUILabel.class, null));
}
}
if (part.getToolbar() != null)
{
labels.addAll(modelService.findElements(part.getToolbar(), null, MUILabel.class, null));
}
}
return labels;
}
/**
* Create a plugin URI for the given resource located in the given plugin
* <p/>
* Plugin URIs have the form
* {@code platform:/plugin/[plugin name]/[resource path]}
*/
private URI makePluginUri(String pluginName, String resourcePath) throws Exception
{
if (resourcePath.startsWith("platform:/plugin/")) //$NON-NLS-1$
{
return new URI(resourcePath);
}
return new URI("platform:/plugin/" + pluginName + "/" + resourcePath); //$NON-NLS-1$ //$NON-NLS-2$
}
}