/******************************************************************************* * 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; } }