| /* |
| * 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.npw; |
| |
| import com.android.annotations.VisibleForTesting; |
| import com.android.tools.idea.gradle.project.ModuleImporter; |
| import com.android.tools.idea.gradle.project.ModuleToImport; |
| import com.android.tools.idea.wizard.dynamic.AndroidStudioWizardStep; |
| import com.android.tools.idea.wizard.template.TemplateWizardStep; |
| import com.google.common.base.Joiner; |
| import com.google.common.base.Objects; |
| import com.google.common.base.Strings; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Sets; |
| import com.intellij.ide.util.projectWizard.ModuleWizardStep; |
| import com.intellij.ide.util.projectWizard.WizardContext; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.fileChooser.FileChooserDescriptor; |
| import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.module.ModuleManager; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.ui.MessageType; |
| import com.intellij.openapi.ui.TextBrowseFolderListener; |
| import com.intellij.openapi.ui.TextFieldWithBrowseButton; |
| import com.intellij.openapi.vfs.VfsUtil; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.ui.DocumentAdapter; |
| import com.intellij.ui.components.JBLabel; |
| import com.intellij.ui.components.JBScrollPane; |
| import com.intellij.util.ui.AsyncProcessIcon; |
| import com.intellij.util.ui.UIUtil; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import javax.swing.border.EmptyBorder; |
| import javax.swing.event.DocumentEvent; |
| import java.awt.*; |
| import java.awt.event.ActionEvent; |
| import java.awt.event.ActionListener; |
| import java.beans.PropertyChangeEvent; |
| import java.beans.PropertyChangeListener; |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import static com.intellij.openapi.ui.MessageType.ERROR; |
| import static com.intellij.openapi.ui.MessageType.WARNING; |
| |
| /** |
| * Wizard page for selecting source location for module import. |
| */ |
| public class ImportSourceLocationStep extends ModuleWizardStep implements AndroidStudioWizardStep { |
| private static final int VALIDATION_STATUS_DISPLAY_DELAY = 50; //ms |
| private final Logger LOG = Logger.getInstance(ImportSourceLocationStep.class); |
| private final NewModuleWizardState myState; |
| private final Timer myDelayedValidationProgressDisplay; |
| @NotNull private final TemplateWizardStep.UpdateListener myUpdateListener; |
| @NotNull private final WizardContext myContext; |
| private JPanel myPanel; |
| private TextFieldWithBrowseButton mySourceLocation; |
| private JBLabel myErrorWarning; |
| private AsyncProcessIcon myValidationProgress; |
| private JBLabel myLocationLabel; |
| private JBScrollPane myModulesScroller; |
| private ModulesTable myModulesPanel; |
| private JLabel myRequiredModulesLabel; |
| private JLabel myModuleNameLabel; |
| private JTextField myModuleNameField; |
| private JLabel myPrimaryModuleState; |
| private AsyncValidator<?> validator; |
| private PathValidationResult myPageValidationResult; |
| private boolean myValidating = false; |
| private PageStatus myStatus; |
| private Icon mySidePanelIcon; |
| |
| public ImportSourceLocationStep(@NotNull WizardContext context, |
| @Nullable VirtualFile importSource, |
| @NotNull NewModuleWizardState state, |
| @Nullable Icon sidePanelIcon, |
| @Nullable TemplateWizardStep.UpdateListener listener) { |
| myErrorWarning.setBorder(BorderFactory.createEmptyBorder(16, 0, 0, 0)); |
| myContext = context; |
| mySidePanelIcon = sidePanelIcon; |
| myUpdateListener = listener == null ? new TemplateWizardStep.UpdateListener() { |
| @Override |
| public void update() { |
| // Do nothing |
| } |
| } : listener; |
| myState = state; |
| myPanel.setBorder(new EmptyBorder(UIUtil.PANEL_REGULAR_INSETS)); |
| myModulesScroller.setVisible(false); |
| myModulesPanel.bindPrimaryModuleEntryComponents(new PrimaryModuleImportSettings(), myRequiredModulesLabel); |
| PropertyChangeListener modulesListener = new PropertyChangeListener() { |
| @SuppressWarnings("unchecked") |
| @Override |
| public void propertyChange(PropertyChangeEvent evt) { |
| if (ModulesTable.PROPERTY_SELECTED_MODULES.equals(evt.getPropertyName())) { |
| updateStepStatus(myPageValidationResult); |
| } |
| } |
| }; |
| myModulesPanel.addPropertyChangeListener(ModulesTable.PROPERTY_SELECTED_MODULES, modulesListener); |
| |
| validator = new AsyncValidator<PathValidationResult>(ApplicationManager.getApplication()) { |
| @Override |
| protected void showValidationResult(PathValidationResult result) { |
| applyBackgroundOperationResult(result); |
| } |
| |
| @NotNull |
| @Override |
| protected PathValidationResult validate() { |
| return checkPath(mySourceLocation.getText()); |
| } |
| }; |
| myErrorWarning.setText(""); |
| myErrorWarning.setIcon(null); |
| setupSourceLocationControls(importSource); |
| myDelayedValidationProgressDisplay = new Timer(VALIDATION_STATUS_DISPLAY_DELAY, new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| if (myValidating) { |
| updateStatusDisplay(PageStatus.VALIDATING, null); |
| } |
| } |
| }); |
| } |
| |
| private static String multiLineJLabelText(String... messages) { |
| StringBuilder builder = new StringBuilder("<html><body><p>"); |
| Joiner.on("<br>").appendTo(builder, messages); |
| builder.append("</p></body></html>"); |
| return builder.toString(); |
| } |
| |
| @Override |
| public Icon getIcon() { |
| return mySidePanelIcon; |
| } |
| |
| private void setupSourceLocationControls(@Nullable VirtualFile importSource) { |
| if (importSource == null) { |
| FileChooserDescriptor descriptor = FileChooserDescriptorFactory.createSingleFileOrFolderDescriptor(); |
| descriptor.setTitle("Select Source Location"); |
| descriptor.setDescription("Select existing ADT or Gradle project to import as a new subproject"); |
| mySourceLocation.addBrowseFolderListener(new TextBrowseFolderListener(descriptor)); |
| mySourceLocation.getTextField().getDocument().addDocumentListener(new DocumentAdapter() { |
| @Override |
| protected void textChanged(DocumentEvent e) { |
| invalidate(); |
| } |
| }); |
| } |
| else { |
| mySourceLocation.setVisible(false); |
| myLocationLabel.setVisible(false); |
| mySourceLocation.setText(importSource.getPath()); |
| } |
| applyBackgroundOperationResult(checkPath(mySourceLocation.getText())); |
| myErrorWarning.setIcon(null); |
| myErrorWarning.setText(null); |
| } |
| |
| private void updateStatusDisplay(@NotNull PageStatus status, @Nullable Object details) { |
| myValidationProgress.setVisible(status.isSpinnerVisible()); |
| myErrorWarning.setText(status.getMessage(details)); |
| myErrorWarning.setIcon(status.getIcon()); |
| myUpdateListener.update(); |
| } |
| |
| private void invalidate() { |
| if (!myDelayedValidationProgressDisplay.isRunning()) { |
| myDelayedValidationProgressDisplay.start(); |
| } |
| myValidating = true; |
| validator.invalidate(); |
| } |
| |
| private void applyBackgroundOperationResult(@NotNull PathValidationResult result) { |
| assert EventQueue.isDispatchThread(); |
| Collection<ModuleToImport> modules = null; |
| Project project = myContext.getProject(); |
| try { |
| if (result.myStatus == PageStatus.OK) { |
| assert result.myVfile != null && result.myImporter != null; |
| modules = result.myImporter.findModules(result.myVfile); |
| Set<String> missingSourceModuleNames = Sets.newTreeSet(); |
| for (ModuleToImport module : modules) { |
| if (module.location == null || !module.location.exists()) { |
| missingSourceModuleNames.add(module.name); |
| } |
| } |
| if (!missingSourceModuleNames.isEmpty()) { |
| result = new PathValidationResult(PageStatus.MISSING_SUBPROJECTS, |
| result.myVfile, result.myImporter, missingSourceModuleNames); |
| } |
| } |
| } |
| catch (IOException e) { |
| LOG.error(e); |
| result = PageStatus.INTERNAL_ERROR.result(); |
| } |
| myValidating = false; |
| myModulesPanel.setModules(project, result.myVfile, modules); |
| myModulesScroller.setVisible(myModulesPanel.getComponentCount() > 0); |
| ModuleImporter.setImporter(myContext, result.myImporter); |
| updateStepStatus(result); |
| } |
| |
| private void updateStepStatus(PathValidationResult result) { |
| Object validationDetails = result.myDetails; |
| PageStatus status = result.myStatus; |
| Map<String, VirtualFile> selectedModules = Collections.emptyMap(); |
| if (!MessageType.ERROR.equals(status.severity)) { |
| final Collection<ModuleToImport> modules = myModulesPanel.getSelectedModules(); |
| if (modules.isEmpty()) { |
| status = PageStatus.NO_MODULES_SELECTED; |
| validationDetails = null; |
| } |
| else { |
| selectedModules = Maps.newHashMap(); |
| for (ModuleToImport module : modules) { |
| selectedModules.put(myModulesPanel.getModuleName(module), module.location); |
| } |
| } |
| } |
| myPageValidationResult = result; |
| myState.setModulesToImport(selectedModules); |
| updateStatusDisplay(status, validationDetails); |
| myStatus = status; |
| myUpdateListener.update(); |
| } |
| |
| private void createUIComponents() { |
| myValidationProgress = new AsyncProcessIcon("validation"); |
| myValidationProgress.setVisible(false); |
| } |
| |
| @Override |
| public boolean validate() { |
| return myStatus.severity != ERROR && !myValidating && myModulesPanel.canImport(); |
| } |
| |
| @Override |
| public boolean isValid() { |
| return validate(); |
| } |
| |
| @NotNull |
| @VisibleForTesting |
| protected PathValidationResult checkPath(@NotNull String path) { |
| path = path.trim(); |
| if (Strings.isNullOrEmpty(path)) { |
| return PageStatus.EMPTY_PATH.result(); |
| } |
| VirtualFile vfile = VfsUtil.findFileByIoFile(new File(path), false); |
| if (vfile == null || !vfile.exists()) { |
| return PageStatus.DOES_NOT_EXIST.result(); |
| } |
| else if (isProjectOrModule(vfile)) { |
| return PageStatus.IS_PROJECT_OR_MODULE.result(); |
| } |
| ModuleImporter kind = ModuleImporter.importerForLocation(myContext, vfile); |
| if (!kind.isValid()) { |
| return PageStatus.NOT_ADT_OR_GRADLE.result(); |
| } |
| return new PathValidationResult(PageStatus.OK, vfile, kind, null); |
| } |
| |
| private boolean isProjectOrModule(@NotNull VirtualFile dir) { |
| Project project = myContext.getProject(); |
| if (project != null) { |
| if (dir.equals(project.getBaseDir())) { |
| return true; |
| } else { |
| for (Module module : ModuleManager.getInstance(project).getModules()) { |
| //noinspection SSBasedInspection |
| VirtualFile moduleFile = module.getModuleFile(); |
| if (moduleFile != null && dir.equals(moduleFile.getParent())) { |
| return true; |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public JComponent getComponent() { |
| return myPanel; |
| } |
| |
| @Override |
| public void updateDataModel() { |
| // Do nothing? |
| } |
| |
| @Override |
| public JComponent getPreferredFocusedComponent() { |
| return mySourceLocation.getTextField(); |
| } |
| |
| @VisibleForTesting |
| enum PageStatus { |
| OK(null, null), EMPTY_PATH("Path is empty", ERROR), DOES_NOT_EXIST("Path does not exist", ERROR), |
| IS_PROJECT_OR_MODULE("This location is already imported", ERROR), |
| MISSING_SUBPROJECTS("Some projects were not found", WARNING), |
| NO_MODULES_SELECTED("Select modules to import", ERROR), |
| NOT_ADT_OR_GRADLE("Specify location of the Gradle or Android Eclipse project", ERROR), |
| INTERNAL_ERROR("Internal error, please check the IDE log", ERROR), VALIDATING("Validating", null); |
| |
| @Nullable public final MessageType severity; |
| @Nullable private final String message; |
| |
| PageStatus(@Nullable String message, @Nullable MessageType severity) { |
| this.message = message; |
| this.severity = severity; |
| } |
| |
| public PathValidationResult result() { |
| return new PathValidationResult(this, null, null, null); |
| } |
| |
| @Nullable |
| public Icon getIcon() { |
| return severity == null ? null : severity.getDefaultIcon(); |
| } |
| |
| public boolean isSpinnerVisible() { |
| return this == VALIDATING; |
| } |
| |
| @SuppressWarnings("unchecked") |
| public String getMessage(@Nullable Object details) { |
| if (this == MISSING_SUBPROJECTS && details instanceof Collection) { |
| final String message = ImportUIUtil |
| .formatElementListString((Collection<String>)details, "Unable to find sources for subproject %1$s.", |
| "Unable to find sources for subprojects %1$s and %2$s.", |
| "Unable to find sources for %1$s and %2$d more subprojects."); |
| return multiLineJLabelText(message, "This may result in missing dependencies."); |
| } |
| else { |
| return Strings.nullToEmpty(message); |
| } |
| } |
| |
| } |
| |
| @VisibleForTesting |
| static final class PathValidationResult { |
| @NotNull public final PageStatus myStatus; |
| @Nullable public final VirtualFile myVfile; |
| @Nullable public final ModuleImporter myImporter; |
| @Nullable public final Object myDetails; |
| |
| private PathValidationResult(@NotNull PageStatus status, |
| @Nullable VirtualFile vfile, |
| @Nullable ModuleImporter importer, |
| @Nullable Object details) { |
| myStatus = status; |
| myVfile = vfile; |
| myImporter = importer; |
| myDetails = details; |
| } |
| } |
| |
| private final class PrimaryModuleImportSettings implements ModuleImportSettings { |
| @Override |
| public boolean isModuleSelected() { |
| return true; |
| } |
| |
| @Override |
| public void setModuleSelected(boolean selected) { |
| // Do nothing - primary module |
| } |
| |
| @Override |
| public String getModuleName() { |
| return myModuleNameField.getText(); |
| } |
| |
| @Override |
| public void setModuleName(String moduleName) { |
| if (!Objects.equal(moduleName, myModuleNameField.getText())) { |
| myModuleNameField.setText(moduleName); |
| } |
| } |
| |
| @Override |
| public void setModuleSourcePath(String relativePath) { |
| // Nothing |
| } |
| |
| @Override |
| public void setCanToggleModuleSelection(boolean b) { |
| // Nothing |
| } |
| |
| @Override |
| public void setCanRenameModule(boolean canRenameModule) { |
| myModuleNameField.setEnabled(canRenameModule); |
| } |
| |
| @Override |
| public void setValidationStatus(@Nullable MessageType statusSeverity, @Nullable String statusDescription) { |
| myPrimaryModuleState.setIcon(statusSeverity == null ? null : statusSeverity.getDefaultIcon()); |
| myPrimaryModuleState.setText(Strings.nullToEmpty(statusDescription)); |
| } |
| |
| @Override |
| public void setVisible(boolean visible) { |
| myPrimaryModuleState.setVisible(visible); |
| myModuleNameField.setVisible(visible); |
| myModuleNameLabel.setVisible(visible); |
| } |
| |
| @Override |
| public void addActionListener(final ActionListener actionListener) { |
| myModuleNameField.getDocument().addDocumentListener(new DocumentAdapter() { |
| @Override |
| protected void textChanged(DocumentEvent e) { |
| actionListener.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "changed")); |
| } |
| }); |
| } |
| } |
| } |