/*
* Copyright (C) 2010 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.adt.gscripts;
/**
* Common IViewRule processing to all view and layout classes.
*/
public class BaseView implements IViewRule {
/**
* Namespace for the Android resource XML,
* i.e. "http://schemas.android.com/apk/res/android"
*/
public static String ANDROID_URI = "http://schemas.android.com/apk/res/android";
// Some common Android layout attribute names used by the view rules.
// All these belong to the attribute namespace ANDROID_URI.
public static String ATTR_ID = "id";
public static String ATTR_TEXT = "text";
public static String ATTR_LAYOUT_WIDTH = "layout_width";
public static String ATTR_LAYOUT_HEIGHT = "layout_height";
// Some common Android layout attribute values used by the view rules.
public static String VALUE_FILL_PARENT = "fill_parent";
public static String VALUE_MATCH_PARENT = "match_parent"; // like fill_parent for API 8
public static String VALUE_WRAP_CONTENT = "wrap_content";
// Cache of attributes. Key is FQCN of a node mixed with its view hierarchy parent.
// Values are a custom map as needed by getContextMenu.
public Map mAttributesMap = [:];
public boolean onInitialize(String fqcn) {
// This base rule can handle any class so we don't need to filter on FQCN.
// Derived classes should do so if they can handle some subclasses.
// For debugging and as an example of how to use the injected _rules_engine property.
// _rules_engine.debugPrintf("Initialize() of %s", _rules_engine.getFqcn());
// If onInitialize returns false, it means it can't handle the given FQCN and
// will be unloaded.
return true;
}
public void onDispose() {
// Nothing to dispose.
}
public String getDisplayName() {
// Default is to not override the selection display name.
return null;
}
public Map, ?> getDefaultAttributes() {
// The base rule does not have any custom default attributes.
return null;
}
// === Context Menu ===
/**
* Generate custom actions for the context menu:
* - Explicit layout_width and layout_height attributes.
* - List of all other simple toggle attributes.
*/
public List getContextMenu(INode selectedNode) {
// Compute the key for mAttributesMap. This depends on the type of this node and
// its parent in the view hierarchy.
def key = selectedNode.getFqcn() + "_";
def parent = selectedNode.getParent();
if (parent) key = key + parent.getFqcn();
def custom_w = "Custom...";
def curr_w = selectedNode.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WIDTH);
if (curr_w == VALUE_FILL_PARENT) {
curr_w = VALUE_MATCH_PARENT;
} else if (curr_w != VALUE_WRAP_CONTENT && curr_w != VALUE_MATCH_PARENT) {
curr_w = "zcustom";
if (!!curr_w) {
custom_w = "Custom: ${curr_w}"
}
}
def custom_h = "Custom...";
def curr_h = selectedNode.getStringAttr(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
if (curr_h == VALUE_FILL_PARENT) {
curr_h = VALUE_MATCH_PARENT;
} else if (curr_h != VALUE_WRAP_CONTENT && curr_h != VALUE_MATCH_PARENT) {
curr_h = "zcustom";
if (!!curr_h) {
custom_h = "Custom: ${curr_h}"
}
}
def onChange = { MenuAction.Action action, String valueId, Boolean newValue ->
def actionId = action.getId();
def node = selectedNode;
switch (actionId) {
case "layout_1width":
if (!valueId.startsWith("z")) {
node.editXml("Change attribute " + ATTR_LAYOUT_WIDTH) {
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, valueId);
}
}
return;
case "layout_2height":
if (!valueId.startsWith("z")) {
node.editXml("Change attribute " + ATTR_LAYOUT_HEIGHT) {
node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, valueId);
}
}
return;
}
if (actionId.startsWith("@prop@")) {
actionId = actionId.substring(6);
def props = mAttributesMap[key];
def prop = props?.get(actionId);
if (prop) {
node.editXml("Change attribute " + actionId) {
if (prop.isToggle) {
// case of toggle
String value = "";
switch(valueId) {
case "1t":
value = newValue ? "true" : "";
break;
case "2f":
value = newValue ? "false" : "";
break;
}
node.setAttribute(ANDROID_URI, actionId, value);
} else if (prop.isFlag) {
// case of a flag
def values = "";
if (valueId != "~2clr") {
values = node.getStringAttr(ANDROID_URI, actionId);
if (!values) {
values = [] as Set;
} else {
values = ([] as Set) + (values.split("\\|") as Set);
}
if (newValue) {
values << valueId;
} else {
values = values - valueId;
}
values = values.join("|");
}
node.setAttribute(ANDROID_URI, actionId, values);
} else {
// case of an enum
def value = "";
if (valueId != "~2clr") {
value = newValue ? valueId : "";
}
node.setAttribute(ANDROID_URI, actionId, value);
}
}
}
}
}
def list1 = [
new MenuAction.Choices("layout_1width", "Layout Width",
[ "wrap_content": "Wrap Content",
"match_parent": "Match Parent",
"zcustom": custom_w ],
curr_w,
onChange ),
new MenuAction.Choices("layout_2height", "Layout Height",
[ "wrap_content": "Wrap Content",
"match_parent": "Match Parent",
"zcustom": custom_h ],
curr_h,
onChange ),
new MenuAction.Group("properties", "Properties"),
];
// Prepare a list of all simple properties.
def props = mAttributesMap[key];
if (props == null) {
// Prepare the property map
props = [:]
for (attrInfo in selectedNode.getDeclaredAttributes()) {
def id = attrInfo?.getName();
if (id == null || id == ATTR_LAYOUT_WIDTH || id == ATTR_LAYOUT_HEIGHT) {
// Layout width/height are already handled at the root level
continue;
}
def formats = attrInfo?.getFormats();
if (formats == null) {
continue;
}
def title = prettyName(id);
if (IAttributeInfo.Format.BOOLEAN in formats) {
props[id] = [ isToggle: true, title: title ];
} else if (IAttributeInfo.Format.ENUM in formats) {
// Convert each enum into a map id=>title
def values = [:];
attrInfo.getEnumValues().each { e -> values[e] = prettyName(e) }
props[id]= [ isToggle: false,
isFlag: false,
title: title,
choices: values ];
} else if (IAttributeInfo.Format.FLAG in formats) {
// Convert each flag into a map id=>title
def values = [:];
attrInfo.getFlagValues().each { e -> values[e] = prettyName(e) };
props[id] = [ isToggle: false,
isFlag: true,
title: title,
choices: values ];
}
}
mAttributesMap[key] = props;
}
def list2 = [];
props.each { id, p ->
def a = null;
if (p.isToggle) {
// Toggles are handled as a multiple-choice between true, false and nothing (clear)
def value = selectedNode.getStringAttr(ANDROID_URI, id);
if (value != null) value = value.toLowerCase();
switch(value) {
case "true":
value = "1t";
break;
case "false":
value = "2f";
break;
default:
value = "4clr";
break;
}
a = new MenuAction.Choices(
"@prop@" + id,
p.title,
[ "1t": "True",
"2f": "False",
"3sep": MenuAction.Choices.SEPARATOR,
"4clr": "Clear" ],
value,
"properties",
onChange);
} else {
// Enum or flags. Their possible values are the multiple-choice items,
// with an extra "clear" option to remove everything.
def current = selectedNode.getStringAttr(ANDROID_URI, id);
if (!current) {
current = "~2clr";
}
a = new MenuAction.Choices(
"@prop@" + id,
p.title,
p.choices + [ "~1sep": MenuAction.Choices.SEPARATOR,
"~2clr": "Clear " + (p.isFlag ? "flag" : "enum") ],
current,
"properties",
onChange);
}
if (a) list2.add(a);
}
return list1 + list2;
}
public String prettyName(String name) {
if (name) {
def c = name[0];
name = name.replaceFirst(c, c.toUpperCase());
name = name.replace("_", " ");
}
return name;
}
// ==== Selection ====
public void onSelected(IGraphics gc, INode selectedNode,
String displayName, boolean isMultipleSelection) {
Rect r = selectedNode.getBounds();
if (!r.isValid()) {
return;
}
gc.setLineWidth(1);
gc.setLineStyle(IGraphics.LineStyle.LINE_SOLID);
gc.drawRect(r);
if (displayName == null || isMultipleSelection) {
return;
}
int xs = r.x + 2;
int ys = r.y - gc.getFontHeight();
if (ys < 0) {
ys = r.y + r.h;
}
gc.drawString(displayName, xs, ys);
}
public void onChildSelected(IGraphics gc, INode parentNode, INode childNode) {
Rect rp = parentNode.getBounds();
Rect rc = childNode.getBounds();
if (rp.isValid() && rc.isValid()) {
gc.setLineWidth(1);
gc.setLineStyle(IGraphics.LineStyle.LINE_DOT);
// top line
int m = rc.x + rc.w / 2;
gc.drawLine(m, rc.y, m, rp.y);
// bottom line
gc.drawLine(m, rc.y + rc.h, m, rp.y + rp.h);
// left line
m = rc.y + rc.h / 2;
gc.drawLine(rc.x, m, rp.x, m);
// right line
gc.drawLine(rc.x + rc.w, m, rp.x + rp.w, m);
}
}
// ==== Drag'n'drop support ====
// By default Views do not accept drag'n'drop.
public DropFeedback onDropEnter(INode targetNode, IDragElement[] elements) {
return null;
}
public DropFeedback onDropMove(INode targetNode, IDragElement[] elements,
DropFeedback feedback, Point p) {
return null;
}
public void onDropLeave(INode targetNode, IDragElement[] elements, DropFeedback feedback) {
// ignore
}
public void onDropped(INode targetNode, IDragElement[] elements, DropFeedback feedback, Point p) {
// ignore
}
// ==== Paste support ====
/**
* Most views can't accept children so there's nothing to paste on them.
* In this case, defer the call to the parent layout and use the target node as
* an indication of where to paste.
*/
public void onPaste(INode targetNode, IDragElement[] elements) {
//
def parent = targetNode.getParent();
def parentFqcn = parent?.getFqcn();
def parentRule = _rules_engine.loadRule(parentFqcn);
if (parentRule instanceof BaseLayout) {
parentRule.onPasteBeforeChild(parent, targetNode, elements);
}
}
}