blob: f22ef2525e9391a6b8e6d83a0945e6e3ab71fa5f [file] [log] [blame]
/*
* 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 org.jetbrains.android.actions;
import com.android.builder.model.SourceProvider;
import com.android.tools.idea.AndroidPsiUtils;
import com.android.tools.idea.navigator.AndroidProjectViewPane;
import com.android.tools.idea.ui.ComboBoxItemWithApiTag;
import com.intellij.ide.IdeView;
import com.intellij.ide.actions.ElementCreator;
import com.intellij.ide.projectView.ProjectView;
import com.intellij.ide.projectView.impl.AbstractProjectViewPane;
import com.intellij.ide.util.DirectoryUtil;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.InputValidator;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiManager;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.facet.IdeaSourceProvider;
import org.jetbrains.android.resourceManagers.LocalResourceManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.io.File;
import java.util.List;
/**
* Based on {@link com.intellij.ide.actions.CreateElementActionBase} but
* customized to let us <b>not</b> show the target directory chooser. This
* is because we'll be able to pick the target directory ourselves based
* on inputs in the dialog (e.g. the resource type, the source set chosen, etc).
*
* Also, instead of passing a specific directory <b>to</b> the dialog, we pass just
* the data context, such that the dialog can figure out the actual directory on
* its own (based on dialog choices like which source provider to use.)
*/
public abstract class CreateResourceActionBase extends AnAction {
protected CreateResourceActionBase() {
}
protected CreateResourceActionBase(String text, String description, Icon icon) {
super(text, description, icon);
}
@Nullable
public static SourceProvider getSourceProvider(@Nullable JComboBox combo) {
if (combo != null && combo.isVisible()) {
Object selectedItem = combo.getSelectedItem();
if (selectedItem instanceof ComboBoxItemWithApiTag) {
return (SourceProvider)((ComboBoxItemWithApiTag)selectedItem).id;
}
}
return null;
}
@Nullable
public static PsiDirectory getResourceDirectory(@Nullable SourceProvider sourceProvider, @NotNull Module module, boolean create) {
ApplicationManager.getApplication().assertReadAccessAllowed();
if (sourceProvider != null) {
final PsiManager manager = PsiManager.getInstance(module.getProject());
for (final File file : sourceProvider.getResDirectories()) {
if (create && !file.exists()) {
PsiDirectory dir = ApplicationManager.getApplication().runWriteAction(new Computable<PsiDirectory>() {
@Override
public PsiDirectory compute() {
return DirectoryUtil.mkdirs(manager, FileUtil.toSystemIndependentName(file.getPath()));
}
});
if (dir != null) {
return dir;
}
}
else {
VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByIoFile(file);
if (virtualFile != null) {
PsiDirectory dir = manager.findDirectory(virtualFile);
if (dir != null) {
return dir;
}
}
}
}
}
// Otherwise use the main source set:
AndroidFacet facet = AndroidFacet.getInstance(module);
if (facet != null) {
VirtualFile res = facet.getPrimaryResourceDir();
if (res != null) {
return PsiManager.getInstance(module.getProject()).findDirectory(res);
}
}
return null;
}
public static void updateSourceSetCombo(@NotNull JComponent label, @NotNull JComboBox combo, @Nullable AndroidFacet facet,
@SuppressWarnings("UnusedParameters") @Nullable PsiDirectory resDirectory) {
// Ideally, if we're in the Project View and you select a file under main/res, we already know that
// the resource folder is "res" and we pass it in here -- and we shouldn't ask the user for a source set.
// However, in the Android Project view there is only a single "res" node, shared by multiple possible source
// sets, so we *always* want to ask for the target source set there. We don't have a way to know which view
// we're in here, so we default to always including the source set combo (if it's a Gradle project that is.)
if (/*resDirectory == null && */ facet != null && facet.requiresAndroidModel() && facet.getAndroidModel() != null) {
List<SourceProvider> providers = IdeaSourceProvider.getAllSourceProviders(facet);
DefaultComboBoxModel model = new DefaultComboBoxModel();
for (SourceProvider sourceProvider : providers) {
//noinspection unchecked
model.addElement(new ComboBoxItemWithApiTag(sourceProvider, sourceProvider.getName(), 0, 0));
}
combo.setModel(model);
label.setVisible(true);
combo.setVisible(true);
} else {
label.setVisible(false);
combo.setVisible(false);
}
}
/**
* @return created elements. Never null.
*/
@NotNull
protected abstract PsiElement[] invokeDialog(@NotNull Project project, @NotNull DataContext dataContext);
/**
* @return created elements. Never null.
*/
@NotNull
protected abstract PsiElement[] create(String newName, PsiDirectory directory) throws Exception;
protected abstract String getErrorTitle();
@SuppressWarnings("UnusedDeclaration") // For symmetry with CreateElementActionBase
protected abstract String getCommandName();
protected abstract String getActionName(PsiDirectory directory, String newName);
@Override
public final void actionPerformed(final AnActionEvent e) {
final DataContext dataContext = e.getDataContext();
final IdeView view = LangDataKeys.IDE_VIEW.getData(dataContext);
if (view == null) {
return;
}
final Project project = CommonDataKeys.PROJECT.getData(dataContext);
if (project == null) {
return;
}
// This is where we differ from CreateElementActionBase:
// Rather than asking for a directory here, we just allow
// *any* directory
final PsiElement[] createdElements = invokeDialog(project, dataContext);
for (PsiElement createdElement : createdElements) {
view.selectElement(createdElement);
}
}
@Override
public void update(final AnActionEvent e) {
final DataContext dataContext = e.getDataContext();
final Presentation presentation = e.getPresentation();
final boolean enabled = isAvailable(dataContext);
presentation.setVisible(enabled);
presentation.setEnabled(enabled);
}
@Override
public boolean isDumbAware() {
return false;
}
protected boolean isAvailable(final DataContext dataContext) {
final Project project = CommonDataKeys.PROJECT.getData(dataContext);
if (project == null) {
return false;
}
if (DumbService.getInstance(project).isDumb() && !isDumbAware()) {
return false;
}
final IdeView view = LangDataKeys.IDE_VIEW.getData(dataContext);
if (view == null || view.getDirectories().length == 0) {
return false;
}
return true;
}
@Nullable
private static VirtualFile getResFolderParent(@NotNull LocalResourceManager manager, @NotNull VirtualFile file) {
VirtualFile current = file;
while (current != null) {
if (current.isDirectory() && manager.isResourceDir(current)) {
return current;
}
current = current.getParent();
}
return null;
}
@Nullable
public static PsiDirectory findResourceDirectory(@NotNull DataContext dataContext) {
// Look at the set of selected files and see if one *specific* resource directory is implied (selected, or a parent
// of all selected nodes); if so, use it; otherwise return null.
//
// In the Android Project View we don't want to do this, since there is only ever a single "res" node,
// even when you have other overlays.
// If you're in the Android View, we want to ask you not just the filename but also let you
// create other resource folder configurations
Project project = CommonDataKeys.PROJECT.getData(dataContext);
if (project != null) {
AbstractProjectViewPane pane = ProjectView.getInstance(project).getCurrentProjectViewPane();
if (pane instanceof AndroidProjectViewPane) {
return null;
}
}
VirtualFile file = CommonDataKeys.VIRTUAL_FILE.getData(dataContext);
if (file != null) {
// See if it's inside a res folder (or is equal to a resource folder
Module module = LangDataKeys.MODULE.getData(dataContext);
if (module != null) {
LocalResourceManager manager = LocalResourceManager.getInstance(module);
if (manager != null) {
VirtualFile resFolder = getResFolderParent(manager, file);
if (resFolder != null) {
return AndroidPsiUtils.getPsiDirectorySafely(module.getProject(), resFolder);
}
}
}
}
return null;
}
protected class MyInputValidator extends ElementCreator implements InputValidator {
private final PsiDirectory myDirectory;
private PsiElement[] myCreatedElements = PsiElement.EMPTY_ARRAY;
public MyInputValidator(final Project project, final PsiDirectory directory) {
super(project, getErrorTitle());
myDirectory = directory;
}
public PsiDirectory getDirectory() {
return myDirectory;
}
@Override
public boolean checkInput(final String inputString) {
return true;
}
@Override
public PsiElement[] create(String newName) throws Exception {
return CreateResourceActionBase.this.create(newName, myDirectory);
}
@Override
public String getActionName(String newName) {
return CreateResourceActionBase.this.getActionName(myDirectory, newName);
}
@Override
public boolean canClose(final String inputString) {
myCreatedElements = tryCreate(inputString);
return myCreatedElements.length > 0;
}
public final PsiElement[] getCreatedElements() {
return myCreatedElements;
}
}
}