/* * 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.wizard; import com.android.annotations.VisibleForTesting; import com.android.tools.idea.gradle.project.ModuleImporter; import com.android.tools.idea.gradle.project.ModuleToImport; 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")); } }); } } }