1 /* 2 * Copyright (C) 2012 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 package com.android.ide.eclipse.adt.internal.editors.layout.gle2; 17 18 import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; 19 import static com.android.SdkConstants.PREFIX_RESOURCE_REF; 20 import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; 21 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.MASK_RENDERING; 22 import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SHADOW_SIZE; 23 import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SMALL_SHADOW_SIZE; 24 import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.DEFAULT; 25 import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.INCLUDES; 26 27 import com.android.annotations.NonNull; 28 import com.android.annotations.Nullable; 29 import com.android.ide.common.rendering.api.RenderSession; 30 import com.android.ide.common.rendering.api.ResourceValue; 31 import com.android.ide.common.rendering.api.Result; 32 import com.android.ide.common.rendering.api.Result.Status; 33 import com.android.ide.common.resources.ResourceFile; 34 import com.android.ide.common.resources.ResourceRepository; 35 import com.android.ide.common.resources.ResourceResolver; 36 import com.android.ide.common.resources.configuration.FolderConfiguration; 37 import com.android.ide.common.resources.configuration.ScreenOrientationQualifier; 38 import com.android.ide.eclipse.adt.AdtPlugin; 39 import com.android.ide.eclipse.adt.AdtUtils; 40 import com.android.ide.eclipse.adt.internal.editors.IconFactory; 41 import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor; 42 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration; 43 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser; 44 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient; 45 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription; 46 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Locale; 47 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.NestedConfiguration; 48 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.VaryingConfiguration; 49 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; 50 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; 51 import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; 52 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; 53 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; 54 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 55 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 56 import com.android.ide.eclipse.adt.io.IFileWrapper; 57 import com.android.io.IAbstractFile; 58 import com.android.resources.Density; 59 import com.android.resources.ResourceType; 60 import com.android.resources.ScreenOrientation; 61 import com.android.sdklib.IAndroidTarget; 62 import com.android.sdklib.devices.Device; 63 import com.android.sdklib.devices.Screen; 64 import com.android.sdklib.devices.State; 65 import com.android.utils.SdkUtils; 66 67 import org.eclipse.core.resources.IFile; 68 import org.eclipse.core.runtime.IProgressMonitor; 69 import org.eclipse.core.runtime.IStatus; 70 import org.eclipse.core.runtime.jobs.IJobChangeEvent; 71 import org.eclipse.core.runtime.jobs.IJobChangeListener; 72 import org.eclipse.core.runtime.jobs.Job; 73 import org.eclipse.jface.dialogs.InputDialog; 74 import org.eclipse.jface.window.Window; 75 import org.eclipse.swt.SWT; 76 import org.eclipse.swt.graphics.Color; 77 import org.eclipse.swt.graphics.GC; 78 import org.eclipse.swt.graphics.Image; 79 import org.eclipse.swt.graphics.ImageData; 80 import org.eclipse.swt.graphics.Point; 81 import org.eclipse.swt.graphics.Region; 82 import org.eclipse.swt.widgets.Display; 83 import org.eclipse.ui.ISharedImages; 84 import org.eclipse.ui.PlatformUI; 85 import org.eclipse.ui.progress.UIJob; 86 import org.w3c.dom.Document; 87 88 import java.awt.Graphics2D; 89 import java.awt.image.BufferedImage; 90 import java.io.File; 91 import java.lang.ref.SoftReference; 92 import java.util.Comparator; 93 import java.util.Map; 94 95 /** 96 * Represents a preview rendering of a given configuration 97 */ 98 public class RenderPreview implements IJobChangeListener { 99 /** Whether previews should use large shadows */ 100 static final boolean LARGE_SHADOWS = false; 101 102 /** 103 * Still doesn't work; get exceptions from layoutlib: 104 * java.lang.IllegalStateException: After scene creation, #init() must be called 105 * at com.android.layoutlib.bridge.impl.RenderAction.acquire(RenderAction.java:151) 106 * <p> 107 * TODO: Investigate. 108 */ 109 private static final boolean RENDER_ASYNC = false; 110 111 /** 112 * Height of the toolbar shown over a preview during hover. Needs to be 113 * large enough to accommodate icons below. 114 */ 115 private static final int HEADER_HEIGHT = 20; 116 117 /** Whether to dump out rendering failures of the previews to the log */ 118 private static final boolean DUMP_RENDER_DIAGNOSTICS = false; 119 120 /** Extra error checking in debug mode */ 121 private static final boolean DEBUG = false; 122 123 private static final Image EDIT_ICON; 124 private static final Image ZOOM_IN_ICON; 125 private static final Image ZOOM_OUT_ICON; 126 private static final Image CLOSE_ICON; 127 private static final int EDIT_ICON_WIDTH; 128 private static final int ZOOM_IN_ICON_WIDTH; 129 private static final int ZOOM_OUT_ICON_WIDTH; 130 private static final int CLOSE_ICON_WIDTH; 131 static { 132 ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); 133 IconFactory icons = IconFactory.getInstance(); 134 CLOSE_ICON = sharedImages.getImage(ISharedImages.IMG_ETOOL_DELETE); 135 EDIT_ICON = icons.getIcon("editPreview"); //$NON-NLS-1$ 136 ZOOM_IN_ICON = icons.getIcon("zoomplus"); //$NON-NLS-1$ 137 ZOOM_OUT_ICON = icons.getIcon("zoomminus"); //$NON-NLS-1$ 138 CLOSE_ICON_WIDTH = CLOSE_ICON.getImageData().width; 139 EDIT_ICON_WIDTH = EDIT_ICON.getImageData().width; 140 ZOOM_IN_ICON_WIDTH = ZOOM_IN_ICON.getImageData().width; 141 ZOOM_OUT_ICON_WIDTH = ZOOM_OUT_ICON.getImageData().width; 142 } 143 144 /** The configuration being previewed */ 145 private @NonNull Configuration mConfiguration; 146 147 /** Configuration to use if we have an alternate input to be rendered */ 148 private @NonNull Configuration mAlternateConfiguration; 149 150 /** The associated manager */ 151 private final @NonNull RenderPreviewManager mManager; 152 private final @NonNull LayoutCanvas mCanvas; 153 154 private @NonNull SoftReference<ResourceResolver> mResourceResolver = 155 new SoftReference<ResourceResolver>(null); 156 private @Nullable Job mJob; 157 private @Nullable Image mThumbnail; 158 private @Nullable String mDisplayName; 159 private int mWidth; 160 private int mHeight; 161 private int mX; 162 private int mY; 163 private int mTitleHeight; 164 private double mScale = 1.0; 165 private double mAspectRatio; 166 167 /** If non null, points to a separate file containing the source */ 168 private @Nullable IFile mAlternateInput; 169 170 /** If included within another layout, the name of that outer layout */ 171 private @Nullable Reference mIncludedWithin; 172 173 /** Whether the mouse is actively hovering over this preview */ 174 private boolean mActive; 175 176 /** 177 * Whether this preview cannot be rendered because of a model error - such 178 * as an invalid configuration, a missing resource, an error in the XML 179 * markup, etc. If non null, contains the error message (or a blank string 180 * if not known), and null if the render was successful. 181 */ 182 private String mError; 183 184 /** Whether in the current layout, this preview is visible */ 185 private boolean mVisible; 186 187 /** Whether the configuration has changed and needs to be refreshed the next time 188 * this preview made visible. This corresponds to the change flags in 189 * {@link ConfigurationClient}. */ 190 private int mDirty; 191 192 /** 193 * Creates a new {@linkplain RenderPreview} 194 * 195 * @param manager the manager 196 * @param canvas canvas where preview is painted 197 * @param configuration the associated configuration 198 * @param width the initial width to use for the preview 199 * @param height the initial height to use for the preview 200 */ RenderPreview( @onNull RenderPreviewManager manager, @NonNull LayoutCanvas canvas, @NonNull Configuration configuration)201 private RenderPreview( 202 @NonNull RenderPreviewManager manager, 203 @NonNull LayoutCanvas canvas, 204 @NonNull Configuration configuration) { 205 mManager = manager; 206 mCanvas = canvas; 207 mConfiguration = configuration; 208 updateSize(); 209 210 // Should only attempt to create configurations for fully configured devices 211 assert mConfiguration.getDevice() != null 212 && mConfiguration.getDeviceState() != null 213 && mConfiguration.getLocale() != null 214 && mConfiguration.getTarget() != null 215 && mConfiguration.getTheme() != null 216 && mConfiguration.getFullConfig() != null 217 && mConfiguration.getFullConfig().getScreenSizeQualifier() != null : 218 mConfiguration; 219 } 220 221 /** 222 * Sets the configuration to use for this preview 223 * 224 * @param configuration the new configuration 225 */ setConfiguration(@onNull Configuration configuration)226 public void setConfiguration(@NonNull Configuration configuration) { 227 mConfiguration = configuration; 228 } 229 230 /** 231 * Gets the scale being applied to the thumbnail 232 * 233 * @return the scale being applied to the thumbnail 234 */ getScale()235 public double getScale() { 236 return mScale; 237 } 238 239 /** 240 * Sets the scale to apply to the thumbnail 241 * 242 * @param scale the factor to scale the thumbnail picture by 243 */ setScale(double scale)244 public void setScale(double scale) { 245 disposeThumbnail(); 246 mScale = scale; 247 } 248 249 /** 250 * Returns the aspect ratio of this render preview 251 * 252 * @return the aspect ratio 253 */ getAspectRatio()254 public double getAspectRatio() { 255 return mAspectRatio; 256 } 257 258 /** 259 * Returns whether the preview is actively hovered 260 * 261 * @return whether the mouse is hovering over the preview 262 */ isActive()263 public boolean isActive() { 264 return mActive; 265 } 266 267 /** 268 * Sets whether the preview is actively hovered 269 * 270 * @param active if the mouse is hovering over the preview 271 */ setActive(boolean active)272 public void setActive(boolean active) { 273 mActive = active; 274 } 275 276 /** 277 * Returns whether the preview is visible. Previews that are off 278 * screen are typically marked invisible during layout, which means we don't 279 * have to expend effort computing preview thumbnails etc 280 * 281 * @return true if the preview is visible 282 */ isVisible()283 public boolean isVisible() { 284 return mVisible; 285 } 286 287 /** 288 * Returns whether this preview represents a forked layout 289 * 290 * @return true if this preview represents a separate file 291 */ isForked()292 public boolean isForked() { 293 return mAlternateInput != null || mIncludedWithin != null; 294 } 295 296 /** 297 * Returns the file to be used for this preview, or null if this is not a 298 * forked layout meaning that the file is the one used in the chooser 299 * 300 * @return the file or null for non-forked layouts 301 */ 302 @Nullable getAlternateInput()303 public IFile getAlternateInput() { 304 if (mAlternateInput != null) { 305 return mAlternateInput; 306 } else if (mIncludedWithin != null) { 307 return mIncludedWithin.getFile(); 308 } 309 310 return null; 311 } 312 313 /** 314 * Returns the area of this render preview, PRIOR to scaling 315 * 316 * @return the area (width times height without scaling) 317 */ getArea()318 int getArea() { 319 return mWidth * mHeight; 320 } 321 322 /** 323 * Sets whether the preview is visible. Previews that are off 324 * screen are typically marked invisible during layout, which means we don't 325 * have to expend effort computing preview thumbnails etc 326 * 327 * @param visible whether this preview is visible 328 */ setVisible(boolean visible)329 public void setVisible(boolean visible) { 330 if (visible != mVisible) { 331 mVisible = visible; 332 if (mVisible) { 333 if (mDirty != 0) { 334 // Just made the render preview visible: 335 configurationChanged(mDirty); // schedules render 336 } else { 337 updateForkStatus(); 338 mManager.scheduleRender(this); 339 } 340 } else { 341 dispose(); 342 } 343 } 344 } 345 346 /** 347 * Sets the layout position relative to the top left corner of the preview 348 * area, in control coordinates 349 */ setPosition(int x, int y)350 void setPosition(int x, int y) { 351 mX = x; 352 mY = y; 353 } 354 355 /** 356 * Gets the layout X position relative to the top left corner of the preview 357 * area, in control coordinates 358 */ getX()359 int getX() { 360 return mX; 361 } 362 363 /** 364 * Gets the layout Y position relative to the top left corner of the preview 365 * area, in control coordinates 366 */ getY()367 int getY() { 368 return mY; 369 } 370 371 /** Determine whether this configuration has a better match in a different layout file */ updateForkStatus()372 private void updateForkStatus() { 373 ConfigurationChooser chooser = mManager.getChooser(); 374 FolderConfiguration config = mConfiguration.getFullConfig(); 375 if (mAlternateInput != null && chooser.isBestMatchFor(mAlternateInput, config)) { 376 return; 377 } 378 379 mAlternateInput = null; 380 IFile editedFile = chooser.getEditedFile(); 381 if (editedFile != null) { 382 if (!chooser.isBestMatchFor(editedFile, config)) { 383 ProjectResources resources = chooser.getResources(); 384 if (resources != null) { 385 ResourceFile best = resources.getMatchingFile(editedFile.getName(), 386 ResourceType.LAYOUT, config); 387 if (best != null) { 388 IAbstractFile file = best.getFile(); 389 if (file instanceof IFileWrapper) { 390 mAlternateInput = ((IFileWrapper) file).getIFile(); 391 } else if (file instanceof File) { 392 mAlternateInput = AdtUtils.fileToIFile(((File) file)); 393 } 394 } 395 } 396 if (mAlternateInput != null) { 397 mAlternateConfiguration = Configuration.create(mConfiguration, 398 mAlternateInput); 399 } 400 } 401 } 402 } 403 404 /** 405 * Creates a new {@linkplain RenderPreview} 406 * 407 * @param manager the manager 408 * @param configuration the associated configuration 409 * @return a new configuration 410 */ 411 @NonNull create( @onNull RenderPreviewManager manager, @NonNull Configuration configuration)412 public static RenderPreview create( 413 @NonNull RenderPreviewManager manager, 414 @NonNull Configuration configuration) { 415 LayoutCanvas canvas = manager.getCanvas(); 416 return new RenderPreview(manager, canvas, configuration); 417 } 418 419 /** 420 * Throws away this preview: cancels any pending rendering jobs and disposes 421 * of image resources etc 422 */ dispose()423 public void dispose() { 424 disposeThumbnail(); 425 426 if (mJob != null) { 427 mJob.cancel(); 428 mJob = null; 429 } 430 } 431 432 /** Disposes the thumbnail rendering. */ disposeThumbnail()433 void disposeThumbnail() { 434 if (mThumbnail != null) { 435 mThumbnail.dispose(); 436 mThumbnail = null; 437 } 438 } 439 440 /** 441 * Returns the display name of this preview 442 * 443 * @return the name of the preview 444 */ 445 @NonNull getDisplayName()446 public String getDisplayName() { 447 if (mDisplayName == null) { 448 String displayName = getConfiguration().getDisplayName(); 449 if (displayName == null) { 450 // No display name: this must be the configuration used by default 451 // for the view which is originally displayed (before adding thumbnails), 452 // and you've switched away to something else; now we need to display a name 453 // for this original configuration. For now, just call it "Original" 454 return "Original"; 455 } 456 457 return displayName; 458 } 459 460 return mDisplayName; 461 } 462 463 /** 464 * Sets the display name of this preview. By default, the display name is 465 * the display name of the configuration, but it can be overridden by calling 466 * this setter (which only sets the preview name, without editing the configuration.) 467 * 468 * @param displayName the new display name 469 */ setDisplayName(@onNull String displayName)470 public void setDisplayName(@NonNull String displayName) { 471 mDisplayName = displayName; 472 } 473 474 /** 475 * Sets an inclusion context to use for this layout, if any. This will render 476 * the configuration preview as the outer layout with the current layout 477 * embedded within. 478 * 479 * @param includedWithin a reference to a layout which includes this one 480 */ setIncludedWithin(Reference includedWithin)481 public void setIncludedWithin(Reference includedWithin) { 482 mIncludedWithin = includedWithin; 483 } 484 485 /** 486 * Request a new render after the given delay 487 * 488 * @param delay the delay to wait before starting the render job 489 */ render(long delay)490 public void render(long delay) { 491 Job job = mJob; 492 if (job != null) { 493 job.cancel(); 494 } 495 if (RENDER_ASYNC) { 496 job = new AsyncRenderJob(); 497 } else { 498 job = new RenderJob(); 499 } 500 job.schedule(delay); 501 job.addJobChangeListener(this); 502 mJob = job; 503 } 504 505 /** Render immediately */ renderSync()506 private void renderSync() { 507 GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor(); 508 if (editor.getReadyLayoutLib(false /*displayError*/) == null) { 509 // Don't attempt to render when there is no ready layout library: most likely 510 // the targets are loading/reloading. 511 return; 512 } 513 514 disposeThumbnail(); 515 516 Configuration configuration = 517 mAlternateInput != null && mAlternateConfiguration != null 518 ? mAlternateConfiguration : mConfiguration; 519 ResourceResolver resolver = getResourceResolver(configuration); 520 RenderService renderService = RenderService.create(editor, configuration, resolver); 521 522 if (mIncludedWithin != null) { 523 renderService.setIncludedWithin(mIncludedWithin); 524 } 525 526 if (mAlternateInput != null) { 527 IAndroidTarget target = editor.getRenderingTarget(); 528 AndroidTargetData data = null; 529 if (target != null) { 530 Sdk sdk = Sdk.getCurrent(); 531 if (sdk != null) { 532 data = sdk.getTargetData(target); 533 } 534 } 535 536 // Construct UI model from XML 537 DocumentDescriptor documentDescriptor; 538 if (data == null) { 539 documentDescriptor = new DocumentDescriptor("temp", null);//$NON-NLS-1$ 540 } else { 541 documentDescriptor = data.getLayoutDescriptors().getDescriptor(); 542 } 543 UiDocumentNode model = (UiDocumentNode) documentDescriptor.createUiNode(); 544 model.setEditor(mCanvas.getEditorDelegate().getEditor()); 545 model.setUnknownDescriptorProvider(editor.getModel().getUnknownDescriptorProvider()); 546 547 Document document = DomUtilities.getDocument(mAlternateInput); 548 if (document == null) { 549 mError = "No document"; 550 createErrorThumbnail(); 551 return; 552 } 553 model.loadFromXmlNode(document); 554 renderService.setModel(model); 555 } else { 556 renderService.setModel(editor.getModel()); 557 } 558 RenderLogger log = editor.createRenderLogger(getDisplayName()); 559 renderService.setLog(log); 560 RenderSession session = renderService.createRenderSession(); 561 Result render = session.render(1000); 562 563 if (DUMP_RENDER_DIAGNOSTICS) { 564 if (log.hasProblems() || !render.isSuccess()) { 565 AdtPlugin.log(IStatus.ERROR, "Found problems rendering preview " 566 + getDisplayName() + ": " 567 + render.getErrorMessage() + " : " 568 + log.getProblems(false)); 569 Throwable exception = render.getException(); 570 if (exception != null) { 571 AdtPlugin.log(exception, "Failure rendering preview " + getDisplayName()); 572 } 573 } 574 } 575 576 if (render.isSuccess()) { 577 mError = null; 578 } else { 579 mError = render.getErrorMessage(); 580 if (mError == null) { 581 mError = ""; 582 } 583 } 584 585 if (render.getStatus() == Status.ERROR_TIMEOUT) { 586 // TODO: Special handling? schedule update again later 587 return; 588 } 589 if (render.isSuccess()) { 590 BufferedImage image = session.getImage(); 591 if (image != null) { 592 createThumbnail(image); 593 } 594 } 595 596 if (mError != null) { 597 createErrorThumbnail(); 598 } 599 } 600 getResourceResolver(Configuration configuration)601 private ResourceResolver getResourceResolver(Configuration configuration) { 602 ResourceResolver resourceResolver = mResourceResolver.get(); 603 if (resourceResolver != null) { 604 return resourceResolver; 605 } 606 607 GraphicalEditorPart graphicalEditor = mCanvas.getEditorDelegate().getGraphicalEditor(); 608 String theme = configuration.getTheme(); 609 if (theme == null) { 610 return null; 611 } 612 613 Map<ResourceType, Map<String, ResourceValue>> configuredFrameworkRes = null; 614 Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes = null; 615 616 FolderConfiguration config = configuration.getFullConfig(); 617 IAndroidTarget target = graphicalEditor.getRenderingTarget(); 618 ResourceRepository frameworkRes = null; 619 if (target != null) { 620 Sdk sdk = Sdk.getCurrent(); 621 if (sdk == null) { 622 return null; 623 } 624 AndroidTargetData data = sdk.getTargetData(target); 625 626 if (data != null) { 627 // TODO: SHARE if possible 628 frameworkRes = data.getFrameworkResources(); 629 configuredFrameworkRes = frameworkRes.getConfiguredResources(config); 630 } else { 631 return null; 632 } 633 } else { 634 return null; 635 } 636 assert configuredFrameworkRes != null; 637 638 639 // get the resources of the file's project. 640 ProjectResources projectRes = ResourceManager.getInstance().getProjectResources( 641 graphicalEditor.getProject()); 642 configuredProjectRes = projectRes.getConfiguredResources(config); 643 644 if (!theme.startsWith(PREFIX_RESOURCE_REF)) { 645 if (frameworkRes.hasResourceItem(ANDROID_STYLE_RESOURCE_PREFIX + theme)) { 646 theme = ANDROID_STYLE_RESOURCE_PREFIX + theme; 647 } else { 648 theme = STYLE_RESOURCE_PREFIX + theme; 649 } 650 } 651 652 resourceResolver = ResourceResolver.create( 653 configuredProjectRes, configuredFrameworkRes, 654 ResourceHelper.styleToTheme(theme), 655 ResourceHelper.isProjectStyle(theme)); 656 mResourceResolver = new SoftReference<ResourceResolver>(resourceResolver); 657 return resourceResolver; 658 } 659 660 /** 661 * Sets the new image of the preview and generates a thumbnail 662 * 663 * @param image the full size image 664 */ createThumbnail(BufferedImage image)665 void createThumbnail(BufferedImage image) { 666 if (image == null) { 667 mThumbnail = null; 668 return; 669 } 670 671 ImageOverlay imageOverlay = mCanvas.getImageOverlay(); 672 boolean drawShadows = imageOverlay == null || imageOverlay.getShowDropShadow(); 673 double scale = getWidth() / (double) image.getWidth(); 674 int shadowSize; 675 if (LARGE_SHADOWS) { 676 shadowSize = drawShadows ? SHADOW_SIZE : 0; 677 } else { 678 shadowSize = drawShadows ? SMALL_SHADOW_SIZE : 0; 679 } 680 if (scale < 1.0) { 681 if (LARGE_SHADOWS) { 682 image = ImageUtils.scale(image, scale, scale, 683 shadowSize, shadowSize); 684 if (drawShadows) { 685 ImageUtils.drawRectangleShadow(image, 0, 0, 686 image.getWidth() - shadowSize, 687 image.getHeight() - shadowSize); 688 } 689 } else { 690 image = ImageUtils.scale(image, scale, scale, 691 shadowSize, shadowSize); 692 if (drawShadows) { 693 ImageUtils.drawSmallRectangleShadow(image, 0, 0, 694 image.getWidth() - shadowSize, 695 image.getHeight() - shadowSize); 696 } 697 } 698 } 699 700 mThumbnail = SwtUtils.convertToSwt(mCanvas.getDisplay(), image, 701 true /* transferAlpha */, -1); 702 } 703 createErrorThumbnail()704 void createErrorThumbnail() { 705 int shadowSize = LARGE_SHADOWS ? SHADOW_SIZE : SMALL_SHADOW_SIZE; 706 int width = getWidth(); 707 int height = getHeight(); 708 BufferedImage image = new BufferedImage(width + shadowSize, height + shadowSize, 709 BufferedImage.TYPE_INT_ARGB); 710 711 Graphics2D g = image.createGraphics(); 712 g.setColor(new java.awt.Color(0xfffbfcc6)); 713 g.fillRect(0, 0, width, height); 714 715 g.dispose(); 716 717 ImageOverlay imageOverlay = mCanvas.getImageOverlay(); 718 boolean drawShadows = imageOverlay == null || imageOverlay.getShowDropShadow(); 719 if (drawShadows) { 720 if (LARGE_SHADOWS) { 721 ImageUtils.drawRectangleShadow(image, 0, 0, 722 image.getWidth() - SHADOW_SIZE, 723 image.getHeight() - SHADOW_SIZE); 724 } else { 725 ImageUtils.drawSmallRectangleShadow(image, 0, 0, 726 image.getWidth() - SMALL_SHADOW_SIZE, 727 image.getHeight() - SMALL_SHADOW_SIZE); 728 } 729 } 730 731 mThumbnail = SwtUtils.convertToSwt(mCanvas.getDisplay(), image, 732 true /* transferAlpha */, -1); 733 } 734 getScale(int width, int height)735 private static double getScale(int width, int height) { 736 int maxWidth = RenderPreviewManager.getMaxWidth(); 737 int maxHeight = RenderPreviewManager.getMaxHeight(); 738 if (width > 0 && height > 0 739 && (width > maxWidth || height > maxHeight)) { 740 if (width >= height) { // landscape 741 return maxWidth / (double) width; 742 } else { // portrait 743 return maxHeight / (double) height; 744 } 745 } 746 747 return 1.0; 748 } 749 750 /** 751 * Returns the width of the preview, in pixels 752 * 753 * @return the width in pixels 754 */ getWidth()755 public int getWidth() { 756 return (int) (mWidth * mScale * RenderPreviewManager.getScale()); 757 } 758 759 /** 760 * Returns the height of the preview, in pixels 761 * 762 * @return the height in pixels 763 */ getHeight()764 public int getHeight() { 765 return (int) (mHeight * mScale * RenderPreviewManager.getScale()); 766 } 767 768 /** 769 * Handles clicks within the preview (x and y are positions relative within the 770 * preview 771 * 772 * @param x the x coordinate within the preview where the click occurred 773 * @param y the y coordinate within the preview where the click occurred 774 * @return true if this preview handled (and therefore consumed) the click 775 */ click(int x, int y)776 public boolean click(int x, int y) { 777 if (y >= mTitleHeight && y < mTitleHeight + HEADER_HEIGHT) { 778 int left = 0; 779 left += CLOSE_ICON_WIDTH; 780 if (x <= left) { 781 // Delete 782 mManager.deletePreview(this); 783 return true; 784 } 785 left += ZOOM_IN_ICON_WIDTH; 786 if (x <= left) { 787 // Zoom in 788 mScale = mScale * (1 / 0.5); 789 if (Math.abs(mScale-1.0) < 0.0001) { 790 mScale = 1.0; 791 } 792 793 render(0); 794 mManager.layout(true); 795 mCanvas.redraw(); 796 return true; 797 } 798 left += ZOOM_OUT_ICON_WIDTH; 799 if (x <= left) { 800 // Zoom out 801 mScale = mScale * (0.5 / 1); 802 if (Math.abs(mScale-1.0) < 0.0001) { 803 mScale = 1.0; 804 } 805 render(0); 806 807 mManager.layout(true); 808 mCanvas.redraw(); 809 return true; 810 } 811 left += EDIT_ICON_WIDTH; 812 if (x <= left) { 813 // Edit. For now, just rename 814 InputDialog d = new InputDialog( 815 AdtPlugin.getShell(), 816 "Rename Preview", // title 817 "Name:", 818 getDisplayName(), 819 null); 820 if (d.open() == Window.OK) { 821 String newName = d.getValue(); 822 mConfiguration.setDisplayName(newName); 823 if (mDescription != null) { 824 mManager.rename(mDescription, newName); 825 } 826 mCanvas.redraw(); 827 } 828 829 return true; 830 } 831 832 // Clicked anywhere else on header 833 // Perhaps open Edit dialog here? 834 } 835 836 mManager.switchTo(this); 837 return true; 838 } 839 840 /** 841 * Paints the preview at the given x/y position 842 * 843 * @param gc the graphics context to paint it into 844 * @param x the x coordinate to paint the preview at 845 * @param y the y coordinate to paint the preview at 846 */ paint(GC gc, int x, int y)847 void paint(GC gc, int x, int y) { 848 mTitleHeight = paintTitle(gc, x, y, true /*showFile*/); 849 y += mTitleHeight; 850 y += 2; 851 852 int width = getWidth(); 853 int height = getHeight(); 854 if (mThumbnail != null && mError == null) { 855 gc.drawImage(mThumbnail, x, y); 856 857 if (mActive) { 858 int oldWidth = gc.getLineWidth(); 859 gc.setLineWidth(3); 860 gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_LIST_SELECTION)); 861 gc.drawRectangle(x - 1, y - 1, width + 2, height + 2); 862 gc.setLineWidth(oldWidth); 863 } 864 } else if (mError != null) { 865 if (mThumbnail != null) { 866 gc.drawImage(mThumbnail, x, y); 867 } else { 868 gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_BORDER)); 869 gc.drawRectangle(x, y, width, height); 870 } 871 872 gc.setClipping(x, y, width, height); 873 Image icon = IconFactory.getInstance().getIcon("renderError"); //$NON-NLS-1$ 874 ImageData data = icon.getImageData(); 875 int prevAlpha = gc.getAlpha(); 876 int alpha = 96; 877 if (mThumbnail != null) { 878 alpha -= 32; 879 } 880 gc.setAlpha(alpha); 881 gc.drawImage(icon, x + (width - data.width) / 2, y + (height - data.height) / 2); 882 883 String msg = mError; 884 Density density = mConfiguration.getDensity(); 885 if (density == Density.TV || density == Density.LOW) { 886 msg = "Broken rendering library; unsupported DPI. Try using the SDK manager " + 887 "to get updated layout libraries."; 888 } 889 int charWidth = gc.getFontMetrics().getAverageCharWidth(); 890 int charsPerLine = (width - 10) / charWidth; 891 msg = SdkUtils.wrap(msg, charsPerLine, null); 892 gc.setAlpha(255); 893 gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_BLACK)); 894 gc.drawText(msg, x + 5, y + HEADER_HEIGHT, true); 895 gc.setAlpha(prevAlpha); 896 gc.setClipping((Region) null); 897 } else { 898 gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_BORDER)); 899 gc.drawRectangle(x, y, width, height); 900 901 Image icon = IconFactory.getInstance().getIcon("refreshPreview"); //$NON-NLS-1$ 902 ImageData data = icon.getImageData(); 903 int prevAlpha = gc.getAlpha(); 904 gc.setAlpha(96); 905 gc.drawImage(icon, x + (width - data.width) / 2, 906 y + (height - data.height) / 2); 907 gc.setAlpha(prevAlpha); 908 } 909 910 if (mActive) { 911 int left = x ; 912 int prevAlpha = gc.getAlpha(); 913 gc.setAlpha(208); 914 Color bg = mCanvas.getDisplay().getSystemColor(SWT.COLOR_WHITE); 915 gc.setBackground(bg); 916 gc.fillRectangle(left, y, x + width - left, HEADER_HEIGHT); 917 gc.setAlpha(prevAlpha); 918 919 y += 2; 920 921 // Paint icons 922 gc.drawImage(CLOSE_ICON, left, y); 923 left += CLOSE_ICON_WIDTH; 924 925 gc.drawImage(ZOOM_IN_ICON, left, y); 926 left += ZOOM_IN_ICON_WIDTH; 927 928 gc.drawImage(ZOOM_OUT_ICON, left, y); 929 left += ZOOM_OUT_ICON_WIDTH; 930 931 gc.drawImage(EDIT_ICON, left, y); 932 left += EDIT_ICON_WIDTH; 933 } 934 } 935 936 /** 937 * Paints the preview title at the given position (and returns the required 938 * height) 939 * 940 * @param gc the graphics context to paint into 941 * @param x the left edge of the preview rectangle 942 * @param y the top edge of the preview rectangle 943 */ paintTitle(GC gc, int x, int y, boolean showFile)944 private int paintTitle(GC gc, int x, int y, boolean showFile) { 945 String displayName = getDisplayName(); 946 return paintTitle(gc, x, y, showFile, displayName); 947 } 948 949 /** 950 * Paints the preview title at the given position (and returns the required 951 * height) 952 * 953 * @param gc the graphics context to paint into 954 * @param x the left edge of the preview rectangle 955 * @param y the top edge of the preview rectangle 956 * @param displayName the title string to be used 957 */ paintTitle(GC gc, int x, int y, boolean showFile, String displayName)958 int paintTitle(GC gc, int x, int y, boolean showFile, String displayName) { 959 int titleHeight = 0; 960 961 if (showFile && mIncludedWithin != null) { 962 if (mManager.getMode() != INCLUDES) { 963 displayName = "<include>"; 964 } else { 965 // Skip: just paint footer instead 966 displayName = null; 967 } 968 } 969 970 int width = getWidth(); 971 int labelTop = y + 1; 972 gc.setClipping(x, labelTop, width, 100); 973 974 // Use font height rather than extent height since we want two adjacent 975 // previews (which may have different display names and therefore end 976 // up with slightly different extent heights) to have identical title 977 // heights such that they are aligned identically 978 int fontHeight = gc.getFontMetrics().getHeight(); 979 980 if (displayName != null && displayName.length() > 0) { 981 gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_WHITE)); 982 Point extent = gc.textExtent(displayName); 983 int labelLeft = Math.max(x, x + (width - extent.x) / 2); 984 Image icon = null; 985 Locale locale = mConfiguration.getLocale(); 986 if (locale != null && (locale.hasLanguage() || locale.hasRegion()) 987 && (!(mConfiguration instanceof NestedConfiguration) 988 || ((NestedConfiguration) mConfiguration).isOverridingLocale())) { 989 icon = locale.getFlagImage(); 990 } 991 992 if (icon != null) { 993 int flagWidth = icon.getImageData().width; 994 int flagHeight = icon.getImageData().height; 995 labelLeft = Math.max(x + flagWidth / 2, labelLeft); 996 gc.drawImage(icon, labelLeft - flagWidth / 2 - 1, labelTop); 997 labelLeft += flagWidth / 2 + 1; 998 gc.drawText(displayName, labelLeft, 999 labelTop - (extent.y - flagHeight) / 2, true); 1000 } else { 1001 gc.drawText(displayName, labelLeft, labelTop, true); 1002 } 1003 1004 labelTop += extent.y; 1005 titleHeight += fontHeight; 1006 } 1007 1008 if (showFile && (mAlternateInput != null || mIncludedWithin != null)) { 1009 // Draw file flag, and parent folder name 1010 IFile file = mAlternateInput != null 1011 ? mAlternateInput : mIncludedWithin.getFile(); 1012 String fileName = file.getParent().getName() + File.separator 1013 + file.getName(); 1014 Point extent = gc.textExtent(fileName); 1015 Image icon = IconFactory.getInstance().getIcon("android_file"); //$NON-NLS-1$ 1016 int flagWidth = icon.getImageData().width; 1017 int flagHeight = icon.getImageData().height; 1018 1019 int labelLeft = Math.max(x, x + (width - extent.x - flagWidth - 1) / 2); 1020 1021 gc.drawImage(icon, labelLeft, labelTop); 1022 1023 gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_GRAY)); 1024 labelLeft += flagWidth + 1; 1025 labelTop -= (extent.y - flagHeight) / 2; 1026 gc.drawText(fileName, labelLeft, labelTop, true); 1027 1028 titleHeight += Math.max(titleHeight, icon.getImageData().height); 1029 } 1030 1031 gc.setClipping((Region) null); 1032 1033 return titleHeight; 1034 } 1035 1036 /** 1037 * Notifies that the preview's configuration has changed. 1038 * 1039 * @param flags the change flags, a bitmask corresponding to the 1040 * {@code CHANGE_} constants in {@link ConfigurationClient} 1041 */ configurationChanged(int flags)1042 public void configurationChanged(int flags) { 1043 if (!mVisible) { 1044 mDirty |= flags; 1045 return; 1046 } 1047 1048 if ((flags & MASK_RENDERING) != 0) { 1049 mResourceResolver.clear(); 1050 // Handle inheritance 1051 mConfiguration.syncFolderConfig(); 1052 updateForkStatus(); 1053 updateSize(); 1054 } 1055 1056 // Sanity check to make sure things are working correctly 1057 if (DEBUG) { 1058 RenderPreviewMode mode = mManager.getMode(); 1059 if (mode == DEFAULT) { 1060 assert mConfiguration instanceof VaryingConfiguration; 1061 VaryingConfiguration config = (VaryingConfiguration) mConfiguration; 1062 int alternateFlags = config.getAlternateFlags(); 1063 switch (alternateFlags) { 1064 case Configuration.CFG_DEVICE_STATE: { 1065 State configState = config.getDeviceState(); 1066 State chooserState = mManager.getChooser().getConfiguration() 1067 .getDeviceState(); 1068 assert configState != null && chooserState != null; 1069 assert !configState.getName().equals(chooserState.getName()) 1070 : configState.toString() + ':' + chooserState; 1071 1072 Device configDevice = config.getDevice(); 1073 Device chooserDevice = mManager.getChooser().getConfiguration() 1074 .getDevice(); 1075 assert configDevice != null && chooserDevice != null; 1076 assert configDevice == chooserDevice 1077 : configDevice.toString() + ':' + chooserDevice; 1078 1079 break; 1080 } 1081 case Configuration.CFG_DEVICE: { 1082 Device configDevice = config.getDevice(); 1083 Device chooserDevice = mManager.getChooser().getConfiguration() 1084 .getDevice(); 1085 assert configDevice != null && chooserDevice != null; 1086 assert configDevice != chooserDevice 1087 : configDevice.toString() + ':' + chooserDevice; 1088 1089 State configState = config.getDeviceState(); 1090 State chooserState = mManager.getChooser().getConfiguration() 1091 .getDeviceState(); 1092 assert configState != null && chooserState != null; 1093 assert configState.getName().equals(chooserState.getName()) 1094 : configState.toString() + ':' + chooserState; 1095 1096 break; 1097 } 1098 case Configuration.CFG_LOCALE: { 1099 Locale configLocale = config.getLocale(); 1100 Locale chooserLocale = mManager.getChooser().getConfiguration() 1101 .getLocale(); 1102 assert configLocale != null && chooserLocale != null; 1103 assert configLocale != chooserLocale 1104 : configLocale.toString() + ':' + chooserLocale; 1105 break; 1106 } 1107 default: { 1108 // Some other type of override I didn't anticipate 1109 assert false : alternateFlags; 1110 } 1111 } 1112 } 1113 } 1114 1115 mDirty = 0; 1116 mManager.scheduleRender(this); 1117 } 1118 updateSize()1119 private void updateSize() { 1120 Device device = mConfiguration.getDevice(); 1121 if (device == null) { 1122 return; 1123 } 1124 Screen screen = device.getDefaultHardware().getScreen(); 1125 if (screen == null) { 1126 return; 1127 } 1128 1129 FolderConfiguration folderConfig = mConfiguration.getFullConfig(); 1130 ScreenOrientationQualifier qualifier = folderConfig.getScreenOrientationQualifier(); 1131 ScreenOrientation orientation = qualifier == null 1132 ? ScreenOrientation.PORTRAIT : qualifier.getValue(); 1133 1134 // compute width and height to take orientation into account. 1135 int x = screen.getXDimension(); 1136 int y = screen.getYDimension(); 1137 int screenWidth, screenHeight; 1138 1139 if (x > y) { 1140 if (orientation == ScreenOrientation.LANDSCAPE) { 1141 screenWidth = x; 1142 screenHeight = y; 1143 } else { 1144 screenWidth = y; 1145 screenHeight = x; 1146 } 1147 } else { 1148 if (orientation == ScreenOrientation.LANDSCAPE) { 1149 screenWidth = y; 1150 screenHeight = x; 1151 } else { 1152 screenWidth = x; 1153 screenHeight = y; 1154 } 1155 } 1156 1157 int width = RenderPreviewManager.getMaxWidth(); 1158 int height = RenderPreviewManager.getMaxHeight(); 1159 if (screenWidth > 0) { 1160 double scale = getScale(screenWidth, screenHeight); 1161 width = (int) (screenWidth * scale); 1162 height = (int) (screenHeight * scale); 1163 } 1164 1165 if (width != mWidth || height != mHeight) { 1166 mWidth = width; 1167 mHeight = height; 1168 1169 Image thumbnail = mThumbnail; 1170 mThumbnail = null; 1171 if (thumbnail != null) { 1172 thumbnail.dispose(); 1173 } 1174 if (mHeight != 0) { 1175 mAspectRatio = mWidth / (double) mHeight; 1176 } 1177 } 1178 } 1179 1180 /** 1181 * Returns the configuration associated with this preview 1182 * 1183 * @return the configuration 1184 */ 1185 @NonNull getConfiguration()1186 public Configuration getConfiguration() { 1187 return mConfiguration; 1188 } 1189 1190 // ---- Implements IJobChangeListener ---- 1191 1192 @Override aboutToRun(IJobChangeEvent event)1193 public void aboutToRun(IJobChangeEvent event) { 1194 } 1195 1196 @Override awake(IJobChangeEvent event)1197 public void awake(IJobChangeEvent event) { 1198 } 1199 1200 @Override done(IJobChangeEvent event)1201 public void done(IJobChangeEvent event) { 1202 mJob = null; 1203 } 1204 1205 @Override running(IJobChangeEvent event)1206 public void running(IJobChangeEvent event) { 1207 } 1208 1209 @Override scheduled(IJobChangeEvent event)1210 public void scheduled(IJobChangeEvent event) { 1211 } 1212 1213 @Override sleeping(IJobChangeEvent event)1214 public void sleeping(IJobChangeEvent event) { 1215 } 1216 1217 // ---- Delayed Rendering ---- 1218 1219 private final class RenderJob extends UIJob { RenderJob()1220 public RenderJob() { 1221 super("RenderPreview"); 1222 setSystem(true); 1223 setUser(false); 1224 } 1225 1226 @Override runInUIThread(IProgressMonitor monitor)1227 public IStatus runInUIThread(IProgressMonitor monitor) { 1228 mJob = null; 1229 if (!mCanvas.isDisposed()) { 1230 renderSync(); 1231 mCanvas.redraw(); 1232 return org.eclipse.core.runtime.Status.OK_STATUS; 1233 } 1234 1235 return org.eclipse.core.runtime.Status.CANCEL_STATUS; 1236 } 1237 1238 @Override getDisplay()1239 public Display getDisplay() { 1240 if (mCanvas.isDisposed()) { 1241 return null; 1242 } 1243 return mCanvas.getDisplay(); 1244 } 1245 } 1246 1247 private final class AsyncRenderJob extends Job { AsyncRenderJob()1248 public AsyncRenderJob() { 1249 super("RenderPreview"); 1250 setSystem(true); 1251 setUser(false); 1252 } 1253 1254 @Override run(IProgressMonitor monitor)1255 protected IStatus run(IProgressMonitor monitor) { 1256 mJob = null; 1257 1258 if (mCanvas.isDisposed()) { 1259 return org.eclipse.core.runtime.Status.CANCEL_STATUS; 1260 } 1261 1262 renderSync(); 1263 1264 // Update display 1265 mCanvas.getDisplay().asyncExec(new Runnable() { 1266 @Override 1267 public void run() { 1268 mCanvas.redraw(); 1269 } 1270 }); 1271 1272 return org.eclipse.core.runtime.Status.OK_STATUS; 1273 } 1274 } 1275 1276 /** 1277 * Sets the input file to use for rendering. If not set, this will just be 1278 * the same file as the configuration chooser. This is used to render other 1279 * layouts, such as variations of the currently edited layout, which are 1280 * not kept in sync with the main layout. 1281 * 1282 * @param file the file to set as input 1283 */ setAlternateInput(@ullable IFile file)1284 public void setAlternateInput(@Nullable IFile file) { 1285 mAlternateInput = file; 1286 } 1287 1288 /** Corresponding description for this preview if it is a manually added preview */ 1289 private @Nullable ConfigurationDescription mDescription; 1290 1291 /** 1292 * Sets the description of this preview, if this preview is a manually added preview 1293 * 1294 * @param description the description of this preview 1295 */ setDescription(@ullable ConfigurationDescription description)1296 public void setDescription(@Nullable ConfigurationDescription description) { 1297 mDescription = description; 1298 } 1299 1300 /** 1301 * Returns the description of this preview, if this preview is a manually added preview 1302 * 1303 * @return the description 1304 */ 1305 @Nullable getDescription()1306 public ConfigurationDescription getDescription() { 1307 return mDescription; 1308 } 1309 1310 @Override toString()1311 public String toString() { 1312 return getDisplayName() + ':' + mConfiguration; 1313 } 1314 1315 /** Sorts render previews into increasing aspect ratio order */ 1316 static Comparator<RenderPreview> INCREASING_ASPECT_RATIO = new Comparator<RenderPreview>() { 1317 @Override 1318 public int compare(RenderPreview preview1, RenderPreview preview2) { 1319 return (int) Math.signum(preview1.mAspectRatio - preview2.mAspectRatio); 1320 } 1321 }; 1322 /** Sorts render previews into visual order: row by row, column by column */ 1323 static Comparator<RenderPreview> VISUAL_ORDER = new Comparator<RenderPreview>() { 1324 @Override 1325 public int compare(RenderPreview preview1, RenderPreview preview2) { 1326 int delta = preview1.mY - preview2.mY; 1327 if (delta == 0) { 1328 delta = preview1.mX - preview2.mX; 1329 } 1330 return delta; 1331 } 1332 }; 1333 } 1334