/*
* 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.welcome;
import com.android.sdklib.devices.Storage.Unit;
import com.android.tools.idea.wizard.ScopedStateStore;
import com.android.tools.idea.wizard.WizardConstants;
import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.JBColor;
import com.intellij.ui.table.JBTable;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.core.GridLayoutManager;
import com.intellij.util.PathUtil;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Set;
/**
* Wizard page for selecting SDK components to download.
*/
public class SdkComponentsStep extends FirstRunWizardStep {
private static final ScopedStateStore.Key<BitSet> KEY_SELECTED_COMPONENTS =
ScopedStateStore.createKey("selected.components", ScopedStateStore.Scope.STEP, BitSet.class);
private final SdkComponent[] mySdkComponents;
private final ScopedStateStore.Key<Boolean> myKeyShouldDownload;
private JPanel myContents;
private JBTable myComponentsTable;
private JTextPane myComponentDescription;
private JLabel myNeededSpace;
private JLabel myAvailableSpace;
private JLabel myErrorMessage;
private JSplitPane mySplitPane;
private Set<SdkComponent> myUncheckedComponents = Sets.newHashSet();
private ScopedStateStore.Key<String> mySdkDownloadPathKey;
private TextFieldWithBrowseButton myPath;
private boolean myUserEditedPath = false;
public SdkComponentsStep(ScopedStateStore.Key<Boolean> keyShouldDownload, ScopedStateStore.Key<String> sdkDownloadPathKey) {
super("SDK Settings");
myPath.addBrowseFolderListener("Android SDK", "Select Android SDK install directory", null,
FileChooserDescriptorFactory.createSingleFolderDescriptor());
myKeyShouldDownload = keyShouldDownload;
mySdkDownloadPathKey = sdkDownloadPathKey;
myComponentDescription.setEditable(false);
myComponentDescription.setContentType("text/html");
myComponentDescription.setBorder(new EmptyBorder(WizardConstants.STUDIO_WIZARD_INSETS));
mySplitPane.setBorder(null);
Font labelFont = UIUtil.getLabelFont();
Font smallLabelFont = labelFont.deriveFont(labelFont.getSize() - 1.0f);
myNeededSpace.setFont(smallLabelFont);
myAvailableSpace.setFont(smallLabelFont);
myErrorMessage.setText(null);
myErrorMessage.setForeground(JBColor.red);
mySdkComponents = createModel();
DefaultTableModel model = new DefaultTableModel(0, 1) {
@Override
public void setValueAt(Object aValue, int row, int column) {
boolean isSelected = ((Boolean)aValue);
SdkComponent sdkComponent = mySdkComponents[row];
if (isSelected) {
select(sdkComponent);
}
else {
deselect(sdkComponent);
}
fireTableRowsUpdated(row, row);
}
};
for (SdkComponent sdkComponent : mySdkComponents) {
model.addRow(new Object[]{sdkComponent});
}
myComponentsTable.setModel(model);
myComponentsTable.setTableHeader(null);
myComponentsTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
int selected = myComponentsTable.getSelectedRow();
String description = selected >= 0 ? mySdkComponents[selected].myDescription : null;
myComponentDescription.setText(description);
}
});
TableColumn column = myComponentsTable.getColumnModel().getColumn(0);
column.setCellRenderer(new SdkComponentRenderer());
column.setCellEditor(new SdkComponentRenderer());
setComponent(myContents);
}
private static SdkComponent[] createModel() {
long mb = Unit.MiB.getNumberOfBytes();
SdkComponent androidSdk = new SdkComponent("Android Studio + SDK", 684 * mb, null, false);
SdkComponent sdkPlatform = new SdkComponent("Android SDK Platform", 0, null, true);
SdkComponent lmp = new SdkComponent("LMP - Android 5.0 (API 21)", 292 * mb, sdkPlatform, true);
SdkComponent root = new SdkComponent("Android Emulator", 0, null, true);
SdkComponent nexus = new SdkComponent("Nexus", 0, root, true);
SdkComponent nexus5 = new SdkComponent("Nexus 5", 2499 * mb, nexus, true);
SdkComponent performance = new SdkComponent("Performance", 0, root, true);
SdkComponent haxm = new SdkComponent("Intel® HAXM", 2306867, performance, true);
return new SdkComponent[]{androidSdk, sdkPlatform, lmp, root, nexus, nexus5, performance, haxm};
}
private static boolean isChild(@Nullable SdkComponent child, @NotNull SdkComponent sdkComponent) {
return child != null && (child == sdkComponent || isChild(child.myParent, sdkComponent));
}
@Nullable
private static File getExistingParentFile(@Nullable String path) {
if (StringUtil.isEmpty(path)) {
return null;
}
File file = new File(path).getAbsoluteFile();
while (file != null && !file.exists()) {
file = file.getParentFile();
}
return file;
}
private static String getDiskSpace(@Nullable String path) {
File file = getExistingParentFile(path);
if (file == null) {
File[] files = File.listRoots();
if (files.length != 0) {
file = files[0];
}
}
if (file == null) {
return "";
}
String available = getSizeLabel(file.getFreeSpace());
if (SystemInfo.isWindows) {
while (file.getParent() != null) {
file = file.getParentFile();
}
return String.format("Disk space available on rive %s: %s", file.getName(), available);
}
else {
return String.format("Available disk space: %s", available);
}
}
private static String getSizeLabel(long freeSpace) {
Unit[] values = Unit.values();
Unit unit = values[values.length - 1];
for (int i = values.length - 2; unit.getNumberOfBytes() > freeSpace && i >= 0; i--) {
unit = values[i];
}
final double space = freeSpace * 1.0 / unit.getNumberOfBytes();
String formatted = roundToNumberOfDigits(space, 3);
return String.format("%s %s", formatted, unit.toString());
}
/**
* <p>Returns a string that rounds the number so number of
* integer places + decimal places is less or equal to maxDigits.</p>
* <p>Number will not be truncated if it has more integer digits
* then macDigits</p>
*/
private static String roundToNumberOfDigits(double number, int maxDigits) {
int multiplier = 1, digits;
for (digits = maxDigits; digits > 0 && number > multiplier; digits--) {
multiplier *= 10;
}
NumberFormat numberInstance = NumberFormat.getNumberInstance();
numberInstance.setGroupingUsed(false);
numberInstance.setRoundingMode(RoundingMode.HALF_UP);
numberInstance.setMaximumFractionDigits(digits);
return numberInstance.format(number);
}
@NotNull
private static String inventDescription(String name, long size) {
return String.format("<html><p>This is a description for <em>%s</em> component</p>" +
"<p>We know is that it takes <strong>%s</strong> disk space</p></html>", name, getSizeLabel(size));
}
@Override
public boolean validate() {
String error = validatePath(myState.get(mySdkDownloadPathKey));
setErrorHtml(myUserEditedPath ? error : null);
return error == null;
}
@Nullable
private String validatePath(@Nullable String path) {
if (StringUtil.isEmpty(path)) {
return "Path is empty";
}
else {
myUserEditedPath = true;
File file = new File(path);
while (file != null && !file.exists()) {
if (!PathUtil.isValidFileName(file.getName())) {
return "Specified path is not valid";
}
file = file.getParentFile();
}
}
return null;
}
@Override
public void deriveValues(Set<ScopedStateStore.Key> modified) {
myAvailableSpace.setText(getDiskSpace(myState.get(mySdkDownloadPathKey)));
BitSet bitSet = myState.get(KEY_SELECTED_COMPONENTS);
long selected = 0;
for (int i = 0; i < mySdkComponents.length; i++) {
if (bitSet == null || bitSet.get(i)) {
SdkComponent sdkComponent = mySdkComponents[i];
selected += sdkComponent.mySize;
}
}
myNeededSpace.setText(String.format("Total disk space required: %s", getSizeLabel(selected)));
super.deriveValues(modified);
}
private void deselect(SdkComponent sdkComponent) {
for (SdkComponent child : mySdkComponents) {
if (child.mySize > 0 && isChild(child, sdkComponent)) {
myUncheckedComponents.add(child);
}
}
}
private Iterable<SdkComponent> getChildren(final SdkComponent sdkComponent) {
return Iterables.filter(Arrays.asList(mySdkComponents), new Predicate<SdkComponent>() {
@Override
public boolean apply(@Nullable SdkComponent input) {
assert input != null;
SdkComponent n = input;
do {
if (n == sdkComponent) {
return true;
}
n = n.myParent;
}
while (n != null);
return false;
}
});
}
private void select(SdkComponent sdkComponent) {
for (SdkComponent child : getChildren(sdkComponent)) {
myUncheckedComponents.remove(child);
}
}
@Override
public void init() {
register(mySdkDownloadPathKey, myPath);
register(KEY_SELECTED_COMPONENTS, myComponentsTable, new ComponentBinding<BitSet, JBTable>() {
@Override
public void setValue(@Nullable BitSet newValue, @NotNull JBTable component) {
for (int i = 0; i < mySdkComponents.length; i++) {
component.getModel().setValueAt(newValue == null || newValue.get(i), i, 0);
}
}
@Nullable
@Override
public BitSet getValue(@NotNull JBTable component) {
BitSet bitSet = new BitSet(mySdkComponents.length);
int i = 0;
for (SdkComponent sdkComponent : mySdkComponents) {
bitSet.set(i++, sdkComponent.mySize > 0 && !myUncheckedComponents.contains(sdkComponent));
}
return bitSet;
}
@Override
public void addActionListener(@NotNull final ActionListener listener, @NotNull final JBTable component) {
component.getModel().addTableModelListener(new TableModelListener() {
@Override
public void tableChanged(TableModelEvent e) {
ActionEvent event = new ActionEvent(component, ActionEvent.ACTION_FIRST + 1, "toggle");
listener.actionPerformed(event);
}
});
}
});
}
@NotNull
@Override
public JLabel getMessageLabel() {
return myErrorMessage;
}
@Override
public JComponent getPreferredFocusedComponent() {
return myComponentsTable;
}
private boolean isSelected(SdkComponent sdkComponent) {
for (SdkComponent child : getChildren(sdkComponent)) {
if (myUncheckedComponents.contains(child)) {
return false;
}
}
return true;
}
@Override
public boolean isStepVisible() {
return Objects.equal(Boolean.TRUE, myState.get(myKeyShouldDownload));
}
private static final class SdkComponent {
@NotNull private final String myName;
private final long mySize;
@Nullable private final SdkComponent myParent;
private final boolean myCanDeselect;
private final String myDescription;
public SdkComponent(@NotNull String name, long size, @Nullable SdkComponent parent, boolean canDeselect) {
this(name, size, parent, canDeselect, inventDescription(name, size));
}
public SdkComponent(@NotNull String name, long size, @Nullable SdkComponent parent, boolean canDeselect, @NotNull String description) {
myName = name;
mySize = size;
myParent = parent;
myCanDeselect = canDeselect;
myDescription = description;
}
@Override
public String toString() {
return myName;
}
public String getLabel() {
return mySize == 0 ? myName : String.format("%s – (%s)", myName, getSizeLabel(mySize));
}
}
private final class SdkComponentRenderer extends AbstractCellEditor implements TableCellRenderer, TableCellEditor {
private final JPanel myPanel;
private final JCheckBox myCheckBox;
private Border myEmptyBorder;
public SdkComponentRenderer() {
myPanel = new JPanel(new GridLayoutManager(1, 1));
myCheckBox = new JCheckBox();
myCheckBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
stopCellEditing();
}
});
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
setupControl(table, value, isSelected, hasFocus);
return myPanel;
}
private void setupControl(JTable table, Object value, boolean isSelected, boolean hasFocus) {
myPanel.setBorder(getCellBorder(table, isSelected && hasFocus));
Color foreground;
if (isSelected) {
myPanel.setBackground(table.getSelectionBackground());
foreground = table.getSelectionForeground();
}
else {
myPanel.setBackground(table.getBackground());
foreground = table.getForeground();
}
myCheckBox.setForeground(foreground);
myPanel.remove(myCheckBox);
SdkComponent sdkComponent = (SdkComponent)value;
int indent = 0;
if (sdkComponent != null) {
myCheckBox.setEnabled(sdkComponent.myCanDeselect);
myCheckBox.setText(sdkComponent.getLabel());
myCheckBox.setSelected(isSelected((SdkComponent)value));
while (sdkComponent.myParent != null) {
indent++;
sdkComponent = sdkComponent.myParent;
assert sdkComponent != null;
}
}
myPanel.add(myCheckBox,
new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED,
GridConstraints.SIZEPOLICY_FIXED, null, null, null, indent * 2));
}
private Border getCellBorder(JTable table, boolean isSelectedFocus) {
Border focusedBorder = UIUtil.getTableFocusCellHighlightBorder();
Border border;
if (isSelectedFocus) {
border = focusedBorder;
}
else {
if (myEmptyBorder == null) {
myEmptyBorder = new EmptyBorder(focusedBorder.getBorderInsets(table));
}
border = myEmptyBorder;
}
return border;
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
setupControl(table, value, true, true);
return myPanel;
}
@Override
public Object getCellEditorValue() {
return myCheckBox.isSelected();
}
}
}