diff options
Diffstat (limited to 'eclipse')
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 <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")); + } +} |