aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java1001
1 files changed, 1001 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java
new file mode 100644
index 000000000..1015d7d86
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java
@@ -0,0 +1,1001 @@
+/*
+ * Copyright (C) 2007 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;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.annotations.VisibleForTesting;
+import com.android.annotations.VisibleForTesting.Visibility;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.XmlEditorMultiOutline;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.IUnknownDescriptorProvider;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutActionBar;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.OutlinePage;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionManager;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertySheetPage;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.lint.EclipseLintClient;
+import com.android.ide.eclipse.adt.internal.lint.EclipseLintRunner;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.resources.ResourceFolderType;
+import com.android.sdklib.IAndroidTarget;
+import com.android.tools.lint.client.api.IssueRegistry;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.jobs.IJobChangeEvent;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.core.runtime.jobs.JobChangeAdapter;
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.ui.IActionBars;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IFileEditorInput;
+import org.eclipse.ui.ISelectionListener;
+import org.eclipse.ui.ISelectionService;
+import org.eclipse.ui.IShowEditorInput;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkbenchPartSite;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.forms.editor.IFormPage;
+import org.eclipse.ui.part.FileEditorInput;
+import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
+import org.eclipse.ui.views.properties.IPropertySheetPage;
+import org.eclipse.wst.sse.ui.StructuredTextEditor;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Multi-page form editor for /res/layout XML files.
+ */
+public class LayoutEditorDelegate extends CommonXmlDelegate
+ implements IShowEditorInput, CommonXmlDelegate.IActionContributorDelegate {
+
+ /** The prefix for layout folders that are not the default layout folder */
+ private static final String LAYOUT_FOLDER_PREFIX = "layout-"; //$NON-NLS-1$
+
+ public static class Creator implements IDelegateCreator {
+ @Override
+ @SuppressWarnings("unchecked")
+ public LayoutEditorDelegate createForFile(
+ @NonNull CommonXmlEditor delegator,
+ @Nullable ResourceFolderType type) {
+ if (ResourceFolderType.LAYOUT == type) {
+ return new LayoutEditorDelegate(delegator);
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * Old standalone-editor ID.
+ * Use {@link CommonXmlEditor#ID} instead.
+ */
+ public static final String LEGACY_EDITOR_ID =
+ AdtConstants.EDITORS_NAMESPACE + ".layout.LayoutEditor"; //$NON-NLS-1$
+
+ /** Root node of the UI element hierarchy */
+ private UiDocumentNode mUiDocRootNode;
+
+ private GraphicalEditorPart mGraphicalEditor;
+ private int mGraphicalEditorIndex;
+
+ /** Implementation of the {@link IContentOutlinePage} for this editor */
+ private OutlinePage mLayoutOutline;
+
+ /** The XML editor outline */
+ private IContentOutlinePage mEditorOutline;
+
+ /** Multiplexing outline, used for multi-page editors that have their own outline */
+ private XmlEditorMultiOutline mMultiOutline;
+
+ /**
+ * Temporary flag set by the editor caret listener which is used to cause
+ * the next getAdapter(IContentOutlinePage.class) call to return the editor
+ * outline rather than the multi-outline. See the {@link #delegateGetAdapter}
+ * method for details.
+ */
+ private boolean mCheckOutlineAdapter;
+
+ /** Custom implementation of {@link IPropertySheetPage} for this editor */
+ private IPropertySheetPage mPropertyPage;
+
+ private final HashMap<String, ElementDescriptor> mUnknownDescriptorMap =
+ new HashMap<String, ElementDescriptor>();
+
+ private EclipseLintClient mClient;
+
+ /**
+ * Flag indicating if the replacement file is due to a config change.
+ * If false, it means the new file is due to an "open action" from the user.
+ */
+ private boolean mNewFileOnConfigChange = false;
+
+ /**
+ * Checks whether an editor part is an instance of {@link CommonXmlEditor}
+ * with an associated {@link LayoutEditorDelegate} delegate.
+ *
+ * @param editorPart An editor part. Can be null.
+ * @return The {@link LayoutEditorDelegate} delegate associated with the editor or null.
+ */
+ public static @Nullable LayoutEditorDelegate fromEditor(@Nullable IEditorPart editorPart) {
+ if (editorPart instanceof CommonXmlEditor) {
+ CommonXmlDelegate delegate = ((CommonXmlEditor) editorPart).getDelegate();
+ if (delegate instanceof LayoutEditorDelegate) {
+ return ((LayoutEditorDelegate) delegate);
+ }
+ } else if (editorPart instanceof GraphicalEditorPart) {
+ GraphicalEditorPart part = (GraphicalEditorPart) editorPart;
+ return part.getEditorDelegate();
+ }
+ return null;
+ }
+
+ /**
+ * Creates the form editor for resources XML files.
+ */
+ @VisibleForTesting(visibility=Visibility.PRIVATE)
+ protected LayoutEditorDelegate(CommonXmlEditor editor) {
+ super(editor, new LayoutContentAssist());
+ // Note that LayoutEditor has its own listeners and does not
+ // need to call editor.addDefaultTargetListener().
+ }
+
+ /**
+ * Returns the {@link RulesEngine} associated with this editor
+ *
+ * @return the {@link RulesEngine} associated with this editor.
+ */
+ public RulesEngine getRulesEngine() {
+ return mGraphicalEditor.getRulesEngine();
+ }
+
+ /**
+ * Returns the {@link GraphicalEditorPart} associated with this editor
+ *
+ * @return the {@link GraphicalEditorPart} associated with this editor
+ */
+ public GraphicalEditorPart getGraphicalEditor() {
+ return mGraphicalEditor;
+ }
+
+ /**
+ * @return The root node of the UI element hierarchy
+ */
+ @Override
+ public UiDocumentNode getUiRootNode() {
+ return mUiDocRootNode;
+ }
+
+ public void setNewFileOnConfigChange(boolean state) {
+ mNewFileOnConfigChange = state;
+ }
+
+ // ---- Base Class Overrides ----
+
+ @Override
+ public void dispose() {
+ super.dispose();
+ if (mGraphicalEditor != null) {
+ mGraphicalEditor.dispose();
+ mGraphicalEditor = null;
+ }
+ }
+
+ /**
+ * Save the XML.
+ * <p/>
+ * Clients must NOT call this directly. Instead they should always
+ * call {@link CommonXmlEditor#doSave(IProgressMonitor)} so that th
+ * editor super class can commit the data properly.
+ * <p/>
+ * Here we just need to tell the graphical editor that the model has
+ * been saved.
+ */
+ @Override
+ public void delegateDoSave(IProgressMonitor monitor) {
+ super.delegateDoSave(monitor);
+ if (mGraphicalEditor != null) {
+ mGraphicalEditor.doSave(monitor);
+ }
+ }
+
+ /**
+ * Create the various form pages.
+ */
+ @Override
+ public void delegateCreateFormPages() {
+ try {
+ // get the file being edited so that it can be passed to the layout editor.
+ IFile editedFile = null;
+ IEditorInput input = getEditor().getEditorInput();
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput)input;
+ editedFile = fileInput.getFile();
+ if (!editedFile.isAccessible()) {
+ return;
+ }
+ } else {
+ AdtPlugin.log(IStatus.ERROR,
+ "Input is not of type FileEditorInput: %1$s", //$NON-NLS-1$
+ input.toString());
+ }
+
+ // It is possible that the Layout Editor already exits if a different version
+ // of the same layout is being opened (either through "open" action from
+ // the user, or through a configuration change in the configuration selector.)
+ if (mGraphicalEditor == null) {
+
+ // Instantiate GLE v2
+ mGraphicalEditor = new GraphicalEditorPart(this);
+
+ mGraphicalEditorIndex = getEditor().addPage(mGraphicalEditor,
+ getEditor().getEditorInput());
+ getEditor().setPageText(mGraphicalEditorIndex, mGraphicalEditor.getTitle());
+
+ mGraphicalEditor.openFile(editedFile);
+ } else {
+ if (mNewFileOnConfigChange) {
+ mGraphicalEditor.changeFileOnNewConfig(editedFile);
+ mNewFileOnConfigChange = false;
+ } else {
+ mGraphicalEditor.replaceFile(editedFile);
+ }
+ }
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$
+ }
+ }
+
+ @Override
+ public void delegatePostCreatePages() {
+ // Optional: set the default page. Eventually a default page might be
+ // restored by selectDefaultPage() later based on the last page used by the user.
+ // For example, to make the last page the default one (rather than the first page),
+ // uncomment this line:
+ // setActivePage(getPageCount() - 1);
+ }
+
+ /* (non-java doc)
+ * Change the tab/title name to include the name of the layout.
+ */
+ @Override
+ public void delegateSetInput(IEditorInput input) {
+ handleNewInput(input);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.ui.part.EditorPart#setInputWithNotify(org.eclipse.ui.IEditorInput)
+ */
+ public void delegateSetInputWithNotify(IEditorInput input) {
+ handleNewInput(input);
+ }
+
+ /**
+ * Called to replace the current {@link IEditorInput} with another one.
+ * <p/>
+ * This is used when {@link LayoutEditorMatchingStrategy} returned
+ * <code>true</code> which means we're opening a different configuration of
+ * the same layout.
+ */
+ @Override
+ public void showEditorInput(IEditorInput editorInput) {
+ if (getEditor().getEditorInput().equals(editorInput)) {
+ return;
+ }
+
+ // Save the current editor input. This must be called on the editor itself
+ // since it's the base editor that commits pending changes.
+ getEditor().doSave(new NullProgressMonitor());
+
+ // Get the current page
+ int currentPage = getEditor().getActivePage();
+
+ // Remove the pages, except for the graphical editor, which will be dynamically adapted
+ // to the new model.
+ // page after the graphical editor:
+ int count = getEditor().getPageCount();
+ for (int i = count - 1 ; i > mGraphicalEditorIndex ; i--) {
+ getEditor().removePage(i);
+ }
+ // Pages before the graphical editor
+ for (int i = mGraphicalEditorIndex - 1 ; i >= 0 ; i--) {
+ getEditor().removePage(i);
+ }
+
+ // Set the current input. We're in the delegate, the input must
+ // be set into the actual editor instance.
+ getEditor().setInputWithNotify(editorInput);
+
+ // Re-create or reload the pages with the default page shown as the previous active page.
+ getEditor().createAndroidPages();
+ getEditor().selectDefaultPage(Integer.toString(currentPage));
+
+ // When changing an input file of an the editor, the titlebar is not refreshed to
+ // show the new path/to/file being edited. So we force a refresh
+ getEditor().firePropertyChange(IWorkbenchPart.PROP_TITLE);
+ }
+
+ /** Performs a complete refresh of the XML model */
+ public void refreshXmlModel() {
+ Document xmlDoc = mUiDocRootNode.getXmlDocument();
+
+ delegateInitUiRootNode(true /*force*/);
+ mUiDocRootNode.loadFromXmlNode(xmlDoc);
+
+ // Update the model first, since it is used by the viewers.
+ // No need to call AndroidXmlEditor.xmlModelChanged(xmlDoc) since it's
+ // a no-op. Instead call onXmlModelChanged on the graphical editor.
+
+ if (mGraphicalEditor != null) {
+ mGraphicalEditor.onXmlModelChanged();
+ }
+ }
+
+ /**
+ * Processes the new XML Model, which XML root node is given.
+ *
+ * @param xml_doc The XML document, if available, or null if none exists.
+ */
+ @Override
+ public void delegateXmlModelChanged(Document xml_doc) {
+ // init the ui root on demand
+ delegateInitUiRootNode(false /*force*/);
+
+ mUiDocRootNode.loadFromXmlNode(xml_doc);
+
+ // Update the model first, since it is used by the viewers.
+ // No need to call AndroidXmlEditor.xmlModelChanged(xmlDoc) since it's
+ // a no-op. Instead call onXmlModelChanged on the graphical editor.
+
+ if (mGraphicalEditor != null) {
+ mGraphicalEditor.onXmlModelChanged();
+ }
+ }
+
+ /**
+ * Tells the graphical editor to recompute its layout.
+ */
+ public void recomputeLayout() {
+ mGraphicalEditor.recomputeLayout();
+ }
+
+ /**
+ * Does this editor participate in the "format GUI editor changes" option?
+ *
+ * @return true since this editor supports automatically formatting XML
+ * affected by GUI changes
+ */
+ @Override
+ public boolean delegateSupportsFormatOnGuiEdit() {
+ return true;
+ }
+
+ /**
+ * Returns one of the issues for the given node (there could be more than one)
+ *
+ * @param node the node to look up lint issues for
+ * @return the marker for one of the issues found for the given node
+ */
+ @Nullable
+ public IMarker getIssueForNode(@Nullable UiViewElementNode node) {
+ if (node == null) {
+ return null;
+ }
+
+ if (mClient != null) {
+ return mClient.getIssueForNode(node);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a collection of nodes that have one or more lint warnings
+ * associated with them (retrievable via
+ * {@link #getIssueForNode(UiViewElementNode)})
+ *
+ * @return a collection of nodes, which should <b>not</b> be modified by the
+ * caller
+ */
+ @Nullable
+ public Collection<Node> getLintNodes() {
+ if (mClient != null) {
+ return mClient.getIssueNodes();
+ }
+
+ return null;
+ }
+
+ @Override
+ public Job delegateRunLint() {
+ // We want to customize the {@link EclipseLintClient} created to run this
+ // single file lint, in particular such that we can set the mode which collects
+ // nodes on that lint job, such that we can quickly look up error nodes
+ //Job job = super.delegateRunLint();
+
+ Job job = null;
+ IFile file = getEditor().getInputFile();
+ if (file != null) {
+ IssueRegistry registry = EclipseLintClient.getRegistry();
+ List<IFile> resources = Collections.singletonList(file);
+ mClient = new EclipseLintClient(registry,
+ resources, getEditor().getStructuredDocument(), false /*fatal*/);
+
+ mClient.setCollectNodes(true);
+
+ job = EclipseLintRunner.startLint(mClient, resources, file,
+ false /*show*/);
+ }
+
+ if (job != null) {
+ GraphicalEditorPart graphicalEditor = getGraphicalEditor();
+ if (graphicalEditor != null) {
+ job.addJobChangeListener(new LintJobListener(graphicalEditor));
+ }
+ }
+ return job;
+ }
+
+ private class LintJobListener extends JobChangeAdapter implements Runnable {
+ private final GraphicalEditorPart mEditor;
+ private final LayoutCanvas mCanvas;
+
+ LintJobListener(GraphicalEditorPart editor) {
+ mEditor = editor;
+ mCanvas = editor.getCanvasControl();
+ }
+
+ @Override
+ public void done(IJobChangeEvent event) {
+ LayoutActionBar bar = mEditor.getLayoutActionBar();
+ if (!bar.isDisposed()) {
+ bar.updateErrorIndicator();
+ }
+
+ // Redraw
+ if (!mCanvas.isDisposed()) {
+ mCanvas.getDisplay().asyncExec(this);
+ }
+ }
+
+ @Override
+ public void run() {
+ if (!mCanvas.isDisposed()) {
+ mCanvas.redraw();
+
+ OutlinePage outlinePage = mCanvas.getOutlinePage();
+ if (outlinePage != null) {
+ outlinePage.refreshIcons();
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the custom IContentOutlinePage or IPropertySheetPage when asked for it.
+ */
+ @Override
+ public Object delegateGetAdapter(Class<?> adapter) {
+ if (adapter == IContentOutlinePage.class) {
+ // Somebody has requested the outline. Eclipse can only have a single outline page,
+ // even for a multi-part editor:
+ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=1917
+ // To work around this we use PDE's workaround of having a single multiplexing
+ // outline which switches its contents between the outline pages we register
+ // for it, and then on page switch we notify it to update itself.
+
+ // There is one complication: The XML editor outline listens for the editor
+ // selection and uses this to automatically expand its tree children and show
+ // the current node containing the caret as selected. Unfortunately, this
+ // listener code contains this:
+ //
+ // /* Bug 136310, unless this page is that part's
+ // * IContentOutlinePage, ignore the selection change */
+ // if (part.getAdapter(IContentOutlinePage.class) == this) {
+ //
+ // This means that when we return the multiplexing outline from this getAdapter
+ // method, the outline no longer updates to track the selection.
+ // To work around this, we use the following hack^H^H^H^H technique:
+ // - Add a selection listener *before* requesting the editor outline, such
+ // that the selection listener is told about the impending selection event
+ // right before the editor outline hears about it. Set the flag
+ // mCheckOutlineAdapter to true. (We also only set it if the editor view
+ // itself is active.)
+ // - In this getAdapter method, when somebody requests the IContentOutline.class,
+ // see if mCheckOutlineAdapter to see if this request is *likely* coming
+ // from the XML editor outline. If so, make sure it is by actually looking
+ // at the signature of the caller. If it's the editor outline, then return
+ // the editor outline instance itself rather than the multiplexing outline.
+ if (mCheckOutlineAdapter && mEditorOutline != null) {
+ mCheckOutlineAdapter = false;
+ // Make *sure* this is really the editor outline calling in case
+ // future versions of Eclipse changes the sequencing or dispatch of selection
+ // events:
+ StackTraceElement[] frames = new Throwable().fillInStackTrace().getStackTrace();
+ if (frames.length > 2) {
+ StackTraceElement frame = frames[2];
+ if (frame.getClassName().equals(
+ "org.eclipse.wst.sse.ui.internal.contentoutline." + //$NON-NLS-1$
+ "ConfigurableContentOutlinePage$PostSelectionServiceListener")) { //$NON-NLS-1$
+ return mEditorOutline;
+ }
+ }
+ }
+
+ // Use a multiplexing outline: workaround for
+ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=1917
+ if (mMultiOutline == null || mMultiOutline.isDisposed()) {
+ mMultiOutline = new XmlEditorMultiOutline();
+ mMultiOutline.addSelectionChangedListener(new ISelectionChangedListener() {
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ ISelection selection = event.getSelection();
+ getEditor().getSite().getSelectionProvider().setSelection(selection);
+ if (getEditor().getIgnoreXmlUpdate()) {
+ return;
+ }
+ SelectionManager manager =
+ mGraphicalEditor.getCanvasControl().getSelectionManager();
+ manager.setSelection(selection);
+ }
+ });
+ updateOutline(getEditor().getActivePageInstance());
+ }
+
+ return mMultiOutline;
+ }
+
+ if (IPropertySheetPage.class == adapter && mGraphicalEditor != null) {
+ if (mPropertyPage == null) {
+ mPropertyPage = new PropertySheetPage(mGraphicalEditor);
+ }
+
+ return mPropertyPage;
+ }
+
+ // return default
+ return super.delegateGetAdapter(adapter);
+ }
+
+ /**
+ * Update the contents of the outline to show either the XML editor outline
+ * or the layout editor graphical outline depending on which tab is visible
+ */
+ private void updateOutline(IFormPage page) {
+ if (mMultiOutline == null) {
+ return;
+ }
+
+ IContentOutlinePage outline;
+ CommonXmlEditor editor = getEditor();
+ if (!editor.isEditorPageActive()) {
+ outline = getGraphicalOutline();
+ } else {
+ // Use plain XML editor outline instead
+ if (mEditorOutline == null) {
+ StructuredTextEditor structuredTextEditor = editor.getStructuredTextEditor();
+ if (structuredTextEditor != null) {
+ IWorkbenchWindow window = editor.getSite().getWorkbenchWindow();
+ ISelectionService service = window.getSelectionService();
+ service.addPostSelectionListener(new ISelectionListener() {
+ @Override
+ public void selectionChanged(IWorkbenchPart part, ISelection selection) {
+ if (getEditor().isEditorPageActive()) {
+ mCheckOutlineAdapter = true;
+ }
+ }
+ });
+
+ mEditorOutline = (IContentOutlinePage) structuredTextEditor.getAdapter(
+ IContentOutlinePage.class);
+ }
+ }
+
+ outline = mEditorOutline;
+ }
+
+ mMultiOutline.setPageActive(outline);
+ }
+
+ /**
+ * Returns the graphical outline associated with the layout editor
+ *
+ * @return the outline page, never null
+ */
+ @NonNull
+ public OutlinePage getGraphicalOutline() {
+ if (mLayoutOutline == null) {
+ mLayoutOutline = new OutlinePage(mGraphicalEditor);
+ }
+
+ return mLayoutOutline;
+ }
+
+ @Override
+ public void delegatePageChange(int newPageIndex) {
+ if (getEditor().getCurrentPage() == getEditor().getTextPageIndex() &&
+ newPageIndex == mGraphicalEditorIndex) {
+ // You're switching from the XML editor to the WYSIWYG editor;
+ // look at the caret position and figure out which node it corresponds to
+ // (if any) and if found, select the corresponding visual element.
+ ISourceViewer textViewer = getEditor().getStructuredSourceViewer();
+ int caretOffset = textViewer.getTextWidget().getCaretOffset();
+ if (caretOffset >= 0) {
+ Node node = DomUtilities.getNode(textViewer.getDocument(), caretOffset);
+ if (node != null && mGraphicalEditor != null) {
+ mGraphicalEditor.select(node);
+ }
+ }
+ }
+
+ super.delegatePageChange(newPageIndex);
+
+ if (mGraphicalEditor != null) {
+ if (newPageIndex == mGraphicalEditorIndex) {
+ mGraphicalEditor.activated();
+ } else {
+ mGraphicalEditor.deactivated();
+ }
+ }
+ }
+
+ @Override
+ public int delegateGetPersistenceCategory() {
+ return AndroidXmlEditor.CATEGORY_LAYOUT;
+ }
+
+ @Override
+ public void delegatePostPageChange(int newPageIndex) {
+ super.delegatePostPageChange(newPageIndex);
+
+ if (mGraphicalEditor != null) {
+ LayoutCanvas canvas = mGraphicalEditor.getCanvasControl();
+ if (canvas != null) {
+ IActionBars bars = getEditor().getEditorSite().getActionBars();
+ if (bars != null) {
+ canvas.updateGlobalActions(bars);
+ }
+ }
+ }
+
+ IFormPage page = getEditor().getActivePageInstance();
+ updateOutline(page);
+ }
+
+ @Override
+ public IFormPage delegatePostSetActivePage(IFormPage superReturned, String pageIndex) {
+ IFormPage page = superReturned;
+ if (page != null) {
+ updateOutline(page);
+ }
+
+ return page;
+ }
+
+ // ----- IActionContributorDelegate methods ----
+
+ @Override
+ public void setActiveEditor(IEditorPart part, IActionBars bars) {
+ if (mGraphicalEditor != null) {
+ LayoutCanvas canvas = mGraphicalEditor.getCanvasControl();
+ if (canvas != null) {
+ canvas.updateGlobalActions(bars);
+ }
+ }
+ }
+
+
+ @Override
+ public void delegateActivated() {
+ if (mGraphicalEditor != null) {
+ if (getEditor().getActivePage() == mGraphicalEditorIndex) {
+ mGraphicalEditor.activated();
+ } else {
+ mGraphicalEditor.deactivated();
+ }
+ }
+ }
+
+ @Override
+ public void delegateDeactivated() {
+ if (mGraphicalEditor != null && getEditor().getActivePage() == mGraphicalEditorIndex) {
+ mGraphicalEditor.deactivated();
+ }
+ }
+
+ @Override
+ public String delegateGetPartName() {
+ IEditorInput editorInput = getEditor().getEditorInput();
+ if (!AdtPrefs.getPrefs().isSharedLayoutEditor()
+ && editorInput instanceof IFileEditorInput) {
+ IFileEditorInput fileInput = (IFileEditorInput) editorInput;
+ IFile file = fileInput.getFile();
+ IContainer parent = file.getParent();
+ if (parent != null) {
+ String parentName = parent.getName();
+ if (parentName.startsWith(LAYOUT_FOLDER_PREFIX)) {
+ parentName = parentName.substring(LAYOUT_FOLDER_PREFIX.length());
+ return parentName + File.separatorChar + file.getName();
+ }
+ }
+ }
+
+ return super.delegateGetPartName();
+ }
+
+ // ---- Local Methods ----
+
+ /**
+ * Returns true if the Graphics editor page is visible. This <b>must</b> be
+ * called from the UI thread.
+ */
+ public boolean isGraphicalEditorActive() {
+ IWorkbenchPartSite workbenchSite = getEditor().getSite();
+ IWorkbenchPage workbenchPage = workbenchSite.getPage();
+
+ // check if the editor is visible in the workbench page
+ if (workbenchPage.isPartVisible(getEditor())
+ && workbenchPage.getActiveEditor() == getEditor()) {
+ // and then if the page of the editor is visible (not to be confused with
+ // the workbench page)
+ return mGraphicalEditorIndex == getEditor().getActivePage();
+ }
+
+ return false;
+ }
+
+ @Override
+ public void delegateInitUiRootNode(boolean force) {
+ // The root UI node is always created, even if there's no corresponding XML node.
+ if (mUiDocRootNode == null || force) {
+ // get the target data from the opened file (and its project)
+ AndroidTargetData data = getEditor().getTargetData();
+
+ Document doc = null;
+ if (mUiDocRootNode != null) {
+ doc = mUiDocRootNode.getXmlDocument();
+ }
+
+ DocumentDescriptor desc;
+ if (data == null) {
+ desc = new DocumentDescriptor("temp", null /*children*/);
+ } else {
+ desc = data.getLayoutDescriptors().getDescriptor();
+ }
+
+ // get the descriptors from the data.
+ mUiDocRootNode = (UiDocumentNode) desc.createUiNode();
+ super.setUiRootNode(mUiDocRootNode);
+ mUiDocRootNode.setEditor(getEditor());
+
+ mUiDocRootNode.setUnknownDescriptorProvider(new IUnknownDescriptorProvider() {
+ @Override
+ public ElementDescriptor getDescriptor(String xmlLocalName) {
+ ElementDescriptor unknown = mUnknownDescriptorMap.get(xmlLocalName);
+ if (unknown == null) {
+ unknown = createUnknownDescriptor(xmlLocalName);
+ mUnknownDescriptorMap.put(xmlLocalName, unknown);
+ }
+
+ return unknown;
+ }
+ });
+
+ onDescriptorsChanged(doc);
+ }
+ }
+
+ /**
+ * Creates a new {@link ViewElementDescriptor} for an unknown XML local name
+ * (i.e. one that was not mapped by the current descriptors).
+ * <p/>
+ * Since we deal with layouts, we returns either a descriptor for a custom view
+ * or one for the base View.
+ *
+ * @param xmlLocalName The XML local name to match.
+ * @return A non-null {@link ViewElementDescriptor}.
+ */
+ private ViewElementDescriptor createUnknownDescriptor(String xmlLocalName) {
+ ViewElementDescriptor desc = null;
+ IEditorInput editorInput = getEditor().getEditorInput();
+ if (editorInput instanceof IFileEditorInput) {
+ IFileEditorInput fileInput = (IFileEditorInput)editorInput;
+ IProject project = fileInput.getFile().getProject();
+
+ // Check if we can find a custom view specific to this project.
+ // This only works if there's an actual matching custom class in the project.
+ if (xmlLocalName.indexOf('.') != -1) {
+ desc = CustomViewDescriptorService.getInstance().getDescriptor(project,
+ xmlLocalName);
+ }
+
+ if (desc == null) {
+ // If we didn't find a custom view, create a synthetic one using the
+ // the base View descriptor as a model.
+ // This is a layout after all, so every XML node should represent
+ // a view.
+
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(project);
+ if (target != null) {
+ AndroidTargetData data = currentSdk.getTargetData(target);
+ if (data != null) {
+ // data can be null when the target is still loading
+ ViewElementDescriptor viewDesc =
+ data.getLayoutDescriptors().getBaseViewDescriptor();
+
+ desc = new ViewElementDescriptor(
+ xmlLocalName, // xml local name
+ xmlLocalName, // ui_name
+ xmlLocalName, // canonical class name
+ null, // tooltip
+ null, // sdk_url
+ viewDesc.getAttributes(),
+ viewDesc.getLayoutAttributes(),
+ null, // children
+ false /* mandatory */);
+ desc.setSuperClass(viewDesc);
+ }
+ }
+ }
+ }
+ }
+
+ if (desc == null) {
+ // We can only arrive here if the SDK's android target has not finished
+ // loading. Just create a dummy descriptor with no attributes to be able
+ // to continue.
+ desc = new ViewElementDescriptor(xmlLocalName, xmlLocalName);
+ }
+ return desc;
+ }
+
+ private void onDescriptorsChanged(Document document) {
+
+ mUnknownDescriptorMap.clear();
+
+ if (document != null) {
+ mUiDocRootNode.loadFromXmlNode(document);
+ } else {
+ mUiDocRootNode.reloadFromXmlNode(mUiDocRootNode.getXmlDocument());
+ }
+
+ if (mGraphicalEditor != null) {
+ mGraphicalEditor.onTargetChange();
+ mGraphicalEditor.reloadPalette();
+ mGraphicalEditor.getCanvasControl().syncPreviewMode();
+ }
+ }
+
+ /**
+ * Handles a new input, and update the part name.
+ * @param input the new input.
+ */
+ private void handleNewInput(IEditorInput input) {
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput) input;
+ IFile file = fileInput.getFile();
+ getEditor().setPartName(String.format("%1$s", file.getName()));
+ }
+ }
+
+ /**
+ * Helper method that returns a {@link ViewElementDescriptor} for the requested FQCN.
+ * Will return null if we can't find that FQCN or we lack the editor/data/descriptors info.
+ */
+ public ViewElementDescriptor getFqcnViewDescriptor(String fqcn) {
+ ViewElementDescriptor desc = null;
+
+ AndroidTargetData data = getEditor().getTargetData();
+ if (data != null) {
+ LayoutDescriptors layoutDesc = data.getLayoutDescriptors();
+ if (layoutDesc != null) {
+ DocumentDescriptor docDesc = layoutDesc.getDescriptor();
+ if (docDesc != null) {
+ desc = internalFindFqcnViewDescriptor(fqcn, docDesc.getChildren(), null);
+ }
+ }
+ }
+
+ if (desc == null) {
+ // We failed to find a descriptor for the given FQCN.
+ // Let's consider custom classes and create one as needed.
+ desc = createUnknownDescriptor(fqcn);
+ }
+
+ return desc;
+ }
+
+ /**
+ * Internal helper to recursively search for a {@link ViewElementDescriptor} that matches
+ * the requested FQCN.
+ *
+ * @param fqcn The target View FQCN to find.
+ * @param descriptors A list of children descriptors to iterate through.
+ * @param visited A set we use to remember which descriptors have already been visited,
+ * necessary since the view descriptor hierarchy is cyclic.
+ * @return Either a matching {@link ViewElementDescriptor} or null.
+ */
+ private ViewElementDescriptor internalFindFqcnViewDescriptor(String fqcn,
+ ElementDescriptor[] descriptors,
+ Set<ElementDescriptor> visited) {
+ if (visited == null) {
+ visited = new HashSet<ElementDescriptor>();
+ }
+
+ if (descriptors != null) {
+ for (ElementDescriptor desc : descriptors) {
+ if (visited.add(desc)) {
+ // Set.add() returns true if this a new element that was added to the set.
+ // That means we haven't visited this descriptor yet.
+ // We want a ViewElementDescriptor with a matching FQCN.
+ if (desc instanceof ViewElementDescriptor &&
+ fqcn.equals(((ViewElementDescriptor) desc).getFullClassName())) {
+ return (ViewElementDescriptor) desc;
+ }
+
+ // Visit its children
+ ViewElementDescriptor vd =
+ internalFindFqcnViewDescriptor(fqcn, desc.getChildren(), visited);
+ if (vd != null) {
+ return vd;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+}