/* * 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.wizard.FormFactorUtils; 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 String SEARCH_RESULTS = "Search Results"; private Map<String, List<Device>> myDeviceCategoryMap = 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<Device> myDevices; public DeviceDefinitionList() { myModel.setColumnInfos(myColumnInfos); myModel.setSortable(true); refreshDeviceProfiles(); 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); myCategoryList.setSelection(ImmutableSet.of(myCategoryModel.getItem(0))); myCreateProfileButton.setAction(new CreateDeviceAction(this)); myCreateProfileButton.setText("New Hardware Profile"); 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); } }); } @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 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 removeSelectionListener(@NotNull DeviceDefinitionSelectionListener listener) { myListeners.remove(listener); } /** * 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) { onSelectionSet(device); if (device != null) { String category = getCategory(device); myCategoryList.setSelection(ImmutableSet.of(category)); setCategory(category); for (Device listItem : myModel.getItems()) { if (listItem.getId().equals(device.getId())) { myTable.setSelection(ImmutableSet.of(listItem)); } } } } /** * Update our listeners */ private void onSelectionSet(@Nullable Device selectedObject) { for (DeviceDefinitionSelectionListener listener : myListeners) { listener.onDeviceSelectionChanged(selectedObject); } } /** * Update our list to display the given category. */ private void setCategory(@Nullable String selectedCategory) { if (myDeviceCategoryMap.containsKey(selectedCategory)) { myModel.setItems(myDeviceCategoryMap.get(selectedCategory)); } } private void refreshDeviceProfiles() { myDevices = DeviceManagerConnection.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)); myModel.setItems(myDeviceCategoryMap.get(categoryArray[0])); } /** * @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)) { return FormFactorUtils.FormFactor.TV.toString(); } else if (HardwareConfigHelper.isWear(d)) { return FormFactorUtils.FormFactor.WEAR.toString(); } else if (isTablet(d)) { return "Tablet"; } else if (isPhone(d)) { return "Phone"; } else { return "Other"; } } 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; } /** * 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)); } myModel.setItems(Lists.newArrayList(Iterables.filter(myDevices, new Predicate<Device>() { @Override public boolean apply(Device input) { return input.getDisplayName().toLowerCase().contains(searchString.toLowerCase()); } }))); } @Nullable @Override public Device getDevice() { return myTable.getSelectedObject(); } @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); } }