/*
* 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.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.AndroidConstants;
import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
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.OutlinePage;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.PropertySheetPage;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.sdklib.IAndroidTarget;
import org.eclipse.core.resources.IFile;
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.jface.text.source.ISourceViewer;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IPartListener;
import org.eclipse.ui.IShowEditorInput;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchPartSite;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
import org.eclipse.ui.views.properties.IPropertySheetPage;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
/**
* Multi-page form editor for /res/layout XML files.
*/
public class LayoutEditor extends AndroidXmlEditor implements IShowEditorInput, IPartListener {
public static final String ID = AndroidConstants.EDITORS_NAMESPACE + ".layout.LayoutEditor"; //$NON-NLS-1$
/** Root node of the UI element hierarchy */
private UiDocumentNode mUiRootNode;
private GraphicalEditorPart mGraphicalEditor;
private int mGraphicalEditorIndex;
/** Implementation of the {@link IContentOutlinePage} for this editor */
private IContentOutlinePage mOutline;
/** Custom implementation of {@link IPropertySheetPage} for this editor */
private IPropertySheetPage mPropertyPage;
private final HashMap<String, ElementDescriptor> mUnknownDescriptorMap =
new HashMap<String, ElementDescriptor>();
/**
* 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;
/**
* Creates the form editor for resources XML files.
*/
public LayoutEditor() {
super(false /* addTargetListener */);
}
/**
* 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 mUiRootNode;
}
public void setNewFileOnConfigChange(boolean state) {
mNewFileOnConfigChange = state;
}
// ---- Base Class Overrides ----
@Override
public void dispose() {
getSite().getPage().removePartListener(this);
super.dispose();
}
/**
* Save the XML.
* <p/>
* The actual save operation is done in the super class by committing
* all data to the XML model and then having the Structured XML Editor
* save the XML.
* <p/>
* Here we just need to tell the graphical editor that the model has
* been saved.
*/
@Override
public void doSave(IProgressMonitor monitor) {
super.doSave(monitor);
if (mGraphicalEditor != null) {
mGraphicalEditor.doSave(monitor);
}
}
/**
* Returns whether the "save as" operation is supported by this editor.
* <p/>
* Save-As is a valid operation for the ManifestEditor since it acts on a
* single source file.
*
* @see IEditorPart
*/
@Override
public boolean isSaveAsAllowed() {
return true;
}
/**
* Create the various form pages.
*/
@Override
protected void createFormPages() {
try {
// The graphical layout editor is now enabled by default.
// In case there's an issue we provide a way to disable it using an
// env variable.
if (System.getenv("ANDROID_DISABLE_LAYOUT") == null) { //$NON-NLS-1$
// get the file being edited so that it can be passed to the layout editor.
IFile editedFile = null;
IEditorInput input = getEditorInput();
if (input instanceof FileEditorInput) {
FileEditorInput fileInput = (FileEditorInput)input;
editedFile = fileInput.getFile();
} 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 = addPage(mGraphicalEditor, getEditorInput());
setPageText(mGraphicalEditorIndex, mGraphicalEditor.getTitle());
mGraphicalEditor.openFile(editedFile);
} else {
if (mNewFileOnConfigChange) {
mGraphicalEditor.changeFileOnNewConfig(editedFile);
mNewFileOnConfigChange = false;
} else {
mGraphicalEditor.replaceFile(editedFile);
}
}
// put in place the listener to handle layout recompute only when needed.
getSite().getPage().addPartListener(this);
}
} catch (PartInitException e) {
AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$
}
}
@Override
protected void postCreatePages() {
super.postCreatePages();
// 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
protected void setInput(IEditorInput input) {
super.setInput(input);
handleNewInput(input);
}
/*
* (non-Javadoc)
* @see org.eclipse.ui.part.EditorPart#setInputWithNotify(org.eclipse.ui.IEditorInput)
*/
@Override
protected void setInputWithNotify(IEditorInput input) {
super.setInputWithNotify(input);
handleNewInput(input);
}
/**
* Called to replace the current {@link IEditorInput} with another one.
* <p/>This is used when {@link MatchingStrategy} returned <code>true</code> which means we're
* opening a different configuration of the same layout.
*/
public void showEditorInput(IEditorInput editorInput) {
if (getEditorInput().equals(editorInput)) {
return;
}
// save the current editor input.
doSave(new NullProgressMonitor());
// get the current page
int currentPage = 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 = getPageCount();
for (int i = count - 1 ; i > mGraphicalEditorIndex ; i--) {
removePage(i);
}
// pages before the graphical editor
for (int i = mGraphicalEditorIndex - 1 ; i >= 0 ; i--) {
removePage(i);
}
// set the current input.
setInputWithNotify(editorInput);
// re-create or reload the pages with the default page shown as the previous active page.
createAndroidPages();
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
firePropertyChange(IWorkbenchPart.PROP_TITLE);
}
/**
* 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
protected void xmlModelChanged(Document xml_doc) {
// init the ui root on demand
initUiRootNode(false /*force*/);
mUiRootNode.loadFromXmlNode(xml_doc);
// update the model first, since it is used by the viewers.
super.xmlModelChanged(xml_doc);
if (mGraphicalEditor != null) {
mGraphicalEditor.onXmlModelChanged();
}
}
/**
* Tells the graphical editor to recompute its layout.
*/
public void recomputeLayout() {
mGraphicalEditor.recomputeLayout();
}
/**
* Returns the custom IContentOutlinePage or IPropertySheetPage when asked for it.
*/
@SuppressWarnings("unchecked")
@Override
public Object getAdapter(Class adapter) {
// For the outline, force it to come from the Graphical Editor.
// This fixes the case where a layout file is opened in XML view first and the outline
// gets stuck in the XML outline.
if (IContentOutlinePage.class == adapter && mGraphicalEditor != null) {
if (mOutline == null && mGraphicalEditor != null) {
mOutline = new OutlinePage(mGraphicalEditor);
}
return mOutline;
}
if (IPropertySheetPage.class == adapter && mGraphicalEditor != null) {
if (mPropertyPage == null) {
mPropertyPage = new PropertySheetPage();
}
return mPropertyPage;
}
// return default
return super.getAdapter(adapter);
}
@Override
protected void pageChange(int newPageIndex) {
if (getCurrentPage() == mTextPageIndex &&
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 = 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.pageChange(newPageIndex);
if (mGraphicalEditor != null) {
if (newPageIndex == mGraphicalEditorIndex) {
mGraphicalEditor.activated();
} else {
mGraphicalEditor.deactivated();
}
}
}
// ----- IPartListener Methods ----
public void partActivated(IWorkbenchPart part) {
if (part == this) {
if (mGraphicalEditor != null) {
if (getActivePage() == mGraphicalEditorIndex) {
mGraphicalEditor.activated();
} else {
mGraphicalEditor.deactivated();
}
}
}
}
public void partBroughtToTop(IWorkbenchPart part) {
partActivated(part);
}
public void partClosed(IWorkbenchPart part) {
// pass
}
public void partDeactivated(IWorkbenchPart part) {
if (part == this) {
if (mGraphicalEditor != null && getActivePage() == mGraphicalEditorIndex) {
mGraphicalEditor.deactivated();
}
}
}
public void partOpened(IWorkbenchPart part) {
/*
* We used to automatically bring the outline and the property sheet to view
* when opening the editor. This behavior has always been a mixed bag and not
* exactly satisfactory. GLE1 is being useless/deprecated and GLE2 will need to
* improve on that, so right now let's comment this out.
*/
//EclipseUiHelper.showView(EclipseUiHelper.CONTENT_OUTLINE_VIEW_ID, false /* activate */);
//EclipseUiHelper.showView(EclipseUiHelper.PROPERTY_SHEET_VIEW_ID, false /* activate */);
}
// ---- 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 = getSite();
IWorkbenchPage workbenchPage = workbenchSite.getPage();
// check if the editor is visible in the workbench page
if (workbenchPage.isPartVisible(this) && workbenchPage.getActiveEditor() == this) {
// and then if the page of the editor is visible (not to be confused with
// the workbench page)
return mGraphicalEditorIndex == getActivePage();
}
return false;
}
@Override
public void initUiRootNode(boolean force) {
// The root UI node is always created, even if there's no corresponding XML node.
if (mUiRootNode == null || force) {
// get the target data from the opened file (and its project)
AndroidTargetData data = getTargetData();
Document doc = null;
if (mUiRootNode != null) {
doc = mUiRootNode.getXmlDocument();
}
DocumentDescriptor desc;
if (data == null) {
desc = new DocumentDescriptor("temp", null /*children*/);
} else {
desc = data.getLayoutDescriptors().getDescriptor();
}
// get the descriptors from the data.
mUiRootNode = (UiDocumentNode) desc.createUiNode();
mUiRootNode.setEditor(this);
mUiRootNode.setUnknownDescriptorProvider(new IUnknownDescriptorProvider() {
public ElementDescriptor getDescriptor(String xmlLocalName) {
ElementDescriptor desc = mUnknownDescriptorMap.get(xmlLocalName);
if (desc == null) {
desc = createUnknownDescriptor(xmlLocalName);
mUnknownDescriptorMap.put(xmlLocalName, desc);
}
return desc;
}
});
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 = 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.
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) {
mUiRootNode.loadFromXmlNode(document);
} else {
mUiRootNode.reloadFromXmlNode(mUiRootNode.getXmlDocument());
}
if (mGraphicalEditor != null) {
mGraphicalEditor.onTargetChange();
mGraphicalEditor.reloadPalette();
}
}
/**
* 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();
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 = 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;
}
}