| /* |
| * 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); |
| } |
| } |
| } |