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 17 package com.android.ide.eclipse.adt.internal.editors.layout.gle2; 18 19 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE; 20 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE_STATE; 21 import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.MASK_ALL; 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.RenderPreview.LARGE_SHADOWS; 25 import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.CUSTOM; 26 import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.NONE; 27 import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.SCREENS; 28 29 import com.android.annotations.NonNull; 30 import com.android.annotations.Nullable; 31 import com.android.ide.common.api.Rect; 32 import com.android.ide.common.rendering.api.Capability; 33 import com.android.ide.common.resources.configuration.DensityQualifier; 34 import com.android.ide.common.resources.configuration.DeviceConfigHelper; 35 import com.android.ide.common.resources.configuration.FolderConfiguration; 36 import com.android.ide.common.resources.configuration.LocaleQualifier; 37 import com.android.ide.common.resources.configuration.ScreenSizeQualifier; 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.common.CommonXmlEditor; 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.preferences.AdtPrefs; 51 import com.android.resources.Density; 52 import com.android.resources.ScreenSize; 53 import com.android.sdklib.devices.Device; 54 import com.android.sdklib.devices.Screen; 55 import com.android.sdklib.devices.State; 56 import com.google.common.collect.Lists; 57 58 import org.eclipse.core.resources.IFile; 59 import org.eclipse.core.resources.IProject; 60 import org.eclipse.jface.dialogs.InputDialog; 61 import org.eclipse.jface.window.Window; 62 import org.eclipse.swt.SWT; 63 import org.eclipse.swt.events.SelectionEvent; 64 import org.eclipse.swt.events.SelectionListener; 65 import org.eclipse.swt.graphics.GC; 66 import org.eclipse.swt.graphics.Image; 67 import org.eclipse.swt.graphics.Rectangle; 68 import org.eclipse.swt.widgets.ScrollBar; 69 import org.eclipse.ui.IWorkbenchPartSite; 70 import org.eclipse.ui.PartInitException; 71 import org.eclipse.ui.ide.IDE; 72 73 import java.io.IOException; 74 import java.util.ArrayList; 75 import java.util.Collection; 76 import java.util.Collections; 77 import java.util.Comparator; 78 import java.util.HashSet; 79 import java.util.Iterator; 80 import java.util.List; 81 import java.util.Set; 82 83 /** 84 * Manager for the configuration previews, which handles layout computations, 85 * managing the image buffer cache, etc 86 */ 87 public class RenderPreviewManager { 88 private static double sScale = 1.0; 89 private static final int RENDER_DELAY = 150; 90 private static final int PREVIEW_VGAP = 18; 91 private static final int PREVIEW_HGAP = 12; 92 private static final int MAX_WIDTH = 200; 93 private static final int MAX_HEIGHT = MAX_WIDTH; 94 private static final int ZOOM_ICON_WIDTH = 16; 95 private static final int ZOOM_ICON_HEIGHT = 16; 96 private @Nullable List<RenderPreview> mPreviews; 97 private @Nullable RenderPreviewList mManualList; 98 private final @NonNull LayoutCanvas mCanvas; 99 private final @NonNull CanvasTransform mVScale; 100 private final @NonNull CanvasTransform mHScale; 101 private int mPrevCanvasWidth; 102 private int mPrevCanvasHeight; 103 private int mPrevImageWidth; 104 private int mPrevImageHeight; 105 private @NonNull RenderPreviewMode mMode = NONE; 106 private @Nullable RenderPreview mActivePreview; 107 private @Nullable ScrollBarListener mListener; 108 private int mLayoutHeight; 109 /** Last seen state revision in this {@link RenderPreviewManager}. If less 110 * than {@link #sRevision}, the previews need to be updated on next exposure */ 111 private static int mRevision; 112 /** Current global revision count */ 113 private static int sRevision; 114 private boolean mNeedLayout; 115 private boolean mNeedRender; 116 private boolean mNeedZoom; 117 private SwapAnimation mAnimation; 118 119 /** 120 * Creates a {@link RenderPreviewManager} associated with the given canvas 121 * 122 * @param canvas the canvas to manage previews for 123 */ 124 public RenderPreviewManager(@NonNull LayoutCanvas canvas) { 125 mCanvas = canvas; 126 mHScale = canvas.getHorizontalTransform(); 127 mVScale = canvas.getVerticalTransform(); 128 } 129 130 /** 131 * Revise the global state revision counter. This will cause all layout 132 * preview managers to refresh themselves to the latest revision when they 133 * are next exposed. 134 */ 135 public static void bumpRevision() { 136 sRevision++; 137 } 138 139 /** 140 * Returns the associated chooser 141 * 142 * @return the associated chooser 143 */ 144 @NonNull 145 ConfigurationChooser getChooser() { 146 GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor(); 147 return editor.getConfigurationChooser(); 148 } 149 150 /** 151 * Returns the associated canvas 152 * 153 * @return the canvas 154 */ 155 @NonNull 156 public LayoutCanvas getCanvas() { 157 return mCanvas; 158 } 159 160 /** Zooms in (grows all previews) */ 161 public void zoomIn() { 162 sScale = sScale * (1 / 0.9); 163 if (Math.abs(sScale-1.0) < 0.0001) { 164 sScale = 1.0; 165 } 166 167 updatedZoom(); 168 } 169 170 /** Zooms out (shrinks all previews) */ 171 public void zoomOut() { 172 sScale = sScale * (0.9 / 1); 173 if (Math.abs(sScale-1.0) < 0.0001) { 174 sScale = 1.0; 175 } 176 updatedZoom(); 177 } 178 179 /** Zooms to 100 (resets zoom) */ 180 public void zoomReset() { 181 sScale = 1.0; 182 updatedZoom(); 183 mNeedZoom = mNeedLayout = true; 184 mCanvas.redraw(); 185 } 186 187 private void updatedZoom() { 188 if (hasPreviews()) { 189 for (RenderPreview preview : mPreviews) { 190 preview.disposeThumbnail(); 191 } 192 RenderPreview preview = mCanvas.getPreview(); 193 if (preview != null) { 194 preview.disposeThumbnail(); 195 } 196 } 197 198 mNeedLayout = mNeedRender = true; 199 mCanvas.redraw(); 200 } 201 202 static int getMaxWidth() { 203 return (int) (sScale * MAX_WIDTH); 204 } 205 206 static int getMaxHeight() { 207 return (int) (sScale * MAX_HEIGHT); 208 } 209 210 static double getScale() { 211 return sScale; 212 } 213 214 /** 215 * Returns whether there are any manual preview items (provided the current 216 * mode is manual previews 217 * 218 * @return true if there are items in the manual preview list 219 */ 220 public boolean hasManualPreviews() { 221 assert mMode == CUSTOM; 222 return mManualList != null && !mManualList.isEmpty(); 223 } 224 225 /** Delete all the previews */ 226 public void deleteManualPreviews() { 227 disposePreviews(); 228 selectMode(NONE); 229 mCanvas.setFitScale(true /* onlyZoomOut */, true /*allowZoomIn*/); 230 231 if (mManualList != null) { 232 mManualList.delete(); 233 } 234 } 235 236 /** Dispose all the previews */ 237 public void disposePreviews() { 238 if (mPreviews != null) { 239 List<RenderPreview> old = mPreviews; 240 mPreviews = null; 241 for (RenderPreview preview : old) { 242 preview.dispose(); 243 } 244 } 245 } 246 247 /** 248 * Deletes the given preview 249 * 250 * @param preview the preview to be deleted 251 */ 252 public void deletePreview(RenderPreview preview) { 253 mPreviews.remove(preview); 254 preview.dispose(); 255 layout(true); 256 mCanvas.redraw(); 257 258 if (mManualList != null) { 259 mManualList.remove(preview); 260 saveList(); 261 } 262 } 263 264 /** 265 * Compute the total width required for the previews, including internal padding 266 * 267 * @return total width in pixels 268 */ 269 public int computePreviewWidth() { 270 int maxPreviewWidth = 0; 271 if (hasPreviews()) { 272 for (RenderPreview preview : mPreviews) { 273 maxPreviewWidth = Math.max(maxPreviewWidth, preview.getWidth()); 274 } 275 276 if (maxPreviewWidth > 0) { 277 maxPreviewWidth += 2 * PREVIEW_HGAP; // 2x for left and right side 278 maxPreviewWidth += LARGE_SHADOWS ? SHADOW_SIZE : SMALL_SHADOW_SIZE; 279 } 280 281 return maxPreviewWidth; 282 } 283 284 return 0; 285 } 286 287 /** 288 * Layout Algorithm. This sets the {@link RenderPreview#getX()} and 289 * {@link RenderPreview#getY()} coordinates of all the previews. It also 290 * marks previews as visible or invisible via 291 * {@link RenderPreview#setVisible(boolean)} according to their position and 292 * the current visible view port in the layout canvas. Finally, it also sets 293 * the {@code mLayoutHeight} field, such that the scrollbars can compute the 294 * right scrolled area, and that scrolling can cause render refreshes on 295 * views that are made visible. 296 * <p> 297 * This is not a traditional bin packing problem, because the objects to be 298 * packaged do not have a fixed size; we can scale them up and down in order 299 * to provide an "optimal" size. 300 * <p> 301 * See http://en.wikipedia.org/wiki/Packing_problem See 302 * http://en.wikipedia.org/wiki/Bin_packing_problem 303 */ 304 void layout(boolean refresh) { 305 mNeedLayout = false; 306 307 if (mPreviews == null || mPreviews.isEmpty()) { 308 return; 309 } 310 311 int scaledImageWidth = mHScale.getScaledImgSize(); 312 int scaledImageHeight = mVScale.getScaledImgSize(); 313 Rectangle clientArea = mCanvas.getClientArea(); 314 315 if (!refresh && 316 (scaledImageWidth == mPrevImageWidth 317 && scaledImageHeight == mPrevImageHeight 318 && clientArea.width == mPrevCanvasWidth 319 && clientArea.height == mPrevCanvasHeight)) { 320 // No change 321 return; 322 } 323 324 mPrevImageWidth = scaledImageWidth; 325 mPrevImageHeight = scaledImageHeight; 326 mPrevCanvasWidth = clientArea.width; 327 mPrevCanvasHeight = clientArea.height; 328 329 if (mListener == null) { 330 mListener = new ScrollBarListener(); 331 mCanvas.getVerticalBar().addSelectionListener(mListener); 332 } 333 334 beginRenderScheduling(); 335 336 mLayoutHeight = 0; 337 338 if (previewsHaveIdenticalSize() || fixedOrder()) { 339 // If all the preview boxes are of identical sizes, or if the order is predetermined, 340 // just lay them out in rows. 341 rowLayout(); 342 } else if (previewsFit()) { 343 layoutFullFit(); 344 } else { 345 rowLayout(); 346 } 347 348 mCanvas.updateScrollBars(); 349 } 350 351 /** 352 * Performs a simple layout where the views are laid out in a row, wrapping 353 * around the top left canvas image. 354 */ 355 private void rowLayout() { 356 // TODO: Separate layout heuristics for portrait and landscape orientations (though 357 // it also depends on the dimensions of the canvas window, which determines the 358 // shape of the leftover space) 359 360 int scaledImageWidth = mHScale.getScaledImgSize(); 361 int scaledImageHeight = mVScale.getScaledImgSize(); 362 Rectangle clientArea = mCanvas.getClientArea(); 363 364 int availableWidth = clientArea.x + clientArea.width - getX(); 365 int availableHeight = clientArea.y + clientArea.height - getY(); 366 int maxVisibleY = clientArea.y + clientArea.height; 367 368 int bottomBorder = scaledImageHeight; 369 int rightHandSide = scaledImageWidth + PREVIEW_HGAP; 370 int nextY = 0; 371 372 // First lay out images across the top right hand side 373 int x = rightHandSide; 374 int y = 0; 375 boolean wrapped = false; 376 377 int vgap = PREVIEW_VGAP; 378 for (RenderPreview preview : mPreviews) { 379 // If we have forked previews, double the vgap to allow space for two labels 380 if (preview.isForked()) { 381 vgap *= 2; 382 break; 383 } 384 } 385 386 List<RenderPreview> aspectOrder; 387 if (!fixedOrder()) { 388 aspectOrder = new ArrayList<RenderPreview>(mPreviews); 389 Collections.sort(aspectOrder, RenderPreview.INCREASING_ASPECT_RATIO); 390 } else { 391 aspectOrder = mPreviews; 392 } 393 394 for (RenderPreview preview : aspectOrder) { 395 if (x > 0 && x + preview.getWidth() > availableWidth) { 396 x = rightHandSide; 397 int prevY = y; 398 y = nextY; 399 if ((prevY <= bottomBorder || 400 y <= bottomBorder) 401 && Math.max(nextY, y + preview.getHeight()) > bottomBorder) { 402 // If there's really no visible room below, don't bother 403 // Similarly, don't wrap individually scaled views 404 if (bottomBorder < availableHeight - 40 && preview.getScale() < 1.2) { 405 // If it's closer to the top row than the bottom, just 406 // mark the next row for left justify instead 407 if (bottomBorder - y > y + preview.getHeight() - bottomBorder) { 408 rightHandSide = 0; 409 wrapped = true; 410 } else if (!wrapped) { 411 y = nextY = Math.max(nextY, bottomBorder + vgap); 412 x = rightHandSide = 0; 413 wrapped = true; 414 } 415 } 416 } 417 } 418 if (x > 0 && y <= bottomBorder 419 && Math.max(nextY, y + preview.getHeight()) > bottomBorder) { 420 if (clientArea.height - bottomBorder < preview.getHeight()) { 421 // No room below the device on the left; just continue on the 422 // bottom row 423 } else if (preview.getScale() < 1.2) { 424 if (bottomBorder - y > y + preview.getHeight() - bottomBorder) { 425 rightHandSide = 0; 426 wrapped = true; 427 } else { 428 y = nextY = Math.max(nextY, bottomBorder + vgap); 429 x = rightHandSide = 0; 430 wrapped = true; 431 } 432 } 433 } 434 435 preview.setPosition(x, y); 436 437 if (y > maxVisibleY && maxVisibleY > 0) { 438 preview.setVisible(false); 439 } else if (!preview.isVisible()) { 440 preview.setVisible(true); 441 } 442 443 x += preview.getWidth(); 444 x += PREVIEW_HGAP; 445 nextY = Math.max(nextY, y + preview.getHeight() + vgap); 446 } 447 448 mLayoutHeight = nextY; 449 } 450 451 private boolean fixedOrder() { 452 return mMode == SCREENS; 453 } 454 455 /** Returns true if all the previews have the same identical size */ 456 private boolean previewsHaveIdenticalSize() { 457 if (!hasPreviews()) { 458 return true; 459 } 460 461 Iterator<RenderPreview> iterator = mPreviews.iterator(); 462 RenderPreview first = iterator.next(); 463 int width = first.getWidth(); 464 int height = first.getHeight(); 465 466 while (iterator.hasNext()) { 467 RenderPreview preview = iterator.next(); 468 if (width != preview.getWidth() || height != preview.getHeight()) { 469 return false; 470 } 471 } 472 473 return true; 474 } 475 476 /** Returns true if all the previews can fully fit in the available space */ 477 private boolean previewsFit() { 478 int scaledImageWidth = mHScale.getScaledImgSize(); 479 int scaledImageHeight = mVScale.getScaledImgSize(); 480 Rectangle clientArea = mCanvas.getClientArea(); 481 int availableWidth = clientArea.x + clientArea.width - getX(); 482 int availableHeight = clientArea.y + clientArea.height - getY(); 483 int bottomBorder = scaledImageHeight; 484 int rightHandSide = scaledImageWidth + PREVIEW_HGAP; 485 486 // First see if we can fit everything; if so, we can try to make the layouts 487 // larger such that they fill up all the available space 488 long availableArea = rightHandSide * bottomBorder + 489 availableWidth * (Math.max(0, availableHeight - bottomBorder)); 490 491 long requiredArea = 0; 492 for (RenderPreview preview : mPreviews) { 493 // Note: This does not include individual preview scale; the layout 494 // algorithm itself may be tweaking the scales to fit elements within 495 // the layout 496 requiredArea += preview.getArea(); 497 } 498 499 return requiredArea * sScale < availableArea; 500 } 501 502 private void layoutFullFit() { 503 int scaledImageWidth = mHScale.getScaledImgSize(); 504 int scaledImageHeight = mVScale.getScaledImgSize(); 505 Rectangle clientArea = mCanvas.getClientArea(); 506 int availableWidth = clientArea.x + clientArea.width - getX(); 507 int availableHeight = clientArea.y + clientArea.height - getY(); 508 int maxVisibleY = clientArea.y + clientArea.height; 509 int bottomBorder = scaledImageHeight; 510 int rightHandSide = scaledImageWidth + PREVIEW_HGAP; 511 512 int minWidth = Integer.MAX_VALUE; 513 int minHeight = Integer.MAX_VALUE; 514 for (RenderPreview preview : mPreviews) { 515 minWidth = Math.min(minWidth, preview.getWidth()); 516 minHeight = Math.min(minHeight, preview.getHeight()); 517 } 518 519 BinPacker packer = new BinPacker(minWidth, minHeight); 520 521 // TODO: Instead of this, just start with client area and occupy scaled image size! 522 523 // Add in gap on right and bottom since we'll add that requirement on the width and 524 // height rectangles too (for spacing) 525 packer.addSpace(new Rect(rightHandSide, 0, 526 availableWidth - rightHandSide + PREVIEW_HGAP, 527 availableHeight + PREVIEW_VGAP)); 528 if (maxVisibleY > bottomBorder) { 529 packer.addSpace(new Rect(0, bottomBorder + PREVIEW_VGAP, 530 availableWidth + PREVIEW_HGAP, maxVisibleY - bottomBorder + PREVIEW_VGAP)); 531 } 532 533 // TODO: Sort previews first before attempting to position them? 534 535 ArrayList<RenderPreview> aspectOrder = new ArrayList<RenderPreview>(mPreviews); 536 Collections.sort(aspectOrder, RenderPreview.INCREASING_ASPECT_RATIO); 537 538 for (RenderPreview preview : aspectOrder) { 539 int previewWidth = preview.getWidth(); 540 int previewHeight = preview.getHeight(); 541 previewHeight += PREVIEW_VGAP; 542 if (preview.isForked()) { 543 previewHeight += PREVIEW_VGAP; 544 } 545 previewWidth += PREVIEW_HGAP; 546 // title height? how do I account for that? 547 Rect position = packer.occupy(previewWidth, previewHeight); 548 if (position != null) { 549 preview.setPosition(position.x, position.y); 550 preview.setVisible(true); 551 } else { 552 // Can't fit: give up and do plain row layout 553 rowLayout(); 554 return; 555 } 556 } 557 558 mLayoutHeight = availableHeight; 559 } 560 /** 561 * Paints the configuration previews 562 * 563 * @param gc the graphics context to paint into 564 */ 565 void paint(GC gc) { 566 if (hasPreviews()) { 567 // Ensure up to date at all times; consider moving if it's too expensive 568 layout(mNeedLayout); 569 if (mNeedRender) { 570 renderPreviews(); 571 } 572 if (mNeedZoom) { 573 boolean allowZoomIn = true /*mMode == NONE*/; 574 mCanvas.setFitScale(false /*onlyZoomOut*/, allowZoomIn); 575 mNeedZoom = false; 576 } 577 int rootX = getX(); 578 int rootY = getY(); 579 580 for (RenderPreview preview : mPreviews) { 581 if (preview.isVisible()) { 582 int x = rootX + preview.getX(); 583 int y = rootY + preview.getY(); 584 preview.paint(gc, x, y); 585 } 586 } 587 588 RenderPreview preview = mCanvas.getPreview(); 589 if (preview != null) { 590 String displayName = null; 591 Configuration configuration = preview.getConfiguration(); 592 if (configuration instanceof VaryingConfiguration) { 593 // Use override flags from stashed preview, but configuration 594 // data from live (not varying) configured configuration 595 VaryingConfiguration cfg = (VaryingConfiguration) configuration; 596 int flags = cfg.getAlternateFlags() | cfg.getOverrideFlags(); 597 displayName = NestedConfiguration.computeDisplayName(flags, 598 getChooser().getConfiguration()); 599 } else if (configuration instanceof NestedConfiguration) { 600 int flags = ((NestedConfiguration) configuration).getOverrideFlags(); 601 displayName = NestedConfiguration.computeDisplayName(flags, 602 getChooser().getConfiguration()); 603 } else { 604 displayName = configuration.getDisplayName(); 605 } 606 if (displayName != null) { 607 CanvasTransform hi = mHScale; 608 CanvasTransform vi = mVScale; 609 610 int destX = hi.translate(0); 611 int destY = vi.translate(0); 612 int destWidth = hi.getScaledImgSize(); 613 int destHeight = vi.getScaledImgSize(); 614 615 int x = destX + destWidth / 2 - preview.getWidth() / 2; 616 int y = destY + destHeight; 617 618 preview.paintTitle(gc, x, y, false /*showFile*/, displayName); 619 } 620 } 621 622 // Zoom overlay 623 int x = getZoomX(); 624 if (x > 0) { 625 int y = getZoomY(); 626 int oldAlpha = gc.getAlpha(); 627 628 // Paint background oval rectangle behind the zoom and close icons 629 gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_GRAY)); 630 gc.setAlpha(128); 631 int padding = 3; 632 int arc = 5; 633 gc.fillRoundRectangle(x - padding, y - padding, 634 ZOOM_ICON_WIDTH + 2 * padding, 635 4 * ZOOM_ICON_HEIGHT + 2 * padding, arc, arc); 636 637 gc.setAlpha(255); 638 IconFactory iconFactory = IconFactory.getInstance(); 639 Image zoomOut = iconFactory.getIcon("zoomminus"); //$NON-NLS-1$); 640 Image zoomIn = iconFactory.getIcon("zoomplus"); //$NON-NLS-1$); 641 Image zoom100 = iconFactory.getIcon("zoom100"); //$NON-NLS-1$); 642 Image close = iconFactory.getIcon("close"); //$NON-NLS-1$); 643 644 gc.drawImage(zoomIn, x, y); 645 y += ZOOM_ICON_HEIGHT; 646 gc.drawImage(zoomOut, x, y); 647 y += ZOOM_ICON_HEIGHT; 648 gc.drawImage(zoom100, x, y); 649 y += ZOOM_ICON_HEIGHT; 650 gc.drawImage(close, x, y); 651 y += ZOOM_ICON_HEIGHT; 652 gc.setAlpha(oldAlpha); 653 } 654 } else if (mMode == CUSTOM) { 655 int rootX = getX(); 656 rootX += mHScale.getScaledImgSize(); 657 rootX += 2 * PREVIEW_HGAP; 658 int rootY = getY(); 659 rootY += 20; 660 gc.setFont(mCanvas.getFont()); 661 gc.setForeground(mCanvas.getDisplay().getSystemColor(SWT.COLOR_BLACK)); 662 gc.drawText("Add previews with \"Add as Thumbnail\"\nin the configuration menu", 663 rootX, rootY, true); 664 } 665 666 if (mAnimation != null) { 667 mAnimation.tick(gc); 668 } 669 } 670 671 private void addPreview(@NonNull RenderPreview preview) { 672 if (mPreviews == null) { 673 mPreviews = Lists.newArrayList(); 674 } 675 mPreviews.add(preview); 676 } 677 678 /** Adds the current configuration as a new configuration preview */ 679 public void addAsThumbnail() { 680 ConfigurationChooser chooser = getChooser(); 681 String name = chooser.getConfiguration().getDisplayName(); 682 if (name == null || name.isEmpty()) { 683 name = getUniqueName(); 684 } 685 InputDialog d = new InputDialog( 686 AdtPlugin.getShell(), 687 "Add as Thumbnail Preview", // title 688 "Name of thumbnail:", 689 name, 690 null); 691 if (d.open() == Window.OK) { 692 selectMode(CUSTOM); 693 694 String newName = d.getValue(); 695 // Create a new configuration from the current settings in the composite 696 Configuration configuration = Configuration.copy(chooser.getConfiguration()); 697 configuration.setDisplayName(newName); 698 699 RenderPreview preview = RenderPreview.create(this, configuration); 700 addPreview(preview); 701 702 layout(true); 703 beginRenderScheduling(); 704 scheduleRender(preview); 705 mCanvas.setFitScale(true /* onlyZoomOut */, false /*allowZoomIn*/); 706 707 if (mManualList == null) { 708 loadList(); 709 } 710 if (mManualList != null) { 711 mManualList.add(preview); 712 saveList(); 713 } 714 } 715 } 716 717 /** 718 * Computes a unique new name for a configuration preview that represents 719 * the current, default configuration 720 * 721 * @return a unique name 722 */ 723 private String getUniqueName() { 724 if (mPreviews == null || mPreviews.isEmpty()) { 725 // NO, not for the first preview! 726 return "Config1"; 727 } 728 729 Set<String> names = new HashSet<String>(mPreviews.size()); 730 for (RenderPreview preview : mPreviews) { 731 names.add(preview.getDisplayName()); 732 } 733 734 int index = 2; 735 while (true) { 736 String name = String.format("Config%1$d", index); 737 if (!names.contains(name)) { 738 return name; 739 } 740 index++; 741 } 742 } 743 744 /** Generates a bunch of default configuration preview thumbnails */ 745 public void addDefaultPreviews() { 746 ConfigurationChooser chooser = getChooser(); 747 Configuration parent = chooser.getConfiguration(); 748 if (parent instanceof NestedConfiguration) { 749 parent = ((NestedConfiguration) parent).getParent(); 750 } 751 if (mCanvas.getImageOverlay().getImage() != null) { 752 // Create Language variation 753 createLocaleVariation(chooser, parent); 754 755 // Vary screen size 756 // TODO: Be smarter here: Pick a screen that is both as differently as possible 757 // from the current screen as well as also supported. So consider 758 // things like supported screens, targetSdk etc. 759 createScreenVariations(parent); 760 761 // Vary orientation 762 createStateVariation(chooser, parent); 763 764 // Vary render target 765 createRenderTargetVariation(chooser, parent); 766 } 767 768 // Also add in include-context previews, if any 769 addIncludedInPreviews(); 770 771 // Make a placeholder preview for the current screen, in case we switch from it 772 RenderPreview preview = RenderPreview.create(this, parent); 773 mCanvas.setPreview(preview); 774 775 sortPreviewsByOrientation(); 776 } 777 778 private void createRenderTargetVariation(ConfigurationChooser chooser, Configuration parent) { 779 /* This is disabled for now: need to load multiple versions of layoutlib. 780 When I did this, there seemed to be some drug interactions between 781 them, and I would end up with NPEs in layoutlib code which normally works. 782 VaryingConfiguration configuration = 783 VaryingConfiguration.create(chooser, parent); 784 configuration.setAlternatingTarget(true); 785 configuration.syncFolderConfig(); 786 addPreview(RenderPreview.create(this, configuration)); 787 */ 788 } 789 790 private void createStateVariation(ConfigurationChooser chooser, Configuration parent) { 791 State currentState = parent.getDeviceState(); 792 State nextState = parent.getNextDeviceState(currentState); 793 if (nextState != currentState) { 794 VaryingConfiguration configuration = 795 VaryingConfiguration.create(chooser, parent); 796 configuration.setAlternateDeviceState(true); 797 configuration.syncFolderConfig(); 798 addPreview(RenderPreview.create(this, configuration)); 799 } 800 } 801 802 private void createLocaleVariation(ConfigurationChooser chooser, Configuration parent) { 803 LocaleQualifier currentLanguage = parent.getLocale().qualifier; 804 for (Locale locale : chooser.getLocaleList()) { 805 LocaleQualifier qualifier = locale.qualifier; 806 if (!qualifier.getLanguage().equals(currentLanguage.getLanguage())) { 807 VaryingConfiguration configuration = 808 VaryingConfiguration.create(chooser, parent); 809 configuration.setAlternateLocale(true); 810 configuration.syncFolderConfig(); 811 addPreview(RenderPreview.create(this, configuration)); 812 break; 813 } 814 } 815 } 816 817 private void createScreenVariations(Configuration parent) { 818 ConfigurationChooser chooser = getChooser(); 819 VaryingConfiguration configuration; 820 821 configuration = VaryingConfiguration.create(chooser, parent); 822 configuration.setVariation(0); 823 configuration.setAlternateDevice(true); 824 configuration.syncFolderConfig(); 825 addPreview(RenderPreview.create(this, configuration)); 826 827 configuration = VaryingConfiguration.create(chooser, parent); 828 configuration.setVariation(1); 829 configuration.setAlternateDevice(true); 830 configuration.syncFolderConfig(); 831 addPreview(RenderPreview.create(this, configuration)); 832 } 833 834 /** 835 * Returns the current mode as seen by this {@link RenderPreviewManager}. 836 * Note that it may not yet have been synced with the global mode kept in 837 * {@link AdtPrefs#getRenderPreviewMode()}. 838 * 839 * @return the current preview mode 840 */ 841 @NonNull 842 public RenderPreviewMode getMode() { 843 return mMode; 844 } 845 846 /** 847 * Update the set of previews for the current mode 848 * 849 * @param force force a refresh even if the preview type has not changed 850 * @return true if the views were recomputed, false if the previews were 851 * already showing and the mode not changed 852 */ 853 public boolean recomputePreviews(boolean force) { 854 RenderPreviewMode newMode = AdtPrefs.getPrefs().getRenderPreviewMode(); 855 if (newMode == mMode && !force 856 && (mRevision == sRevision 857 || mMode == NONE 858 || mMode == CUSTOM)) { 859 return false; 860 } 861 862 RenderPreviewMode oldMode = mMode; 863 mMode = newMode; 864 mRevision = sRevision; 865 866 sScale = 1.0; 867 disposePreviews(); 868 869 switch (mMode) { 870 case DEFAULT: 871 addDefaultPreviews(); 872 break; 873 case INCLUDES: 874 addIncludedInPreviews(); 875 break; 876 case LOCALES: 877 addLocalePreviews(); 878 break; 879 case SCREENS: 880 addScreenSizePreviews(); 881 break; 882 case VARIATIONS: 883 addVariationPreviews(); 884 break; 885 case CUSTOM: 886 addManualPreviews(); 887 break; 888 case NONE: 889 // Can't just set mNeedZoom because with no previews, the paint 890 // method does nothing 891 mCanvas.setFitScale(false /*onlyZoomOut*/, true /*allowZoomIn*/); 892 break; 893 default: 894 assert false : mMode; 895 } 896 897 // We schedule layout for the next redraw rather than process it here immediately; 898 // not only does this let us avoid doing work for windows where the tab is in the 899 // background, but when a file is opened we may not know the size of the canvas 900 // yet, and the layout methods need it in order to do a good job. By the time 901 // the canvas is painted, we have accurate bounds. 902 mNeedLayout = mNeedRender = true; 903 mCanvas.redraw(); 904 905 if (oldMode != mMode && (oldMode == NONE || mMode == NONE)) { 906 // If entering or exiting preview mode: updating padding which is compressed 907 // only in preview mode. 908 mCanvas.getHorizontalTransform().refresh(); 909 mCanvas.getVerticalTransform().refresh(); 910 } 911 912 return true; 913 } 914 915 /** 916 * Sets the new render preview mode to use 917 * 918 * @param mode the new mode 919 */ 920 public void selectMode(@NonNull RenderPreviewMode mode) { 921 if (mode != mMode) { 922 AdtPrefs.getPrefs().setPreviewMode(mode); 923 recomputePreviews(false); 924 } 925 } 926 927 /** Similar to {@link #addDefaultPreviews()} but for locales */ 928 public void addLocalePreviews() { 929 930 ConfigurationChooser chooser = getChooser(); 931 List<Locale> locales = chooser.getLocaleList(); 932 Configuration parent = chooser.getConfiguration(); 933 934 for (Locale locale : locales) { 935 if (!locale.hasLanguage() && !locale.hasRegion()) { 936 continue; 937 } 938 NestedConfiguration configuration = NestedConfiguration.create(chooser, parent); 939 configuration.setOverrideLocale(true); 940 configuration.setLocale(locale, false); 941 942 String displayName = ConfigurationChooser.getLocaleLabel(chooser, locale, false); 943 assert displayName != null; // it's never non null when locale is non null 944 configuration.setDisplayName(displayName); 945 946 addPreview(RenderPreview.create(this, configuration)); 947 } 948 949 // Make a placeholder preview for the current screen, in case we switch from it 950 Configuration configuration = parent; 951 Locale locale = configuration.getLocale(); 952 String label = ConfigurationChooser.getLocaleLabel(chooser, locale, false); 953 if (label == null) { 954 label = "default"; 955 } 956 configuration.setDisplayName(label); 957 RenderPreview preview = RenderPreview.create(this, parent); 958 if (preview != null) { 959 mCanvas.setPreview(preview); 960 } 961 962 // No need to sort: they should all be identical 963 } 964 965 /** Similar to {@link #addDefaultPreviews()} but for screen sizes */ 966 public void addScreenSizePreviews() { 967 ConfigurationChooser chooser = getChooser(); 968 Collection<Device> devices = chooser.getDevices(); 969 Configuration configuration = chooser.getConfiguration(); 970 boolean canScaleNinePatch = configuration.supports(Capability.FIXED_SCALABLE_NINE_PATCH); 971 972 // Rearrange the devices a bit such that the most interesting devices bubble 973 // to the front 974 // 10" tablet, 7" tablet, reference phones, tiny phone, and in general the first 975 // version of each seen screen size 976 List<Device> sorted = new ArrayList<Device>(devices); 977 Set<ScreenSize> seenSizes = new HashSet<ScreenSize>(); 978 State currentState = configuration.getDeviceState(); 979 String currentStateName = currentState != null ? currentState.getName() : ""; 980 981 for (int i = 0, n = sorted.size(); i < n; i++) { 982 Device device = sorted.get(i); 983 boolean interesting = false; 984 985 State state = device.getState(currentStateName); 986 if (state == null) { 987 state = device.getAllStates().get(0); 988 } 989 990 if (device.getName().startsWith("Nexus ") //$NON-NLS-1$ 991 || device.getName().endsWith(" Nexus")) { //$NON-NLS-1$ 992 // Not String#contains("Nexus") because that would also pick up all the generic 993 // entries ("3.7in WVGA (Nexus One)") so we'd have them duplicated 994 interesting = true; 995 } 996 997 FolderConfiguration c = DeviceConfigHelper.getFolderConfig(state); 998 if (c != null) { 999 ScreenSizeQualifier sizeQualifier = c.getScreenSizeQualifier(); 1000 if (sizeQualifier != null) { 1001 ScreenSize size = sizeQualifier.getValue(); 1002 if (!seenSizes.contains(size)) { 1003 seenSizes.add(size); 1004 interesting = true; 1005 } 1006 } 1007 1008 // Omit LDPI, not really used anymore 1009 DensityQualifier density = c.getDensityQualifier(); 1010 if (density != null) { 1011 Density d = density.getValue(); 1012 if (d == Density.LOW) { 1013 interesting = false; 1014 } 1015 1016 if (!canScaleNinePatch && d == Density.TV) { 1017 interesting = false; 1018 } 1019 } 1020 } 1021 1022 if (interesting) { 1023 NestedConfiguration screenConfig = NestedConfiguration.create(chooser, 1024 configuration); 1025 screenConfig.setOverrideDevice(true); 1026 screenConfig.setDevice(device, true); 1027 screenConfig.syncFolderConfig(); 1028 screenConfig.setDisplayName(ConfigurationChooser.getDeviceLabel(device, true)); 1029 addPreview(RenderPreview.create(this, screenConfig)); 1030 } 1031 } 1032 1033 // Sorted by screen size, in decreasing order 1034 sortPreviewsByScreenSize(); 1035 } 1036 1037 /** 1038 * Previews this layout as included in other layouts 1039 */ 1040 public void addIncludedInPreviews() { 1041 ConfigurationChooser chooser = getChooser(); 1042 IProject project = chooser.getProject(); 1043 if (project == null) { 1044 return; 1045 } 1046 IncludeFinder finder = IncludeFinder.get(project); 1047 1048 final List<Reference> includedBy = finder.getIncludedBy(chooser.getEditedFile()); 1049 1050 if (includedBy == null || includedBy.isEmpty()) { 1051 // TODO: Generate some useful defaults, such as including it in a ListView 1052 // as the list item layout? 1053 return; 1054 } 1055 1056 for (final Reference reference : includedBy) { 1057 String title = reference.getDisplayName(); 1058 Configuration config = Configuration.create(chooser.getConfiguration(), 1059 reference.getFile()); 1060 RenderPreview preview = RenderPreview.create(this, config); 1061 preview.setDisplayName(title); 1062 preview.setIncludedWithin(reference); 1063 1064 addPreview(preview); 1065 } 1066 1067 sortPreviewsByOrientation(); 1068 } 1069 1070 /** 1071 * Previews this layout as included in other layouts 1072 */ 1073 public void addVariationPreviews() { 1074 ConfigurationChooser chooser = getChooser(); 1075 1076 IFile file = chooser.getEditedFile(); 1077 List<IFile> variations = AdtUtils.getResourceVariations(file, false /*includeSelf*/); 1078 1079 // Sort by parent folder 1080 Collections.sort(variations, new Comparator<IFile>() { 1081 @Override 1082 public int compare(IFile file1, IFile file2) { 1083 return file1.getParent().getName().compareTo(file2.getParent().getName()); 1084 } 1085 }); 1086 1087 Configuration currentConfig = chooser.getConfiguration(); 1088 1089 for (IFile variation : variations) { 1090 String title = variation.getParent().getName(); 1091 Configuration config = Configuration.create(chooser.getConfiguration(), variation); 1092 config.setTheme(currentConfig.getTheme()); 1093 config.setActivity(currentConfig.getActivity()); 1094 RenderPreview preview = RenderPreview.create(this, config); 1095 preview.setDisplayName(title); 1096 preview.setAlternateInput(variation); 1097 1098 addPreview(preview); 1099 } 1100 1101 sortPreviewsByOrientation(); 1102 } 1103 1104 /** 1105 * Previews this layout using a custom configured set of layouts 1106 */ 1107 public void addManualPreviews() { 1108 if (mManualList == null) { 1109 loadList(); 1110 } else { 1111 mPreviews = mManualList.createPreviews(mCanvas); 1112 } 1113 } 1114 1115 private void loadList() { 1116 IProject project = getChooser().getProject(); 1117 if (project == null) { 1118 return; 1119 } 1120 1121 if (mManualList == null) { 1122 mManualList = RenderPreviewList.get(project); 1123 } 1124 1125 try { 1126 mManualList.load(getChooser().getDevices()); 1127 mPreviews = mManualList.createPreviews(mCanvas); 1128 } catch (IOException e) { 1129 AdtPlugin.log(e, null); 1130 } 1131 } 1132 1133 private void saveList() { 1134 if (mManualList != null) { 1135 try { 1136 mManualList.save(); 1137 } catch (IOException e) { 1138 AdtPlugin.log(e, null); 1139 } 1140 } 1141 } 1142 1143 void rename(ConfigurationDescription description, String newName) { 1144 IProject project = getChooser().getProject(); 1145 if (project == null) { 1146 return; 1147 } 1148 1149 if (mManualList == null) { 1150 mManualList = RenderPreviewList.get(project); 1151 } 1152 description.displayName = newName; 1153 saveList(); 1154 } 1155 1156 1157 /** 1158 * Notifies that the main configuration has changed. 1159 * 1160 * @param flags the change flags, a bitmask corresponding to the 1161 * {@code CHANGE_} constants in {@link ConfigurationClient} 1162 */ 1163 public void configurationChanged(int flags) { 1164 // Similar to renderPreviews, but only acts on incomplete previews 1165 if (hasPreviews()) { 1166 // Do zoomed images first 1167 beginRenderScheduling(); 1168 for (RenderPreview preview : mPreviews) { 1169 if (preview.getScale() > 1.2) { 1170 preview.configurationChanged(flags); 1171 } 1172 } 1173 for (RenderPreview preview : mPreviews) { 1174 if (preview.getScale() <= 1.2) { 1175 preview.configurationChanged(flags); 1176 } 1177 } 1178 RenderPreview preview = mCanvas.getPreview(); 1179 if (preview != null) { 1180 preview.configurationChanged(flags); 1181 preview.dispose(); 1182 } 1183 mNeedLayout = true; 1184 mCanvas.redraw(); 1185 } 1186 } 1187 1188 /** Updates the configuration preview thumbnails */ 1189 public void renderPreviews() { 1190 if (hasPreviews()) { 1191 beginRenderScheduling(); 1192 1193 // Process in visual order 1194 ArrayList<RenderPreview> visualOrder = new ArrayList<RenderPreview>(mPreviews); 1195 Collections.sort(visualOrder, RenderPreview.VISUAL_ORDER); 1196 1197 // Do zoomed images first 1198 for (RenderPreview preview : visualOrder) { 1199 if (preview.getScale() > 1.2 && preview.isVisible()) { 1200 scheduleRender(preview); 1201 } 1202 } 1203 // Non-zoomed images 1204 for (RenderPreview preview : visualOrder) { 1205 if (preview.getScale() <= 1.2 && preview.isVisible()) { 1206 scheduleRender(preview); 1207 } 1208 } 1209 } 1210 1211 mNeedRender = false; 1212 } 1213 1214 private int mPendingRenderCount; 1215 1216 /** 1217 * Reset rendering scheduling. The next render request will be scheduled 1218 * after a single delay unit. 1219 */ 1220 public void beginRenderScheduling() { 1221 mPendingRenderCount = 0; 1222 } 1223 1224 /** 1225 * Schedule rendering the given preview. Each successive call will add an additional 1226 * delay unit to the schedule from the previous {@link #scheduleRender(RenderPreview)} 1227 * call, until {@link #beginRenderScheduling()} is called again. 1228 * 1229 * @param preview the preview to render 1230 */ 1231 public void scheduleRender(@NonNull RenderPreview preview) { 1232 mPendingRenderCount++; 1233 preview.render(mPendingRenderCount * RENDER_DELAY); 1234 } 1235 1236 /** 1237 * Switch to the given configuration preview 1238 * 1239 * @param preview the preview to switch to 1240 */ 1241 public void switchTo(@NonNull RenderPreview preview) { 1242 IFile input = preview.getAlternateInput(); 1243 if (input != null) { 1244 IWorkbenchPartSite site = mCanvas.getEditorDelegate().getEditor().getSite(); 1245 try { 1246 // This switches to the given file, but the file might not have 1247 // an identical configuration to what was shown in the preview. 1248 // For example, while viewing a 10" layout-xlarge file, it might 1249 // show a preview for a 5" version tied to the default layout. If 1250 // you click on it, it will open the default layout file, but it might 1251 // be using a different screen size; any of those that match the 1252 // default layout, say a 3.8". 1253 // 1254 // Thus, we need to also perform a screen size sync first 1255 Configuration configuration = preview.getConfiguration(); 1256 boolean setSize = false; 1257 if (configuration instanceof NestedConfiguration) { 1258 NestedConfiguration nestedConfig = (NestedConfiguration) configuration; 1259 setSize = nestedConfig.isOverridingDevice(); 1260 if (configuration instanceof VaryingConfiguration) { 1261 VaryingConfiguration c = (VaryingConfiguration) configuration; 1262 setSize |= c.isAlternatingDevice(); 1263 } 1264 1265 if (setSize) { 1266 ConfigurationChooser chooser = getChooser(); 1267 IFile editedFile = chooser.getEditedFile(); 1268 if (editedFile != null) { 1269 chooser.syncToVariations(CFG_DEVICE|CFG_DEVICE_STATE, 1270 editedFile, configuration, false, false); 1271 } 1272 } 1273 } 1274 1275 IDE.openEditor(site.getWorkbenchWindow().getActivePage(), input, 1276 CommonXmlEditor.ID); 1277 } catch (PartInitException e) { 1278 AdtPlugin.log(e, null); 1279 } 1280 return; 1281 } 1282 1283 GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor(); 1284 ConfigurationChooser chooser = editor.getConfigurationChooser(); 1285 1286 Configuration originalConfiguration = chooser.getConfiguration(); 1287 1288 // The new configuration is the configuration which will become the configuration 1289 // in the layout editor's chooser 1290 Configuration previewConfiguration = preview.getConfiguration(); 1291 Configuration newConfiguration = previewConfiguration; 1292 if (newConfiguration instanceof NestedConfiguration) { 1293 // Should never use a complementing configuration for the main 1294 // rendering's configuration; instead, create a new configuration 1295 // with a snapshot of the configuration's current values 1296 newConfiguration = Configuration.copy(previewConfiguration); 1297 1298 // Remap all the previews to be parented to this new copy instead 1299 // of the old one (which is no longer controlled by the chooser) 1300 for (RenderPreview p : mPreviews) { 1301 Configuration configuration = p.getConfiguration(); 1302 if (configuration instanceof NestedConfiguration) { 1303 NestedConfiguration nested = (NestedConfiguration) configuration; 1304 nested.setParent(newConfiguration); 1305 } 1306 } 1307 } 1308 1309 // Make a preview for the configuration which *was* showing in the 1310 // chooser up until this point: 1311 RenderPreview newPreview = mCanvas.getPreview(); 1312 if (newPreview == null) { 1313 newPreview = RenderPreview.create(this, originalConfiguration); 1314 } 1315 1316 // Update its configuration such that it is complementing or inheriting 1317 // from the new chosen configuration 1318 if (previewConfiguration instanceof VaryingConfiguration) { 1319 VaryingConfiguration varying = VaryingConfiguration.create( 1320 (VaryingConfiguration) previewConfiguration, 1321 newConfiguration); 1322 varying.updateDisplayName(); 1323 originalConfiguration = varying; 1324 newPreview.setConfiguration(originalConfiguration); 1325 } else if (previewConfiguration instanceof NestedConfiguration) { 1326 NestedConfiguration nested = NestedConfiguration.create( 1327 (NestedConfiguration) previewConfiguration, 1328 originalConfiguration, 1329 newConfiguration); 1330 nested.setDisplayName(nested.computeDisplayName()); 1331 originalConfiguration = nested; 1332 newPreview.setConfiguration(originalConfiguration); 1333 } 1334 1335 // Replace clicked preview with preview of the formerly edited main configuration 1336 // This doesn't work yet because the image overlay has had its image 1337 // replaced by the configuration previews! I should make a list of them 1338 //newPreview.setFullImage(mImageOverlay.getAwtImage()); 1339 for (int i = 0, n = mPreviews.size(); i < n; i++) { 1340 if (preview == mPreviews.get(i)) { 1341 mPreviews.set(i, newPreview); 1342 break; 1343 } 1344 } 1345 1346 // Stash the corresponding preview (not active) on the canvas so we can 1347 // retrieve it if clicking to some other preview later 1348 mCanvas.setPreview(preview); 1349 preview.setVisible(false); 1350 1351 // Switch to the configuration from the clicked preview (though it's 1352 // most likely a copy, see above) 1353 chooser.setConfiguration(newConfiguration); 1354 editor.changed(MASK_ALL); 1355 1356 // Scroll to the top again, if necessary 1357 mCanvas.getVerticalBar().setSelection(mCanvas.getVerticalBar().getMinimum()); 1358 1359 mNeedLayout = mNeedZoom = true; 1360 mCanvas.redraw(); 1361 mAnimation = new SwapAnimation(preview, newPreview); 1362 } 1363 1364 /** 1365 * Gets the preview at the given location, or null if none. This is 1366 * currently deeply tied to where things are painted in onPaint(). 1367 */ 1368 RenderPreview getPreview(ControlPoint mousePos) { 1369 if (hasPreviews()) { 1370 int rootX = getX(); 1371 if (mousePos.x < rootX) { 1372 return null; 1373 } 1374 int rootY = getY(); 1375 1376 for (RenderPreview preview : mPreviews) { 1377 int x = rootX + preview.getX(); 1378 int y = rootY + preview.getY(); 1379 if (mousePos.x >= x && mousePos.x <= x + preview.getWidth()) { 1380 if (mousePos.y >= y && mousePos.y <= y + preview.getHeight()) { 1381 return preview; 1382 } 1383 } 1384 } 1385 } 1386 1387 return null; 1388 } 1389 1390 private int getX() { 1391 return mHScale.translate(0); 1392 } 1393 1394 private int getY() { 1395 return mVScale.translate(0); 1396 } 1397 1398 private int getZoomX() { 1399 Rectangle clientArea = mCanvas.getClientArea(); 1400 int x = clientArea.x + clientArea.width - ZOOM_ICON_WIDTH; 1401 if (x < mHScale.getScaledImgSize() + PREVIEW_HGAP) { 1402 // No visible previews because the main image is zoomed too far 1403 return -1; 1404 } 1405 1406 return x - 6; 1407 } 1408 1409 private int getZoomY() { 1410 Rectangle clientArea = mCanvas.getClientArea(); 1411 return clientArea.y + 5; 1412 } 1413 1414 /** 1415 * Returns the height of the layout 1416 * 1417 * @return the height 1418 */ 1419 public int getHeight() { 1420 return mLayoutHeight; 1421 } 1422 1423 /** 1424 * Notifies that preview manager that the mouse cursor has moved to the 1425 * given control position within the layout canvas 1426 * 1427 * @param mousePos the mouse position, relative to the layout canvas 1428 */ 1429 public void moved(ControlPoint mousePos) { 1430 RenderPreview hovered = getPreview(mousePos); 1431 if (hovered != mActivePreview) { 1432 if (mActivePreview != null) { 1433 mActivePreview.setActive(false); 1434 } 1435 mActivePreview = hovered; 1436 if (mActivePreview != null) { 1437 mActivePreview.setActive(true); 1438 } 1439 mCanvas.redraw(); 1440 } 1441 } 1442 1443 /** 1444 * Notifies that preview manager that the mouse cursor has entered the layout canvas 1445 * 1446 * @param mousePos the mouse position, relative to the layout canvas 1447 */ 1448 public void enter(ControlPoint mousePos) { 1449 moved(mousePos); 1450 } 1451 1452 /** 1453 * Notifies that preview manager that the mouse cursor has exited the layout canvas 1454 * 1455 * @param mousePos the mouse position, relative to the layout canvas 1456 */ 1457 public void exit(ControlPoint mousePos) { 1458 if (mActivePreview != null) { 1459 mActivePreview.setActive(false); 1460 } 1461 mActivePreview = null; 1462 mCanvas.redraw(); 1463 } 1464 1465 /** 1466 * Process a mouse click, and return true if it was handled by this manager 1467 * (e.g. the click was on a preview) 1468 * 1469 * @param mousePos the mouse position where the click occurred 1470 * @return true if the click occurred over a preview and was handled, false otherwise 1471 */ 1472 public boolean click(ControlPoint mousePos) { 1473 // Clicked zoom? 1474 int x = getZoomX(); 1475 if (x > 0) { 1476 if (mousePos.x >= x && mousePos.x <= x + ZOOM_ICON_WIDTH) { 1477 int y = getZoomY(); 1478 if (mousePos.y >= y && mousePos.y <= y + 4 * ZOOM_ICON_HEIGHT) { 1479 if (mousePos.y < y + ZOOM_ICON_HEIGHT) { 1480 zoomIn(); 1481 } else if (mousePos.y < y + 2 * ZOOM_ICON_HEIGHT) { 1482 zoomOut(); 1483 } else if (mousePos.y < y + 3 * ZOOM_ICON_HEIGHT) { 1484 zoomReset(); 1485 } else { 1486 selectMode(NONE); 1487 } 1488 return true; 1489 } 1490 } 1491 } 1492 1493 RenderPreview preview = getPreview(mousePos); 1494 if (preview != null) { 1495 boolean handled = preview.click(mousePos.x - getX() - preview.getX(), 1496 mousePos.y - getY() - preview.getY()); 1497 if (handled) { 1498 // In case layout was performed, there could be a new preview 1499 // under this coordinate now, so make sure it's hover etc 1500 // shows up 1501 moved(mousePos); 1502 return true; 1503 } 1504 } 1505 1506 return false; 1507 } 1508 1509 /** 1510 * Returns true if there are thumbnail previews 1511 * 1512 * @return true if thumbnails are being shown 1513 */ 1514 public boolean hasPreviews() { 1515 return mPreviews != null && !mPreviews.isEmpty(); 1516 } 1517 1518 1519 private void sortPreviewsByScreenSize() { 1520 if (mPreviews != null) { 1521 Collections.sort(mPreviews, new Comparator<RenderPreview>() { 1522 @Override 1523 public int compare(RenderPreview preview1, RenderPreview preview2) { 1524 Configuration config1 = preview1.getConfiguration(); 1525 Configuration config2 = preview2.getConfiguration(); 1526 Device device1 = config1.getDevice(); 1527 Device device2 = config1.getDevice(); 1528 if (device1 != null && device2 != null) { 1529 Screen screen1 = device1.getDefaultHardware().getScreen(); 1530 Screen screen2 = device2.getDefaultHardware().getScreen(); 1531 if (screen1 != null && screen2 != null) { 1532 double delta = screen1.getDiagonalLength() 1533 - screen2.getDiagonalLength(); 1534 if (delta != 0.0) { 1535 return (int) Math.signum(delta); 1536 } else { 1537 if (screen1.getPixelDensity() != screen2.getPixelDensity()) { 1538 return screen1.getPixelDensity().compareTo( 1539 screen2.getPixelDensity()); 1540 } 1541 } 1542 } 1543 1544 } 1545 State state1 = config1.getDeviceState(); 1546 State state2 = config2.getDeviceState(); 1547 if (state1 != state2 && state1 != null && state2 != null) { 1548 return state1.getName().compareTo(state2.getName()); 1549 } 1550 1551 return preview1.getDisplayName().compareTo(preview2.getDisplayName()); 1552 } 1553 }); 1554 } 1555 } 1556 1557 private void sortPreviewsByOrientation() { 1558 if (mPreviews != null) { 1559 Collections.sort(mPreviews, new Comparator<RenderPreview>() { 1560 @Override 1561 public int compare(RenderPreview preview1, RenderPreview preview2) { 1562 Configuration config1 = preview1.getConfiguration(); 1563 Configuration config2 = preview2.getConfiguration(); 1564 State state1 = config1.getDeviceState(); 1565 State state2 = config2.getDeviceState(); 1566 if (state1 != state2 && state1 != null && state2 != null) { 1567 return state1.getName().compareTo(state2.getName()); 1568 } 1569 1570 return preview1.getDisplayName().compareTo(preview2.getDisplayName()); 1571 } 1572 }); 1573 } 1574 } 1575 1576 /** 1577 * Vertical scrollbar listener which updates render previews which are not 1578 * visible and triggers a redraw 1579 */ 1580 private class ScrollBarListener implements SelectionListener { 1581 @Override 1582 public void widgetSelected(SelectionEvent e) { 1583 if (mPreviews == null) { 1584 return; 1585 } 1586 1587 ScrollBar bar = mCanvas.getVerticalBar(); 1588 int selection = bar.getSelection(); 1589 int thumb = bar.getThumb(); 1590 int maxY = selection + thumb; 1591 beginRenderScheduling(); 1592 for (RenderPreview preview : mPreviews) { 1593 if (!preview.isVisible() && preview.getY() <= maxY) { 1594 preview.setVisible(true); 1595 } 1596 } 1597 } 1598 1599 @Override 1600 public void widgetDefaultSelected(SelectionEvent e) { 1601 } 1602 } 1603 1604 /** Animation overlay shown briefly after swapping two previews */ 1605 private class SwapAnimation implements Runnable { 1606 private long begin; 1607 private long end; 1608 private static final long DURATION = 400; // ms 1609 private Rect initialRect1; 1610 private Rect targetRect1; 1611 private Rect initialRect2; 1612 private Rect targetRect2; 1613 private RenderPreview preview; 1614 1615 SwapAnimation(RenderPreview preview1, RenderPreview preview2) { 1616 begin = System.currentTimeMillis(); 1617 end = begin + DURATION; 1618 1619 initialRect1 = new Rect(preview1.getX(), preview1.getY(), 1620 preview1.getWidth(), preview1.getHeight()); 1621 1622 CanvasTransform hi = mCanvas.getHorizontalTransform(); 1623 CanvasTransform vi = mCanvas.getVerticalTransform(); 1624 initialRect2 = new Rect(hi.translate(0), vi.translate(0), 1625 hi.getScaledImgSize(), vi.getScaledImgSize()); 1626 preview = preview2; 1627 } 1628 1629 void tick(GC gc) { 1630 long now = System.currentTimeMillis(); 1631 if (now > end || mCanvas.isDisposed()) { 1632 mAnimation = null; 1633 return; 1634 } 1635 1636 CanvasTransform hi = mCanvas.getHorizontalTransform(); 1637 CanvasTransform vi = mCanvas.getVerticalTransform(); 1638 if (targetRect1 == null) { 1639 targetRect1 = new Rect(hi.translate(0), vi.translate(0), 1640 hi.getScaledImgSize(), vi.getScaledImgSize()); 1641 } 1642 double portion = (now - begin) / (double) DURATION; 1643 Rect rect1 = new Rect( 1644 (int) (portion * (targetRect1.x - initialRect1.x) + initialRect1.x), 1645 (int) (portion * (targetRect1.y - initialRect1.y) + initialRect1.y), 1646 (int) (portion * (targetRect1.w - initialRect1.w) + initialRect1.w), 1647 (int) (portion * (targetRect1.h - initialRect1.h) + initialRect1.h)); 1648 1649 if (targetRect2 == null) { 1650 targetRect2 = new Rect(preview.getX(), preview.getY(), 1651 preview.getWidth(), preview.getHeight()); 1652 } 1653 portion = (now - begin) / (double) DURATION; 1654 Rect rect2 = new Rect( 1655 (int) (portion * (targetRect2.x - initialRect2.x) + initialRect2.x), 1656 (int) (portion * (targetRect2.y - initialRect2.y) + initialRect2.y), 1657 (int) (portion * (targetRect2.w - initialRect2.w) + initialRect2.w), 1658 (int) (portion * (targetRect2.h - initialRect2.h) + initialRect2.h)); 1659 1660 gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_GRAY)); 1661 gc.drawRectangle(rect1.x, rect1.y, rect1.w, rect1.h); 1662 gc.drawRectangle(rect2.x, rect2.y, rect2.w, rect2.h); 1663 1664 mCanvas.getDisplay().timerExec(5, this); 1665 } 1666 1667 @Override 1668 public void run() { 1669 mCanvas.redraw(); 1670 } 1671 } 1672 1673 /** 1674 * Notifies the {@linkplain RenderPreviewManager} that the configuration used 1675 * in the main chooser has been changed. This may require updating parent references 1676 * in the preview configurations inheriting from it. 1677 * 1678 * @param oldConfiguration the previous configuration 1679 * @param newConfiguration the new configuration in the chooser 1680 */ 1681 public void updateChooserConfig( 1682 @NonNull Configuration oldConfiguration, 1683 @NonNull Configuration newConfiguration) { 1684 if (hasPreviews()) { 1685 for (RenderPreview preview : mPreviews) { 1686 Configuration configuration = preview.getConfiguration(); 1687 if (configuration instanceof NestedConfiguration) { 1688 NestedConfiguration nestedConfig = (NestedConfiguration) configuration; 1689 if (nestedConfig.getParent() == oldConfiguration) { 1690 nestedConfig.setParent(newConfiguration); 1691 } 1692 } 1693 } 1694 } 1695 } 1696 } 1697