/*
* Copyright (C) 2014 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.avdmanager;
import com.android.resources.Density;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.devices.Storage;
import com.android.sdklib.internal.avd.AvdInfo;
import com.android.tools.idea.templates.TemplateUtils;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.IconLoader;
import com.intellij.psi.util.FileTypeUtils;
import com.intellij.ui.IdeBorderFactory;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.table.TableView;
import com.intellij.util.ui.AbstractTableCellEditor;
import com.intellij.util.ui.ColumnInfo;
import com.intellij.util.ui.ListTableModel;
import icons.AndroidIcons;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.util.*;
import java.util.List;
/**
* A UI component which lists the existing AVDs
*/
public class AvdDisplayList extends JPanel implements ListSelectionListener, AvdActionPanel.AvdRefreshProvider,
AvdUiAction.AvdInfoProvider {
private static final Logger LOG = Logger.getInstance(AvdDisplayList.class);
public static final String NONEMPTY = "nonempty";
public static final String EMPTY = "empty";
private final JButton myRefreshButton = new JButton(AllIcons.Actions.Refresh);
private final JPanel myCenterCardPanel;
private TableView<AvdInfo> myTable;
private ListTableModel<AvdInfo> myModel = new ListTableModel<AvdInfo>();
private Set<AvdSelectionListener> myListeners = Sets.newHashSet();
/**
* Components which wish to receive a notification when the user has selected an AVD from this
* table must implement this interface and register themselves through {@link #addSelectionListener(AvdSelectionListener)}
*/
public interface AvdSelectionListener {
void onAvdSelected(@Nullable AvdInfo avdInfo);
}
public AvdDisplayList() {
myModel.setColumnInfos(ourColumnInfos);
myModel.setSortable(true);
myTable = new TableView<AvdInfo>();
myTable.setModelAndUpdateColumns(myModel);
setLayout(new BorderLayout());
myCenterCardPanel = new JPanel(new CardLayout());
myCenterCardPanel.add(ScrollPaneFactory.createScrollPane(myTable), NONEMPTY);
myCenterCardPanel.add(new EmptyAvdListPanel(this), EMPTY);
add(myCenterCardPanel, BorderLayout.CENTER);
JPanel southPanel = new JPanel(new BorderLayout());
southPanel.add(myRefreshButton, BorderLayout.EAST);
myRefreshButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
refreshAvds();
}
});
CreateAvdAction createAvdAction = new CreateAvdAction(this);
JButton newButton = new JButton(createAvdAction);
newButton.setIcon(createAvdAction.getIcon());
newButton.setText(createAvdAction.getText());
southPanel.add(newButton, BorderLayout.WEST);
add(southPanel, BorderLayout.SOUTH);
myTable.getSelectionModel().addListSelectionListener(this);
myTable.addMouseListener(myEditingListener);
myTable.addMouseMotionListener(myEditingListener);
refreshAvds();
}
public void addSelectionListener(AvdSelectionListener listener) {
myListeners.add(listener);
}
public void removeSelectionListener(AvdSelectionListener listener) {
myListeners.remove(listener);
}
/**
* This class implements the table selection interface and passes the selection events on to its listeners.
* @param e
*/
@Override
public void valueChanged(ListSelectionEvent e) {
AvdInfo selected = myTable.getSelectedObject();
for (AvdSelectionListener listener : myListeners) {
listener.onAvdSelected(selected);
}
}
@Nullable
@Override
public AvdInfo getAvdInfo() {
return myTable.getSelectedObject();
}
/**
* Reload AVD definitions from disk and repopulate the table
*/
@Override
public void refreshAvds() {
List<AvdInfo> avds = AvdManagerConnection.getAvds(true);
myModel.setItems(avds);
if (avds.isEmpty()) {
((CardLayout)myCenterCardPanel.getLayout()).show(myCenterCardPanel, EMPTY);
} else {
((CardLayout)myCenterCardPanel.getLayout()).show(myCenterCardPanel, NONEMPTY);
}
}
private final MouseAdapter myEditingListener = new MouseAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
possiblySwitchEditors(e);
}
@Override
public void mouseEntered(MouseEvent e) {
possiblySwitchEditors(e);
}
@Override
public void mouseExited(MouseEvent e) {
possiblySwitchEditors(e);
}
@Override
public void mouseClicked(MouseEvent e) {
possiblySwitchEditors(e);
}
@Override
public void mousePressed(MouseEvent e) {
possiblyShowPopup(e);
}
@Override
public void mouseReleased(MouseEvent e) {
possiblyShowPopup(e);
}
};
private void possiblySwitchEditors(MouseEvent e) {
Point p = e.getPoint();
int row = myTable.rowAtPoint(p);
int col = myTable.columnAtPoint(p);
if (row != myTable.getEditingRow() || col != myTable.getEditingColumn()) {
if (row != -1 && col != -1 && myTable.isCellEditable(row, col)) {
myTable.editCellAt(row, col);
}
}
}
private void possiblyShowPopup(MouseEvent e) {
if (!e.isPopupTrigger()) {
return;
}
Point p = e.getPoint();
int row = myTable.rowAtPoint(p);
int col = myTable.columnAtPoint(p);
if (row != -1 && col != -1) {
int lastColumn = myTable.getColumnCount() - 1;
Component maybeActionPanel = myTable.getCellRenderer(row, lastColumn).
getTableCellRendererComponent(myTable, myTable.getValueAt(row, lastColumn), false, true, row, lastColumn);
if (maybeActionPanel instanceof AvdActionPanel) {
((AvdActionPanel)maybeActionPanel).showPopup(myTable, e);
}
}
}
/**
* Return the resolution of a given AVD as a string of the format [width]x[height] - [density]
* (e.g. 1200x1920 - xhdpi) or "Unknown Resolution" if the AVD does not define a resolution.
*/
protected static String getResolution(AvdInfo info) {
String resolution;
Dimension res = AvdManagerConnection.getAvdResolution(info);
Density density = AvdManagerConnection.getAvdDensity(info);
String densityString = density == null ? "Unknown Density" : density.getResourceValue();
if (res != null) {
resolution = String.format(Locale.getDefault(), "%1$d \u00D7 %2$d: %3$s", res.width, res.height, densityString);
} else {
resolution = "Unknown Resolution";
}
return resolution;
}
/**
* Get the device icon representing the device class of the given AVD (e.g. phone/tablet or TV)
*/
private static Icon getIcon(@NotNull AvdInfo info) {
String id = info.getTag().getId();
String path;
if (id.contains("android-")) {
path = String.format("/icons/formfactors/%s_32.png", id.substring("android-".length()));
return IconLoader.getIcon(path, AvdDisplayList.class);
} else {
return AndroidIcons.FormFactors.Mobile_32;
}
}
/**
* Replaces underscores with spaces and capitalizes first letter of each word
*/
private static String getPrettyDeviceName(@NotNull AvdInfo info) {
String name = info.getDeviceName();
StringBuilder sb = new StringBuilder(name.length());
int n = name.length();
boolean upperCaseNext = false;
for (int i = 0; i < n; i++) {
char c = name.charAt(i);
if (c == '_') {
upperCaseNext = true;
c = ' ';
} else if (upperCaseNext || i == 0) {
c = Character.toUpperCase(c);
upperCaseNext = false;
}
sb.append(c);
}
return sb.toString();
}
/**
* List of columns present in our table. Each column is represented by a ColumnInfo which tells the table how to get
* the cell value in that column for a given row item.
*/
private final ColumnInfo[] ourColumnInfos = new ColumnInfo[] {
new AvdIconColumnInfo("Type") {
@Nullable
@Override
public Icon valueOf(AvdInfo avdInfo) {
return AvdDisplayList.getIcon(avdInfo);
}
},
new AvdColumnInfo("Name") {
@Nullable
@Override
public String valueOf(AvdInfo info) {
return getPrettyDeviceName(info);
}
},
new AvdColumnInfo("Resolution") {
@Nullable
@Override
public String valueOf(AvdInfo avdInfo) {
return getResolution(avdInfo);
}
/**
* We override the comparator here to sort the AVDs by total number of pixels on the screen rather than the
* default sort order (lexicographically by string representation)
*/
@Nullable
@Override
public Comparator<AvdInfo> getComparator() {
return new Comparator<AvdInfo>() {
@Override
public int compare(AvdInfo o1, AvdInfo o2) {
Dimension d1 = AvdManagerConnection.getAvdResolution(o1);
Dimension d2 = AvdManagerConnection.getAvdResolution(o2);
if (d1 == d2) {
return 0;
} else if (d1 == null) {
return -1;
} else if (d2 == null) {
return 1;
} else {
return d1.width * d1.height - d2.width * d2.height;
}
}
};
}
},
new AvdColumnInfo("API", 50) {
@Nullable
@Override
public String valueOf(AvdInfo avdInfo) {
IAndroidTarget target = avdInfo.getTarget();
if (target == null) {
return "N/A";
}
return target.getVersion().getApiString();
}
},
new AvdColumnInfo("Target") {
@Nullable
@Override
public String valueOf(AvdInfo info) {
IAndroidTarget target = info.getTarget();
if (target == null) {
return "N/A";
}
return target.getName();
}
},
new AvdColumnInfo("CPU/ABI", 60) {
@Nullable
@Override
public String valueOf(AvdInfo avdInfo) {
return avdInfo.getCpuArch();
}
},
new AvdSizeColumnInfo("Size on Disk"),
new AvdActionsColumnInfo("Actions", 2 /* Num Visible Actions */),
};
/**
* This class extends {@link ColumnInfo} in order to pull an {@link Icon} value from a given {@link AvdInfo}.
* This is the column info used for the Type and Status columns.
* It uses the icon field renderer ({@link #ourIconRenderer}) and does not sort by default. An explicit width may be used
* by calling the overloaded constructor, otherwise the column will be 50px wide.
*/
private static abstract class AvdIconColumnInfo extends ColumnInfo<AvdInfo, Icon> {
private final int myWidth;
/**
* Renders an icon in a small square field
*/
private static final TableCellRenderer ourIconRenderer = new DefaultTableCellRenderer() {
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
JBLabel label = new JBLabel((Icon)value);
if (table.getSelectedRow() == row) {
label.setBackground(table.getSelectionBackground());
label.setForeground(table.getSelectionForeground());
label.setOpaque(true);
}
return label;
}
};
public AvdIconColumnInfo(@NotNull String name, int width) {
super(name);
myWidth = width;
}
public AvdIconColumnInfo(@NotNull String name) {
this(name, 50);
}
@Nullable
@Override
public TableCellRenderer getRenderer(AvdInfo o) {
return ourIconRenderer;
}
@Override
public int getWidth(JTable table) {
return myWidth;
}
}
/**
* This class extends {@link com.intellij.util.ui.ColumnInfo} in order to pull a string value from a given {@link com.android.sdklib.internal.avd.AvdInfo}.
* This is the column info used for most of our table, including the Name, Resolution, and API level columns.
* It uses the text field renderer ({@link #myRenderer}) and allows for sorting by the lexicographical value
* of the string displayed by the {@link com.intellij.ui.components.JBLabel} rendered as the cell component. An explicit width may be used
* by calling the overloaded constructor, otherwise the column will auto-scale to fill available space.
*/
public abstract static class AvdColumnInfo extends ColumnInfo<AvdInfo, String> {
private final Border myBorder = IdeBorderFactory.createEmptyBorder(10, 10, 10, 10);
/**
* Renders a simple text field.
*/
private final TableCellRenderer myRenderer = new TableCellRenderer() {
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
JBLabel label = new JBLabel((String)value);
label.setBorder(myBorder);
if (table.getSelectedRow() == row) {
label.setBackground(table.getSelectionBackground());
label.setForeground(table.getSelectionForeground());
label.setOpaque(true);
}
return label;
}
};
private final int myWidth;
public AvdColumnInfo(@NotNull String name, int width) {
super(name);
myWidth = width;
}
public AvdColumnInfo(@NotNull String name) {
this(name, -1);
}
@Nullable
@Override
public TableCellRenderer getRenderer(AvdInfo o) {
return myRenderer;
}
@Nullable
@Override
public Comparator<AvdInfo> getComparator() {
return new Comparator<AvdInfo>() {
@Override
public int compare(AvdInfo o1, AvdInfo o2) {
String s1 = valueOf(o1);
String s2 = valueOf(o2);
return Comparing.compare(s1, s2);
}
};
}
@Override
public int getWidth(JTable table) {
return myWidth;
}
}
private static abstract class ActionRenderer extends AbstractTableCellEditor implements TableCellRenderer {}
/**
* Custom table cell renderer that renders an action panel for a given AVD entry
*/
private class AvdActionsColumnInfo extends ColumnInfo<AvdInfo, AvdInfo> {
private int myNumVisibleActions = -1;
private int myWidth;
/**
* This cell renders an action panel for both the editor component and the display component
*/
private final ActionRenderer ourActionPanelRendererEditor = new ActionRenderer() {
Map<Object, Component> myInfoToComponentMap = Maps.newHashMap();
private Component getComponent(JTable table, Object value, boolean isSelected, int row) {
Component panel;
if (myInfoToComponentMap.containsKey(value)) {
panel = myInfoToComponentMap.get(value);
} else {
panel = new AvdActionPanel((AvdInfo)value, myNumVisibleActions, AvdDisplayList.this);
myInfoToComponentMap.put(value, panel);
}
if (table.getSelectedRow() == row || isSelected) {
panel.setBackground(table.getSelectionBackground());
panel.setForeground(table.getSelectionForeground());
} else {
panel.setBackground(table.getBackground());
panel.setForeground(table.getForeground());
}
return panel;
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
return getComponent(table, value, isSelected, row);
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
return getComponent(table, value, isSelected, row);
}
@Override
public Object getCellEditorValue() {
return null;
}
};
public AvdActionsColumnInfo(@NotNull String name, int numVisibleActions) {
super(name);
myNumVisibleActions = numVisibleActions;
myWidth = numVisibleActions == -1 ? -1 : 45 * numVisibleActions + 50;
}
public AvdActionsColumnInfo(@NotNull String name) {
this(name, -1);
}
@Nullable
@Override
public AvdInfo valueOf(AvdInfo avdInfo) {
return avdInfo;
}
/**
* We override the comparator here so that we can sort by healthy vs not healthy AVDs
*/
@Nullable
@Override
public Comparator<AvdInfo> getComparator() {
return new Comparator<AvdInfo>() {
@Override
public int compare(AvdInfo o1, AvdInfo o2) {
return o1.getStatus().compareTo(o2.getStatus());
}
};
}
@Nullable
@Override
public TableCellRenderer getRenderer(AvdInfo o) {
return ourActionPanelRendererEditor;
}
@Nullable
@Override
public TableCellEditor getEditor(AvdInfo avdInfo) {
return ourActionPanelRendererEditor;
}
@Override
public boolean isCellEditable(AvdInfo avdInfo) {
return true;
}
@Override
public int getWidth(JTable table) {
return myWidth;
}
}
private class AvdSizeColumnInfo extends AvdColumnInfo {
public AvdSizeColumnInfo(@NotNull String name) {
super(name);
}
@NotNull
private Storage getSize(AvdInfo avdInfo) {
long sizeInBytes = 0;
if (avdInfo != null) {
File avdDir = new File(avdInfo.getDataFolderPath());
for (File file : TemplateUtils.listFiles(avdDir)) {
sizeInBytes += file.length();
}
}
return new Storage(sizeInBytes);
}
@Nullable
@Override
public String valueOf(AvdInfo avdInfo) {
Storage size = getSize(avdInfo);
return String.format(Locale.getDefault(), "%1$d MB", size.getSizeAsUnit(Storage.Unit.MiB));
}
@Nullable
@Override
public Comparator<AvdInfo> getComparator() {
return new Comparator<AvdInfo>() {
@Override
public int compare(AvdInfo o1, AvdInfo o2) {
Storage s1 = getSize(o1);
Storage s2 = getSize(o2);
return Comparing.compare(s1.getSize(), s2.getSize());
}
};
}
}
}