blob: 8df5efb0322ff4a7e85a1c335f4c5ba7d3b8a02f [file] [log] [blame]
/*
* 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;
}
}