blob: 2b98ee90cb3f8982b86b18b939d01a54262a2e4e [file] [log] [blame]
/*
* Copyright 2016 The Bazel Authors. All rights reserved.
*
* 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.google.idea.blaze.android.resources.actions;
import com.android.SdkConstants;
import com.android.resources.ResourceFolderType;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.Sets;
import com.google.idea.blaze.android.sync.model.AndroidResourceModule;
import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
import com.google.idea.blaze.base.model.BlazeProjectData;
import com.google.idea.blaze.base.model.primitives.Label;
import com.google.idea.blaze.base.rulemaps.SourceToRuleMap;
import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
import com.intellij.ide.util.DirectoryUtil;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiManager;
import com.intellij.ui.ComboboxWithBrowseButton;
import com.intellij.ui.components.JBLabel;
import java.io.File;
import java.util.Set;
import javax.swing.JComboBox;
import org.jetbrains.annotations.Nullable;
/** Utilities for setting up create resource actions and dialogs. */
class BlazeCreateResourceUtils {
private static final String PLACEHOLDER_TEXT =
"choose a res/ directory with dropdown or browse button";
static void setupResDirectoryChoices(
Project project,
@Nullable VirtualFile contextFile,
JBLabel resDirLabel,
ComboboxWithBrowseButton resDirComboAndBrowser) {
// Reset the item list before filling it back up.
resDirComboAndBrowser.getComboBox().removeAllItems();
BlazeProjectData blazeProjectData =
BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
if (blazeProjectData != null) {
BlazeAndroidSyncData syncData = blazeProjectData.syncState.get(BlazeAndroidSyncData.class);
if (syncData != null) {
ImmutableCollection<Label> labelsRelatedToContext = null;
File fileFromContext = null;
if (contextFile != null) {
fileFromContext = VfsUtilCore.virtualToIoFile(contextFile);
labelsRelatedToContext =
SourceToRuleMap.getInstance(project).getTargetsForSourceFile(fileFromContext);
if (labelsRelatedToContext.isEmpty()) {
labelsRelatedToContext = null;
}
}
// Sort:
// - contextFile/res if contextFile is a directory,
// to optimize the right click on directory case, or the "closest" string
// match to the contextFile from the res directories known to blaze
// - the rest of the direct dirs, then transitive dirs of the context rules,
// then any known res dir in the project
// as a backup, in alphabetical order.
Set<File> resourceDirs = Sets.newTreeSet();
Set<File> transitiveDirs = Sets.newTreeSet();
Set<File> allResDirs = Sets.newTreeSet();
for (AndroidResourceModule androidResourceModule :
syncData.importResult.androidResourceModules) {
// labelsRelatedToContext should include deps,
// but as a first pass we only check the rules themselves
// for resources. If we come up empty, then have anyResDir as a backup.
allResDirs.addAll(androidResourceModule.transitiveResources);
if (labelsRelatedToContext != null
&& !labelsRelatedToContext.contains(androidResourceModule.label)) {
continue;
}
for (File resDir : androidResourceModule.resources) {
resourceDirs.add(resDir);
}
for (File resDir : androidResourceModule.transitiveResources) {
transitiveDirs.add(resDir);
}
}
// No need to show some directories twice.
transitiveDirs.removeAll(resourceDirs);
JComboBox resDirCombo = resDirComboAndBrowser.getComboBox();
// Allow the user to browse and overwrite some of the entries,
// in case our inference is wrong.
resDirCombo.setEditable(true);
// Optimize the right-click on a non-res directory (consider res directory right under that)
// After the use confirms the choice, a directory will be created if it is missing.
if (fileFromContext != null && fileFromContext.isDirectory()) {
File closestDirToContext = new File(fileFromContext.getPath(), "res");
resDirCombo.setSelectedItem(closestDirToContext);
} else {
// If we're not completely sure, let people know there are options
// via the placeholder text, and put the most likely on top.
String placeHolder = PLACEHOLDER_TEXT;
resDirCombo.addItem(placeHolder);
resDirCombo.setSelectedItem(placeHolder);
if (fileFromContext != null) {
File closestDirToContext =
findClosestDirToContext(fileFromContext.getPath(), resourceDirs);
closestDirToContext =
closestDirToContext != null
? closestDirToContext
: findClosestDirToContext(fileFromContext.getPath(), transitiveDirs);
if (closestDirToContext != null) {
resDirCombo.addItem(closestDirToContext);
resourceDirs.remove(closestDirToContext);
transitiveDirs.remove(closestDirToContext);
}
}
}
if (!resourceDirs.isEmpty() || !transitiveDirs.isEmpty()) {
for (File resourceDir : resourceDirs) {
resDirCombo.addItem(resourceDir);
}
for (File resourceDir : transitiveDirs) {
resDirCombo.addItem(resourceDir);
}
} else {
for (File resourceDir : allResDirs) {
resDirCombo.addItem(resourceDir);
}
}
resDirComboAndBrowser.setVisible(true);
resDirLabel.setVisible(true);
}
}
}
private static File findClosestDirToContext(String contextPath, Set<File> resourceDirs) {
File closestDirToContext = null;
int curStringDistance = Integer.MAX_VALUE;
for (File resDir : resourceDirs) {
int distance = StringUtil.difference(contextPath, resDir.getPath());
if (distance < curStringDistance) {
curStringDistance = distance;
closestDirToContext = resDir;
}
}
return closestDirToContext;
}
static PsiDirectory getResDirFromUI(Project project, ComboboxWithBrowseButton directoryCombo) {
PsiManager psiManager = PsiManager.getInstance(project);
Object selectedItem = directoryCombo.getComboBox().getEditor().getItem();
File selectedFile = null;
if (selectedItem instanceof File) {
selectedFile = (File) selectedItem;
} else if (selectedItem instanceof String) {
String selectedDir = (String) selectedItem;
if (!selectedDir.equals(PLACEHOLDER_TEXT)) {
selectedFile = new File(selectedDir);
}
}
if (selectedFile == null) {
return null;
}
final File finalSelectedFile = selectedFile;
return ApplicationManager.getApplication()
.runWriteAction(
new Computable<PsiDirectory>() {
@Override
public PsiDirectory compute() {
return DirectoryUtil.mkdirs(psiManager, finalSelectedFile.getPath());
}
});
}
static VirtualFile getResDirFromDataContext(VirtualFile contextFile) {
// Check if the contextFile is somewhere in
// the <path>/res/resType/foo.xml hierarchy and return <path>/res/.
if (contextFile.isDirectory()) {
if (contextFile.getName().equalsIgnoreCase(SdkConstants.FD_RES)) {
return contextFile;
}
if (ResourceFolderType.getFolderType(contextFile.getName()) != null) {
VirtualFile parent = contextFile.getParent();
if (parent != null && parent.getName().equalsIgnoreCase(SdkConstants.FD_RES)) {
return parent;
}
}
} else {
VirtualFile parent = contextFile.getParent();
if (parent != null && ResourceFolderType.getFolderType(parent.getName()) != null) {
// Otherwise, the contextFile is a file w/ a parent that is plausible.
// Recurse one level, on the parent.
return getResDirFromDataContext(parent);
}
}
// Otherwise, it may be too ambiguous to figure out (e.g., we're in a .java file).
return null;
}
}