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