| /* |
| * Copyright (C) 2013 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.structure.gradle; |
| |
| import com.android.ide.common.repository.GradleCoordinate; |
| import com.android.ide.common.repository.SdkMavenRepository; |
| import com.android.sdklib.repository.descriptors.IPkgDesc; |
| import com.android.tools.idea.gradle.parser.*; |
| import com.android.tools.idea.gradle.util.GradleUtil; |
| import com.android.tools.idea.sdk.wizard.SdkQuickfixWizard; |
| import com.android.tools.idea.structure.EditorPanel; |
| import com.android.tools.idea.templates.RepositoryUrlManager; |
| import com.google.common.base.Predicate; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Sets; |
| import com.intellij.icons.AllIcons; |
| import com.intellij.ide.util.ChooseElementsDialog; |
| import com.intellij.openapi.actionSystem.ActionManager; |
| import com.intellij.openapi.actionSystem.ActionPlaces; |
| import com.intellij.openapi.actionSystem.DefaultActionGroup; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.fileChooser.FileChooser; |
| import com.intellij.openapi.fileChooser.FileChooserDescriptor; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.project.ProjectBundle; |
| import com.intellij.openapi.roots.ui.CellAppearanceEx; |
| import com.intellij.openapi.roots.ui.util.SimpleTextCellAppearance; |
| import com.intellij.openapi.ui.ComboBox; |
| import com.intellij.openapi.ui.ComboBoxTableRenderer; |
| import com.intellij.openapi.ui.DialogWrapper; |
| import com.intellij.openapi.ui.Messages; |
| import com.intellij.openapi.ui.popup.JBPopup; |
| import com.intellij.openapi.ui.popup.JBPopupFactory; |
| import com.intellij.openapi.ui.popup.PopupStep; |
| import com.intellij.openapi.ui.popup.util.BaseListPopupStep; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vfs.VfsUtilCore; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.ui.*; |
| import com.intellij.ui.table.JBTable; |
| import com.intellij.util.ActionRunner; |
| import com.intellij.util.PlatformIcons; |
| import icons.AndroidIcons; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import javax.swing.border.Border; |
| import javax.swing.event.ListSelectionEvent; |
| import javax.swing.event.ListSelectionListener; |
| import javax.swing.table.TableColumn; |
| import javax.swing.table.TableRowSorter; |
| import java.awt.*; |
| import java.util.EnumSet; |
| import java.util.List; |
| |
| import static com.android.tools.idea.templates.RepositoryUrlManager.REVISION_ANY; |
| |
| /** |
| * A GUI object that displays and modifies dependencies for an Android-Gradle module. |
| */ |
| public class ModuleDependenciesPanel extends EditorPanel { |
| private static final Logger LOG = Logger.getInstance(ModuleDependenciesPanel.class); |
| private static final int SCOPE_COLUMN_WIDTH = 120; |
| private final JBTable myEntryTable; |
| private final ModuleDependenciesTableModel myModel; |
| private final String myModulePath; |
| private final Project myProject; |
| private final GradleBuildFile myGradleBuildFile; |
| private final GradleSettingsFile myGradleSettingsFile; |
| private AnActionButton myRemoveButton; |
| |
| public ModuleDependenciesPanel(@NotNull Project project, @NotNull String modulePath) { |
| super(new BorderLayout()); |
| |
| myModulePath = modulePath; |
| myProject = project; |
| myModel = new ModuleDependenciesTableModel(); |
| myGradleSettingsFile = GradleSettingsFile.get(myProject); |
| |
| Module module = GradleUtil.findModuleByGradlePath(myProject, modulePath); |
| myGradleBuildFile = module != null ? GradleBuildFile.get(module) : null; |
| if (myGradleBuildFile != null) { |
| List<BuildFileStatement> dependencies = myGradleBuildFile.getDependencies(); |
| for (BuildFileStatement dependency : dependencies) { |
| myModel.addItem(new ModuleDependenciesTableItem(dependency)); |
| } |
| } else { |
| LOG.warn("Unable to find Gradle build file for module " + myModulePath); |
| } |
| myModel.resetModified(); |
| |
| myEntryTable = new JBTable(myModel); |
| TableRowSorter<ModuleDependenciesTableModel> sorter = new TableRowSorter<ModuleDependenciesTableModel>(myModel); |
| sorter.setRowFilter(myModel.getFilter()); |
| myEntryTable.setRowSorter(sorter); |
| myEntryTable.setShowGrid(false); |
| myEntryTable.setDragEnabled(false); |
| myEntryTable.setIntercellSpacing(new Dimension(0, 0)); |
| |
| myEntryTable.setDefaultRenderer(ModuleDependenciesTableItem.class, new TableItemRenderer()); |
| |
| if (myGradleBuildFile == null) { |
| return; |
| } |
| final boolean isAndroid = myGradleBuildFile.hasAndroidPlugin(); |
| List<Dependency.Scope> scopes = Lists.newArrayList( |
| Sets.filter(EnumSet.allOf(Dependency.Scope.class), new Predicate<Dependency.Scope>() { |
| @Override |
| public boolean apply(Dependency.Scope input) { |
| return isAndroid ? input.isAndroidScope() : input.isJavaScope(); |
| } |
| })); |
| ComboBoxModel boxModel = new CollectionComboBoxModel(scopes, null); |
| JComboBox scopeEditor = new ComboBox(boxModel); |
| myEntryTable.setDefaultEditor(Dependency.Scope.class, new DefaultCellEditor(scopeEditor)); |
| myEntryTable.setDefaultRenderer(Dependency.Scope.class, new ComboBoxTableRenderer<Dependency.Scope>(Dependency.Scope.values()) { |
| @Override |
| protected String getTextFor(@NotNull final Dependency.Scope value) { |
| return value.getDisplayName(); |
| } |
| }); |
| |
| myEntryTable.getSelectionModel().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); |
| |
| new SpeedSearchBase<JBTable>(myEntryTable) { |
| @Override |
| public int getSelectedIndex() { |
| return myEntryTable.getSelectedRow(); |
| } |
| |
| @Override |
| protected int convertIndexToModel(int viewIndex) { |
| return myEntryTable.convertRowIndexToModel(viewIndex); |
| } |
| |
| @Override |
| @NotNull |
| public Object[] getAllElements() { |
| return myModel.getItems().toArray(); |
| } |
| |
| @Override |
| @NotNull |
| public String getElementText(Object element) { |
| return getCellAppearance((ModuleDependenciesTableItem)element).getText(); |
| } |
| |
| @Override |
| public void selectElement(@NotNull Object element, @NotNull String selectedText) { |
| final int count = myModel.getRowCount(); |
| for (int row = 0; row < count; row++) { |
| if (element.equals(myModel.getItemAt(row))) { |
| final int viewRow = myEntryTable.convertRowIndexToView(row); |
| myEntryTable.getSelectionModel().setSelectionInterval(viewRow, viewRow); |
| TableUtil.scrollSelectionToVisible(myEntryTable); |
| break; |
| } |
| } |
| } |
| }; |
| |
| TableColumn column = myEntryTable.getTableHeader().getColumnModel().getColumn(ModuleDependenciesTableModel.SCOPE_COLUMN); |
| column.setResizable(false); |
| column.setMaxWidth(SCOPE_COLUMN_WIDTH); |
| column.setMinWidth(SCOPE_COLUMN_WIDTH); |
| |
| add(createTableWithButtons(), BorderLayout.CENTER); |
| |
| if (myEntryTable.getRowCount() > 0) { |
| myEntryTable.getSelectionModel().setSelectionInterval(0, 0); |
| } |
| |
| DefaultActionGroup actionGroup = new DefaultActionGroup(); |
| actionGroup.add(myRemoveButton); |
| PopupHandler.installPopupHandler(myEntryTable, actionGroup, ActionPlaces.UNKNOWN, ActionManager.getInstance()); |
| } |
| |
| @NotNull |
| private JComponent createTableWithButtons() { |
| myEntryTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() { |
| @Override |
| public void valueChanged(ListSelectionEvent e) { |
| if (e.getValueIsAdjusting()) { |
| return; |
| } |
| updateButtons(); |
| } |
| }); |
| |
| final ToolbarDecorator decorator = ToolbarDecorator.createDecorator(myEntryTable); |
| decorator.setAddAction(new AnActionButtonRunnable() { |
| @Override |
| public void run(AnActionButton button) { |
| ImmutableList<PopupAction> popupActions = ImmutableList.of( |
| new PopupAction(AndroidIcons.MavenLogo, 1, "Library dependency") { |
| @Override |
| public void run() { |
| addExternalDependency(); |
| } |
| }, new PopupAction(PlatformIcons.LIBRARY_ICON, 2, "File dependency") { |
| @Override |
| public void run() { |
| addFileDependency(); |
| } |
| }, new PopupAction(AllIcons.Nodes.Module, 3, "Module dependency") { |
| @Override |
| public void run() { |
| addModuleDependency(); |
| } |
| } |
| ); |
| final JBPopup popup = JBPopupFactory.getInstance().createListPopup(new BaseListPopupStep<PopupAction>(null, popupActions) { |
| @Override |
| public Icon getIconFor(PopupAction value) { |
| return value.myIcon; |
| } |
| |
| @Override |
| public boolean hasSubstep(PopupAction value) { |
| return false; |
| } |
| |
| @Override |
| public boolean isMnemonicsNavigationEnabled() { |
| return true; |
| } |
| |
| @Override |
| public PopupStep onChosen(final PopupAction value, final boolean finalChoice) { |
| return doFinalStep(new Runnable() { |
| @Override |
| public void run() { |
| value.run(); |
| } |
| }); |
| } |
| |
| @Override |
| @NotNull |
| public String getTextFor(PopupAction value) { |
| return "&" + value.myIndex + " " + value.myTitle; |
| } |
| }); |
| popup.show(button.getPreferredPopupPoint()); |
| } |
| }); |
| decorator.setRemoveAction(new AnActionButtonRunnable() { |
| @Override |
| public void run(AnActionButton button) { |
| removeSelectedItems(); |
| } |
| }); |
| decorator.setMoveUpAction(new AnActionButtonRunnable() { |
| @Override |
| public void run(AnActionButton button) { |
| moveSelectedRows(-1); |
| } |
| }); |
| decorator.setMoveDownAction(new AnActionButtonRunnable() { |
| @Override |
| public void run(AnActionButton button) { |
| moveSelectedRows(+1); |
| } |
| }); |
| |
| final JPanel panel = decorator.createPanel(); |
| myRemoveButton = ToolbarDecorator.findRemoveButton(panel); |
| return panel; |
| } |
| |
| private void addExternalDependency() { |
| Module module = GradleUtil.findModuleByGradlePath(myProject, myModulePath); |
| MavenDependencyLookupDialog dialog = new MavenDependencyLookupDialog(myProject, module); |
| dialog.setTitle("Choose Library Dependency"); |
| dialog.show(); |
| if (dialog.getExitCode() == DialogWrapper.OK_EXIT_CODE) { |
| String coordinateText = dialog.getSearchText(); |
| coordinateText = installRepositoryIfNeeded(coordinateText); |
| if (coordinateText != null) { |
| myModel.addItem(new ModuleDependenciesTableItem( |
| new Dependency(Dependency.Scope.COMPILE, Dependency.Type.EXTERNAL, coordinateText))); |
| } |
| } |
| myModel.fireTableDataChanged(); |
| } |
| |
| private String installRepositoryIfNeeded(String coordinateText) { |
| GradleCoordinate gradleCoordinate = GradleCoordinate.parseCoordinateString(coordinateText); |
| assert gradleCoordinate != null; // Only allowed to click ok when the string is valid. |
| if (!REVISION_ANY.equals(gradleCoordinate.getFullRevision()) || |
| !RepositoryUrlManager.EXTRAS_REPOSITORY.containsKey(gradleCoordinate.getArtifactId())) { |
| // No installation needed, or it's not a local repository. |
| return coordinateText; |
| } |
| String message = "Library " + gradleCoordinate.getArtifactId() + " is not installed. Install repository?"; |
| if (Messages.showYesNoDialog(myProject, message, "Install Repository", Messages.getQuestionIcon()) != Messages.YES) { |
| // User cancelled installation. |
| return null; |
| } |
| List<IPkgDesc> requested = Lists.newArrayList(); |
| SdkMavenRepository repository; |
| if (coordinateText.startsWith("com.android.support")) { |
| repository = SdkMavenRepository.ANDROID; |
| } |
| else if (coordinateText.startsWith("com.google.android")) { |
| repository = SdkMavenRepository.GOOGLE; |
| } |
| else { |
| // Not a local repository. |
| assert false; // EXTRAS_REPOSITORY.containsKey() should have returned false. |
| return coordinateText + ':' + REVISION_ANY; |
| } |
| requested.add(repository.getPackageDescription()); |
| SdkQuickfixWizard wizard = new SdkQuickfixWizard(myProject, null, requested); |
| wizard.init(); |
| wizard.setTitle("Install Missing Components"); |
| if (wizard.showAndGet()) { |
| return RepositoryUrlManager.get().getLibraryCoordinate(gradleCoordinate.getArtifactId()); |
| } |
| else { |
| // Installation wizard didn't complete - skip adding the dependency. |
| return null; |
| } |
| } |
| |
| private void addFileDependency() { |
| FileChooserDescriptor descriptor = new FileChooserDescriptor(false, false, true, true, false, false); |
| VirtualFile buildFile = myGradleBuildFile.getFile(); |
| VirtualFile parent = buildFile.getParent(); |
| descriptor.setRoots(parent); |
| VirtualFile virtualFile = FileChooser.chooseFile(descriptor, myProject, null); |
| if (virtualFile != null) { |
| String path = VfsUtilCore.getRelativePath(virtualFile, parent, '/'); |
| if (path == null) { |
| path = virtualFile.getPath(); |
| } |
| myModel.addItem(new ModuleDependenciesTableItem(new Dependency(Dependency.Scope.COMPILE, Dependency.Type.FILES, path))); |
| } |
| myModel.fireTableDataChanged(); |
| } |
| |
| private void addModuleDependency() { |
| List<String> modules = Lists.newArrayList(); |
| for (String s : myGradleSettingsFile.getModules()) { |
| modules.add(s); |
| } |
| List<BuildFileStatement> dependencies = myGradleBuildFile.getDependencies(); |
| for (BuildFileStatement dependency : dependencies) { |
| if (dependency instanceof Dependency) { |
| Object data = ((Dependency)dependency).data; |
| if (data instanceof String) { |
| modules.remove(data); |
| } |
| } |
| } |
| modules.remove(myModulePath); |
| final Component parent = this; |
| final String title = ProjectBundle.message("classpath.chooser.title.add.module.dependency"); |
| final String description = ProjectBundle.message("classpath.chooser.description.add.module.dependency"); |
| ChooseElementsDialog<String> dialog = new ChooseElementsDialog<String>(parent, modules, title, description, true) { |
| @Override |
| protected Icon getItemIcon(final String item) { |
| return AllIcons.Nodes.Module; |
| } |
| |
| @Override |
| protected String getItemText(final String item) { |
| return item; |
| } |
| }; |
| dialog.show(); |
| for (String module : dialog.getChosenElements()) { |
| myModel.addItem(new ModuleDependenciesTableItem( |
| new Dependency(Dependency.Scope.COMPILE, Dependency.Type.MODULE, module))); |
| } |
| myModel.fireTableDataChanged(); |
| } |
| |
| @Override |
| public void addNotify() { |
| super.addNotify(); |
| updateButtons(); |
| } |
| |
| private void updateButtons() { |
| final int[] selectedRows = myEntryTable.getSelectedRows(); |
| boolean removeButtonEnabled = true; |
| int minRow = myEntryTable.getRowCount() + 1; |
| int maxRow = -1; |
| for (final int selectedRow : selectedRows) { |
| minRow = Math.min(minRow, selectedRow); |
| maxRow = Math.max(maxRow, selectedRow); |
| final ModuleDependenciesTableItem item = myModel.getItemAt(selectedRow); |
| if (!item.isRemovable()) { |
| removeButtonEnabled = false; |
| } |
| } |
| if (myRemoveButton != null) { |
| myRemoveButton.setEnabled(removeButtonEnabled && selectedRows.length > 0); |
| } |
| } |
| |
| private void removeSelectedItems() { |
| if (myEntryTable.isEditing()){ |
| myEntryTable.getCellEditor().stopCellEditing(); |
| } |
| for (int modelRow = myModel.getRowCount() - 1; modelRow >= 0; modelRow--) { |
| if (myEntryTable.isCellSelected(myEntryTable.convertRowIndexToView(modelRow), 0)) { |
| myModel.removeDataRow(modelRow); |
| } |
| } |
| myModel.fireTableDataChanged(); |
| myModel.setModified(); |
| } |
| |
| private void moveSelectedRows(int increment) { |
| if (increment == 0) { |
| return; |
| } |
| if (myEntryTable.isEditing()){ |
| myEntryTable.getCellEditor().stopCellEditing(); |
| } |
| final ListSelectionModel selectionModel = myEntryTable.getSelectionModel(); |
| for (int modelRow = increment < 0 ? 0 : myModel.getRowCount() - 1; |
| increment < 0 ? modelRow < myModel.getRowCount() : modelRow >= 0; |
| modelRow += increment < 0 ? +1 : -1) { |
| int visibleRow = myEntryTable.convertRowIndexToView(modelRow); |
| if (selectionModel.isSelectedIndex(visibleRow)) { |
| int newVisibleRow = myEntryTable.convertRowIndexToView(moveRow(modelRow, increment)); |
| selectionModel.removeSelectionInterval(visibleRow, visibleRow); |
| myModel.fireTableDataChanged(); |
| selectionModel.addSelectionInterval(newVisibleRow, newVisibleRow); |
| } |
| } |
| Rectangle cellRect = myEntryTable.getCellRect(selectionModel.getMinSelectionIndex(), 0, true); |
| myEntryTable.scrollRectToVisible(cellRect); |
| myEntryTable.repaint(); |
| } |
| |
| private int moveRow(final int row, final int increment) { |
| int newIndex = Math.abs(row + increment) % myModel.getRowCount(); |
| final ModuleDependenciesTableItem item = myModel.removeDataRow(row); |
| myModel.addItemAt(item, newIndex); |
| return newIndex; |
| } |
| |
| @NotNull |
| private static CellAppearanceEx getCellAppearance(@NotNull final ModuleDependenciesTableItem item) { |
| BuildFileStatement entry = item.getEntry(); |
| String data = ""; |
| Icon icon = null; |
| if (entry instanceof Dependency) { |
| Dependency dependency = (Dependency)entry; |
| data = dependency.getValueAsString(); |
| //noinspection EnumSwitchStatementWhichMissesCases |
| switch (dependency.type) { |
| case EXTERNAL: |
| icon = AndroidIcons.MavenLogo; |
| break; |
| case FILES: |
| icon = PlatformIcons.LIBRARY_ICON; |
| break; |
| case MODULE: |
| icon = AllIcons.Nodes.Module; |
| break; |
| } |
| } else if (entry != null) { |
| data = entry.toString(); |
| } |
| return SimpleTextCellAppearance.regular(data, icon); |
| } |
| |
| @Override |
| public void apply() { |
| List<ModuleDependenciesTableItem> items = myModel.getItems(); |
| final List<BuildFileStatement> dependencies = Lists.newArrayListWithExpectedSize(items.size()); |
| for (ModuleDependenciesTableItem item : items) { |
| dependencies.add(item.getEntry()); |
| } |
| try { |
| ActionRunner.runInsideWriteAction(new ActionRunner.InterruptibleRunnable() { |
| @Override |
| public void run() throws Exception { |
| myGradleBuildFile.setValue(BuildFileKey.DEPENDENCIES, dependencies); |
| } |
| }); |
| } |
| catch (Exception e) { |
| LOG.error("Unable to commit dependency changes", e); |
| } |
| myModel.resetModified(); |
| } |
| |
| @Override |
| public boolean isModified() { |
| return myModel.isModified(); |
| } |
| |
| public void select(@NotNull GradleCoordinate dependency) { |
| int row = myModel.getRow(dependency); |
| if (row >= 0) { |
| myEntryTable.getSelectionModel().setSelectionInterval(row, row); |
| } |
| } |
| |
| private static class TableItemRenderer extends ColoredTableCellRenderer { |
| private final Border NO_FOCUS_BORDER = BorderFactory.createEmptyBorder(1, 1, 1, 1); |
| |
| @Override |
| protected void customizeCellRenderer(JTable table, @Nullable Object value, boolean selected, boolean hasFocus, |
| int row, int column) { |
| setPaintFocusBorder(false); |
| setFocusBorderAroundIcon(true); |
| setBorder(NO_FOCUS_BORDER); |
| if (value != null && value instanceof ModuleDependenciesTableItem) { |
| final ModuleDependenciesTableItem tableItem = (ModuleDependenciesTableItem)value; |
| getCellAppearance(tableItem).customize(this); |
| setToolTipText(tableItem.getTooltipText()); |
| } |
| } |
| } |
| |
| private abstract static class PopupAction implements Runnable { |
| private Icon myIcon; |
| private Object myIndex; |
| private Object myTitle; |
| |
| protected PopupAction(Icon icon, Object index, Object title) { |
| myIcon = icon; |
| myIndex = index; |
| myTitle = title; |
| } |
| } |
| } |