/* * 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; } }