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