blob: e7950d9ae9ca0e705bdba84be1416179623a1c68 [file] [log] [blame]
/*
* Copyright (C) 2015 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.gradle.project.subset;
import com.android.SdkConstants;
import com.android.tools.idea.gradle.IdeaAndroidProject;
import com.android.tools.idea.gradle.IdeaGradleProject;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.externalSystem.model.DataNode;
import com.intellij.openapi.externalSystem.model.project.ModuleData;
import com.intellij.openapi.fileChooser.*;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.ValidationInfo;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileWrapper;
import com.intellij.ui.Cell;
import com.intellij.ui.TableSpeedSearch;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.table.JBTable;
import com.intellij.util.PairFunction;
import com.intellij.util.PlatformIcons;
import com.intellij.util.SystemProperties;
import org.jdesktop.swingx.JXLabel;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableRowSorter;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.text.Collator;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import static com.android.tools.idea.gradle.AndroidProjectKeys.IDE_ANDROID_PROJECT;
import static com.android.tools.idea.gradle.AndroidProjectKeys.IDE_GRADLE_PROJECT;
import static com.intellij.icons.AllIcons.Nodes.PpJdk;
import static com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil.getChildren;
import static com.intellij.openapi.util.JDOMUtil.loadDocument;
import static com.intellij.openapi.util.JDOMUtil.writeDocument;
import static com.intellij.openapi.util.text.StringUtil.isNotEmpty;
import static com.intellij.openapi.vfs.VfsUtilCore.virtualToIoFile;
import static com.intellij.util.containers.ContainerUtil.getFirstItem;
import static icons.AndroidIcons.AppModule;
import static icons.AndroidIcons.LibraryModule;
import static java.awt.BorderLayout.NORTH;
import static java.awt.BorderLayout.SOUTH;
import static java.awt.event.InputEvent.CTRL_MASK;
import static java.awt.event.InputEvent.META_MASK;
import static java.awt.event.KeyEvent.*;
import static java.util.Collections.emptyList;
import static javax.swing.ListSelectionModel.SINGLE_SELECTION;
public class ModulesToImportDialog extends DialogWrapper {
private static final int SELECTED_MODULE_COLUMN = 0;
private static final int MODULE_NAME_COLUMN = 1;
@NotNull private final List<DataNode<ModuleData>> alwaysIncludedModules = Lists.newArrayList();
@Nullable private final Project myProject;
private JPanel myPanel;
private JBTable myModulesTable;
private JXLabel myDescriptionLabel;
private JPanel myContentsPanel;
private JBLabel mySelectionStatusLabel;
private volatile boolean mySkipValidation;
private int myMaxSelectionCount = -1;
public ModulesToImportDialog(@NotNull Collection<DataNode<ModuleData>> modules, @Nullable Project project) {
super(project, true, IdeModalityType.IDE);
setTitle("Select Modules to Include in Project Subset");
myProject = project;
init();
ModuleTableModel model = getModulesTable().getModel();
for (DataNode<ModuleData> module : modules) {
Collection<DataNode<IdeaGradleProject>> gradleProjects = getChildren(module, IDE_GRADLE_PROJECT);
if (gradleProjects.isEmpty()) {
alwaysIncludedModules.add(module);
}
else {
// We only show modules that are recognized in Gradle.
// For example, in a multi-module project the top-level module is just a folder that contains the rest of
// modules, which is not defined in settings.gradle.
model.add(module);
}
}
getModulesTable().sort();
myDescriptionLabel.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
DefaultActionGroup group = new DefaultActionGroup();
group.addAll(new SelectAllAction(true), new SelectAllAction(false));
group.addSeparator();
group.add(new ShowSelectedModulesAction(getModulesTable()));
group.addSeparator();
group.addAll(new LoadFromFileAction(), new SaveToFileAction());
ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar("android.gradle.module.selection.dialog.toolbar", group, true);
myContentsPanel.add(toolbar.getComponent(), NORTH);
mySelectionStatusLabel = new JBLabel();
myContentsPanel.add(mySelectionStatusLabel, SOUTH);
updateSelectionStatus();
}
@NotNull
private ModuleTable getModulesTable() {
return (ModuleTable)myModulesTable;
}
private void updateSelectionStatus() {
ModuleTable table = getModulesTable();
ModuleTableModel model = table.getModel();
int rowCount = model.getRowCount();
int selectedRowCount = model.selectedRowCount;
String msg = String.format("%1$d Modules. %2$d selected", rowCount, selectedRowCount);
mySelectionStatusLabel.setText(msg);
table.updateFilter();
}
@NotNull
private static String getNameOf(@NotNull DataNode<ModuleData> module) {
return module.getData().getExternalName();
}
public void setDescription(@NotNull String description) {
myDescriptionLabel.setText(description);
}
@Override
@Nullable
protected ValidationInfo doValidate() {
int selectionCount = getModulesTable().getModel().selectedRowCount;
if (selectionCount <= 0) {
return new ValidationInfo("Please select at least one Module", myModulesTable);
}
if (myMaxSelectionCount > 0 && selectionCount > myMaxSelectionCount) {
String message = "Please select only " + myMaxSelectionCount + " module";
if (myMaxSelectionCount > 1) {
message += "s";
}
message += ".";
return new ValidationInfo(message, myModulesTable);
}
return null;
}
private boolean hasSelectedModules() {
return getModulesTable().getModel().selectedRowCount > 0;
}
private void setAllSelected(boolean selected) {
ModuleTableModel model = getModulesTable().getModel();
int count = model.getRowCount();
mySkipValidation = true;
for (int i = 0; i < count; i++) {
model.setItemSelected(i, selected);
}
mySkipValidation = false;
if (!selected) {
initValidation();
}
updateSelectionStatus();
}
@NotNull
public Collection<DataNode<ModuleData>> getSelectedModules() {
List<DataNode<ModuleData>> modules = Lists.newArrayList(alwaysIncludedModules);
modules.addAll(getUserSelectedModules());
return modules;
}
@VisibleForTesting
@NotNull
public Collection<String> getDisplayedModules() {
return getModulesTable().getModel().getModuleNames();
}
@NotNull
private Collection<DataNode<ModuleData>> getUserSelectedModules() {
List<DataNode<ModuleData>> modules = Lists.newArrayList();
ModuleTable table = getModulesTable();
ModuleTableModel model = table.getModel();
int count = model.getRowCount();
for (int i = 0; i < count; i++) {
if (model.isItemSelected(i)) {
modules.add(model.getItemAt(i));
}
}
return modules;
}
private void select(@NotNull List<String> moduleNames) {
ModuleTableModel model = getModulesTable().getModel();
int count = model.getRowCount();
for (int i = 0; i < count; i++) {
DataNode<ModuleData> module = model.getItemAt(i);
String name = getNameOf(module);
model.setItemSelected(i, moduleNames.contains(name));
}
}
@Override
@NotNull
protected JComponent createCenterPanel() {
return myPanel;
}
@Override
@Nullable
public JComponent getPreferredFocusedComponent() {
return myModulesTable;
}
private void createUIComponents() {
myModulesTable = new ModuleTable();
new TableSpeedSearch(myModulesTable, new PairFunction<Object, Cell, String>() {
@Override
public String fun(Object o, Cell v) {
if (o instanceof ModuleRow) {
ModuleRow row = (ModuleRow)o;
return getNameOf(row.module);
}
return o == null || o instanceof Boolean ? "" : o.toString();
}
});
}
public void updateSelection(@NotNull Collection<String> selection) {
ModuleTableModel model = getModulesTable().getModel();
int count = model.getRowCount();
mySkipValidation = true;
for (int i = 0; i < count; i++) {
DataNode<ModuleData> module = model.getItemAt(i);
String name = getNameOf(module);
boolean selected = selection.contains(name);
model.setItemSelected(i, selected);
}
mySkipValidation = false;
initValidation();
updateSelectionStatus();
}
public void setMaxSelectionCount(int maxSelectionCount) {
if (maxSelectionCount == 0) {
throw new IllegalArgumentException("Value must be different than zero");
}
myMaxSelectionCount = maxSelectionCount;
}
public void clearSelection() {
List<String> selection = Collections.emptyList();
updateSelection(selection);
}
private static class ShowSelectedModulesAction extends ToggleAction {
private ModuleTable myTable;
ShowSelectedModulesAction(@NotNull ModuleTable table) {
super("Show Selected Modules Only", null, AllIcons.Actions.ShowHiddens);
myTable = table;
}
@Override
public boolean isSelected(AnActionEvent e) {
return myTable.myShowSelectedRowsOnly;
}
@Override
public void setSelected(AnActionEvent e, boolean state) {
myTable.setShowSelectedRowsOnly(state);
}
}
private class SelectAllAction extends DumbAwareAction {
private final boolean mySelect;
SelectAllAction(boolean select) {
super(select ? "Select All" : "Unselect All", null, select ? PlatformIcons.SELECT_ALL_ICON : PlatformIcons.UNSELECT_ALL_ICON);
mySelect = select;
int keyCode = select ? VK_A : VK_N;
registerCustomShortcutSet(keyCode, SystemInfo.isMac? META_MASK : CTRL_MASK, myModulesTable);
}
@Override
public void update(@NotNull AnActionEvent e) {
int rowCount = getModulesTable().getModel().getRowCount();
e.getPresentation().setEnabled(rowCount > 0);
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
setAllSelected(mySelect);
}
}
private class LoadFromFileAction extends DumbAwareAction {
LoadFromFileAction() {
super("Load Selection from File", null, AllIcons.Actions.Menu_open);
registerCustomShortcutSet(VK_O, SystemInfo.isMac ? META_MASK : CTRL_MASK, myModulesTable);
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
FileChooserDescriptor descriptor = new FileChooserDescriptor(true, false, false, false, false, false) {
@Override
public boolean isFileSelectable(VirtualFile file) {
boolean selectable = super.isFileSelectable(file);
if (selectable) {
selectable = SdkConstants.EXT_XML.equals(file.getExtension());
}
return selectable;
}
};
String title = "Load Module Selection";
descriptor.setTitle(title);
FileChooserDialog dialog = FileChooserFactory.getInstance().createFileChooser(descriptor, myProject, getWindow());
VirtualFile[] allSelected = dialog.choose(myProject);
if (allSelected.length > 0) {
File file = virtualToIoFile(allSelected[0]);
try {
List<String> loadedModuleNames = Selection.load(file);
select(loadedModuleNames);
}
catch (Throwable error) {
String msg = String.format("Failed to load Module selection from file '%1$s'", file.getPath());
Messages.showErrorDialog(getWindow(), msg, title);
String cause = error.getMessage();
if (isNotEmpty(cause)) {
msg = msg + ":\n" + cause;
}
Logger.getInstance(ModulesToImportDialog.class).info(msg, error);
}
}
}
}
private class SaveToFileAction extends DumbAwareAction {
SaveToFileAction() {
super("Save Selection As", null, AllIcons.Actions.Menu_saveall);
registerCustomShortcutSet(VK_S, SystemInfo.isMac? META_MASK : CTRL_MASK, myModulesTable);
}
@Override
public void update(@NotNull AnActionEvent e) {
e.getPresentation().setEnabled(hasSelectedModules());
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
String title = "Save Module Selection";
FileSaverDescriptor descriptor = new FileSaverDescriptor(title, "Save the list of selected Modules to a file",
SdkConstants.EXT_XML);
FileSaverDialog dialog = FileChooserFactory.getInstance().createSaveFileDialog(descriptor, getWindow());
VirtualFile baseDir = myProject != null ? myProject.getBaseDir() : null;
VirtualFileWrapper result = dialog.save(baseDir, null);
if (result != null) {
File file = result.getFile();
try {
Selection.save(getUserSelectedModules(), file);
}
catch (IOException error) {
String msg = String.format("Failed to save Module selection to file '%1$s'", file.getPath());
Messages.showErrorDialog(getWindow(), msg, title);
String cause = error.getMessage();
if (isNotEmpty(cause)) {
msg = msg + ":\n" + cause;
}
Logger.getInstance(ModulesToImportDialog.class).info(msg, error);
}
}
}
}
// Module selection can be stored in XML files. Sample:
// <?xml version="1.0" encoding="UTF-8"?>
// <selectedModules>
// <module name="app" />
// <module name="mylibrary" />
//</selectedModules>
@VisibleForTesting
static class Selection {
@NonNls private static final String ROOT_ELEMENT_NAME = "selectedModules";
@NonNls private static final String MODULE_ELEMENT_NAME = "module";
@NonNls private static final String MODULE_NAME_ATTRIBUTE_NAME = "name";
@NotNull
static List<String> load(@NotNull File file) throws JDOMException, IOException {
Document document = loadDocument(file);
List<String> modules = Lists.newArrayList();
Element rootElement = document.getRootElement();
if (rootElement != null && ROOT_ELEMENT_NAME.equals(rootElement.getName())) {
for (Element child : rootElement.getChildren(MODULE_ELEMENT_NAME)) {
String moduleName = child.getAttributeValue(MODULE_NAME_ATTRIBUTE_NAME);
if (isNotEmpty(moduleName)) {
modules.add(moduleName);
}
}
}
return modules;
}
static void save(@NotNull Collection<DataNode<ModuleData>> modules, @NotNull File file) throws IOException {
Document document = new Document(new Element(ROOT_ELEMENT_NAME));
for (DataNode<ModuleData> module : modules) {
Element child = new Element(MODULE_ELEMENT_NAME);
child.setAttribute(MODULE_NAME_ATTRIBUTE_NAME, getNameOf(module));
document.getRootElement().addContent(child);
}
writeDocument(document, file, SystemProperties.getLineSeparator());
}
}
private class ModuleTable extends JBTable {
private ModuleTableRowSorter myRowSorter;
private boolean myShowSelectedRowsOnly;
ModuleTable() {
super(new ModuleTableModel());
setCheckBoxColumnWidth();
setModuleNameCellRenderer();
setAutoResizeMode(AUTO_RESIZE_LAST_COLUMN);
setIntercellSpacing(new Dimension(0, 0));
setRowSelectionAllowed(true);
setSelectionMode(SINGLE_SELECTION);
setShowGrid(false);
setTableHeader(null);
}
private void setCheckBoxColumnWidth() {
TableColumn column = getColumnModel().getColumn(SELECTED_MODULE_COLUMN);
int width = 30;
column.setMaxWidth(width);
column.setPreferredWidth(width);
column.setWidth(width);
}
private void setModuleNameCellRenderer() {
TableColumn column = getColumnModel().getColumn(MODULE_NAME_COLUMN);
column.setCellRenderer(new DefaultTableCellRenderer() {
@Override
public Component getTableCellRendererComponent(JTable table,
Object value,
boolean isSelected,
boolean hasFocus,
int rowIndex,
int columnIndex) {
Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, rowIndex, columnIndex);
if (c instanceof JLabel && value instanceof ModuleRow) {
JLabel label = (JLabel)c;
ModuleRow row = (ModuleRow)value;
label.setIcon(row.icon);
label.setText(getNameOf(row.module));
}
return c;
}
});
}
void updateFilter() {
setShowSelectedRowsOnly(myShowSelectedRowsOnly);
}
void setShowSelectedRowsOnly(boolean showSelectedRowsOnly) {
myShowSelectedRowsOnly = showSelectedRowsOnly;
if (myRowSorter == null) {
sort();
}
if (showSelectedRowsOnly) {
myRowSorter.setRowFilter(new RowFilter<ModuleTableModel, Integer>() {
@Override
public boolean include(Entry<? extends ModuleTableModel, ? extends Integer> entry) {
Object value = entry.getValue(MODULE_NAME_COLUMN);
if (value instanceof ModuleRow) {
ModuleRow row = (ModuleRow)value;
return row.selected;
}
return false;
}
});
}
else {
myRowSorter.setRowFilter(null);
}
}
void sort() {
myRowSorter = new ModuleTableRowSorter(getModel());
setRowSorter(myRowSorter);
}
@Override
@NotNull
public ModuleTableModel getModel() {
return (ModuleTableModel)super.getModel();
}
}
private static class ModuleTableRowSorter extends TableRowSorter<ModuleTableModel> {
ModuleTableRowSorter(@NotNull ModuleTableModel model) {
super(model);
setComparator(MODULE_NAME_COLUMN, new Comparator<ModuleRow>() {
@Override
public int compare(ModuleRow row1, ModuleRow row2) {
return Collator.getInstance().compare(row1.toString(), row2.toString());
}
});
List<RowSorter.SortKey> sortKeys = Lists.newArrayList();
sortKeys.add(new RowSorter.SortKey(MODULE_NAME_COLUMN, SortOrder.ASCENDING));
setSortKeys(sortKeys);
}
}
private class ModuleTableModel extends AbstractTableModel {
public int selectedRowCount;
@NotNull private final List<ModuleRow> rows = Lists.newArrayList();
@NotNull
Collection<String> getModuleNames() {
if (rows.isEmpty()) {
return emptyList();
}
List<String> names = Lists.newArrayListWithExpectedSize(rows.size());
for (ModuleRow row : rows) {
names.add(getNameOf(row.module));
}
return names;
}
@Override
public int getRowCount() {
return rows.size();
}
@Override
public int getColumnCount() {
return 2;
}
@Override
@Nullable
public Object getValueAt(int rowIndex, int columnIndex) {
if (rowIndex < rows.size()) {
ModuleRow row = rows.get(rowIndex);
switch (columnIndex) {
case SELECTED_MODULE_COLUMN:
return row.selected;
default:
return row;
}
}
return null;
}
@Override
public Class<?> getColumnClass(int columnIndex) {
switch (columnIndex) {
case SELECTED_MODULE_COLUMN:
return Boolean.class;
case MODULE_NAME_COLUMN:
return ModuleRow.class;
default:
return Object.class;
}
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
return columnIndex == SELECTED_MODULE_COLUMN;
}
@Override
public void setValueAt(@Nullable Object aValue, int rowIndex, int columnIndex) {
if (rowIndex < rows.size() && columnIndex == SELECTED_MODULE_COLUMN && aValue instanceof Boolean) {
boolean selected = (Boolean)aValue;
if (setItemSelected(rowIndex, selected) && !mySkipValidation) {
initValidation();
updateSelectionStatus();
}
}
}
void add(@NotNull DataNode<ModuleData> module) {
rows.add(new ModuleRow(module));
selectedRowCount++;
}
boolean isItemSelected(int rowIndex) {
ModuleRow row = rows.get(rowIndex);
return row.selected;
}
@NotNull
DataNode<ModuleData> getItemAt(int rowIndex) {
ModuleRow row = rows.get(rowIndex);
return row.module;
}
boolean setItemSelected(int rowIndex, boolean selected) {
ModuleRow row = rows.get(rowIndex);
if (row.selected != selected) {
row.selected = selected;
if (row.selected) {
selectedRowCount++;
}
else {
selectedRowCount--;
}
return true;
}
return false;
}
}
private static class ModuleRow {
@NotNull public final DataNode<ModuleData> module;
@NotNull public final Icon icon;
public boolean selected = true;
ModuleRow(@NotNull DataNode<ModuleData> module) {
this.module = module;
icon = getModuleIcon(module);
}
@NotNull
private static Icon getModuleIcon(@NotNull DataNode<ModuleData> module) {
Collection<DataNode<IdeaAndroidProject>> children = getChildren(module, IDE_ANDROID_PROJECT);
if (!children.isEmpty()) {
DataNode<IdeaAndroidProject> child = getFirstItem(children);
if (child != null) {
IdeaAndroidProject androidModel = child.getData();
return androidModel.getAndroidProject().isLibrary() ? LibraryModule : AppModule;
}
}
return PpJdk;
}
@Override
public String toString() {
return getNameOf(module);
}
}
}