blob: 470c7318eed313123faeff2d47c95425f0749348 [file] [log] [blame]
/*
* 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.npw.WizardUtils;
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.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.IconLoader;
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.*;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
import java.awt.event.*;
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 Project myProject;
private final JButton myRefreshButton = new JButton(AllIcons.Actions.Refresh);
private final JPanel myCenterCardPanel;
private final AvdListDialog myDialog;
private TableView<AvdInfo> myTable;
private ListTableModel<AvdInfo> myModel = new ListTableModel<AvdInfo>();
private Set<AvdSelectionListener> myListeners = Sets.newHashSet();
private final AvdActionsColumnInfo myActionsColumnRenderer = new AvdActionsColumnInfo("Actions", 2 /* Num Visible Actions */);
/**
* 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(@NotNull AvdListDialog dialog, @Nullable Project project) {
myDialog = dialog;
myProject = project;
myModel.setColumnInfos(myColumnInfos);
myModel.setSortable(true);
myTable = new TableView<AvdInfo>();
myTable.setModelAndUpdateColumns(myModel);
myTable.setDefaultRenderer(Object.class, new MyRenderer(myTable.getDefaultRenderer(Object.class)));
setLayout(new BorderLayout());
myCenterCardPanel = new JPanel(new CardLayout());
JPanel nonemptyPanel = new JPanel(new BorderLayout());
myCenterCardPanel.add(nonemptyPanel, NONEMPTY);
nonemptyPanel.add(ScrollPaneFactory.createScrollPane(myTable), BorderLayout.CENTER);
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();
}
});
myRefreshButton.putClientProperty("JButton.buttonType", "segmented-only");
CreateAvdAction createAvdAction = new CreateAvdAction(this);
JButton newButton = new JButton(createAvdAction);
newButton.putClientProperty("JButton.buttonType", "segmented-only");
southPanel.add(newButton, BorderLayout.WEST);
nonemptyPanel.add(southPanel, BorderLayout.SOUTH);
myTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
myTable.getSelectionModel().addListSelectionListener(this);
myTable.addMouseListener(myEditingListener);
myTable.addMouseMotionListener(myEditingListener);
LaunchListener launchListener = new LaunchListener();
myTable.addMouseListener(launchListener);
myTable.addKeyListener(launchListener);
ActionMap am = myTable.getActionMap();
am.put("selectPreviousColumnCell", new CycleAction(true));
am.put("selectNextColumnCell", new CycleAction(false));
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) {
// Required so the editor component is updated to know it's selected.
myTable.editCellAt(myTable.getSelectedRow(), myTable.getSelectedColumn());
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.getDefaultAvdManagerConnection().getAvds(true);
myModel.setItems(avds);
if (avds.isEmpty()) {
((CardLayout)myCenterCardPanel.getLayout()).show(myCenterCardPanel, EMPTY);
} else {
((CardLayout)myCenterCardPanel.getLayout()).show(myCenterCardPanel, NONEMPTY);
}
}
@Nullable
@Override
public Project getProject() {
return myProject;
}
@Override
public void notifyRun() {
}
@NotNull
@Override
public JComponent getComponent() {
return this;
}
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.getDefaultAvdManagerConnection().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;
}
}
/**
* 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[] myColumnInfos = 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 AvdManagerConnection.getAvdDisplayName(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) {
AvdManagerConnection connection = AvdManagerConnection.getDefaultAvdManagerConnection();
Dimension d1 = connection.getAvdResolution(o1);
Dimension d2 = connection.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) {
@NotNull
@Override
public String valueOf(AvdInfo avdInfo) {
IAndroidTarget target = avdInfo.getTarget();
if (target == null) {
return "N/A";
}
return target.getVersion().getApiString();
}
/**
* We override the comparator here to sort the API levels numerically (when possible;
* with preview platforms codenames are compared alphabetically)
*/
@Nullable
@Override
public Comparator<AvdInfo> getComparator() {
final ApiLevelComparator comparator = new ApiLevelComparator();
return new Comparator<AvdInfo>() {
@Override
public int compare(AvdInfo o1, AvdInfo o2) {
return comparator.compare(valueOf(o1), valueOf(o2));
}
};
}
},
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"),
myActionsColumnRenderer,
};
/**
* 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 int myWidth;
public AvdColumnInfo(@NotNull String name, int width) {
super(name);
myWidth = width;
}
public AvdColumnInfo(@NotNull String name) {
this(name, -1);
}
@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 class ActionRenderer extends AbstractTableCellEditor implements TableCellRenderer {
AvdActionPanel myComponent;
private int myNumVisibleActions = -1;
ActionRenderer(int numVisibleActions, AvdInfo info) {
myNumVisibleActions = numVisibleActions;
myComponent = new AvdActionPanel(info, myNumVisibleActions, AvdDisplayList.this);
}
private Component getComponent(JTable table, int row, int column) {
if (table.getSelectedRow() == row) {
myComponent.setBackground(table.getSelectionBackground());
myComponent.setForeground(table.getSelectionForeground());
} else {
myComponent.setBackground(table.getBackground());
myComponent.setForeground(table.getForeground());
}
myComponent.setFocused(table.getSelectedRow() == row && table.getSelectedColumn() == column);
return myComponent;
}
public boolean cycleFocus(boolean backward) {
return myComponent.cycleFocus(backward);
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
return getComponent(table, row, column);
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
return getComponent(table, row, column);
}
@Override
public Object getCellEditorValue() {
return null;
}
}
/**
* Custom table cell renderer that renders an action panel for a given AVD entry
*/
private class AvdActionsColumnInfo extends ColumnInfo<AvdInfo, AvdInfo> {
private int myWidth;
private int myNumVisibleActions;
/**
* This cell renders an action panel for both the editor component and the display component
*/
private final Map<Object, ActionRenderer> ourActionPanelRendererEditor = Maps.newHashMap();
public AvdActionsColumnInfo(@NotNull String name, int numVisibleActions) {
super(name);
myNumVisibleActions = numVisibleActions;
myWidth = numVisibleActions == -1 ? -1 : 45 * numVisibleActions + 75;
}
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 avdInfo) {
return getComponent(avdInfo);
}
public ActionRenderer getComponent(AvdInfo avdInfo) {
ActionRenderer renderer = ourActionPanelRendererEditor.get(avdInfo);
if (renderer == null) {
renderer = new ActionRenderer(myNumVisibleActions, avdInfo);
ourActionPanelRendererEditor.put(avdInfo, renderer);
}
return renderer;
}
@Nullable
@Override
public TableCellEditor getEditor(AvdInfo avdInfo) {
return getComponent(avdInfo);
}
@Override
public boolean isCellEditable(AvdInfo avdInfo) {
return true;
}
@Override
public int getWidth(JTable table) {
return myWidth;
}
public boolean cycleFocus(AvdInfo info, boolean backward) {
return getComponent(info).cycleFocus(backward);
}
}
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 : WizardUtils.listFiles(avdDir)) {
sizeInBytes += file.length();
}
}
return new Storage(sizeInBytes);
}
@Nullable
@Override
public String valueOf(AvdInfo avdInfo) {
Storage size = getSize(avdInfo);
String unitString = "MB";
Long value = size.getSizeAsUnit(Storage.Unit.MiB);
if (value > 1024) {
unitString = "GB";
value = size.getSizeAsUnit(Storage.Unit.GiB);
}
return String.format(Locale.getDefault(), "%1$d %2$s", value, unitString);
}
@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());
}
};
}
}
private class LaunchListener extends MouseAdapter implements KeyListener {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
doAction();
}
}
private void doAction() {
notifyRun();
AvdInfo info = getAvdInfo();
if (info != null) {
if (info.getStatus() == AvdInfo.AvdStatus.OK) {
new RunAvdAction(AvdDisplayList.this).actionPerformed(null);
} else {
new EditAvdAction(AvdDisplayList.this).actionPerformed(null);
}
}
}
@Override
public void keyTyped(KeyEvent e) {
if (e.getKeyChar() == KeyEvent.VK_ENTER || e.getKeyChar() == KeyEvent.VK_SPACE) {
doAction();
}
}
@Override
public void keyPressed(KeyEvent e) {}
@Override
public void keyReleased(KeyEvent e) {}
}
private class CycleAction extends AbstractAction {
boolean myBackward;
CycleAction(boolean backward) {
myBackward = backward;
}
@Override
public void actionPerformed(ActionEvent evt) {
int selectedRow = myTable.getSelectedRow();
int selectedColumn = myTable.getSelectedColumn();
int actionsColumn = myModel.findColumn(myActionsColumnRenderer.getName());
if (myBackward) {
cycleBackward(selectedRow, selectedColumn, actionsColumn);
}
else {
cycleForward(selectedRow, selectedColumn, actionsColumn);
}
selectedRow = myTable.getSelectedRow();
if (selectedRow != -1) {
myTable.editCellAt(selectedRow, myTable.getSelectedColumn());
}
repaint();
}
private void cycleForward(int selectedRow, int selectedColumn, int actionsColumn) {
if (selectedColumn == actionsColumn && selectedRow == myTable.getRowCount() - 1) {
// We're in the last cell of the table. Check whether we can cycle action buttons
if (!myActionsColumnRenderer.cycleFocus(myTable.getSelectedObject(), false)) {
// At the end of action buttons. Remove selection and leave table.
myActionsColumnRenderer.getEditor(getAvdInfo()).stopCellEditing();
myTable.removeRowSelectionInterval(selectedRow, selectedRow);
KeyboardFocusManager manager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
manager.focusNextComponent(myTable);
}
} else if (selectedColumn != actionsColumn && selectedRow != -1) {
// We're in the table, but not on the action column. Select the action column.
myTable.setColumnSelectionInterval(actionsColumn, actionsColumn);
myActionsColumnRenderer.cycleFocus(myTable.getSelectedObject(), false);
} else if (selectedRow == -1 || !myActionsColumnRenderer.cycleFocus(myTable.getSelectedObject(), false)) {
// We aren't in the table yet, or we are in the actions column and at the end of the focusable actions. Move to the next row
// and select the first column
myTable.setColumnSelectionInterval(0, 0);
myTable.setRowSelectionInterval(selectedRow + 1, selectedRow + 1);
}
}
private void cycleBackward(int selectedRow, int selectedColumn, int actionsColumn) {
if (selectedColumn == 0 && selectedRow == 0) {
// We're in the first cell of the table. Remove selection and leave table.
myTable.removeRowSelectionInterval(selectedRow, selectedRow);
KeyboardFocusManager manager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
manager.focusPreviousComponent();
} else if (selectedColumn == actionsColumn && selectedRow != -1 && !myActionsColumnRenderer.cycleFocus(myTable.getSelectedObject(), true)) {
// We're on an actions column. If we fail to cycle actions, select the first cell in the row.
myTable.setColumnSelectionInterval(0, 0);
} else if (selectedRow == -1 || selectedColumn != actionsColumn) {
// We aren't in the table yet, or we're not in the actions column. Move to the previous (or last) row.
// and select the actions column
if (selectedRow == -1) {
selectedRow = myTable.getRowCount();
}
myTable.setRowSelectionInterval(selectedRow - 1, selectedRow - 1);
myTable.setColumnSelectionInterval(actionsColumn, actionsColumn);
myActionsColumnRenderer.cycleFocus(myTable.getSelectedObject(), true);
}
}
}
/**
* Renders a cell with borders.
*/
private static class MyRenderer implements TableCellRenderer {
private static final Border myBorder = IdeBorderFactory.createEmptyBorder(10, 10, 10, 10);
TableCellRenderer myDefaultRenderer;
MyRenderer(TableCellRenderer defaultRenderer) {
myDefaultRenderer = defaultRenderer;
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
JComponent result = (JComponent)myDefaultRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
result.setBorder(myBorder);
return result;
}
};
}