| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 |
| * |
| * 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.tools.idea.ddms.screenshot; |
| |
| import com.android.SdkConstants; |
| import com.android.ddmlib.IDevice; |
| import com.android.resources.ScreenOrientation; |
| import com.android.tools.idea.rendering.ImageUtils; |
| import com.intellij.icons.AllIcons; |
| import com.intellij.openapi.actionSystem.DataProvider; |
| import com.intellij.openapi.actionSystem.PlatformDataKeys; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.fileChooser.FileChooserFactory; |
| import com.intellij.openapi.fileChooser.FileSaverDescriptor; |
| import com.intellij.openapi.fileChooser.FileSaverDialog; |
| import com.intellij.openapi.fileEditor.FileEditorProvider; |
| import com.intellij.openapi.fileEditor.ex.FileEditorProviderManager; |
| import com.intellij.openapi.progress.ProgressIndicator; |
| import com.intellij.openapi.progress.Task; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.ui.DialogWrapper; |
| import com.intellij.openapi.ui.Messages; |
| import com.intellij.openapi.util.Condition; |
| import com.intellij.openapi.vfs.*; |
| import com.intellij.ui.components.JBScrollPane; |
| import com.intellij.util.containers.ContainerUtil; |
| import org.intellij.images.editor.ImageEditor; |
| import org.intellij.images.editor.ImageFileEditor; |
| import org.intellij.images.editor.ImageZoomModel; |
| import org.jetbrains.android.util.AndroidBundle; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.imageio.ImageIO; |
| import javax.swing.*; |
| import java.awt.event.ActionEvent; |
| import java.awt.event.ActionListener; |
| import java.awt.image.BufferedImage; |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.Calendar; |
| import java.util.List; |
| import java.util.concurrent.atomic.AtomicReference; |
| |
| public class ScreenshotViewer extends DialogWrapper implements DataProvider { |
| @NonNls private static final String SCREENSHOT_VIEWER_DIMENSIONS_KEY = "ScreenshotViewer.Dimensions"; |
| |
| private static VirtualFile ourLastSavedFolder = null; |
| |
| private final Project myProject; |
| private final IDevice myDevice; |
| |
| private final VirtualFile myBackingVirtualFile; |
| private final ImageFileEditor myImageFileEditor; |
| private final FileEditorProvider myProvider; |
| |
| private final List<DeviceArtDescriptor> myDeviceArtDescriptors; |
| |
| private JPanel myPanel; |
| private JButton myRefreshButton; |
| private JButton myRotateButton; |
| private JBScrollPane myScrollPane; |
| private JCheckBox myFrameScreenshotCheckBox; |
| private JComboBox myDeviceArtCombo; |
| private JCheckBox myDropShadowCheckBox; |
| private JCheckBox myScreenGlareCheckBox; |
| |
| /** Angle in degrees by which the screenshot from the device has been rotated. One of 0, 90, 180 or 270. */ |
| private int myRotationAngle = 0; |
| |
| /** |
| * Reference to the screenshot obtained from the device and then rotated by {@link #myRotationAngle} degrees. |
| * Accessed from both EDT and background threads. |
| */ |
| private AtomicReference<BufferedImage> mySourceImageRef = new AtomicReference<BufferedImage>(); |
| |
| /** Reference to the framed screenshot displayed on screen. Accessed from both EDT and background threads. */ |
| private AtomicReference<BufferedImage> myDisplayedImageRef = new AtomicReference<BufferedImage>(); |
| |
| /** User specified destination where the screenshot is saved. */ |
| private File myScreenshotFile; |
| |
| public ScreenshotViewer(@NotNull Project project, |
| @NotNull BufferedImage image, |
| @NotNull File backingFile, |
| @Nullable IDevice device, |
| @Nullable String deviceModel) { |
| super(project, true); |
| |
| myProject = project; |
| myDevice = device; |
| mySourceImageRef.set(image); |
| myDisplayedImageRef.set(image); |
| |
| myBackingVirtualFile = LocalFileSystem.getInstance().findFileByIoFile(backingFile); |
| assert myBackingVirtualFile != null; |
| |
| myRefreshButton.setIcon(AllIcons.Actions.Refresh); |
| myRefreshButton.setEnabled(device != null); |
| myRotateButton.setIcon(AllIcons.Actions.AllRight); |
| |
| myProvider = getImageFileEditorProvider(); |
| myImageFileEditor = (ImageFileEditor)myProvider.createEditor(myProject, myBackingVirtualFile); |
| myScrollPane.getViewport().add(myImageFileEditor.getComponent()); |
| |
| ActionListener l = new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent actionEvent) { |
| if (actionEvent.getSource() == myRefreshButton) { |
| doRefreshScreenshot(); |
| } else if (actionEvent.getSource() == myRotateButton) { |
| doRotateScreenshot(); |
| } else if (actionEvent.getSource() == myFrameScreenshotCheckBox |
| || actionEvent.getSource() == myDeviceArtCombo |
| || actionEvent.getSource() == myDropShadowCheckBox |
| || actionEvent.getSource() == myScreenGlareCheckBox) { |
| doFrameScreenshot(); |
| } |
| } |
| }; |
| |
| myRefreshButton.addActionListener(l); |
| myRotateButton.addActionListener(l); |
| myFrameScreenshotCheckBox.addActionListener(l); |
| myDeviceArtCombo.addActionListener(l); |
| myDropShadowCheckBox.addActionListener(l); |
| myScreenGlareCheckBox.addActionListener(l); |
| |
| myDeviceArtDescriptors = getDescriptorsToFrame(image); |
| String[] titles = new String[myDeviceArtDescriptors.size()]; |
| for (int i = 0; i < myDeviceArtDescriptors.size(); i++) { |
| titles[i] = myDeviceArtDescriptors.get(i).getName(); |
| } |
| DefaultComboBoxModel model = new DefaultComboBoxModel(titles); |
| myDeviceArtCombo.setModel(model); |
| |
| // Set the default device art descriptor selection |
| myDeviceArtCombo.setSelectedIndex(getDefaultDescriptor(myDeviceArtDescriptors, image, deviceModel)); |
| |
| setModal(false); |
| init(); |
| } |
| |
| // returns the list of descriptors capable of framing the given image |
| private List<DeviceArtDescriptor> getDescriptorsToFrame(final BufferedImage image) { |
| double imgAspectRatio = image.getWidth() / (double) image.getHeight(); |
| final ScreenOrientation orientation = |
| imgAspectRatio >= (1 - ImageUtils.EPSILON) ? ScreenOrientation.LANDSCAPE : ScreenOrientation.PORTRAIT; |
| |
| List<DeviceArtDescriptor> allDescriptors = DeviceArtDescriptor.getDescriptors(null); |
| return ContainerUtil.filter(allDescriptors, new Condition<DeviceArtDescriptor>() { |
| @Override |
| public boolean value(DeviceArtDescriptor descriptor) { |
| return descriptor.canFrameImage(image, orientation); |
| } |
| }); |
| } |
| |
| private static int getDefaultDescriptor(List<DeviceArtDescriptor> deviceArtDescriptors, BufferedImage image, |
| @Nullable String deviceModel) { |
| int index = -1; |
| |
| if (deviceModel != null) { |
| index = findDescriptorIndexForProduct(deviceArtDescriptors, deviceModel); |
| } |
| |
| if (index < 0) { |
| // Assume that if the min resolution is > 1280, then we are on a tablet |
| String defaultDevice = Math.min(image.getWidth(), image.getHeight()) > 1280 ? "Generic Tablet" : "Generic Phone"; |
| index = findDescriptorIndexForProduct(deviceArtDescriptors, defaultDevice); |
| } |
| |
| // If we can't find anything (which shouldn't happen since we should get the Generic Phone/Tablet), |
| // default to the first one. |
| if (index < 0) { |
| index = 0; |
| } |
| |
| return index; |
| } |
| |
| private static int findDescriptorIndexForProduct(List<DeviceArtDescriptor> descriptors, String deviceModel) { |
| for (int i = 0; i < descriptors.size(); i++) { |
| DeviceArtDescriptor d = descriptors.get(i); |
| if (d.getName().equalsIgnoreCase(deviceModel)) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| @Override |
| protected void dispose() { |
| myProvider.disposeEditor(myImageFileEditor); |
| super.dispose(); |
| } |
| |
| private void doRefreshScreenshot() { |
| assert myDevice != null; |
| new ScreenshotTask(myProject, myDevice) { |
| @Override |
| public void onSuccess() { |
| String msg = getError(); |
| if (msg != null) { |
| Messages.showErrorDialog(myProject, msg, AndroidBundle.message("android.ddms.actions.screenshot")); |
| return; |
| } |
| |
| BufferedImage image = getScreenshot(); |
| mySourceImageRef.set(image); |
| processScreenshot(myFrameScreenshotCheckBox.isSelected(), myRotationAngle); |
| } |
| }.queue(); |
| } |
| |
| private void doRotateScreenshot() { |
| myRotationAngle = (myRotationAngle + 90) % 360; |
| processScreenshot(myFrameScreenshotCheckBox.isSelected(), 90); |
| } |
| |
| private void doFrameScreenshot() { |
| boolean shouldFrame = myFrameScreenshotCheckBox.isSelected(); |
| |
| myDeviceArtCombo.setEnabled(shouldFrame); |
| myDropShadowCheckBox.setEnabled(shouldFrame); |
| myScreenGlareCheckBox.setEnabled(shouldFrame); |
| |
| if (shouldFrame) { |
| processScreenshot(true, 0); |
| } else { |
| myDisplayedImageRef.set(mySourceImageRef.get()); |
| updateEditorImage(); |
| } |
| } |
| |
| private void processScreenshot(boolean addFrame, int rotateByAngle) { |
| DeviceArtDescriptor spec = addFrame ? myDeviceArtDescriptors.get(myDeviceArtCombo.getSelectedIndex()) : null; |
| boolean shadow = addFrame && myDropShadowCheckBox.isSelected(); |
| boolean reflection = addFrame && myScreenGlareCheckBox.isSelected(); |
| |
| new ImageProcessorTask(myProject, mySourceImageRef.get(), rotateByAngle, spec, shadow, reflection, myBackingVirtualFile) { |
| @Override |
| public void onSuccess() { |
| mySourceImageRef.set(getRotatedImage()); |
| myDisplayedImageRef.set(getProcessedImage()); |
| updateEditorImage(); |
| } |
| }.queue(); |
| } |
| |
| private static class ImageProcessorTask extends Task.Modal { |
| private final BufferedImage mySrcImage; |
| private final int myRotationAngle; |
| private final DeviceArtDescriptor myDescriptor; |
| private final boolean myAddShadow; |
| private final boolean myAddReflection; |
| private final VirtualFile myDestinationFile; |
| |
| private BufferedImage myRotatedImage; |
| private BufferedImage myProcessedImage; |
| |
| public ImageProcessorTask(@Nullable Project project, |
| @NotNull BufferedImage srcImage, |
| int rotateByAngle, |
| @Nullable DeviceArtDescriptor descriptor, |
| boolean addShadow, |
| boolean addReflection, |
| VirtualFile writeToFile) { |
| super(project, AndroidBundle.message("android.ddms.screenshot.image.processor.task.title"), false); |
| |
| mySrcImage = srcImage; |
| myRotationAngle = rotateByAngle; |
| myDescriptor = descriptor; |
| myAddShadow = addShadow; |
| myAddReflection = addReflection; |
| myDestinationFile = writeToFile; |
| } |
| |
| @Override |
| public void run(@NotNull ProgressIndicator indicator) { |
| if (myRotationAngle != 0) { |
| myRotatedImage = ImageUtils.rotateByRightAngle(mySrcImage, myRotationAngle); |
| } else { |
| myRotatedImage = mySrcImage; |
| } |
| |
| if (myDescriptor != null) { |
| myProcessedImage = DeviceArtPainter.createFrame(myRotatedImage, myDescriptor, myAddShadow, myAddReflection); |
| } else { |
| myProcessedImage = myRotatedImage; |
| } |
| |
| myProcessedImage = ImageUtils.cropBlank(myProcessedImage, null); |
| |
| // update backing file, this is necessary for operations that read the backing file from the editor, |
| // such as: Right click image -> Open in external editor |
| if (myDestinationFile != null) { |
| File file = VfsUtilCore.virtualToIoFile(myDestinationFile); |
| try { |
| ImageIO.write(myProcessedImage, SdkConstants.EXT_PNG, file); |
| } |
| catch (IOException e) { |
| Logger.getInstance(ImageProcessorTask.class).error("Unexpected error while writing to backing file", e); |
| } |
| } |
| } |
| |
| protected BufferedImage getProcessedImage() { |
| return myProcessedImage; |
| } |
| |
| protected BufferedImage getRotatedImage() { |
| return myRotatedImage; |
| } |
| } |
| |
| private void updateEditorImage() { |
| BufferedImage image = myDisplayedImageRef.get(); |
| ImageEditor imageEditor = myImageFileEditor.getImageEditor(); |
| |
| ImageZoomModel zoomModel = imageEditor.getZoomModel(); |
| double zoom = zoomModel.getZoomFactor(); |
| |
| imageEditor.getDocument().setValue(image); |
| pack(); |
| |
| zoomModel.setZoomFactor(zoom); |
| } |
| |
| private FileEditorProvider getImageFileEditorProvider() { |
| FileEditorProvider[] providers = FileEditorProviderManager.getInstance().getProviders(myProject, myBackingVirtualFile); |
| assert providers.length > 0; |
| |
| // Note: In case there are multiple providers for image files, we'd prefer to get the bundled |
| // image editor, but we don't have access to any of its implementation details so we rely |
| // on the editor type id being "images" as defined by ImageFileEditorProvider#EDITOR_TYPE_ID. |
| for (FileEditorProvider p : providers) { |
| if (p.getEditorTypeId().equals("images")) { |
| return p; |
| } |
| } |
| |
| return providers[0]; |
| } |
| |
| @Nullable |
| @Override |
| protected JComponent createCenterPanel() { |
| return myPanel; |
| } |
| |
| @NonNls |
| @Override |
| @Nullable |
| protected String getDimensionServiceKey() { |
| return SCREENSHOT_VIEWER_DIMENSIONS_KEY; |
| } |
| |
| @Nullable |
| @Override |
| public Object getData(@NonNls String dataId) { |
| // This is required since the Image Editor's actions are dependent on the context |
| // being a ImageFileEditor. |
| return PlatformDataKeys.FILE_EDITOR.getName().equals(dataId) ? myImageFileEditor : null; |
| } |
| |
| @Override |
| protected void createDefaultActions() { |
| super.createDefaultActions(); |
| getOKAction().putValue(Action.NAME, AndroidBundle.message("android.ddms.screenshot.save.ok.button.text")); |
| } |
| |
| @Override |
| protected void doOKAction() { |
| FileSaverDescriptor descriptor = |
| new FileSaverDescriptor(AndroidBundle.message("android.ddms.screenshot.save.title"), "", SdkConstants.EXT_PNG); |
| FileSaverDialog saveFileDialog = FileChooserFactory.getInstance().createSaveFileDialog(descriptor, myProject); |
| VirtualFile baseDir = ourLastSavedFolder != null ? ourLastSavedFolder : myProject.getBaseDir(); |
| VirtualFileWrapper fileWrapper = saveFileDialog.save(baseDir, getDefaultFileName()); |
| if (fileWrapper == null) { |
| return; |
| } |
| |
| myScreenshotFile = fileWrapper.getFile(); |
| try { |
| ImageIO.write(myDisplayedImageRef.get(), SdkConstants.EXT_PNG, myScreenshotFile); |
| } |
| catch (IOException e) { |
| Messages.showErrorDialog(myProject, |
| AndroidBundle.message("android.ddms.screenshot.save.error", e), |
| AndroidBundle.message("android.ddms.actions.screenshot")); |
| return; |
| } |
| |
| VirtualFile virtualFile = fileWrapper.getVirtualFile(); |
| if (virtualFile != null) { |
| //noinspection AssignmentToStaticFieldFromInstanceMethod |
| ourLastSavedFolder = virtualFile.getParent(); |
| } |
| |
| super.doOKAction(); |
| } |
| |
| private String getDefaultFileName() { |
| Calendar now = Calendar.getInstance(); |
| return String.format("%s-%tF-%tH%tM%tS.png", myDevice != null ? "device" : "layout", now, now, now, now); |
| } |
| |
| public File getScreenshot() { |
| return myScreenshotFile; |
| } |
| } |