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