/* * 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.gradle.variant.profiles; import com.android.builder.model.AndroidLibrary; import com.android.builder.model.Variant; import com.android.tools.idea.gradle.IdeaAndroidProject; import com.android.tools.idea.gradle.util.GradleUtil; import com.android.tools.idea.gradle.variant.conflict.Conflict; import com.google.common.base.Joiner; import com.google.common.base.Objects; import com.google.common.collect.HashMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.intellij.icons.AllIcons; import com.intellij.ide.CommonActionsManager; import com.intellij.ide.TreeExpander; import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.actionSystem.ActionToolbar; import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ui.configuration.ModulesAlphaComparator; import com.intellij.openapi.ui.DetailsComponent; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.ui.Splitter; import com.intellij.openapi.util.text.StringUtil; import com.intellij.ui.*; import com.intellij.ui.components.panels.Wrapper; import com.intellij.ui.table.JBTable; import com.intellij.util.Function; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.tree.TreeUtil; import icons.AndroidIcons; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.util.BooleanCellRenderer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreeNode; import javax.swing.tree.TreeSelectionModel; import java.awt.*; import java.text.Collator; import java.util.*; import java.util.List; public class ProjectProfileSelectionDialog extends DialogWrapper { private static final SimpleTextAttributes UNRESOLVED_ATTRIBUTES = new SimpleTextAttributes(SimpleTextAttributes.STYLE_STRIKEOUT, SimpleTextAttributes.GRAY_ATTRIBUTES.getFgColor()); @NotNull private final Project myProject; @NotNull private final List<Conflict> myConflicts; @NotNull private final JPanel myPanel; @NotNull private CheckboxTreeView myProjectStructureTree; @NotNull private ConflictsTable myConflictsTable; @NotNull private CheckboxTreeView myConflictTree; @NotNull private DetailsComponent myConflictDetails; public ProjectProfileSelectionDialog(@NotNull Project project, @NotNull List<Conflict> conflicts) { super(project); myProject = project; myConflicts = conflicts; for (Conflict conflict : conflicts) { conflict.refreshStatus(); } myPanel = new JPanel(new BorderLayout()); Splitter splitter = new Splitter(false, .35f); splitter.setHonorComponentsMinimumSize(true); myPanel.add(splitter, BorderLayout.CENTER); splitter.setFirstComponent(createProjectStructurePanel()); splitter.setSecondComponent(createConflictsPanel()); init(); myProjectStructureTree.expandAll(); } @NotNull private JComponent createProjectStructurePanel() { createProjectStructureTree(); DetailsComponent details = new DetailsComponent(); details.setText("Project Structure"); details.setContent(createTreePanel(myProjectStructureTree)); removeEmptyBorder(details); return details.getComponent(); } private void createProjectStructureTree() { CheckboxTree.CheckboxTreeCellRenderer renderer = new CheckboxTree.CheckboxTreeCellRenderer() { @Override public void customizeRenderer(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { if (value instanceof DefaultMutableTreeNode) { Object data = ((DefaultMutableTreeNode)value).getUserObject(); ColoredTreeCellRenderer textRenderer = getTextRenderer(); if (data instanceof ModuleTreeElement) { ModuleTreeElement moduleElement = (ModuleTreeElement)data; textRenderer.append(moduleElement.myModule.getName()); if (!moduleElement.myConflicts.isEmpty()) { boolean allResolved = true; for (Conflict conflict : moduleElement.myConflicts) { if (!conflict.isResolved()) { allResolved = false; break; } } SimpleTextAttributes attributes = allResolved ? UNRESOLVED_ATTRIBUTES : SimpleTextAttributes.GRAY_ATTRIBUTES; textRenderer.append(" "); textRenderer.append(myConflicts.size() == 1 ? "[Conflict]" : "[Conflicts]", attributes); } textRenderer.setIcon(AllIcons.Actions.Module); } else if (data instanceof String) { textRenderer.append((String)data, SimpleTextAttributes.REGULAR_ITALIC_ATTRIBUTES); textRenderer.setIcon(AndroidIcons.Variant); } else if (data instanceof DependencyTreeElement) { DependencyTreeElement dependency = (DependencyTreeElement)data; textRenderer.append(dependency.myModule.getName()); if (!StringUtil.isEmpty(dependency.myVariant)) { textRenderer.append(" (" + dependency.myVariant + ")", SimpleTextAttributes.GRAY_ATTRIBUTES); } Icon icon = dependency.myConflict != null ? AllIcons.RunConfigurations.TestFailed : AllIcons.RunConfigurations.TestPassed; textRenderer.setIcon(icon); } } } }; CheckedTreeNode rootNode = new FilterAwareCheckedTreeNode(null); ModuleManager moduleManager = ModuleManager.getInstance(myProject); Module[] modules = moduleManager.getModules(); Arrays.sort(modules, ModulesAlphaComparator.INSTANCE); Map<String, Module> modulesByGradlePath = Maps.newHashMap(); for (Module module : modules) { String gradlePath = GradleUtil.getGradlePath(module); if (StringUtil.isEmpty(gradlePath)) { // The top-level module representing the project usually does not have a Gradle path. // We always want to include it, therefore we don't give users a chance to uncheck it in the "Project Structure" pane. continue; } modulesByGradlePath.put(gradlePath, module); ModuleTreeElement moduleElement = new ModuleTreeElement(module); CheckedTreeNode moduleNode = new FilterAwareCheckedTreeNode(moduleElement); rootNode.add(moduleNode); IdeaAndroidProject androidProject = getAndroidProject(module); if (androidProject == null) { continue; } Multimap<String, DependencyTreeElement> dependenciesByVariant = HashMultimap.create(); for (Variant variant : androidProject.getDelegate().getVariants()) { for (AndroidLibrary library : GradleUtil.getDirectLibraryDependencies(variant)) { gradlePath = library.getProject(); if (gradlePath == null) { continue; } Module dependency = modulesByGradlePath.get(gradlePath); if (dependency == null) { dependency = GradleUtil.findModuleByGradlePath(myProject, gradlePath); } if (dependency == null) { continue; } Conflict conflict = getConflict(dependency); modulesByGradlePath.put(gradlePath, dependency); DependencyTreeElement dependencyElement = new DependencyTreeElement(dependency, gradlePath, library.getProjectVariant(), conflict); dependenciesByVariant.put(variant.getName(), dependencyElement); } } List<String> variantNames = Lists.newArrayList(dependenciesByVariant.keySet()); Collections.sort(variantNames); List<String> consolidatedVariants = Lists.newArrayList(); List<String> variantsToSkip = Lists.newArrayList(); int variantCount = variantNames.size(); for (int i = 0; i < variantCount; i++) { String variant1 = variantNames.get(i); if (variantsToSkip.contains(variant1)) { continue; } Collection<DependencyTreeElement> set1 = dependenciesByVariant.get(variant1); for (int j = i + 1; j < variantCount; j++) { String variant2 = variantNames.get(j); Collection<DependencyTreeElement> set2 = dependenciesByVariant.get(variant2); if (set1.equals(set2)) { variantsToSkip.add(variant2); if (!consolidatedVariants.contains(variant1)) { consolidatedVariants.add(variant1); } consolidatedVariants.add(variant2); } } String variantName = variant1; if (!consolidatedVariants.isEmpty()) { variantName = Joiner.on(", ").join(consolidatedVariants); } DefaultMutableTreeNode variantNode = new DefaultMutableTreeNode(variantName); moduleNode.add(variantNode); List<DependencyTreeElement> dependencyElements = Lists.newArrayList(set1); Collections.sort(dependencyElements); for (DependencyTreeElement dependencyElement : dependencyElements) { if (dependencyElement.myConflict != null) { moduleElement.addConflict(dependencyElement.myConflict); } variantNode.add(new DefaultMutableTreeNode(dependencyElement)); } consolidatedVariants.clear(); } } myProjectStructureTree = new CheckboxTreeView(renderer, rootNode) { @Override protected void onNodeStateChanged(@NotNull CheckedTreeNode node) { Module module = null; Object data = node.getUserObject(); if (data instanceof ModuleTreeElement) { module = ((ModuleTreeElement)data).myModule; } if (module == null) { return; } boolean updated = false; Enumeration variantNodes = myConflictTree.myRoot.children(); while (variantNodes.hasMoreElements()) { Object child = variantNodes.nextElement(); if (!(child instanceof CheckedTreeNode)) { continue; } CheckedTreeNode variantNode = (CheckedTreeNode)child; Enumeration moduleNodes = variantNode.children(); while (moduleNodes.hasMoreElements()) { child = moduleNodes.nextElement(); if (!(child instanceof CheckedTreeNode)) { continue; } CheckedTreeNode moduleNode = (CheckedTreeNode)child; data = moduleNode.getUserObject(); if (!(data instanceof Conflict.AffectedModule)) { continue; } Conflict.AffectedModule affected = (Conflict.AffectedModule)data; boolean checked = node.isChecked(); if (module.equals(affected.getTarget()) && moduleNode.isChecked() != checked) { affected.setSelected(checked); moduleNode.setChecked(checked); updated = true; } } } if (updated) { repaintAll(); } } }; myProjectStructureTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); myProjectStructureTree.setRootVisible(false); } @Nullable private static IdeaAndroidProject getAndroidProject(@NotNull Module module) { AndroidFacet facet = AndroidFacet.getInstance(module); return facet != null ? facet.getIdeaAndroidProject() : null; } @Nullable private Conflict getConflict(@NotNull Module source) { for (Conflict conflict : myConflicts) { if (source.equals(conflict.getSource())) { return conflict; } } return null; } @NotNull private JComponent createConflictsPanel() { createConflictTree(); myConflictDetails = new DetailsComponent(); myConflictDetails.setText("Conflict Detail"); myConflictDetails.setContent(createTreePanel(myConflictTree)); removeEmptyBorder(myConflictDetails); createConflictsTable(); myConflictsTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent e) { if (e.getValueIsAdjusting()) { return; } showConflictDetail(); } }); Splitter splitter = new Splitter(true, .25f); splitter.setHonorComponentsMinimumSize(true); DetailsComponent details = new DetailsComponent(); details.setText("Variant Selection Conflicts"); details.setContent(ScrollPaneFactory.createScrollPane(myConflictsTable)); removeEmptyBorder(details); splitter.setFirstComponent(details.getComponent()); splitter.setSecondComponent(myConflictDetails.getComponent()); return splitter; } @NotNull static JPanel createTreePanel(@NotNull CheckboxTreeView tree) { JPanel treePanel = new JPanel(new BorderLayout()); DefaultActionGroup group = new DefaultActionGroup(); CommonActionsManager actions = CommonActionsManager.getInstance(); group.addAll(actions.createExpandAllAction(tree, treePanel), actions.createCollapseAllAction(tree, treePanel)); ActionToolbar actionToolBar = ActionManager.getInstance().createActionToolbar("", group, true); JPanel buttonsPanel = new JPanel(new BorderLayout()); buttonsPanel.add(actionToolBar.getComponent(), BorderLayout.CENTER); buttonsPanel.setBorder(new SideBorder(UIUtil.getBorderColor(), SideBorder.TOP | SideBorder.LEFT | SideBorder.RIGHT, false, 1)); treePanel.add(buttonsPanel, BorderLayout.NORTH); treePanel.add(ScrollPaneFactory.createScrollPane(tree), BorderLayout.CENTER); return treePanel; } private static void removeEmptyBorder(@NotNull DetailsComponent details) { JComponent gutter = details.getContentGutter(); for (Component child : gutter.getComponents()) { if (child instanceof Wrapper) { ((Wrapper)child).setBorder(null); } } } private void createConflictsTable() { ConflictsTableModel tableModel = new ConflictsTableModel(myConflicts, new Function<List<ConflictTableRow>, Void>() { @Override public Void fun(List<ConflictTableRow> rows) { filterProjectStructure(rows); return null; } }); myConflictsTable = new ConflictsTable(tableModel); if (!tableModel.myRows.isEmpty()) { showConflictDetail(); } } private void showConflictDetail() { myConflictTree.myRoot.removeAllChildren(); int selectedIndex = myConflictsTable.getSelectedRow(); ConflictsTableModel tableModel = (ConflictsTableModel)myConflictsTable.getModel(); ConflictTableRow row = tableModel.myRows.get(selectedIndex); Conflict conflict = row.myConflict; myConflictDetails.setText("Conflict Detail: " + conflict.getSource().getName()); List<String> variants = Lists.newArrayList(conflict.getVariants()); Collections.sort(variants); for (String variant : variants) { CheckedTreeNode variantNode = new CheckedTreeNode(variant); myConflictTree.myRoot.add(variantNode); for (Conflict.AffectedModule module : conflict.getModulesExpectingVariant(variant)) { CheckedTreeNode moduleNode = new CheckedTreeNode(module); variantNode.add(moduleNode); } } myConflictTree.reload(); myConflictTree.expandAll(); } private void filterProjectStructure(@NotNull List<ConflictTableRow> rows) { List<Module> selectedConflictSources = Lists.newArrayList(); for (ConflictTableRow row : rows) { if (row.myFilter) { selectedConflictSources.add(row.myConflict.getSource()); } } Enumeration moduleNodes = myProjectStructureTree.myRoot.children(); while (moduleNodes.hasMoreElements()) { boolean show = false; Object child = moduleNodes.nextElement(); if (!(child instanceof FilterAwareCheckedTreeNode)) { continue; } FilterAwareCheckedTreeNode moduleNode = (FilterAwareCheckedTreeNode)child; Object data = moduleNode.getUserObject(); if (!(data instanceof ModuleTreeElement)) { continue; } ModuleTreeElement moduleElement = (ModuleTreeElement)data; if (selectedConflictSources.isEmpty()) { show = true; } else { // We show the modules that depend on any of the selected conflict sources. for (Conflict conflict : moduleElement.myConflicts) { if (selectedConflictSources.contains(conflict.getSource())) { show = true; break; } } // We show the conflict sources as well. if (!show && selectedConflictSources.contains(moduleElement.myModule)) { show = true; } } moduleNode.myVisible = show; } myProjectStructureTree.reload(); myProjectStructureTree.expandAll(); } private void createConflictTree() { CheckboxTree.CheckboxTreeCellRenderer renderer = new CheckboxTree.CheckboxTreeCellRenderer() { @Override public void customizeRenderer(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { if (value instanceof DefaultMutableTreeNode) { Object data = ((DefaultMutableTreeNode)value).getUserObject(); ColoredTreeCellRenderer textRenderer = getTextRenderer(); if (data instanceof Conflict.AffectedModule) { textRenderer.append(((Conflict.AffectedModule)data).getTarget().getName()); textRenderer.setIcon(AllIcons.Actions.Module); } else if (data instanceof String) { textRenderer.append((String)data, SimpleTextAttributes.REGULAR_ITALIC_ATTRIBUTES); textRenderer.setIcon(AndroidIcons.Variant); } } } }; FilterAwareCheckedTreeNode root = new FilterAwareCheckedTreeNode(null); myConflictTree = new CheckboxTreeView(renderer, root) { @Override protected void onNodeStateChanged(@NotNull CheckedTreeNode node) { Object data = node.getUserObject(); if (!(data instanceof Conflict.AffectedModule)) { return; } Conflict.AffectedModule affected = (Conflict.AffectedModule)data; Module module = affected.getTarget(); Enumeration moduleNodes = myProjectStructureTree.myRoot.children(); while (moduleNodes.hasMoreElements()) { Object child = moduleNodes.nextElement(); if (!(child instanceof CheckedTreeNode)) { continue; } CheckedTreeNode moduleNode = (CheckedTreeNode)child; data = moduleNode.getUserObject(); if (data instanceof ModuleTreeElement) { ModuleTreeElement moduleElement = (ModuleTreeElement)data; boolean checked = node.isChecked(); if (module.equals(moduleElement.myModule) && moduleNode.isChecked() != checked) { moduleNode.setChecked(checked); affected.setSelected(checked); repaintAll(); break; } } } } }; } private void repaintAll() { myConflictsTable.repaint(); myConflictTree.repaint(); myProjectStructureTree.repaint(); } @Override @NotNull protected JComponent createCenterPanel() { return myPanel; } private static class ConflictsTable extends JBTable { ConflictsTable(@NotNull ConflictsTableModel model) { super(model); setSelectionMode(ListSelectionModel.SINGLE_SELECTION); if (!model.myRows.isEmpty()) { // select first row by default. getSelectionModel().setSelectionInterval(0, 0); } TableColumn filterColumn = getColumnByIndex(ConflictsTableModel.FILTER_COLUMN); filterColumn.setCellEditor(new BooleanTableCellEditor()); setUpBooleanColumn(filterColumn, true); TableColumn resolvedColumn = getColumnByIndex(ConflictsTableModel.RESOLVED_COLUMN); setUpBooleanColumn(resolvedColumn, false); } private static void setUpBooleanColumn(@NotNull TableColumn column, boolean editable) { TableCellRenderer renderer = editable ? new BooleanTableCellRenderer() : new BooleanCellRenderer(); column.setCellRenderer(renderer); column.setMaxWidth(50); } @NotNull private TableColumn getColumnByIndex(int index) { return getColumn(getColumnName(index)); } } private static class ConflictsTableModel extends DefaultTableModel { static final Object[] COLUMN_NAMES = { "Filter", "Source", "Resolved" }; static final int FILTER_COLUMN = 0; static final int SOURCE_COLUMN = 1; static final int RESOLVED_COLUMN = 2; final List<ConflictTableRow> myRows = Lists.newArrayList(); final Function<List<ConflictTableRow>, Void> myFilterFunction; ConflictsTableModel(@NotNull List<Conflict> conflicts, Function<List<ConflictTableRow>, Void> filterFunction) { super(COLUMN_NAMES, conflicts.size()); myFilterFunction = filterFunction; for (Conflict conflict : conflicts) { myRows.add(new ConflictTableRow(conflict)); } Collections.sort(myRows); } @Override public Object getValueAt(int row, int column) { ConflictTableRow tableRow = myRows.get(row); switch (column) { case FILTER_COLUMN: return tableRow.myFilter; case SOURCE_COLUMN: return tableRow.myConflict.getSource().getName(); case RESOLVED_COLUMN: return tableRow.myConflict.isResolved(); } throw new IllegalArgumentException(String.format("Column index '%d' is not valid", column)); } @Override public boolean isCellEditable(int row, int column) { return column == FILTER_COLUMN; } @Override public void setValueAt(Object aValue, int row, int column) { if (column == FILTER_COLUMN && aValue instanceof Boolean) { ConflictTableRow conflictTableRow = myRows.get(row); conflictTableRow.myFilter = (Boolean)aValue; myFilterFunction.fun(myRows); } } } private static class ConflictTableRow implements Comparable<ConflictTableRow> { final Conflict myConflict; boolean myFilter; ConflictTableRow(Conflict conflict) { myConflict = conflict; } @Override public int compareTo(@NotNull ConflictTableRow other) { return Collator.getInstance().compare(myConflict.getSource().getName(), other.myConflict.getSource().getName()); } } private static abstract class CheckboxTreeView extends CheckboxTree implements TreeExpander { @NotNull final CheckedTreeNode myRoot; CheckboxTreeView(@NotNull CheckboxTreeCellRenderer cellRenderer, @NotNull CheckedTreeNode root) { super(cellRenderer, root); myRoot = root; } @Override public void expandAll() { TreeUtil.expandAll(this); } @Override public boolean canExpand() { return canCollapse(); } @Override public void collapseAll() { TreeUtil.collapseAll(this, 1); } @Override public boolean canCollapse() { return myRoot.getChildCount() > 0; } @Override public DefaultTreeModel getModel() { return (DefaultTreeModel)super.getModel(); } void reload() { ((DefaultTreeModel)super.getModel()).reload(); } } private static class FilterAwareCheckedTreeNode extends CheckedTreeNode { boolean myVisible = true; FilterAwareCheckedTreeNode(@Nullable Object userObject) { //noinspection ConstantConditions super(userObject); } @Override public TreeNode getChildAt(int index) { if (children == null) { // We use the same error message as super. throw new ArrayIndexOutOfBoundsException("node has no children"); } int realIndex = -1; int visibleIndex = -1; Enumeration e = children.elements(); while (e.hasMoreElements()) { Object child = e.nextElement(); if (child instanceof FilterAwareCheckedTreeNode) { FilterAwareCheckedTreeNode node = (FilterAwareCheckedTreeNode)child; if (node.myVisible) { visibleIndex++; } } else { visibleIndex++; } realIndex++; if (visibleIndex == index) { return (TreeNode) children.elementAt(realIndex); } } throw new ArrayIndexOutOfBoundsException("index unmatched"); } @Override public int getChildCount() { if (children == null) { return 0; } int count = 0; Enumeration e = children.elements(); while (e.hasMoreElements()) { Object child = e.nextElement(); if (child instanceof FilterAwareCheckedTreeNode) { FilterAwareCheckedTreeNode node = (FilterAwareCheckedTreeNode)child; if (node.myVisible) { count++; } } else { count++; } } return count; } } private static class ModuleTreeElement { @NotNull final Module myModule; @NotNull final List<Conflict> myConflicts = Lists.newArrayList(); ModuleTreeElement(@NotNull Module module) { myModule = module; } void addConflict(@NotNull Conflict conflict) { myConflicts.add(conflict); } } private static class DependencyTreeElement implements Comparable<DependencyTreeElement> { @NotNull final Module myModule; @NotNull final String myGradlePath; @Nullable final String myVariant; @Nullable final Conflict myConflict; DependencyTreeElement(@NotNull Module module, @NotNull String gradlePath, @Nullable String variant, @Nullable Conflict conflict) { myGradlePath = gradlePath; myVariant = variant; myModule = module; myConflict = conflict; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; DependencyTreeElement that = (DependencyTreeElement)o; return Objects.equal(myGradlePath, that.myGradlePath) && Objects.equal(myVariant, that.myVariant); } @Override public int hashCode() { return Objects.hashCode(myGradlePath, myVariant); } @Override public int compareTo(@NotNull DependencyTreeElement other) { return Collator.getInstance().compare(myModule.getName(), other.myModule.getName()); } } }