diff options
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.java | 876 |
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; + } +} |