diff options
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.java | 1001 |
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; + } +} |