| /******************************************************************************* |
| * Copyright (c) 2010, 2018 Tom Schindl and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Tom Schindl <tom.schindl@bestsolution.at> - initial API and implementation |
| * Brian de Alwis - added support for multiple CSS engines |
| * Lars Vogel <Lars.Vogel@gmail.com> - Bug 422702 |
| * IBM Corporation - initial API and implementation |
| * Lucas Bullen (Red Hat Inc.) - [Bug 527806] Ship all themes for all OS & WS |
| *******************************************************************************/ |
| package org.eclipse.e4.ui.css.swt.internal.theme; |
| |
| import java.io.BufferedOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.URL; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.eclipse.core.runtime.Assert; |
| import org.eclipse.core.runtime.FileLocator; |
| import org.eclipse.core.runtime.IConfigurationElement; |
| import org.eclipse.core.runtime.IExtension; |
| import org.eclipse.core.runtime.IExtensionPoint; |
| import org.eclipse.core.runtime.IExtensionRegistry; |
| import org.eclipse.core.runtime.IPath; |
| import org.eclipse.core.runtime.Path; |
| import org.eclipse.core.runtime.Platform; |
| import org.eclipse.core.runtime.RegistryFactory; |
| import org.eclipse.core.runtime.preferences.IEclipsePreferences; |
| import org.eclipse.core.runtime.preferences.InstanceScope; |
| import org.eclipse.e4.ui.css.core.engine.CSSElementContext; |
| import org.eclipse.e4.ui.css.core.engine.CSSEngine; |
| import org.eclipse.e4.ui.css.core.util.impl.resources.FileResourcesLocatorImpl; |
| import org.eclipse.e4.ui.css.core.util.impl.resources.OSGiResourceLocator; |
| import org.eclipse.e4.ui.css.core.util.resources.IResourceLocator; |
| import org.eclipse.e4.ui.css.swt.helpers.EclipsePreferencesHelper; |
| import org.eclipse.e4.ui.css.swt.theme.ITheme; |
| import org.eclipse.e4.ui.css.swt.theme.IThemeEngine; |
| import org.eclipse.osgi.service.datalocation.Location; |
| import org.eclipse.swt.widgets.Display; |
| import org.osgi.framework.Bundle; |
| import org.osgi.framework.BundleContext; |
| import org.osgi.framework.FrameworkUtil; |
| import org.osgi.framework.ServiceReference; |
| import org.osgi.service.event.Event; |
| import org.osgi.service.event.EventAdmin; |
| import org.osgi.service.prefs.BackingStoreException; |
| import org.w3c.css.sac.InputSource; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.css.CSSStyleDeclaration; |
| |
| public class ThemeEngine implements IThemeEngine { |
| private List<Theme> themes = new ArrayList<>(); |
| private List<CSSEngine> cssEngines = new ArrayList<>(); |
| |
| // kept for theme notifications only |
| private Display display; |
| |
| private ITheme currentTheme; |
| |
| private List<String> globalStyles = new ArrayList<>(); |
| private List<IResourceLocator> globalSourceLocators = new ArrayList<>(); |
| |
| private HashMap<String, List<String>> stylesheets = new HashMap<>(); |
| private HashMap<String, List<String>> modifiedStylesheets = new HashMap<>(); |
| private HashMap<String, List<IResourceLocator>> sourceLocators = new HashMap<>(); |
| |
| private static final String THEMEID_KEY = "themeid"; |
| |
| public static final String THEME_PLUGIN_ID = "org.eclipse.e4.ui.css.swt.theme"; |
| |
| public static final String E4_DARK_THEME_ID = "org.eclipse.e4.ui.css.theme.e4_dark"; |
| |
| public ThemeEngine(Display display) { |
| this.display = display; |
| |
| IExtensionRegistry registry = RegistryFactory.getRegistry(); |
| IExtensionPoint extPoint = registry.getExtensionPoint(THEME_PLUGIN_ID); |
| |
| //load any modified style sheets |
| Location configLocation = org.eclipse.core.runtime.Platform.getConfigurationLocation(); |
| String e4CSSPath = null; |
| try { |
| URL locationURL = new URL(configLocation.getDataArea(ThemeEngine.THEME_PLUGIN_ID).toString()); |
| File locationFile = new File(locationURL.getFile()); |
| e4CSSPath = locationFile.getPath(); |
| } catch (IOException e1) { |
| } |
| |
| IPath path = new Path(e4CSSPath + File.separator); |
| File modDir= new File(path.toFile().toURI()); |
| if (!modDir.exists()) { |
| modDir.mkdirs(); |
| } |
| |
| File[] modifiedFiles = modDir.listFiles(); |
| String currentOS = Platform.getOS(); |
| String currentWS = Platform.getWS(); |
| boolean e4_dark_mac_found = false; |
| |
| for (IExtension e : extPoint.getExtensions()) { |
| for (IConfigurationElement ce : getPlatformMatches(e.getConfigurationElements())) { |
| if (ce.getName().equals("theme")) { |
| try { |
| String id = ce.getAttribute("id"); |
| String os = ce.getAttribute("os"); |
| String version = ce.getAttribute("os_version"); |
| |
| /* |
| * Code to support e4 dark theme on Mac 10.13 and older. For e4 dark theme on |
| * Mac, register the theme with matching OS version if specified. |
| */ |
| if (E4_DARK_THEME_ID.equals(id) && Platform.OS_MACOSX.equals(currentOS) && os != null |
| && os.equals(currentOS)) { |
| // If e4 dark theme on Mac was already registered, don't try to match or |
| // register again. |
| if (e4_dark_mac_found) { |
| continue; |
| } |
| if (version != null && !isOsVersionMatch(version)) { |
| continue; |
| } else { |
| e4_dark_mac_found = true; |
| version = ""; |
| } |
| } |
| |
| if (version == null) { |
| version = ""; |
| } |
| |
| final String themeBaseId = id + version; |
| String themeId = themeBaseId; |
| String label = ce.getAttribute("label"); |
| String originalCSSFile; |
| String basestylesheeturi = originalCSSFile = ce.getAttribute("basestylesheeturi"); |
| if (!basestylesheeturi.startsWith("platform:/plugin/")) { |
| basestylesheeturi = "platform:/plugin/" + ce.getContributor().getName() + "/" |
| + basestylesheeturi; |
| } |
| registerTheme(themeId, label, basestylesheeturi, version); |
| |
| //check for modified files |
| if (modifiedFiles != null) { |
| int slash = originalCSSFile.lastIndexOf('/'); |
| if (slash != -1) { |
| originalCSSFile = originalCSSFile.substring(slash + 1); |
| for (File modifiedFile : modifiedFiles) { |
| String modifiedFileName = modifiedFile.getName(); |
| if (modifiedFileName.contains(".css") && modifiedFileName.equals(originalCSSFile)) { //$NON-NLS-1$ |
| // modifiedStylesheets |
| ArrayList<String> styleSheets = new ArrayList<>(); |
| styleSheets.add(modifiedFile.toURI().toString()); |
| modifiedStylesheets.put(themeId, styleSheets); |
| } |
| } |
| } |
| } |
| } catch (IllegalArgumentException e1) { |
| ThemeEngineManager.logError(e1.getMessage(), e1); |
| } |
| } |
| } |
| } |
| |
| for (IExtension e : extPoint.getExtensions()) { |
| for (IConfigurationElement ce : getPlatformMatches(e.getConfigurationElements())) { |
| if (ce.getName().equals("stylesheet")) { |
| IConfigurationElement[] cces = ce.getChildren("themeid"); |
| if (cces.length == 0) { |
| registerStylesheet("platform:/plugin/" |
| + ce.getContributor().getName() + "/" |
| + ce.getAttribute("uri")); |
| |
| for (IConfigurationElement resourceEl : ce |
| .getChildren("osgiresourcelocator")) { |
| String uri = resourceEl.getAttribute("uri"); |
| if (uri != null) { |
| registerResourceLocator(new OSGiResourceLocator( |
| uri)); |
| } |
| } |
| } else { |
| String[] themes = new String[cces.length]; |
| for (int i = 0; i < cces.length; i++) { |
| themes[i] = cces[i].getAttribute("refid"); |
| } |
| registerStylesheet( |
| "platform:/plugin/" + ce.getContributor().getName() + "/" + ce.getAttribute("uri"), |
| themes); |
| |
| for (IConfigurationElement resourceEl : ce |
| .getChildren("osgiresourcelocator")) { |
| String uri = resourceEl.getAttribute("uri"); |
| if (uri != null) { |
| registerResourceLocator(new OSGiResourceLocator( |
| uri)); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| // register a default resolver for platform uri's |
| registerResourceLocator(new OSGiResourceLocator( |
| "platform:/plugin/org.eclipse.ui.themes/css/")); |
| // register a default resolver for file uri's |
| registerResourceLocator(new FileResourcesLocatorImpl()); |
| // FIXME: perhaps ResourcesLocatorManager shouldn't have a default? |
| // registerResourceLocator(new HttpResourcesLocatorImpl()); |
| } |
| |
| |
| private boolean isOsVersionMatch(String osVersionList) { |
| boolean found = false; |
| String osVersion = System.getProperty("os.version"); |
| if (osVersion != null) { |
| if (osVersionList != null) { |
| String[] osVersions = osVersionList.split(","); //$NON-NLS-1$ |
| for (String osVersionFromTheme : osVersions) { |
| if (osVersionFromTheme != null) { |
| if (osVersion.contains(osVersionFromTheme)) { |
| found = true; |
| break; |
| } |
| } |
| } |
| } |
| } |
| return found; |
| } |
| |
| @Override |
| public synchronized ITheme registerTheme(String id, String label, String basestylesheetURI) |
| throws IllegalArgumentException { |
| return registerTheme(id, label, basestylesheetURI, ""); |
| } |
| |
| public synchronized ITheme registerTheme(String id, String label, |
| String basestylesheetURI, String osVersion) throws IllegalArgumentException { |
| for (Theme t : themes) { |
| if (t.getId().equals(id)) { |
| throw new IllegalArgumentException("A theme with the id '" + id |
| + "' is already registered"); |
| } |
| } |
| Theme theme = new Theme(id, label); |
| if (osVersion != "") { |
| theme.setOsVersion(osVersion); |
| } |
| themes.add(theme); |
| registerStyle(id, basestylesheetURI); |
| return theme; |
| } |
| |
| @Override |
| public synchronized void registerStylesheet(String uri, String... themes) { |
| Bundle bundle = FrameworkUtil.getBundle(ThemeEngine.class); |
| String osname = bundle.getBundleContext().getProperty("osgi.os"); |
| String wsname = bundle.getBundleContext().getProperty("osgi.ws"); |
| |
| uri = uri.replaceAll("\\$os\\$", osname).replaceAll("\\$ws\\$", wsname); |
| |
| if (themes.length == 0) { |
| globalStyles.add(uri); |
| } else { |
| for (String t : themes) { |
| registerStyle(t, uri); |
| } |
| } |
| } |
| |
| @Override |
| public synchronized void registerResourceLocator(IResourceLocator locator, |
| String... themes) { |
| if (themes.length == 0) { |
| globalSourceLocators.add(locator); |
| } else { |
| for (String t : themes) { |
| List<IResourceLocator> list = sourceLocators.get(t); |
| if (list == null) { |
| list = new ArrayList<>(); |
| sourceLocators.put(t, list); |
| } |
| list.add(locator); |
| } |
| } |
| } |
| |
| private void registerStyle(String id, String stylesheet) { |
| List<String> s = stylesheets.get(id); |
| if (s == null) { |
| s = new ArrayList<>(); |
| stylesheets.put(id, s); |
| } |
| s.add(stylesheet); |
| } |
| |
| private List<String> getAllStyles(String id) { |
| // check for any modifications first |
| List<String> m = modifiedStylesheets.get(id); |
| if (m != null) { |
| m = new ArrayList<>(m); |
| m.addAll(globalStyles); |
| return m; |
| } |
| |
| List<String> s = stylesheets.get(id); |
| if (s == null) { |
| s = Collections.emptyList(); |
| } |
| |
| s = new ArrayList<>(s); |
| s.addAll(globalStyles); |
| return s; |
| |
| } |
| |
| private List<IResourceLocator> getResourceLocators(String id) { |
| List<IResourceLocator> list = new ArrayList<>( |
| globalSourceLocators); |
| List<IResourceLocator> s = sourceLocators.get(id); |
| if (s != null) { |
| list.addAll(s); |
| } |
| |
| return list; |
| } |
| |
| /** |
| * Get all elements that have os/ws attributes that best match the current |
| * platform. |
| * |
| * @param elements the elements to check |
| * @return the best matches, if any |
| */ |
| private IConfigurationElement[] getPlatformMatches(IConfigurationElement[] elements) { |
| Bundle bundle = FrameworkUtil.getBundle(ThemeEngine.class); |
| String osname = bundle.getBundleContext().getProperty("osgi.os"); |
| // TODO: Need to differentiate win32 versions |
| String wsname = bundle.getBundleContext().getProperty("osgi.ws"); |
| ArrayList<IConfigurationElement> matchingElements = new ArrayList<>(); |
| for (IConfigurationElement element : elements) { |
| String elementOs = element.getAttribute("os"); |
| String elementWs = element.getAttribute("ws"); |
| if (osname != null && (elementOs == null || elementOs.contains(osname))) { |
| matchingElements.add(element); |
| } else if (wsname != null && wsname.equalsIgnoreCase(elementWs)) { |
| matchingElements.add(element); |
| } |
| } |
| return matchingElements.toArray(new IConfigurationElement[matchingElements.size()]); |
| } |
| |
| @Override |
| public void setTheme(String themeId, boolean restore) { |
| String osVersion = System.getProperty("os.version"); |
| if (osVersion != null) { |
| boolean found = false; |
| for (Theme t : themes) { |
| String osVersionList = t.getOsVersion(); |
| if (osVersionList != null) { |
| String[] osVersions = osVersionList.split(","); //$NON-NLS-1$ |
| for (String osVersionFromTheme : osVersions) { |
| if (osVersionFromTheme != null && osVersion.contains(osVersionFromTheme)) { |
| String themeVersion = themeId + osVersionList; |
| if (t.getId().equals(themeVersion)) { |
| setTheme(t, restore); |
| found = true; |
| break; |
| } |
| } |
| } |
| } |
| } |
| if (found) { |
| return; |
| } |
| } |
| //try generic |
| for (Theme t : themes) { |
| if (t.getId().equals(themeId)) { |
| setTheme(t, restore); |
| break; |
| } |
| } |
| } |
| |
| @Override |
| public void setTheme(ITheme theme, boolean restore) { |
| setTheme(theme, restore, false); |
| } |
| |
| public void setTheme(ITheme theme, boolean restore, boolean force) { |
| Assert.isNotNull(theme, "The theme must not be null"); |
| |
| if (this.currentTheme != theme || force) { |
| if (currentTheme != null) { |
| for (IResourceLocator l : getResourceLocators(currentTheme |
| .getId())) { |
| for (CSSEngine engine : cssEngines) { |
| engine.getResourcesLocatorManager() |
| .unregisterResourceLocator(l); |
| } |
| } |
| } |
| |
| this.currentTheme = theme; |
| for (CSSEngine engine : cssEngines) { |
| engine.reset(); |
| } |
| |
| for (IResourceLocator l : getResourceLocators(theme.getId())) { |
| for (CSSEngine engine : cssEngines) { |
| engine.getResourcesLocatorManager() |
| .registerResourceLocator(l); |
| } |
| } |
| for (String stylesheet : getAllStyles(theme.getId())) { |
| URL url; |
| InputStream stream = null; |
| try { |
| url = FileLocator.resolve(new URL(stylesheet)); |
| for (CSSEngine engine : cssEngines) { |
| try { |
| stream = url.openStream(); |
| InputSource source = new InputSource(); |
| source.setByteStream(stream); |
| source.setURI(url.toString()); |
| engine.parseStyleSheet(source); |
| } catch (IOException e) { |
| ThemeEngineManager.logError(e.getMessage(), e); |
| } finally { |
| if (stream != null) { |
| try { |
| stream.close(); |
| } catch (IOException e) { |
| ThemeEngineManager.logError(e.getMessage(), e); |
| } |
| } |
| } |
| } |
| } catch (IOException e) { |
| ThemeEngineManager.logError(e.getMessage(), e); |
| } |
| } |
| } |
| |
| if (restore) { |
| IEclipsePreferences pref = getPreferences(); |
| EclipsePreferencesHelper.setPreviousThemeId(pref.get(THEMEID_KEY, null)); |
| EclipsePreferencesHelper.setCurrentThemeId(theme.getId()); |
| |
| pref.put(THEMEID_KEY, theme.getId()); |
| try { |
| pref.flush(); |
| } catch (BackingStoreException e) { |
| ThemeEngineManager.logError(e.getMessage(), e); |
| } |
| } |
| sendThemeChangeEvent(restore); |
| |
| for (CSSEngine engine : cssEngines) { |
| engine.reapply(); |
| } |
| } |
| |
| /** |
| * Broadcast theme-change event using OSGi Event Admin. |
| */ |
| private void sendThemeChangeEvent(boolean restore) { |
| EventAdmin eventAdmin = getEventAdmin(); |
| if (eventAdmin == null) { |
| return; |
| } |
| Map<String, Object> data = new HashMap<>(); |
| data.put(IThemeEngine.Events.THEME_ENGINE, this); |
| data.put(IThemeEngine.Events.THEME, currentTheme); |
| data.put(IThemeEngine.Events.DEVICE, display); |
| data.put(IThemeEngine.Events.RESTORE, restore); |
| Event event = new Event(IThemeEngine.Events.THEME_CHANGED, data); |
| eventAdmin.sendEvent(event); // synchronous |
| } |
| |
| public List<IResourceLocator> getCurrentResourceLocators() { |
| if(currentTheme == null) { return Collections.emptyList(); } |
| return getResourceLocators(currentTheme.getId()); |
| } |
| |
| private EventAdmin getEventAdmin() { |
| Bundle bundle = FrameworkUtil.getBundle(this.getClass()); |
| if (bundle == null) { |
| return null; |
| } |
| BundleContext context = bundle.getBundleContext(); |
| ServiceReference<EventAdmin> eventAdminRef = context.getServiceReference(EventAdmin.class); |
| return context.getService(eventAdminRef); |
| } |
| |
| @Override |
| public synchronized List<ITheme> getThemes() { |
| return Collections.unmodifiableList(new ArrayList<ITheme>(themes)); |
| } |
| |
| @Override |
| public void applyStyles(Object widget, boolean applyStylesToChildNodes) { |
| for (CSSEngine engine : cssEngines) { |
| Element element = engine.getElement(widget); |
| if (element != null) { |
| engine.applyStyles(element, applyStylesToChildNodes); |
| } |
| } |
| } |
| |
| private String getPreferenceThemeId() { |
| return getPreferences().get(THEMEID_KEY, null); |
| } |
| |
| private IEclipsePreferences getPreferences() { |
| return InstanceScope.INSTANCE.getNode(FrameworkUtil.getBundle(ThemeEngine.class).getSymbolicName()); |
| } |
| |
| void copyFile(String from, String to) throws IOException { |
| FileInputStream fStream = null; |
| BufferedOutputStream outputStream = null; |
| try { |
| fStream = new FileInputStream(from); |
| outputStream = new BufferedOutputStream(new FileOutputStream(to)); |
| byte[] buffer = new byte[4096]; |
| int c; |
| while ((c = fStream.read(buffer)) != -1) { |
| outputStream.write(buffer, 0, c); |
| } |
| |
| } finally { |
| if (fStream != null) { |
| fStream.close(); |
| } |
| if (outputStream != null) { |
| outputStream.close(); |
| } |
| } |
| } |
| |
| @Override |
| public void restore(String alternateTheme) { |
| String prefThemeId = getPreferenceThemeId(); |
| boolean flag = true; |
| if (prefThemeId != null) { |
| for (ITheme t : getThemes()) { |
| if (prefThemeId.equals(t.getId())) { |
| setTheme(t, false); |
| flag = false; |
| break; |
| } |
| } |
| } |
| |
| // For Mac & GTK, if the system has Dark appearance set and Eclipse is using the |
| // default settings, then start Eclipse in Dark theme. |
| |
| // Check that there is a dark theme present |
| boolean hasDarkTheme = getThemes().stream().anyMatch(t -> t.getId().startsWith(E4_DARK_THEME_ID)); |
| |
| String os = Platform.getOS(); |
| String themeToRestore = (Platform.OS_LINUX.equals(os) || Platform.OS_MACOSX.equals(os)) |
| && Display.isSystemDarkTheme() && hasDarkTheme ? E4_DARK_THEME_ID : alternateTheme; |
| if (themeToRestore != null && flag) { |
| setTheme(themeToRestore, false); |
| } |
| } |
| |
| @Override |
| public ITheme getActiveTheme() { |
| return currentTheme; |
| } |
| |
| @Override |
| public CSSStyleDeclaration getStyle(Object widget) { |
| for (CSSEngine engine : cssEngines) { |
| CSSElementContext context = engine.getCSSElementContext(widget); |
| if (context != null) { |
| Element e = context.getElement(); |
| if (e != null) { |
| return engine.getViewCSS().getComputedStyle(e, null); |
| } |
| } |
| } |
| return null; |
| } |
| |
| public List<String> getStylesheets(ITheme selection) { |
| List<String> ss = stylesheets.get(selection.getId()); |
| return ss == null ? new ArrayList<>() : ss; |
| } |
| |
| public void themeModified(ITheme theme, List<String> paths) { |
| modifiedStylesheets.put(theme.getId(), paths); |
| setTheme(theme, false, true); |
| } |
| |
| public void resetCurrentTheme() { |
| if (currentTheme != null) { |
| setTheme(currentTheme, false, true); |
| } |
| } |
| |
| public List<String> getModifiedStylesheets(ITheme selection) { |
| List<String> ss = modifiedStylesheets.get(selection.getId()); |
| return ss == null ? new ArrayList<>() : ss; |
| } |
| |
| public void resetModifiedStylesheets(ITheme selection) { |
| modifiedStylesheets.remove(selection.getId()); |
| } |
| |
| @Override |
| public void addCSSEngine(CSSEngine cssEngine) { |
| cssEngines.add(cssEngine); |
| resetCurrentTheme(); |
| } |
| |
| public Collection<CSSEngine> getCSSEngines() { |
| return cssEngines; |
| } |
| |
| @Override |
| public void removeCSSEngine(CSSEngine cssEngine) { |
| cssEngines.remove(cssEngine); |
| } |
| } |