/*
* Copyright (C) 2009 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.gre;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.editors.layout.gscripts.INodeProxy;
import com.android.ide.eclipse.adt.editors.layout.gscripts.Rect;
import com.android.ide.eclipse.adt.internal.editors.AndroidEditor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
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.layout.LayoutEditor;
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.uimodel.UiViewElementNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
import com.android.sdklib.SdkConstants;
import org.eclipse.swt.graphics.Rectangle;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import java.util.HashSet;
import java.util.Set;
import groovy.lang.Closure;
/**
*
*/
public class NodeProxy implements INodeProxy {
private final UiViewElementNode mNode;
private final Rect mBounds;
private boolean mXmlEditOK;
/**
* Creates a new {@link INodeProxy} that wraps an {@link UiViewElementNode} that is
* actually valid in the current UI/XML model. The view may not be part of the canvas
* yet (e.g. if it has just been dynamically added and the canvas hasn't reloaded yet.)
*
* @param node The node to wrap.
* @param bounds The bounds of a the view in the canvas. Must be a valid rect for a view
* that is actually in the canvas and must be null (or an invalid rect) for a view
* that has just been added dynamically to the model.
*/
public NodeProxy(UiViewElementNode node, Rectangle bounds) {
mNode = node;
if (bounds == null) {
mBounds = new Rect();
} else {
mBounds = new Rect(bounds.x, bounds.y, bounds.width, bounds.height);
}
}
public void debugPrintf(String msg, Object...params) {
AdtPlugin.printToConsole(
mNode == null ? "Groovy" : mNode.getDescriptor().getXmlLocalName() + ".groovy",
String.format(msg, params)
);
}
public Rect getBounds() {
return mBounds;
}
/* package */ UiViewElementNode getNode() {
return mNode;
}
// ---- XML Editing ---
public void editXml(String undoName, final Closure c) {
if (mXmlEditOK) {
throw new RuntimeException("Error: nested calls to INodeProxy.editXml!");
}
try {
mXmlEditOK = true;
final AndroidEditor editor = mNode.getEditor();
if (editor instanceof LayoutEditor) {
// Create an undo wrapper, which takes a runnable
((LayoutEditor) editor).wrapUndoRecording(
undoName,
new Runnable() {
public void run() {
// Create an edit-XML wrapper, which takes a runnable
editor.editXmlModel(new Runnable() {
public void run() {
// Finally execute the closure that will act on the XML
c.call(this);
}
});
}
});
}
} finally {
mXmlEditOK = false;
}
}
private void checkEditOK() {
if (!mXmlEditOK) {
throw new RuntimeException("Error: XML edit call without using INodeProxy.editXml!");
}
}
public INodeProxy createChild(String viewFqcn) {
checkEditOK();
// Find the descriptor for this FQCN
ViewElementDescriptor vd = getFqcnViewDescritor(viewFqcn);
if (vd == null) {
debugPrintf("Can't create a new %s element", viewFqcn);
return null;
}
// TODO use UiElementNode.insertNewUiChild() to control the position, which is
// needed for a relative layout.
UiElementNode uiNew = mNode.appendNewUiChild(vd);
// TODO we probably want to defer that to the GRE to use IViewRule#getDefaultAttributes()
DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/);
Node xmlNode = uiNew.createXmlNode();
if (!(uiNew instanceof UiViewElementNode) || xmlNode == null) {
// Both things are not supposed to happen. When they do, we're in big trouble.
// We don't really know how to revert the state at this point and the UI model is
// now out of sync with the XML model.
// Panic ensues.
// The best bet is to abort now. The edit wrapper will release the edit and the
// XML/UI should get reloaded properly (with a likely invalid XML.)
debugPrintf("Failed to create a new %s element", viewFqcn);
throw new RuntimeException("XML node creation failed."); //$NON-NLS-1$
}
return new NodeProxy((UiViewElementNode) uiNew, null);
}
public boolean setAttribute(String attributeName, String value) {
checkEditOK();
UiAttributeNode attr = mNode.setAttributeValue(attributeName, value, true /* override */);
mNode.commitDirtyAttributesToXml();
return attr != null;
}
// --- internal helpers ---
/**
* Returns a given XML attribute.
* @param attrName The local name of the attribute.
* @return the attribute as a {@link String}, if it exists, or <code>null</code>
*/
private String getStringAttr(String attrName) {
// TODO this was just copy-pasted from the GLE1 edit code. Need to adapt to this context.
UiElementNode uiNode = mNode;
if (uiNode.getXmlNode() != null) {
Node xmlNode = uiNode.getXmlNode();
if (xmlNode != null) {
NamedNodeMap nodeAttributes = xmlNode.getAttributes();
if (nodeAttributes != null) {
Node attr = nodeAttributes.getNamedItemNS(SdkConstants.NS_RESOURCES, attrName);
if (attr != null) {
return attr.getNodeValue();
}
}
}
}
return null;
}
/**
* Helper methods 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
* (which shouldn't really happen since at this point the SDK should be fully loaded and
* isn't reloading, or we wouldn't be here editing XML for a groovy script.)
*/
private ViewElementDescriptor getFqcnViewDescritor(String fqcn) {
AndroidEditor editor = mNode.getEditor();
if (editor != null) {
AndroidTargetData data = editor.getTargetData();
if (data != null) {
LayoutDescriptors layoutDesc = data.getLayoutDescriptors();
if (layoutDesc != null) {
DocumentDescriptor docDesc = layoutDesc.getDescriptor();
if (docDesc != null) {
return internalFindFqcnViewDescritor(fqcn, docDesc.getChildren(), null);
}
}
}
}
return null;
}
/**
* 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 cildren 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 internalFindFqcnViewDescritor(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 =
internalFindFqcnViewDescritor(fqcn, desc.getChildren(), visited);
if (vd != null) {
return vd;
}
}
}
}
return null;
}
}