OSDN Git Service

Palette with previews, categories and view modes
authorTor Norbye <tnorbye@google.com>
Fri, 21 Jan 2011 05:13:39 +0000 (21:13 -0800)
committerTor Norbye <tnorbye@google.com>
Wed, 26 Jan 2011 02:03:02 +0000 (18:03 -0800)
This changeset contains the following improvements to the palette:

1. Display modes. The palette now supports several different view
   modes, and you can switch these via the context menu. The modes
   are:

   a. Previews. This renders previews for all the views using the
      current SDK platform, theme, screen density, etc.
   b. Small Previews. This is like (a), but scaled down to 75% size.
   c. Tiny Previews. Like (a), but scaled down to 50% size.
   d. Text + Icon. This shows an icon and the name of the view; this
      is the same as what the palette has looked like before this
      changeset.
   e. Icons only.

   All the modes, except for the Text+Icon mode, will lay out the
   views in a row (with vertical centering) to fit as much as possible
   in the available space for that category.

   The view mode, along with other view flags described below, are
   preserved across IDE sessions.

2. An accordion view. The palette is now using an Accordion control,
   which means it by default will keep a single category open, and it
   will always ensure that ALL the category labels are visible in the
   current view without scrolling. Via the context menu you can turn
   off the auto-close of the previous category.  The accordion view
   uses vertical scrollbars within each category content area, if
   necessary.

   The accordion view renders the category headers using open/close
   folder icons, a bold font, and a background gradient which varies
   between the normal and hovered states.

3. Categories. The category metadata is now used to organize the views
   into a handful of different categories. The categories can be
   enabled and disabled via the context menu. When you turn off
   categories, you get all the views in a single large list.

4. Alphabetical sorting. By default, the views are now sorted
   "naturally" (e.g.  the metadata provided order, where important
   views are listed first). You can switch it to alphabetical order
   via the context menu, in which case the items are listed in
   alphabetical order, either within their individual categories, or
   if categories are turned off, the global view list.

This changeset also adds a new SWT ImageControl. This is necessary to
display the preview images, because the CLabel, which is usually used
to display images in SWT, is hardcoded to hide the icon if there is
not enough horizontal space to display the full label (even when it
has no text label), so for wide preview images the images would simply
disappear when the palette was resized.

Change-Id: I1e1fe051947809206ef9f3a2dfa2fbeae0341107

22 files changed:
eclipse/dictionary.txt
eclipse/plugins/com.android.ide.eclipse.adt/icons/closed-folder.png [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/icons/open-folder.png [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AndroidConstants.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationComposite.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/AccordionControl.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageControl.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SubmenuAction.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/rendering-configs.xml [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/DecorComposite.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/AdtPrefs.java
eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/AdtPluginTest.java
eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtilsTest.java

index cc6db64..0ce2caa 100644 (file)
@@ -98,6 +98,7 @@ inline
 instanceof
 instantiatable
 int
+iterable
 javac
 javadoc
 keystore
@@ -194,6 +195,7 @@ thematically
 themed
 tmp
 tooltip
+tooltips
 traceview
 translucency
 ui
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/closed-folder.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/closed-folder.png
new file mode 100644 (file)
index 0000000..3fe8d8b
Binary files /dev/null and b/eclipse/plugins/com.android.ide.eclipse.adt/icons/closed-folder.png differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/open-folder.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/open-folder.png
new file mode 100644 (file)
index 0000000..8c4a2e1
Binary files /dev/null and b/eclipse/plugins/com.android.ide.eclipse.adt/icons/open-folder.png differ
index 93b7d51..194ac41 100755 (executable)
@@ -310,6 +310,9 @@ public class RelativeLayoutRule extends BaseLayoutRule {
                     continue;
                 }
                 String childId = child.getStringAttr(ANDROID_URI, ATTR_ID);
+                if (childId == null) {
+                    continue;
+                }
                 childId = normalizeId(childId);
                 if (id.equals(childId)) {
                     Set<String> linkedIds = getLinkedIds(child, cachedLinkIds);
index 49b0884..7f70859 100644 (file)
@@ -98,6 +98,7 @@ import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileReader;
+import java.io.FileWriter;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -459,6 +460,34 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger {
     }
 
     /**
+     * Writes the given content out to the given {@link File}. The file will be deleted if
+     * it already exists.
+     *
+     * @param file the target file
+     * @param content the content to be written into the file
+     */
+    public static void writeFile(File file, String content) {
+        if (file.exists()) {
+            file.delete();
+        }
+        FileWriter fw = null;
+        try {
+            fw = new FileWriter(file);
+            fw.write(content);
+        } catch (IOException e) {
+            AdtPlugin.log(e, null);
+        } finally {
+            if (fw != null) {
+                try {
+                    fw.close();
+                } catch (IOException e) {
+                    AdtPlugin.log(e, null);
+                }
+            }
+        }
+    }
+
+    /**
      * Returns true iff the given file contains the given String.
      *
      * @param file the file to look for the string in
@@ -1167,7 +1196,7 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger {
             @Override
             protected IStatus run(IProgressMonitor monitor) {
                 try {
-                    pingUsageServer(); //$NON-NLS-1$
+                    pingUsageServer();
 
                     return Status.OK_STATUS;
                 } catch (Throwable t) {
index 7d62acf..44c3659 100644 (file)
@@ -97,6 +97,8 @@ public class AndroidConstants {
     public final static String DOT_DEX = DOT + EXT_DEX;
     /** Dot-Extension for temporary resource files, ie "ap_ */
     public final static String DOT_RES = DOT + EXT_RES;
+    /** Dot-Extension for PNG files, i.e. ".png" */
+    public static final String DOT_PNG = ".png"; //$NON-NLS-1$
 
     /** Name of the android sources directory */
     public static final String FD_ANDROID_SOURCES = "sources"; //$NON-NLS-1$
index 73c8d2a..179674d 100644 (file)
@@ -38,9 +38,9 @@ import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType
 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
 import com.android.ide.eclipse.adt.internal.sdk.LayoutDevice;
+import com.android.ide.eclipse.adt.internal.sdk.LayoutDevice.DeviceConfig;
 import com.android.ide.eclipse.adt.internal.sdk.LayoutDeviceManager;
 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
-import com.android.ide.eclipse.adt.internal.sdk.LayoutDevice.DeviceConfig;
 import com.android.sdklib.IAndroidTarget;
 import com.android.sdklib.resources.Density;
 import com.android.sdklib.resources.DockMode;
@@ -1556,6 +1556,8 @@ public class ConfigurationComposite extends Composite {
 
     /**
      * Returns the current theme, or null if the combo has no selection.
+     *
+     * @return the theme name, or null
      */
     public String getTheme() {
         int themeIndex = mThemeCombo.getSelectionIndex();
@@ -1567,6 +1569,20 @@ public class ConfigurationComposite extends Composite {
     }
 
     /**
+     * Returns the current device string, or null if the combo has no selection.
+     *
+     * @return the device name, or null
+     */
+    public String getDevice() {
+        int deviceIndex = mDeviceCombo.getSelectionIndex();
+        if (deviceIndex != -1) {
+            return mDeviceCombo.getItem(deviceIndex);
+        }
+
+        return null;
+    }
+
+    /**
      * Returns whether the current theme selection is a project theme.
      * <p/>The returned value is meaningless if {@link #getTheme()} returns <code>null</code>.
      * @return true for project theme, false for framework theme
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/AccordionControl.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/AccordionControl.java
new file mode 100644 (file)
index 0000000..4a99dab
--- /dev/null
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2011 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.gle2;
+
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CLabel;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseTrackListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The accordion control allows a series of labels with associated content that can be
+ * shown. For more details on accordions, see http://en.wikipedia.org/wiki/Accordion_(GUI)
+ * <p>
+ * This control allows the children to be created lazily. You can also customize the
+ * composite which is created to hold the children items, to for example allow multiple
+ * columns of items rather than just the default vertical stack.
+ * <p>
+ * The visual appearance of the headers is built in; it uses a mild gradient, with a
+ * heavier gradient during mouse-overs. It also uses a bold label along with the eclipse
+ * folder icons.
+ * <p>
+ * The control can be configured to enforce a single category open at any time (the
+ * default), or allowing multiple categories open (where they share the available space).
+ * The control can also be configured to fill the available vertical space for the open
+ * category/categories.
+ */
+public abstract class AccordionControl extends Composite {
+    /** Pixel spacing between header items */
+    private static final int HEADER_SPACING = 0;
+
+    /** Pixel spacing between items in the content area */
+    private static final int ITEM_SPACING = 0;
+
+    private static final String KEY_CONTENT = "content"; //$NON-NLS-1$
+    private static final String KEY_HEADER = "header"; //$NON-NLS-1$
+
+    private Image mClosed;
+    private Image mOpen;
+    private boolean mSingle = true;
+    private boolean mWrap;
+
+    /**
+     * Creates the container which will hold the items in a category; this can be
+     * overridden to lay out the children with a different layout than the default
+     * vertical RowLayout
+     */
+    protected Composite createChildContainer(Composite parent) {
+        Composite composite = new Composite(parent, SWT.NONE);
+        if (mWrap) {
+            RowLayout layout = new RowLayout(SWT.HORIZONTAL);
+            layout.center = true;
+            composite.setLayout(layout);
+        } else {
+            RowLayout layout = new RowLayout(SWT.VERTICAL);
+            layout.spacing = ITEM_SPACING;
+            layout.marginHeight = 0;
+            layout.marginWidth = 0;
+            layout.marginLeft = 0;
+            layout.marginTop = 0;
+            layout.marginRight = 0;
+            layout.marginBottom = 0;
+            composite.setLayout(layout);
+        }
+
+        // TODO - maybe do multi-column arrangement for simple nodes
+        return composite;
+    }
+
+    /**
+     * Creates the children under a particular header
+     *
+     * @param parent the parent composite to add the SWT items to
+     * @param header the header object that is being opened for the first time
+     */
+    protected abstract void createChildren(Composite parent, Object header);
+
+    /**
+     * Set whether a single category should be enforced or not (default=true)
+     *
+     * @param single if true, enforce a single category open at a time
+     */
+    public void setAutoClose(boolean single) {
+        mSingle = single;
+    }
+
+    /**
+     * Returns whether a single category should be enforced or not (default=true)
+     *
+     * @return true if only a single category can be open at a time
+     */
+    public boolean isAutoClose() {
+        return mSingle;
+    }
+
+    /**
+     * Returns the labels used as header categories
+     *
+     * @return list of header labels
+     */
+    public List<CLabel> getHeaderLabels() {
+        List<CLabel> headers = new ArrayList<CLabel>();
+        for (Control c : getChildren()) {
+            if (c instanceof CLabel) {
+                headers.add((CLabel) c);
+            }
+        }
+
+        return headers;
+    }
+
+    /**
+     * Show all categories
+     *
+     * @param performLayout if true, call {@link #layout} and {@link #pack} when done
+     */
+    public void expandAll(boolean performLayout) {
+        for (Control c : getChildren()) {
+            if (c instanceof CLabel) {
+                if (!isOpen(c)) {
+                    toggle((CLabel) c, false, false);
+                }
+            }
+        }
+        if (performLayout) {
+            pack();
+            layout();
+        }
+    }
+
+    /**
+     * Hide all categories
+     *
+     * @param performLayout if true, call {@link #layout} and {@link #pack} when done
+     */
+    public void collapseAll(boolean performLayout) {
+        for (Control c : getChildren()) {
+            if (c instanceof CLabel) {
+                if (isOpen(c)) {
+                    toggle((CLabel) c, false, false);
+                }
+            }
+        }
+        if (performLayout) {
+            layout();
+        }
+    }
+
+    /**
+     * Create the composite.
+     *
+     * @param parent the parent widget to add the accordion to
+     * @param style the SWT style mask to use
+     * @param headers a list of headers, whose {@link Object#toString} method should
+     *            produce the heading label
+     * @param greedy if true, grow vertically as much as possible
+     * @param wrapChildren if true, configure the child area to be horizontally laid out
+     *            with wrapping
+     */
+    public AccordionControl(Composite parent, int style, List<?> headers,
+            boolean greedy, boolean wrapChildren) {
+        super(parent, style);
+        mWrap = wrapChildren;
+
+        GridLayout gridLayout = new GridLayout(1, false);
+        gridLayout.verticalSpacing = HEADER_SPACING;
+        gridLayout.horizontalSpacing = 0;
+        gridLayout.marginWidth = 0;
+        gridLayout.marginHeight = 0;
+        setLayout(gridLayout);
+
+        Font labelFont = null;
+
+        mOpen = IconFactory.getInstance().getIcon("open-folder");     //$NON-NLS-1$
+        mClosed = IconFactory.getInstance().getIcon("closed-folder"); //$NON-NLS-1$
+        CLabel first = null;
+
+        for (Object header : headers) {
+            final CLabel label = new CLabel(this, SWT.SHADOW_OUT);
+            label.setText(header.toString().replace("&", "&&")); //$NON-NLS-1$ //$NON-NLS-2$
+            updateBackground(label, false);
+            if (labelFont == null) {
+                labelFont = label.getFont();
+                FontData normal = labelFont.getFontData()[0];
+                FontData bold = new FontData(normal.getName(), normal.getHeight(), SWT.BOLD);
+                labelFont = new Font(null, bold);
+            }
+            label.setFont(labelFont);
+            label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+            setHeader(header, label);
+            label.addMouseListener(new MouseAdapter() {
+                @Override
+                public void mouseUp(MouseEvent e) {
+                    if (e.button == 1 && (e.stateMask & SWT.MODIFIER_MASK) == 0) {
+                        toggle(label, true, mSingle);
+                    }
+                }
+            });
+            label.addMouseTrackListener(new MouseTrackListener() {
+                public void mouseEnter(MouseEvent e) {
+                    updateBackground(label, true);
+                }
+
+                public void mouseExit(MouseEvent e) {
+                    updateBackground(label, false);
+                }
+
+                public void mouseHover(MouseEvent e) {
+                }
+            });
+
+            // Turn off border?
+            final ScrolledComposite scrolledComposite = new ScrolledComposite(this, SWT.V_SCROLL);
+
+            // Do we need the scrolled composite or can we just look at the next
+            // wizard in the hierarchy?
+
+            setContentArea(label, scrolledComposite);
+            scrolledComposite.setExpandHorizontal(true);
+            scrolledComposite.setExpandVertical(true);
+            GridData scrollGridData = new GridData(SWT.FILL,
+                    greedy ? SWT.FILL : SWT.TOP, false, greedy, 1, 1);
+            scrollGridData.exclude = true;
+            scrollGridData.grabExcessHorizontalSpace = wrapChildren;
+            scrolledComposite.setLayoutData(scrollGridData);
+
+            if (wrapChildren) {
+                scrolledComposite.addControlListener(new ControlAdapter() {
+                    @Override
+                    public void controlResized(ControlEvent e) {
+                        Rectangle r = scrolledComposite.getClientArea();
+                        Control content = scrolledComposite.getContent();
+                        if (content != null && r != null) {
+                            Point minSize = content.computeSize(r.width, SWT.DEFAULT);
+                            scrolledComposite.setMinSize(minSize);
+                        }
+                    }
+                  });
+            }
+
+            updateIcon(label);
+            if (first == null) {
+                first = label;
+            }
+        }
+
+        // Initially show the first category
+        if (headers.size() > 0) {
+            toggle(first, false, false);
+        }
+    }
+
+    /** Updates the background gradient of the given header label */
+    private void updateBackground(CLabel label, boolean mouseOver) {
+        Display display = label.getDisplay();
+        label.setBackground(new Color[] {
+                display.getSystemColor(SWT.COLOR_WIDGET_HIGHLIGHT_SHADOW),
+                display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND),
+                display.getSystemColor(SWT.COLOR_WIDGET_LIGHT_SHADOW)
+        }, new int[] {
+                mouseOver ? 60 : 40, 100
+        }, true);
+    }
+
+    /**
+     * Updates the icon for a header label to be open/close based on the {@link #isOpen}
+     * state
+     */
+    private void updateIcon(CLabel label) {
+        label.setImage(isOpen(label) ? mOpen : mClosed);
+    }
+
+    /** Returns true if the content area for the given label is open/showing */
+    private boolean isOpen(Control label) {
+        return !((GridData) getContentArea(label).getLayoutData()).exclude;
+    }
+
+    /** Toggles the visibility of the children of the given label */
+    private void toggle(CLabel label, boolean performLayout, boolean autoClose) {
+        if (autoClose) {
+            collapseAll(true);
+        }
+        ScrolledComposite scrolledComposite = getContentArea(label);
+
+        GridData scrollGridData = (GridData) scrolledComposite.getLayoutData();
+        boolean close = !scrollGridData.exclude;
+        scrollGridData.exclude = close;
+        scrolledComposite.setVisible(!close);
+        updateIcon(label);
+
+        if (!scrollGridData.exclude && scrolledComposite.getContent() == null) {
+            Composite composite = createChildContainer(scrolledComposite);
+            Object header = getHeader(label);
+            createChildren(composite, header);
+            scrolledComposite.setContent(composite);
+            scrolledComposite.setMinSize(composite.computeSize(SWT.DEFAULT, SWT.DEFAULT));
+        }
+
+        if (performLayout) {
+            layout(true);
+        }
+    }
+
+    /** Returns the header object for the given header label */
+    private Object getHeader(Control label) {
+        return label.getData(KEY_HEADER);
+    }
+
+    /** Sets the header object for the given header label */
+    private void setHeader(Object header, final CLabel label) {
+        label.setData(KEY_HEADER, header);
+    }
+
+    /** Returns the content area for the given header label */
+    private ScrolledComposite getContentArea(Control label) {
+        return (ScrolledComposite) label.getData(KEY_CONTENT);
+    }
+
+    /** Sets the content area for the given header label */
+    private void setContentArea(final CLabel label, ScrolledComposite scrolledComposite) {
+        label.setData(KEY_CONTENT, scrolledComposite);
+    }
+
+    @Override
+    protected void checkSubclass() {
+        // Disable the check that prevents subclassing of SWT components
+    }
+}
index 217f853..5464917 100644 (file)
@@ -37,11 +37,10 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.TreeMap;
 import java.util.Map.Entry;
+import java.util.TreeMap;
 import java.util.regex.Pattern;
 
-
 /**
  * Helper class that is responsible for adding and managing the dynamic menu items
  * contributed by the {@link IViewRule} instances, based on the current selection
index 9dff02f..902d3d0 100644 (file)
@@ -28,10 +28,10 @@ import com.android.ide.common.rendering.api.Capability;
 import com.android.ide.common.rendering.api.ILayoutPullParser;
 import com.android.ide.common.rendering.api.LayoutLog;
 import com.android.ide.common.rendering.api.Params;
+import com.android.ide.common.rendering.api.Params.RenderingMode;
 import com.android.ide.common.rendering.api.RenderSession;
 import com.android.ide.common.rendering.api.ResourceValue;
 import com.android.ide.common.rendering.api.Result;
-import com.android.ide.common.rendering.api.Params.RenderingMode;
 import com.android.ide.common.resources.ResourceResolver;
 import com.android.ide.common.sdk.LoadStatus;
 import com.android.ide.eclipse.adt.AdtPlugin;
@@ -42,14 +42,14 @@ import com.android.ide.eclipse.adt.internal.editors.layout.ContextPullParser;
 import com.android.ide.eclipse.adt.internal.editors.layout.ExplodedRenderingHelper;
 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor;
-import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback;
-import com.android.ide.eclipse.adt.internal.editors.layout.UiElementPullParser;
 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ChangeFlags;
 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ILayoutReloadListener;
+import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback;
+import com.android.ide.eclipse.adt.internal.editors.layout.UiElementPullParser;
 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite;
-import com.android.ide.eclipse.adt.internal.editors.layout.configuration.LayoutCreatorDialog;
 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite.CustomButton;
 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite.IConfigListener;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.LayoutCreatorDialog;
 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
 import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
 import com.android.ide.eclipse.adt.internal.editors.ui.DecorComposite;
@@ -683,6 +683,8 @@ public class GraphicalEditorPart extends EditorPart
                             mEditedFile.getName());
                 }
             }
+
+            reloadPalette();
         }
 
         public void onThemeChange() {
@@ -690,6 +692,8 @@ public class GraphicalEditorPart extends EditorPart
             mConfigComposite.storeState();
 
             recomputeLayout();
+
+            reloadPalette();
         }
 
         public void onCreate() {
@@ -710,6 +714,8 @@ public class GraphicalEditorPart extends EditorPart
         public void onRenderingTargetPostChange(IAndroidTarget target) {
             AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target);
             updateCapabilities(targetData);
+
+            mPalette.reloadPalette(target);
         }
 
         public Map<String, Map<String, ResourceValue>> getConfiguredFrameworkResources() {
@@ -1222,8 +1228,8 @@ public class GraphicalEditorPart extends EditorPart
                     computeAndSetRealScale(false /* redraw */);
                 }
 
-                IProject iProject = mEditedFile.getProject();
-                renderWithBridge(iProject, model, layoutLib);
+                IProject project = mEditedFile.getProject();
+                renderWithBridge(project, model, layoutLib);
             }
         } finally {
             // no matter the result, we are done doing the recompute based on the latest
@@ -1234,7 +1240,10 @@ public class GraphicalEditorPart extends EditorPart
 
     public void reloadPalette() {
         if (mPalette != null) {
-            mPalette.reloadPalette(mLayoutEditor.getTargetData());
+            IAndroidTarget renderingTarget = getRenderingTarget();
+            if (renderingTarget != null) {
+                mPalette.reloadPalette(renderingTarget);
+            }
         }
     }
 
@@ -1253,11 +1262,12 @@ public class GraphicalEditorPart extends EditorPart
      *            normal background requested by the theme, and it will instead paint the
      *            background using a fully transparent background color
      * @param logger a logger where rendering errors are reported
+     * @param renderingMode the {@link RenderingMode} to use for rendering
      * @return the resulting rendered image wrapped in an {@link RenderSession}
      */
     public RenderSession render(UiDocumentNode model, int width, int height,
             Set<UiElementNode> explodeNodes, boolean transparentBackground,
-            LayoutLog logger) {
+            LayoutLog logger, RenderingMode renderingMode) {
         if (!ensureFileValid()) {
             return null;
         }
@@ -1268,7 +1278,7 @@ public class GraphicalEditorPart extends EditorPart
 
         IProject iProject = mEditedFile.getProject();
         return renderWithBridge(iProject, model, layoutLib, width, height, explodeNodes,
-                transparentBackground, logger);
+                transparentBackground, logger, null /* includeWithin */, renderingMode);
     }
 
     /**
@@ -1490,8 +1500,20 @@ public class GraphicalEditorPart extends EditorPart
 
         RenderLogger logger = new RenderLogger(mEditedFile.getName());
 
+        RenderingMode renderingMode = RenderingMode.NORMAL;
+        if (mClippingButton.getSelection() == false) {
+            renderingMode = RenderingMode.FULL_EXPAND;
+        } else {
+            // FIXME set the rendering mode using ViewRule or something.
+            List<UiElementNode> children = model.getUiChildren();
+            if (children.size() > 0 &&
+                    children.get(0).getDescriptor().getXmlLocalName().equals(SCROLL_VIEW)) {
+                renderingMode = RenderingMode.V_SCROLL;
+            }
+        }
+
         RenderSession session = renderWithBridge(iProject, model, layoutLib, width, height,
-                explodeNodes, false, logger);
+                explodeNodes, false, logger, mIncludedWithin, renderingMode);
 
         canvas.setSession(session, explodeNodes);
 
@@ -1533,7 +1555,8 @@ public class GraphicalEditorPart extends EditorPart
 
     private RenderSession renderWithBridge(IProject iProject, UiDocumentNode model,
             LayoutLibrary layoutLib, int width, int height, Set<UiElementNode> explodeNodes,
-            boolean transparentBackground, LayoutLog logger) {
+            boolean transparentBackground, LayoutLog logger, Reference includeWithin,
+            RenderingMode renderingMode) {
         ResourceManager resManager = ResourceManager.getInstance();
 
         ProjectResources projectRes = resManager.getProjectResources(iProject);
@@ -1606,8 +1629,8 @@ public class GraphicalEditorPart extends EditorPart
         // Code to support editing included layout
 
         // Outer layout name:
-        if (mIncludedWithin != null) {
-            String contextLayoutName = mIncludedWithin.getName();
+        if (includeWithin != null) {
+            String contextLayoutName = includeWithin.getName();
 
             // Find the layout file.
             Map<String, ResourceValue> layouts = configuredProjectRes.get(
@@ -1632,18 +1655,6 @@ public class GraphicalEditorPart extends EditorPart
             }
         }
 
-        RenderingMode renderingMode = RenderingMode.NORMAL;
-        if (mClippingButton.getSelection() == false) {
-            renderingMode = RenderingMode.FULL_EXPAND;
-        } else {
-            // FIXME set the rendering mode using ViewRule or something.
-            List<UiElementNode> children = model.getUiChildren();
-            if (children.size() > 0 &&
-                    children.get(0).getDescriptor().getXmlLocalName().equals(SCROLL_VIEW)) {
-                renderingMode = RenderingMode.V_SCROLL;
-            }
-        }
-
         // FIXME: make resource resolver persistent, and only update it when something changes.
         ResourceResolver resolver = ResourceResolver.create(
                 configuredProjectRes, frameworkResources,
@@ -1738,6 +1749,9 @@ public class GraphicalEditorPart extends EditorPart
 
         /** Reload layout. <b>Must be called on the SWT thread</b> */
         private void reloadLayoutSwt(ChangeFlags flags, boolean libraryChanged) {
+            if (mConfigComposite.isDisposed()) {
+                return;
+            }
             assert mConfigComposite.getDisplay().getThread() == Thread.currentThread();
 
             boolean recompute = false;
@@ -2383,7 +2397,6 @@ public class GraphicalEditorPart extends EditorPart
         return mConfigComposite.getCurrentConfig();
     }
 
-
     /**
      * Figures out the project's minSdkVersion and targetSdkVersion and return whether the values
      * have changed.
@@ -2428,4 +2441,8 @@ public class GraphicalEditorPart extends EditorPart
 
         return oldMinSdkVersion != mMinSdkVersion || oldTargetSdkVersion != mTargetSdkVersion;
     }
+
+    public ConfigurationComposite getConfigurationComposite() {
+        return mConfigComposite;
+    }
 }
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageControl.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageControl.java
new file mode 100644 (file)
index 0000000..d455fd8
--- /dev/null
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2011 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.gle2;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CLabel;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseTrackListener;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * An ImageControl which simply renders an image, with optional margins and tooltips. This
+ * is useful since a {@link CLabel}, even without text, will hide the image when there is
+ * not enough room to fully fit it.
+ * <p>
+ * The image is always rendered left and top aligned.
+ */
+public class ImageControl extends Canvas implements MouseTrackListener {
+    private Image mImage;
+    private int mLeftMargin;
+    private int mTopMargin;
+    private int mRightMargin;
+    private int mBottomMargin;
+    private boolean mShowMouseOver;
+    private boolean mMouseIn;
+    private Color mHoverColor;
+    private float mScale = 1.0f;
+
+    /**
+     * Creates an ImageControl rendering the given image, which will be dispose when this
+     * control is disposed
+     *
+     * @param parent the parent to add the image control to
+     * @param style the SWT style to use
+     * @param image the image to be rendered, which must not be null and should be unique
+     *            for this image control since it will be disposed by this control when
+     *            the control is disposed
+     */
+    public ImageControl(Composite parent, int style, Image image) {
+        super(parent, style | SWT.NO_FOCUS | SWT.DOUBLE_BUFFERED);
+        this.mImage = image;
+
+        addPaintListener(new PaintListener() {
+            public void paintControl(PaintEvent event) {
+                onPaint(event);
+            }
+        });
+    }
+
+    public void setScale(float scale) {
+        mScale = scale;
+    }
+
+    public float getScale() {
+        return mScale;
+    }
+
+    public void setHoverColor(Color hoverColor) {
+        mHoverColor = hoverColor;
+        if (hoverColor != null) {
+            addMouseTrackListener(this);
+        }
+    }
+
+    public Color getHoverColor() {
+        return mHoverColor;
+    }
+
+    @Override
+    public void dispose() {
+        super.dispose();
+
+        mImage.dispose();
+        mImage = null;
+    }
+
+    @Override
+    public Point computeSize(int wHint, int hHint, boolean changed) {
+        checkWidget();
+        Point e = new Point(0, 0);
+        if (mImage != null) {
+            Rectangle r = mImage.getBounds();
+            if (mScale != 1.0f) {
+                e.x += mScale * r.width;
+                e.y += mScale * r.height;
+            } else {
+                e.x += r.width;
+                e.y += r.height;
+            }
+        }
+        if (wHint == SWT.DEFAULT) {
+            e.x += mLeftMargin + mRightMargin;
+        } else {
+            e.x = wHint;
+        }
+        if (hHint == SWT.DEFAULT) {
+            e.y += mTopMargin + mBottomMargin;
+        } else {
+            e.y = hHint;
+        }
+
+        return e;
+    }
+
+    private void onPaint(PaintEvent event) {
+        Rectangle rect = getClientArea();
+        if (mImage == null || rect.width == 0 || rect.height == 0) {
+            return;
+        }
+
+        GC gc = event.gc;
+        Rectangle imageRect = mImage.getBounds();
+        int imageHeight = imageRect.height;
+        int imageWidth = imageRect.width;
+        int destWidth = imageWidth;
+        int destHeight = imageHeight;
+
+        if (mScale != 1.0f) {
+            destWidth = (int) (mScale * destWidth);
+            destHeight = (int) (mScale * destHeight);
+        }
+
+        gc.drawImage(mImage, 0, 0, imageWidth, imageHeight, rect.x + mLeftMargin, rect.y
+                + mTopMargin, destWidth, destHeight);
+
+        if (mHoverColor != null && mMouseIn) {
+            gc.setAlpha(60);
+            gc.setBackground(mHoverColor);
+            gc.setLineWidth(1);
+            gc.fillRectangle(0, 0, destWidth, destHeight);
+        }
+    }
+
+    public void setMargins(int leftMargin, int topMargin, int rightMargin, int bottomMargin) {
+        checkWidget();
+        mLeftMargin = Math.max(0, leftMargin);
+        mTopMargin = Math.max(0, topMargin);
+        mRightMargin = Math.max(0, rightMargin);
+        mBottomMargin = Math.max(0, bottomMargin);
+        redraw();
+    }
+
+    // ---- Implements MouseTrackListener ----
+
+    public void mouseEnter(MouseEvent e) {
+        mMouseIn = true;
+        if (mHoverColor != null) {
+            redraw();
+        }
+    }
+
+    public void mouseExit(MouseEvent e) {
+        mMouseIn = false;
+        if (mHoverColor != null) {
+            redraw();
+        }
+    }
+
+    public void mouseHover(MouseEvent e) {
+    }
+}
index f83d4a5..1952462 100644 (file)
@@ -351,4 +351,26 @@ public class ImageUtils {
 
         return union;
     }
+
+    /**
+     * Returns a new image which contains of the sub image given by the rectangle (x1,y1)
+     * to (x2,y2)
+     *
+     * @param source the source image
+     * @param x1 top left X coordinate
+     * @param y1 top left Y coordinate
+     * @param x2 bottom right X coordinate
+     * @param y2 bottom right Y coordinate
+     * @return a new image containing the pixels in the given range
+     */
+    public static BufferedImage subImage(BufferedImage source, int x1, int y1, int x2, int y2) {
+        int width = x2 - x1;
+        int height = y2 - y1;
+        BufferedImage sub = new BufferedImage(width, height, source.getType());
+        Graphics g = sub.getGraphics();
+        g.drawImage(source, 0, 0, width, height, x1, y1, x2, y2, null);
+        g.dispose();
+
+        return sub;
+    }
 }
index 0fd2bbd..1aeda77 100755 (executable)
@@ -21,8 +21,6 @@ import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT;
 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH;
 import static com.android.ide.common.layout.LayoutConstants.ATTR_TEXT;
 import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT;
-import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_INCLUDE;
-import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_MERGE;
 
 import com.android.ide.common.api.InsertType;
 import com.android.ide.common.api.Rect;
@@ -31,24 +29,34 @@ import com.android.ide.common.rendering.api.Capability;
 import com.android.ide.common.rendering.api.LayoutLog;
 import com.android.ide.common.rendering.api.RenderSession;
 import com.android.ide.common.rendering.api.ViewInfo;
+import com.android.ide.common.rendering.api.Params.RenderingMode;
 import com.android.ide.eclipse.adt.internal.editors.IconFactory;
 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.descriptors.XmlnsAttributeDescriptor;
 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite;
 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
 import com.android.ide.eclipse.adt.internal.editors.ui.DecorComposite;
-import com.android.ide.eclipse.adt.internal.editors.ui.GridDataBuilder;
-import com.android.ide.eclipse.adt.internal.editors.ui.GridLayoutBuilder;
 import com.android.ide.eclipse.adt.internal.editors.ui.IDecorContent;
 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.sdklib.IAndroidTarget;
 import com.android.sdklib.SdkConstants;
+import com.android.sdklib.util.Pair;
 
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.resource.ImageDescriptor;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.CLabel;
 import org.eclipse.swt.dnd.DND;
@@ -56,23 +64,24 @@ import org.eclipse.swt.dnd.DragSource;
 import org.eclipse.swt.dnd.DragSourceEvent;
 import org.eclipse.swt.dnd.DragSourceListener;
 import org.eclipse.swt.dnd.Transfer;
-import org.eclipse.swt.events.ControlEvent;
-import org.eclipse.swt.events.ControlListener;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.MenuDetectEvent;
+import org.eclipse.swt.events.MenuDetectListener;
 import org.eclipse.swt.events.MouseEvent;
-import org.eclipse.swt.events.MouseListener;
 import org.eclipse.swt.events.MouseTrackListener;
+import org.eclipse.swt.graphics.Color;
 import org.eclipse.swt.graphics.GC;
 import org.eclipse.swt.graphics.Image;
 import org.eclipse.swt.graphics.ImageData;
 import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
 import org.eclipse.swt.graphics.Rectangle;
-import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.FillLayout;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Display;
-import org.eclipse.swt.widgets.Event;
-import org.eclipse.swt.widgets.Listener;
-import org.eclipse.swt.widgets.ScrollBar;
+import org.eclipse.swt.widgets.Menu;
 import org.w3c.dom.Attr;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
@@ -81,7 +90,10 @@ import java.awt.image.BufferedImage;
 import java.io.IOException;
 import java.io.StringWriter;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
@@ -94,20 +106,13 @@ import javax.xml.parsers.ParserConfigurationException;
  * with a list of element descriptors.
  * <p/>
  *
- * @since GLE2
- *
  * TODO list:
  *   - The available items should depend on the actual GLE2 Canvas selection. Selected android
  *     views should force filtering on what they accept can be dropped on them (e.g. TabHost,
  *     TableLayout). Should enable/disable them, not hide them, to avoid shuffling around.
  *   - Optional: a text filter
- *   - Optional: have icons that depict the element and/or automatically rendered icons
- *     based on a rendering of the widget.
  *   - Optional: have context-sensitive tools items, e.g. selection arrow tool,
  *     group selection tool, alignment, etc.
- *   - Different view strategies: big icon, small icons, text vs no text, compact grid.
- *     - This would only be useful with meaningful icons. Out current 1-letter icons are not enough
- *       to get rid of text labels.
  */
 public class PaletteControl extends Composite {
 
@@ -139,12 +144,70 @@ public class PaletteControl extends Composite {
         }
     }
 
-    /** The parent grid layout that contains all the {@link Toggle} and {@link Item} widgets. */
-    private Composite mRoot;
-    private ScrollBar mVBar;
-    private ControlListener mControlListener;
-    private Listener mScrollbarListener;
+    /**
+     * The parent grid layout that contains all the {@link Toggle} and
+     * {@link IconTextItem} widgets.
+     */
     private GraphicalEditorPart mEditor;
+    private Color mBackground;
+
+    /** The palette modes control various ways to visualize and lay out the views */
+    private static enum PaletteMode {
+        /** Show rendered previews of the views */
+        PREVIEW("Show Previews", true),
+        /** Show rendered previews of the views, scaled down to 75% */
+        SMALL_PREVIEW("Show Small Previews", true),
+        /** Show rendered previews of the views, scaled down to 50% */
+        TINY_PREVIEW("Show Tiny Previews", true),
+        /** Show an icon + text label */
+        ICON_TEXT("Show Icon and Text", false),
+        /** Show only icons, packed multiple per row */
+        ICON_ONLY("Show Only Icons", true);
+
+        PaletteMode(String actionLabel, boolean wrap) {
+            mActionLabel = actionLabel;
+            mWrap = wrap;
+        }
+
+        public String getActionLabel() {
+            return mActionLabel;
+        }
+
+        public boolean getWrap() {
+            return mWrap;
+        }
+
+        public boolean isPreview() {
+            return this == PREVIEW || this == SMALL_PREVIEW || this == TINY_PREVIEW;
+        }
+
+        public boolean isScaledPreview() {
+            return this == SMALL_PREVIEW || this == TINY_PREVIEW;
+        }
+
+        private final String mActionLabel;
+        private final boolean mWrap;
+    };
+
+    /** Token used in preference string to record alphabetical sorting */
+    private static final String VALUE_ALPHABETICAL = "alpha";   //$NON-NLS-1$
+    /** Token used in preference string to record categories being turned off */
+    private static final String VALUE_NO_CATEGORIES = "nocat"; //$NON-NLS-1$
+    /** Token used in preference string to record auto close being turned off */
+    private static final String VALUE_NO_AUTOCLOSE = "noauto";      //$NON-NLS-1$
+
+    private final PreviewIconFactory mPreviewIconFactory = new PreviewIconFactory(this);
+    private PaletteMode mPaletteMode = PaletteMode.SMALL_PREVIEW;
+    /** Use alphabetical sorting instead of natural order? */
+    private boolean mAlphabetical;
+    /** Use categories instead of a single large list of views? */
+    private boolean mCategories = true;
+    /** Auto-close the previous category when new categories are opened */
+    private boolean mAutoClose = true;
+    private AccordionControl mAccordion;
+    private String mCurrentTheme;
+    private String mCurrentDevice;
+    private IAndroidTarget mCurrentTarget;
 
     /**
      * Create the composite.
@@ -152,32 +215,59 @@ public class PaletteControl extends Composite {
      * @param editor An editor associated with this palette.
      */
     public PaletteControl(Composite parent, GraphicalEditorPart editor) {
-        super(parent, SWT.V_SCROLL);
+        super(parent, SWT.NONE);
 
         mEditor = editor;
-        mVBar = getVerticalBar();
+        loadPaletteMode();
+    }
 
-        mScrollbarListener = new Listener() {
-            public void handleEvent(Event event) {
-                scrollScrollbar();
+    /** Reads UI mode from persistent store to preserve palette mode across IDE sessions */
+    private void loadPaletteMode() {
+        String paletteModes = AdtPrefs.getPrefs().getPaletteModes();
+        if (paletteModes.length() > 0) {
+            String[] tokens = paletteModes.split(","); //$NON-NLS-1$
+            try {
+                mPaletteMode = PaletteMode.valueOf(tokens[0]);
+            } catch (Throwable t) {
+                mPaletteMode = PaletteMode.values()[0];
             }
-        };
-
-        mVBar.addListener(SWT.Selection, mScrollbarListener);
+            mAlphabetical = paletteModes.contains(VALUE_ALPHABETICAL);
+            mCategories = !paletteModes.contains(VALUE_NO_CATEGORIES);
+            mAutoClose = !paletteModes.contains(VALUE_NO_AUTOCLOSE);
+        }
+    }
 
+    /**
+     * Returns the most recently stored version of auto-close-mode; this is the last
+     * user-initiated setting of the auto-close mode (we programmatically switch modes when
+     * you enter icons-only mode, and set it back to this when going to any other mode)
+     */
+    private boolean getSavedAutoCloseMode() {
+        return !AdtPrefs.getPrefs().getPaletteModes().contains(VALUE_NO_AUTOCLOSE);
+    }
 
-        mControlListener = new ControlListener() {
-            public void controlMoved(ControlEvent e) {
-                // Ignore
-            }
-            public void controlResized(ControlEvent e) {
-                if (recomputeScrollbar()) {
-                    redraw();
-                }
-            }
-        };
+    /** Saves UI mode to persistent store to preserve palette mode across IDE sessions */
+    private void savePaletteMode() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(mPaletteMode);
+        if (mAlphabetical) {
+            sb.append(',').append(VALUE_ALPHABETICAL);
+        }
+        if (!mCategories) {
+            sb.append(',').append(VALUE_NO_CATEGORIES);
+        }
+        if (!mAutoClose) {
+            sb.append(',').append(VALUE_NO_AUTOCLOSE);
+        }
+        AdtPrefs.getPrefs().setPaletteModes(sb.toString());
+    }
 
-        addControlListener(mControlListener);
+    private void refreshPalette() {
+        IAndroidTarget oldTarget = mCurrentTarget;
+        mCurrentTarget = null;
+        mCurrentTheme = null;
+        mCurrentDevice = null;
+        reloadPalette(oldTarget);
     }
 
     @Override
@@ -187,299 +277,226 @@ public class PaletteControl extends Composite {
 
     @Override
     public void dispose() {
-        if (mControlListener != null) {
-            removeControlListener(mControlListener);
-            mControlListener = null;
-        }
-
-        if (mVBar != null && !mVBar.isDisposed()) {
-            if (mScrollbarListener != null) {
-                mVBar.removeListener(SWT.Selection, mScrollbarListener);
-            }
-            mVBar = null;
+        if (mBackground != null) {
+            mBackground.dispose();
+            mBackground = null;
         }
 
         super.dispose();
     }
 
     /**
+     * Returns the currently displayed target
+     *
+     * @return the current target, or null
+     */
+    public IAndroidTarget getCurrentTarget() {
+        return mCurrentTarget;
+    }
+
+    /**
+     * Returns the currently displayed theme (in palette modes that support previewing)
+     *
+     * @return the current theme, or null
+     */
+    public String getCurrentTheme() {
+        return mCurrentTheme;
+    }
+
+    /**
+     * Returns the currently displayed device (in palette modes that support previewing)
+     *
+     * @return the current device, or null
+     */
+    public String getCurrentDevice() {
+        return mCurrentDevice;
+    }
+
+    /**
      * Loads or reloads the palette elements by using the layout and view descriptors from the
      * given target data.
      *
-     * @param targetData The target data that contains the descriptors. If null or empty,
-     *   no groups will be created.
+     * @param target The target that has just been loaded
      */
-    public void reloadPalette(AndroidTargetData targetData) {
-
+    public void reloadPalette(IAndroidTarget target) {
+        ConfigurationComposite configuration = mEditor.getConfigurationComposite();
+        String theme = configuration.getTheme();
+        String device = configuration.getDevice();
+        if (target == mCurrentTarget && mCurrentTheme != null && mCurrentTheme.equals(theme) &&
+                mCurrentDevice != null && mCurrentDevice.equals(device)) {
+            return;
+        }
+        mCurrentTheme = theme;
+        mCurrentTarget = target;
+        mCurrentDevice = device;
+        mPreviewIconFactory.reset();
+
+        // Erase old content and recreate new
         for (Control c : getChildren()) {
             c.dispose();
         }
 
-        GridLayoutBuilder.create(this).columns(1).columnsEqual().hSpacing(0).noMargins();
+        if (mPaletteMode.isPreview()) {
+            RGB background = mPreviewIconFactory.getBackgroundColor();
+            if (mBackground != null) {
+                mBackground.dispose();
+            }
+            mBackground = new Color(getDisplay(), background);
+        }
 
-        mRoot = new Composite(this, SWT.NONE);
-        GridLayoutBuilder.create(mRoot).columns(1).columnsEqual().spacing(0).noMargins();
-        GridDataBuilder.create(mRoot).hGrab().hFill();
+        AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target);
 
+        List<Object> headers = Collections.emptyList();
+        final Map<String, List<ViewElementDescriptor>> categoryToItems;
         if (targetData != null) {
-            // TODO: Use metadata categories instead:
-            //List<Pair<String,List<ViewElementDescriptor>>> paletteEntries =
-            //    ViewMetadataRepository.get().getPaletteEntries(targetData);
-            //for (Pair<String,List<ViewElementDescriptor>> pair : paletteEntries) {
-            //    addGroup(mRoot, pair.getFirst(), pair.getSecond());
-            //}
-            addGroup(mRoot, "Views", targetData.getLayoutDescriptors().getViewDescriptors());
-            addGroup(mRoot, "Layouts", targetData.getLayoutDescriptors().getLayoutDescriptors());
+            categoryToItems = new HashMap<String, List<ViewElementDescriptor>>();
+            headers = new ArrayList<Object>();
+            List<Pair<String,List<ViewElementDescriptor>>> paletteEntries =
+                ViewMetadataRepository.get().getPaletteEntries(targetData,
+                        mAlphabetical, mCategories);
+            for (Pair<String,List<ViewElementDescriptor>> pair : paletteEntries) {
+                String category = pair.getFirst();
+                List<ViewElementDescriptor> categoryItems = pair.getSecond();
+                headers.add(category);
+                categoryToItems.put(category, categoryItems);
+            }
+        } else {
+            categoryToItems = null;
         }
 
-        layout(true);
+        boolean wrap = mPaletteMode.getWrap();
 
-        recomputeScrollbar();
-    }
+        // Pack icon-only view vertically; others stretch to fill palette region
+        boolean fillVertical = mPaletteMode != PaletteMode.ICON_ONLY;
 
-    // ----- private methods ----
-
-    /** Returns true if scrollbar changed. */
-    private  boolean recomputeScrollbar() {
-        if (mVBar != null && mRoot != null) {
-
-            int sel = mVBar.getSelection();
-            int max = mVBar.getMaximum();
-            float current = max > 0 ? (float)sel / max : 0;
-
-            int ry = mRoot.getSize().y;
-
-            // The root contains composite groups
-            // which in turn contain Toggle/Item CLabel instances
-            Control[] children = mRoot.getChildren();
-            findVisibleItem: for (int i = children.length - 1; i >= 0; i--) {
-                Control ci = children[i];
-                if (ci.isVisible() && ci instanceof Composite) {
-                    Control[] children2 = ((Composite) ci).getChildren();
-                    for (int j = children2.length - 1; j >= 0; j--) {
-                        Control cj = children2[j];
-                        if (cj.isVisible()) {
-                            // This is the bottom-most visible item
-                            ry = ci.getLocation().y + cj.getLocation().y + cj.getSize().y;
-                            break findVisibleItem;
-                        }
-                    }
+        mAccordion = new AccordionControl(this, SWT.NONE, headers, fillVertical, wrap) {
+            @Override
+            protected Composite createChildContainer(Composite parent) {
+                Composite composite = super.createChildContainer(parent);
+                if (mPaletteMode.isPreview()) {
+                    composite.setBackground(mBackground);
                 }
+                addMenu(composite);
+                return composite;
             }
-
-
-            int vy = getSize().y;
-            // Scrollable size is the height of the root view
-            // less the current view visible height.
-            int y = ry > vy ? ry - vy + 2 : 0;
-            // Thumb size is the ratio between root view and visible height.
-            float ft = ry > 0 ? (float)vy / ry : 1;
-            int thumb = (int) Math.ceil(y * ft);
-            if (thumb < 1) {
-                thumb = 1;
-            }
-            y += thumb;
-
-
-            if (y != max) {
-                int minimum = mVBar.getMinimum();
-                mVBar.setEnabled(y > 0);
-                int maximum = y < 0 ? 1 : y;
-                int selection = (int) (y * current);
-                int increment = 20;
-                int pageIncrement = thumb;
-                mVBar.setValues(selection, minimum, maximum, thumb, increment, pageIncrement);
-                scrollScrollbar();
-                return true;
+            @Override
+            protected void createChildren(Composite parent, Object header) {
+                assert categoryToItems != null;
+                List<ViewElementDescriptor> list = categoryToItems.get(header);
+                for (ViewElementDescriptor desc : list) {
+                    createItem(parent, desc);
+                }
             }
+        };
+        addMenu(mAccordion);
+        for (CLabel headerLabel : mAccordion.getHeaderLabels()) {
+            addMenu(headerLabel);
         }
+        setLayout(new FillLayout());
 
-        return false;
-    }
-
-    private void scrollScrollbar() {
-        if (mVBar != null && mRoot != null) {
-            Point p = mRoot.getLocation();
-            p.y = - mVBar.getSelection();
-            mRoot.setLocation(p);
+        // Expand All for icon-only mode, but don't store it as the persistent auto-close mode;
+        // when we enter other modes it will read back whatever persistent mode.
+        if (mPaletteMode == PaletteMode.ICON_ONLY) {
+            mAccordion.expandAll(true);
+            mAccordion.setAutoClose(false);
+        } else {
+            mAccordion.setAutoClose(getSavedAutoCloseMode());
         }
-    }
-
-    private void addGroup(Composite parent,
-            String uiName,
-            List<ViewElementDescriptor> descriptors) {
-
-        Composite group = new Composite(parent, SWT.NONE);
-        GridLayoutBuilder.create(group).columns(1).columnsEqual().spacing(0).noMargins();
-        GridDataBuilder.create(group).hFill().hGrab();
-
-        Toggle toggle = new Toggle(group, uiName);
-        GridDataBuilder.create(toggle).hFill().hGrab();
-
-        for (ViewElementDescriptor desc : descriptors) {
 
-            // Exclude the <include> and <merge> tags from the View palette.
-            // We don't have drop support for it right now, although someday we should.
-            String xmlName = desc.getXmlName();
-            if (VIEW_INCLUDE.equals(xmlName) || VIEW_MERGE.equals(xmlName)) {
-                continue;
-            }
-
-            Item item = new Item(group, desc);
-            toggle.addItem(item);
-            GridDataBuilder.create(item).hFill().hGrab();
-        }
+        layout(true);
     }
 
     /* package */ GraphicalEditorPart getEditor() {
         return mEditor;
     }
 
-    /**
-     * A Toggle widget is a row that is the head of a group.
-     * <p/>
-     * When clicked, the toggle will show/hide all the {@link Item} widgets that have been
-     * added to it using {@link #addItem(Item)}.
-     */
-    private class Toggle extends CLabel implements MouseTrackListener, MouseListener {
-        private boolean mMouseIn;
-        private DragSource mSource;
-        private ArrayList<Item> mItems = new ArrayList<Item>();
-
-        public Toggle(Composite parent, String groupName) {
-            super(parent, SWT.NONE);
-            mMouseIn = false;
-
-            setData(null);
-
-            String s = String.format("-= %s =-", groupName);
-            setText(s);
-            setToolTipText(s);
-            //TODO use triangle icon and swap it -- setImage(desc.getIcon());
-            addMouseTrackListener(this);
-            addMouseListener(this);
-        }
-
-        public void addItem(Item item) {
-            mItems.add(item);
-        }
-
-        @Override
-        public void dispose() {
-            if (mSource != null) {
-                mSource.dispose();
-                mSource = null;
+    private Control createItem(Composite parent, ViewElementDescriptor desc) {
+        Control item = null;
+        switch (mPaletteMode) {
+            case SMALL_PREVIEW:
+            case TINY_PREVIEW:
+            case PREVIEW: {
+                ImageDescriptor descriptor = mPreviewIconFactory.getImageDescriptor(desc);
+                if (descriptor != null) {
+                    Image image = descriptor.createImage();
+                    ImageControl imageControl = new ImageControl(parent, SWT.None, image);
+                    if (mPaletteMode.isScaledPreview()) {
+                        // Try to preserve the overall size since rendering sizes typically
+                        // vary with the dpi - so while the scaling factor for a 160 dpi
+                        // rendering the scaling factor should be 0.5, for a 320 dpi one the
+                        // scaling factor should be half that, 0.25.
+                        float scale = 1.0f;
+                        if (mPaletteMode == PaletteMode.SMALL_PREVIEW) {
+                            scale = 0.75f;
+                        } else if (mPaletteMode == PaletteMode.TINY_PREVIEW) {
+                            scale = 0.5f;
+                        }
+                        int dpi = mEditor.getConfigurationComposite().getDensity().getDpiValue();
+                        while (dpi > 160) {
+                            scale = scale / 2;
+                            dpi = dpi / 2;
+                        }
+                        imageControl.setScale(scale);
+                    }
+                    imageControl.setHoverColor(getDisplay().getSystemColor(SWT.COLOR_WHITE));
+                    imageControl.setBackground(mBackground);
+                    String toolTip = desc.getUiName();
+                    // It appears pretty much none of the descriptors have tooltips
+                    //String descToolTip = desc.getTooltip();
+                    //if (descToolTip != null && descToolTip.length() > 0) {
+                    //    toolTip = toolTip + "\n" + descToolTip;
+                    //}
+                    imageControl.setToolTipText(toolTip);
+
+                    item = imageControl;
+                } else {
+                    // Just use an Icon+Text item for these for now
+                    item = new IconTextItem(parent, desc);
+                }
+                break;
             }
-            super.dispose();
-        }
-
-        @Override
-        public int getStyle() {
-            int style = super.getStyle();
-            if (mMouseIn) {
-                style |= SWT.SHADOW_IN;
+            case ICON_TEXT: {
+                item = new IconTextItem(parent, desc);
+                break;
             }
-            return style;
-        }
-
-        // -- MouseTrackListener callbacks
-
-        public void mouseEnter(MouseEvent e) {
-            if (!mMouseIn) {
-                mMouseIn = true;
-                redraw();
+            case ICON_ONLY: {
+                item = new ImageControl(parent, SWT.None, desc.getIcon());
+                item.setToolTipText(desc.getUiName());
+                break;
             }
+            default:
+                throw new IllegalArgumentException("Not yet implemented");
         }
 
-        public void mouseExit(MouseEvent e) {
-            if (mMouseIn) {
-                mMouseIn = false;
-                redraw();
+        final DragSource source = new DragSource(item, DND.DROP_COPY);
+        source.setTransfer(new Transfer[] { SimpleXmlTransfer.getInstance() });
+        source.addDragListener(new DescDragSourceListener(desc));
+        item.addDisposeListener(new DisposeListener() {
+            public void widgetDisposed(DisposeEvent e) {
+                source.dispose();
             }
-        }
-
-        public void mouseHover(MouseEvent e) {
-            // pass
-        }
+        });
+        addMenu(item);
 
-        // -- MouseListener callbacks
-
-        public void mouseDoubleClick(MouseEvent arg0) {
-            // pass
-        }
-
-        public void mouseDown(MouseEvent arg0) {
-            // pass
-        }
-
-        public void mouseUp(MouseEvent arg0) {
-            for (Item i : mItems) {
-                if (i.isVisible()) {
-                    Object ld = i.getLayoutData();
-                    if (ld instanceof GridData) {
-                        GridData gd = (GridData) ld;
-
-                        i.setData(gd.heightHint != SWT.DEFAULT ?
-                                    Integer.valueOf(gd.heightHint) :
-                                        null);
-                        gd.heightHint = 0;
-                    }
-                } else {
-                    Object ld = i.getLayoutData();
-                    if (ld instanceof GridData) {
-                        GridData gd = (GridData) ld;
-
-                        Object d = i.getData();
-                        if (d instanceof Integer) {
-                            gd.heightHint = ((Integer) d).intValue();
-                        } else {
-                            gd.heightHint = SWT.DEFAULT;
-                        }
-                    }
-                }
-                i.setVisible(!i.isVisible());
-            }
-
-            // Tell the root composite that its content changed.
-            mRoot.layout(true /*changed*/);
-            // Force the top composite to recompute the scrollbar and refresh it.
-            mControlListener.controlResized(null /*event*/);
-        }
+        return item;
     }
 
     /**
      * An Item widget represents one {@link ElementDescriptor} that can be dropped on the
      * GLE2 canvas using drag'n'drop.
      */
-    private class Item extends CLabel implements MouseTrackListener {
+    private class IconTextItem extends CLabel implements MouseTrackListener {
 
         private boolean mMouseIn;
-        private DragSource mSource;
-        private final ViewElementDescriptor mDesc;
 
-        public Item(Composite parent, ViewElementDescriptor desc) {
+        public IconTextItem(Composite parent, ViewElementDescriptor desc) {
             super(parent, SWT.NONE);
-            mDesc = desc;
             mMouseIn = false;
 
             setText(desc.getUiName());
             setImage(desc.getIcon());
             setToolTipText(desc.getTooltip());
             addMouseTrackListener(this);
-
-            // DND Reference: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html
-            mSource = new DragSource(this, DND.DROP_COPY);
-            mSource.setTransfer(new Transfer[] { SimpleXmlTransfer.getInstance() });
-            mSource.addDragListener(new DescDragSourceListener(mDesc));
-        }
-
-        @Override
-        public void dispose() {
-            if (mSource != null) {
-                mSource.dispose();
-                mSource = null;
-            }
-            super.dispose();
         }
 
         @Override
@@ -613,7 +630,7 @@ public class PaletteControl extends Composite {
                 // example the preview of an empty layout), so instead create a placeholder
                 // image
                 // Render the palette item itself as an image
-                Control control = PaletteControl.this;
+                Control control = ((DragSource) event.widget).getControl();
                 GC gc = new GC(control);
                 Point size = control.getSize();
                 Display display = getDisplay();
@@ -733,7 +750,7 @@ public class PaletteControl extends Composite {
                 int renderHeight = Math.min(screenBounds.height, MAX_RENDER_HEIGHT);
                 LayoutLog silentLogger = new LayoutLog();
                 session = editor.render(model, renderWidth, renderHeight,
-                    null /* explodeNodes */, hasTransparency, silentLogger);
+                    null /* explodeNodes */, hasTransparency, silentLogger, RenderingMode.NORMAL);
             } catch (Throwable t) {
                 // Previews can fail for a variety of reasons -- let's not bug
                 // the user with it
@@ -842,4 +859,90 @@ public class PaletteControl extends Composite {
             }
         }
     }
+
+    /** Action for switching view modes via radio buttons */
+    private class PaletteModeAction extends Action {
+        private final PaletteMode mMode;
+
+        PaletteModeAction(PaletteMode mode) {
+            super(mode.getActionLabel(), IAction.AS_RADIO_BUTTON);
+            mMode = mode;
+            boolean selected = mMode == mPaletteMode;
+            setChecked(selected);
+            setEnabled(!selected);
+        }
+
+        @Override
+        public void run() {
+            if (isEnabled()) {
+                mPaletteMode = mMode;
+                refreshPalette();
+                savePaletteMode();
+            }
+        }
+    }
+
+    /** Action for toggling various checkbox view modes - categories, sorting, etc */
+    private class ToggleViewOptionAction extends Action {
+        private final int mAction;
+        final static int TOGGLE_CATEGORY = 1;
+        final static int TOGGLE_ALPHABETICAL = 2;
+        final static int TOGGLE_AUTO_CLOSE = 3;
+
+        ToggleViewOptionAction(String title, int action, boolean checked) {
+            super(title, IAction.AS_CHECK_BOX);
+            mAction = action;
+            setChecked(checked);
+        }
+
+        @Override
+        public void run() {
+            switch (mAction) {
+                case TOGGLE_CATEGORY:
+                    mCategories = !mCategories;
+                    refreshPalette();
+                    break;
+                case TOGGLE_ALPHABETICAL:
+                    mAlphabetical = !mAlphabetical;
+                    refreshPalette();
+                    break;
+                case TOGGLE_AUTO_CLOSE:
+                    mAutoClose = !mAutoClose;
+                    mAccordion.setAutoClose(mAutoClose);
+                    break;
+            }
+            savePaletteMode();
+        }
+    }
+
+    private void addMenu(Control control) {
+        control.addMenuDetectListener(new MenuDetectListener() {
+            public void menuDetected(MenuDetectEvent e) {
+                MenuManager manager = new MenuManager() {
+                    @Override
+                    public boolean isDynamic() {
+                        return true;
+                    }
+                };
+                for (PaletteMode mode : PaletteMode.values()) {
+                        manager.add(new PaletteModeAction(mode));
+                }
+                manager.add(new Separator());
+                manager.add(new ToggleViewOptionAction("Show Categories",
+                        ToggleViewOptionAction.TOGGLE_CATEGORY,
+                        mCategories));
+                manager.add(new ToggleViewOptionAction("Sort Alphabetically",
+                        ToggleViewOptionAction.TOGGLE_ALPHABETICAL,
+                        mAlphabetical));
+                manager.add(new Separator());
+                manager.add(new ToggleViewOptionAction("Auto Close Previous",
+                        ToggleViewOptionAction.TOGGLE_AUTO_CLOSE,
+                        mAutoClose));
+                Menu menu = manager.createContextMenu(PaletteControl.this);
+                Point point = new Point(e.x, e.y);
+                menu.setLocation(point.x, point.y);
+                menu.setVisible(true);
+            }
+        });
+    }
 }
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java
new file mode 100644 (file)
index 0000000..998b28a
--- /dev/null
@@ -0,0 +1,387 @@
+/*
+ * Copyright (C) 2011 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.gle2;
+
+import static com.android.ide.eclipse.adt.AndroidConstants.DOT_PNG;
+
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.ViewInfo;
+import com.android.ide.common.rendering.api.Params.RenderingMode;
+import com.android.ide.eclipse.adt.AdtPlugin;
+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.gre.ViewMetadataRepository;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository.RenderMode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.graphics.RGB;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.awt.image.BufferedImage;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.net.MalformedURLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+
+import javax.imageio.ImageIO;
+
+/**
+ * Factory which can provide preview icons for android views of a particular SDK and
+ * editor's configuration chooser
+ */
+public class PreviewIconFactory {
+    private PaletteControl mPalette;
+    private RGB mBackground;
+    private File mImageDir;
+
+    private static final String PREVIEW_INFO_FILE = "preview.properties"; //$NON-NLS-1$
+
+    public PreviewIconFactory(PaletteControl palette) {
+        mPalette = palette;
+    }
+
+    /**
+     * Resets the state in the preview icon factory such that it will re-fetch information
+     * like the theme and SDK (the icons themselves are cached in a directory across IDE
+     * session though)
+     */
+    public void reset() {
+        mImageDir = null;
+        mBackground = null;
+    }
+
+    /**
+     * Returns an image descriptor for the given element descriptor, or null if no image
+     * could be computed. The rendering parameters (SDK, theme etc) correspond to those
+     * stored in the associated palette.
+     *
+     * @param desc the element descriptor to get an image for
+     * @return an image descriptor, or null if no image could be rendered
+     */
+    public ImageDescriptor getImageDescriptor(ElementDescriptor desc) {
+        File imageDir = getImageDir(false);
+        if (!imageDir.exists()) {
+            render();
+        }
+        File file = new File(imageDir, getFileName(desc));
+        if (file.exists()) {
+            try {
+                return ImageDescriptor.createFromURL(file.toURI().toURL());
+            } catch (MalformedURLException e) {
+                AdtPlugin.log(e, "Could not create image descriptor for %s", file);
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Partition the elements in the document according to their rendering preferences;
+     * elements that should be skipped are removed, elements that should be rendered alone
+     * are placed in their own list, etc
+     *
+     * @param document the document containing render fragments for the various elements
+     * @return
+     */
+    private List<List<Element>> partitionRenderElements(Document document) {
+        List<List<Element>> elements = new ArrayList<List<Element>>();
+
+        List<Element> shared = new ArrayList<Element>();
+        Element root = document.getDocumentElement();
+        elements.add(shared);
+
+        ViewMetadataRepository repository = ViewMetadataRepository.get();
+
+        NodeList children = root.getChildNodes();
+        for (int i = 0, n = children.getLength(); i < n; i++) {
+            Node node = children.item(i);
+            if (node.getNodeType() == Node.ELEMENT_NODE) {
+                Element element = (Element) node;
+                String fqn = repository.getFullClassName(element);
+                assert fqn.length() > 0 : element.getNodeName();
+                RenderMode renderMode = repository.getRenderMode(fqn);
+                if (renderMode == RenderMode.ALONE) {
+                    elements.add(Collections.singletonList(element));
+                } else if (renderMode == RenderMode.NORMAL) {
+                    shared.add(element);
+                } else {
+                    assert renderMode == RenderMode.SKIP;
+                }
+            }
+        }
+
+        return elements;
+    }
+
+    /**
+     * Renders ALL the widgets and then extracts image data for each view and saves it
+     * on disk
+     */
+    private boolean render() {
+        LayoutEditor layoutEditor = mPalette.getEditor().getLayoutEditor();
+
+        ViewMetadataRepository repository = ViewMetadataRepository.get();
+        Document document = repository.getRenderingConfigDoc();
+
+        if (document == null) {
+            return false;
+        }
+
+        // Construct UI model from XML
+        AndroidTargetData data = layoutEditor.getTargetData();
+        DocumentDescriptor documentDescriptor;
+        if (data == null) {
+            documentDescriptor = new DocumentDescriptor("temp", null/*children*/);//$NON-NLS-1$
+        } else {
+            documentDescriptor = data.getLayoutDescriptors().getDescriptor();
+        }
+        UiDocumentNode model = (UiDocumentNode) documentDescriptor.createUiNode();
+        model.setEditor(layoutEditor);
+        GraphicalEditorPart editor = mPalette.getEditor();
+        model.setUnknownDescriptorProvider(editor.getModel().getUnknownDescriptorProvider());
+
+        Element documentElement = document.getDocumentElement();
+        List<List<Element>> elements = partitionRenderElements(document);
+        for (List<Element> elementGroup : elements) {
+            // Replace the document elements with the current element group
+            while (documentElement.getFirstChild() != null) {
+                documentElement.removeChild(documentElement.getFirstChild());
+            }
+            for (Element element : elementGroup) {
+                documentElement.appendChild(element);
+            }
+
+            model.loadFromXmlNode(document);
+
+            RenderSession session = null;
+            try {
+                LayoutLog logger = new RenderLogger("palette");
+                // Important to get these sizes large enough for clients that don't support
+                // RenderMode.FULL_EXPAND such as 1.6
+                int width = 200;
+                int height = 2000;
+                Set<UiElementNode> expandNodes = Collections.<UiElementNode>emptySet();
+                RenderingMode renderingMode = RenderingMode.FULL_EXPAND;
+                session = editor.render(model, width, height, expandNodes,
+                        false /*hasTransparency*/, logger,
+                        renderingMode);
+
+            } catch (Throwable t) {
+                // If there are internal errors previewing the components just revert to plain
+                // icons and labels
+                continue;
+            }
+
+            if (session != null) {
+                if (session.getResult().isSuccess()) {
+                    BufferedImage image = session.getImage();
+                    if (image != null && image.getWidth() > 0 && image.getHeight() > 0) {
+                        File imageDir = getImageDir(true);
+
+                        // TODO - use resource resolution instead?
+                        if (mBackground == null) {
+                            int rgb = image.getRGB(image.getWidth()-1, image.getHeight()-1);
+                            RGB color = new RGB((rgb & 0xFF0000) >> 16, (rgb & 0xFF00) >> 8,
+                                    rgb & 0xFF);
+                            storeBackground(imageDir, color);
+                        }
+
+                        List<ViewInfo> viewInfoList = session.getRootViews();
+                        if (viewInfoList != null && viewInfoList.size() > 0) {
+                            // We don't render previews under a <merge> so there should
+                            // only be one root.
+                            ViewInfo firstRoot = viewInfoList.get(0);
+                            List<ViewInfo> infos = firstRoot.getChildren();
+                            for (ViewInfo info : infos) {
+                                Object cookie = info.getCookie();
+                                if (!(cookie instanceof UiElementNode)) {
+                                    continue;
+                                }
+                                UiElementNode node = (UiElementNode) cookie;
+                                String fileName = getFileName(node);
+                                File file = new File(imageDir, fileName);
+                                if (file.exists()) {
+                                    // On Windows, perhaps we need to rename instead?
+                                    file.delete();
+                                }
+                                int x1 = info.getLeft();
+                                int y1 = info.getTop();
+                                int x2 = info.getRight();
+                                int y2 = info.getBottom();
+                                if (x1 != x2 && y1 != y2) {
+                                    savePreview(file, image, x1, y1, x2, y2);
+                                }
+                            }
+                        }
+                    }
+                }
+
+                session.dispose();
+            }
+        }
+
+        return true;
+    }
+
+    private String getFileName(ElementDescriptor descriptor) {
+        return descriptor.getUiName() + DOT_PNG;
+    }
+
+    private String getFileName(UiElementNode node) {
+        ViewMetadataRepository repository = ViewMetadataRepository.get();
+        String fqn = repository.getFullClassName((Element) node.getXmlNode());
+        return fqn.substring(fqn.lastIndexOf('.') + 1) + DOT_PNG;
+    }
+
+    /**
+     * Cleans up a name by removing punctuation and whitespace etc to make
+     * it a better filename
+     * @param name
+     * @return
+     */
+    private static String cleanup(String name) {
+        // Extract just the characters (no whitespace, parentheses, punctuation etc)
+        // to ensure that the filename is pretty portable
+        StringBuilder sb = new StringBuilder(name.length());
+        for (int i = 0; i < name.length(); i++) {
+            char c = name.charAt(i);
+            if (Character.isJavaIdentifierPart(c)) {
+                sb.append(Character.toLowerCase(c));
+            }
+        }
+
+        return sb.toString();
+    }
+
+    /** Returns the location of a directory containing image previews (which may not exist) */
+    private File getImageDir(boolean create) {
+        if (mImageDir == null) {
+            // Location for plugin-related state data
+            IPath pluginState = AdtPlugin.getDefault().getStateLocation();
+
+            // We have multiple directories - one for each combination of SDK, theme and device
+            // (and later, possibly other qualifiers).
+            // These are created -lazily-.
+            String targetName = mPalette.getCurrentTarget().getFullName();
+            String androidTargetNamePrefix = "Android ";
+            String themeNamePrefix = "Theme.";
+            if (targetName.startsWith(androidTargetNamePrefix)) {
+                targetName = targetName.substring(androidTargetNamePrefix.length());
+            }
+            String themeName = mPalette.getCurrentTheme();
+            if (themeName == null) {
+                themeName = "Theme"; //$NON-NLS-1$
+            }
+            if (themeName.startsWith(themeNamePrefix)) {
+                themeName = themeName.substring(themeNamePrefix.length());
+            }
+            String dirName = String.format("palette-%s-%s-%s",
+                    cleanup(targetName), cleanup(themeName), cleanup(mPalette.getCurrentDevice()));
+            IPath dirPath = pluginState.append(dirName);
+
+            mImageDir = new File(dirPath.toOSString());
+        }
+
+        if (create && !mImageDir.exists()) {
+            mImageDir.mkdirs();
+        }
+
+        return mImageDir;
+    }
+
+    private void savePreview(File output, BufferedImage image,
+            int left, int top, int right, int bottom) {
+        try {
+            BufferedImage im = ImageUtils.subImage(image, left, top, right, bottom);
+            ImageIO.write(im, "PNG", output); //$NON-NLS-1$
+        } catch (IOException e) {
+            AdtPlugin.log(e, "Failed writing palette file");
+        }
+    }
+
+    private void storeBackground(File imageDir, RGB color) {
+        mBackground = color;
+        File file = new File(imageDir, PREVIEW_INFO_FILE);
+        String colors = String.format("background=%02x,%02x,%02x",
+                color.red, color.green, color.blue);
+        AdtPlugin.writeFile(file, colors);
+    }
+
+    public RGB getBackgroundColor() {
+        if (mBackground == null) {
+            mBackground = null;
+
+            File imageDir = getImageDir(false);
+            if (!imageDir.exists()) {
+                render();
+            }
+
+            File file = new File(imageDir, PREVIEW_INFO_FILE);
+            if (file.exists()) {
+                Properties properties = new Properties();
+                Reader reader = null;
+                try {
+                    reader = new BufferedReader(new FileReader(file));
+                    properties.load(reader);
+                } catch (IOException e) {
+                    AdtPlugin.log(e, "Can't read preview properties");
+                } finally {
+                    if (reader != null) {
+                        try {
+                            reader.close();
+                        } catch (IOException e) {
+                            // Nothing useful can be done.
+                        }
+                    }
+                }
+
+                String colorString = (String) properties.get("background"); //$NON-NLS-1$
+                if (colorString != null) {
+                    String[] colors = colorString.split(","); //$NON-NLS-1$
+                    if (colors.length == 3) {
+                        colorString = colorString.trim();
+                        int red = Integer.parseInt(colors[0], 16);
+                        int green = Integer.parseInt(colors[1], 16);
+                        int blue = Integer.parseInt(colors[2], 16);
+                        mBackground = new RGB(red, green, blue);
+                    }
+                }
+            }
+
+            if (mBackground == null) {
+                mBackground = new RGB(0,0,0);
+            }
+        }
+
+        return mBackground;
+    }
+}
index 095c6d7..04325ef 100644 (file)
@@ -49,6 +49,7 @@ abstract class SubmenuAction extends Action implements MenuListener, IMenuCreato
     protected abstract void addMenuItems(Menu menu);
 
     public void menuShown(MenuEvent e) {
+        // TODO: Replace this stuff with manager.setRemoveAllWhenShown(true);
         MenuItem[] menuItems = mMenu.getItems();
         for (int i = 0; i < menuItems.length; i++) {
             menuItems[i].dispose();
index 84fe12d..049ac64 100644 (file)
 package com.android.ide.eclipse.adt.internal.editors.layout.gre;
 
 import static com.android.ide.common.api.IViewMetadata.FillPreference.NONE;
-import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_INCLUDE;
-import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_MERGE;
+import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
+import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX;
+import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX;
 
 import com.android.ide.common.api.IViewMetadata.FillPreference;
 import com.android.ide.eclipse.adt.AdtPlugin;
@@ -52,6 +54,9 @@ import javax.xml.parsers.DocumentBuilderFactory;
  * classes
  */
 public class ViewMetadataRepository {
+    private static final String PREVIEW_CONFIG_FILENAME = "rendering-configs.xml";  //$NON-NLS-1$
+    private static final String METADATA_FILENAME = "extra-view-metadata.xml";  //$NON-NLS-1$
+
     /** Singleton instance */
     private static ViewMetadataRepository sInstance = new ViewMetadataRepository();
 
@@ -103,18 +108,96 @@ public class ViewMetadataRepository {
         return mClassToView;
     }
 
+    /**
+     * Returns an XML document containing rendering configurations for the various Android
+     * views. The FQN of each view can be obtained via the
+     * {@link #getFullClassName(Element)} method
+     *
+     * @return an XML document containing rendering elements
+     */
+    public Document getRenderingConfigDoc() {
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        Class<ViewMetadataRepository> clz = ViewMetadataRepository.class;
+        InputStream paletteStream = clz.getResourceAsStream(PREVIEW_CONFIG_FILENAME);
+        InputSource is = new InputSource(paletteStream);
+        try {
+            factory.setNamespaceAware(true);
+            factory.setValidating(false);
+            factory.setIgnoringElementContentWhitespace(true);
+            DocumentBuilder builder = factory.newDocumentBuilder();
+            return builder.parse(is);
+        } catch (Exception e) {
+            AdtPlugin.log(e, "Parsing palette file failed");
+            return null;
+        }
+    }
+
+    /**
+     * Returns a fully qualified class name for an element in the rendering document
+     * returned by {@link #getRenderingConfigDoc()}
+     *
+     * @param element the element to look up the fqcn for
+     * @return the fqcn of the view the element represents a preview for
+     */
+    public String getFullClassName(Element element) {
+        // We don't use the element tag name, because in some cases we have
+        // an outer element to render some interesting inner element, such as a tab widget
+        // (which must be rendered inside a tab host).
+        //
+        // Therefore, we instead use the convention that the id is the fully qualified
+        // class name, with .'s replaced with _'s.
+
+
+        // Special case: for tab host we aren't allowed to mess with the id
+
+        String id = element.getAttributeNS(ANDROID_URI, ATTR_ID);
+
+        if ("@android:id/tabhost".equals(id)) {
+            // Special case to distinguish TabHost and TabWidget
+            NodeList children = element.getChildNodes();
+            if (children.getLength() > 1 && (children.item(1) instanceof Element)) {
+                Element child = (Element) children.item(1);
+                String childId = child.getAttributeNS(ANDROID_URI, ATTR_ID);
+                if ("@+id/android_widget_TabWidget".equals(childId)) {
+                    return "android.widget.TabWidget"; // TODO: Tab widget!
+                }
+            }
+            return "android.widget.TabHost"; // TODO: Tab widget!
+        }
+
+        StringBuilder sb = new StringBuilder();
+        int i = 0;
+        if (id.startsWith(NEW_ID_PREFIX)) {
+            i = NEW_ID_PREFIX.length();
+        } else if (id.startsWith(ID_PREFIX)) {
+            i = ID_PREFIX.length();
+        }
+
+        for (; i < id.length(); i++) {
+            char c = id.charAt(i);
+            if (c == '_') {
+                sb.append('.');
+            } else {
+                sb.append(c);
+            }
+        }
+
+        return sb.toString();
+    }
+
     /** Returns an ordered list of categories and views, parsed from a metadata file */
     private List<CategoryData> getCategories() {
         if (mCategories == null) {
             mCategories = new ArrayList<CategoryData>();
 
             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
-            String fileName = "extra-view-metadata.xml"; //$NON-NLS-1$
-            InputStream inputStream = ViewMetadataRepository.class.getResourceAsStream(fileName);
+            Class<ViewMetadataRepository> clz = ViewMetadataRepository.class;
+            InputStream inputStream = clz.getResourceAsStream(METADATA_FILENAME);
             InputSource is = new InputSource(new BufferedInputStream(inputStream));
             try {
                 factory.setNamespaceAware(true);
                 factory.setValidating(false);
+                factory.setIgnoringElementContentWhitespace(true);
                 DocumentBuilder builder = factory.newDocumentBuilder();
                 Document document = builder.parse(is);
                 Map<String, FillPreference> fillTypes = new HashMap<String, FillPreference>();
@@ -144,7 +227,15 @@ public class ViewMetadataRepository {
                                     if (fillPreference == null) {
                                         fillPreference = NONE;
                                     }
-                                    ViewData view = new ViewData(category, fqcn, fillPreference);
+                                    String skip = child.getAttribute("skip"); //$NON-NLS-1$
+                                    RenderMode mode = RenderMode.NORMAL;
+                                    String render = child.getAttribute("render"); //$NON-NLS-1$
+                                    if (render.length() > 0) {
+                                        mode = RenderMode.get(render);
+                                    }
+                                    ViewData view = new ViewData(category, fqcn, fillPreference,
+                                            skip.length() == 0 ? false : Boolean.valueOf(skip),
+                                            mode);
                                     category.addView(view);
                                 }
                             }
@@ -166,11 +257,13 @@ public class ViewMetadataRepository {
      * available node descriptors, categorizing and sorting them.
      *
      * @param targetData the target data for which to compute palette entries
+     * @param alphabetical if true, sort all items in alphabetical order
+     * @param createCategories if true, organize the items into categories
      * @return a list of pairs where each pair contains of the category label and an
      *         ordered list of elements to be included in that category
      */
     public List<Pair<String, List<ViewElementDescriptor>>> getPaletteEntries(
-            AndroidTargetData targetData) {
+            AndroidTargetData targetData, boolean alphabetical, boolean createCategories) {
         List<Pair<String, List<ViewElementDescriptor>>> result =
             new ArrayList<Pair<String, List<ViewElementDescriptor>>>();
 
@@ -194,17 +287,12 @@ public class ViewMetadataRepository {
 
         for (List<ViewElementDescriptor> list : lists) {
             for (ViewElementDescriptor view : list) {
-                String name = view.getXmlLocalName();
-
-                // Exclude the <include> and <merge> tags from the View palette.
-                // We don't have drop support for it right now, although someday we should.
-                if (VIEW_INCLUDE.equals(name) || VIEW_MERGE.equals(name)) {
-                    continue;
-                }
-
                 ViewData viewData = getClassToView().get(view.getFullClassName());
                 CategoryData category = other;
                 if (viewData != null) {
+                    if (viewData.getSkip()) {
+                        continue;
+                    }
                     category = viewData.getCategory();
                 }
 
@@ -218,6 +306,18 @@ public class ViewMetadataRepository {
             }
         }
 
+        if (!createCategories) {
+            // Squash all categories into a single one, "Views"
+            Map<CategoryData, List<ViewElementDescriptor>> singleCategory =
+                new HashMap<CategoryData, List<ViewElementDescriptor>>();
+            List<ViewElementDescriptor> items = new ArrayList<ViewElementDescriptor>(100);
+            for (Map.Entry<CategoryData, List<ViewElementDescriptor>> entry : categories.entrySet()) {
+                items.addAll(entry.getValue());
+            }
+            singleCategory.put(new CategoryData("Views"), items);
+            categories = singleCategory;
+        }
+
         for (Map.Entry<CategoryData, List<ViewElementDescriptor>> entry : categories.entrySet()) {
             String name = entry.getKey().getName();
             List<ViewElementDescriptor> items = entry.getValue();
@@ -226,38 +326,43 @@ public class ViewMetadataRepository {
             }
 
             // Natural sort of the descriptors
-            Comparator<ViewElementDescriptor> comparator = new Comparator<ViewElementDescriptor>() {
-                public int compare(ViewElementDescriptor v1, ViewElementDescriptor v2) {
-                    String fqcn1 = v1.getFullClassName();
-                    String fqcn2 = v2.getFullClassName();
-                    if (fqcn1 == null) {
-                        // <view> and <merge> tags etc
-                        fqcn1 = v1.getUiName();
-                    }
-                    if (fqcn2 == null) {
-                        fqcn2 = v2.getUiName();
-                    }
-                    ViewData d1 = viewMap.get(fqcn1);
-                    ViewData d2 = viewMap.get(fqcn2);
-
-                    // Use natural sorting order of the view data
-                    // Sort unknown views to the end (and alphabetically among themselves)
-                    if (d1 != null) {
-                        if (d2 != null) {
-                            return d1.getOrdinal() - d2.getOrdinal();
-                        } else {
-                            return 1;
+            if (alphabetical) {
+                Collections.sort(items);
+            } else {
+                Collections.sort(items, new Comparator<ViewElementDescriptor>() {
+                    public int compare(ViewElementDescriptor v1, ViewElementDescriptor v2) {
+                        String fqcn1 = v1.getFullClassName();
+                        String fqcn2 = v2.getFullClassName();
+                        if (fqcn1 == null) {
+                            // <view> and <merge> tags etc
+                            fqcn1 = v1.getUiName();
+                        }
+                        if (fqcn2 == null) {
+                            fqcn2 = v2.getUiName();
                         }
-                    } else {
-                        if (d2 == null) {
-                            return v1.getUiName().compareTo(v2.getUiName());
+                        ViewData d1 = viewMap.get(fqcn1);
+                        ViewData d2 = viewMap.get(fqcn2);
+
+                        // Use natural sorting order of the view data
+                        // Sort unknown views to the end (and alphabetically among themselves)
+                        if (d1 != null) {
+                            if (d2 != null) {
+                                return d1.getOrdinal() - d2.getOrdinal();
+                            } else {
+                                return 1;
+                            }
                         } else {
-                            return -1;
+                            if (d2 == null) {
+                                return v1.getUiName().compareTo(v2.getUiName());
+                            } else {
+                                return -1;
+                            }
                         }
                     }
-                }
-            };
-            Collections.sort(items, comparator);
+                });
+            }
+
+
             result.add(Pair.of(name, items));
         }
 
@@ -315,15 +420,22 @@ public class ViewMetadataRepository {
         private final FillPreference mFillPreference;
         /** The category that the view belongs to */
         private final CategoryData mCategory;
+        /** Skip this item in the palette? */
+        private final boolean mSkip;
+        /** Must this item be rendered alone? skipped? etc */
+        private final RenderMode mRenderMode;
         /** The relative rank of the view for natural ordering */
         private final int mOrdinal = sNextOrdinal++;
 
         /** Constructs a new view data for the given class */
-        private ViewData(CategoryData category, String fqcn, FillPreference fillPreference) {
+        private ViewData(CategoryData category, String fqcn,
+                FillPreference fillPreference, boolean skip, RenderMode renderMode) {
             super();
             mCategory = category;
             mFqcn = fqcn;
             mFillPreference = fillPreference;
+            mSkip = skip;
+            mRenderMode = renderMode;
         }
 
         /** Returns the category for views of this type */
@@ -350,6 +462,14 @@ public class ViewMetadataRepository {
         public int compareTo(ViewData other) {
             return mOrdinal - other.mOrdinal;
         }
+
+        public RenderMode getRenderMode() {
+            return mRenderMode;
+        }
+
+        public boolean getSkip() {
+            return mSkip;
+        }
     }
 
     /**
@@ -367,4 +487,62 @@ public class ViewMetadataRepository {
 
         return FillPreference.NONE;
     }
+
+    /**
+     * Returns the {@link RenderMode} for classes with the given fully qualified class
+     * name
+     *
+     * @param fqcn the fully qualified class name
+     * @return the {@link RenderMode} to use for previews of the given view type
+     */
+    public RenderMode getRenderMode(String fqcn) {
+        ViewData view = getClassToView().get(fqcn);
+        if (view != null) {
+            return view.getRenderMode();
+        }
+
+        return RenderMode.ALONE;
+    }
+
+    /**
+     * Returns true if classes with the given fully qualified class name should be hidden
+     * or skipped from the palette
+     *
+     * @param fqcn the fully qualified class name
+     * @return true if views of the given type should be hidden from the palette
+     */
+    public boolean getSkip(String fqcn) {
+        ViewData view = getClassToView().get(fqcn);
+        if (view != null) {
+            return view.getSkip();
+        }
+
+        return false;
+    }
+
+    /** Render mode for palette preview */
+    public enum RenderMode {
+        /**
+         * Render previews, and it can be rendered as a sibling of many other views in a
+         * big linear layout
+         */
+        NORMAL,
+        /** This view needs to be rendered alone */
+        ALONE,
+        /**
+         * Skip this element; it doesn't work or does not produce any visible artifacts
+         * (such as the basic layouts)
+         */
+        SKIP;
+
+        public static RenderMode get(String render) {
+            if ("alone".equals(render)) {
+                return ALONE;
+            } else if ("skip".equals(render)) {
+                return SKIP;
+            } else {
+                return NORMAL;
+            }
+        }
+    }
 }
index 31b71aa..9fa235b 100644 (file)
@@ -19,6 +19,8 @@
 <!ATTLIST view
     class CDATA #REQUIRED
     previewXml CDATA #IMPLIED
+    skip (true|false) "false"
+    render (alone|skip|normal) "normal"
     fill ( none|both|width|height|opposite|
           width_in_vertical|height_in_horizontal) "none"
 >
         <view class="android.widget.RatingBar" />
         <view class="android.widget.SeekBar" fill="width_in_vertical" />
     </category>
+    <category name="Layouts">
+        <view class="android.widget.LinearLayout" fill="opposite" render="skip"/>
+        <view class="android.widget.RelativeLayout" fill="opposite" render="skip"/>
+        <view class="android.widget.FrameLayout" fill="opposite" render="skip"/>
+        <view class="android.widget.AbsoluteLayout" fill="opposite" render="skip"/>
+        <view class="android.widget.TableLayout" fill="opposite" render="skip"/>
+        <view class="android.widget.TableRow" fill="opposite" render="skip"/>
+    </category>
     <category name="Composite">
-        <view class="android.widget.ListView" fill="width_in_vertical" />
-        <view class="android.widget.ExpandableListView" fill="width_in_vertical" />
-        <view class="android.widget.TwoLineListItem" />
-        <view class="android.widget.GridView" fill="opposite"/>
-        <view class="android.widget.ScrollView" fill="opposite"/>
-        <view class="android.widget.HorizontalScrollView" />
-        <view class="android.widget.SearchView" />
-        <view class="android.widget.SlidingDrawer" />
-        <view class="android.widget.TabHost" fill="width_in_vertical" />
-        <view class="android.widget.TabWidget" />
-        <view class="android.webkit.WebView" fill="opposite"/>
+        <view class="android.widget.ListView" fill="width_in_vertical"  render="skip"/>
+        <view class="android.widget.ExpandableListView" fill="width_in_vertical"  render="skip"/>
+        <view class="android.widget.TwoLineListItem"  render="skip"/>
+        <view class="android.widget.GridView" fill="opposite" render="skip"/>
+        <view class="android.widget.ScrollView" fill="opposite" render="skip"/>
+        <view class="android.widget.HorizontalScrollView" render="skip"/>
+        <view class="android.widget.SearchView" render="skip"/>
+        <view class="android.widget.SlidingDrawer"/>
+        <view class="android.widget.TabHost" fill="width_in_vertical" render="alone" />
+        <view class="android.widget.TabWidget" render="alone" />
+        <view class="android.webkit.WebView" fill="opposite" render="skip" />
     </category>
     <category name="Images &amp; Media">
         <view class="android.widget.ImageView" />
         <view class="android.widget.ImageButton" />
-        <view class="android.widget.Gallery" fill="width_in_vertical" />
-        <view class="android.widget.MediaController" />
-        <view class="android.widget.VideoView"  fill="opposite" />
-    </category>
-    <category name="Layouts">
-        <view class="android.widget.LinearLayout" fill="opposite"/>
-        <view class="android.widget.RelativeLayout" fill="opposite"/>
-        <view class="android.widget.FrameLayout" fill="opposite"/>
-        <view class="android.widget.AbsoluteLayout" fill="opposite"/>
-        <view class="android.widget.TableLayout" fill="opposite"/>
-        <view class="android.widget.TableRow" fill="opposite"/>
+        <view class="android.widget.Gallery" fill="width_in_vertical" render="skip"/>
+        <view class="android.widget.MediaController" render="skip"/>
+        <view class="android.widget.VideoView" fill="opposite" render="skip"/>
     </category>
     <category name="Time &amp; Date">
-        <view class="android.widget.TimePicker" />
-        <view class="android.widget.DatePicker" />
+        <view class="android.widget.TimePicker" render="alone"/>
+        <view class="android.widget.DatePicker" render="alone"/>
         <view class="android.widget.CalendarView" />
         <view class="android.widget.Chronometer" />
         <view class="android.widget.AnalogClock" />
         <view class="android.widget.DigitalClock" />
     </category>
     <category name="Transitions">
-        <view class="android.widget.ImageSwitcher" />
-        <view class="android.widget.AdapterViewFlipper" fill="opposite"/>
-        <view class="android.widget.StackView" fill="opposite"/>
-        <view class="android.widget.TextSwitcher" fill="opposite"/>
-        <view class="android.widget.ViewAnimator" fill="opposite"/>
-        <view class="android.widget.ViewFlipper" fill="opposite"/>
-        <view class="android.widget.ViewSwitcher" fill="opposite"/>
+        <view class="android.widget.ImageSwitcher" render="skip"/>
+        <view class="android.widget.AdapterViewFlipper" fill="opposite" render="skip"/>
+        <view class="android.widget.StackView" fill="opposite" render="skip"/>
+        <view class="android.widget.TextSwitcher" fill="opposite" render="skip"/>
+        <view class="android.widget.ViewAnimator" fill="opposite" render="skip"/>
+        <view class="android.widget.ViewFlipper" fill="opposite" render="skip"/>
+        <view class="android.widget.ViewSwitcher" fill="opposite" render="skip"/>
     </category>
     <category name="Advanced">
-        <view class="android.view.View" />
-        <view class="android.view.ViewStub" />
-        <view class="android.gesture.GestureOverlayView" />
-        <view class="android.view.SurfaceView" />
-        <view class="android.widget.NumberPicker" />
-        <view class="android.widget.ZoomButton" />
-        <view class="android.widget.ZoomControls" />
-        <view class="android.widget.DialerFilter" fill="width_in_vertical" />
+        <view class="android.view.View" render="skip"/>
+        <view class="android.view.ViewStub" render="skip"/>
+        <view class="android.gesture.GestureOverlayView" render="skip"/>
+        <view class="android.view.SurfaceView" render="skip"/>
+        <view class="android.widget.NumberPicker" render="alone"/>
+        <view class="android.widget.ZoomButton"/>
+        <view class="android.widget.ZoomControls"/>
+        <view class="include" skip="true" render="skip"/>
+        <view class="merge" skip="true" render="skip"/>
+        <view class="android.widget.DialerFilter" fill="width_in_vertical" render="skip"/>
     </category>
     <category name="Other">
         <!--  This is the catch-all category which contains unknown views if we encounter any -->
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/rendering-configs.xml b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/rendering-configs.xml
new file mode 100644 (file)
index 0000000..2420f8f
--- /dev/null
@@ -0,0 +1,220 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Default configuration for various views to be rendered 
+    TODO: Remove views that don't have custom configuration
+    TODO: Parameterize the custom width (200dip) in the below?
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+    <AnalogClock
+        android:layout_width="wrap_content"
+        android:id="@+id/android_widget_AnalogClock"
+        android:layout_height="75dip">
+    </AnalogClock>
+    <AutoCompleteTextView
+        android:layout_height="wrap_content"
+        android:layout_width="200dip"
+        android:text="AutoComplete"
+        android:id="@+id/android_widget_AutoCompleteTextView">
+    </AutoCompleteTextView>
+    <Button
+        android:text="Button"
+        android:id="@+id/android_widget_Button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content">
+    </Button>
+    <CheckBox
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:text="CheckBox"
+        android:id="@+id/android_widget_CheckBox"
+        android:checked="true">
+    </CheckBox>
+    <CheckedTextView
+        android:text="CheckedTextView"
+        android:id="@+id/android_widget_CheckedTextView"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content">
+    </CheckedTextView>
+    <Chronometer
+        android:text="Chronometer"
+        android:id="@+id/android_widget_Chronometer"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content">
+    </Chronometer>
+    <DigitalClock
+        android:text="DigitalClock"
+        android:id="@+id/android_widget_DigitalClock"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content">
+    </DigitalClock>
+    <EditText
+        android:id="@+id/android_widget_EditText"
+        android:layout_height="wrap_content"
+        android:text="EditText"
+        android:layout_width="200dip">
+    </EditText>
+    <ImageButton
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:id="@+id/android_widget_ImageButton"
+        android:src="@android:drawable/ic_menu_gallery">
+    </ImageButton>
+    <ImageView
+        android:id="@+id/android_widget_ImageView"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:src="@android:drawable/ic_menu_gallery">
+    </ImageView>
+    <MultiAutoCompleteTextView
+        android:layout_height="wrap_content"
+        android:layout_width="200dip"
+        android:text="MultiAutoComplete"
+        android:id="@+id/android_widget_MultiAutoCompleteTextView">
+    </MultiAutoCompleteTextView>
+    <ProgressBar
+        android:id="@+id/android_widget_ProgressBar"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content">
+    </ProgressBar>
+    <QuickContactBadge
+        android:src="@android:drawable/ic_dialog_email"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:id="@+id/android_widget_QuickContactBadge">
+    </QuickContactBadge>
+    <RadioButton
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:id="@+id/android_widget_RadioButton"
+        android:text="RadioButton"
+        android:checked="true">
+    </RadioButton>
+    <RatingBar
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:id="@+id/android_widget_RatingBar"
+        android:rating="1">
+    </RatingBar>
+    <SeekBar
+        android:layout_height="wrap_content"
+        android:id="@+id/android_widget_SeekBar"
+        android:layout_width="200dip"
+        android:progress="30">
+    </SeekBar>
+    <Spinner
+        android:layout_height="wrap_content"
+        android:id="@+id/android_widget_Spinner"
+        android:layout_width="200dip">
+    </Spinner>
+    <TextView
+        android:text="TextView"
+        android:id="@+id/android_widget_TextView"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content">
+    </TextView>
+    <ToggleButton
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:checked="false"
+        android:id="@+id/android_widget_ToggleButton"
+        android:text="ToggleButton">
+    </ToggleButton>
+    <ZoomButton
+        android:id="@+id/android_widget_ZoomButton"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:src="@android:drawable/btn_plus">
+    </ZoomButton>
+    <ZoomControls
+        android:id="@+id/android_widget_ZoomControls"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content">
+    </ZoomControls>
+    <TimePicker
+        android:id="@+id/android_widget_TimePicker"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content">
+    </TimePicker>
+    <DatePicker
+        android:id="@+id/android_widget_DatePicker"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content">
+    </DatePicker>
+    <RadioGroup
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:orientation="horizontal"
+        android:id="@+id/android_widget_RadioGroup">
+        <RadioButton
+            android:checked="true">
+        </RadioButton>
+        <RadioButton></RadioButton>
+        <RadioButton></RadioButton>
+    </RadioGroup>
+    <TabHost
+        android:id="@android:id/tabhost"
+        android:layout_width="200dip"
+        android:layout_height="100dip">
+        <LinearLayout
+            android:id="@+id/linearLayout1"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="vertical">
+            <TabWidget
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:id="@android:id/tabs">
+            </TabWidget>
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:id="@android:id/tabcontent">
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:id="@+id/Tab1">
+                </LinearLayout>
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:id="@+id/Tab2">
+                </LinearLayout>
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:id="@+id/Tab3">
+                </LinearLayout>
+            </FrameLayout>
+        </LinearLayout>
+    </TabHost>
+    <TabHost
+        android:id="@android:id/tabhost"
+        android:layout_width="70dip"
+        android:layout_height="100dip">
+        <LinearLayout
+            android:id="@+id/android_widget_TabWidget"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="vertical">
+            <TabWidget
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:id="@android:id/tabs">
+            </TabWidget>
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:id="@android:id/tabcontent">
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:id="@+id/Tab1">
+                </LinearLayout>
+            </FrameLayout>
+        </LinearLayout>
+    </TabHost>
+</LinearLayout>
\ No newline at end of file
index 6b310b3..96539fa 100755 (executable)
@@ -37,7 +37,7 @@ public class DecorComposite extends Composite {
     public DecorComposite(Composite parent, int style) {
         super(parent, style);
 
-        GridLayoutBuilder.create(this).noMargins().columns(2);
+        GridLayoutBuilder.create(this).noMargins().columns(2).vSpacing(1);
 
         mTitle = new CLabel(this, SWT.NONE);
         GridDataBuilder.create(mTitle).hGrab().hFill().vCenter();
index 6c38ff4..ff39366 100644 (file)
@@ -48,6 +48,8 @@ public final class AdtPrefs extends AbstractPreferenceInitializer {
 
     public final static String PREFS_FORMAT_XML = AdtPlugin.PLUGIN_ID + ".formatXml"; //$NON-NLS-1$
 
+    public final static String PREFS_PALETTE_MODE = AdtPlugin.PLUGIN_ID + ".palette"; //$NON-NLS-1$
+
     /** singleton instance */
     private final static AdtPrefs sThis = new AdtPrefs();
 
@@ -64,6 +66,7 @@ public final class AdtPrefs extends AbstractPreferenceInitializer {
     private boolean mBuildForceErrorOnNativeLibInJar = true;
     private boolean mFormatXml = false;
     private float mMonitorDensity = 0.f;
+    private String mPalette;
 
     public static enum BuildVerbosity {
         /** Build verbosity "Always". Those messages are always displayed, even in silent mode */
@@ -157,6 +160,10 @@ public final class AdtPrefs extends AbstractPreferenceInitializer {
         if (property == null || PREFS_FORMAT_XML.equals(property)) {
             mFormatXml = mStore.getBoolean(PREFS_FORMAT_XML);
         }
+
+        if (property == null || PREFS_PALETTE_MODE.equals(property)) {
+            mPalette = mStore.getString(PREFS_PALETTE_MODE);
+        }
     }
 
     /**
@@ -183,6 +190,18 @@ public final class AdtPrefs extends AbstractPreferenceInitializer {
         return mBuildForceErrorOnNativeLibInJar;
     }
 
+    public String getPaletteModes() {
+        return mPalette;
+    }
+
+    public void setPaletteModes(String palette) {
+        mPalette = palette;
+
+        // need to save this new value to the store
+        IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
+        store.setValue(PREFS_PALETTE_MODE, palette);
+    }
+
     public float getMonitorDensity() {
         return mMonitorDensity;
     }
index ab20473..e198c6e 100644 (file)
@@ -15,6 +15,7 @@
  */
 package com.android.ide.eclipse.adt;
 
+import java.io.File;
 import java.io.StringReader;
 
 import junit.framework.TestCase;
@@ -38,4 +39,11 @@ public class AdtPluginTest extends TestCase {
         assertEquals(input, contents);
     }
 
+    public void testReadWriteFile() throws Exception {
+        File temp = File.createTempFile("test", ".txt");
+        String myContent = "this is\na test";
+        AdtPlugin.writeFile(temp, myContent);
+        String readBack = AdtPlugin.readFile(temp);
+        assertEquals(myContent, readBack);
+    }
 }
index 4b6c642..c611bab 100644 (file)
@@ -275,4 +275,28 @@ public class ImageUtilsTest extends TestCase {
 
         return dest;
     }
+
+    public void testSubImage() throws Exception {
+        BufferedImage image = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB_PRE);
+        Graphics g = image.getGraphics();
+        g.setColor(new Color(0xFF00FF00, true));
+        g.fillRect(0, 0, image.getWidth(), image.getHeight());
+        g.setColor(new Color(0xFFFF0000, true));
+        g.fillRect(25, 25, 50, 50);
+        g.dispose();
+
+        BufferedImage sub = ImageUtils.subImage(image, 25, 25, 35, 45);
+        assertEquals(10, sub.getWidth());
+        assertEquals(20, sub.getHeight());
+        assertEquals(0xFFFF0000, sub.getRGB(0, 0));
+        assertEquals(0xFFFF0000, sub.getRGB(9, 9));
+
+        sub = ImageUtils.subImage(image, 23, 23, 5, 5);
+        assertEquals(5, sub.getWidth());
+        assertEquals(5, sub.getHeight());
+        assertEquals(0xFF00FF00, sub.getRGB(0, 0));
+        assertEquals(0xFFFF0000, sub.getRGB(9, 9));
+    }
+
+
 }