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