diff options
author | Tor Norbye <tnorbye@google.com> | 2010-11-28 19:23:42 -0800 |
---|---|---|
committer | Tor Norbye <tnorbye@google.com> | 2010-12-07 11:25:44 -0800 |
commit | 4517a1f5f4f9fd21b6a611d8a40ac8b81a7bb9c5 (patch) | |
tree | 2bf4d3c7222262f39bf14dc38a6cdffad2519ff3 /eclipse/plugins | |
parent | 78086a6fea4a2e0e3c0b7aed8685711d12cc3788 (diff) | |
download | sdk-4517a1f5f4f9fd21b6a611d8a40ac8b81a7bb9c5.zip sdk-4517a1f5f4f9fd21b6a611d8a40ac8b81a7bb9c5.tar.gz sdk-4517a1f5f4f9fd21b6a611d8a40ac8b81a7bb9c5.tar.bz2 |
Include View Support
Add improved support for includes. You can now view and edit a layout
within another containing layout. On a page with includes, you can
double click to warp into the included layout, and it will be shown
within the container layout (but with a semi-translucent mask to make
it obvious that you are editing only the included content, not the
container.)
You can also right click on a view and choose "Show Included In",
which is a pull-right which lists all the other layouts that are
including this view. (If there are no such views, the menu item will
be empty).
In addition, this changeset adds code to detect if there are cycles in
the includes, and if so adds a problem marker in the Problems view
listing the offending chain of includes.
This is all managed by a new "IncludeFinder" class, which listens for
resource file edits (so it only kicks in when you save an XML file,
not after each XML edit). It scans layout XML files for includes and
maintains a map of file includes. This is done such that it can very
quickly provide a list of all files that are including a given target
file. This list is also persisted across IDE sessions via a project
property.
Also fixes outline-expansion to ensure that the outline always shows
the top level children.
Note: The include-relationships are based on the base layouts (the
ones in layouts/, not in customized versions in layout-land,
layout-port, etc.)
Change-Id: I710560f03f7e214219669af8ffba91874d9881b9
Diffstat (limited to 'eclipse/plugins')
11 files changed, 1422 insertions, 55 deletions
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 <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")); + } +} |