1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ide.eclipse.adt.internal.editors.layout.gle2; 18 19 import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; 20 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT; 21 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH; 22 import static com.android.ide.common.layout.LayoutConstants.ATTR_TEXT; 23 import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; 24 25 import com.android.ide.common.api.InsertType; 26 import com.android.ide.common.api.Rect; 27 import com.android.ide.common.api.RuleAction.Toggle; 28 import com.android.ide.common.rendering.LayoutLibrary; 29 import com.android.ide.common.rendering.api.Capability; 30 import com.android.ide.common.rendering.api.LayoutLog; 31 import com.android.ide.common.rendering.api.RenderSession; 32 import com.android.ide.common.rendering.api.ViewInfo; 33 import com.android.ide.eclipse.adt.AdtPlugin; 34 import com.android.ide.eclipse.adt.internal.editors.IconFactory; 35 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; 36 import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor; 37 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; 38 import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor; 39 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; 40 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite; 41 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService; 42 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; 43 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory; 44 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; 45 import com.android.ide.eclipse.adt.internal.editors.layout.gre.PaletteMetadataDescriptor; 46 import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository; 47 import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository.RenderMode; 48 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; 49 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; 50 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; 51 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; 52 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 53 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 54 import com.android.sdklib.IAndroidTarget; 55 import com.android.util.Pair; 56 57 import org.eclipse.jface.action.Action; 58 import org.eclipse.jface.action.IAction; 59 import org.eclipse.jface.action.IToolBarManager; 60 import org.eclipse.jface.action.MenuManager; 61 import org.eclipse.jface.action.Separator; 62 import org.eclipse.jface.resource.ImageDescriptor; 63 import org.eclipse.swt.SWT; 64 import org.eclipse.swt.custom.CLabel; 65 import org.eclipse.swt.dnd.DND; 66 import org.eclipse.swt.dnd.DragSource; 67 import org.eclipse.swt.dnd.DragSourceEvent; 68 import org.eclipse.swt.dnd.DragSourceListener; 69 import org.eclipse.swt.dnd.Transfer; 70 import org.eclipse.swt.events.DisposeEvent; 71 import org.eclipse.swt.events.DisposeListener; 72 import org.eclipse.swt.events.MenuDetectEvent; 73 import org.eclipse.swt.events.MenuDetectListener; 74 import org.eclipse.swt.events.MouseAdapter; 75 import org.eclipse.swt.events.MouseEvent; 76 import org.eclipse.swt.events.MouseTrackListener; 77 import org.eclipse.swt.events.SelectionAdapter; 78 import org.eclipse.swt.events.SelectionEvent; 79 import org.eclipse.swt.graphics.Color; 80 import org.eclipse.swt.graphics.GC; 81 import org.eclipse.swt.graphics.Image; 82 import org.eclipse.swt.graphics.ImageData; 83 import org.eclipse.swt.graphics.Point; 84 import org.eclipse.swt.graphics.RGB; 85 import org.eclipse.swt.graphics.Rectangle; 86 import org.eclipse.swt.layout.FillLayout; 87 import org.eclipse.swt.layout.GridData; 88 import org.eclipse.swt.layout.GridLayout; 89 import org.eclipse.swt.widgets.Button; 90 import org.eclipse.swt.widgets.Composite; 91 import org.eclipse.swt.widgets.Control; 92 import org.eclipse.swt.widgets.Display; 93 import org.eclipse.swt.widgets.Menu; 94 import org.eclipse.swt.widgets.ToolBar; 95 import org.eclipse.swt.widgets.ToolItem; 96 import org.eclipse.wb.internal.core.editor.structure.IPage; 97 import org.w3c.dom.Attr; 98 import org.w3c.dom.Document; 99 import org.w3c.dom.Element; 100 101 import java.awt.image.BufferedImage; 102 import java.io.IOException; 103 import java.io.StringWriter; 104 import java.util.ArrayList; 105 import java.util.Collection; 106 import java.util.Collections; 107 import java.util.HashMap; 108 import java.util.List; 109 import java.util.Map; 110 import java.util.Set; 111 112 /** 113 * A palette control for the {@link GraphicalEditorPart}. 114 * <p/> 115 * The palette contains several groups, each with a UI name (e.g. layouts and views) and each 116 * with a list of element descriptors. 117 * <p/> 118 * 119 * TODO list: 120 * - The available items should depend on the actual GLE2 Canvas selection. Selected android 121 * views should force filtering on what they accept can be dropped on them (e.g. TabHost, 122 * TableLayout). Should enable/disable them, not hide them, to avoid shuffling around. 123 * - Optional: a text filter 124 * - Optional: have context-sensitive tools items, e.g. selection arrow tool, 125 * group selection tool, alignment, etc. 126 */ 127 public class PaletteControl extends Composite { 128 129 /** 130 * Wrapper to create a {@link PaletteControl} 131 */ 132 static class PalettePage implements IPage { 133 private final GraphicalEditorPart mEditorPart; 134 private PaletteControl mControl; 135 136 PalettePage(GraphicalEditorPart editor) { 137 mEditorPart = editor; 138 } 139 140 @Override 141 public void createControl(Composite parent) { 142 mControl = new PaletteControl(parent, mEditorPart); 143 } 144 145 @Override 146 public Control getControl() { 147 return mControl; 148 } 149 150 @Override 151 public void dispose() { 152 mControl.dispose(); 153 } 154 155 @Override 156 public void setToolBar(IToolBarManager toolBarManager) { 157 } 158 159 /** 160 * Add tool bar items to the given toolbar 161 * 162 * @param toolbar the toolbar to add items into 163 */ 164 void createToolbarItems(final ToolBar toolbar) { 165 final ToolItem popupMenuItem = new ToolItem(toolbar, SWT.PUSH); 166 popupMenuItem.setToolTipText("View Menu"); 167 popupMenuItem.setImage(IconFactory.getInstance().getIcon("view_menu")); 168 popupMenuItem.addSelectionListener(new SelectionAdapter() { 169 @Override 170 public void widgetSelected(SelectionEvent e) { 171 Rectangle bounds = popupMenuItem.getBounds(); 172 // Align menu horizontally with the toolbar button and 173 // vertically with the bottom of the toolbar 174 Point point = toolbar.toDisplay(bounds.x, bounds.y + bounds.height); 175 mControl.showMenu(point.x, point.y); 176 } 177 }); 178 } 179 180 @Override 181 public void setFocus() { 182 mControl.setFocus(); 183 } 184 } 185 186 /** 187 * The parent grid layout that contains all the {@link Toggle} and 188 * {@link IconTextItem} widgets. 189 */ 190 private GraphicalEditorPart mEditor; 191 private Color mBackground; 192 private Color mForeground; 193 194 /** The palette modes control various ways to visualize and lay out the views */ 195 private static enum PaletteMode { 196 /** Show rendered previews of the views */ 197 PREVIEW("Show Previews", true), 198 /** Show rendered previews of the views, scaled down to 75% */ 199 SMALL_PREVIEW("Show Small Previews", true), 200 /** Show rendered previews of the views, scaled down to 50% */ 201 TINY_PREVIEW("Show Tiny Previews", true), 202 /** Show an icon + text label */ 203 ICON_TEXT("Show Icon and Text", false), 204 /** Show only icons, packed multiple per row */ 205 ICON_ONLY("Show Only Icons", true); 206 207 PaletteMode(String actionLabel, boolean wrap) { 208 mActionLabel = actionLabel; 209 mWrap = wrap; 210 } 211 212 public String getActionLabel() { 213 return mActionLabel; 214 } 215 216 public boolean getWrap() { 217 return mWrap; 218 } 219 220 public boolean isPreview() { 221 return this == PREVIEW || this == SMALL_PREVIEW || this == TINY_PREVIEW; 222 } 223 224 public boolean isScaledPreview() { 225 return this == SMALL_PREVIEW || this == TINY_PREVIEW; 226 } 227 228 private final String mActionLabel; 229 private final boolean mWrap; 230 }; 231 232 /** Token used in preference string to record alphabetical sorting */ 233 private static final String VALUE_ALPHABETICAL = "alpha"; //$NON-NLS-1$ 234 /** Token used in preference string to record categories being turned off */ 235 private static final String VALUE_NO_CATEGORIES = "nocat"; //$NON-NLS-1$ 236 /** Token used in preference string to record auto close being turned off */ 237 private static final String VALUE_NO_AUTOCLOSE = "noauto"; //$NON-NLS-1$ 238 239 private final PreviewIconFactory mPreviewIconFactory = new PreviewIconFactory(this); 240 private PaletteMode mPaletteMode = null; 241 /** Use alphabetical sorting instead of natural order? */ 242 private boolean mAlphabetical; 243 /** Use categories instead of a single large list of views? */ 244 private boolean mCategories = true; 245 /** Auto-close the previous category when new categories are opened */ 246 private boolean mAutoClose = true; 247 private AccordionControl mAccordion; 248 private String mCurrentTheme; 249 private String mCurrentDevice; 250 private IAndroidTarget mCurrentTarget; 251 private AndroidTargetData mCurrentTargetData; 252 253 /** 254 * Create the composite. 255 * @param parent The parent composite. 256 * @param editor An editor associated with this palette. 257 */ 258 public PaletteControl(Composite parent, GraphicalEditorPart editor) { 259 super(parent, SWT.NONE); 260 261 mEditor = editor; 262 } 263 264 /** Reads UI mode from persistent store to preserve palette mode across IDE sessions */ 265 private void loadPaletteMode() { 266 String paletteModes = AdtPrefs.getPrefs().getPaletteModes(); 267 if (paletteModes.length() > 0) { 268 String[] tokens = paletteModes.split(","); //$NON-NLS-1$ 269 try { 270 mPaletteMode = PaletteMode.valueOf(tokens[0]); 271 } catch (Throwable t) { 272 mPaletteMode = PaletteMode.values()[0]; 273 } 274 mAlphabetical = paletteModes.contains(VALUE_ALPHABETICAL); 275 mCategories = !paletteModes.contains(VALUE_NO_CATEGORIES); 276 mAutoClose = !paletteModes.contains(VALUE_NO_AUTOCLOSE); 277 } else { 278 mPaletteMode = PaletteMode.SMALL_PREVIEW; 279 } 280 } 281 282 /** 283 * Returns the most recently stored version of auto-close-mode; this is the last 284 * user-initiated setting of the auto-close mode (we programmatically switch modes when 285 * you enter icons-only mode, and set it back to this when going to any other mode) 286 */ 287 private boolean getSavedAutoCloseMode() { 288 return !AdtPrefs.getPrefs().getPaletteModes().contains(VALUE_NO_AUTOCLOSE); 289 } 290 291 /** Saves UI mode to persistent store to preserve palette mode across IDE sessions */ 292 private void savePaletteMode() { 293 StringBuilder sb = new StringBuilder(); 294 sb.append(mPaletteMode); 295 if (mAlphabetical) { 296 sb.append(',').append(VALUE_ALPHABETICAL); 297 } 298 if (!mCategories) { 299 sb.append(',').append(VALUE_NO_CATEGORIES); 300 } 301 if (!mAutoClose) { 302 sb.append(',').append(VALUE_NO_AUTOCLOSE); 303 } 304 AdtPrefs.getPrefs().setPaletteModes(sb.toString()); 305 } 306 307 private void refreshPalette() { 308 IAndroidTarget oldTarget = mCurrentTarget; 309 mCurrentTarget = null; 310 mCurrentTargetData = null; 311 mCurrentTheme = null; 312 mCurrentDevice = null; 313 reloadPalette(oldTarget); 314 } 315 316 @Override 317 protected void checkSubclass() { 318 // Disable the check that prevents subclassing of SWT components 319 } 320 321 @Override 322 public void dispose() { 323 if (mBackground != null) { 324 mBackground.dispose(); 325 mBackground = null; 326 } 327 if (mForeground != null) { 328 mForeground.dispose(); 329 mForeground = null; 330 } 331 332 super.dispose(); 333 } 334 335 /** 336 * Returns the currently displayed target 337 * 338 * @return the current target, or null 339 */ 340 public IAndroidTarget getCurrentTarget() { 341 return mCurrentTarget; 342 } 343 344 /** 345 * Returns the currently displayed theme (in palette modes that support previewing) 346 * 347 * @return the current theme, or null 348 */ 349 public String getCurrentTheme() { 350 return mCurrentTheme; 351 } 352 353 /** 354 * Returns the currently displayed device (in palette modes that support previewing) 355 * 356 * @return the current device, or null 357 */ 358 public String getCurrentDevice() { 359 return mCurrentDevice; 360 } 361 362 /** Returns true if previews in the palette should be made available */ 363 private boolean previewsAvailable() { 364 // Not layoutlib 5 -- we require custom background support to do 365 // a decent job with previews 366 LayoutLibrary layoutLibrary = mEditor.getLayoutLibrary(); 367 return layoutLibrary != null && layoutLibrary.supports(Capability.CUSTOM_BACKGROUND_COLOR); 368 } 369 370 /** 371 * Loads or reloads the palette elements by using the layout and view descriptors from the 372 * given target data. 373 * 374 * @param target The target that has just been loaded 375 */ 376 public void reloadPalette(IAndroidTarget target) { 377 ConfigurationComposite configuration = mEditor.getConfigurationComposite(); 378 String theme = configuration.getTheme(); 379 String device = configuration.getDevice(); 380 AndroidTargetData targetData = 381 target != null ? Sdk.getCurrent().getTargetData(target) : null; 382 if (target == mCurrentTarget && targetData == mCurrentTargetData 383 && mCurrentTheme != null && mCurrentTheme.equals(theme) 384 && mCurrentDevice != null && mCurrentDevice.equals(device)) { 385 return; 386 } 387 mCurrentTheme = theme; 388 mCurrentTarget = target; 389 mCurrentTargetData = targetData; 390 mCurrentDevice = device; 391 mPreviewIconFactory.reset(); 392 393 if (targetData == null) { 394 return; 395 } 396 397 Set<String> expandedCategories = null; 398 if (mAccordion != null) { 399 expandedCategories = mAccordion.getExpandedCategories(); 400 // We auto-expand all categories when showing icons-only. When returning to some 401 // other mode we don't want to retain all categories open. 402 if (expandedCategories.size() > 3) { 403 expandedCategories = null; 404 } 405 } 406 407 // Erase old content and recreate new 408 for (Control c : getChildren()) { 409 c.dispose(); 410 } 411 412 if (mPaletteMode == null) { 413 loadPaletteMode(); 414 assert mPaletteMode != null; 415 } 416 417 // Ensure that the palette mode is supported on this version of the layout library 418 if (!previewsAvailable()) { 419 if (mPaletteMode.isPreview()) { 420 mPaletteMode = PaletteMode.ICON_TEXT; 421 } 422 } 423 424 if (mPaletteMode.isPreview()) { 425 if (mForeground != null) { 426 mForeground.dispose(); 427 mForeground = null; 428 } 429 if (mBackground != null) { 430 mBackground.dispose(); 431 mBackground = null; 432 } 433 RGB background = mPreviewIconFactory.getBackgroundColor(); 434 if (background != null) { 435 mBackground = new Color(getDisplay(), background); 436 } 437 RGB foreground = mPreviewIconFactory.getForegroundColor(); 438 if (foreground != null) { 439 mForeground = new Color(getDisplay(), foreground); 440 } 441 } 442 443 List<String> headers = Collections.emptyList(); 444 final Map<String, List<ViewElementDescriptor>> categoryToItems; 445 categoryToItems = new HashMap<String, List<ViewElementDescriptor>>(); 446 headers = new ArrayList<String>(); 447 List<Pair<String,List<ViewElementDescriptor>>> paletteEntries = 448 ViewMetadataRepository.get().getPaletteEntries(targetData, 449 mAlphabetical, mCategories); 450 for (Pair<String,List<ViewElementDescriptor>> pair : paletteEntries) { 451 String category = pair.getFirst(); 452 List<ViewElementDescriptor> categoryItems = pair.getSecond(); 453 headers.add(category); 454 categoryToItems.put(category, categoryItems); 455 } 456 457 headers.add("Custom & Library Views"); 458 459 // Set the categories to expand the first item if 460 // (1) we don't have a previously selected category, or 461 // (2) there's just one category anyway, or 462 // (3) the set of categories have changed so our previously selected category 463 // doesn't exist anymore (can happen when you toggle "Show Categories") 464 if ((expandedCategories == null && headers.size() > 0) || headers.size() == 1 || 465 (expandedCategories != null && expandedCategories.size() >= 1 466 && !headers.contains( 467 expandedCategories.iterator().next().replace("&&", "&")))) { //$NON-NLS-1$ //$NON-NLS-2$ 468 // Expand the first category if we don't have a previous selection (e.g. refresh) 469 expandedCategories = Collections.singleton(headers.get(0)); 470 } 471 472 boolean wrap = mPaletteMode.getWrap(); 473 474 // Pack icon-only view vertically; others stretch to fill palette region 475 boolean fillVertical = mPaletteMode != PaletteMode.ICON_ONLY; 476 477 mAccordion = new AccordionControl(this, SWT.NONE, headers, fillVertical, wrap, 478 expandedCategories) { 479 @Override 480 protected Composite createChildContainer(Composite parent, Object header, int style) { 481 assert categoryToItems != null; 482 List<ViewElementDescriptor> list = categoryToItems.get(header); 483 final Composite composite; 484 if (list == null) { 485 assert header.equals("Custom & Library Views"); 486 487 Composite wrapper = new Composite(parent, SWT.NONE); 488 GridLayout gridLayout = new GridLayout(1, false); 489 gridLayout.marginWidth = gridLayout.marginHeight = 0; 490 gridLayout.horizontalSpacing = gridLayout.verticalSpacing = 0; 491 gridLayout.marginBottom = 3; 492 wrapper.setLayout(gridLayout); 493 if (mPaletteMode.isPreview() && mBackground != null) { 494 wrapper.setBackground(mBackground); 495 } 496 composite = super.createChildContainer(wrapper, header, style); 497 if (mPaletteMode.isPreview() && mBackground != null) { 498 composite.setBackground(mBackground); 499 } 500 composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); 501 502 Button refreshButton = new Button(wrapper, SWT.PUSH | SWT.FLAT); 503 refreshButton.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, 504 false, false, 1, 1)); 505 refreshButton.setText("Refresh"); 506 refreshButton.setImage(IconFactory.getInstance().getIcon("refresh")); //$NON-NLS-1$ 507 refreshButton.addSelectionListener(new SelectionAdapter() { 508 @Override 509 public void widgetSelected(SelectionEvent e) { 510 CustomViewFinder finder = CustomViewFinder.get(mEditor.getProject()); 511 finder.refresh(new ViewFinderListener(composite)); 512 } 513 }); 514 515 wrapper.layout(true); 516 } else { 517 composite = super.createChildContainer(parent, header, style); 518 if (mPaletteMode.isPreview() && mBackground != null) { 519 composite.setBackground(mBackground); 520 } 521 } 522 addMenu(composite); 523 return composite; 524 } 525 @Override 526 protected void createChildren(Composite parent, Object header) { 527 assert categoryToItems != null; 528 List<ViewElementDescriptor> list = categoryToItems.get(header); 529 if (list == null) { 530 assert header.equals("Custom & Library Views"); 531 addCustomItems(parent); 532 return; 533 } else { 534 for (ViewElementDescriptor desc : list) { 535 createItem(parent, desc); 536 } 537 } 538 } 539 }; 540 addMenu(mAccordion); 541 for (CLabel headerLabel : mAccordion.getHeaderLabels()) { 542 addMenu(headerLabel); 543 } 544 setLayout(new FillLayout()); 545 546 // Expand All for icon-only mode, but don't store it as the persistent auto-close mode; 547 // when we enter other modes it will read back whatever persistent mode. 548 if (mPaletteMode == PaletteMode.ICON_ONLY) { 549 mAccordion.expandAll(true); 550 mAccordion.setAutoClose(false); 551 } else { 552 mAccordion.setAutoClose(getSavedAutoCloseMode()); 553 } 554 555 layout(true); 556 } 557 558 protected void addCustomItems(final Composite parent) { 559 final CustomViewFinder finder = CustomViewFinder.get(mEditor.getProject()); 560 Collection<String> allViews = finder.getAllViews(); 561 if (allViews == null) { // Not yet initialized: trigger an async refresh 562 finder.refresh(new ViewFinderListener(parent)); 563 return; 564 } 565 566 // Remove previous content 567 for (Control c : parent.getChildren()) { 568 c.dispose(); 569 } 570 571 // Add new views 572 for (final String fqcn : allViews) { 573 CustomViewDescriptorService service = CustomViewDescriptorService.getInstance(); 574 ViewElementDescriptor desc = service.getDescriptor(mEditor.getProject(), fqcn); 575 if (desc == null) { 576 // The descriptor lookup performs validation steps of the class, and may 577 // in some cases determine that this is not a view and will return null; 578 // guard against that. 579 continue; 580 } 581 582 Control item = createItem(parent, desc); 583 584 // Add control-click listener on custom view items to you can warp to 585 // (and double click listener too -- the more discoverable, the better.) 586 if (item instanceof IconTextItem) { 587 IconTextItem it = (IconTextItem) item; 588 it.addMouseListener(new MouseAdapter() { 589 @Override 590 public void mouseDoubleClick(MouseEvent e) { 591 AdtPlugin.openJavaClass(mEditor.getProject(), fqcn); 592 } 593 594 @Override 595 public void mouseDown(MouseEvent e) { 596 if ((e.stateMask & SWT.MOD1) != 0) { 597 AdtPlugin.openJavaClass(mEditor.getProject(), fqcn); 598 } 599 } 600 }); 601 } 602 } 603 } 604 605 /* package */ GraphicalEditorPart getEditor() { 606 return mEditor; 607 } 608 609 private Control createItem(Composite parent, ViewElementDescriptor desc) { 610 Control item = null; 611 switch (mPaletteMode) { 612 case SMALL_PREVIEW: 613 case TINY_PREVIEW: 614 case PREVIEW: { 615 ImageDescriptor descriptor = mPreviewIconFactory.getImageDescriptor(desc); 616 if (descriptor != null) { 617 Image image = descriptor.createImage(); 618 ImageControl imageControl = new ImageControl(parent, SWT.None, image); 619 if (mPaletteMode.isScaledPreview()) { 620 // Try to preserve the overall size since rendering sizes typically 621 // vary with the dpi - so while the scaling factor for a 160 dpi 622 // rendering the scaling factor should be 0.5, for a 320 dpi one the 623 // scaling factor should be half that, 0.25. 624 float scale = 1.0f; 625 if (mPaletteMode == PaletteMode.SMALL_PREVIEW) { 626 scale = 0.75f; 627 } else if (mPaletteMode == PaletteMode.TINY_PREVIEW) { 628 scale = 0.5f; 629 } 630 int dpi = mEditor.getConfigurationComposite().getDensity().getDpiValue(); 631 while (dpi > 160) { 632 scale = scale / 2; 633 dpi = dpi / 2; 634 } 635 imageControl.setScale(scale); 636 } 637 imageControl.setHoverColor(getDisplay().getSystemColor(SWT.COLOR_WHITE)); 638 if (mBackground != null) { 639 imageControl.setBackground(mBackground); 640 } 641 String toolTip = desc.getUiName(); 642 // It appears pretty much none of the descriptors have tooltips 643 //String descToolTip = desc.getTooltip(); 644 //if (descToolTip != null && descToolTip.length() > 0) { 645 // toolTip = toolTip + "\n" + descToolTip; 646 //} 647 imageControl.setToolTipText(toolTip); 648 649 item = imageControl; 650 } else { 651 // Just use an Icon+Text item for these for now 652 item = new IconTextItem(parent, desc); 653 if (mForeground != null) { 654 item.setForeground(mForeground); 655 item.setBackground(mBackground); 656 } 657 } 658 break; 659 } 660 case ICON_TEXT: { 661 item = new IconTextItem(parent, desc); 662 break; 663 } 664 case ICON_ONLY: { 665 item = new ImageControl(parent, SWT.None, desc.getGenericIcon()); 666 item.setToolTipText(desc.getUiName()); 667 break; 668 } 669 default: 670 throw new IllegalArgumentException("Not yet implemented"); 671 } 672 673 final DragSource source = new DragSource(item, DND.DROP_COPY); 674 source.setTransfer(new Transfer[] { SimpleXmlTransfer.getInstance() }); 675 source.addDragListener(new DescDragSourceListener(desc)); 676 item.addDisposeListener(new DisposeListener() { 677 @Override 678 public void widgetDisposed(DisposeEvent e) { 679 source.dispose(); 680 } 681 }); 682 addMenu(item); 683 684 return item; 685 } 686 687 /** 688 * An Item widget represents one {@link ElementDescriptor} that can be dropped on the 689 * GLE2 canvas using drag'n'drop. 690 */ 691 private static class IconTextItem extends CLabel implements MouseTrackListener { 692 693 private boolean mMouseIn; 694 695 public IconTextItem(Composite parent, ViewElementDescriptor desc) { 696 super(parent, SWT.NONE); 697 mMouseIn = false; 698 699 setText(desc.getUiName()); 700 setImage(desc.getGenericIcon()); 701 setToolTipText(desc.getTooltip()); 702 addMouseTrackListener(this); 703 } 704 705 @Override 706 public int getStyle() { 707 int style = super.getStyle(); 708 if (mMouseIn) { 709 style |= SWT.SHADOW_IN; 710 } 711 return style; 712 } 713 714 @Override 715 public void mouseEnter(MouseEvent e) { 716 if (!mMouseIn) { 717 mMouseIn = true; 718 redraw(); 719 } 720 } 721 722 @Override 723 public void mouseExit(MouseEvent e) { 724 if (mMouseIn) { 725 mMouseIn = false; 726 redraw(); 727 } 728 } 729 730 @Override 731 public void mouseHover(MouseEvent e) { 732 // pass 733 } 734 } 735 736 /** 737 * A {@link DragSourceListener} that deals with drag'n'drop of 738 * {@link ElementDescriptor}s. 739 */ 740 private class DescDragSourceListener implements DragSourceListener { 741 private final ViewElementDescriptor mDesc; 742 private SimpleElement[] mElements; 743 744 public DescDragSourceListener(ViewElementDescriptor desc) { 745 mDesc = desc; 746 } 747 748 @Override 749 public void dragStart(DragSourceEvent e) { 750 // See if we can find out the bounds of this element from a preview image. 751 // Preview images are created before the drag source listener is notified 752 // of the started drag. 753 Rect bounds = null; 754 Rect dragBounds = null; 755 756 createDragImage(e); 757 if (mImage != null && !mIsPlaceholder) { 758 int width = mImageLayoutBounds.width; 759 int height = mImageLayoutBounds.height; 760 assert mImageLayoutBounds.x == 0; 761 assert mImageLayoutBounds.y == 0; 762 bounds = new Rect(0, 0, width, height); 763 double scale = mEditor.getCanvasControl().getScale(); 764 int scaledWidth = (int) (scale * width); 765 int scaledHeight = (int) (scale * height); 766 int x = -scaledWidth / 2; 767 int y = -scaledHeight / 2; 768 dragBounds = new Rect(x, y, scaledWidth, scaledHeight); 769 } 770 771 SimpleElement se = new SimpleElement( 772 SimpleXmlTransfer.getFqcn(mDesc), 773 null /* parentFqcn */, 774 bounds /* bounds */, 775 null /* parentBounds */); 776 if (mDesc instanceof PaletteMetadataDescriptor) { 777 PaletteMetadataDescriptor pm = (PaletteMetadataDescriptor) mDesc; 778 pm.initializeNew(se); 779 } 780 mElements = new SimpleElement[] { se }; 781 782 // Register this as the current dragged data 783 GlobalCanvasDragInfo dragInfo = GlobalCanvasDragInfo.getInstance(); 784 dragInfo.startDrag( 785 mElements, 786 null /* selection */, 787 null /* canvas */, 788 null /* removeSource */); 789 dragInfo.setDragBounds(dragBounds); 790 dragInfo.setDragBaseline(mBaseline); 791 792 793 e.doit = true; 794 } 795 796 @Override 797 public void dragSetData(DragSourceEvent e) { 798 // Provide the data for the drop when requested by the other side. 799 if (SimpleXmlTransfer.getInstance().isSupportedType(e.dataType)) { 800 e.data = mElements; 801 } 802 } 803 804 @Override 805 public void dragFinished(DragSourceEvent e) { 806 // Unregister the dragged data. 807 GlobalCanvasDragInfo.getInstance().stopDrag(); 808 mElements = null; 809 if (mImage != null) { 810 mImage.dispose(); 811 mImage = null; 812 } 813 } 814 815 // TODO: Figure out the right dimensions to use for rendering. 816 // We WILL crop this after rendering, but for performance reasons it would be good 817 // not to make it much larger than necessary since to crop this we rely on 818 // actually scanning pixels. 819 820 /** 821 * Width of the rendered preview image (before it is cropped), although the actual 822 * width may be smaller (since we also take the device screen's size into account) 823 */ 824 private static final int MAX_RENDER_HEIGHT = 400; 825 826 /** 827 * Height of the rendered preview image (before it is cropped), although the 828 * actual width may be smaller (since we also take the device screen's size into 829 * account) 830 */ 831 private static final int MAX_RENDER_WIDTH = 500; 832 833 /** Amount of alpha to multiply into the image (divided by 256) */ 834 private static final int IMG_ALPHA = 128; 835 836 /** The image shown during the drag */ 837 private Image mImage; 838 /** The non-effect bounds of the drag image */ 839 private Rectangle mImageLayoutBounds; 840 private int mBaseline = -1; 841 842 /** 843 * If true, the image is a preview of the view, and if not it is a "fallback" 844 * image of some sort, such as a rendering of the palette item itself 845 */ 846 private boolean mIsPlaceholder; 847 848 private void createDragImage(DragSourceEvent event) { 849 mBaseline = -1; 850 Pair<Image, Rectangle> preview = renderPreview(); 851 if (preview != null) { 852 mImage = preview.getFirst(); 853 mImageLayoutBounds = preview.getSecond(); 854 } else { 855 mImage = null; 856 mImageLayoutBounds = null; 857 } 858 859 mIsPlaceholder = mImage == null; 860 if (mIsPlaceholder) { 861 // Couldn't render preview (or the preview is a blank image, such as for 862 // example the preview of an empty layout), so instead create a placeholder 863 // image 864 // Render the palette item itself as an image 865 Control control = ((DragSource) event.widget).getControl(); 866 GC gc = new GC(control); 867 Point size = control.getSize(); 868 Display display = getDisplay(); 869 final Image image = new Image(display, size.x, size.y); 870 gc.copyArea(image, 0, 0); 871 gc.dispose(); 872 873 BufferedImage awtImage = SwtUtils.convertToAwt(image); 874 if (awtImage != null) { 875 awtImage = ImageUtils.createDropShadow(awtImage, 3 /* shadowSize */, 876 0.7f /* shadowAlpha */, 0x000000 /* shadowRgb */); 877 mImage = SwtUtils.convertToSwt(display, awtImage, true, IMG_ALPHA); 878 } else { 879 ImageData data = image.getImageData(); 880 data.alpha = IMG_ALPHA; 881 882 // Changing the ImageData -after- constructing an image on it 883 // has no effect, so we have to construct a new image. Luckily these 884 // are tiny images. 885 mImage = new Image(display, data); 886 } 887 image.dispose(); 888 } 889 890 event.image = mImage; 891 892 if (!mIsPlaceholder) { 893 // Shift the drag feedback image up such that it's centered under the 894 // mouse pointer 895 double scale = mEditor.getCanvasControl().getScale(); 896 event.offsetX = (int) (scale * mImageLayoutBounds.width / 2); 897 event.offsetY = (int) (scale * mImageLayoutBounds.height / 2); 898 } 899 } 900 901 /** 902 * Performs the actual rendering of the descriptor into an image and returns the 903 * image as well as the layout bounds of the image (not including drop shadow etc) 904 */ 905 private Pair<Image, Rectangle> renderPreview() { 906 ViewMetadataRepository repository = ViewMetadataRepository.get(); 907 RenderMode renderMode = repository.getRenderMode(mDesc.getFullClassName()); 908 if (renderMode == RenderMode.SKIP) { 909 return null; 910 } 911 912 // Create blank XML document 913 Document document = DomUtilities.createEmptyDocument(); 914 915 // Insert our target view's XML into it as a node 916 GraphicalEditorPart editor = getEditor(); 917 LayoutEditorDelegate layoutEditorDelegate = editor.getEditorDelegate(); 918 919 String viewName = mDesc.getXmlLocalName(); 920 Element element = document.createElement(viewName); 921 922 // Set up a proper name space 923 Attr attr = document.createAttributeNS(XmlnsAttributeDescriptor.XMLNS_URI, 924 "xmlns:android"); //$NON-NLS-1$ 925 attr.setValue(ANDROID_URI); 926 element.getAttributes().setNamedItemNS(attr); 927 928 element.setAttributeNS(ANDROID_URI, 929 ATTR_LAYOUT_WIDTH, VALUE_WRAP_CONTENT); 930 element.setAttributeNS(ANDROID_URI, 931 ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT); 932 933 // This doesn't apply to all, but doesn't seem to cause harm and makes for a 934 // better experience with text-oriented views like buttons and texts 935 element.setAttributeNS(ANDROID_URI, ATTR_TEXT, 936 DescriptorsUtils.getBasename(mDesc.getUiName())); 937 938 // Is this a palette variation? 939 if (mDesc instanceof PaletteMetadataDescriptor) { 940 PaletteMetadataDescriptor pm = (PaletteMetadataDescriptor) mDesc; 941 pm.initializeNew(element); 942 } 943 944 document.appendChild(element); 945 946 // Construct UI model from XML 947 AndroidTargetData data = layoutEditorDelegate.getEditor().getTargetData(); 948 DocumentDescriptor documentDescriptor; 949 if (data == null) { 950 documentDescriptor = new DocumentDescriptor("temp", null/*children*/);//$NON-NLS-1$ 951 } else { 952 documentDescriptor = data.getLayoutDescriptors().getDescriptor(); 953 } 954 UiDocumentNode model = (UiDocumentNode) documentDescriptor.createUiNode(); 955 model.setEditor(layoutEditorDelegate.getEditor()); 956 model.setUnknownDescriptorProvider(editor.getModel().getUnknownDescriptorProvider()); 957 model.loadFromXmlNode(document); 958 959 // Call the create-hooks such that we for example insert mandatory 960 // children into views like the DialerFilter, apply image source attributes 961 // to ImageButtons, etc. 962 LayoutCanvas canvas = editor.getCanvasControl(); 963 NodeFactory nodeFactory = canvas.getNodeFactory(); 964 UiElementNode parent = model.getUiRoot(); 965 UiElementNode child = parent.getUiChildren().get(0); 966 if (child instanceof UiViewElementNode) { 967 UiViewElementNode childUiNode = (UiViewElementNode) child; 968 NodeProxy childNode = nodeFactory.create(childUiNode); 969 970 // Applying create hooks as part of palette render should 971 // not trigger model updates 972 layoutEditorDelegate.getEditor().setIgnoreXmlUpdate(true); 973 try { 974 canvas.getRulesEngine().callCreateHooks(layoutEditorDelegate.getEditor(), 975 null, childNode, InsertType.CREATE_PREVIEW); 976 childNode.applyPendingChanges(); 977 } catch (Throwable t) { 978 AdtPlugin.log(t, "Failed calling creation hooks for widget %1$s", viewName); 979 } finally { 980 layoutEditorDelegate.getEditor().setIgnoreXmlUpdate(false); 981 } 982 } 983 984 Integer overrideBgColor = null; 985 boolean hasTransparency = false; 986 LayoutLibrary layoutLibrary = editor.getLayoutLibrary(); 987 if (layoutLibrary != null && 988 layoutLibrary.supports(Capability.CUSTOM_BACKGROUND_COLOR)) { 989 // It doesn't matter what the background color is as long as the alpha 990 // is 0 (fully transparent). We're using red to make it more obvious if 991 // for some reason the background is painted when it shouldn't be. 992 overrideBgColor = new Integer(0x00FF0000); 993 } 994 995 RenderSession session = null; 996 try { 997 // Use at most the size of the screen for the preview render. 998 // This is important since when we fill the size of certain views (like 999 // a SeekBar), we want it to at most be the width of the screen, and for small 1000 // screens the RENDER_WIDTH was wider. 1001 Rect screenBounds = editor.getScreenBounds(); 1002 int renderWidth = Math.min(screenBounds.w, MAX_RENDER_WIDTH); 1003 int renderHeight = Math.min(screenBounds.h, MAX_RENDER_HEIGHT); 1004 LayoutLog silentLogger = new LayoutLog(); 1005 1006 session = RenderService.create(editor) 1007 .setModel(model) 1008 .setSize(renderWidth, renderHeight) 1009 .setLog(silentLogger) 1010 .setOverrideBgColor(overrideBgColor) 1011 .setDecorations(false) 1012 .createRenderSession(); 1013 } catch (Throwable t) { 1014 // Previews can fail for a variety of reasons -- let's not bug 1015 // the user with it 1016 return null; 1017 } 1018 1019 if (session != null) { 1020 if (session.getResult().isSuccess()) { 1021 BufferedImage image = session.getImage(); 1022 if (image != null) { 1023 BufferedImage cropped; 1024 Rect initialCrop = null; 1025 ViewInfo viewInfo = null; 1026 1027 List<ViewInfo> viewInfoList = session.getRootViews(); 1028 1029 if (viewInfoList != null && viewInfoList.size() > 0) { 1030 viewInfo = viewInfoList.get(0); 1031 mBaseline = viewInfo.getBaseLine(); 1032 } 1033 1034 if (viewInfo != null) { 1035 int x1 = viewInfo.getLeft(); 1036 int x2 = viewInfo.getRight(); 1037 int y2 = viewInfo.getBottom(); 1038 int y1 = viewInfo.getTop(); 1039 initialCrop = new Rect(x1, y1, x2 - x1, y2 - y1); 1040 } 1041 1042 if (hasTransparency) { 1043 cropped = ImageUtils.cropBlank(image, initialCrop); 1044 } else { 1045 // Find out what the "background" color is such that we can properly 1046 // crop it out of the image. To do this we pick out a pixel in the 1047 // bottom right unpainted area. Rather than pick the one in the far 1048 // bottom corner, we pick one as close to the bounds of the view as 1049 // possible (but still outside of the bounds), such that we can 1050 // deal with themes like the dialog theme. 1051 int edgeX = image.getWidth() -1; 1052 int edgeY = image.getHeight() -1; 1053 if (viewInfo != null) { 1054 if (viewInfo.getRight() < image.getWidth()-1) { 1055 edgeX = viewInfo.getRight()+1; 1056 } 1057 if (viewInfo.getBottom() < image.getHeight()-1) { 1058 edgeY = viewInfo.getBottom()+1; 1059 } 1060 } 1061 int edgeColor = image.getRGB(edgeX, edgeY); 1062 cropped = ImageUtils.cropColor(image, edgeColor, initialCrop); 1063 } 1064 1065 if (cropped != null) { 1066 int width = initialCrop != null ? initialCrop.w : cropped.getWidth(); 1067 int height = initialCrop != null ? initialCrop.h : cropped.getHeight(); 1068 boolean needsContrast = hasTransparency 1069 && !ImageUtils.containsDarkPixels(cropped); 1070 cropped = ImageUtils.createDropShadow(cropped, 1071 hasTransparency ? 3 : 5 /* shadowSize */, 1072 !hasTransparency ? 0.6f : needsContrast ? 0.8f : 0.7f/*alpha*/, 1073 0x000000 /* shadowRgb */); 1074 1075 double scale = canvas.getScale(); 1076 if (scale != 1L) { 1077 cropped = ImageUtils.scale(cropped, scale, scale); 1078 } 1079 1080 Display display = getDisplay(); 1081 int alpha = (!hasTransparency || !needsContrast) ? IMG_ALPHA : -1; 1082 Image swtImage = SwtUtils.convertToSwt(display, cropped, true, alpha); 1083 Rectangle imageBounds = new Rectangle(0, 0, width, height); 1084 return Pair.of(swtImage, imageBounds); 1085 } 1086 } 1087 } 1088 1089 session.dispose(); 1090 } 1091 1092 return null; 1093 } 1094 1095 /** 1096 * Utility method to print out the contents of the given XML document. This is 1097 * really useful when working on the preview code above. I'm including all the 1098 * code inside a constant false, which means the compiler will omit all the code, 1099 * but I'd like to leave it in the code base and by doing it this way rather than 1100 * as commented out code the code won't be accidentally broken. 1101 */ 1102 @SuppressWarnings("all") 1103 private void dumpDocument(Document document) { 1104 // Diagnostics: print out the XML that we're about to render 1105 if (false) { // Will be omitted by the compiler 1106 org.apache.xml.serialize.OutputFormat outputFormat = 1107 new org.apache.xml.serialize.OutputFormat( 1108 "XML", "ISO-8859-1", true); //$NON-NLS-1$ //$NON-NLS-2$ 1109 outputFormat.setIndent(2); 1110 outputFormat.setLineWidth(100); 1111 outputFormat.setIndenting(true); 1112 outputFormat.setOmitXMLDeclaration(true); 1113 outputFormat.setOmitDocumentType(true); 1114 StringWriter stringWriter = new StringWriter(); 1115 // Using FQN here to avoid having an import above, which will result 1116 // in a deprecation warning, and there isn't a way to annotate a single 1117 // import element with a SuppressWarnings. 1118 org.apache.xml.serialize.XMLSerializer serializer = 1119 new org.apache.xml.serialize.XMLSerializer(stringWriter, outputFormat); 1120 serializer.setNamespaces(true); 1121 try { 1122 serializer.serialize(document.getDocumentElement()); 1123 System.out.println(stringWriter.toString()); 1124 } catch (IOException e) { 1125 e.printStackTrace(); 1126 } 1127 } 1128 } 1129 } 1130 1131 /** Action for switching view modes via radio buttons */ 1132 private class PaletteModeAction extends Action { 1133 private final PaletteMode mMode; 1134 1135 PaletteModeAction(PaletteMode mode) { 1136 super(mode.getActionLabel(), IAction.AS_RADIO_BUTTON); 1137 mMode = mode; 1138 boolean selected = mMode == mPaletteMode; 1139 setChecked(selected); 1140 setEnabled(!selected); 1141 } 1142 1143 @Override 1144 public void run() { 1145 if (isEnabled()) { 1146 mPaletteMode = mMode; 1147 refreshPalette(); 1148 savePaletteMode(); 1149 } 1150 } 1151 } 1152 1153 /** Action for toggling various checkbox view modes - categories, sorting, etc */ 1154 private class ToggleViewOptionAction extends Action { 1155 private final int mAction; 1156 final static int TOGGLE_CATEGORY = 1; 1157 final static int TOGGLE_ALPHABETICAL = 2; 1158 final static int TOGGLE_AUTO_CLOSE = 3; 1159 final static int REFRESH = 4; 1160 final static int RESET = 5; 1161 1162 ToggleViewOptionAction(String title, int action, boolean checked) { 1163 super(title, (action == REFRESH || action == RESET) ? IAction.AS_PUSH_BUTTON 1164 : IAction.AS_CHECK_BOX); 1165 mAction = action; 1166 if (checked) { 1167 setChecked(checked); 1168 } 1169 } 1170 1171 @Override 1172 public void run() { 1173 switch (mAction) { 1174 case TOGGLE_CATEGORY: 1175 mCategories = !mCategories; 1176 refreshPalette(); 1177 break; 1178 case TOGGLE_ALPHABETICAL: 1179 mAlphabetical = !mAlphabetical; 1180 refreshPalette(); 1181 break; 1182 case TOGGLE_AUTO_CLOSE: 1183 mAutoClose = !mAutoClose; 1184 mAccordion.setAutoClose(mAutoClose); 1185 break; 1186 case REFRESH: 1187 mPreviewIconFactory.refresh(); 1188 refreshPalette(); 1189 break; 1190 case RESET: 1191 mAlphabetical = false; 1192 mCategories = true; 1193 mAutoClose = true; 1194 mPaletteMode = PaletteMode.SMALL_PREVIEW; 1195 refreshPalette(); 1196 break; 1197 } 1198 savePaletteMode(); 1199 } 1200 } 1201 1202 private void addMenu(Control control) { 1203 control.addMenuDetectListener(new MenuDetectListener() { 1204 @Override 1205 public void menuDetected(MenuDetectEvent e) { 1206 showMenu(e.x, e.y); 1207 } 1208 }); 1209 } 1210 1211 private void showMenu(int x, int y) { 1212 MenuManager manager = new MenuManager() { 1213 @Override 1214 public boolean isDynamic() { 1215 return true; 1216 } 1217 }; 1218 boolean previews = previewsAvailable(); 1219 for (PaletteMode mode : PaletteMode.values()) { 1220 if (mode.isPreview() && !previews) { 1221 continue; 1222 } 1223 manager.add(new PaletteModeAction(mode)); 1224 } 1225 if (mPaletteMode.isPreview()) { 1226 manager.add(new Separator()); 1227 manager.add(new ToggleViewOptionAction("Refresh Previews", 1228 ToggleViewOptionAction.REFRESH, 1229 false)); 1230 } 1231 manager.add(new Separator()); 1232 manager.add(new ToggleViewOptionAction("Show Categories", 1233 ToggleViewOptionAction.TOGGLE_CATEGORY, 1234 mCategories)); 1235 manager.add(new ToggleViewOptionAction("Sort Alphabetically", 1236 ToggleViewOptionAction.TOGGLE_ALPHABETICAL, 1237 mAlphabetical)); 1238 manager.add(new Separator()); 1239 manager.add(new ToggleViewOptionAction("Auto Close Previous", 1240 ToggleViewOptionAction.TOGGLE_AUTO_CLOSE, 1241 mAutoClose)); 1242 manager.add(new Separator()); 1243 manager.add(new ToggleViewOptionAction("Reset", 1244 ToggleViewOptionAction.RESET, 1245 false)); 1246 1247 Menu menu = manager.createContextMenu(PaletteControl.this); 1248 menu.setLocation(x, y); 1249 menu.setVisible(true); 1250 } 1251 1252 private final class ViewFinderListener implements CustomViewFinder.Listener { 1253 private final Composite mParent; 1254 1255 private ViewFinderListener(Composite parent) { 1256 this.mParent = parent; 1257 } 1258 1259 @Override 1260 public void viewsUpdated(Collection<String> customViews, 1261 Collection<String> thirdPartyViews) { 1262 addCustomItems(mParent); 1263 mParent.layout(true); 1264 } 1265 } 1266 } 1267