New custom widgets library

Adds the widgets needed by the theme editor as a jar file that will be
linked from AS. The controls currently are:
- ThemePreviewLayout: A custom layout that allows to dynamically reflow
  cards that display the theme preview.
- PressedButton: Custom button to display in a pressed state (currently
  we can only do it programatically).
- ErrorCatcher: View that can wrap other view and stops a view throwing
  exceptions that will break the theme editor rendering. This is
  currently used to wrap the custom controls loaded from the source
  code.

The new library currently depends on bridge to be able to use the logger
for the ErrorCatcher view. The ErrorCatcher view is a temporary solution
that will be replaced in a future CL and the dependency will be removed.

Change-Id: I832c447e22e1381abff74c46c4282921b3f6fa23
diff --git a/tools/layoutlib/studio-custom-widgets/src/com/android/tools/idea/editors/theme/widgets/ErrorCatcher.java b/tools/layoutlib/studio-custom-widgets/src/com/android/tools/idea/editors/theme/widgets/ErrorCatcher.java
new file mode 100644
index 0000000..ecf39b3
--- /dev/null
+++ b/tools/layoutlib/studio-custom-widgets/src/com/android/tools/idea/editors/theme/widgets/ErrorCatcher.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.tools.idea.editors.theme.widgets;
+
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * {@link ViewGroup} that wraps another view and catches any possible exceptions that the child view
+ * might generate.
+ * This is used by the theme editor to stop custom views from breaking the preview.
+ */
+// TODO: This view is just a temporary solution that will be replaced by adding a try / catch
+// for custom views in the ClassConverter
+public class ErrorCatcher extends ViewGroup {
+    public ErrorCatcher(Context context) {
+        super(context);
+    }
+
+    public ErrorCatcher(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public ErrorCatcher(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public ErrorCatcher(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        assert getChildCount() == 1 : "ErrorCatcher can only have one child";
+
+        View child = getChildAt(0);
+        try {
+            measureChild(child, widthMeasureSpec, heightMeasureSpec);
+
+            setMeasuredDimension(resolveSize(child.getMeasuredWidth(), widthMeasureSpec),
+                    resolveSize(child.getMeasuredHeight(), heightMeasureSpec));
+        } catch (Throwable t) {
+            Bridge.getLog().warning(LayoutLog.TAG_BROKEN, "Failed to do onMeasure for view " +
+                    child.getClass().getCanonicalName(), t);
+            setMeasuredDimension(resolveSize(0, widthMeasureSpec),
+                    resolveSize(0, heightMeasureSpec));
+        }
+    }
+
+    @Override
+    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+        try {
+            return super.drawChild(canvas, child, drawingTime);
+        } catch (Throwable t) {
+            Bridge.getLog().warning(LayoutLog.TAG_BROKEN, "Failed to draw for view " +
+                    child.getClass().getCanonicalName(), t);
+        }
+
+        return false;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        assert getChildCount() == 1 : "ErrorCatcher can only have one child";
+
+        View child = getChildAt(0);
+        try {
+            child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
+        } catch (Throwable e) {
+            Bridge.getLog().warning(LayoutLog.TAG_BROKEN, "Failed to do onLayout for view " +
+                    child.getClass().getCanonicalName(), e);
+        }
+    }
+}
diff --git a/tools/layoutlib/studio-custom-widgets/src/com/android/tools/idea/editors/theme/widgets/PressedButton.java b/tools/layoutlib/studio-custom-widgets/src/com/android/tools/idea/editors/theme/widgets/PressedButton.java
new file mode 100644
index 0000000..4320157
--- /dev/null
+++ b/tools/layoutlib/studio-custom-widgets/src/com/android/tools/idea/editors/theme/widgets/PressedButton.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.tools.idea.editors.theme.widgets;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Button;
+
+@SuppressWarnings("unused")
+public class PressedButton extends Button {
+    public PressedButton(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        setPressed(true);
+        jumpDrawablesToCurrentState();
+    }
+}
diff --git a/tools/layoutlib/studio-custom-widgets/src/com/android/tools/idea/editors/theme/widgets/ThemePreviewLayout.java b/tools/layoutlib/studio-custom-widgets/src/com/android/tools/idea/editors/theme/widgets/ThemePreviewLayout.java
new file mode 100644
index 0000000..af89910
--- /dev/null
+++ b/tools/layoutlib/studio-custom-widgets/src/com/android/tools/idea/editors/theme/widgets/ThemePreviewLayout.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.tools.idea.editors.theme.widgets;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Custom layout used in the theme editor to display the component preview. It arranges the child
+ * Views as a grid of cards.
+ * <p/>
+ * The Views are measured and the maximum width and height are used to dimension all the child
+ * components. Any margin attributes from the children are ignored and only the item_margin element
+ * is used.
+ */
+@SuppressWarnings("unused")
+public class ThemePreviewLayout extends ViewGroup {
+    private final int mMaxColumns;
+    private final int mMaxColumnWidth;
+    private final int mMinColumnWidth;
+    private final int mItemHorizontalMargin;
+    private final int mItemVerticalMargin;
+
+    /** Item width to use for every card component. This includes margins. */
+    private int mItemWidth;
+    /** Item height to use for every card component. This includes margins. */
+    private int mItemHeight;
+
+    /** Calculated number of columns */
+    private int mNumColumns;
+
+    public ThemePreviewLayout(Context context) {
+        this(context, null);
+    }
+
+    public ThemePreviewLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public ThemePreviewLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+
+        if (attrs == null) {
+            mMaxColumnWidth = Integer.MAX_VALUE;
+            mMinColumnWidth = 0;
+            mMaxColumns = Integer.MAX_VALUE;
+            mItemHorizontalMargin = 0;
+            mItemVerticalMargin = 0;
+            return;
+        }
+
+        DisplayMetrics dm = getResources().getDisplayMetrics();
+        int maxColumnWidth = attrs.getAttributeIntValue(null, "max_column_width", Integer
+                .MAX_VALUE);
+        int minColumnWidth = attrs.getAttributeIntValue(null, "min_column_width", 0);
+        int itemHorizontalMargin = attrs.getAttributeIntValue(null, "item_horizontal_margin", 0);
+        int itemVerticalMargin = attrs.getAttributeIntValue(null, "item_vertical_margin", 0);
+
+        mMaxColumnWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                maxColumnWidth,
+                dm);
+        mMinColumnWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                minColumnWidth,
+                dm);
+        mItemHorizontalMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                itemHorizontalMargin,
+                dm);
+        mItemVerticalMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                itemVerticalMargin,
+                dm);
+        mMaxColumns = attrs.getAttributeIntValue(null, "max_columns", Integer.MAX_VALUE);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // Measure the column size.
+        // The column has a minimum width that will be used to calculate the maximum number of
+        // columns that we can fit in the available space.
+        //
+        // Once we have the maximum number of columns, we will span all columns width evenly to fill
+        // all the available space.
+        int wSize = MeasureSpec.getSize(widthMeasureSpec) - mPaddingLeft - mPaddingRight;
+
+        // Calculate the desired width of all columns and take the maximum.
+        // This step can be skipped if we have a fixed column height so we do not have to
+        // dynamically calculate it.
+        int childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+        int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+        int itemWidth = 0;
+        int itemHeight = 0;
+        for (int i = 0; i < getChildCount(); i++) {
+            View v = getChildAt(i);
+
+            if (v.getVisibility() == GONE) {
+                continue;
+            }
+
+            measureChild(v, childWidthSpec, childHeightSpec);
+
+            itemWidth = Math.max(itemWidth, v.getMeasuredWidth());
+            itemHeight = Math.max(itemHeight, v.getMeasuredHeight());
+        }
+
+        itemWidth = Math.min(Math.max(itemWidth, mMinColumnWidth), mMaxColumnWidth);
+        mNumColumns = Math.min((int) Math.ceil((double) wSize / itemWidth), mMaxColumns);
+
+        // Check how much space this distribution would take taking into account the margins.
+        // If it's bigger than what we have, remove one column.
+        int wSizeNeeded = mNumColumns * itemWidth + (mNumColumns - 1) * mItemHorizontalMargin;
+        if (wSizeNeeded > wSize && mNumColumns > 1) {
+            mNumColumns--;
+        }
+
+        if (getChildCount() < mNumColumns) {
+            mNumColumns = getChildCount();
+        }
+        if (mNumColumns == 0) {
+            mNumColumns = 1;
+        }
+
+        // Inform each child of the measurement
+        childWidthSpec = MeasureSpec.makeMeasureSpec(itemWidth, MeasureSpec.EXACTLY);
+        childHeightSpec = MeasureSpec.makeMeasureSpec(itemHeight, MeasureSpec.EXACTLY);
+        for (int i = 0; i < getChildCount(); i++) {
+            View v = getChildAt(i);
+
+            if (v.getVisibility() == GONE) {
+                continue;
+            }
+
+            measureChild(v, childWidthSpec, childHeightSpec);
+        }
+
+        // Calculate the height of the first column to measure our own size
+        int firstColumnItems = getChildCount() / mNumColumns + ((getChildCount() % mNumColumns) > 0
+                ? 1 : 0);
+
+        int horizontalMarginsTotalWidth = (mNumColumns - 1) * mItemHorizontalMargin;
+        int verticalMarginsTotalHeight = (firstColumnItems - 1) * mItemVerticalMargin;
+        int totalWidth = mNumColumns * itemWidth + horizontalMarginsTotalWidth +
+                mPaddingRight + mPaddingLeft;
+        int totalHeight = firstColumnItems * itemHeight + verticalMarginsTotalHeight +
+                mPaddingBottom + mPaddingTop;
+
+        setMeasuredDimension(resolveSize(totalWidth, widthMeasureSpec),
+                resolveSize(totalHeight, heightMeasureSpec));
+
+        mItemWidth = itemWidth;
+        mItemHeight = itemHeight;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        int itemsPerColumn = getChildCount() / mNumColumns;
+        // The remainder items are distributed one per column.
+        int remainderItems = getChildCount() % mNumColumns;
+
+        int x = mPaddingLeft;
+        int y = mPaddingTop;
+        int position = 1;
+        for (int i = 0; i < getChildCount(); i++) {
+            View v = getChildAt(i);
+            v.layout(x,
+                    y,
+                    x + mItemWidth,
+                    y + mItemHeight);
+
+            if (position == itemsPerColumn + (remainderItems > 0 ? 1 : 0)) {
+                // Break column
+                position = 1;
+                remainderItems--;
+                x += mItemWidth + mItemHorizontalMargin;
+                y = mPaddingTop;
+            } else {
+                position++;
+                y += mItemHeight + mItemVerticalMargin;
+            }
+        }
+    }
+}
+
+
diff --git a/tools/layoutlib/studio-custom-widgets/studio-android-widgets.iml b/tools/layoutlib/studio-custom-widgets/studio-android-widgets.iml
new file mode 100644
index 0000000..b0363d7
--- /dev/null
+++ b/tools/layoutlib/studio-custom-widgets/studio-android-widgets.iml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+  <component name="NewModuleRootManager" inherit-compiler-output="true">
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="library" name="layoutlib_api-prebuilt" level="project" />
+    <orderEntry type="library" name="framework.jar" level="project" />
+    <orderEntry type="module" module-name="bridge" />
+  </component>
+</module>
\ No newline at end of file