blob: 49435269f61f4e9bb18548cbe67d67b7ea2b1d6c [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.ide.common.rendering.HardwareConfigHelper;
import com.android.sdklib.devices.Device;
import com.android.tools.idea.npw.FormFactorUtils;
import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.ui.JBMenuItem;
import com.intellij.openapi.ui.JBPopupMenu;
import com.intellij.ui.IdeBorderFactory;
import com.intellij.ui.SearchTextField;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.table.TableView;
import com.intellij.util.ui.ColumnInfo;
import com.intellij.util.ui.ListTableModel;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.TableCellRenderer;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
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.text.DecimalFormat;
import java.util.*;
import java.util.List;
/**
* Lists the available device definitions by category
*/
public class DeviceDefinitionList extends JPanel implements ListSelectionListener, DocumentListener, DeviceUiAction.DeviceProvider {
private static final double PHONE_SIZE_CUTOFF = 6.0;
private static final double TV_SIZE_CUTOFF = 15.0;
private static final String SEARCH_RESULTS = "Search Results";
private static final String PHONE_TYPE = "Phone";
private static final String TABLET_TYPE = "Tablet";
private static final String OTHER_TYPE = "Other";
private static final String DEFAULT_PHONE = "Nexus 5";
private static final String DEFAULT_TABLET = "Nexus 10";
private static final String DEFAULT_WEAR = "Android Wear Square";
private static final String DEFAULT_TV = "Android TV (1080p)";
private Map<String, List<Device>> myDeviceCategoryMap = Maps.newHashMap();
private static final Map<String, Device> myDefaultCategoryDeviceMap = Maps.newHashMap();
private static final DecimalFormat ourDecimalFormat = new DecimalFormat(".##");
private final ListTableModel<Device> myModel = new ListTableModel<Device>();
private TableView<Device> myTable;
private final ListTableModel<String> myCategoryModel = new ListTableModel<String>();
private TableView<String> myCategoryList;
private JButton myCreateProfileButton;
private JButton myImportProfileButton;
private JButton myRefreshButton;
private JPanel myPanel;
private SearchTextField mySearchTextField;
private List<DeviceDefinitionSelectionListener> myListeners = Lists.newArrayList();
private List<DeviceCategorySelectionListener> myCategoryListeners = Lists.newArrayList();
private List<Device> myDevices;
private Device myDefaultDevice;
public DeviceDefinitionList() {
myModel.setColumnInfos(myColumnInfos);
myModel.setSortable(true);
refreshDeviceProfiles();
setDefaultDevices();
myTable.setModelAndUpdateColumns(myModel);
myTable.getRowSorter().toggleSortOrder(0);
myTable.getRowSorter().toggleSortOrder(0);
myTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
myTable.setRowSelectionAllowed(true);
setLayout(new BorderLayout());
myRefreshButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
refreshDeviceProfiles();
}
});
myTable.getSelectionModel().addListSelectionListener(this);
myCategoryModel.setColumnInfos(myCategoryInfo);
myCategoryList.setModelAndUpdateColumns(myCategoryModel);
myCategoryList.getSelectionModel().addListSelectionListener(this);
mySearchTextField.addDocumentListener(this);
add(myPanel, BorderLayout.CENTER);
myCreateProfileButton.setAction(new CreateDeviceAction(this));
myCreateProfileButton.setText("New Hardware Profile");
myImportProfileButton.setAction(new ImportDevicesAction(this));
myImportProfileButton.setText("Import Hardware Profiles");
myTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
possiblyShowPopup(e);
}
@Override
public void mousePressed(MouseEvent e) {
possiblyShowPopup(e);
}
@Override
public void mouseReleased(MouseEvent e) {
possiblyShowPopup(e);
}
});
}
private void setDefaultDevices() {
for (Device d : myDeviceCategoryMap.get(PHONE_TYPE)) {
if (d.getDisplayName().equals(DEFAULT_PHONE)) {
myDefaultCategoryDeviceMap.put(PHONE_TYPE, d);
myDefaultDevice = d;
break;
}
}
for (Device d : myDeviceCategoryMap.get(TABLET_TYPE)) {
if (d.getDisplayName().equals(DEFAULT_TABLET)) {
myDefaultCategoryDeviceMap.put(TABLET_TYPE, d);
break;
}
}
for (Device d : myDeviceCategoryMap.get(FormFactorUtils.FormFactor.WEAR.toString())) {
if (d.getDisplayName().equals(DEFAULT_WEAR)) {
myDefaultCategoryDeviceMap.put(FormFactorUtils.FormFactor.WEAR.toString(), d);
break;
}
}
for (Device d : myDeviceCategoryMap.get(FormFactorUtils.FormFactor.TV.toString())) {
if (d.getDisplayName().equals(DEFAULT_TV)) {
myDefaultCategoryDeviceMap.put(FormFactorUtils.FormFactor.TV.toString(), d);
break;
}
}
}
@NotNull
private static JBMenuItem createMenuItem(@NotNull DeviceUiAction action) {
JBMenuItem item = new JBMenuItem(action);
item.setText(action.getText());
return item;
}
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) {
JBPopupMenu menu = new JBPopupMenu();
menu.add(createMenuItem(new CloneDeviceAction(this)));
menu.add(createMenuItem(new EditDeviceAction(this)));
menu.add(createMenuItem(new ExportDeviceAction(this)));
menu.add(createMenuItem(new DeleteDeviceAction(this)));
menu.show(myTable, p.x, p.y);
}
}
@Override
public void valueChanged(ListSelectionEvent e) {
if (e.getSource().equals(myCategoryList.getSelectionModel())) {
setCategory(myCategoryList.getSelectedObject());
} else if (e.getSource().equals(myTable.getSelectionModel())){
onSelectionSet(myTable.getSelectedObject());
}
}
public void addSelectionListener(@NotNull DeviceDefinitionSelectionListener listener) {
myListeners.add(listener);
}
public void addCategoryListener(@NotNull DeviceCategorySelectionListener listener) {
myCategoryListeners.add(listener);
}
public void removeSelectionListener(@NotNull DeviceDefinitionSelectionListener listener) {
myListeners.remove(listener);
}
@Override
public void selectDefaultDevice() {
setSelectedDevice(myDefaultDevice);
}
/**
* Set the list's selection to the given device, or clear the selection if the
* given device is null. The category list will also select the category to which the
* given device belongs.
*/
public void setSelectedDevice(@Nullable Device device) {
if (Objects.equal(device, myTable.getSelectedObject())) {
return;
}
onSelectionSet(device);
if (device != null) {
String category = getCategory(device);
for (Device listItem : myModel.getItems()) {
if (listItem.getId().equals(device.getId())) {
myTable.setSelection(ImmutableSet.of(listItem));
}
}
myCategoryList.setSelection(ImmutableSet.of(category));
setCategory(category);
}
}
/**
* Update our listeners
*/
private void onSelectionSet(@Nullable Device selectedObject) {
if (selectedObject != null) {
myDefaultCategoryDeviceMap.put(getCategory(selectedObject), selectedObject);
}
for (DeviceDefinitionSelectionListener listener : myListeners) {
listener.onDeviceSelectionChanged(selectedObject);
}
}
/**
* Update our list to display the given category.
*/
public void setCategory(@Nullable String selectedCategory) {
if (myDeviceCategoryMap.containsKey(selectedCategory)) {
List<Device> newItems = myDeviceCategoryMap.get(selectedCategory);
if (!myModel.getItems().equals(newItems)) {
myModel.setItems(newItems);
setSelectedDevice(myDefaultCategoryDeviceMap.get(selectedCategory));
notifyCategoryListeners(selectedCategory, newItems);
}
}
}
private void notifyCategoryListeners(@Nullable String selectedCategory, @Nullable List<Device> items) {
for (DeviceCategorySelectionListener listener : myCategoryListeners) {
listener.onCategorySelectionChanged(selectedCategory, items);
}
}
private void refreshDeviceProfiles() {
myDevices = DeviceManagerConnection.getDefaultDeviceManagerConnection().getDevices();
myDeviceCategoryMap.clear();
for (Device d : myDevices) {
String category = getCategory(d);
if (!myDeviceCategoryMap.containsKey(category)) {
myDeviceCategoryMap.put(category, new ArrayList<Device>(1));
}
myDeviceCategoryMap.get(category).add(d);
}
Set<String> categories = myDeviceCategoryMap.keySet();
String[] categoryArray = categories.toArray(new String[categories.size()]);
myCategoryModel.setItems(Lists.newArrayList(categoryArray));
}
/**
* @return the category of the specified device. One of:
* TV, Wear, Tablet, and Phone, or Other if the category can
* not be determined. Mobile devices are considered tablets if
* their screen size is over {@link #PHONE_SIZE_CUTOFF}
*/
private static String getCategory(@NotNull Device d) {
if (HardwareConfigHelper.isTv(d) || hasTvSizedScreen(d)) {
return FormFactorUtils.FormFactor.TV.toString();
} else if (HardwareConfigHelper.isWear(d)) {
return FormFactorUtils.FormFactor.WEAR.toString();
} else if (isTablet(d)) {
return TABLET_TYPE;
} else if (isPhone(d)) {
return PHONE_TYPE;
} else {
return OTHER_TYPE;
}
}
private static boolean isPhone(@NotNull Device d) {
return d.getDefaultHardware().getScreen().getDiagonalLength() < PHONE_SIZE_CUTOFF;
}
private static boolean isTablet(@NotNull Device d) {
return d.getDefaultHardware().getScreen().getDiagonalLength() >= PHONE_SIZE_CUTOFF;
}
private static boolean hasTvSizedScreen(@NotNull Device d) {
return d.getDefaultHardware().getScreen().getDiagonalLength() >= TV_SIZE_CUTOFF;
}
/**
* The singular column that serves as the header for our category list
*/
private final ColumnInfo[] myCategoryInfo = new ColumnInfo[] {
new ColumnInfo<String, String>("Category") {
@Nullable
@Override
public String valueOf(String category) {
return category;
}
@Nullable
@Override
public TableCellRenderer getRenderer(String s) {
return myRenderer;
}
}
};
/**
* @return the diagonal screen size of the given device
*/
public static String getDiagonalSize(@NotNull Device device) {
return ourDecimalFormat.format(device.getDefaultHardware().getScreen().getDiagonalLength()) + '"';
}
/**
* @return a string of the form [width]x[height] in pixel units representing the screen resolution of the given device
*/
public static String getDimensionString(@NotNull Device device) {
Dimension size = device.getScreenSize(device.getDefaultState().getOrientation());
return size == null ? "Unknown Resolution" : String.format(Locale.getDefault(), "%dx%d", size.width, size.height);
}
/**
* @return a string representing the density bucket of the given device
*/
public static String getDensityString(@NotNull Device device) {
return device.getDefaultHardware().getScreen().getPixelDensity().getResourceValue();
}
/**
* 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 DeviceColumnInfo("Name") {
@Nullable
@Override
public String valueOf(Device device) {
return device.getDisplayName();
}
@Nullable
@Override
public Comparator<Device> getComparator() {
return new Comparator<Device>() {
@Override
public int compare(Device o1, Device o2) {
String name1 = valueOf(o1);
String name2 = valueOf(o2);
if (name1 == name2) {
return 0;
}
if (name1 == null || name2 == null || name1.isEmpty() || name2.isEmpty()) {
return name1 == null ? -1 : 1;
}
char firstChar1 = name1.charAt(0);
char firstChar2 = name2.charAt(0);
// Prefer letters to anything else
if (Character.isLetter(firstChar1) && !Character.isLetter(firstChar2)) {
return 1;
} else if (Character.isLetter(firstChar2) && !Character.isLetter(firstChar1)) {
return -1;
}
// Fall back to string comparison
return name1.compareTo(name2);
}
};
}
},
new DeviceColumnInfo("Size") {
@Nullable
@Override
public String valueOf(Device device) {
return getDiagonalSize(device);
}
@Nullable
@Override
public Comparator<Device> getComparator() {
return new Comparator<Device>() {
@Override
public int compare(Device o1, Device o2) {
if (o1 == null) {
return -1;
} else if (o2 == null) {
return 1;
} else {
return Double.valueOf(o1.getDefaultHardware().getScreen().getDiagonalLength()).
compareTo(o2.getDefaultHardware().getScreen().getDiagonalLength());
}
}
};
}
},
new DeviceColumnInfo("Resolution") {
@Nullable
@Override
public String valueOf(Device device) {
return getDimensionString(device);
}
@Nullable
@Override
public Comparator<Device> getComparator() {
return new Comparator<Device>() {
@Override
public int compare(Device o1, Device o2) {
if (o1 == null) {
return -1;
} else if (o2 == null) {
return 1;
} else {
Dimension d1 = o1.getScreenSize(o1.getDefaultState().getOrientation());
Dimension d2 = o2.getScreenSize(o2.getDefaultState().getOrientation());
if (d1 == null) {
return -1;
} else if (d2 == null) {
return 1;
} else {
return Integer.valueOf(d1.width*d1.height).compareTo(d2.width*d2.height);
}
}
}
};
}
},
new DeviceColumnInfo("Density") {
@Nullable
@Override
public String valueOf(Device device) {
return getDensityString(device);
}
}
};
private void createUIComponents() {
myCategoryList = new TableView<String>();
myTable = new TableView<Device>();
myRefreshButton = new JButton(AllIcons.Actions.Refresh);
}
@Override
public void insertUpdate(DocumentEvent e) {
updateSearchResults(getText(e.getDocument()));
}
@Override
public void removeUpdate(DocumentEvent e) {
updateSearchResults(getText(e.getDocument()));
}
@Override
public void changedUpdate(DocumentEvent e) {
updateSearchResults(getText(e.getDocument()));
}
private String getText(Document d) {
try {
return d.getText(0, d.getLength());
}
catch (BadLocationException e) {
return "";
}
}
/**
* Set the "Search Results" category to the set of devices whose names match the given search string
*/
private void updateSearchResults(@NotNull final String searchString) {
if (searchString.isEmpty()) {
if (myCategoryModel.getItem(myCategoryModel.getRowCount() - 1).equals(SEARCH_RESULTS)) {
myCategoryModel.removeRow(myCategoryModel.getRowCount() - 1);
setCategory(myCategoryList.getRow(0));
}
return;
} else if (!myCategoryModel.getItem(myCategoryModel.getRowCount() - 1).equals(SEARCH_RESULTS)) {
myCategoryModel.addRow(SEARCH_RESULTS);
myCategoryList.setSelection(ImmutableSet.of(SEARCH_RESULTS));
}
List<Device> items = Lists.newArrayList(Iterables.filter(myDevices, new Predicate<Device>() {
@Override
public boolean apply(Device input) {
return input.getDisplayName().toLowerCase().contains(searchString.toLowerCase());
}
}));
myModel.setItems(items);
notifyCategoryListeners(null, items);
}
@Nullable
@Override
public Device getDevice() {
return myTable.getSelectedObject();
}
@Override
public void setDevice(@Nullable Device device) {
setSelectedDevice(device);
}
@Override
public void refreshDevices() {
refreshDeviceProfiles();
}
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 abstract class DeviceColumnInfo extends ColumnInfo<Device, String> {
private final int myWidth;
@Nullable
@Override
public Comparator<Device> getComparator() {
return new Comparator<Device>() {
@Override
public int compare(Device o1, Device o2) {
if (o1 == null || valueOf(o1) == null) {
return -1;
} else if (o2 == null || valueOf(o2) == null) {
return 1;
} else {
//noinspection ConstantConditions
return valueOf(o1).compareTo(valueOf(o2));
}
}
};
}
public DeviceColumnInfo(@NotNull String name, int width) {
super(name);
myWidth = width;
}
public DeviceColumnInfo(String name) {
this(name, -1);
}
@Nullable
@Override
public TableCellRenderer getRenderer(Device device) {
return myRenderer;
}
@Override
public int getWidth(JTable table) {
return myWidth;
}
}
public interface DeviceDefinitionSelectionListener {
void onDeviceSelectionChanged(@Nullable Device selectedDevice);
}
public interface DeviceCategorySelectionListener {
void onCategorySelectionChanged(@Nullable String category, @Nullable List<Device> devices);
}
}