aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--eclipse/dictionary.txt14
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java5
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AndroidConstants.java3
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationComposite.java43
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java88
-rw-r--r--[-rwxr-xr-x]eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java159
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java886
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeOverlay.java111
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java77
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java11
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java31
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinderTest.java63
12 files changed, 1436 insertions, 55 deletions
diff --git a/eclipse/dictionary.txt b/eclipse/dictionary.txt
index 390a2c5..35ccfff 100644
--- a/eclipse/dictionary.txt
+++ b/eclipse/dictionary.txt
@@ -29,7 +29,10 @@ codebase
codename
codenames
combo
+combobox
+combos
config
+configs
configurability
congrats
coords
@@ -48,6 +51,7 @@ diff
diffs
dirs
dpi
+ed
editable
enum
enums
@@ -58,6 +62,7 @@ foo
fqcn
gen
groovy
+guava
hardcoded
hotfix
href
@@ -68,6 +73,7 @@ iff
img
infos
init
+inits
inline
instanceof
int
@@ -79,6 +85,7 @@ lifecycle
linestyle
linux
locale
+locales
logo
luminance
mac
@@ -87,6 +94,8 @@ marquee
metadata
min
multi
+multimap
+multimaps
namespace
namespaces
num
@@ -120,6 +129,7 @@ reparse
reparses
rescales
residual
+sans
scrollable
scrollbar
scrollbars
@@ -139,6 +149,7 @@ subclassing
submenu
supertype
syncs
+thematically
themed
tmp
tooltip
@@ -155,12 +166,15 @@ unset
upcoming
uri
url
+urls
validator
varargs
verbosity
vs
webtools
workflow
+xdpi
xml
xmlns
+ydpi
zipalign
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java
index 406e84d..5050afe 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java
@@ -23,6 +23,7 @@ import com.android.ide.common.sdk.LoadStatus;
import com.android.ide.eclipse.adt.internal.VersionCheck;
import com.android.ide.eclipse.adt.internal.editors.IconFactory;
import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder;
import com.android.ide.eclipse.adt.internal.editors.menu.MenuEditor;
import com.android.ide.eclipse.adt.internal.editors.resources.ResourcesEditor;
import com.android.ide.eclipse.adt.internal.editors.xml.XmlEditor;
@@ -249,6 +250,9 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger {
// initialize editors
startEditors();
+ // Listen on resource file edits for updates to file inclusion
+ IncludeFinder.start();
+
// Ping the usage server and parse the SDK content.
// This is deferred in separate jobs to avoid blocking the bundle start.
// We also serialize them to avoid too many parallel jobs when Eclipse starts.
@@ -282,6 +286,7 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger {
super.stop(context);
stopEditors();
+ IncludeFinder.stop();
mRed.dispose();
synchronized (AdtPlugin.class) {
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AndroidConstants.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AndroidConstants.java
index 9c31b11..3a6f3ff 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AndroidConstants.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AndroidConstants.java
@@ -133,6 +133,9 @@ public class AndroidConstants {
/** Absolute path of the resource folder, e.g. "/assets".<br> This is a workspace path. */
public final static String WS_ASSETS = WS_SEP + SdkConstants.FD_ASSETS;
+ /** Absolute path of the layout folder, e.g. "/res/layout".<br> This is a workspace path. */
+ public final static String WS_LAYOUTS = WS_RESOURCES + WS_SEP + SdkConstants.FD_LAYOUT;
+
/** Leaf of the javaDoc folder. Does not start with a separator. */
public final static String WS_JAVADOC_FOLDER_LEAF = SdkConstants.FD_DOCS + "/" + //$NON-NLS-1$
SdkConstants.FD_DOCS_REFERENCE;
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationComposite.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationComposite.java
index 76eeb7e..ee6aeca 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationComposite.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationComposite.java
@@ -65,7 +65,6 @@ import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
-import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -76,7 +75,7 @@ import java.util.SortedSet;
* A composite that displays the current configuration displayed in a Graphical Layout Editor.
* <p/>
* The composite has several entry points:<br>
- * - {@link #setFile(File)}<br>
+ * - {@link #setFile(IFile)}<br>
* Called after the constructor to set the file being edited. Nothing else is performed.<br>
*<br>
* - {@link #onXmlModelLoaded()}<br>
@@ -85,11 +84,11 @@ import java.util.SortedSet;
* to restore a configuration if one is found to have been saved in the file persistent storage.
* (see {@link #storeState()})<br>
*<br>
- * - {@link #replaceFile(File)}<br>
+ * - {@link #replaceFile(IFile)}<br>
* Called when a file, representing the same resource but with a different config is opened<br>
* by the user.<br>
*<br>
- * - {@link #changeFileOnNewConfig(FolderConfiguration)}<br>
+ * - {@link #changeFileOnNewConfig(IFile)}<br>
* Called when config change triggers the editing of a file with a different config.
*<p/>
* Additionally, the composite can handle the following events.<br>
@@ -99,7 +98,8 @@ import java.util.SortedSet;
*/
public class ConfigurationComposite extends Composite {
- private final static String CONFIG_STATE = "state"; //$NON-NLS-1$
+ public final static QualifiedName NAME_CONFIG_STATE =
+ new QualifiedName(AdtPlugin.PLUGIN_ID, "state");//$NON-NLS-1$
private final static String THEME_SEPARATOR = "----------"; //$NON-NLS-1$
private final static int LOCALE_LANG = 0;
@@ -147,7 +147,8 @@ public class ConfigurationComposite extends Composite {
private IAndroidTarget mRenderingTarget;
/** The {@link FolderConfiguration} being edited. */
private FolderConfiguration mEditedConfig;
-
+ /** Serialized state to use when initializing the configuration after the SDK is loaded */
+ private String mInitialState;
/**
* Interface implemented by the part which owns a {@link ConfigurationComposite}.
@@ -486,12 +487,14 @@ public class ConfigurationComposite extends Composite {
* buttons to display at the top of the composite. Can be empty or null.
* @param parent The parent composite.
* @param style The style of this composite.
+ * @param initialState The initial state (serialized form) to use for the configuration
*/
public ConfigurationComposite(IConfigListener listener,
CustomButton[][] customButtons,
- Composite parent, int style) {
+ Composite parent, int style, String initialState) {
super(parent, style);
mListener = listener;
+ mInitialState = initialState;
if (customButtons == null) {
customButtons = new CustomButton[0][0];
@@ -648,8 +651,8 @@ public class ConfigurationComposite extends Composite {
* @param file the file being opened
*
* @see #onXmlModelLoaded()
- * @see #replaceFile(FolderConfiguration)
- * @see #changeFileOnNewConfig(FolderConfiguration)
+ * @see #replaceFile(IFile)
+ * @see #changeFileOnNewConfig(IFile)
*/
public void setFile(IFile file) {
mEditedFile = file;
@@ -663,10 +666,6 @@ public class ConfigurationComposite extends Composite {
* <p/>This will NOT trigger a redraw event (will not call
* {@link IConfigListener#onConfigurationChange()}.)
* @param file the file being opened.
- * @param fileConfig The {@link FolderConfiguration} of the opened file.
- * @param target the {@link IAndroidTarget} of the file's project.
- *
- * @see #replaceFile(FolderConfiguration)
*/
public void replaceFile(IFile file) {
// if there is no previous selection, revert to default mode.
@@ -714,8 +713,7 @@ public class ConfigurationComposite extends Composite {
* Updates the UI with a new file that was opened in response to a config change.
* @param file the file being opened.
*
- * @see #openFile(FolderConfiguration, IAndroidTarget)
- * @see #replaceFile(FolderConfiguration)
+ * @see #replaceFile(IFile)
*/
public void changeFileOnNewConfig(IFile file) {
mEditedFile = file;
@@ -759,7 +757,7 @@ public class ConfigurationComposite extends Composite {
* <p>This initializes the UI, either with the first compatible configuration found,
* or attempts to restore a configuration if one is found to have been saved in the file
* persistent storage.
- * <p>If the SDK or target are not loaded, nothing will happend (but the method must be called
+ * <p>If the SDK or target are not loaded, nothing will happened (but the method must be called
* back when those are loaded).
* <p>The method automatically handles being called the first time after editor creation, or
* being called after during SDK/Target changes (as long as {@link #onSdkLoaded(IAndroidTarget)}
@@ -811,8 +809,12 @@ public class ConfigurationComposite extends Composite {
// get the file stored state
boolean loadedConfigData = false;
try {
- QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, CONFIG_STATE);
- String data = mEditedFile.getPersistentProperty(qname);
+ String data = mEditedFile.getPersistentProperty(NAME_CONFIG_STATE);
+
+ if (mInitialState != null) {
+ data = mInitialState;
+ mInitialState = null;
+ }
if (data != null) {
loadedConfigData = mState.setData(data);
}
@@ -1106,8 +1108,7 @@ public class ConfigurationComposite extends Composite {
*/
public void storeState() {
try {
- QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, CONFIG_STATE);
- mEditedFile.setPersistentProperty(qname, mState.getData());
+ mEditedFile.setPersistentProperty(NAME_CONFIG_STATE, mState.getData());
} catch (CoreException e) {
// pass
}
@@ -1711,7 +1712,7 @@ public class ConfigurationComposite extends Composite {
}
/**
- * fills the config combo with new values based on {@link #mCurrentState#device}.
+ * fills the config combo with new values based on {@link #mState}.device.
* @param refName an optional name. if set the selection will match this name (if found)
*/
private void fillConfigCombo(String refName) {
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java
index ca08209..8ccf557 100755
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java
@@ -25,14 +25,18 @@ import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+import org.eclipse.core.resources.IProject;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.ActionContributionItem;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IContributionItem;
+import org.eclipse.jface.action.IMenuCreator;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Menu;
import java.util.ArrayList;
import java.util.HashMap;
@@ -70,7 +74,7 @@ import java.util.regex.Pattern;
* Creates a new helper responsible for adding and managing the dynamic menu items
* contributed by the {@link IViewRule} instances, based on the current selection
* on the {@link LayoutCanvas}.
- *
+ * @param editor the editor owning the menu
* @param canvas The {@link LayoutCanvas} providing the selection, the node factory and
* the rules engine.
* @param rootMenu The root of the context menu displayed. In practice this may be the
@@ -201,6 +205,85 @@ import java.util.regex.Pattern;
}
}
}
+
+ insertShowIncludedMenu(endId);
+ }
+
+ /**
+ * Inserts a "Show Included In" context menu, if the current view is included in this
+ * view.
+ */
+ private void insertShowIncludedMenu(String beforeId) {
+ IProject project = mEditor.getProject();
+ String me = mCanvas.getLayoutResourceName();
+ final List<String> includedBy = IncludeFinder.get(project).getIncludedBy(me);
+
+ Action includeAction = new Action("Show Included In", IAction.AS_DROP_DOWN_MENU) {
+ @Override
+ public IMenuCreator getMenuCreator() {
+ return new IMenuCreator() {
+ private Menu mMenu;
+
+ public void dispose() {
+ if (mMenu != null) {
+ mMenu.dispose();
+ mMenu = null;
+ }
+ }
+
+ public Menu getMenu(Control parent) {
+ return null;
+ }
+
+ public Menu getMenu(Menu parent) {
+ mMenu = new Menu(parent);
+ if (includedBy != null && includedBy.size() > 0) {
+ for (final String s : includedBy) {
+ String title = s;
+ IAction action = new ShowWithinAction(title, s);
+ new ActionContributionItem(action).fill(mMenu, -1);
+ }
+ new Separator().fill(mMenu, -1);
+ }
+ IAction action = new ShowWithinAction("Nothing", null);
+ if (includedBy == null || includedBy.size() == 0) {
+ action.setEnabled(false);
+ }
+ new ActionContributionItem(action).fill(mMenu, -1);
+
+ return mMenu;
+ }
+
+ };
+ }
+ };
+ mMenuManager.insertBefore(beforeId, includeAction);
+ }
+
+ private class ShowWithinAction extends Action {
+ private String mId;
+
+ public ShowWithinAction(String title, String id) {
+ super(title, IAction.AS_RADIO_BUTTON);
+ mId = id;
+ }
+
+ @Override
+ public boolean isChecked() {
+ String within = mEditor.getGraphicalEditor().getIncludedWithinId();
+ if (within == null) {
+ return mId == null;
+ } else {
+ return within.equals(mId);
+ }
+ }
+
+ @Override
+ public void run() {
+ if (!isChecked()) {
+ mEditor.getGraphicalEditor().showIn(mId);
+ }
+ }
}
/**
@@ -273,6 +356,9 @@ import java.util.regex.Pattern;
/**
* Returns the menu actions computed by the rule associated with this view.
+ *
+ * @param vi the canvas view info we need menu actions for
+ * @return a list of {@link MenuAction} objects applicable to the view info
*/
public List<MenuAction> getMenuActions(CanvasViewInfo vi) {
if (vi == null) {
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java
index 04b268d..ce21726 100755..100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java
@@ -69,6 +69,7 @@ import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.draw2d.geometry.Rectangle;
@@ -156,6 +157,20 @@ public class GraphicalEditorPart extends EditorPart
* which all listen to each others indirectly.
*/
+ /**
+ * Session-property on files which specifies the initial config state to be used on
+ * this file
+ */
+ public final static QualifiedName NAME_INITIAL_STATE =
+ new QualifiedName(AdtPlugin.PLUGIN_ID, "initialstate");//$NON-NLS-1$
+
+ /**
+ * Session-property on files which specifies the inclusion-context (name of another layout
+ * which should be "including" this layout) when the file is opened
+ */
+ public final static QualifiedName NAME_INCLUDE =
+ new QualifiedName(AdtPlugin.PLUGIN_ID, "includer");//$NON-NLS-1$
+
/** Reference to the layout editor */
private final LayoutEditor mLayoutEditor;
@@ -184,6 +199,12 @@ public class GraphicalEditorPart extends EditorPart
/** Styled text displaying the most recent error in the error view. */
private StyledText mErrorLabel;
+ /**
+ * The resource name of a file that should surround this file (e.g. include this file
+ * visually), or null if not applicable
+ */
+ private String mIncludedWithinId;
+
private Map<String, Map<String, IResourceValue>> mConfiguredFrameworkRes;
private Map<String, Map<String, IResourceValue>> mConfiguredProjectRes;
private ProjectCallback mProjectCallback;
@@ -341,7 +362,25 @@ public class GraphicalEditorPart extends EditorPart
};
mConfigListener = new ConfigListener();
- mConfigComposite = new ConfigurationComposite(mConfigListener, customButtons, parent, SWT.BORDER);
+
+ // Check whether somebody has requested an initial state for the newly opened file.
+ // The initial state is a serialized version of the state compatible with
+ // {@link ConfigurationComposite#CONFIG_STATE}.
+ String initialState = null;
+ if (mEditedFile != null) {
+ try {
+ initialState = (String) mEditedFile.getSessionProperty(NAME_INITIAL_STATE);
+ if (initialState != null) {
+ // Only use once
+ mEditedFile.setSessionProperty(NAME_INITIAL_STATE, null);
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't read session property %1$s", NAME_INITIAL_STATE);
+ }
+ }
+
+ mConfigComposite = new ConfigurationComposite(mConfigListener, customButtons, parent,
+ SWT.BORDER, initialState);
mSashPalette = new SashForm(parent, SWT.HORIZONTAL);
mSashPalette.setLayoutData(new GridData(GridData.FILL_BOTH));
@@ -825,6 +864,28 @@ public class GraphicalEditorPart extends EditorPart
}
}
+ /**
+ * Returns the currently edited file
+ *
+ * @return the currently edited file, or null
+ */
+ public IFile getEditedFile() {
+ return mEditedFile;
+ }
+
+ /**
+ * Returns the project for the currently edited file, or null
+ *
+ * @return the project containing the edited file, or null
+ */
+ public IProject getProject() {
+ if (mEditedFile != null) {
+ return mEditedFile.getProject();
+ } else {
+ return null;
+ }
+ }
+
// ----------------
/**
@@ -920,6 +981,20 @@ public class GraphicalEditorPart extends EditorPart
mCanvasViewer.getCanvas().setRulesEngine(mRulesEngine);
}
}
+
+ // Pick up hand-off data: somebody requesting this file to be opened may have
+ // requested that it should be opened as included within another file
+ if (mEditedFile != null) {
+ try {
+ mIncludedWithinId = (String) mEditedFile.getSessionProperty(NAME_INCLUDE);
+ if (mIncludedWithinId != null) {
+ // Only use once
+ mEditedFile.setSessionProperty(NAME_INCLUDE, null);
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't access session property %1$s", NAME_INCLUDE);
+ }
+ }
}
/**
@@ -1315,10 +1390,12 @@ public class GraphicalEditorPart extends EditorPart
// Abort the rendering if the resources are not found.
if (configuredProjectRes == null) {
displayError("Missing project resources for current configuration.");
+ return null;
}
if (frameworkResources == null) {
displayError("Missing framework resources.");
+ return null;
}
// Lazily create the project callback the first time we need it
@@ -1368,6 +1445,7 @@ public class GraphicalEditorPart extends EditorPart
String theme = mConfigComposite.getTheme();
if (theme == null) {
displayError("Missing theme.");
+ return null;
}
if (mUseExplodeMode) {
@@ -1396,32 +1474,31 @@ public class GraphicalEditorPart extends EditorPart
IXmlPullParser topParser = modelParser;
// Code to support editing included layout
- // FIXME: refactor this somewhere else, and deal with edit workflow
- if (false) {
- // name of the top layout.
- String contextLayoutName = "includes";
- // find the layout file.
+ // Outer layout name:
+ String contextLayoutName = mIncludedWithinId;
+ if (contextLayoutName != null) {
+ // Find the layout file.
Map<String, IResourceValue> layouts = configuredProjectRes.get(
ResourceType.LAYOUT.getName());
-
IResourceValue contextLayout = layouts.get(contextLayoutName);
- File layoutFile = new File(contextLayout.getValue());
- if (layoutFile.isFile()) {
- try {
- // get the name of the layout actually being edited, without the extension
- // as it's what IXmlPullParser.getParser(String) will receive.
- String queryLayoutName = mEditedFile.getName().substring(
- 0, mEditedFile.getName().indexOf('.'));
-
- topParser = new ContextPullParser(queryLayoutName, modelParser);
- topParser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
- topParser.setInput(new FileReader(layoutFile));
- } catch (XmlPullParserException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (FileNotFoundException e) {
- // this will not happen since we check above.
+ if (contextLayout != null) {
+ String path = contextLayout.getValue();
+
+ File layoutFile = new File(path);
+ if (layoutFile.isFile()) {
+ try {
+ // Get the name of the layout actually being edited, without the extension
+ // as it's what IXmlPullParser.getParser(String) will receive.
+ String queryLayoutName = getLayoutResourceName();
+ topParser = new ContextPullParser(queryLayoutName, modelParser);
+ topParser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ topParser.setInput(new FileReader(layoutFile));
+ } catch (XmlPullParserException e) {
+ AdtPlugin.log(e, ""); //NON-NLS-1$
+ } catch (FileNotFoundException e) {
+ // this will not happen since we check above.
+ }
}
}
}
@@ -1464,6 +1541,20 @@ public class GraphicalEditorPart extends EditorPart
}
/**
+ * Returns the resource name of this layout, NOT including the @layout/ prefix
+ *
+ * @return the resource name of this layout, NOT including the @layout/ prefix
+ */
+ public String getLayoutResourceName() {
+ String name = mEditedFile.getName();
+ int dotIndex = name.indexOf('.');
+ if (dotIndex != -1) {
+ name = name.substring(0, dotIndex);
+ }
+ return name;
+ }
+
+ /**
* Cleans up when the rendering target is about to change
* @param oldTarget the old rendering target.
*/
@@ -1923,4 +2014,26 @@ public class GraphicalEditorPart extends EditorPart
}
}
+ /**
+ * Reopens this file as included within the given file (this assumes that the given
+ * file has an include tag referencing this view, and the set of views that have this
+ * property can be found using the {@link IncludeFinder}.
+ *
+ * @param relativePath project-relative path to the file to open as a surrounding context,
+ * or null to show the file standalone
+ */
+ public void showIn(String relativePath) {
+ mIncludedWithinId = relativePath;
+ recomputeLayout();
+ }
+
+ /**
+ * Returns the resource name of the file that is including this current layout, if any
+ * (may be null)
+ *
+ * @return the resource name of an including layout, or null
+ */
+ public String getIncludedWithinId() {
+ return mIncludedWithinId;
+ }
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java
new file mode 100644
index 0000000..f33c296
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java
@@ -0,0 +1,886 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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 com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.ide.eclipse.adt.AndroidConstants.EXT_XML;
+import static com.android.ide.eclipse.adt.AndroidConstants.WS_LAYOUTS;
+import static com.android.ide.eclipse.adt.AndroidConstants.WS_SEP;
+import static org.eclipse.core.resources.IResourceDelta.ADDED;
+import static org.eclipse.core.resources.IResourceDelta.CHANGED;
+import static org.eclipse.core.resources.IResourceDelta.CONTENT;
+import static org.eclipse.core.resources.IResourceDelta.REMOVED;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.resources.ResourceType;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResourceItem;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFile;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolder;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager.IResourceListener;
+import com.android.ide.eclipse.adt.io.IFileWrapper;
+import com.android.sdklib.SdkConstants;
+import com.android.sdklib.annotations.VisibleForTesting;
+import com.android.sdklib.io.StreamException;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringReader;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+/**
+ * The include finder finds other XML files that are including a given XML file, and does
+ * so efficiently (caching results across IDE sessions etc).
+ */
+public class IncludeFinder {
+ /** Qualified name for the per-project persistent property include-map */
+ private final static QualifiedName CONFIG_INCLUDES = new QualifiedName(AdtPlugin.PLUGIN_ID,
+ "includes");//$NON-NLS-1$
+
+ /**
+ * Qualified name for the per-project non-persistent property storing the
+ * {@link IncludeFinder} for this project
+ */
+ private final static QualifiedName INCLUDE_FINDER = new QualifiedName(AdtPlugin.PLUGIN_ID,
+ "finder"); //$NON-NLS-1$
+
+ /** Project that the include finder locates includes for */
+ private final IProject mProject;
+
+ /** Map from a layout resource name to a set of layouts included by the given resource */
+ private Map<String, List<String>> mIncludes = null;
+
+ /**
+ * Reverse map of {@link #mIncludes}; points to other layouts that are including a
+ * given layouts
+ */
+ private Map<String, List<String>> mIncludedBy = null;
+
+ /** Flag set during a refresh; ignore updates when this is true */
+ private static boolean sRefreshing;
+
+ /** Global (cross-project) resource listener */
+ private static ResourceListener sListener;
+
+ /**
+ * Constructs an {@link IncludeFinder} for the given project. Don't use this method;
+ * use the {@link #get} factory method instead.
+ *
+ * @param project project to create an {@link IncludeFinder} for
+ */
+ private IncludeFinder(IProject project) {
+ mProject = project;
+ }
+
+ /**
+ * Returns the {@link IncludeFinder} for the given project
+ *
+ * @param project the project the finder is associated with
+ * @return an {@IncludeFinder} for the given project, never null
+ */
+ public static IncludeFinder get(IProject project) {
+ IncludeFinder finder = null;
+ try {
+ finder = (IncludeFinder) project.getSessionProperty(INCLUDE_FINDER);
+ } catch (CoreException e) {
+ // Not a problem; we will just create a new one
+ }
+
+ if (finder == null) {
+ finder = new IncludeFinder(project);
+ try {
+ project.setSessionProperty(INCLUDE_FINDER, finder);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't store IncludeFinder");
+ }
+ }
+
+ return finder;
+ }
+
+ /**
+ * Returns a list of resource names that are included by the given resource
+ *
+ * @param includer the resource name to return included layouts for
+ * @return the layouts included by the given resource
+ */
+ public List<String> getIncludesFrom(String includer) {
+ ensureInitialized();
+
+ return mIncludes.get(includer);
+ }
+
+ /**
+ * Gets the list of all other layouts that are including the given layout
+ *
+ * @param included the file that is included
+ * @return the files that are including the given file, or null or empty
+ */
+ public List<String> getIncludedBy(String included) {
+ ensureInitialized();
+ return mIncludedBy.get(included);
+ }
+
+ /** Initialize the inclusion data structures, if not already done */
+ private void ensureInitialized() {
+ if (mIncludes == null) {
+ // Initialize
+ if (!readSettings()) {
+ // Couldn't read settings: probably the first time this code is running
+ // so there is no known data about includes.
+
+ // Yes, these should be multimaps! If we start using Guava replace
+ // these with multimaps.
+ mIncludes = new HashMap<String, List<String>>();
+ mIncludedBy = new HashMap<String, List<String>>();
+
+ scanProject();
+ saveSettings();
+ }
+ }
+ }
+
+ // ----- Persistence -----
+
+ /**
+ * Create a String serialization of the includes map. The map attempts to be compact;
+ * it strips out the @layout/ prefix, and eliminates the values for empty string
+ * values. The map can be restored by calling {@link #decodeMap}. The encoded String
+ * will have sorted keys.
+ *
+ * @param map the map to be serialized
+ * @return a serialization (never null) of the given map
+ */
+ @VisibleForTesting
+ public static String encodeMap(Map<String, List<String>> map) {
+ StringBuilder sb = new StringBuilder();
+
+ if (map != null) {
+ // Process the keys in sorted order rather than just
+ // iterating over the entry set to ensure stable output
+ List<String> keys = new ArrayList<String>(map.keySet());
+ Collections.sort(keys);
+ for (String key : keys) {
+ List<String> values = map.get(key);
+
+ if (sb.length() > 0) {
+ sb.append(',');
+ }
+ sb.append(key);
+ if (values.size() > 0) {
+ sb.append('=').append('>');
+ sb.append('{');
+ boolean first = true;
+ for (String value : values) {
+ if (first) {
+ first = false;
+ } else {
+ sb.append(',');
+ }
+ sb.append(value);
+ }
+ sb.append('}');
+ }
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Decodes the encoding (produced by {@link #encodeMap}) back into the original map,
+ * modulo any key sorting differences.
+ *
+ * @param encoded an encoding of a map created by {@link #encodeMap}
+ * @return a map corresponding to the encoded values, never null
+ */
+ @VisibleForTesting
+ public static Map<String, List<String>> decodeMap(String encoded) {
+ HashMap<String, List<String>> map = new HashMap<String, List<String>>();
+
+ if (encoded.length() > 0) {
+ int i = 0;
+ int end = encoded.length();
+
+ while (i < end) {
+
+ // Find key range
+ int keyBegin = i;
+ int keyEnd = i;
+ while (i < end) {
+ char c = encoded.charAt(i);
+ if (c == ',') {
+ break;
+ } else if (c == '=') {
+ i += 2; // Skip =>
+ break;
+ }
+ i++;
+ keyEnd = i;
+ }
+
+ List<String> values = new ArrayList<String>();
+ // Find values
+ if (i < end && encoded.charAt(i) == '{') {
+ i++;
+ while (i < end) {
+ int valueBegin = i;
+ int valueEnd = i;
+ char c = 0;
+ while (i < end) {
+ c = encoded.charAt(i);
+ if (c == ',' || c == '}') {
+ valueEnd = i;
+ break;
+ }
+ i++;
+ }
+ if (valueEnd > valueBegin) {
+ values.add(encoded.substring(valueBegin, valueEnd));
+ }
+
+ if (c == '}') {
+ break;
+ }
+ assert c == ',';
+ i++;
+ }
+ }
+
+ String key = encoded.substring(keyBegin, keyEnd);
+ map.put(key, values);
+ i++;
+ }
+ }
+
+ return map;
+ }
+
+ /**
+ * Stores the settings in the persistent project storage.
+ */
+ private void saveSettings() {
+ // Serialize the mIncludes map into a compact String. The mIncludedBy map can be
+ // inferred from it.
+ String encoded = encodeMap(mIncludes);
+
+ try {
+ if (encoded.length() >= 2048) {
+ // The maximum length of a setting key is 2KB, according to the javadoc
+ // for the project class. It's unlikely that we'll
+ // hit this -- even with an average layout root name of 20 characters
+ // we can still store over a hundred names. But JUST IN CASE we run
+ // into this, we'll clear out the key in this name which means that the
+ // information will need to be recomputed in the next IDE session.
+ mProject.setPersistentProperty(CONFIG_INCLUDES, null);
+ } else {
+ String existing = mProject.getPersistentProperty(CONFIG_INCLUDES);
+ if (!encoded.equals(existing)) {
+ mProject.setPersistentProperty(CONFIG_INCLUDES, encoded);
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't store include settings");
+ }
+ }
+
+ /**
+ * Reads previously stored settings from the persistent project storage
+ *
+ * @return true iff settings were restored from the project
+ */
+ private boolean readSettings() {
+ try {
+ String encoded = mProject.getPersistentProperty(CONFIG_INCLUDES);
+ if (encoded != null) {
+ mIncludes = decodeMap(encoded);
+
+ // Set up a reverse map, pointing from included files to the files that
+ // included them
+ mIncludedBy = new HashMap<String, List<String>>(2 * mIncludes.size());
+ for (Map.Entry<String, List<String>> entry : mIncludes.entrySet()) {
+ // File containing the <include>
+ String includer = entry.getKey();
+ // Files being <include>'ed by the above file
+ List<String> included = entry.getValue();
+ setIncludedBy(includer, included);
+ }
+
+ return true;
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't read include settings");
+ }
+
+ return false;
+ }
+
+ // ----- File scanning -----
+
+ /**
+ * Scan the whole project for XML layout resources that are performing includes.
+ */
+ private void scanProject() {
+ ProjectResources resources = ResourceManager.getInstance().getProjectResources(mProject);
+ if (resources != null) {
+ ProjectResourceItem[] layouts = resources.getResources(ResourceType.LAYOUT);
+ for (ProjectResourceItem layout : layouts) {
+ List<ResourceFile> sources = layout.getSourceFileList();
+ for (ResourceFile source : sources) {
+ updateFileIncludes(source, false);
+ }
+ }
+
+ return;
+ }
+ }
+
+ /**
+ * Scans the given {@link ResourceFile} and if it is a layout resource, updates the
+ * includes in it.
+ *
+ * @param resourceFile the {@link ResourceFile} to be scanned for includes
+ * @param singleUpdate true if this is a single file being updated, false otherwise
+ * (e.g. during initial project scanning)
+ * @return true if we updated the includes for the resource file
+ */
+ @SuppressWarnings("restriction")
+ private boolean updateFileIncludes(ResourceFile resourceFile, boolean singleUpdate) {
+ String folderName = resourceFile.getFolder().getFolder().getName();
+ if (!folderName.equals(SdkConstants.FD_LAYOUT)) {
+ // For now we only track layouts in the main layout/ folder;
+ // consider merging the various configurations and doing something
+ // clever in Show Include.
+ return false;
+ }
+
+ ResourceType[] resourceTypes = resourceFile.getResourceTypes();
+ for (ResourceType type : resourceTypes) {
+ if (type == ResourceType.LAYOUT) {
+ ensureInitialized();
+
+ String name = resourceFile.getFile().getName();
+ int baseEnd = name.length() - EXT_XML.length() - 1; // -1: the dot
+ if (baseEnd > 0) {
+ name = name.substring(0, baseEnd);
+ }
+
+ List<String> includes = Collections.emptyList();
+ if (resourceFile.getFile() instanceof IFileWrapper) {
+ IFile file = ((IFileWrapper) resourceFile.getFile()).getIFile();
+
+ // See if we have an existing XML model for this file; if so, we can
+ // just look directly at the parse tree
+ boolean hadXmlModel = false;
+ IStructuredModel model = null;
+ try {
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ model = modelManager.getExistingModelForEdit(file);
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ Document document = domModel.getDocument();
+ includes = findIncludesInDocument(document);
+ hadXmlModel = true;
+ }
+ } finally {
+ if (model != null) {
+ model.releaseFromRead();
+ }
+ }
+
+ // If no XML model we have to read the XML contents and (possibly)
+ // parse it
+ if (!hadXmlModel) {
+ String xml = readFile(file);
+ includes = findIncludes(xml);
+ }
+ } else {
+ String xml = readFile(resourceFile);
+ includes = findIncludes(xml);
+ }
+
+ if (includes.equals(getIncludesFrom(name))) {
+ // Common case -- so avoid doing settings flush etc
+ return false;
+ }
+
+ boolean detectCycles = singleUpdate;
+ setIncluded(name, includes, detectCycles);
+
+ if (singleUpdate) {
+ saveSettings();
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Finds the list of includes in the given XML content. It attempts quickly return
+ * empty if the file does not include any include tags; it does this by only parsing
+ * if it detects the string &lt;include in the file.
+ */
+ private List<String> findIncludes(String xml) {
+ int index = xml.indexOf("<include"); //NON-NLS-1$
+ if (index != -1) {
+ return findIncludesInXml(xml);
+ }
+
+ return Collections.emptyList();
+ }
+
+ /**
+ * Parses the given XML content and extracts all the included URLs and returns them
+ *
+ * @param xml layout XML content to be parsed for includes
+ * @return a list of included urls, or null
+ */
+ private List<String> findIncludesInXml(String xml) {
+ Document document = null;
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ InputSource is = new InputSource(new StringReader(xml));
+ try {
+ factory.setNamespaceAware(true);
+ factory.setValidating(false);
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ document = builder.parse(is);
+
+ return findIncludesInDocument(document);
+
+ } catch (ParserConfigurationException e) {
+ // pass -- ignore files we can't parse
+ } catch (SAXException e) {
+ // pass -- ignore files we can't parse
+ } catch (IOException e) {
+ // pass -- ignore files we can't parse
+ }
+
+ return Collections.emptyList();
+ }
+
+ /** Searches the given DOM document and returns the list of includes, if any */
+ private List<String> findIncludesInDocument(Document document) {
+ NodeList includes = document.getElementsByTagName(LayoutDescriptors.VIEW_INCLUDE);
+ if (includes.getLength() > 0) {
+ List<String> urls = new ArrayList<String>();
+ for (int i = 0; i < includes.getLength(); i++) {
+ Element element = (Element) includes.item(i);
+ String url = element.getAttribute(LayoutDescriptors.ATTR_LAYOUT);
+ if (url.length() > 0) {
+ String resourceName = urlToLocalResource(url);
+ if (resourceName != null) {
+ urls.add(resourceName);
+ }
+ }
+ }
+
+ return urls;
+ }
+
+ return Collections.emptyList();
+ }
+
+ /**
+ * Returns the layout URL to a local resource name (provided the URL is a local
+ * resource, not something in @android etc.) Returns null otherwise.
+ */
+ private static String urlToLocalResource(String url) {
+ if (!url.startsWith("@")) { //$NON-NLS-1$
+ return null;
+ }
+ int typeEnd = url.indexOf('/', 1);
+ if (typeEnd == -1) {
+ return null;
+ }
+ int nameBegin = typeEnd + 1;
+ int typeBegin = 1;
+ int colon = url.lastIndexOf(':', typeEnd);
+ if (colon != -1) {
+ String packageName = url.substring(typeBegin, colon);
+ if ("android".equals(packageName)) { //$NON-NLS-1$
+ // Don't want to point to non-local resources
+ return null;
+ }
+
+ typeBegin = colon + 1;
+ assert "layout".equals(url.substring(typeBegin, typeEnd)); //NON-NLS-1$
+ }
+
+ return url.substring(nameBegin);
+ }
+
+ /**
+ * Record the list of included layouts from the given layout
+ *
+ * @param includer the layout including other layouts
+ * @param included the layouts that were included by the including layout
+ * @param detectCycles if true, check for cycles and report them as project errors
+ */
+ @VisibleForTesting
+ /* package */ void setIncluded(String includer, List<String> included, boolean detectCycles) {
+ // Remove previously linked inverse mappings
+ List<String> oldIncludes = mIncludes.get(includer);
+ if (oldIncludes != null && oldIncludes.size() > 0) {
+ for (String includee : oldIncludes) {
+ List<String> includers = mIncludedBy.get(includee);
+ if (includers != null) {
+ includers.remove(includer);
+ }
+ }
+ }
+
+ mIncludes.put(includer, included);
+ // Reverse mapping: for included items, point back to including file
+ setIncludedBy(includer, included);
+
+ if (detectCycles) {
+ detectCycles(includer);
+ }
+ }
+
+ /** Record the list of included layouts from the given layout */
+ private void setIncludedBy(String includer, List<String> included) {
+ for (String target : included) {
+ List<String> list = mIncludedBy.get(target);
+ if (list == null) {
+ list = new ArrayList<String>(2); // We don't expect many includes
+ mIncludedBy.put(target, list);
+ }
+ if (!list.contains(includer)) {
+ list.add(includer);
+ }
+ }
+ }
+
+ /** Start listening on project resources */
+ public static void start() {
+ assert sListener == null;
+ sListener = new ResourceListener();
+ ResourceManager.getInstance().addListener(sListener);
+ }
+
+ public static void stop() {
+ assert sListener != null;
+ ResourceManager.getInstance().addListener(sListener);
+ }
+
+ /** Listener of resource file saves, used to update layout inclusion data structures */
+ private static class ResourceListener implements IResourceListener {
+ public void fileChanged(IProject project, ResourceFile file, int eventType) {
+ if (sRefreshing) {
+ return;
+ }
+
+ if ((eventType & (CHANGED | ADDED | REMOVED | CONTENT)) == 0) {
+ return;
+ }
+
+ IncludeFinder finder = get(project);
+ if (finder != null) {
+ if (finder.updateFileIncludes(file, true)) {
+ finder.saveSettings();
+ }
+ }
+ }
+
+ public void folderChanged(IProject project, ResourceFolder folder, int eventType) {
+ // We only care about layout resource files
+ }
+ }
+
+ // ----- I/O Utilities -----
+
+ /** Reads the contents of an {@link IFile} and return it as a String */
+ private static String readFile(IFile file) {
+ InputStream contents = null;
+ try {
+ contents = file.getContents();
+ String charset = file.getCharset();
+ return readFile(new InputStreamReader(contents, charset));
+ } catch (CoreException e) {
+ // pass -- ignore files we can't read
+ } catch (UnsupportedEncodingException e) {
+ // pass -- ignore files we can't read
+ } finally {
+ try {
+ if (contents != null) {
+ contents.close();
+ }
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Can't read layout file"); //NON-NLS-1$
+ }
+ }
+
+ return null;
+ }
+
+ /** Reads the contents of a {@link ResourceFile} and returns it as a String */
+ private static String readFile(ResourceFile file) {
+ InputStream contents = null;
+ try {
+ contents = file.getFile().getContents();
+ return readFile(new InputStreamReader(contents));
+ } catch (StreamException e) {
+ // pass -- ignore files we can't read
+ } finally {
+ try {
+ if (contents != null) {
+ contents.close();
+ }
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Can't read layout file"); //NON-NLS-1$
+ }
+ }
+
+ return null;
+ }
+
+ /** Reads the contents of an {@link InputStreamReader} and return it as a String */
+ private static String readFile(InputStreamReader is) {
+ BufferedReader reader = null;
+ try {
+ reader = new BufferedReader(is);
+ StringBuilder sb = new StringBuilder(2000);
+ while (true) {
+ int c = reader.read();
+ if (c == -1) {
+ return sb.toString();
+ } else {
+ sb.append((char) c);
+ }
+ }
+ } catch (IOException e) {
+ // pass -- ignore files we can't read
+ } finally {
+ try {
+ if (reader != null) {
+ reader.close();
+ }
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Can't read layout file"); //NON-NLS-1$
+ }
+ }
+
+ return null;
+ }
+
+ // ----- Cycle detection -----
+
+ private void detectCycles(String from) {
+ // Perform DFS on the include graph and look for a cycle; if we find one, produce
+ // a chain of includes on the way back to show to the user
+ if (mIncludes.size() > 0) {
+ Set<String> seen = new HashSet<String>(mIncludes.size());
+ String chain = dfs(from, seen);
+ if (chain != null) {
+ addError(from, chain);
+ } else {
+ // Is there an existing error for us to clean up?
+ removeErrors(from);
+ }
+ }
+ }
+
+ /** Format to chain include cycles in: a=>b=>c=>d etc */
+ private final String CHAIN_FORMAT = "%1$s=>%2$s"; //NON-NLS-1$
+
+ private String dfs(String from, Set<String> seen) {
+ seen.add(from);
+
+ List<String> includes = mIncludes.get(from);
+ if (includes != null && includes.size() > 0) {
+ for (String include : includes) {
+ if (seen.contains(include)) {
+ return String.format(CHAIN_FORMAT, from, include);
+ }
+ String chain = dfs(include, seen);
+ if (chain != null) {
+ return String.format(CHAIN_FORMAT, from, chain);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private void removeErrors(String from) {
+ final IResource resource = findResource(from);
+ if (resource != null) {
+ try {
+ final String markerId = IMarker.PROBLEM;
+
+ IMarker[] markers = resource.findMarkers(markerId, true, IResource.DEPTH_ZERO);
+
+ for (final IMarker marker : markers) {
+ String tmpMsg = marker.getAttribute(IMarker.MESSAGE, null);
+ if (tmpMsg == null || tmpMsg.startsWith(MESSAGE)) {
+ // Remove
+ runLater(new Runnable() {
+ public void run() {
+ try {
+ sRefreshing = true;
+ marker.delete();
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't delete problem marker");
+ } finally {
+ sRefreshing = false;
+ }
+ }
+ });
+ }
+ }
+ } catch (CoreException e) {
+ // if we couldn't get the markers, then we just mark the file again
+ // (since markerAlreadyExists is initialized to false, we do nothing)
+ }
+ }
+ }
+
+ /** Error message for cycles */
+ private static final String MESSAGE = "Found cyclical <include> chain";
+
+ private void addError(String from, String chain) {
+ final IResource resource = findResource(from);
+ if (resource != null) {
+ final String markerId = IMarker.PROBLEM;
+ final String message = String.format("%1$s: %2$s", MESSAGE, chain);
+ final int lineNumber = 1;
+ final int severity = IMarker.SEVERITY_ERROR;
+
+ // check if there's a similar marker already, since aapt is launched twice
+ boolean markerAlreadyExists = false;
+ try {
+ IMarker[] markers = resource.findMarkers(markerId, true, IResource.DEPTH_ZERO);
+
+ for (IMarker marker : markers) {
+ int tmpLine = marker.getAttribute(IMarker.LINE_NUMBER, -1);
+ if (tmpLine != lineNumber) {
+ break;
+ }
+
+ int tmpSeverity = marker.getAttribute(IMarker.SEVERITY, -1);
+ if (tmpSeverity != severity) {
+ break;
+ }
+
+ String tmpMsg = marker.getAttribute(IMarker.MESSAGE, null);
+ if (tmpMsg == null || tmpMsg.equals(message) == false) {
+ break;
+ }
+
+ // if we're here, all the marker attributes are equals, we found it
+ // and exit
+ markerAlreadyExists = true;
+ break;
+ }
+
+ } catch (CoreException e) {
+ // if we couldn't get the markers, then we just mark the file again
+ // (since markerAlreadyExists is initialized to false, we do nothing)
+ }
+
+ if (!markerAlreadyExists) {
+ runLater(new Runnable() {
+ public void run() {
+ try {
+ sRefreshing = true;
+
+ // Adding a resource will force a refresh on the file;
+ // ignore these updates
+ BaseProjectHelper.markResource(resource, markerId, message, lineNumber,
+ severity);
+ } finally {
+ sRefreshing = false;
+ }
+ }
+ });
+ }
+ }
+ }
+
+ // FIXME: Find more standard Eclipse way to do this.
+ // We need to run marker registration/deletion "later", because when the include
+ // scanning is running it's in the middle of resource notification, so the IDE
+ // throws an exception
+ private static void runLater(Runnable runnable) {
+ Display display = Display.findDisplay(Thread.currentThread());
+ if (display != null) {
+ display.asyncExec(runnable);
+ } else {
+ AdtPlugin.log(IStatus.WARNING, "Could not find display");
+ }
+ }
+
+ /**
+ * Finds the project resource for the given layout path
+ *
+ * @param from the resource name
+ * @return the {@link IResource}, or null if not found
+ */
+ private IResource findResource(String from) {
+ final IResource resource = mProject.findMember(WS_LAYOUTS + WS_SEP + from + '.' + EXT_XML);
+ return resource;
+ }
+
+ /**
+ * Creates a blank, project-less {@link IncludeFinder} <b>for use by unit tests
+ * only</b>
+ */
+ @VisibleForTesting
+ /* package */ static IncludeFinder create() {
+ IncludeFinder finder = new IncludeFinder(null);
+ finder.mIncludes = new HashMap<String, List<String>>();
+ finder.mIncludedBy = new HashMap<String, List<String>>();
+ return finder;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeOverlay.java
new file mode 100644
index 0000000..7b95e70
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeOverlay.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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 com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Rectangle;
+
+import java.util.List;
+
+/**
+ * The {@link IncludeOverlay} class renders masks to -partially- hide everything outside
+ * an included file's own content. This overlay is in use when you are editing an included
+ * file shown within a different file's context (e.g. "Show In > other").
+ */
+public class IncludeOverlay extends Overlay {
+ /** Mask transparency - 0 is transparent, 255 is opaque */
+ private static final int MASK_TRANSPARENCY = 208;
+
+ /** The associated {@link LayoutCanvas}. */
+ private LayoutCanvas mCanvas;
+
+ /**
+ * Constructs an {@link IncludeOverlay} tied to the given canvas.
+ *
+ * @param canvas The {@link LayoutCanvas} to paint the overlay over.
+ */
+ public IncludeOverlay(LayoutCanvas canvas) {
+ this.mCanvas = canvas;
+ }
+
+ @Override
+ public void paint(GC gc) {
+ List<CanvasViewInfo> included = mCanvas.getViewHierarchy().getIncluded();
+ if (included == null || included.size() != 1) {
+ // We don't support multiple included children yet. When that works,
+ // this code should use a BSP tree to figure out which regions to paint
+ // to leave holes in the mask.
+ return;
+ }
+
+ Image image = mCanvas.getImageOverlay().getImage();
+ if (image == null) {
+ return;
+ }
+ ImageData data = image.getImageData();
+
+ Rectangle hole = included.get(0).getAbsRect();
+
+ int oldAlpha = gc.getAlpha();
+ gc.setAlpha(MASK_TRANSPARENCY);
+ Color bg = gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
+ gc.setBackground(bg);
+
+ ControlPoint topLeft = LayoutPoint.create(mCanvas, hole.x, hole.y).toControl();
+ ControlPoint bottomRight = LayoutPoint.create(mCanvas, hole.x + hole.width,
+ hole.y + hole.height).toControl();
+ CanvasTransform hi = mCanvas.getHorizontalTransform();
+ CanvasTransform vi = mCanvas.getVerticalTransform();
+ int deltaX = hi.translate(0);
+ int deltaY = vi.translate(0);
+ int x1 = topLeft.x;
+ int y1 = topLeft.y;
+ int x2 = bottomRight.x;
+ int y2 = bottomRight.y;
+ int width = data.width;
+ int height = data.height;
+
+ width = hi.getScalledImgSize();
+ height = vi.getScalledImgSize();
+
+ if (y1 > deltaX) {
+ // Top
+ gc.fillRectangle(deltaX, deltaY, width, y1 - deltaY);
+ }
+
+ if (y2 < height) {
+ // Bottom
+ gc.fillRectangle(deltaX, y2, width, height - y2 + deltaY);
+ }
+
+ if (x1 > deltaX) {
+ // Left
+ gc.fillRectangle(deltaX, y1, x1 - deltaX, y2 - y1);
+ }
+
+ if (x2 < width) {
+ // Right
+ gc.fillRectangle(x2, y1, width - x2 + deltaX, y2 - y1);
+ }
+
+ gc.setAlpha(oldAlpha);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java
index b431f07..02f26aa 100755
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java
@@ -22,6 +22,7 @@ import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite;
import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
@@ -36,10 +37,13 @@ import com.android.sdklib.SdkConstants;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
+import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.ActionContributionItem;
@@ -74,6 +78,7 @@ import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.ui.IActionBars;
+import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PartInitException;
@@ -195,6 +200,9 @@ class LayoutCanvas extends Canvas {
/** The overlay which paints the rendered layout image. */
private ImageOverlay mImageOverlay;
+ /** The overlay which paints masks hiding everything but included content. */
+ private IncludeOverlay mIncludeOverlay;
+
/**
* Gesture Manager responsible for identifying mouse, keyboard and drag and
* drop events.
@@ -230,6 +238,7 @@ class LayoutCanvas extends Canvas {
mSelectionOverlay = new SelectionOverlay();
mSelectionOverlay.create(display);
mImageOverlay = new ImageOverlay(this, mHScale, mVScale);
+ mIncludeOverlay = new IncludeOverlay(this);
mImageOverlay.create(display);
// --- Set up listeners
@@ -345,6 +354,11 @@ class LayoutCanvas extends Canvas {
mImageOverlay = null;
}
+ if (mIncludeOverlay != null) {
+ mIncludeOverlay.dispose();
+ mIncludeOverlay = null;
+ }
+
mViewHierarchy.dispose();
}
@@ -582,6 +596,8 @@ class LayoutCanvas extends Canvas {
}
mHoverOverlay.paint(gc);
+ mIncludeOverlay.paint(gc);
+
mSelectionOverlay.paint(mSelectionManager, mGCWrapper, mRulesEngine);
mGestureManager.paint(gc);
@@ -726,11 +742,40 @@ class LayoutCanvas extends Canvas {
if (workspacePath.isPrefixOf(filePath)) {
IPath relativePath = Sdk.makeRelativeTo(filePath, workspacePath);
IResource xmlFile = workspace.findMember(relativePath);
- try {
- EditorUtility.openInEditor(xmlFile, true);
- return;
- } catch (PartInitException ex) {
- AdtPlugin.log(ex, "Can't open %$1s", url); //$NON-NLS-1$
+ if (xmlFile != null) {
+ String nextName = getLayoutResourceName();
+ IFile leavingFile = graphicalEditor.getEditedFile();
+ try {
+ // TODO - only consider this if we're going to open a new file...
+ // And even then, whether the target version actually needs it...
+ QualifiedName qname = ConfigurationComposite.NAME_CONFIG_STATE;
+ String state = leavingFile.getPersistentProperty(qname);
+ xmlFile.setSessionProperty(GraphicalEditorPart.NAME_INITIAL_STATE, state);
+ } catch (CoreException e) {
+ // pass
+ }
+
+ try {
+ IEditorPart openAlready = EditorUtility.isOpenInEditor(xmlFile);
+ if (openAlready != null) {
+ if (openAlready instanceof LayoutEditor) {
+ LayoutEditor editor = (LayoutEditor)openAlready;
+ GraphicalEditorPart gEditor = editor.getGraphicalEditor();
+ gEditor.showIn(nextName);
+ }
+ } else {
+ try {
+ xmlFile.setSessionProperty(GraphicalEditorPart.NAME_INCLUDE, nextName);
+ } catch (CoreException e) {
+ // pass - worst that can happen is that we don't start with inclusion
+ }
+ }
+
+ EditorUtility.openInEditor(xmlFile, true);
+ return;
+ } catch (PartInitException ex) {
+ AdtPlugin.log(ex, "Can't open %$1s", url); //$NON-NLS-1$
+ }
}
} else {
// It's not a path in the workspace; look externally
@@ -758,6 +803,28 @@ class LayoutCanvas extends Canvas {
}
/**
+ * Returns the layout resource name of this layout
+ * @return
+ */
+ public String getLayoutResourceName() {
+ GraphicalEditorPart graphicalEditor = mLayoutEditor.getGraphicalEditor();
+ return graphicalEditor.getLayoutResourceName();
+ }
+
+ /**
+ * Returns the layout resource url of the current layout
+ *
+ * @return
+ */
+ /*
+ public String getMe() {
+ GraphicalEditorPart graphicalEditor = mLayoutEditor.getGraphicalEditor();
+ IFile editedFile = graphicalEditor.getEditedFile();
+ return editedFile.getProjectRelativePath().toOSString();
+ }
+ */
+
+ /**
* Show the XML element corresponding to the given {@link CanvasViewInfo} (unless it's
* a root).
*
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java
index 939d1f4..c3581c4 100755
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java
@@ -204,6 +204,8 @@ public class OutlinePage extends ContentOutlinePage
Object[] expanded = tv.getExpandedElements();
tv.refresh();
tv.setExpandedElements(expanded);
+ // Ensure that the root is expanded
+ tv.expandToLevel(rootViewInfo, 2);
}
}
@@ -343,7 +345,7 @@ public class OutlinePage extends ContentOutlinePage
* Label provider for the Outline model.
* Objects are going to be {@link CanvasViewInfo}.
*/
- private static class LabelProvider implements ILabelProvider {
+ private class LabelProvider implements ILabelProvider {
/**
* Returns the element's logo with a fallback on the android logo.
@@ -375,13 +377,18 @@ public class OutlinePage extends ContentOutlinePage
* Uses UiElementNode.shortDescription for the label for this tree item.
*/
public String getText(Object element) {
+ CanvasViewInfo vi = null;
if (element instanceof CanvasViewInfo) {
- element = ((CanvasViewInfo) element).getUiViewNode();
+ vi = (CanvasViewInfo) element;
+ element = vi.getUiViewNode();
}
if (element instanceof UiElementNode) {
UiElementNode node = (UiElementNode) element;
return node.getShortDescription();
+ } else if (element == null && vi != null) {
+ // It's an inclusion-context
+ return mGraphicalEditorPart.getIncludedWithinId();
}
return element == null ? "(null)" : element.toString(); //$NON-NLS-1$
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java
index 2278cfd..1521152 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java
@@ -94,6 +94,12 @@ public class ViewHierarchy {
*/
private boolean mExplodedParents;
+ /**
+ * List of included view infos in the current view hierarchy.
+ */
+ private List<CanvasViewInfo> mIncluded;
+
+ /** The layout scene for the current view hierarchy */
private LayoutScene mScene;
/**
@@ -131,6 +137,7 @@ public class ViewHierarchy {
mScene = scene;
mIsResultValid = (scene != null && scene.getResult().isSuccess());
mExplodedParents = false;
+ mIncluded = null;
if (mIsResultValid && scene != null) {
ViewInfo root = scene.getRootView();
@@ -140,7 +147,7 @@ public class ViewHierarchy {
mLastValidViewInfoRoot = new CanvasViewInfo(scene.getRootView());
}
- updateNodeProxies(mLastValidViewInfoRoot);
+ updateNodeProxies(mLastValidViewInfoRoot, null);
// Update the data structures related to tracking invisible and exploded nodes.
// We need to find the {@link CanvasViewInfo} objects that correspond to
@@ -166,7 +173,7 @@ public class ViewHierarchy {
* This is a recursive call that updates the whole hierarchy starting at the given
* view info.
*/
- private void updateNodeProxies(CanvasViewInfo vi) {
+ private void updateNodeProxies(CanvasViewInfo vi, UiViewElementNode parentKey) {
if (vi == null) {
return;
}
@@ -175,10 +182,18 @@ public class ViewHierarchy {
if (key != null) {
mCanvas.getNodeFactory().create(vi);
+
+ if (key != null && parentKey == null && vi.getParent() != null) {
+ // This is an included view root
+ if (mIncluded == null) {
+ mIncluded = new ArrayList<CanvasViewInfo>();
+ }
+ mIncluded.add(vi);
+ }
}
for (CanvasViewInfo child : vi.getChildren()) {
- updateNodeProxies(child);
+ updateNodeProxies(child, key);
}
}
@@ -579,4 +594,14 @@ public class ViewHierarchy {
return nodes;
}
+
+ /**
+ * Returns the list of included views in the current view hierarchy. Can be null
+ * when there are no included views.
+ *
+ * @return a list of included views, or null
+ */
+ public List<CanvasViewInfo> getIncluded() {
+ return mIncluded;
+ }
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinderTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinderTest.java
new file mode 100644
index 0000000..4c0f83e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinderTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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 com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import junit.framework.TestCase;
+
+public class IncludeFinderTest extends TestCase {
+ public void testEncodeDecode1() throws Exception {
+ // Test ending with just a key
+ String s = "bar,baz,foo";
+ assertEquals(s, IncludeFinder.encodeMap(IncludeFinder.decodeMap(s)));
+ }
+
+ public void testDecode1() throws Exception {
+ // Test ending with just a key
+ String s = "foo";
+ assertTrue(IncludeFinder.decodeMap(s).containsKey("foo"));
+ assertEquals(0, IncludeFinder.decodeMap(s).get("foo").size());
+ }
+
+ public void testDecode2() throws Exception {
+ // Test ending with just a key
+ String s = "foo=>{bar,baz}";
+ assertTrue(IncludeFinder.decodeMap(s).containsKey("foo"));
+ assertEquals("[bar, baz]",
+ IncludeFinder.decodeMap(s).get("foo").toString());
+ }
+
+ public void testEncodeDecode2() throws Exception {
+ // Test ending with just a key
+ String s = "bar,key1=>{value1,value2},key2=>{value3,value4}";
+ assertEquals(s, IncludeFinder.encodeMap(IncludeFinder.decodeMap(s)));
+ }
+
+ public void testUpdates() throws Exception {
+ IncludeFinder finder = IncludeFinder.create();
+ assertEquals(null, finder.getIncludedBy("foo"));
+
+ finder.setIncluded("bar", Arrays.<String>asList("foo", "baz"), false);
+ finder.setIncluded("baz", Arrays.<String>asList("foo"), false);
+ assertEquals(Arrays.asList("bar", "baz"), finder.getIncludedBy("foo"));
+ finder.setIncluded("bar", Collections.<String>emptyList(), false);
+ assertEquals(Arrays.asList("baz"), finder.getIncludedBy("foo"));
+ finder.setIncluded("baz", Collections.<String>emptyList(), false);
+ assertEquals(Collections.emptyList(), finder.getIncludedBy("foo"));
+ }
+}