blob: 109c80bc5f5b37261eef2a4977d72d1f770c291b [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004, 2016 IBM Corporation and others.
* 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:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.ui.internal.intro.impl.model;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.help.UAContentFilter;
import org.eclipse.help.internal.UAElementFactory;
import org.eclipse.help.internal.util.ProductPreferences;
import org.eclipse.jface.util.SafeRunnable;
import org.eclipse.ui.IPropertyListener;
import org.eclipse.ui.internal.intro.impl.FontSelection;
import org.eclipse.ui.internal.intro.impl.IntroPlugin;
import org.eclipse.ui.internal.intro.impl.model.loader.IntroContentParser;
import org.eclipse.ui.internal.intro.impl.model.loader.ModelLoaderUtil;
import org.eclipse.ui.internal.intro.impl.model.util.BundleUtil;
import org.eclipse.ui.internal.intro.impl.model.util.ModelUtil;
import org.eclipse.ui.internal.intro.impl.util.IntroEvaluationContext;
import org.eclipse.ui.internal.intro.impl.util.Log;
import org.eclipse.ui.internal.intro.impl.util.StringUtil;
import org.eclipse.ui.intro.config.IntroConfigurer;
import org.osgi.framework.Bundle;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
/**
* The root class for the OOBE model. It loads the configuration into the
* appropriate classes.
*
* Model rules:
* <ol>
* <li>if an attribute is not included in the markup, its value will be null in
* the model.</li>
* <li>Resources in plugin.xml are not implicitly resolved against $nl$.
* Resources in pages are implicitly resolved against $nl$
* <li>the current page id is set silently when loading the model. You do not
* need the event notification on model load.</li>
* <li>Children of a given parent (ie: model root, page, or group) *must* have
* distinctive IDs otherwise resolving includes and extensions may fail.</li>
* <li>Containers have the concept of loading children and resolving children.
* At the model root level, resolving children means resolving ALL extensions of
* model. At the container level, resolving children means resolving includes.
* </li>
* <li>Extensions are resolved before includes at the container level to avoid
* race conditions. eg: if a page includes a shared group and an extension
* extends this shared group, you want the include to get the extended group and
* not the original group.</li>
* <li>Resolving extensions should not resolve includes. No need to load other
* models when we dont have to. Plus, extensions can only reference anchors, and
* so no need to resolve includes.</li>
* <li>Extensions can not target containers *after* they are resolved. For
* example, an extension can not target a shared group after it has been
* included in a page. It can target the initial shared group as a path, but not
* the group in the page as a path. Again this is because extensions extends
* anchors that already have a path, not a resolved path.</li>
* <li>Pages and shared groups that are contributed through extensions become
* children of the atrget configuration, and so any includes they may have will
* be resolved correctly.</li>
* <li>An infinite loop can occur if page A includes from page B and page B in
* turn includes from page A. ie: cyclic includes. For performnace, accept.
* </li>
* <li>When resolving includes, if the target is a container, it must be
* resolved to resolve its includes correctly. Otherwise, included includes will
* fail due to reparenting.</li>
* <li>unresolved includes are left as children of the parent container.</li>
* <li>Unresolved extensions are left as children of the targetted model.</li>
* <li>For dynamic awarness, the model is nulled and then reloaded. However, we
* need to preserve the presentation instance since the UI is already loaded.
* This is done by reloading the model, and directly resetting the presentation
* to what it was.</li>
* <li>Model classes should not have DOM classes as instance vars, and if this
* is a must, null the DOM class instance the minute you are done. This is
* because you want the VM to garbage collect the DOM model. Keeping a reference
* to the DOM model from the Intro model will prevent that.</li>
* </ol>
* <li>(since 3.0.2) several passes are used to resolve contributions to
* anchors that themselves where contributed through an extension. Each time a
* contribution is resolved, the model tries to resolve all unresolved
* contribution, recursively.
* </ul>
*/
public class IntroModelRoot extends AbstractIntroContainer {
/**
* Model constants that fire property change event when they are changed in
* the model.
*/
public static final int CURRENT_PAGE_PROPERTY_ID = 1;
private static final String ATT_CONTENT = "content"; //$NON-NLS-1$
private static final String ATT_CONFIGURER = "configurer"; //$NON-NLS-1$
private static final String VAR_THEME = "theme"; //$NON-NLS-1$
private static final String VAR_DIRECTION = "direction"; //$NON-NLS-1$
// False if there is no valid contribution to the
// org.eclipse.ui.intro.config extension point. Start off with true, and set
// to false whenever something bad happens.
private boolean hasValidConfig = true;
private IntroConfigurer configurer;
private IntroTheme theme;
private IntroPartPresentation introPartPresentation;
private IntroHomePage rootPage;
private String currentPageId;
private String startPageId;
private AbstractIntroPage standbyPage;
private AbstractIntroPage homePage;
private String modelStandbyPageId;
// the config extensions for this model.
private IConfigurationElement[] configExtensionElements;
// maintain listener list for model changes.
public ListenerList<IPropertyListener> propChangeListeners = new ListenerList<>();
// a list to hold all loaded DOMs until resolving all configExtensions
// is done.
private List<ExtensionContent> unresolvedConfigExt = new ArrayList<>();
private class ExtensionContent {
Element element;
IConfigurationElement configExtElement;
ExtensionContent(Element element,
IConfigurationElement configExtElement) {
this.element = element;
this.configExtElement = configExtElement;
}
}
/**
* Model root. Takes a configElement that represents <config>in the
* plugin.xml markup AND all the extension contributed to this model through
* the configExtension point.
*/
public IntroModelRoot(IConfigurationElement configElement,
IConfigurationElement[] configExtensionElements) {
// the config element that represents the correct model root.
super(configElement);
this.configExtensionElements = configExtensionElements;
}
public void loadModel() {
getChildren();
determineHomePage();
}
/**
* Loads the full model. The children of a model root are the presentation,
* followed by all pages, and all shared groups. Then if the model has
* extension, its the unresolved container extensions, followed by all
* extension pages and groups. The presentation is loaded from the
* IConfiguration element representing the config. All else is loaded from
* xml content file.
*
*/
@Override
protected void loadChildren() {
children = new Vector<>();
if (Log.logInfo)
Log.info("Creating Intro plugin model...."); //$NON-NLS-1$
// load presentation first and create the model class for it. If there
// is more than one presentation, load first one, and log rest.
IConfigurationElement presentationElement = loadPresentation();
if (presentationElement == null) {
// no presentations at all, exit.
setModelState(true, false);
Log.warning("Could not find presentation element in intro config."); //$NON-NLS-1$
return;
}
loadTheme();
loadConfigurer();
introPartPresentation = new IntroPartPresentation(presentationElement);
children.add(introPartPresentation);
// set parent.
introPartPresentation.setParent(this);
// now load all children of the config. There should only be pages and
// groups here. And order is not important. These elements are loaded
// from the content file DOM.
Document document = loadDOM(getCfgElement());
if (document == null) {
// we failed to parse the content file. Intro Parser would have
// logged the fact. Parser would also have checked to see if the
// content file has the correct root tag.
setModelState(true, false);
return;
}
// set base for this model.
this.base = getBase(getCfgElement());
// now load content.
loadPages(document, getBundle());
loadSharedGroups(document, getBundle());
// Attributes of root page decide if we have a static or dynamic case.
setModelState(true, true);
if (configurer != null) {
// The configurer may vary its returned results based on the theme
// properties
configurer.bind(this);
}
}
/**
* Sets the presentation to the given presentation. The model always has the
* presentation as the first child, so use that fact. This method is used
* for dynamic awarness to enable replacing the new presentation with the
* existing one after a model refresh.
*
* @param presentation
*/
public void setPresentation(IntroPartPresentation presentation) {
this.introPartPresentation = presentation;
presentation.setParent(this);
children.set(0, presentation);
}
/**
* Resolve contributions into this container's children.
*/
@Override
protected void resolveChildren() {
// now handle config extension.
resolveConfigExtensions();
resolved = true;
}
private IConfigurationElement loadPresentation() {
// If there is more than one presentation, load first one, and log
// rest.
IConfigurationElement[] presentationElements = getCfgElement()
.getChildren(IntroPartPresentation.TAG_PRESENTATION);
IConfigurationElement presentationElement = ModelLoaderUtil
.validateSingleContribution(presentationElements,
IntroPartPresentation.ATT_HOME_PAGE_ID);
return presentationElement;
}
private void loadConfigurer() {
String cname = getCfgElement().getAttribute(ATT_CONFIGURER);
if (cname!=null) {
try {
Object obj = getCfgElement().createExecutableExtension(ATT_CONFIGURER);
if (obj instanceof IntroConfigurer) {
configurer = (IntroConfigurer)obj;
}
}
catch (CoreException e) {
Log.error("Error loading intro configurer", e); //$NON-NLS-1$
}
}
}
private void determineHomePage() {
String pid = Platform.getProduct().getId();
startPageId = getProcessPreference("INTRO_START_PAGE", pid); //$NON-NLS-1$
String homePagePreference = getProcessPreference("INTRO_HOME_PAGE", pid); //$NON-NLS-1$
homePage = rootPage; // Default, may be overridden
if (homePagePreference.length() != 0) {
AbstractIntroPage page = (AbstractIntroPage) findChild(homePagePreference,
ABSTRACT_PAGE);
if (page != null) {
homePage = page;
if(startPageId.length() == 0) {
startPageId = homePagePreference;
}
}
}
String standbyPagePreference = getProcessPreference("INTRO_STANDBY_PAGE", pid); //$NON-NLS-1$
modelStandbyPageId = getPresentation().getStandbyPageId();
if (standbyPagePreference.length() != 0) {
standbyPage = (AbstractIntroPage) findChild(standbyPagePreference,
ABSTRACT_PAGE);
}
if (standbyPage == null && modelStandbyPageId != null && modelStandbyPageId.length() != 0) {
standbyPage = (AbstractIntroPage) findChild(modelStandbyPageId,
ABSTRACT_PAGE);
}
if (standbyPage != null) {
standbyPage.setStandbyPage(true);
}
}
private void loadTheme() {
String pid = Platform.getProduct().getId();
String themeId = getProcessPreference("INTRO_THEME", pid); //$NON-NLS-1$
IConfigurationElement [] elements = Platform.getExtensionRegistry().getConfigurationElementsFor("org.eclipse.ui.intro.configExtension"); //$NON-NLS-1$
IConfigurationElement themeElement=null;
for (int i=0; i<elements.length; i++) {
if (elements[i].getName().equals("theme")) { //$NON-NLS-1$
String id = elements[i].getAttribute("id"); //$NON-NLS-1$
if (themeId!=null) {
if (id!=null && themeId.equals(id)) {
// use this one
themeElement = elements[i];
break;
}
}
else {
// see if this one is the default
String value = elements[i].getAttribute("default"); //$NON-NLS-1$
if (value!=null && value.equalsIgnoreCase("true")) { //$NON-NLS-1$
themeElement = elements[i];
break;
}
}
}
}
if (themeElement!=null) {
theme = new IntroTheme(themeElement);
}
}
/**
* Loads all pages defined in this config from the xml content file.
*/
private void loadPages(Document dom, Bundle bundle) {
String rootPageId = getPresentation().getHomePageId();
Element[] pages = ModelUtil.getElementsByTagName(dom,
AbstractIntroPage.TAG_PAGE);
for (int i = 0; i < pages.length; i++) {
Element pageElement = pages[i];
if (pageElement.getAttribute(AbstractIntroIdElement.ATT_ID).equals(
rootPageId)) {
// Create the model class for the Root Page.
rootPage = new IntroHomePage(pageElement, bundle, base);
rootPage.setParent(this);
currentPageId = rootPage.getId();
children.add(rootPage);
} else {
// Create the model class for an intro Page.
IntroPage page = new IntroPage(pageElement, bundle, base);
page.setParent(this);
children.add(page);
}
}
}
/**
* Loads all shared groups defined in this config, from the DOM.
*/
private void loadSharedGroups(Document dom, Bundle bundle) {
Element[] groups = ModelUtil.getElementsByTagName(dom,
IntroGroup.TAG_GROUP);
for (int i = 0; i < groups.length; i++) {
IntroGroup group = new IntroGroup(groups[i], bundle, base);
group.setParent(this);
children.add(group);
}
}
/**
* Handles all the configExtensions to this current model. Resolving
* configExts means finding target anchor and inserting extension content at
* target. Also, several passes are used to resolve as many extensions as
* possible. This allows for resolving nested anchors (ie: anchors to
* anchors in contributions).
*/
private void resolveConfigExtensions() {
for (int i = 0; i < configExtensionElements.length; i++) {
processConfigExtension(configExtensionElements[i]);
}
tryResolvingExtensions();
// At this stage all pages will be resolved, some contributions may not be
// now add all unresolved extensions as model children and log fact.
Iterator keys = unresolvedConfigExt.iterator();
while (keys.hasNext()) {
ExtensionContent extension = (ExtensionContent) keys.next();
Element configExtensionElement = extension.element;
IConfigurationElement configExtConfigurationElement = extension.configExtElement;
Bundle bundle = BundleUtil
.getBundleFromConfigurationElement(configExtConfigurationElement);
String base = getBase(configExtConfigurationElement);
children.add(new IntroExtensionContent(configExtensionElement,
bundle, base, configExtConfigurationElement));
// INTRO: fix log strings.
Log
.warning("Could not resolve the following configExtension: " //$NON-NLS-1$
+ ModelLoaderUtil.getLogString(bundle,
configExtensionElement,
IntroExtensionContent.ATT_PATH));
}
}
private void processConfigExtension(IConfigurationElement configExtElement) {
// This call will extract the parent folder if needed.
Document dom = loadDOM(configExtElement);
if (dom == null)
// we failed to parse the content file. Intro Parser would
// have logged the fact. Parser would also have checked to
// see if the content file has the correct root tag.
return;
processConfigExtension(dom, configExtElement);
}
private void processConfigExtension(Document dom,
IConfigurationElement configExtElement) {
// Find the target of this container extension, and add all its
// children to target. Make sure to pass correct bundle and base to
// propagate to all children.
String base = getBase(configExtElement);
Element[] extensionContentElements = loadExtensionContent(dom,
configExtElement, base);
for (int i = 0; i < extensionContentElements.length; i++) {
Element extensionContentElement = extensionContentElements[i];
unresolvedConfigExt.add(new ExtensionContent(extensionContentElement,
configExtElement));
}
// Now load all pages and shared groups
// from this config extension. Get the bundle from the extensions since they are
// defined in other plugins.
Bundle bundle = BundleUtil
.getBundleFromConfigurationElement(configExtElement);
Element[] pages = ModelUtil.getElementsByTagName(dom,
AbstractIntroPage.TAG_PAGE);
for (int j = 0; j < pages.length; j++) {
// Create the model class for an intro Page.
if (!UAContentFilter.isFiltered(UAElementFactory.newElement(pages[j]), IntroEvaluationContext.getContext())) {
IntroPage page = new IntroPage(pages[j], bundle, base);
page.setParent(this);
children.add(page);
}
}
}
private void tryResolvingExtensions() {
int previousSize;
do {
previousSize = unresolvedConfigExt.size();
List<ExtensionContent> stillUnresolved = new ArrayList<>();
for (ExtensionContent content : unresolvedConfigExt) {
Element extensionContentElement = content.element;
IConfigurationElement configExtElement = content.configExtElement;
Bundle bundle = BundleUtil.getBundleFromConfigurationElement(configExtElement);
String elementBase = getBase(configExtElement);
processOneExtension(configExtElement, elementBase, bundle, extensionContentElement);
if (extensionContentElement.hasAttribute("failed")) { //$NON-NLS-1$
stillUnresolved.add(content);
}
}
unresolvedConfigExt = stillUnresolved;
} while (unresolvedConfigExt.size() < previousSize);
}
/**
* load the extension content of this configExtension into model classes,
* and insert them at target. A config extension can have only ONE extension
* content. This is because if the extension fails, we need to be able to
* not include the page and group contributions as part of the model. If
* extension content has XHTML content (ie: content attribute is defined) we
* load extension DOM into target page dom.
*
* note: the extension Element is returned to enable creating a child model
* element on failure.
*
* @param
* @return
*/
private Element[] loadExtensionContent(Document dom,
IConfigurationElement configExtElement, String base) {
// get the bundle from the extensions since they are defined in
// other plugins.
List<Element> elements = new ArrayList<>();
Element[] extensionContents = ModelUtil.getElementsByTagName(dom,
IntroExtensionContent.TAG_CONTAINER_EXTENSION);
Element[] replacementContents = ModelUtil.getElementsByTagName(dom,
IntroExtensionContent.TAG_CONTAINER_REPLACE);
addUnfilteredExtensions(elements, extensionContents);
addUnfilteredExtensions(elements, replacementContents);
return (Element[])elements.toArray(new Element[elements.size()]);
}
private void addUnfilteredExtensions(List<Element> elements, Element[] extensionContents) {
for (int i = 0; i < extensionContents.length; i++) {
Element extensionContentElement = extensionContents[i];
if (!UAContentFilter.isFiltered(UAElementFactory.newElement(extensionContentElement), IntroEvaluationContext.getContext())) {
elements.add(extensionContentElement);
}
}
}
private void processOneExtension(IConfigurationElement configExtElement, String base, Bundle bundle,
Element extensionContentElement) {
// Create the model class for extension content.
IntroExtensionContent extensionContent = new IntroExtensionContent(
extensionContentElement, bundle, base, configExtElement);
boolean success = false;
if (extensionContent.isXHTMLContent())
success = loadXHTMLExtensionContent(extensionContent);
else
success = load3_0ExtensionContent(extensionContent);
if (success) {
if (extensionContentElement.hasAttribute("failed")) //$NON-NLS-1$
extensionContentElement.removeAttribute("failed"); //$NON-NLS-1$
} else
extensionContentElement.setAttribute("failed", "true"); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Insert the extension content into the target.
*
* @param extensionContent
* @return
*/
private boolean loadXHTMLExtensionContent(
IntroExtensionContent extensionContent) {
String path = extensionContent.getPath();
// path must be pageId/anchorID in the case of anchors in XHTML pages.
String[] pathSegments = StringUtil.split(path, "/"); //$NON-NLS-1$
if (pathSegments.length != 2)
// path does not have correct format.
return false;
AbstractIntroPage targetPage = (AbstractIntroPage) findChild(
pathSegments[0], ABSTRACT_PAGE);
if (targetPage == null)
// target could not be found. Signal failure.
return false;
// Insert all children of this extension before the target element. Anchors need
// to stay in DOM, even after all extensions have been resolved, to enable other
// plugins to contribute. Find the target node.
Document pageDom = targetPage.getDocument();
Element targetElement = targetPage.findDomChild(pathSegments[1], "*"); //$NON-NLS-1$
if (targetElement == null)
return false;
// get extension content
Element[] elements = extensionContent.getElements();
// insert all children before anchor in page body.
for (int i = 0; i < elements.length; i++) {
Node targetNode = pageDom.importNode(elements[i], true);
// update the src attribute of this node, if defined by w3
// specs.
ModelUtil.updateResourceAttributes((Element) targetNode,
extensionContent);
targetElement.getParentNode().insertBefore(targetNode, targetElement);
}
if (extensionContent.getExtensionType() == IntroExtensionContent.TYPE_REPLACEMENT) {
targetElement.getParentNode().removeChild(targetElement);
}
// now handle style inheritance.
// Update the parent page styles. skip style if it is null;
String[] styles = extensionContent.getStyles();
if (styles != null) {
for (int i = 0; i < styles.length; i++)
ModelUtil.insertStyle(pageDom, styles[i]);
}
return true;
}
/**
* Insert the extension content (3.0 format) into the target.
*
* @param extensionContent
* @return
*/
private boolean load3_0ExtensionContent(IntroExtensionContent extensionContent) {
String path = extensionContent.getPath();
int type = extensionContent.getExtensionType();
AbstractIntroElement target = findTarget(this, path, extensionContent.getId());
if (target != null && target.isOfType(AbstractIntroElement.ANCHOR) == (type == IntroExtensionContent.TYPE_CONTRIBUTION)) {
// insert all children of this extension before the target element/anchor.
insertExtensionChildren(target, extensionContent, extensionContent.getBundle(), extensionContent.getBase());
// anchors need to stay around to receive other contributions
if (type == IntroExtensionContent.TYPE_REPLACEMENT) {
AbstractIntroContainer parent = (AbstractIntroContainer)target.getParent();
parent.removeChild(target);
}
handleExtensionStyleInheritence(target, extensionContent);
return true;
}
// appropriate target could not be found. Signal failure.
return false;
}
private void insertExtensionChildren(AbstractIntroElement target,
IntroExtensionContent extensionContent, Bundle bundle, String base) {
AbstractIntroContainer parent = (AbstractIntroContainer)target.getParent();
// insert the elements of the extension before the target
String mixinStyle = getMixinStyle(extensionContent);
Element [] children = extensionContent.getChildren();
parent.insertElementsBefore(children, bundle, base, target, mixinStyle);
}
private String getMixinStyle(IntroExtensionContent extensionContent) {
String path = extensionContent.getPath();
if (!path.endsWith("/@")) //$NON-NLS-1$
return null;
String pageId = path.substring(0, path.length()-2);
IntroModelRoot modelRoot = getModelRoot();
if (modelRoot==null)
return null;
IntroConfigurer configurer = modelRoot.getConfigurer();
if (configurer==null)
return null;
String extensionId = extensionContent.getId();
// if this is a replace, take the mixin style as what is being replaced
if (extensionContent.getExtensionType() == IntroExtensionContent.TYPE_REPLACEMENT) {
IPath ipath = new Path(extensionContent.getPath());
String s2 = ipath.segment(1);
if (s2 != null && s2.startsWith("@") && s2.length() > 1) { //$NON-NLS-1$
extensionId = s2.substring(1);
}
}
return configurer.getMixinStyle(pageId, extensionId);
}
/**
* Updates the inherited styles based on the style attribtes defined in the
* configExtension. If we are extending a shared group do nothing. For
* inherited alt-styles, we have to cache the bundle from which we inherited
* the styles to be able to access resources in that plugin.
*
* @param include
* @param target
*/
private void handleExtensionStyleInheritence(AbstractIntroElement target,
IntroExtensionContent extension) {
AbstractIntroContainer targetContainer = (AbstractIntroContainer)target.getParent();
if (targetContainer.getType() == AbstractIntroElement.GROUP
&& targetContainer.getParent().getType() == AbstractIntroElement.MODEL_ROOT)
// if we are extending a shared group, defined under a config, we
// can not include styles.
return;
// Update the parent page styles. skip style if it is null;
String[] styles = extension.getStyles();
if (styles != null)
targetContainer.getParentPage().addStyles(styles);
// for alt-style cache bundle for loading resources.
Map<String, Bundle> altStyles = extension.getAltStyles();
if (altStyles != null)
targetContainer.getParentPage().addAltStyles(altStyles);
}
/**
* Sets the model state based on all the model classes.
*/
private void setModelState(boolean loaded, boolean hasValidConfig) {
this.loaded = loaded;
this.hasValidConfig = hasValidConfig;
}
/**
* Returns true if there is a valid contribution to
* org.eclipse.ui.intro.config extension point, with a valid Presentation,
* and pages.
*
* @return Returns the hasValidConfig.
*/
public boolean hasValidConfig() {
return hasValidConfig;
}
/**
* @return Returns the introPartPresentation.
*/
public IntroPartPresentation getPresentation() {
return introPartPresentation;
}
public IntroConfigurer getConfigurer() {
return configurer;
}
/**
* @return Returns the home Page.
*/
public AbstractIntroPage getHomePage() {
return homePage;
}
/**
* @return Returns the root Page.
*/
public IntroHomePage getRootPage() {
return rootPage;
}
/**
* @return Returns the standby Page. May return null if standby page is not
* defined.
*/
public AbstractIntroPage getStandbyPage() {
return standbyPage;
}
/**
* @return all pages *excluding* the Home Page. If all pages are needed,
* call <code>(AbstractIntroPage[])
* getChildrenOfType(IntroElement.ABSTRACT_PAGE);</code>
*/
public IntroPage[] getPages() {
return (IntroPage[]) getChildrenOfType(AbstractIntroElement.PAGE);
}
/**
* @return Returns the isdynamicIntro.
*/
public boolean isDynamic() {
if ("swt".equals(getPresentation().getImplementationKind())) { //$NON-NLS-1$
return rootPage != null && rootPage.isDynamic();
}
return true;
}
/**
* @return Returns the currentPageId.
*/
public String getCurrentPageId() {
return currentPageId;
}
/**
* Sets the current page. If the model does not have a page with the passed
* id, the message is logged, and the model retains its old current page.
*
* @param currentPageId
* The currentPageId to set. *
* @param fireEvent
* flag to indicate if event notification is needed.
* @return true if the model has a page with the passed id, false otherwise.
* If the method fails, the current page remains the same as the
* last state.
*/
public boolean setCurrentPageId(String pageId, boolean fireEvent) {
if (pageId.equals(currentPageId))
// setting to the same page does nothing. Return true because we did
// not actually fail. just a no op.
return true;
AbstractIntroPage page = (AbstractIntroPage) findChild(pageId,
ABSTRACT_PAGE);
if (page == null) {
// not a page. Test for root page.
if (!pageId.equals(rootPage.getId())) {
// not a page nor the home page.
Log
.warning("Could not set current page to Intro page with id: " + pageId); //$NON-NLS-1$
return false;
}
}
currentPageId = pageId;
if (fireEvent)
firePropertyChange(CURRENT_PAGE_PROPERTY_ID);
return true;
}
public boolean setCurrentPageId(String pageId) {
return setCurrentPageId(pageId, true);
}
public void addPropertyListener(IPropertyListener l) {
propChangeListeners.add(l);
}
/**
* Fires a property changed event. Made public because it can be used to
* trigger a UI refresh.
*
* @param propertyId
* the id of the property that changed
*/
public void firePropertyChange(final int propertyId) {
Object[] array = propChangeListeners.getListeners();
for (int i = 0; i < array.length; i++) {
final IPropertyListener l = (IPropertyListener) array[i];
SafeRunner.run(new SafeRunnable() {
@Override
public void run() {
l.propertyChanged(this, propertyId);
}
@Override
public void handleException(Throwable e) {
super.handleException(e);
// If an unexpected exception happens, remove it
// to make sure the workbench keeps running.
propChangeListeners.remove(l);
}
});
}
}
public void removePropertyListener(IPropertyListener l) {
propChangeListeners.remove(l);
}
/**
* @return Returns the currentPage. return null if page is not found, or if
* we are not in a dynamic intro mode.
*/
public AbstractIntroPage getCurrentPage() {
if (!isDynamic())
return null;
AbstractIntroPage page = (AbstractIntroPage) findChild(currentPageId,
ABSTRACT_PAGE);
if (page != null)
return page;
// not a page. Test for root page.
if (currentPageId.equals(rootPage.getId()))
return rootPage;
// return null if page is not found.
return null;
}
@Override
public int getType() {
return AbstractIntroElement.MODEL_ROOT;
}
/**
* Assumes that the passed config element has a "content" attribute. Reads
* it and loads a DOM based on that attribute value. It does not explicitly
* resolve the resource because this method only loads the introContent and
* the configExt content files. ie: in plugin.xml. <br>
* This method also sets the base attribute on the root element in the DOM
* to enable resolving all resources relative to this DOM.
*
* @return
*/
protected Document loadDOM(IConfigurationElement cfgElement) {
String content = cfgElement.getAttribute(ATT_CONTENT);
// To support jarring, extract parent folder of where the intro content
// file is. It is expected that all intro content is in that one parent
// folder. This works for both content files and configExtension content
// files.
Bundle domBundle = BundleUtil
.getBundleFromConfigurationElement(cfgElement);
ModelUtil.ensureFileURLsExist(domBundle, content);
// Resolve.
content = BundleUtil.getResourceLocation(content, cfgElement);
Document document = new IntroContentParser(content).getDocument();
return document;
}
private String getBase(IConfigurationElement configElement) {
String content = configElement.getAttribute(ATT_CONTENT);
return ModelUtil.getParentFolderToString(content);
}
public String resolveVariables(String text) {
if (text==null) return null;
if (text.indexOf('$')== -1)
return text;
// resolve
boolean inVariable=false;
StringBuffer buf = new StringBuffer();
int vindex=0;
for (int i=0; i<text.length(); i++) {
char c = text.charAt(i);
if (c=='$') {
if (!inVariable) {
inVariable=true;
vindex=i+1;
continue;
}
inVariable=false;
String variable=text.substring(vindex, i);
String value = getVariableValue(variable);
if (value==null)
value = "$"+variable+"$"; //$NON-NLS-1$ //$NON-NLS-2$
buf.append(value);
continue;
}
else if (!inVariable)
buf.append(c);
}
return buf.toString();
}
private String getVariableValue(String variable) {
if (variable.equals(VAR_THEME)) {
if (theme!=null)
return theme.getPath();
}
if (variable.equals(FontSelection.VAR_FONT_STYLE)) {
return FontSelection.getFontStyle();
}
if (variable.equals(VAR_DIRECTION)) {
if (ProductPreferences.isRTL()) {
return "rtl"; //$NON-NLS-1$
} else {
return "ltr"; //$NON-NLS-1$
}
}
if (configurer!=null)
return configurer.getVariable(variable);
return null;
}
public String resolvePath(String extensionId, String path) {
if (configurer==null) return null;
return configurer.resolvePath(extensionId, path);
}
public IntroTheme getTheme() {
return theme;
}
public String getStartPageId() {
return startPageId;
}
private String getProcessPreference(String key, String pid) {
String result = Platform.getPreferencesService().getString
(IntroPlugin.PLUGIN_ID, pid + '_' + key, "", null); //$NON-NLS-1$
if (result.length() == 0) {
result = Platform.getPreferencesService().getString
(IntroPlugin.PLUGIN_ID, key, "", null); //$NON-NLS-1$
}
return result;
}
}