aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java876
1 files changed, 876 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java
new file mode 100644
index 000000000..8f9923749
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java
@@ -0,0 +1,876 @@
+/*
+ * 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 static com.android.SdkConstants.ANDROID_WIDGET_PREFIX;
+import static com.android.SdkConstants.VIEW_MERGE;
+import static com.android.SdkConstants.VIEW_TAG;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.DropFeedback;
+import com.android.ide.common.api.IDragElement;
+import com.android.ide.common.api.IGraphics;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.InsertType;
+import com.android.ide.common.api.Point;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.RuleAction;
+import com.android.ide.common.api.SegmentType;
+import com.android.ide.common.layout.ViewRule;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GCWrapper;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SimpleElement;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+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.IProject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The rule engine manages the layout rules and interacts with them.
+ * There's one {@link RulesEngine} instance per layout editor.
+ * Each instance has 2 sets of rules: the static ADT rules (shared across all instances)
+ * and the project specific rules (local to the current instance / layout editor).
+ */
+public class RulesEngine {
+ private final IProject mProject;
+ private final Map<Object, IViewRule> mRulesCache = new HashMap<Object, IViewRule>();
+
+ /**
+ * The type of any upcoming node manipulations performed by the {@link IViewRule}s.
+ * When actions are performed in the tool (like a paste action, or a drag from palette,
+ * or a drag move within the canvas, etc), these are different types of inserts,
+ * and we don't want to have the rules track them closely (and pass them back to us
+ * in the {@link INode#insertChildAt} methods etc), so instead we track the state
+ * here on behalf of the currently executing rule.
+ */
+ private InsertType mInsertType = InsertType.CREATE;
+
+ /**
+ * Per-project loader for custom view rules
+ */
+ private RuleLoader mRuleLoader;
+ private ClassLoader mUserClassLoader;
+
+ /**
+ * The editor which owns this {@link RulesEngine}
+ */
+ private final GraphicalEditorPart mEditor;
+
+ /**
+ * Creates a new {@link RulesEngine} associated with the selected project.
+ * <p/>
+ * The rules engine will look in the project for a tools jar to load custom view rules.
+ *
+ * @param editor the editor which owns this {@link RulesEngine}
+ * @param project A non-null open project.
+ */
+ public RulesEngine(GraphicalEditorPart editor, IProject project) {
+ mProject = project;
+ mEditor = editor;
+
+ mRuleLoader = RuleLoader.get(project);
+ }
+
+ /**
+ * Returns the {@link IProject} on which the {@link RulesEngine} was created.
+ */
+ public IProject getProject() {
+ return mProject;
+ }
+
+ /**
+ * Returns the {@link GraphicalEditorPart} for which the {@link RulesEngine} was
+ * created.
+ *
+ * @return the associated editor
+ */
+ public GraphicalEditorPart getEditor() {
+ return mEditor;
+ }
+
+ /**
+ * Called by the owner of the {@link RulesEngine} when it is going to be disposed.
+ * This frees some resources, such as the project's folder monitor.
+ */
+ public void dispose() {
+ clearCache();
+ }
+
+ /**
+ * Invokes {@link IViewRule#getDisplayName()} on the rule matching the specified element.
+ *
+ * @param element The view element to target. Can be null.
+ * @return Null if the rule failed, there's no rule or the rule does not want to override
+ * the display name. Otherwise, a string as returned by the rule.
+ */
+ public String callGetDisplayName(UiViewElementNode element) {
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(element);
+
+ if (rule != null) {
+ try {
+ return rule.getDisplayName();
+
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.getDisplayName() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Invokes {@link IViewRule#addContextMenuActions(List, INode)} on the rule matching the specified element.
+ *
+ * @param selectedNode The node selected. Never null.
+ * @return Null if the rule failed, there's no rule or the rule does not provide
+ * any custom menu actions. Otherwise, a list of {@link RuleAction}.
+ */
+ @Nullable
+ public List<RuleAction> callGetContextMenu(NodeProxy selectedNode) {
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(selectedNode.getNode());
+
+ if (rule != null) {
+ try {
+ mInsertType = InsertType.CREATE;
+ List<RuleAction> actions = new ArrayList<RuleAction>();
+ rule.addContextMenuActions(actions, selectedNode);
+ Collections.sort(actions);
+
+ return actions;
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.getContextMenu() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Calls the selected node to return its default action
+ *
+ * @param selectedNode the node to apply the action to
+ * @return the default action id
+ */
+ public String callGetDefaultActionId(@NonNull NodeProxy selectedNode) {
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(selectedNode.getNode());
+
+ if (rule != null) {
+ try {
+ mInsertType = InsertType.CREATE;
+ return rule.getDefaultActionId(selectedNode);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.getDefaultAction() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Invokes {@link IViewRule#addLayoutActions(List, INode, List)} on the rule
+ * matching the specified element.
+ *
+ * @param actions The list of actions to add layout actions into
+ * @param parentNode The layout node
+ * @param children The selected children of the node, if any (used to
+ * initialize values of child layout controls, if applicable)
+ * @return Null if the rule failed, there's no rule or the rule does not
+ * provide any custom menu actions. Otherwise, a list of
+ * {@link RuleAction}.
+ */
+ public List<RuleAction> callAddLayoutActions(List<RuleAction> actions,
+ NodeProxy parentNode, List<NodeProxy> children ) {
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(parentNode.getNode());
+
+ if (rule != null) {
+ try {
+ mInsertType = InsertType.CREATE;
+ rule.addLayoutActions(actions, parentNode, children);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.getContextMenu() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Invokes {@link IViewRule#getSelectionHint(INode, INode)}
+ * on the rule matching the specified element.
+ *
+ * @param parentNode The parent of the node selected. Never null.
+ * @param childNode The child node that was selected. Never null.
+ * @return a list of strings to be displayed, or null or empty to display nothing
+ */
+ public List<String> callGetSelectionHint(NodeProxy parentNode, NodeProxy childNode) {
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(parentNode.getNode());
+
+ if (rule != null) {
+ try {
+ return rule.getSelectionHint(parentNode, childNode);
+
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.getSelectionHint() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+
+ return null;
+ }
+
+ public void callPaintSelectionFeedback(GCWrapper gcWrapper, NodeProxy parentNode,
+ List<? extends INode> childNodes, Object view) {
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(parentNode.getNode());
+
+ if (rule != null) {
+ try {
+ rule.paintSelectionFeedback(gcWrapper, parentNode, childNodes, view);
+
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.callPaintSelectionFeedback() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+ }
+
+ /**
+ * Called when the d'n'd starts dragging over the target node.
+ * If interested, returns a DropFeedback passed to onDrop/Move/Leave/Paint.
+ * If not interested in drop, return false.
+ * Followed by a paint.
+ */
+ public DropFeedback callOnDropEnter(NodeProxy targetNode,
+ Object targetView, IDragElement[] elements) {
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(targetNode.getNode());
+
+ if (rule != null) {
+ try {
+ return rule.onDropEnter(targetNode, targetView, elements);
+
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onDropEnter() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Called after onDropEnter.
+ * Returns a DropFeedback passed to onDrop/Move/Leave/Paint (typically same
+ * as input one).
+ */
+ public DropFeedback callOnDropMove(NodeProxy targetNode,
+ IDragElement[] elements,
+ DropFeedback feedback,
+ Point where) {
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(targetNode.getNode());
+
+ if (rule != null) {
+ try {
+ return rule.onDropMove(targetNode, elements, feedback, where);
+
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onDropMove() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Called when drop leaves the target without actually dropping
+ */
+ public void callOnDropLeave(NodeProxy targetNode,
+ IDragElement[] elements,
+ DropFeedback feedback) {
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(targetNode.getNode());
+
+ if (rule != null) {
+ try {
+ rule.onDropLeave(targetNode, elements, feedback);
+
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onDropLeave() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+ }
+
+ /**
+ * Called when drop is released over the target to perform the actual drop.
+ */
+ public void callOnDropped(NodeProxy targetNode,
+ IDragElement[] elements,
+ DropFeedback feedback,
+ Point where,
+ InsertType insertType) {
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(targetNode.getNode());
+
+ if (rule != null) {
+ try {
+ mInsertType = insertType;
+ rule.onDropped(targetNode, elements, feedback, where);
+
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onDropped() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+ }
+
+ /**
+ * Called when a paint has been requested via DropFeedback.
+ */
+ public void callDropFeedbackPaint(IGraphics gc,
+ NodeProxy targetNode,
+ DropFeedback feedback) {
+ if (gc != null && feedback != null && feedback.painter != null) {
+ try {
+ feedback.painter.paint(gc, targetNode, feedback);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "DropFeedback.painter failed: %s",
+ e.toString());
+ }
+ }
+ }
+
+ /**
+ * Called when pasting elements in an existing document on the selected target.
+ *
+ * @param targetNode The first node selected.
+ * @param targetView The view object for the target node, or null if not known
+ * @param pastedElements The elements being pasted.
+ * @return the parent node the paste was applied into
+ */
+ public NodeProxy callOnPaste(NodeProxy targetNode, Object targetView,
+ SimpleElement[] pastedElements) {
+
+ // Find a target which accepts children. If you for example select a button
+ // and attempt to paste, this will reselect the parent of the button as the paste
+ // target. (This is a loop rather than just checking the direct parent since
+ // we will soon ask each child whether they are *willing* to accept the new child.
+ // A ScrollView for example, which only accepts one child, might also say no
+ // and delegate to its parent in turn.
+ INode parent = targetNode;
+ while (parent instanceof NodeProxy) {
+ NodeProxy np = (NodeProxy) parent;
+ if (np.getNode() != null && np.getNode().getDescriptor() != null) {
+ ElementDescriptor descriptor = np.getNode().getDescriptor();
+ if (descriptor.hasChildren()) {
+ targetNode = np;
+ break;
+ }
+ }
+ parent = parent.getParent();
+ }
+
+ // try to find a rule for this element's FQCN
+ IViewRule rule = loadRule(targetNode.getNode());
+
+ if (rule != null) {
+ try {
+ mInsertType = InsertType.PASTE;
+ rule.onPaste(targetNode, targetView, pastedElements);
+
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onPaste() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+
+ return targetNode;
+ }
+
+ // ---- Resize operations ----
+
+ public DropFeedback callOnResizeBegin(NodeProxy child, NodeProxy parent, Rect newBounds,
+ SegmentType horizontalEdge, SegmentType verticalEdge, Object childView,
+ Object parentView) {
+ IViewRule rule = loadRule(parent.getNode());
+
+ if (rule != null) {
+ try {
+ return rule.onResizeBegin(child, parent, horizontalEdge, verticalEdge,
+ childView, parentView);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onResizeBegin() failed: %s", rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+
+ return null;
+ }
+
+ public void callOnResizeUpdate(DropFeedback feedback, NodeProxy child, NodeProxy parent,
+ Rect newBounds, int modifierMask) {
+ IViewRule rule = loadRule(parent.getNode());
+
+ if (rule != null) {
+ try {
+ rule.onResizeUpdate(feedback, child, parent, newBounds, modifierMask);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onResizeUpdate() failed: %s", rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+ }
+
+ public void callOnResizeEnd(DropFeedback feedback, NodeProxy child, NodeProxy parent,
+ Rect newBounds) {
+ IViewRule rule = loadRule(parent.getNode());
+
+ if (rule != null) {
+ try {
+ rule.onResizeEnd(feedback, child, parent, newBounds);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onResizeEnd() failed: %s", rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+ }
+
+ // ---- Creation customizations ----
+
+ /**
+ * Invokes the create hooks ({@link IViewRule#onCreate},
+ * {@link IViewRule#onChildInserted} when a new child has been created/pasted/moved, and
+ * is inserted into a given parent. The parent may be null (for example when rendering
+ * top level items for preview).
+ *
+ * @param editor the XML editor to apply edits to the model for (performed by view
+ * rules)
+ * @param parentNode the parent XML node, or null if unknown
+ * @param childNode the XML node of the new node, never null
+ * @param overrideInsertType If not null, specifies an explicit insert type to use for
+ * edits made during the customization
+ */
+ public void callCreateHooks(
+ AndroidXmlEditor editor,
+ NodeProxy parentNode, NodeProxy childNode,
+ InsertType overrideInsertType) {
+ IViewRule parentRule = null;
+
+ if (parentNode != null) {
+ UiViewElementNode parentUiNode = parentNode.getNode();
+ parentRule = loadRule(parentUiNode);
+ }
+
+ if (overrideInsertType != null) {
+ mInsertType = overrideInsertType;
+ }
+
+ UiViewElementNode newUiNode = childNode.getNode();
+ IViewRule childRule = loadRule(newUiNode);
+ if (childRule != null || parentRule != null) {
+ callCreateHooks(editor, mInsertType, parentRule, parentNode,
+ childRule, childNode);
+ }
+ }
+
+ private static void callCreateHooks(
+ final AndroidXmlEditor editor, final InsertType insertType,
+ final IViewRule parentRule, final INode parentNode,
+ final IViewRule childRule, final INode newNode) {
+ // Notify the parent about the new child in case it wants to customize it
+ // (For example, a ScrollView parent can go and set all its children's layout params to
+ // fill the parent.)
+ if (!editor.isEditXmlModelPending()) {
+ editor.wrapEditXmlModel(new Runnable() {
+ @Override
+ public void run() {
+ callCreateHooks(editor, insertType,
+ parentRule, parentNode, childRule, newNode);
+ }
+ });
+ return;
+ }
+
+ if (parentRule != null) {
+ parentRule.onChildInserted(newNode, parentNode, insertType);
+ }
+
+ // Look up corresponding IViewRule, and notify the rule about
+ // this create action in case it wants to customize the new object.
+ // (For example, a rule for TabHosts can go and create a default child tab
+ // when you create it.)
+ if (childRule != null) {
+ childRule.onCreate(newNode, parentNode, insertType);
+ }
+
+ if (parentNode != null) {
+ ((NodeProxy) parentNode).applyPendingChanges();
+ }
+ }
+
+ /**
+ * Set the type of insert currently in progress
+ *
+ * @param insertType the insert type to use for the next operation
+ */
+ public void setInsertType(InsertType insertType) {
+ mInsertType = insertType;
+ }
+
+ /**
+ * Return the type of insert currently in progress
+ *
+ * @return the type of insert currently in progress
+ */
+ public InsertType getInsertType() {
+ return mInsertType;
+ }
+
+ // ---- Deletion ----
+
+ public void callOnRemovingChildren(NodeProxy parentNode,
+ List<INode> children) {
+ if (parentNode != null) {
+ UiViewElementNode parentUiNode = parentNode.getNode();
+ IViewRule parentRule = loadRule(parentUiNode);
+ if (parentRule != null) {
+ try {
+ parentRule.onRemovingChildren(children, parentNode,
+ mInsertType == InsertType.MOVE_WITHIN);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onDispose() failed: %s",
+ parentRule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+ }
+ }
+
+ // ---- private ---
+
+ /**
+ * Returns the descriptor for the base View class.
+ * This could be null if the SDK or the given platform target hasn't loaded yet.
+ */
+ private ViewElementDescriptor getBaseViewDescriptor() {
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(mProject);
+ if (target != null) {
+ AndroidTargetData data = currentSdk.getTargetData(target);
+ return data.getLayoutDescriptors().getBaseViewDescriptor();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Clear the Rules cache. Calls onDispose() on each rule.
+ */
+ private void clearCache() {
+ // The cache can contain multiple times the same rule instance for different
+ // keys (e.g. the UiViewElementNode key vs. the FQCN string key.) So transfer
+ // all values to a unique set.
+ HashSet<IViewRule> rules = new HashSet<IViewRule>(mRulesCache.values());
+
+ mRulesCache.clear();
+
+ for (IViewRule rule : rules) {
+ if (rule != null) {
+ try {
+ rule.onDispose();
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onDispose() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+ }
+ }
+ }
+
+ /**
+ * Checks whether the project class loader has changed, and if so
+ * unregisters any view rules that use classes from the old class loader. It
+ * then returns the class loader to be used.
+ */
+ private ClassLoader updateClassLoader() {
+ ClassLoader classLoader = mRuleLoader.getClassLoader();
+ if (mUserClassLoader != null && classLoader != mUserClassLoader) {
+ // We have to unload all the IViewRules from the old class
+ List<Object> dispose = new ArrayList<Object>();
+ for (Map.Entry<Object, IViewRule> entry : mRulesCache.entrySet()) {
+ IViewRule rule = entry.getValue();
+ if (rule.getClass().getClassLoader() == mUserClassLoader) {
+ dispose.add(entry.getKey());
+ }
+ }
+ for (Object object : dispose) {
+ mRulesCache.remove(object);
+ }
+ }
+
+ mUserClassLoader = classLoader;
+ return mUserClassLoader;
+ }
+
+ /**
+ * Load a rule using its descriptor. This will try to first load the rule using its
+ * actual FQCN and if that fails will find the first parent that works in the view
+ * hierarchy.
+ */
+ private IViewRule loadRule(UiViewElementNode element) {
+ if (element == null) {
+ return null;
+ }
+
+ String targetFqcn = null;
+ ViewElementDescriptor targetDesc = null;
+
+ ElementDescriptor d = element.getDescriptor();
+ if (d instanceof ViewElementDescriptor) {
+ targetDesc = (ViewElementDescriptor) d;
+ }
+ if (d == null || !(d instanceof ViewElementDescriptor)) {
+ // This should not happen. All views should have some kind of *view* element
+ // descriptor. Maybe the project is not complete and doesn't build or something.
+ // In this case, we'll use the descriptor of the base android View class.
+ targetDesc = getBaseViewDescriptor();
+ }
+
+ // Check whether any of the custom view .jar files have changed and if so
+ // unregister previously cached view rules to force a new view rule to be loaded.
+ updateClassLoader();
+
+ // Return the rule if we find it in the cache, even if it was stored as null
+ // (which means we didn't find it earlier, so don't look for it again)
+ IViewRule rule = mRulesCache.get(targetDesc);
+ if (rule != null || mRulesCache.containsKey(targetDesc)) {
+ return rule;
+ }
+
+ // Get the descriptor and loop through the super class hierarchy
+ for (ViewElementDescriptor desc = targetDesc;
+ desc != null;
+ desc = desc.getSuperClassDesc()) {
+
+ // Get the FQCN of this View
+ String fqcn = desc.getFullClassName();
+ if (fqcn == null) {
+ // Shouldn't be happening.
+ return null;
+ }
+
+ // The first time we keep the FQCN around as it's the target class we were
+ // initially trying to load. After, as we move through the hierarchy, the
+ // target FQCN remains constant.
+ if (targetFqcn == null) {
+ targetFqcn = fqcn;
+ }
+
+ if (fqcn.indexOf('.') == -1) {
+ // Deal with unknown descriptors; these lack the full qualified path and
+ // elements in the layout without a package are taken to be in the
+ // android.widget package.
+ fqcn = ANDROID_WIDGET_PREFIX + fqcn;
+ }
+
+ // Try to find a rule matching the "real" FQCN. If we find it, we're done.
+ // If not, the for loop will move to the parent descriptor.
+ rule = loadRule(fqcn, targetFqcn);
+ if (rule != null) {
+ // We found one.
+ // As a side effect, loadRule() also cached the rule using the target FQCN.
+ return rule;
+ }
+ }
+
+ // Memorize in the cache that we couldn't find a rule for this descriptor
+ mRulesCache.put(targetDesc, null);
+ return null;
+ }
+
+ /**
+ * Try to load a rule given a specific FQCN. This looks for an exact match in either
+ * the ADT scripts or the project scripts and does not look at parent hierarchy.
+ * <p/>
+ * Once a rule is found (or not), it is stored in a cache using its target FQCN
+ * so we don't try to reload it.
+ * <p/>
+ * The real FQCN is the actual rule class we're loading, e.g. "android.view.View"
+ * where target FQCN is the class we were initially looking for, which might be the same as
+ * the real FQCN or might be a derived class, e.g. "android.widget.TextView".
+ *
+ * @param realFqcn The FQCN of the rule class actually being loaded.
+ * @param targetFqcn The FQCN of the class actually processed, which might be different from
+ * the FQCN of the rule being loaded.
+ */
+ IViewRule loadRule(String realFqcn, String targetFqcn) {
+ if (realFqcn == null || targetFqcn == null) {
+ return null;
+ }
+
+ // Return the rule if we find it in the cache, even if it was stored as null
+ // (which means we didn't find it earlier, so don't look for it again)
+ IViewRule rule = mRulesCache.get(realFqcn);
+ if (rule != null || mRulesCache.containsKey(realFqcn)) {
+ return rule;
+ }
+
+ // Look for class via reflection
+ try {
+ // For now, we package view rules for the builtin Android views and
+ // widgets with the tool in a special package, so look there rather
+ // than in the same package as the widgets.
+ String ruleClassName;
+ ClassLoader classLoader;
+ if (realFqcn.startsWith("android.") || //$NON-NLS-1$
+ realFqcn.equals(VIEW_MERGE) ||
+ realFqcn.endsWith(".GridLayout") || //$NON-NLS-1$ // Temporary special case
+ // FIXME: Remove this special case as soon as we pull
+ // the MapViewRule out of this code base and bundle it
+ // with the add ons
+ realFqcn.startsWith("com.google.android.maps.")) { //$NON-NLS-1$
+ // This doesn't handle a case where there are name conflicts
+ // (e.g. where there are multiple different views with the same
+ // class name and only differing in package names, but that's a
+ // really bad practice in the first place, and if that situation
+ // should come up in the API we can enhance this algorithm.
+ String packageName = ViewRule.class.getName();
+ packageName = packageName.substring(0, packageName.lastIndexOf('.'));
+ classLoader = RulesEngine.class.getClassLoader();
+ int dotIndex = realFqcn.lastIndexOf('.');
+ String baseName = realFqcn.substring(dotIndex+1);
+ // Capitalize rule class name to match naming conventions, if necessary (<merge>)
+ if (Character.isLowerCase(baseName.charAt(0))) {
+ if (baseName.equals(VIEW_TAG)) {
+ // Hack: ViewRule is generic for the "View" class, so we can't use it
+ // for the special XML "view" tag (lowercase); instead, the rule is
+ // named "ViewTagRule" instead.
+ baseName = "ViewTag"; //$NON-NLS-1$
+ }
+ baseName = Character.toUpperCase(baseName.charAt(0)) + baseName.substring(1);
+ }
+ ruleClassName = packageName + "." + //$NON-NLS-1$
+ baseName + "Rule"; //$NON-NLS-1$
+ } else {
+ // Initialize the user-classpath for 3rd party IViewRules, if necessary
+ classLoader = updateClassLoader();
+ if (classLoader == null) {
+ // The mUserClassLoader can be null; this is the typical scenario,
+ // when the user is only using builtin layout rules.
+ // This means however we can't resolve this fqcn since it's not
+ // in the name space of the builtin rules.
+ mRulesCache.put(realFqcn, null);
+ return null;
+ }
+
+ // For other (3rd party) widgets, look in the same package (though most
+ // likely not in the same jar!)
+ ruleClassName = realFqcn + "Rule"; //$NON-NLS-1$
+ }
+
+ Class<?> clz = Class.forName(ruleClassName, true, classLoader);
+ rule = (IViewRule) clz.newInstance();
+ return initializeRule(rule, targetFqcn);
+ } catch (ClassNotFoundException ex) {
+ // Not an unexpected error - this means that there isn't a helper for this
+ // class.
+ } catch (InstantiationException e) {
+ // This is NOT an expected error: fail.
+ AdtPlugin.log(e, "load rule error (%s): %s", realFqcn, e.toString());
+ } catch (IllegalAccessException e) {
+ // This is NOT an expected error: fail.
+ AdtPlugin.log(e, "load rule error (%s): %s", realFqcn, e.toString());
+ }
+
+ // Memorize in the cache that we couldn't find a rule for this real FQCN
+ mRulesCache.put(realFqcn, null);
+ return null;
+ }
+
+ /**
+ * Initialize a rule we just loaded. The rule has a chance to examine the target FQCN
+ * and bail out.
+ * <p/>
+ * Contract: the rule is not in the {@link #mRulesCache} yet and this method will
+ * cache it using the target FQCN if the rule is accepted.
+ * <p/>
+ * The real FQCN is the actual rule class we're loading, e.g. "android.view.View"
+ * where target FQCN is the class we were initially looking for, which might be the same as
+ * the real FQCN or might be a derived class, e.g. "android.widget.TextView".
+ *
+ * @param rule A rule freshly loaded.
+ * @param targetFqcn The FQCN of the class actually processed, which might be different from
+ * the FQCN of the rule being loaded.
+ * @return The rule if accepted, or null if the rule can't handle that FQCN.
+ */
+ private IViewRule initializeRule(IViewRule rule, String targetFqcn) {
+
+ try {
+ if (rule.onInitialize(targetFqcn, new ClientRulesEngine(this, targetFqcn))) {
+ // Add it to the cache and return it
+ mRulesCache.put(targetFqcn, rule);
+ return rule;
+ } else {
+ rule.onDispose();
+ }
+ } catch (Exception e) {
+ AdtPlugin.log(e, "%s.onInit() failed: %s",
+ rule.getClass().getSimpleName(),
+ e.toString());
+ }
+
+ return null;
+ }
+}