aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java1265
1 files changed, 1265 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java
new file mode 100644
index 000000000..46168b70f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java
@@ -0,0 +1,1265 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_TEXT;
+import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
+import static com.android.SdkConstants.XMLNS_ANDROID;
+import static com.android.SdkConstants.XMLNS_URI;
+
+import com.android.ide.common.api.InsertType;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.RuleAction.Toggle;
+import com.android.ide.common.rendering.LayoutLibrary;
+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.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+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.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService;
+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.PaletteMetadataDescriptor;
+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.layout.uimodel.UiViewElementNode;
+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.utils.Pair;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.IToolBarManager;
+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;
+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.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.MenuDetectEvent;
+import org.eclipse.swt.events.MenuDetectListener;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseTrackListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+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.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.ToolItem;
+import org.eclipse.wb.internal.core.editor.structure.IPage;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A palette control for the {@link GraphicalEditorPart}.
+ * <p/>
+ * The palette contains several groups, each with a UI name (e.g. layouts and views) and each
+ * with a list of element descriptors.
+ * <p/>
+ *
+ * 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 context-sensitive tools items, e.g. selection arrow tool,
+ * group selection tool, alignment, etc.
+ */
+public class PaletteControl extends Composite {
+
+ /**
+ * Wrapper to create a {@link PaletteControl}
+ */
+ static class PalettePage implements IPage {
+ private final GraphicalEditorPart mEditorPart;
+ private PaletteControl mControl;
+
+ PalettePage(GraphicalEditorPart editor) {
+ mEditorPart = editor;
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ mControl = new PaletteControl(parent, mEditorPart);
+ }
+
+ @Override
+ public Control getControl() {
+ return mControl;
+ }
+
+ @Override
+ public void dispose() {
+ mControl.dispose();
+ }
+
+ @Override
+ public void setToolBar(IToolBarManager toolBarManager) {
+ }
+
+ /**
+ * Add tool bar items to the given toolbar
+ *
+ * @param toolbar the toolbar to add items into
+ */
+ void createToolbarItems(final ToolBar toolbar) {
+ final ToolItem popupMenuItem = new ToolItem(toolbar, SWT.PUSH);
+ popupMenuItem.setToolTipText("View Menu");
+ popupMenuItem.setImage(IconFactory.getInstance().getIcon("view_menu"));
+ popupMenuItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ Rectangle bounds = popupMenuItem.getBounds();
+ // Align menu horizontally with the toolbar button and
+ // vertically with the bottom of the toolbar
+ Point point = toolbar.toDisplay(bounds.x, bounds.y + bounds.height);
+ mControl.showMenu(point.x, point.y);
+ }
+ });
+ }
+
+ @Override
+ public void setFocus() {
+ mControl.setFocus();
+ }
+ }
+
+ /**
+ * The parent grid layout that contains all the {@link Toggle} and
+ * {@link IconTextItem} widgets.
+ */
+ private GraphicalEditorPart mEditor;
+ private Color mBackground;
+ private Color mForeground;
+
+ /** 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 = null;
+ /** 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;
+ private AndroidTargetData mCurrentTargetData;
+
+ /**
+ * Create the composite.
+ * @param parent The parent composite.
+ * @param editor An editor associated with this palette.
+ */
+ public PaletteControl(Composite parent, GraphicalEditorPart editor) {
+ super(parent, SWT.NONE);
+
+ mEditor = editor;
+ }
+
+ /** 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];
+ }
+ mAlphabetical = paletteModes.contains(VALUE_ALPHABETICAL);
+ mCategories = !paletteModes.contains(VALUE_NO_CATEGORIES);
+ mAutoClose = !paletteModes.contains(VALUE_NO_AUTOCLOSE);
+ } else {
+ mPaletteMode = PaletteMode.SMALL_PREVIEW;
+ }
+ }
+
+ /**
+ * 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);
+ }
+
+ /** 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());
+ }
+
+ private void refreshPalette() {
+ IAndroidTarget oldTarget = mCurrentTarget;
+ mCurrentTarget = null;
+ mCurrentTargetData = null;
+ mCurrentTheme = null;
+ mCurrentDevice = null;
+ reloadPalette(oldTarget);
+ }
+
+ @Override
+ protected void checkSubclass() {
+ // Disable the check that prevents subclassing of SWT components
+ }
+
+ @Override
+ public void dispose() {
+ if (mBackground != null) {
+ mBackground.dispose();
+ mBackground = null;
+ }
+ if (mForeground != null) {
+ mForeground.dispose();
+ mForeground = 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;
+ }
+
+ /** Returns true if previews in the palette should be made available */
+ private boolean previewsAvailable() {
+ // Not layoutlib 5 -- we require custom background support to do
+ // a decent job with previews
+ LayoutLibrary layoutLibrary = mEditor.getLayoutLibrary();
+ return layoutLibrary != null && layoutLibrary.supports(Capability.CUSTOM_BACKGROUND_COLOR);
+ }
+
+ /**
+ * Loads or reloads the palette elements by using the layout and view descriptors from the
+ * given target data.
+ *
+ * @param target The target that has just been loaded
+ */
+ public void reloadPalette(IAndroidTarget target) {
+ ConfigurationChooser configChooser = mEditor.getConfigurationChooser();
+ String theme = configChooser.getThemeName();
+ String device = configChooser.getDeviceName();
+ if (device == null) {
+ return;
+ }
+ AndroidTargetData targetData =
+ target != null ? Sdk.getCurrent().getTargetData(target) : null;
+ if (target == mCurrentTarget && targetData == mCurrentTargetData
+ && mCurrentTheme != null && mCurrentTheme.equals(theme)
+ && mCurrentDevice != null && mCurrentDevice.equals(device)) {
+ return;
+ }
+ mCurrentTheme = theme;
+ mCurrentTarget = target;
+ mCurrentTargetData = targetData;
+ mCurrentDevice = device;
+ mPreviewIconFactory.reset();
+
+ if (targetData == null) {
+ return;
+ }
+
+ Set<String> expandedCategories = null;
+ if (mAccordion != null) {
+ expandedCategories = mAccordion.getExpandedCategories();
+ // We auto-expand all categories when showing icons-only. When returning to some
+ // other mode we don't want to retain all categories open.
+ if (expandedCategories.size() > 3) {
+ expandedCategories = null;
+ }
+ }
+
+ // Erase old content and recreate new
+ for (Control c : getChildren()) {
+ c.dispose();
+ }
+
+ if (mPaletteMode == null) {
+ loadPaletteMode();
+ assert mPaletteMode != null;
+ }
+
+ // Ensure that the palette mode is supported on this version of the layout library
+ if (!previewsAvailable()) {
+ if (mPaletteMode.isPreview()) {
+ mPaletteMode = PaletteMode.ICON_TEXT;
+ }
+ }
+
+ if (mPaletteMode.isPreview()) {
+ if (mForeground != null) {
+ mForeground.dispose();
+ mForeground = null;
+ }
+ if (mBackground != null) {
+ mBackground.dispose();
+ mBackground = null;
+ }
+ RGB background = mPreviewIconFactory.getBackgroundColor();
+ if (background != null) {
+ mBackground = new Color(getDisplay(), background);
+ }
+ RGB foreground = mPreviewIconFactory.getForegroundColor();
+ if (foreground != null) {
+ mForeground = new Color(getDisplay(), foreground);
+ }
+ }
+
+ List<String> headers = Collections.emptyList();
+ final Map<String, List<ViewElementDescriptor>> categoryToItems;
+ categoryToItems = new HashMap<String, List<ViewElementDescriptor>>();
+ headers = new ArrayList<String>();
+ 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);
+ }
+
+ headers.add("Custom & Library Views");
+
+ // Set the categories to expand the first item if
+ // (1) we don't have a previously selected category, or
+ // (2) there's just one category anyway, or
+ // (3) the set of categories have changed so our previously selected category
+ // doesn't exist anymore (can happen when you toggle "Show Categories")
+ if ((expandedCategories == null && headers.size() > 0) || headers.size() == 1 ||
+ (expandedCategories != null && expandedCategories.size() >= 1
+ && !headers.contains(
+ expandedCategories.iterator().next().replace("&&", "&")))) { //$NON-NLS-1$ //$NON-NLS-2$
+ // Expand the first category if we don't have a previous selection (e.g. refresh)
+ expandedCategories = Collections.singleton(headers.get(0));
+ }
+
+ boolean wrap = mPaletteMode.getWrap();
+
+ // Pack icon-only view vertically; others stretch to fill palette region
+ boolean fillVertical = mPaletteMode != PaletteMode.ICON_ONLY;
+
+ mAccordion = new AccordionControl(this, SWT.NONE, headers, fillVertical, wrap,
+ expandedCategories) {
+ @Override
+ protected Composite createChildContainer(Composite parent, Object header, int style) {
+ assert categoryToItems != null;
+ List<ViewElementDescriptor> list = categoryToItems.get(header);
+ final Composite composite;
+ if (list == null) {
+ assert header.equals("Custom & Library Views");
+
+ Composite wrapper = new Composite(parent, SWT.NONE);
+ GridLayout gridLayout = new GridLayout(1, false);
+ gridLayout.marginWidth = gridLayout.marginHeight = 0;
+ gridLayout.horizontalSpacing = gridLayout.verticalSpacing = 0;
+ gridLayout.marginBottom = 3;
+ wrapper.setLayout(gridLayout);
+ if (mPaletteMode.isPreview() && mBackground != null) {
+ wrapper.setBackground(mBackground);
+ }
+ composite = super.createChildContainer(wrapper, header, style);
+ if (mPaletteMode.isPreview() && mBackground != null) {
+ composite.setBackground(mBackground);
+ }
+ composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
+
+ Button refreshButton = new Button(wrapper, SWT.PUSH | SWT.FLAT);
+ refreshButton.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER,
+ false, false, 1, 1));
+ refreshButton.setText("Refresh");
+ refreshButton.setImage(IconFactory.getInstance().getIcon("refresh")); //$NON-NLS-1$
+ refreshButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ CustomViewFinder finder = CustomViewFinder.get(mEditor.getProject());
+ finder.refresh(new ViewFinderListener(composite));
+ }
+ });
+
+ wrapper.layout(true);
+ } else {
+ composite = super.createChildContainer(parent, header, style);
+ if (mPaletteMode.isPreview() && mBackground != null) {
+ composite.setBackground(mBackground);
+ }
+ }
+ addMenu(composite);
+ return composite;
+ }
+ @Override
+ protected void createChildren(Composite parent, Object header) {
+ assert categoryToItems != null;
+ List<ViewElementDescriptor> list = categoryToItems.get(header);
+ if (list == null) {
+ assert header.equals("Custom & Library Views");
+ addCustomItems(parent);
+ return;
+ } else {
+ for (ViewElementDescriptor desc : list) {
+ createItem(parent, desc);
+ }
+ }
+ }
+ };
+ addMenu(mAccordion);
+ for (CLabel headerLabel : mAccordion.getHeaderLabels()) {
+ addMenu(headerLabel);
+ }
+ setLayout(new FillLayout());
+
+ // 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());
+ }
+
+ layout(true);
+ }
+
+ protected void addCustomItems(final Composite parent) {
+ final CustomViewFinder finder = CustomViewFinder.get(mEditor.getProject());
+ Collection<String> allViews = finder.getAllViews();
+ if (allViews == null) { // Not yet initialized: trigger an async refresh
+ finder.refresh(new ViewFinderListener(parent));
+ return;
+ }
+
+ // Remove previous content
+ for (Control c : parent.getChildren()) {
+ c.dispose();
+ }
+
+ // Add new views
+ for (final String fqcn : allViews) {
+ CustomViewDescriptorService service = CustomViewDescriptorService.getInstance();
+ ViewElementDescriptor desc = service.getDescriptor(mEditor.getProject(), fqcn);
+ if (desc == null) {
+ // The descriptor lookup performs validation steps of the class, and may
+ // in some cases determine that this is not a view and will return null;
+ // guard against that.
+ continue;
+ }
+
+ Control item = createItem(parent, desc);
+
+ // Add control-click listener on custom view items to you can warp to
+ // (and double click listener too -- the more discoverable, the better.)
+ if (item instanceof IconTextItem) {
+ IconTextItem it = (IconTextItem) item;
+ it.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseDoubleClick(MouseEvent e) {
+ AdtPlugin.openJavaClass(mEditor.getProject(), fqcn);
+ }
+
+ @Override
+ public void mouseDown(MouseEvent e) {
+ if ((e.stateMask & SWT.MOD1) != 0) {
+ AdtPlugin.openJavaClass(mEditor.getProject(), fqcn);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ /* package */ GraphicalEditorPart getEditor() {
+ return mEditor;
+ }
+
+ 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;
+ }
+ ConfigurationChooser chooser = mEditor.getConfigurationChooser();
+ int dpi = chooser.getConfiguration().getDensity().getDpiValue();
+ while (dpi > 160) {
+ scale = scale / 2;
+ dpi = dpi / 2;
+ }
+ imageControl.setScale(scale);
+ }
+ imageControl.setHoverColor(getDisplay().getSystemColor(SWT.COLOR_WHITE));
+ if (mBackground != null) {
+ 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);
+ if (mForeground != null) {
+ item.setForeground(mForeground);
+ item.setBackground(mBackground);
+ }
+ }
+ break;
+ }
+ case ICON_TEXT: {
+ item = new IconTextItem(parent, desc);
+ break;
+ }
+ case ICON_ONLY: {
+ item = new ImageControl(parent, SWT.None, desc.getGenericIcon());
+ item.setToolTipText(desc.getUiName());
+ break;
+ }
+ default:
+ throw new IllegalArgumentException("Not yet implemented");
+ }
+
+ final DragSource source = new DragSource(item, DND.DROP_COPY);
+ source.setTransfer(new Transfer[] { SimpleXmlTransfer.getInstance() });
+ source.addDragListener(new DescDragSourceListener(desc));
+ item.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ source.dispose();
+ }
+ });
+ addMenu(item);
+
+ return item;
+ }
+
+ /**
+ * An Item widget represents one {@link ElementDescriptor} that can be dropped on the
+ * GLE2 canvas using drag'n'drop.
+ */
+ private static class IconTextItem extends CLabel implements MouseTrackListener {
+
+ private boolean mMouseIn;
+
+ public IconTextItem(Composite parent, ViewElementDescriptor desc) {
+ super(parent, SWT.NONE);
+ mMouseIn = false;
+
+ setText(desc.getUiName());
+ setImage(desc.getGenericIcon());
+ setToolTipText(desc.getTooltip());
+ addMouseTrackListener(this);
+ }
+
+ @Override
+ public int getStyle() {
+ int style = super.getStyle();
+ if (mMouseIn) {
+ style |= SWT.SHADOW_IN;
+ }
+ return style;
+ }
+
+ @Override
+ public void mouseEnter(MouseEvent e) {
+ if (!mMouseIn) {
+ mMouseIn = true;
+ redraw();
+ }
+ }
+
+ @Override
+ public void mouseExit(MouseEvent e) {
+ if (mMouseIn) {
+ mMouseIn = false;
+ redraw();
+ }
+ }
+
+ @Override
+ public void mouseHover(MouseEvent e) {
+ // pass
+ }
+ }
+
+ /**
+ * A {@link DragSourceListener} that deals with drag'n'drop of
+ * {@link ElementDescriptor}s.
+ */
+ private class DescDragSourceListener implements DragSourceListener {
+ private final ViewElementDescriptor mDesc;
+ private SimpleElement[] mElements;
+
+ public DescDragSourceListener(ViewElementDescriptor desc) {
+ mDesc = desc;
+ }
+
+ @Override
+ public void dragStart(DragSourceEvent e) {
+ // See if we can find out the bounds of this element from a preview image.
+ // Preview images are created before the drag source listener is notified
+ // of the started drag.
+ Rect bounds = null;
+ Rect dragBounds = null;
+
+ createDragImage(e);
+ if (mImage != null && !mIsPlaceholder) {
+ int width = mImageLayoutBounds.width;
+ int height = mImageLayoutBounds.height;
+ assert mImageLayoutBounds.x == 0;
+ assert mImageLayoutBounds.y == 0;
+ bounds = new Rect(0, 0, width, height);
+ double scale = mEditor.getCanvasControl().getScale();
+ int scaledWidth = (int) (scale * width);
+ int scaledHeight = (int) (scale * height);
+ int x = -scaledWidth / 2;
+ int y = -scaledHeight / 2;
+ dragBounds = new Rect(x, y, scaledWidth, scaledHeight);
+ }
+
+ SimpleElement se = new SimpleElement(
+ SimpleXmlTransfer.getFqcn(mDesc),
+ null /* parentFqcn */,
+ bounds /* bounds */,
+ null /* parentBounds */);
+ if (mDesc instanceof PaletteMetadataDescriptor) {
+ PaletteMetadataDescriptor pm = (PaletteMetadataDescriptor) mDesc;
+ pm.initializeNew(se);
+ }
+ mElements = new SimpleElement[] { se };
+
+ // Register this as the current dragged data
+ GlobalCanvasDragInfo dragInfo = GlobalCanvasDragInfo.getInstance();
+ dragInfo.startDrag(
+ mElements,
+ null /* selection */,
+ null /* canvas */,
+ null /* removeSource */);
+ dragInfo.setDragBounds(dragBounds);
+ dragInfo.setDragBaseline(mBaseline);
+
+
+ e.doit = true;
+ }
+
+ @Override
+ public void dragSetData(DragSourceEvent e) {
+ // Provide the data for the drop when requested by the other side.
+ if (SimpleXmlTransfer.getInstance().isSupportedType(e.dataType)) {
+ e.data = mElements;
+ }
+ }
+
+ @Override
+ public void dragFinished(DragSourceEvent e) {
+ // Unregister the dragged data.
+ GlobalCanvasDragInfo.getInstance().stopDrag();
+ mElements = null;
+ if (mImage != null) {
+ mImage.dispose();
+ mImage = null;
+ }
+ }
+
+ // TODO: Figure out the right dimensions to use for rendering.
+ // We WILL crop this after rendering, but for performance reasons it would be good
+ // not to make it much larger than necessary since to crop this we rely on
+ // actually scanning pixels.
+
+ /**
+ * Width of the rendered preview image (before it is cropped), although the actual
+ * width may be smaller (since we also take the device screen's size into account)
+ */
+ private static final int MAX_RENDER_HEIGHT = 400;
+
+ /**
+ * Height of the rendered preview image (before it is cropped), although the
+ * actual width may be smaller (since we also take the device screen's size into
+ * account)
+ */
+ private static final int MAX_RENDER_WIDTH = 500;
+
+ /** Amount of alpha to multiply into the image (divided by 256) */
+ private static final int IMG_ALPHA = 128;
+
+ /** The image shown during the drag */
+ private Image mImage;
+ /** The non-effect bounds of the drag image */
+ private Rectangle mImageLayoutBounds;
+ private int mBaseline = -1;
+
+ /**
+ * If true, the image is a preview of the view, and if not it is a "fallback"
+ * image of some sort, such as a rendering of the palette item itself
+ */
+ private boolean mIsPlaceholder;
+
+ private void createDragImage(DragSourceEvent event) {
+ mBaseline = -1;
+ Pair<Image, Rectangle> preview = renderPreview();
+ if (preview != null) {
+ mImage = preview.getFirst();
+ mImageLayoutBounds = preview.getSecond();
+ } else {
+ mImage = null;
+ mImageLayoutBounds = null;
+ }
+
+ mIsPlaceholder = mImage == null;
+ if (mIsPlaceholder) {
+ // Couldn't render preview (or the preview is a blank image, such as for
+ // example the preview of an empty layout), so instead create a placeholder
+ // image
+ // Render the palette item itself as an image
+ Control control = ((DragSource) event.widget).getControl();
+ GC gc = new GC(control);
+ Point size = control.getSize();
+ Display display = getDisplay();
+ final Image image = new Image(display, size.x, size.y);
+ gc.copyArea(image, 0, 0);
+ gc.dispose();
+
+ BufferedImage awtImage = SwtUtils.convertToAwt(image);
+ if (awtImage != null) {
+ awtImage = ImageUtils.createDropShadow(awtImage, 3 /* shadowSize */,
+ 0.7f /* shadowAlpha */, 0x000000 /* shadowRgb */);
+ mImage = SwtUtils.convertToSwt(display, awtImage, true, IMG_ALPHA);
+ } else {
+ ImageData data = image.getImageData();
+ data.alpha = IMG_ALPHA;
+
+ // Changing the ImageData -after- constructing an image on it
+ // has no effect, so we have to construct a new image. Luckily these
+ // are tiny images.
+ mImage = new Image(display, data);
+ }
+ image.dispose();
+ }
+
+ event.image = mImage;
+
+ if (!mIsPlaceholder) {
+ // Shift the drag feedback image up such that it's centered under the
+ // mouse pointer
+ double scale = mEditor.getCanvasControl().getScale();
+ event.offsetX = (int) (scale * mImageLayoutBounds.width / 2);
+ event.offsetY = (int) (scale * mImageLayoutBounds.height / 2);
+ }
+ }
+
+ /**
+ * Performs the actual rendering of the descriptor into an image and returns the
+ * image as well as the layout bounds of the image (not including drop shadow etc)
+ */
+ private Pair<Image, Rectangle> renderPreview() {
+ ViewMetadataRepository repository = ViewMetadataRepository.get();
+ RenderMode renderMode = repository.getRenderMode(mDesc.getFullClassName());
+ if (renderMode == RenderMode.SKIP) {
+ return null;
+ }
+
+ // Create blank XML document
+ Document document = DomUtilities.createEmptyDocument();
+
+ // Insert our target view's XML into it as a node
+ GraphicalEditorPart editor = getEditor();
+ LayoutEditorDelegate layoutEditorDelegate = editor.getEditorDelegate();
+
+ String viewName = mDesc.getXmlLocalName();
+ Element element = document.createElement(viewName);
+
+ // Set up a proper name space
+ Attr attr = document.createAttributeNS(XMLNS_URI, XMLNS_ANDROID);
+ attr.setValue(ANDROID_URI);
+ element.getAttributes().setNamedItemNS(attr);
+
+ element.setAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH, VALUE_WRAP_CONTENT);
+ element.setAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT);
+
+ // This doesn't apply to all, but doesn't seem to cause harm and makes for a
+ // better experience with text-oriented views like buttons and texts
+ element.setAttributeNS(ANDROID_URI, ATTR_TEXT,
+ DescriptorsUtils.getBasename(mDesc.getUiName()));
+
+ // Is this a palette variation?
+ if (mDesc instanceof PaletteMetadataDescriptor) {
+ PaletteMetadataDescriptor pm = (PaletteMetadataDescriptor) mDesc;
+ pm.initializeNew(element);
+ }
+
+ document.appendChild(element);
+
+ // Construct UI model from XML
+ AndroidTargetData data = layoutEditorDelegate.getEditor().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(layoutEditorDelegate.getEditor());
+ model.setUnknownDescriptorProvider(editor.getModel().getUnknownDescriptorProvider());
+ model.loadFromXmlNode(document);
+
+ // Call the create-hooks such that we for example insert mandatory
+ // children into views like the DialerFilter, apply image source attributes
+ // to ImageButtons, etc.
+ LayoutCanvas canvas = editor.getCanvasControl();
+ NodeFactory nodeFactory = canvas.getNodeFactory();
+ UiElementNode parent = model.getUiRoot();
+ UiElementNode child = parent.getUiChildren().get(0);
+ if (child instanceof UiViewElementNode) {
+ UiViewElementNode childUiNode = (UiViewElementNode) child;
+ NodeProxy childNode = nodeFactory.create(childUiNode);
+
+ // Applying create hooks as part of palette render should
+ // not trigger model updates
+ layoutEditorDelegate.getEditor().setIgnoreXmlUpdate(true);
+ try {
+ canvas.getRulesEngine().callCreateHooks(layoutEditorDelegate.getEditor(),
+ null, childNode, InsertType.CREATE_PREVIEW);
+ childNode.applyPendingChanges();
+ } catch (Throwable t) {
+ AdtPlugin.log(t, "Failed calling creation hooks for widget %1$s", viewName);
+ } finally {
+ layoutEditorDelegate.getEditor().setIgnoreXmlUpdate(false);
+ }
+ }
+
+ Integer overrideBgColor = null;
+ boolean hasTransparency = false;
+ LayoutLibrary layoutLibrary = editor.getLayoutLibrary();
+ if (layoutLibrary != null &&
+ layoutLibrary.supports(Capability.CUSTOM_BACKGROUND_COLOR)) {
+ // It doesn't matter what the background color is as long as the alpha
+ // is 0 (fully transparent). We're using red to make it more obvious if
+ // for some reason the background is painted when it shouldn't be.
+ overrideBgColor = new Integer(0x00FF0000);
+ }
+
+ RenderSession session = null;
+ try {
+ // Use at most the size of the screen for the preview render.
+ // This is important since when we fill the size of certain views (like
+ // a SeekBar), we want it to at most be the width of the screen, and for small
+ // screens the RENDER_WIDTH was wider.
+ LayoutLog silentLogger = new LayoutLog();
+
+ session = RenderService.create(editor)
+ .setModel(model)
+ .setMaxRenderSize(MAX_RENDER_WIDTH, MAX_RENDER_HEIGHT)
+ .setLog(silentLogger)
+ .setOverrideBgColor(overrideBgColor)
+ .setDecorations(false)
+ .createRenderSession();
+ } catch (Throwable t) {
+ // Previews can fail for a variety of reasons -- let's not bug
+ // the user with it
+ return null;
+ }
+
+ if (session != null) {
+ if (session.getResult().isSuccess()) {
+ BufferedImage image = session.getImage();
+ if (image != null) {
+ BufferedImage cropped;
+ Rect initialCrop = null;
+ ViewInfo viewInfo = null;
+
+ List<ViewInfo> viewInfoList = session.getRootViews();
+
+ if (viewInfoList != null && viewInfoList.size() > 0) {
+ viewInfo = viewInfoList.get(0);
+ mBaseline = viewInfo.getBaseLine();
+ }
+
+ if (viewInfo != null) {
+ int x1 = viewInfo.getLeft();
+ int x2 = viewInfo.getRight();
+ int y2 = viewInfo.getBottom();
+ int y1 = viewInfo.getTop();
+ initialCrop = new Rect(x1, y1, x2 - x1, y2 - y1);
+ }
+
+ if (hasTransparency) {
+ cropped = ImageUtils.cropBlank(image, initialCrop);
+ } else {
+ // Find out what the "background" color is such that we can properly
+ // crop it out of the image. To do this we pick out a pixel in the
+ // bottom right unpainted area. Rather than pick the one in the far
+ // bottom corner, we pick one as close to the bounds of the view as
+ // possible (but still outside of the bounds), such that we can
+ // deal with themes like the dialog theme.
+ int edgeX = image.getWidth() -1;
+ int edgeY = image.getHeight() -1;
+ if (viewInfo != null) {
+ if (viewInfo.getRight() < image.getWidth()-1) {
+ edgeX = viewInfo.getRight()+1;
+ }
+ if (viewInfo.getBottom() < image.getHeight()-1) {
+ edgeY = viewInfo.getBottom()+1;
+ }
+ }
+ int edgeColor = image.getRGB(edgeX, edgeY);
+ cropped = ImageUtils.cropColor(image, edgeColor, initialCrop);
+ }
+
+ if (cropped != null) {
+ int width = initialCrop != null ? initialCrop.w : cropped.getWidth();
+ int height = initialCrop != null ? initialCrop.h : cropped.getHeight();
+ boolean needsContrast = hasTransparency
+ && !ImageUtils.containsDarkPixels(cropped);
+ cropped = ImageUtils.createDropShadow(cropped,
+ hasTransparency ? 3 : 5 /* shadowSize */,
+ !hasTransparency ? 0.6f : needsContrast ? 0.8f : 0.7f/*alpha*/,
+ 0x000000 /* shadowRgb */);
+
+ double scale = canvas.getScale();
+ if (scale != 1L) {
+ cropped = ImageUtils.scale(cropped, scale, scale);
+ }
+
+ Display display = getDisplay();
+ int alpha = (!hasTransparency || !needsContrast) ? IMG_ALPHA : -1;
+ Image swtImage = SwtUtils.convertToSwt(display, cropped, true, alpha);
+ Rectangle imageBounds = new Rectangle(0, 0, width, height);
+ return Pair.of(swtImage, imageBounds);
+ }
+ }
+ }
+
+ session.dispose();
+ }
+
+ return null;
+ }
+
+ /**
+ * Utility method to print out the contents of the given XML document. This is
+ * really useful when working on the preview code above. I'm including all the
+ * code inside a constant false, which means the compiler will omit all the code,
+ * but I'd like to leave it in the code base and by doing it this way rather than
+ * as commented out code the code won't be accidentally broken.
+ */
+ @SuppressWarnings("all")
+ private void dumpDocument(Document document) {
+ // Diagnostics: print out the XML that we're about to render
+ if (false) { // Will be omitted by the compiler
+ org.apache.xml.serialize.OutputFormat outputFormat =
+ new org.apache.xml.serialize.OutputFormat(
+ "XML", "ISO-8859-1", true); //$NON-NLS-1$ //$NON-NLS-2$
+ outputFormat.setIndent(2);
+ outputFormat.setLineWidth(100);
+ outputFormat.setIndenting(true);
+ outputFormat.setOmitXMLDeclaration(true);
+ outputFormat.setOmitDocumentType(true);
+ StringWriter stringWriter = new StringWriter();
+ // Using FQN here to avoid having an import above, which will result
+ // in a deprecation warning, and there isn't a way to annotate a single
+ // import element with a SuppressWarnings.
+ org.apache.xml.serialize.XMLSerializer serializer =
+ new org.apache.xml.serialize.XMLSerializer(stringWriter, outputFormat);
+ serializer.setNamespaces(true);
+ try {
+ serializer.serialize(document.getDocumentElement());
+ System.out.println(stringWriter.toString());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ /** 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;
+ final static int REFRESH = 4;
+ final static int RESET = 5;
+
+ ToggleViewOptionAction(String title, int action, boolean checked) {
+ super(title, (action == REFRESH || action == RESET) ? IAction.AS_PUSH_BUTTON
+ : IAction.AS_CHECK_BOX);
+ mAction = action;
+ if (checked) {
+ 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;
+ case REFRESH:
+ mPreviewIconFactory.refresh();
+ refreshPalette();
+ break;
+ case RESET:
+ mAlphabetical = false;
+ mCategories = true;
+ mAutoClose = true;
+ mPaletteMode = PaletteMode.SMALL_PREVIEW;
+ refreshPalette();
+ break;
+ }
+ savePaletteMode();
+ }
+ }
+
+ private void addMenu(Control control) {
+ control.addMenuDetectListener(new MenuDetectListener() {
+ @Override
+ public void menuDetected(MenuDetectEvent e) {
+ showMenu(e.x, e.y);
+ }
+ });
+ }
+
+ private void showMenu(int x, int y) {
+ MenuManager manager = new MenuManager() {
+ @Override
+ public boolean isDynamic() {
+ return true;
+ }
+ };
+ boolean previews = previewsAvailable();
+ for (PaletteMode mode : PaletteMode.values()) {
+ if (mode.isPreview() && !previews) {
+ continue;
+ }
+ manager.add(new PaletteModeAction(mode));
+ }
+ if (mPaletteMode.isPreview()) {
+ manager.add(new Separator());
+ manager.add(new ToggleViewOptionAction("Refresh Previews",
+ ToggleViewOptionAction.REFRESH,
+ false));
+ }
+ 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));
+ manager.add(new Separator());
+ manager.add(new ToggleViewOptionAction("Reset",
+ ToggleViewOptionAction.RESET,
+ false));
+
+ Menu menu = manager.createContextMenu(PaletteControl.this);
+ menu.setLocation(x, y);
+ menu.setVisible(true);
+ }
+
+ private final class ViewFinderListener implements CustomViewFinder.Listener {
+ private final Composite mParent;
+
+ private ViewFinderListener(Composite parent) {
+ mParent = parent;
+ }
+
+ @Override
+ public void viewsUpdated(Collection<String> customViews,
+ Collection<String> thirdPartyViews) {
+ addCustomItems(mParent);
+ mParent.layout(true);
+ }
+ }
+}