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