| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Eclipse Public License, Version 1.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.eclipse.org/org/documents/epl-v10.php |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.ide.eclipse.adt.internal.editors.layout.gle2; |
| |
| import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE; |
| import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE_STATE; |
| import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.MASK_ALL; |
| import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SHADOW_SIZE; |
| import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SMALL_SHADOW_SIZE; |
| import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreview.LARGE_SHADOWS; |
| import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.CUSTOM; |
| import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.NONE; |
| import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.SCREENS; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.ide.common.api.Rect; |
| import com.android.ide.common.rendering.api.Capability; |
| import com.android.ide.common.resources.configuration.DensityQualifier; |
| import com.android.ide.common.resources.configuration.DeviceConfigHelper; |
| import com.android.ide.common.resources.configuration.FolderConfiguration; |
| import com.android.ide.common.resources.configuration.LocaleQualifier; |
| import com.android.ide.common.resources.configuration.ScreenSizeQualifier; |
| import com.android.ide.eclipse.adt.AdtPlugin; |
| import com.android.ide.eclipse.adt.AdtUtils; |
| import com.android.ide.eclipse.adt.internal.editors.IconFactory; |
| import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; |
| import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration; |
| import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser; |
| import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient; |
| import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription; |
| import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Locale; |
| import com.android.ide.eclipse.adt.internal.editors.layout.configuration.NestedConfiguration; |
| import com.android.ide.eclipse.adt.internal.editors.layout.configuration.VaryingConfiguration; |
| import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; |
| import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; |
| import com.android.resources.Density; |
| import com.android.resources.ScreenSize; |
| import com.android.sdklib.devices.Device; |
| import com.android.sdklib.devices.Screen; |
| import com.android.sdklib.devices.State; |
| import com.google.common.collect.Lists; |
| |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IProject; |
| import org.eclipse.jface.dialogs.InputDialog; |
| import org.eclipse.jface.window.Window; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.events.SelectionEvent; |
| import org.eclipse.swt.events.SelectionListener; |
| import org.eclipse.swt.graphics.GC; |
| import org.eclipse.swt.graphics.Image; |
| import org.eclipse.swt.graphics.Rectangle; |
| import org.eclipse.swt.widgets.ScrollBar; |
| import org.eclipse.ui.IWorkbenchPartSite; |
| import org.eclipse.ui.PartInitException; |
| import org.eclipse.ui.ide.IDE; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * Manager for the configuration previews, which handles layout computations, |
| * managing the image buffer cache, etc |
| */ |
| public class RenderPreviewManager { |
| private static double sScale = 1.0; |
| private static final int RENDER_DELAY = 150; |
| private static final int PREVIEW_VGAP = 18; |
| private static final int PREVIEW_HGAP = 12; |
| private static final int MAX_WIDTH = 200; |
| private static final int MAX_HEIGHT = MAX_WIDTH; |
| private static final int ZOOM_ICON_WIDTH = 16; |
| private static final int ZOOM_ICON_HEIGHT = 16; |
| private @Nullable List<RenderPreview> mPreviews; |
| private @Nullable RenderPreviewList mManualList; |
| private final @NonNull LayoutCanvas mCanvas; |
| private final @NonNull CanvasTransform mVScale; |
| private final @NonNull CanvasTransform mHScale; |
| private int mPrevCanvasWidth; |
| private int mPrevCanvasHeight; |
| private int mPrevImageWidth; |
| private int mPrevImageHeight; |
| private @NonNull RenderPreviewMode mMode = NONE; |
| private @Nullable RenderPreview mActivePreview; |
| private @Nullable ScrollBarListener mListener; |
| private int mLayoutHeight; |
| /** Last seen state revision in this {@link RenderPreviewManager}. If less |
| * than {@link #sRevision}, the previews need to be updated on next exposure */ |
| private static int mRevision; |
| /** Current global revision count */ |
| private static int sRevision; |
| private boolean mNeedLayout; |
| private boolean mNeedRender; |
| private boolean mNeedZoom; |
| private SwapAnimation mAnimation; |
| |
| /** |
| * Creates a {@link RenderPreviewManager} associated with the given canvas |
| * |
| * @param canvas the canvas to manage previews for |
| */ |
| public RenderPreviewManager(@NonNull LayoutCanvas canvas) { |
| mCanvas = canvas; |
| mHScale = canvas.getHorizontalTransform(); |
| mVScale = canvas.getVerticalTransform(); |
| } |
| |
| /** |
| * Revise the global state revision counter. This will cause all layout |
| * preview managers to refresh themselves to the latest revision when they |
| * are next exposed. |
| */ |
| public static void bumpRevision() { |
| sRevision++; |
| } |
| |
| /** |
| * Returns the associated chooser |
| * |
| * @return the associated chooser |
| */ |
| @NonNull |
| ConfigurationChooser getChooser() { |
| GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor(); |
| return editor.getConfigurationChooser(); |
| } |
| |
| /** |
| * Returns the associated canvas |
| * |
| * @return the canvas |
| */ |
| @NonNull |
| public LayoutCanvas getCanvas() { |
| return mCanvas; |
| } |
| |
| /** Zooms in (grows all previews) */ |
| public void zoomIn() { |
| sScale = sScale * (1 / 0.9); |
| if (Math.abs(sScale-1.0) < 0.0001) { |
| sScale = 1.0; |
| } |
| |
| updatedZoom(); |
| } |
| |
| /** Zooms out (shrinks all previews) */ |
| public void zoomOut() { |
| sScale = sScale * (0.9 / 1); |
| if (Math.abs(sScale-1.0) < 0.0001) { |
| sScale = 1.0; |
| } |
| updatedZoom(); |
| } |
| |
| /** Zooms to 100 (resets zoom) */ |
| public void zoomReset() { |
| sScale = 1.0; |
| updatedZoom(); |
| mNeedZoom = mNeedLayout = true; |
| mCanvas.redraw(); |
| } |
| |
| private void updatedZoom() { |
| if (hasPreviews()) { |
| for (RenderPreview preview : mPreviews) { |
| preview.disposeThumbnail(); |
| } |
| RenderPreview preview = mCanvas.getPreview(); |
| if (preview != null) { |
| preview.disposeThumbnail(); |
| } |
| } |
| |
| mNeedLayout = mNeedRender = true; |
| mCanvas.redraw(); |
| } |
| |
| static int getMaxWidth() { |
| return (int) (sScale * MAX_WIDTH); |
| } |
| |
| static int getMaxHeight() { |
| return (int) (sScale * MAX_HEIGHT); |
| } |
| |
| static double getScale() { |
| return sScale; |
| } |
| |
| /** |
| * Returns whether there are any manual preview items (provided the current |
| * mode is manual previews |
| * |
| * @return true if there are items in the manual preview list |
| */ |
| public boolean hasManualPreviews() { |
| assert mMode == CUSTOM; |
| return mManualList != null && !mManualList.isEmpty(); |
| } |
| |
| /** Delete all the previews */ |
| public void deleteManualPreviews() { |
| disposePreviews(); |
| selectMode(NONE); |
| mCanvas.setFitScale(true /* onlyZoomOut */, true /*allowZoomIn*/); |
| |
| if (mManualList != null) { |
| mManualList.delete(); |
| } |
| } |
| |
| /** Dispose all the previews */ |
| public void disposePreviews() { |
| if (mPreviews != null) { |
| List<RenderPreview> old = mPreviews; |
| mPreviews = null; |
| for (RenderPreview preview : old) { |
| preview.dispose(); |
| } |
| } |
| } |
| |
| /** |
| * Deletes the given preview |
| * |
| * @param preview the preview to be deleted |
| */ |
| public void deletePreview(RenderPreview preview) { |
| mPreviews.remove(preview); |
| preview.dispose(); |
| layout(true); |
| mCanvas.redraw(); |
| |
| if (mManualList != null) { |
| mManualList.remove(preview); |
| saveList(); |
| } |
| } |
| |
| /** |
| * Compute the total width required for the previews, including internal padding |
| * |
| * @return total width in pixels |
| */ |
| public int computePreviewWidth() { |
| int maxPreviewWidth = 0; |
| if (hasPreviews()) { |
| for (RenderPreview preview : mPreviews) { |
| maxPreviewWidth = Math.max(maxPreviewWidth, preview.getWidth()); |
| } |
| |
| if (maxPreviewWidth > 0) { |
| maxPreviewWidth += 2 * PREVIEW_HGAP; // 2x for left and right side |
| maxPreviewWidth += LARGE_SHADOWS ? SHADOW_SIZE : SMALL_SHADOW_SIZE; |
| } |
| |
| return maxPreviewWidth; |
| } |
| |
| return 0; |
| } |
| |
| /** |
| * Layout Algorithm. This sets the {@link RenderPreview#getX()} and |
| * {@link RenderPreview#getY()} coordinates of all the previews. It also |
| * marks previews as visible or invisible via |
| * {@link RenderPreview#setVisible(boolean)} according to their position and |
| * the current visible view port in the layout canvas. Finally, it also sets |
| * the {@code mLayoutHeight} field, such that the scrollbars can compute the |
| * right scrolled area, and that scrolling can cause render refreshes on |
| * views that are made visible. |
| * <p> |
| * This is not a traditional bin packing problem, because the objects to be |
| * packaged do not have a fixed size; we can scale them up and down in order |
| * to provide an "optimal" size. |
| * <p> |
| * See http://en.wikipedia.org/wiki/Packing_problem See |
| * http://en.wikipedia.org/wiki/Bin_packing_problem |
| */ |
| void layout(boolean refresh) { |
| mNeedLayout = false; |
| |
| if (mPreviews == null || mPreviews.isEmpty()) { |
| return; |
| } |
| |
| int scaledImageWidth = mHScale.getScaledImgSize(); |
| int scaledImageHeight = mVScale.getScaledImgSize(); |
| Rectangle clientArea = mCanvas.getClientArea(); |
| |
| if (!refresh && |
| (scaledImageWidth == mPrevImageWidth |
| && scaledImageHeight == mPrevImageHeight |
| && clientArea.width == mPrevCanvasWidth |
| && clientArea.height == mPrevCanvasHeight)) { |
| // No change |
| return; |
| } |
| |
| mPrevImageWidth = scaledImageWidth; |
| mPrevImageHeight = scaledImageHeight; |
| mPrevCanvasWidth = clientArea.width; |
| mPrevCanvasHeight = clientArea.height; |
| |
| if (mListener == null) { |
| mListener = new ScrollBarListener(); |
| mCanvas.getVerticalBar().addSelectionListener(mListener); |
| } |
| |
| beginRenderScheduling(); |
| |
| mLayoutHeight = 0; |
| |
| if (previewsHaveIdenticalSize() || fixedOrder()) { |
| // If all the preview boxes are of identical sizes, or if the order is predetermined, |
| // just lay them out in rows. |
| rowLayout(); |
| } else if (previewsFit()) { |
| layoutFullFit(); |
| } else { |
| rowLayout(); |
| } |
| |
| mCanvas.updateScrollBars(); |
| } |
| |
| /** |
| * Performs a simple layout where the views are laid out in a row, wrapping |
| * around the top left canvas image. |
| */ |
| private void rowLayout() { |
| // TODO: Separate layout heuristics for portrait and landscape orientations (though |
| // it also depends on the dimensions of the canvas window, which determines the |
| // shape of the leftover space) |
| |
| int scaledImageWidth = mHScale.getScaledImgSize(); |
| int scaledImageHeight = mVScale.getScaledImgSize(); |
| Rectangle clientArea = mCanvas.getClientArea(); |
| |
| int availableWidth = clientArea.x + clientArea.width - getX(); |
| int availableHeight = clientArea.y + clientArea.height - getY(); |
| int maxVisibleY = clientArea.y + clientArea.height; |
| |
| int bottomBorder = scaledImageHeight; |
| int rightHandSide = scaledImageWidth + PREVIEW_HGAP; |
| int nextY = 0; |
| |
| // First lay out images across the top right hand side |
| int x = rightHandSide; |
| int y = 0; |
| boolean wrapped = false; |
| |
| int vgap = PREVIEW_VGAP; |
| for (RenderPreview preview : mPreviews) { |
| // If we have forked previews, double the vgap to allow space for two labels |
| if (preview.isForked()) { |
| vgap *= 2; |
| break; |
| } |
| } |
| |
| List<RenderPreview> aspectOrder; |
| if (!fixedOrder()) { |
| aspectOrder = new ArrayList<RenderPreview>(mPreviews); |
| Collections.sort(aspectOrder, RenderPreview.INCREASING_ASPECT_RATIO); |
| } else { |
| aspectOrder = mPreviews; |
| } |
| |
| for (RenderPreview preview : aspectOrder) { |
| if (x > 0 && x + preview.getWidth() > availableWidth) { |
| x = rightHandSide; |
| int prevY = y; |
| y = nextY; |
| if ((prevY <= bottomBorder || |
| y <= bottomBorder) |
| && Math.max(nextY, y + preview.getHeight()) > bottomBorder) { |
| // If there's really no visible room below, don't bother |
| // Similarly, don't wrap individually scaled views |
| if (bottomBorder < availableHeight - 40 && preview.getScale() < 1.2) { |
| // If it's closer to the top row than the bottom, just |
| // mark the next row for left justify instead |
| if (bottomBorder - y > y + preview.getHeight() - bottomBorder) { |
| rightHandSide = 0; |
| wrapped = true; |
| } else if (!wrapped) { |
| y = nextY = Math.max(nextY, bottomBorder + vgap); |
| x = rightHandSide = 0; |
| wrapped = true; |
| } |
| } |
| } |
| } |
| if (x > 0 && y <= bottomBorder |
| && Math.max(nextY, y + preview.getHeight()) > bottomBorder) { |
| if (clientArea.height - bottomBorder < preview.getHeight()) { |
| // No room below the device on the left; just continue on the |
| // bottom row |
| } else if (preview.getScale() < 1.2) { |
| if (bottomBorder - y > y + preview.getHeight() - bottomBorder) { |
| rightHandSide = 0; |
| wrapped = true; |
| } else { |
| y = nextY = Math.max(nextY, bottomBorder + vgap); |
| x = rightHandSide = 0; |
| wrapped = true; |
| } |
| } |
| } |
| |
| preview.setPosition(x, y); |
| |
| if (y > maxVisibleY && maxVisibleY > 0) { |
| preview.setVisible(false); |
| } else if (!preview.isVisible()) { |
| preview.setVisible(true); |
| } |
| |
| x += preview.getWidth(); |
| x += PREVIEW_HGAP; |
| nextY = Math.max(nextY, y + preview.getHeight() + vgap); |
| } |
| |
| mLayoutHeight = nextY; |
| } |
| |
| private boolean fixedOrder() { |
| return mMode == SCREENS; |
| } |
| |
| /** Returns true if all the previews have the same identical size */ |
| private boolean previewsHaveIdenticalSize() { |
| if (!hasPreviews()) { |
| return true; |
| } |
| |
| Iterator<RenderPreview> iterator = mPreviews.iterator(); |
| RenderPreview first = iterator.next(); |
| int width = first.getWidth(); |
| int height = first.getHeight(); |
| |
| while (iterator.hasNext()) { |
| RenderPreview preview = iterator.next(); |
| if (width != preview.getWidth() || height != preview.getHeight()) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| /** Returns true if all the previews can fully fit in the available space */ |
| private boolean previewsFit() { |
| int scaledImageWidth = mHScale.getScaledImgSize(); |
| int scaledImageHeight = mVScale.getScaledImgSize(); |
| Rectangle clientArea = mCanvas.getClientArea(); |
| int availableWidth = clientArea.x + clientArea.width - getX(); |
| int availableHeight = clientArea.y + clientArea.height - getY(); |
| int bottomBorder = scaledImageHeight; |
| int rightHandSide = scaledImageWidth + PREVIEW_HGAP; |
| |
| // First see if we can fit everything; if so, we can try to make the layouts |
| // larger such that they fill up all the available space |
| long availableArea = rightHandSide * bottomBorder + |
| availableWidth * (Math.max(0, availableHeight - bottomBorder)); |
| |
| long requiredArea = 0; |
| for (RenderPreview preview : mPreviews) { |
| // Note: This does not include individual preview scale; the layout |
| // algorithm itself may be tweaking the scales to fit elements within |
| // the layout |
| requiredArea += preview.getArea(); |
| } |
| |
| return requiredArea * sScale < availableArea; |
| } |
| |
| private void layoutFullFit() { |
| int scaledImageWidth = mHScale.getScaledImgSize(); |
| int scaledImageHeight = mVScale.getScaledImgSize(); |
| Rectangle clientArea = mCanvas.getClientArea(); |
| int availableWidth = clientArea.x + clientArea.width - getX(); |
| int availableHeight = clientArea.y + clientArea.height - getY(); |
| int maxVisibleY = clientArea.y + clientArea.height; |
| int bottomBorder = scaledImageHeight; |
| int rightHandSide = scaledImageWidth + PREVIEW_HGAP; |
| |
| int minWidth = Integer.MAX_VALUE; |
| int minHeight = Integer.MAX_VALUE; |
| for (RenderPreview preview : mPreviews) { |
| minWidth = Math.min(minWidth, preview.getWidth()); |
| minHeight = Math.min(minHeight, preview.getHeight()); |
| } |
| |
| BinPacker packer = new BinPacker(minWidth, minHeight); |
| |
| // TODO: Instead of this, just start with client area and occupy scaled image size! |
| |
| // Add in gap on right and bottom since we'll add that requirement on the width and |
| // height rectangles too (for spacing) |
| packer.addSpace(new Rect(rightHandSide, 0, |
| availableWidth - rightHandSide + PREVIEW_HGAP, |
| availableHeight + PREVIEW_VGAP)); |
| if (maxVisibleY > bottomBorder) { |
| packer.addSpace(new Rect(0, bottomBorder + PREVIEW_VGAP, |
| availableWidth + PREVIEW_HGAP, maxVisibleY - bottomBorder + PREVIEW_VGAP)); |
| } |
| |
| // TODO: Sort previews first before attempting to position them? |
| |
| ArrayList<RenderPreview> aspectOrder = new ArrayList<RenderPreview>(mPreviews); |
| Collections.sort(aspectOrder, RenderPreview.INCREASING_ASPECT_RATIO); |
| |
| for (RenderPreview preview : aspectOrder) { |
| int previewWidth = preview.getWidth(); |
| int previewHeight = preview.getHeight(); |
| previewHeight += PREVIEW_VGAP; |
| if (preview.isForked()) { |
| previewHeight += PREVIEW_VGAP; |
| } |
| previewWidth += PREVIEW_HGAP; |
| // title height? how do I account for that? |
| Rect position = packer.occupy(previewWidth, previewHeight); |
| if (position != null) { |
| preview.setPosition(position.x, position.y); |
| preview.setVisible(true); |
| } else { |
| // Can't fit: give up and do plain row layout |
| rowLayout(); |
| return; |
| } |
| } |
| |
| mLayoutHeight = availableHeight; |
| } |
| /** |
| * Paints the configuration previews |
| * |
| * @param gc the graphics context to paint into |
| */ |
| void paint(GC gc) { |
| if (hasPreviews()) { |
| // Ensure up to date at all times; consider moving if it's too expensive |
| layout(mNeedLayout); |
| if (mNeedRender) { |
| renderPreviews(); |
| } |
| if (mNeedZoom) { |
| boolean allowZoomIn = true /*mMode == NONE*/; |
| mCanvas.setFitScale(false /*onlyZoomOut*/, allowZoomIn); |
| mNeedZoom = false; |
| } |
| int rootX = getX(); |
| int rootY = getY(); |
| |
| for (RenderPreview preview : mPreviews) { |
| if (preview.isVisible()) { |
| int x = rootX + preview.getX(); |
| int y = rootY + preview.getY(); |
| preview.paint(gc, x, y); |
| } |
| } |
| |
| RenderPreview preview = mCanvas.getPreview(); |
| if (preview != null) { |
| String displayName = null; |
| Configuration configuration = preview.getConfiguration(); |
| if (configuration instanceof VaryingConfiguration) { |
| // Use override flags from stashed preview, but configuration |
| // data from live (not varying) configured configuration |
| VaryingConfiguration cfg = (VaryingConfiguration) configuration; |
| int flags = cfg.getAlternateFlags() | cfg.getOverrideFlags(); |
| displayName = NestedConfiguration.computeDisplayName(flags, |
| getChooser().getConfiguration()); |
| } else if (configuration instanceof NestedConfiguration) { |
| int flags = ((NestedConfiguration) configuration).getOverrideFlags(); |
| displayName = NestedConfiguration.computeDisplayName(flags, |
| getChooser().getConfiguration()); |
| } else { |
| displayName = configuration.getDisplayName(); |
| } |
| if (displayName != null) { |
| CanvasTransform hi = mHScale; |
| CanvasTransform vi = mVScale; |
| |
| int destX = hi.translate(0); |
| int destY = vi.translate(0); |
| int destWidth = hi.getScaledImgSize(); |
| int destHeight = vi.getScaledImgSize(); |
| |
| int x = destX + destWidth / 2 - preview.getWidth() / 2; |
| int y = destY + destHeight; |
| |
| preview.paintTitle(gc, x, y, false /*showFile*/, displayName); |
| } |
| } |
| |
| // Zoom overlay |
| int x = getZoomX(); |
| if (x > 0) { |
| int y = getZoomY(); |
| int oldAlpha = gc.getAlpha(); |
| |
| // Paint background oval rectangle behind the zoom and close icons |
| gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_GRAY)); |
| gc.setAlpha(128); |
| int padding = 3; |
| int arc = 5; |
| gc.fillRoundRectangle(x - padding, y - padding, |
| ZOOM_ICON_WIDTH + 2 * padding, |
| 4 * ZOOM_ICON_HEIGHT + 2 * padding, arc, arc); |
| |
| gc.setAlpha(255); |
| IconFactory iconFactory = IconFactory.getInstance(); |
| Image zoomOut = iconFactory.getIcon("zoomminus"); //$NON-NLS-1$); |
| Image zoomIn = iconFactory.getIcon("zoomplus"); //$NON-NLS-1$); |
| Image zoom100 = iconFactory.getIcon("zoom100"); //$NON-NLS-1$); |
| Image close = iconFactory.getIcon("close"); //$NON-NLS-1$); |
| |
| gc.drawImage(zoomIn, x, y); |
| y += ZOOM_ICON_HEIGHT; |
| gc.drawImage(zoomOut, x, y); |
| y += ZOOM_ICON_HEIGHT; |
| gc.drawImage(zoom100, x, y); |
| y += ZOOM_ICON_HEIGHT; |
| gc.drawImage(close, x, y); |
| y += ZOOM_ICON_HEIGHT; |
| gc.setAlpha(oldAlpha); |
| } |
| } else if (mMode == CUSTOM) { |
| int rootX = getX(); |
| rootX += mHScale.getScaledImgSize(); |
| rootX += 2 * PREVIEW_HGAP; |
| int rootY = getY(); |
| rootY += 20; |
| gc.setFont(mCanvas.getFont()); |
| gc.setForeground(mCanvas.getDisplay().getSystemColor(SWT.COLOR_BLACK)); |
| gc.drawText("Add previews with \"Add as Thumbnail\"\nin the configuration menu", |
| rootX, rootY, true); |
| } |
| |
| if (mAnimation != null) { |
| mAnimation.tick(gc); |
| } |
| } |
| |
| private void addPreview(@NonNull RenderPreview preview) { |
| if (mPreviews == null) { |
| mPreviews = Lists.newArrayList(); |
| } |
| mPreviews.add(preview); |
| } |
| |
| /** Adds the current configuration as a new configuration preview */ |
| public void addAsThumbnail() { |
| ConfigurationChooser chooser = getChooser(); |
| String name = chooser.getConfiguration().getDisplayName(); |
| if (name == null || name.isEmpty()) { |
| name = getUniqueName(); |
| } |
| InputDialog d = new InputDialog( |
| AdtPlugin.getShell(), |
| "Add as Thumbnail Preview", // title |
| "Name of thumbnail:", |
| name, |
| null); |
| if (d.open() == Window.OK) { |
| selectMode(CUSTOM); |
| |
| String newName = d.getValue(); |
| // Create a new configuration from the current settings in the composite |
| Configuration configuration = Configuration.copy(chooser.getConfiguration()); |
| configuration.setDisplayName(newName); |
| |
| RenderPreview preview = RenderPreview.create(this, configuration); |
| addPreview(preview); |
| |
| layout(true); |
| beginRenderScheduling(); |
| scheduleRender(preview); |
| mCanvas.setFitScale(true /* onlyZoomOut */, false /*allowZoomIn*/); |
| |
| if (mManualList == null) { |
| loadList(); |
| } |
| if (mManualList != null) { |
| mManualList.add(preview); |
| saveList(); |
| } |
| } |
| } |
| |
| /** |
| * Computes a unique new name for a configuration preview that represents |
| * the current, default configuration |
| * |
| * @return a unique name |
| */ |
| private String getUniqueName() { |
| if (mPreviews == null || mPreviews.isEmpty()) { |
| // NO, not for the first preview! |
| return "Config1"; |
| } |
| |
| Set<String> names = new HashSet<String>(mPreviews.size()); |
| for (RenderPreview preview : mPreviews) { |
| names.add(preview.getDisplayName()); |
| } |
| |
| int index = 2; |
| while (true) { |
| String name = String.format("Config%1$d", index); |
| if (!names.contains(name)) { |
| return name; |
| } |
| index++; |
| } |
| } |
| |
| /** Generates a bunch of default configuration preview thumbnails */ |
| public void addDefaultPreviews() { |
| ConfigurationChooser chooser = getChooser(); |
| Configuration parent = chooser.getConfiguration(); |
| if (parent instanceof NestedConfiguration) { |
| parent = ((NestedConfiguration) parent).getParent(); |
| } |
| if (mCanvas.getImageOverlay().getImage() != null) { |
| // Create Language variation |
| createLocaleVariation(chooser, parent); |
| |
| // Vary screen size |
| // TODO: Be smarter here: Pick a screen that is both as differently as possible |
| // from the current screen as well as also supported. So consider |
| // things like supported screens, targetSdk etc. |
| createScreenVariations(parent); |
| |
| // Vary orientation |
| createStateVariation(chooser, parent); |
| |
| // Vary render target |
| createRenderTargetVariation(chooser, parent); |
| } |
| |
| // Also add in include-context previews, if any |
| addIncludedInPreviews(); |
| |
| // Make a placeholder preview for the current screen, in case we switch from it |
| RenderPreview preview = RenderPreview.create(this, parent); |
| mCanvas.setPreview(preview); |
| |
| sortPreviewsByOrientation(); |
| } |
| |
| private void createRenderTargetVariation(ConfigurationChooser chooser, Configuration parent) { |
| /* This is disabled for now: need to load multiple versions of layoutlib. |
| When I did this, there seemed to be some drug interactions between |
| them, and I would end up with NPEs in layoutlib code which normally works. |
| VaryingConfiguration configuration = |
| VaryingConfiguration.create(chooser, parent); |
| configuration.setAlternatingTarget(true); |
| configuration.syncFolderConfig(); |
| addPreview(RenderPreview.create(this, configuration)); |
| */ |
| } |
| |
| private void createStateVariation(ConfigurationChooser chooser, Configuration parent) { |
| State currentState = parent.getDeviceState(); |
| State nextState = parent.getNextDeviceState(currentState); |
| if (nextState != currentState) { |
| VaryingConfiguration configuration = |
| VaryingConfiguration.create(chooser, parent); |
| configuration.setAlternateDeviceState(true); |
| configuration.syncFolderConfig(); |
| addPreview(RenderPreview.create(this, configuration)); |
| } |
| } |
| |
| private void createLocaleVariation(ConfigurationChooser chooser, Configuration parent) { |
| LocaleQualifier currentLanguage = parent.getLocale().qualifier; |
| for (Locale locale : chooser.getLocaleList()) { |
| LocaleQualifier qualifier = locale.qualifier; |
| if (!qualifier.getLanguage().equals(currentLanguage.getLanguage())) { |
| VaryingConfiguration configuration = |
| VaryingConfiguration.create(chooser, parent); |
| configuration.setAlternateLocale(true); |
| configuration.syncFolderConfig(); |
| addPreview(RenderPreview.create(this, configuration)); |
| break; |
| } |
| } |
| } |
| |
| private void createScreenVariations(Configuration parent) { |
| ConfigurationChooser chooser = getChooser(); |
| VaryingConfiguration configuration; |
| |
| configuration = VaryingConfiguration.create(chooser, parent); |
| configuration.setVariation(0); |
| configuration.setAlternateDevice(true); |
| configuration.syncFolderConfig(); |
| addPreview(RenderPreview.create(this, configuration)); |
| |
| configuration = VaryingConfiguration.create(chooser, parent); |
| configuration.setVariation(1); |
| configuration.setAlternateDevice(true); |
| configuration.syncFolderConfig(); |
| addPreview(RenderPreview.create(this, configuration)); |
| } |
| |
| /** |
| * Returns the current mode as seen by this {@link RenderPreviewManager}. |
| * Note that it may not yet have been synced with the global mode kept in |
| * {@link AdtPrefs#getRenderPreviewMode()}. |
| * |
| * @return the current preview mode |
| */ |
| @NonNull |
| public RenderPreviewMode getMode() { |
| return mMode; |
| } |
| |
| /** |
| * Update the set of previews for the current mode |
| * |
| * @param force force a refresh even if the preview type has not changed |
| * @return true if the views were recomputed, false if the previews were |
| * already showing and the mode not changed |
| */ |
| public boolean recomputePreviews(boolean force) { |
| RenderPreviewMode newMode = AdtPrefs.getPrefs().getRenderPreviewMode(); |
| if (newMode == mMode && !force |
| && (mRevision == sRevision |
| || mMode == NONE |
| || mMode == CUSTOM)) { |
| return false; |
| } |
| |
| RenderPreviewMode oldMode = mMode; |
| mMode = newMode; |
| mRevision = sRevision; |
| |
| sScale = 1.0; |
| disposePreviews(); |
| |
| switch (mMode) { |
| case DEFAULT: |
| addDefaultPreviews(); |
| break; |
| case INCLUDES: |
| addIncludedInPreviews(); |
| break; |
| case LOCALES: |
| addLocalePreviews(); |
| break; |
| case SCREENS: |
| addScreenSizePreviews(); |
| break; |
| case VARIATIONS: |
| addVariationPreviews(); |
| break; |
| case CUSTOM: |
| addManualPreviews(); |
| break; |
| case NONE: |
| // Can't just set mNeedZoom because with no previews, the paint |
| // method does nothing |
| mCanvas.setFitScale(false /*onlyZoomOut*/, true /*allowZoomIn*/); |
| break; |
| default: |
| assert false : mMode; |
| } |
| |
| // We schedule layout for the next redraw rather than process it here immediately; |
| // not only does this let us avoid doing work for windows where the tab is in the |
| // background, but when a file is opened we may not know the size of the canvas |
| // yet, and the layout methods need it in order to do a good job. By the time |
| // the canvas is painted, we have accurate bounds. |
| mNeedLayout = mNeedRender = true; |
| mCanvas.redraw(); |
| |
| if (oldMode != mMode && (oldMode == NONE || mMode == NONE)) { |
| // If entering or exiting preview mode: updating padding which is compressed |
| // only in preview mode. |
| mCanvas.getHorizontalTransform().refresh(); |
| mCanvas.getVerticalTransform().refresh(); |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Sets the new render preview mode to use |
| * |
| * @param mode the new mode |
| */ |
| public void selectMode(@NonNull RenderPreviewMode mode) { |
| if (mode != mMode) { |
| AdtPrefs.getPrefs().setPreviewMode(mode); |
| recomputePreviews(false); |
| } |
| } |
| |
| /** Similar to {@link #addDefaultPreviews()} but for locales */ |
| public void addLocalePreviews() { |
| |
| ConfigurationChooser chooser = getChooser(); |
| List<Locale> locales = chooser.getLocaleList(); |
| Configuration parent = chooser.getConfiguration(); |
| |
| for (Locale locale : locales) { |
| if (!locale.hasLanguage() && !locale.hasRegion()) { |
| continue; |
| } |
| NestedConfiguration configuration = NestedConfiguration.create(chooser, parent); |
| configuration.setOverrideLocale(true); |
| configuration.setLocale(locale, false); |
| |
| String displayName = ConfigurationChooser.getLocaleLabel(chooser, locale, false); |
| assert displayName != null; // it's never non null when locale is non null |
| configuration.setDisplayName(displayName); |
| |
| addPreview(RenderPreview.create(this, configuration)); |
| } |
| |
| // Make a placeholder preview for the current screen, in case we switch from it |
| Configuration configuration = parent; |
| Locale locale = configuration.getLocale(); |
| String label = ConfigurationChooser.getLocaleLabel(chooser, locale, false); |
| if (label == null) { |
| label = "default"; |
| } |
| configuration.setDisplayName(label); |
| RenderPreview preview = RenderPreview.create(this, parent); |
| if (preview != null) { |
| mCanvas.setPreview(preview); |
| } |
| |
| // No need to sort: they should all be identical |
| } |
| |
| /** Similar to {@link #addDefaultPreviews()} but for screen sizes */ |
| public void addScreenSizePreviews() { |
| ConfigurationChooser chooser = getChooser(); |
| Collection<Device> devices = chooser.getDevices(); |
| Configuration configuration = chooser.getConfiguration(); |
| boolean canScaleNinePatch = configuration.supports(Capability.FIXED_SCALABLE_NINE_PATCH); |
| |
| // Rearrange the devices a bit such that the most interesting devices bubble |
| // to the front |
| // 10" tablet, 7" tablet, reference phones, tiny phone, and in general the first |
| // version of each seen screen size |
| List<Device> sorted = new ArrayList<Device>(devices); |
| Set<ScreenSize> seenSizes = new HashSet<ScreenSize>(); |
| State currentState = configuration.getDeviceState(); |
| String currentStateName = currentState != null ? currentState.getName() : ""; |
| |
| for (int i = 0, n = sorted.size(); i < n; i++) { |
| Device device = sorted.get(i); |
| boolean interesting = false; |
| |
| State state = device.getState(currentStateName); |
| if (state == null) { |
| state = device.getAllStates().get(0); |
| } |
| |
| if (device.getName().startsWith("Nexus ") //$NON-NLS-1$ |
| || device.getName().endsWith(" Nexus")) { //$NON-NLS-1$ |
| // Not String#contains("Nexus") because that would also pick up all the generic |
| // entries ("3.7in WVGA (Nexus One)") so we'd have them duplicated |
| interesting = true; |
| } |
| |
| FolderConfiguration c = DeviceConfigHelper.getFolderConfig(state); |
| if (c != null) { |
| ScreenSizeQualifier sizeQualifier = c.getScreenSizeQualifier(); |
| if (sizeQualifier != null) { |
| ScreenSize size = sizeQualifier.getValue(); |
| if (!seenSizes.contains(size)) { |
| seenSizes.add(size); |
| interesting = true; |
| } |
| } |
| |
| // Omit LDPI, not really used anymore |
| DensityQualifier density = c.getDensityQualifier(); |
| if (density != null) { |
| Density d = density.getValue(); |
| if (d == Density.LOW) { |
| interesting = false; |
| } |
| |
| if (!canScaleNinePatch && d == Density.TV) { |
| interesting = false; |
| } |
| } |
| } |
| |
| if (interesting) { |
| NestedConfiguration screenConfig = NestedConfiguration.create(chooser, |
| configuration); |
| screenConfig.setOverrideDevice(true); |
| screenConfig.setDevice(device, true); |
| screenConfig.syncFolderConfig(); |
| screenConfig.setDisplayName(ConfigurationChooser.getDeviceLabel(device, true)); |
| addPreview(RenderPreview.create(this, screenConfig)); |
| } |
| } |
| |
| // Sorted by screen size, in decreasing order |
| sortPreviewsByScreenSize(); |
| } |
| |
| /** |
| * Previews this layout as included in other layouts |
| */ |
| public void addIncludedInPreviews() { |
| ConfigurationChooser chooser = getChooser(); |
| IProject project = chooser.getProject(); |
| if (project == null) { |
| return; |
| } |
| IncludeFinder finder = IncludeFinder.get(project); |
| |
| final List<Reference> includedBy = finder.getIncludedBy(chooser.getEditedFile()); |
| |
| if (includedBy == null || includedBy.isEmpty()) { |
| // TODO: Generate some useful defaults, such as including it in a ListView |
| // as the list item layout? |
| return; |
| } |
| |
| for (final Reference reference : includedBy) { |
| String title = reference.getDisplayName(); |
| Configuration config = Configuration.create(chooser.getConfiguration(), |
| reference.getFile()); |
| RenderPreview preview = RenderPreview.create(this, config); |
| preview.setDisplayName(title); |
| preview.setIncludedWithin(reference); |
| |
| addPreview(preview); |
| } |
| |
| sortPreviewsByOrientation(); |
| } |
| |
| /** |
| * Previews this layout as included in other layouts |
| */ |
| public void addVariationPreviews() { |
| ConfigurationChooser chooser = getChooser(); |
| |
| IFile file = chooser.getEditedFile(); |
| List<IFile> variations = AdtUtils.getResourceVariations(file, false /*includeSelf*/); |
| |
| // Sort by parent folder |
| Collections.sort(variations, new Comparator<IFile>() { |
| @Override |
| public int compare(IFile file1, IFile file2) { |
| return file1.getParent().getName().compareTo(file2.getParent().getName()); |
| } |
| }); |
| |
| Configuration currentConfig = chooser.getConfiguration(); |
| |
| for (IFile variation : variations) { |
| String title = variation.getParent().getName(); |
| Configuration config = Configuration.create(chooser.getConfiguration(), variation); |
| config.setTheme(currentConfig.getTheme()); |
| config.setActivity(currentConfig.getActivity()); |
| RenderPreview preview = RenderPreview.create(this, config); |
| preview.setDisplayName(title); |
| preview.setAlternateInput(variation); |
| |
| addPreview(preview); |
| } |
| |
| sortPreviewsByOrientation(); |
| } |
| |
| /** |
| * Previews this layout using a custom configured set of layouts |
| */ |
| public void addManualPreviews() { |
| if (mManualList == null) { |
| loadList(); |
| } else { |
| mPreviews = mManualList.createPreviews(mCanvas); |
| } |
| } |
| |
| private void loadList() { |
| IProject project = getChooser().getProject(); |
| if (project == null) { |
| return; |
| } |
| |
| if (mManualList == null) { |
| mManualList = RenderPreviewList.get(project); |
| } |
| |
| try { |
| mManualList.load(getChooser().getDevices()); |
| mPreviews = mManualList.createPreviews(mCanvas); |
| } catch (IOException e) { |
| AdtPlugin.log(e, null); |
| } |
| } |
| |
| private void saveList() { |
| if (mManualList != null) { |
| try { |
| mManualList.save(); |
| } catch (IOException e) { |
| AdtPlugin.log(e, null); |
| } |
| } |
| } |
| |
| void rename(ConfigurationDescription description, String newName) { |
| IProject project = getChooser().getProject(); |
| if (project == null) { |
| return; |
| } |
| |
| if (mManualList == null) { |
| mManualList = RenderPreviewList.get(project); |
| } |
| description.displayName = newName; |
| saveList(); |
| } |
| |
| |
| /** |
| * Notifies that the main configuration has changed. |
| * |
| * @param flags the change flags, a bitmask corresponding to the |
| * {@code CHANGE_} constants in {@link ConfigurationClient} |
| */ |
| public void configurationChanged(int flags) { |
| // Similar to renderPreviews, but only acts on incomplete previews |
| if (hasPreviews()) { |
| // Do zoomed images first |
| beginRenderScheduling(); |
| for (RenderPreview preview : mPreviews) { |
| if (preview.getScale() > 1.2) { |
| preview.configurationChanged(flags); |
| } |
| } |
| for (RenderPreview preview : mPreviews) { |
| if (preview.getScale() <= 1.2) { |
| preview.configurationChanged(flags); |
| } |
| } |
| RenderPreview preview = mCanvas.getPreview(); |
| if (preview != null) { |
| preview.configurationChanged(flags); |
| preview.dispose(); |
| } |
| mNeedLayout = true; |
| mCanvas.redraw(); |
| } |
| } |
| |
| /** Updates the configuration preview thumbnails */ |
| public void renderPreviews() { |
| if (hasPreviews()) { |
| beginRenderScheduling(); |
| |
| // Process in visual order |
| ArrayList<RenderPreview> visualOrder = new ArrayList<RenderPreview>(mPreviews); |
| Collections.sort(visualOrder, RenderPreview.VISUAL_ORDER); |
| |
| // Do zoomed images first |
| for (RenderPreview preview : visualOrder) { |
| if (preview.getScale() > 1.2 && preview.isVisible()) { |
| scheduleRender(preview); |
| } |
| } |
| // Non-zoomed images |
| for (RenderPreview preview : visualOrder) { |
| if (preview.getScale() <= 1.2 && preview.isVisible()) { |
| scheduleRender(preview); |
| } |
| } |
| } |
| |
| mNeedRender = false; |
| } |
| |
| private int mPendingRenderCount; |
| |
| /** |
| * Reset rendering scheduling. The next render request will be scheduled |
| * after a single delay unit. |
| */ |
| public void beginRenderScheduling() { |
| mPendingRenderCount = 0; |
| } |
| |
| /** |
| * Schedule rendering the given preview. Each successive call will add an additional |
| * delay unit to the schedule from the previous {@link #scheduleRender(RenderPreview)} |
| * call, until {@link #beginRenderScheduling()} is called again. |
| * |
| * @param preview the preview to render |
| */ |
| public void scheduleRender(@NonNull RenderPreview preview) { |
| mPendingRenderCount++; |
| preview.render(mPendingRenderCount * RENDER_DELAY); |
| } |
| |
| /** |
| * Switch to the given configuration preview |
| * |
| * @param preview the preview to switch to |
| */ |
| public void switchTo(@NonNull RenderPreview preview) { |
| IFile input = preview.getAlternateInput(); |
| if (input != null) { |
| IWorkbenchPartSite site = mCanvas.getEditorDelegate().getEditor().getSite(); |
| try { |
| // This switches to the given file, but the file might not have |
| // an identical configuration to what was shown in the preview. |
| // For example, while viewing a 10" layout-xlarge file, it might |
| // show a preview for a 5" version tied to the default layout. If |
| // you click on it, it will open the default layout file, but it might |
| // be using a different screen size; any of those that match the |
| // default layout, say a 3.8". |
| // |
| // Thus, we need to also perform a screen size sync first |
| Configuration configuration = preview.getConfiguration(); |
| boolean setSize = false; |
| if (configuration instanceof NestedConfiguration) { |
| NestedConfiguration nestedConfig = (NestedConfiguration) configuration; |
| setSize = nestedConfig.isOverridingDevice(); |
| if (configuration instanceof VaryingConfiguration) { |
| VaryingConfiguration c = (VaryingConfiguration) configuration; |
| setSize |= c.isAlternatingDevice(); |
| } |
| |
| if (setSize) { |
| ConfigurationChooser chooser = getChooser(); |
| IFile editedFile = chooser.getEditedFile(); |
| if (editedFile != null) { |
| chooser.syncToVariations(CFG_DEVICE|CFG_DEVICE_STATE, |
| editedFile, configuration, false, false); |
| } |
| } |
| } |
| |
| IDE.openEditor(site.getWorkbenchWindow().getActivePage(), input, |
| CommonXmlEditor.ID); |
| } catch (PartInitException e) { |
| AdtPlugin.log(e, null); |
| } |
| return; |
| } |
| |
| GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor(); |
| ConfigurationChooser chooser = editor.getConfigurationChooser(); |
| |
| Configuration originalConfiguration = chooser.getConfiguration(); |
| |
| // The new configuration is the configuration which will become the configuration |
| // in the layout editor's chooser |
| Configuration previewConfiguration = preview.getConfiguration(); |
| Configuration newConfiguration = previewConfiguration; |
| if (newConfiguration instanceof NestedConfiguration) { |
| // Should never use a complementing configuration for the main |
| // rendering's configuration; instead, create a new configuration |
| // with a snapshot of the configuration's current values |
| newConfiguration = Configuration.copy(previewConfiguration); |
| |
| // Remap all the previews to be parented to this new copy instead |
| // of the old one (which is no longer controlled by the chooser) |
| for (RenderPreview p : mPreviews) { |
| Configuration configuration = p.getConfiguration(); |
| if (configuration instanceof NestedConfiguration) { |
| NestedConfiguration nested = (NestedConfiguration) configuration; |
| nested.setParent(newConfiguration); |
| } |
| } |
| } |
| |
| // Make a preview for the configuration which *was* showing in the |
| // chooser up until this point: |
| RenderPreview newPreview = mCanvas.getPreview(); |
| if (newPreview == null) { |
| newPreview = RenderPreview.create(this, originalConfiguration); |
| } |
| |
| // Update its configuration such that it is complementing or inheriting |
| // from the new chosen configuration |
| if (previewConfiguration instanceof VaryingConfiguration) { |
| VaryingConfiguration varying = VaryingConfiguration.create( |
| (VaryingConfiguration) previewConfiguration, |
| newConfiguration); |
| varying.updateDisplayName(); |
| originalConfiguration = varying; |
| newPreview.setConfiguration(originalConfiguration); |
| } else if (previewConfiguration instanceof NestedConfiguration) { |
| NestedConfiguration nested = NestedConfiguration.create( |
| (NestedConfiguration) previewConfiguration, |
| originalConfiguration, |
| newConfiguration); |
| nested.setDisplayName(nested.computeDisplayName()); |
| originalConfiguration = nested; |
| newPreview.setConfiguration(originalConfiguration); |
| } |
| |
| // Replace clicked preview with preview of the formerly edited main configuration |
| // This doesn't work yet because the image overlay has had its image |
| // replaced by the configuration previews! I should make a list of them |
| //newPreview.setFullImage(mImageOverlay.getAwtImage()); |
| for (int i = 0, n = mPreviews.size(); i < n; i++) { |
| if (preview == mPreviews.get(i)) { |
| mPreviews.set(i, newPreview); |
| break; |
| } |
| } |
| |
| // Stash the corresponding preview (not active) on the canvas so we can |
| // retrieve it if clicking to some other preview later |
| mCanvas.setPreview(preview); |
| preview.setVisible(false); |
| |
| // Switch to the configuration from the clicked preview (though it's |
| // most likely a copy, see above) |
| chooser.setConfiguration(newConfiguration); |
| editor.changed(MASK_ALL); |
| |
| // Scroll to the top again, if necessary |
| mCanvas.getVerticalBar().setSelection(mCanvas.getVerticalBar().getMinimum()); |
| |
| mNeedLayout = mNeedZoom = true; |
| mCanvas.redraw(); |
| mAnimation = new SwapAnimation(preview, newPreview); |
| } |
| |
| /** |
| * Gets the preview at the given location, or null if none. This is |
| * currently deeply tied to where things are painted in onPaint(). |
| */ |
| RenderPreview getPreview(ControlPoint mousePos) { |
| if (hasPreviews()) { |
| int rootX = getX(); |
| if (mousePos.x < rootX) { |
| return null; |
| } |
| int rootY = getY(); |
| |
| for (RenderPreview preview : mPreviews) { |
| int x = rootX + preview.getX(); |
| int y = rootY + preview.getY(); |
| if (mousePos.x >= x && mousePos.x <= x + preview.getWidth()) { |
| if (mousePos.y >= y && mousePos.y <= y + preview.getHeight()) { |
| return preview; |
| } |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| private int getX() { |
| return mHScale.translate(0); |
| } |
| |
| private int getY() { |
| return mVScale.translate(0); |
| } |
| |
| private int getZoomX() { |
| Rectangle clientArea = mCanvas.getClientArea(); |
| int x = clientArea.x + clientArea.width - ZOOM_ICON_WIDTH; |
| if (x < mHScale.getScaledImgSize() + PREVIEW_HGAP) { |
| // No visible previews because the main image is zoomed too far |
| return -1; |
| } |
| |
| return x - 6; |
| } |
| |
| private int getZoomY() { |
| Rectangle clientArea = mCanvas.getClientArea(); |
| return clientArea.y + 5; |
| } |
| |
| /** |
| * Returns the height of the layout |
| * |
| * @return the height |
| */ |
| public int getHeight() { |
| return mLayoutHeight; |
| } |
| |
| /** |
| * Notifies that preview manager that the mouse cursor has moved to the |
| * given control position within the layout canvas |
| * |
| * @param mousePos the mouse position, relative to the layout canvas |
| */ |
| public void moved(ControlPoint mousePos) { |
| RenderPreview hovered = getPreview(mousePos); |
| if (hovered != mActivePreview) { |
| if (mActivePreview != null) { |
| mActivePreview.setActive(false); |
| } |
| mActivePreview = hovered; |
| if (mActivePreview != null) { |
| mActivePreview.setActive(true); |
| } |
| mCanvas.redraw(); |
| } |
| } |
| |
| /** |
| * Notifies that preview manager that the mouse cursor has entered the layout canvas |
| * |
| * @param mousePos the mouse position, relative to the layout canvas |
| */ |
| public void enter(ControlPoint mousePos) { |
| moved(mousePos); |
| } |
| |
| /** |
| * Notifies that preview manager that the mouse cursor has exited the layout canvas |
| * |
| * @param mousePos the mouse position, relative to the layout canvas |
| */ |
| public void exit(ControlPoint mousePos) { |
| if (mActivePreview != null) { |
| mActivePreview.setActive(false); |
| } |
| mActivePreview = null; |
| mCanvas.redraw(); |
| } |
| |
| /** |
| * Process a mouse click, and return true if it was handled by this manager |
| * (e.g. the click was on a preview) |
| * |
| * @param mousePos the mouse position where the click occurred |
| * @return true if the click occurred over a preview and was handled, false otherwise |
| */ |
| public boolean click(ControlPoint mousePos) { |
| // Clicked zoom? |
| int x = getZoomX(); |
| if (x > 0) { |
| if (mousePos.x >= x && mousePos.x <= x + ZOOM_ICON_WIDTH) { |
| int y = getZoomY(); |
| if (mousePos.y >= y && mousePos.y <= y + 4 * ZOOM_ICON_HEIGHT) { |
| if (mousePos.y < y + ZOOM_ICON_HEIGHT) { |
| zoomIn(); |
| } else if (mousePos.y < y + 2 * ZOOM_ICON_HEIGHT) { |
| zoomOut(); |
| } else if (mousePos.y < y + 3 * ZOOM_ICON_HEIGHT) { |
| zoomReset(); |
| } else { |
| selectMode(NONE); |
| } |
| return true; |
| } |
| } |
| } |
| |
| RenderPreview preview = getPreview(mousePos); |
| if (preview != null) { |
| boolean handled = preview.click(mousePos.x - getX() - preview.getX(), |
| mousePos.y - getY() - preview.getY()); |
| if (handled) { |
| // In case layout was performed, there could be a new preview |
| // under this coordinate now, so make sure it's hover etc |
| // shows up |
| moved(mousePos); |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Returns true if there are thumbnail previews |
| * |
| * @return true if thumbnails are being shown |
| */ |
| public boolean hasPreviews() { |
| return mPreviews != null && !mPreviews.isEmpty(); |
| } |
| |
| |
| private void sortPreviewsByScreenSize() { |
| if (mPreviews != null) { |
| Collections.sort(mPreviews, new Comparator<RenderPreview>() { |
| @Override |
| public int compare(RenderPreview preview1, RenderPreview preview2) { |
| Configuration config1 = preview1.getConfiguration(); |
| Configuration config2 = preview2.getConfiguration(); |
| Device device1 = config1.getDevice(); |
| Device device2 = config1.getDevice(); |
| if (device1 != null && device2 != null) { |
| Screen screen1 = device1.getDefaultHardware().getScreen(); |
| Screen screen2 = device2.getDefaultHardware().getScreen(); |
| if (screen1 != null && screen2 != null) { |
| double delta = screen1.getDiagonalLength() |
| - screen2.getDiagonalLength(); |
| if (delta != 0.0) { |
| return (int) Math.signum(delta); |
| } else { |
| if (screen1.getPixelDensity() != screen2.getPixelDensity()) { |
| return screen1.getPixelDensity().compareTo( |
| screen2.getPixelDensity()); |
| } |
| } |
| } |
| |
| } |
| State state1 = config1.getDeviceState(); |
| State state2 = config2.getDeviceState(); |
| if (state1 != state2 && state1 != null && state2 != null) { |
| return state1.getName().compareTo(state2.getName()); |
| } |
| |
| return preview1.getDisplayName().compareTo(preview2.getDisplayName()); |
| } |
| }); |
| } |
| } |
| |
| private void sortPreviewsByOrientation() { |
| if (mPreviews != null) { |
| Collections.sort(mPreviews, new Comparator<RenderPreview>() { |
| @Override |
| public int compare(RenderPreview preview1, RenderPreview preview2) { |
| Configuration config1 = preview1.getConfiguration(); |
| Configuration config2 = preview2.getConfiguration(); |
| State state1 = config1.getDeviceState(); |
| State state2 = config2.getDeviceState(); |
| if (state1 != state2 && state1 != null && state2 != null) { |
| return state1.getName().compareTo(state2.getName()); |
| } |
| |
| return preview1.getDisplayName().compareTo(preview2.getDisplayName()); |
| } |
| }); |
| } |
| } |
| |
| /** |
| * Vertical scrollbar listener which updates render previews which are not |
| * visible and triggers a redraw |
| */ |
| private class ScrollBarListener implements SelectionListener { |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| if (mPreviews == null) { |
| return; |
| } |
| |
| ScrollBar bar = mCanvas.getVerticalBar(); |
| int selection = bar.getSelection(); |
| int thumb = bar.getThumb(); |
| int maxY = selection + thumb; |
| beginRenderScheduling(); |
| for (RenderPreview preview : mPreviews) { |
| if (!preview.isVisible() && preview.getY() <= maxY) { |
| preview.setVisible(true); |
| } |
| } |
| } |
| |
| @Override |
| public void widgetDefaultSelected(SelectionEvent e) { |
| } |
| } |
| |
| /** Animation overlay shown briefly after swapping two previews */ |
| private class SwapAnimation implements Runnable { |
| private long begin; |
| private long end; |
| private static final long DURATION = 400; // ms |
| private Rect initialRect1; |
| private Rect targetRect1; |
| private Rect initialRect2; |
| private Rect targetRect2; |
| private RenderPreview preview; |
| |
| SwapAnimation(RenderPreview preview1, RenderPreview preview2) { |
| begin = System.currentTimeMillis(); |
| end = begin + DURATION; |
| |
| initialRect1 = new Rect(preview1.getX(), preview1.getY(), |
| preview1.getWidth(), preview1.getHeight()); |
| |
| CanvasTransform hi = mCanvas.getHorizontalTransform(); |
| CanvasTransform vi = mCanvas.getVerticalTransform(); |
| initialRect2 = new Rect(hi.translate(0), vi.translate(0), |
| hi.getScaledImgSize(), vi.getScaledImgSize()); |
| preview = preview2; |
| } |
| |
| void tick(GC gc) { |
| long now = System.currentTimeMillis(); |
| if (now > end || mCanvas.isDisposed()) { |
| mAnimation = null; |
| return; |
| } |
| |
| CanvasTransform hi = mCanvas.getHorizontalTransform(); |
| CanvasTransform vi = mCanvas.getVerticalTransform(); |
| if (targetRect1 == null) { |
| targetRect1 = new Rect(hi.translate(0), vi.translate(0), |
| hi.getScaledImgSize(), vi.getScaledImgSize()); |
| } |
| double portion = (now - begin) / (double) DURATION; |
| Rect rect1 = new Rect( |
| (int) (portion * (targetRect1.x - initialRect1.x) + initialRect1.x), |
| (int) (portion * (targetRect1.y - initialRect1.y) + initialRect1.y), |
| (int) (portion * (targetRect1.w - initialRect1.w) + initialRect1.w), |
| (int) (portion * (targetRect1.h - initialRect1.h) + initialRect1.h)); |
| |
| if (targetRect2 == null) { |
| targetRect2 = new Rect(preview.getX(), preview.getY(), |
| preview.getWidth(), preview.getHeight()); |
| } |
| portion = (now - begin) / (double) DURATION; |
| Rect rect2 = new Rect( |
| (int) (portion * (targetRect2.x - initialRect2.x) + initialRect2.x), |
| (int) (portion * (targetRect2.y - initialRect2.y) + initialRect2.y), |
| (int) (portion * (targetRect2.w - initialRect2.w) + initialRect2.w), |
| (int) (portion * (targetRect2.h - initialRect2.h) + initialRect2.h)); |
| |
| gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_GRAY)); |
| gc.drawRectangle(rect1.x, rect1.y, rect1.w, rect1.h); |
| gc.drawRectangle(rect2.x, rect2.y, rect2.w, rect2.h); |
| |
| mCanvas.getDisplay().timerExec(5, this); |
| } |
| |
| @Override |
| public void run() { |
| mCanvas.redraw(); |
| } |
| } |
| |
| /** |
| * Notifies the {@linkplain RenderPreviewManager} that the configuration used |
| * in the main chooser has been changed. This may require updating parent references |
| * in the preview configurations inheriting from it. |
| * |
| * @param oldConfiguration the previous configuration |
| * @param newConfiguration the new configuration in the chooser |
| */ |
| public void updateChooserConfig( |
| @NonNull Configuration oldConfiguration, |
| @NonNull Configuration newConfiguration) { |
| if (hasPreviews()) { |
| for (RenderPreview preview : mPreviews) { |
| Configuration configuration = preview.getConfiguration(); |
| if (configuration instanceof NestedConfiguration) { |
| NestedConfiguration nestedConfig = (NestedConfiguration) configuration; |
| if (nestedConfig.getParent() == oldConfiguration) { |
| nestedConfig.setParent(newConfiguration); |
| } |
| } |
| } |
| } |
| } |
| } |