blob: d8dcfb5a288b818f05d3093a473b028b3ae032bf [file] [log] [blame]
/*
* 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.rendering;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_CLASS;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.ATTR_LAYOUT;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.CLASS_ATTRIBUTE_SET;
import static com.android.SdkConstants.CLASS_CONTEXT;
import static com.android.SdkConstants.CLASS_FRAGMENT;
import static com.android.SdkConstants.CLASS_V4_FRAGMENT;
import static com.android.SdkConstants.CLASS_VIEW;
import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.TOOLS_URI;
import static com.android.SdkConstants.VALUE_FALSE;
import static com.android.SdkConstants.VIEW_FRAGMENT;
import static com.android.tools.idea.layoutlib.LayoutLibrary.LAYOUTLIB_NATIVE_PLUGIN;
import static com.android.tools.idea.layoutlib.LayoutLibrary.LAYOUTLIB_STANDARD_PLUGIN;
import com.android.ide.common.repository.GradleCoordinate;
import com.android.resources.ResourceType;
import com.android.tools.analytics.UsageTracker;
import com.android.tools.idea.projectsystem.GoogleMavenArtifactId;
import com.android.tools.idea.projectsystem.ProjectSystemSyncManager;
import com.android.tools.idea.projectsystem.ProjectSystemUtil;
import com.android.tools.idea.ui.designer.EditorDesignSurface;
import com.android.tools.idea.ui.resourcechooser.util.ResourceChooserHelperKt;
import com.android.tools.idea.ui.resourcemanager.ResourcePickerDialog;
import com.android.tools.idea.util.DependencyManagementUtil;
import com.android.tools.lint.detector.api.Lint;
import com.android.utils.SdkUtils;
import com.android.utils.SparseArray;
import com.google.common.annotations.VisibleForTesting;
import com.google.wireless.android.sdk.stats.AndroidStudioEvent;
import com.google.wireless.android.sdk.stats.LayoutEditorEvent;
import com.intellij.codeInsight.daemon.impl.quickfix.CreateClassKind;
import com.intellij.codeInsight.intention.impl.CreateClassDialog;
import com.intellij.ide.browsers.BrowserLauncher;
import com.intellij.ide.plugins.PluginManagerConfigurable;
import com.intellij.ide.plugins.PluginManagerCore;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationDisplayType;
import com.intellij.notification.NotificationGroup;
import com.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.Result;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.PluginId;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.JavaDirectoryService;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElementFactory;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiImportList;
import com.intellij.psi.PsiJavaCodeReferenceElement;
import com.intellij.psi.PsiJavaFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiReferenceList;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.search.FilenameIndex;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.ClassUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.AlarmFactory;
import com.intellij.util.PsiNavigateUtil;
import java.io.File;
import java.net.MalformedURLException;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jetbrains.android.dom.manifest.AndroidManifestUtils;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.uipreview.ChooseClassDialog;
import com.android.tools.idea.res.IdeResourcesUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class HtmlLinkManager {
private static final String URL_EDIT_CLASSPATH = "action:classpath";
private static final String URL_BUILD = "action:build";
private static final String URL_SYNC = "action:sync";
private static final String URL_SHOW_XML = "action:showXml";
private static final String URL_ACTION_IGNORE_FRAGMENTS = "action:ignoreFragment";
private static final String URL_RUNNABLE = "runnable:";
private static final String URL_COMMAND = "command:";
private static final String URL_REPLACE_TAGS = "replaceTags:";
private static final String URL_SHOW_TAG = "showTag:";
private static final String URL_OPEN = "open:";
private static final String URL_CREATE_CLASS = "createClass:";
private static final String URL_OPEN_CLASS = "openClass:";
private static final String URL_ASSIGN_FRAGMENT_URL = "assignFragmentUrl:";
private static final String URL_ASSIGN_LAYOUT_URL = "assignLayoutUrl:";
private static final String URL_EDIT_ATTRIBUTE = "editAttribute:";
private static final String URL_REPLACE_ATTRIBUTE_VALUE = "replaceAttributeValue:";
private static final String URL_DISABLE_SANDBOX = "disableSandbox:";
private static final String URL_REFRESH_RENDER = "refreshRender";
private static final String URL_ADD_DEPENDENCY = "addDependency:";
private static final String URL_CLEAR_CACHE_AND_NOTIFY = "clearCacheAndNotify";
private static final String URL_ENABLE_LAYOUTLIB_NATIVE = "enableLayoutlibNative";
private static final String URL_DISABLE_LAYOUTLIB_NATIVE = "disableLayoutlibNative";
private SparseArray<Runnable> myLinkRunnables;
private SparseArray<WriteCommandAction> myLinkCommands;
private int myNextLinkId = 0;
public HtmlLinkManager() {
}
/**
* {@link NotificationGroup} used to let the user now that the click on a link did something. This is meant to be used
* in those actions that do not trigger any UI updates (like Copy stack trace to clipboard).
*/
private static final NotificationGroup NOTIFICATIONS_GROUP = new NotificationGroup(
"Render error panel notifications", NotificationDisplayType.BALLOON, false, null, null, null, PluginId.getId("org.jetbrains.android"));
public static void showNotification(@NotNull String content) {
Notification notification = NOTIFICATIONS_GROUP.createNotification(content, NotificationType.INFORMATION);
Notifications.Bus.notify(notification);
AlarmFactory.getInstance().create().addRequest(
notification::expire,
TimeUnit.SECONDS.toMillis(2)
);
}
public void handleUrl(@NotNull String url, @Nullable Module module, @Nullable PsiFile file, @Nullable DataContext dataContext,
@Nullable RenderResult result, @Nullable EditorDesignSurface surface) {
if (url.startsWith("http:") || url.startsWith("https:")) {
BrowserLauncher.getInstance().browse(url, null, module == null ? null : module.getProject());
}
else if (url.startsWith("file:")) {
assert module != null;
handleFileUrl(url, module);
}
else if (url.startsWith(URL_REPLACE_TAGS)) {
assert module != null;
assert file != null;
handleReplaceTagsUrl(url, module, file);
}
else if (url.equals(URL_BUILD)) {
assert dataContext != null;
assert module != null;
handleBuildProjectUrl(url, module.getProject());
}
else if (url.equals(URL_SYNC)) {
assert dataContext != null;
assert module != null;
handleSyncProjectUrl(url, module.getProject());
}
else if (url.equals(URL_EDIT_CLASSPATH)) {
assert module != null;
handleEditClassPathUrl(url, module);
}
else if (url.startsWith(URL_CREATE_CLASS)) {
assert module != null && file != null;
handleNewClassUrl(url, module);
}
else if (url.startsWith(URL_OPEN)) {
assert module != null;
handleOpenStackUrl(url, module);
}
else if (url.startsWith(URL_OPEN_CLASS)) {
assert module != null;
handleOpenClassUrl(url, module);
}
else if (url.equals(URL_SHOW_XML)) {
assert module != null && file != null;
handleShowXmlUrl(url, module, file);
}
else if (url.startsWith(URL_SHOW_TAG)) {
assert module != null && file != null;
handleShowTagUrl(url, module, file);
}
else if (url.startsWith(URL_ASSIGN_FRAGMENT_URL)) {
assert module != null && file != null;
handleAssignFragmentUrl(url, module, file);
}
else if (url.startsWith(URL_ASSIGN_LAYOUT_URL)) {
assert module != null && file != null;
handleAssignLayoutUrl(url, module, file);
}
else if (url.equals(URL_ACTION_IGNORE_FRAGMENTS)) {
assert result != null;
handleIgnoreFragments(url, surface);
}
else if (url.startsWith(URL_EDIT_ATTRIBUTE)) {
assert result != null;
if (module != null && file != null) {
handleEditAttribute(url, module, file);
}
}
else if (url.startsWith(URL_REPLACE_ATTRIBUTE_VALUE)) {
assert result != null;
if (module != null && file != null) {
handleReplaceAttributeValue(url, module, file);
}
}
else if (url.startsWith(URL_DISABLE_SANDBOX)) {
assert module != null;
handleDisableSandboxUrl(module, surface);
}
else if (url.startsWith(URL_RUNNABLE)) {
Runnable linkRunnable = getLinkRunnable(url);
if (linkRunnable != null) {
linkRunnable.run();
}
}
else if (url.startsWith(URL_ADD_DEPENDENCY) && module != null) {
assert module.getModuleFile() != null;
handleAddDependency(url, module);
ProjectSystemUtil.getSyncManager(module.getProject())
.syncProject(ProjectSystemSyncManager.SyncReason.PROJECT_MODIFIED);
}
else if (url.startsWith(URL_COMMAND)) {
WriteCommandAction command = getLinkCommand(url);
if (command != null) {
command.execute();
}
}
else if (url.startsWith(URL_REFRESH_RENDER)) {
handleRefreshRenderUrl(surface);
}
else if (url.startsWith(URL_CLEAR_CACHE_AND_NOTIFY)) {
// This does the same as URL_REFRESH_RENDERER with the only difference of displaying a notification afterwards. The reason to have
// handler is that we have different entry points for the action, one of which is "Clear cache". The user probably expects a result
// of clicking that link that has something to do with the cache being cleared.
handleRefreshRenderUrl(surface);
showNotification("Cache cleared");
}
else if (url.startsWith(URL_ENABLE_LAYOUTLIB_NATIVE)) {
handleEnableLayoutlibNative(true);
}
else if (url.startsWith(URL_DISABLE_LAYOUTLIB_NATIVE)) {
handleEnableLayoutlibNative(false);
}
else {
assert false : "Unexpected URL: " + url;
}
}
/**
* Creates a file url for the given file and line position
*
* @param file the file
* @param line the line, or -1 if not known
* @param column the column, or 0 if not known
* @return a URL which points to a given position in a file
*/
@Nullable
public static String createFilePositionUrl(@NotNull File file, int line, int column) {
try {
String fileUrl = SdkUtils.fileToUrlString(file);
if (line != -1) {
if (column > 0) {
return fileUrl + ':' + line + ':' + column;
}
else {
return fileUrl + ':' + line;
}
}
return fileUrl;
}
catch (MalformedURLException e) {
// Ignore
Logger.getInstance(HtmlLinkManager.class).error(e);
return null;
}
}
private static void handleFileUrl(@NotNull String url, @NotNull Module module) {
Project project = module.getProject();
try {
// Allow line numbers and column numbers to be tacked on at the end of
// the file URL:
// file:<path>:<line>:<column>
// file:<path>:<line>
int line = -1;
int column = 0;
Pattern pattern = Pattern.compile(".*:(\\d+)(:(\\d+))");
Matcher matcher = pattern.matcher(url);
if (matcher.matches()) {
line = Integer.parseInt(matcher.group(1));
column = Integer.parseInt(matcher.group(3));
url = url.substring(0, matcher.start(1) - 1);
}
else {
matcher = Pattern.compile(".*:(\\d+)").matcher(url);
if (matcher.matches()) {
line = Integer.parseInt(matcher.group(1));
url = url.substring(0, matcher.start(1) - 1);
}
}
File ioFile = SdkUtils.urlToFile(url);
VirtualFile file = LocalFileSystem.getInstance().findFileByIoFile(ioFile);
if (file != null) {
openEditor(project, file, line, column);
}
}
catch (MalformedURLException e) {
// Ignore
}
}
public String createCommandLink(@NotNull WriteCommandAction command) {
String url = URL_COMMAND + myNextLinkId;
if (myLinkCommands == null) {
myLinkCommands = new SparseArray<>(5);
}
myLinkCommands.put(myNextLinkId, command);
myNextLinkId++;
return url;
}
@Nullable
private WriteCommandAction getLinkCommand(String url) {
if (myLinkCommands != null && url.startsWith(URL_COMMAND)) {
String idString = url.substring(URL_COMMAND.length());
int id = Integer.decode(idString);
return myLinkCommands.get(id);
}
return null;
}
public String createRunnableLink(@NotNull Runnable runnable) {
String url = URL_RUNNABLE + myNextLinkId;
if (myLinkRunnables == null) {
myLinkRunnables = new SparseArray<>(5);
}
myLinkRunnables.put(myNextLinkId, runnable);
myNextLinkId++;
return url;
}
@Nullable
private Runnable getLinkRunnable(String url) {
if (myLinkRunnables != null && url.startsWith(URL_RUNNABLE)) {
String idString = url.substring(URL_RUNNABLE.length());
int id = Integer.decode(idString);
return myLinkRunnables.get(id);
}
return null;
}
public String createReplaceTagsUrl(String from, String to) {
return URL_REPLACE_TAGS + from + '/' + to;
}
private static void handleReplaceTagsUrl(@NotNull String url, @NotNull Module module, @NotNull PsiFile file) {
assert url.startsWith(URL_REPLACE_TAGS) : url;
int start = URL_REPLACE_TAGS.length();
int delimiterPos = url.indexOf('/', start);
if (delimiterPos != -1) {
String wrongTag = url.substring(start, delimiterPos);
String rightTag = url.substring(delimiterPos + 1);
ReplaceTagFix fix = new ReplaceTagFix(module.getProject(), (XmlFile)file, wrongTag, rightTag);
fix.execute();
}
}
public String createBuildProjectUrl() {
return URL_BUILD;
}
private static void handleBuildProjectUrl(@NotNull String url, @NotNull Project project) {
assert url.equals(URL_BUILD) : url;
ProjectSystemUtil.getProjectSystem(project).buildProject();
}
public String createSyncProjectUrl() {
return URL_SYNC;
}
private static void handleSyncProjectUrl(@NotNull String url, @NotNull Project project) {
assert url.equals(URL_SYNC) : url;
ProjectSystemSyncManager.SyncReason reason = project.isInitialized() ? ProjectSystemSyncManager.SyncReason.PROJECT_MODIFIED : ProjectSystemSyncManager.SyncReason.PROJECT_LOADED;
ProjectSystemUtil.getProjectSystem(project).getSyncManager().syncProject(reason);
}
public String createEditClassPathUrl() {
return URL_EDIT_CLASSPATH;
}
private static void handleEditClassPathUrl(@NotNull String url, @NotNull Module module) {
assert url.equals(URL_EDIT_CLASSPATH) : url;
ProjectSettingsService.getInstance(module.getProject()).openModuleSettings(module);
}
public String createOpenClassUrl(@NotNull String className) {
return URL_OPEN_CLASS + className;
}
private static void handleOpenClassUrl(@NotNull String url, @NotNull Module module) {
assert url.startsWith(URL_OPEN_CLASS) : url;
String className = url.substring(URL_OPEN_CLASS.length());
Project project = module.getProject();
PsiClass clz = JavaPsiFacade.getInstance(project).findClass(className, GlobalSearchScope.allScope(project));
if (clz != null) {
PsiFile containingFile = clz.getContainingFile();
if (containingFile != null) {
openEditor(project, containingFile, clz.getTextOffset());
}
}
}
private static void handleShowXmlUrl(@NotNull String url, @NotNull Module module, @NotNull PsiFile file) {
assert url.equals(URL_SHOW_XML) : url;
openEditor(module.getProject(), file, 0, -1);
}
public String createShowTagUrl(String tag) {
return URL_SHOW_TAG + tag;
}
private static void handleShowTagUrl(@NotNull String url, @NotNull Module module, @NotNull final PsiFile file) {
assert url.startsWith(URL_SHOW_TAG) : url;
final String tagName = url.substring(URL_SHOW_TAG.length());
XmlTag first = ApplicationManager.getApplication().runReadAction((Computable<XmlTag>)() -> {
Collection<XmlTag> xmlTags = PsiTreeUtil.findChildrenOfType(file, XmlTag.class);
for (XmlTag tag : xmlTags) {
if (tagName.equals(tag.getName())) {
return tag;
}
}
return null;
});
if (first != null) {
PsiNavigateUtil.navigate(first);
}
else {
// Fall back to just opening the editor
openEditor(module.getProject(), file, 0, -1);
}
}
public String createNewClassUrl(String className) {
return URL_CREATE_CLASS + className;
}
private static void handleNewClassUrl(@NotNull String url, @NotNull Module module) {
assert url.startsWith(URL_CREATE_CLASS) : url;
String s = url.substring(URL_CREATE_CLASS.length());
final Project project = module.getProject();
String title = "Create Custom View";
final String className;
final String packageName;
int index = s.lastIndexOf('.');
if (index == -1) {
className = s;
packageName = AndroidManifestUtils.getPackageName(module);
if (packageName == null) {
return;
}
}
else {
packageName = s.substring(0, index);
className = s.substring(index + 1);
}
CreateClassDialog dialog = new CreateClassDialog(project, title, className, packageName, CreateClassKind.CLASS, true, module) {
@Override
protected boolean reportBaseInSourceSelectionInTest() {
return true;
}
};
dialog.show();
if (dialog.getExitCode() == DialogWrapper.OK_EXIT_CODE) {
final PsiDirectory targetDirectory = dialog.getTargetDirectory();
if (targetDirectory != null) {
PsiClass newClass = new WriteCommandAction<PsiClass>(project, "Create Class") {
@Override
protected void run(@NotNull Result<PsiClass> result) throws Throwable {
PsiClass targetClass = JavaDirectoryService.getInstance().createClass(targetDirectory, className);
PsiManager manager = PsiManager.getInstance(project);
final JavaPsiFacade facade = JavaPsiFacade.getInstance(manager.getProject());
final PsiElementFactory factory = facade.getElementFactory();
// Extend android.view.View
PsiJavaCodeReferenceElement superclassReference =
factory.createReferenceElementByFQClassName(CLASS_VIEW, targetClass.getResolveScope());
PsiReferenceList extendsList = targetClass.getExtendsList();
if (extendsList != null) {
extendsList.add(superclassReference);
}
// Add constructor
GlobalSearchScope scope = GlobalSearchScope.allScope(project);
PsiJavaFile javaFile = (PsiJavaFile)targetClass.getContainingFile();
PsiImportList importList = javaFile.getImportList();
if (importList != null) {
PsiClass contextClass = JavaPsiFacade.getInstance(project).findClass(CLASS_CONTEXT, scope);
if (contextClass != null) {
importList.add(factory.createImportStatement(contextClass));
}
PsiClass attributeSetClass = JavaPsiFacade.getInstance(project).findClass(CLASS_ATTRIBUTE_SET, scope);
if (attributeSetClass != null) {
importList.add(factory.createImportStatement(attributeSetClass));
}
}
PsiMethod constructor1arg = factory.createMethodFromText(
"public " + className + "(Context context) {\n" +
" this(context, null);\n" +
"}\n", targetClass);
targetClass.add(constructor1arg);
PsiMethod constructor2args = factory.createMethodFromText(
"public " + className + "(Context context, AttributeSet attrs) {\n" +
" this(context, attrs, 0);\n" +
"}\n", targetClass);
targetClass.add(constructor2args);
PsiMethod constructor3args = factory.createMethodFromText(
"public " + className + "(Context context, AttributeSet attrs, int defStyle) {\n" +
" super(context, attrs, defStyle);\n" +
"}\n", targetClass);
targetClass.add(constructor3args);
// Format class
CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(manager.getProject());
PsiFile containingFile = targetClass.getContainingFile();
if (containingFile != null) {
codeStyleManager.reformat(javaFile);
}
result.setResult(targetClass);
}
}.execute().getResultObject();
if (newClass != null) {
PsiFile file = newClass.getContainingFile();
if (file != null) {
openEditor(project, file, newClass.getTextOffset());
}
}
}
}
}
public String createOpenStackUrl(@NotNull String className, @NotNull String methodName, @NotNull String fileName, int lineNumber) {
return URL_OPEN + className + '#' + methodName + ';' + fileName + ':' + lineNumber;
}
private static void handleOpenStackUrl(@NotNull String url, @NotNull Module module) {
assert url.startsWith(URL_OPEN) : url;
// Syntax: URL_OPEN + className + '#' + methodName + ';' + fileName + ':' + lineNumber;
int start = URL_OPEN.length();
int semi = url.indexOf(';', start);
String className;
String fileName;
int line;
if (semi != -1) {
className = url.substring(start, semi);
int colon = url.indexOf(':', semi + 1);
if (colon != -1) {
fileName = url.substring(semi + 1, colon);
line = Integer.decode(url.substring(colon + 1));
}
else {
fileName = url.substring(semi + 1);
line = -1;
}
// Attempt to open file
}
else {
className = url.substring(start);
fileName = null;
line = -1;
}
String method = null;
int hash = className.indexOf('#');
if (hash != -1) {
method = className.substring(hash + 1);
className = className.substring(0, hash);
}
Project project = module.getProject();
PsiClass clz = JavaPsiFacade.getInstance(project).findClass(className, GlobalSearchScope.allScope(project));
if (clz != null) {
PsiFile containingFile = clz.getContainingFile();
if (fileName != null && containingFile != null && line != -1) {
VirtualFile virtualFile = containingFile.getVirtualFile();
if (virtualFile != null) {
String name = virtualFile.getName();
if (fileName.equals(name)) {
// Use the line number rather than the methodName
openEditor(project, containingFile, line - 1, -1);
return;
}
}
}
if (method != null) {
PsiMethod[] methodsByName = clz.findMethodsByName(method, true);
for (PsiMethod m : methodsByName) {
PsiFile psiFile = m.getContainingFile();
if (psiFile != null) {
VirtualFile virtualFile = psiFile.getVirtualFile();
if (virtualFile != null) {
OpenFileDescriptor descriptor = new OpenFileDescriptor(project, virtualFile, m.getTextOffset());
FileEditorManager.getInstance(project).openEditor(descriptor, true);
return;
}
}
}
}
if (fileName != null) {
PsiFile[] files = FilenameIndex.getFilesByName(project, fileName, GlobalSearchScope.allScope(project));
for (PsiFile psiFile : files) {
if (openEditor(project, psiFile, line != -1 ? line - 1 : -1, -1)) {
break;
}
}
}
}
}
private static boolean openEditor(@NotNull Project project, @NotNull PsiFile psiFile, int line, int column) {
VirtualFile file = psiFile.getVirtualFile();
if (file != null) {
return openEditor(project, file, line, column);
}
return false;
}
private static boolean openEditor(@NotNull Project project, @NotNull VirtualFile file, int line, int column) {
OpenFileDescriptor descriptor = new OpenFileDescriptor(project, file, line, column);
FileEditorManager manager = FileEditorManager.getInstance(project);
// Attempt to prefer text editor if it's available for this file
if (manager.openTextEditor(descriptor, true) != null) {
return true;
}
return !manager.openEditor(descriptor, true).isEmpty();
}
private static boolean openEditor(@NotNull Project project, @NotNull PsiFile psiFile, int offset) {
VirtualFile file = psiFile.getVirtualFile();
if (file != null) {
return openEditor(project, file, offset);
}
return false;
}
private static boolean openEditor(@NotNull Project project, @NotNull VirtualFile file, int offset) {
OpenFileDescriptor descriptor = new OpenFileDescriptor(project, file, offset);
return !FileEditorManager.getInstance(project).openEditor(descriptor, true).isEmpty();
}
public String createAssignFragmentUrl(@Nullable String id) {
return URL_ASSIGN_FRAGMENT_URL + (id != null ? id : "");
}
/**
* Converts a (possibly ambiguous) class name like A.B.C.D into an Android-style class name
* like A.B$C$D where package names are separated by dots and inner classes are separated by dollar
* signs (similar to how the internal JVM format uses dollar signs for inner classes but slashes
* as package separators)
*/
@NotNull
private static String getFragmentClass(@NotNull final Module module, @NotNull final String fqcn) {
return ApplicationManager.getApplication().runReadAction((Computable<String>)() -> {
Project project = module.getProject();
JavaPsiFacade finder = JavaPsiFacade.getInstance(project);
PsiClass psiClass = finder.findClass(fqcn, module.getModuleScope());
if (psiClass == null) {
psiClass = finder.findClass(fqcn, GlobalSearchScope.allScope(project));
}
if (psiClass != null) {
String jvmClassName = ClassUtil.getJVMClassName(psiClass);
if (jvmClassName != null) {
return jvmClassName.replace('/', '.');
}
}
return fqcn;
});
}
private static void handleAssignFragmentUrl(@NotNull String url, @NotNull Module module, @NotNull final PsiFile file) {
assert url.startsWith(URL_ASSIGN_FRAGMENT_URL) : url;
Predicate<PsiClass> psiFilter = ChooseClassDialog.getUserDefinedPublicAndUnrestrictedFilter();
String className = ChooseClassDialog
.openDialog(module, "Fragments", null, psiFilter, CLASS_FRAGMENT, CLASS_V4_FRAGMENT.oldName(), CLASS_V4_FRAGMENT.newName());
if (className == null) {
return;
}
final String fragmentClass = getFragmentClass(module, className);
int start = URL_ASSIGN_FRAGMENT_URL.length();
final String id;
if (start == url.length()) {
// No specific fragment identified; use the first one
id = null;
}
else {
id = Lint.stripIdPrefix(url.substring(start));
}
WriteCommandAction<Void> action = new WriteCommandAction<Void>(module.getProject(), "Assign Fragment", file) {
@Override
protected void run(@NotNull Result<Void> result) throws Throwable {
Collection<XmlTag> tags = PsiTreeUtil.findChildrenOfType(file, XmlTag.class);
for (XmlTag tag : tags) {
if (!tag.getName().equals(VIEW_FRAGMENT)) {
continue;
}
if (id != null) {
String tagId = tag.getAttributeValue(ATTR_ID, ANDROID_URI);
if (tagId == null || !tagId.endsWith(id) || !id.equals(Lint.stripIdPrefix(tagId))) {
continue;
}
}
if (tag.getAttribute(ATTR_NAME, ANDROID_URI) == null && tag.getAttribute(ATTR_CLASS) == null) {
tag.setAttribute(ATTR_NAME, ANDROID_URI, fragmentClass);
return;
}
}
if (id == null) {
for (XmlTag tag : tags) {
if (!tag.getName().equals(VIEW_FRAGMENT)) {
continue;
}
tag.setAttribute(ATTR_NAME, ANDROID_URI, fragmentClass);
break;
}
}
}
};
action.execute();
}
public String createPickLayoutUrl(@NotNull String activityName) {
return URL_ASSIGN_LAYOUT_URL + activityName;
}
public String createAssignLayoutUrl(@NotNull String activityName, @NotNull String layout) {
return URL_ASSIGN_LAYOUT_URL + activityName + ':' + layout;
}
private static void handleAssignLayoutUrl(@NotNull String url, @NotNull final Module module, @NotNull final PsiFile file) {
assert url.startsWith(URL_ASSIGN_LAYOUT_URL) : url;
int start = URL_ASSIGN_LAYOUT_URL.length();
int layoutStart = url.indexOf(':', start + 1);
Project project = module.getProject();
XmlFile xmlFile = (XmlFile)file;
if (layoutStart == -1) {
// Only specified activity; pick it
String activityName = url.substring(start);
pickLayout(module, xmlFile, activityName);
}
else {
// Set directory to specified layoutName
final String activityName = url.substring(start, layoutStart);
final String layoutName = url.substring(layoutStart + 1);
final String layout = LAYOUT_RESOURCE_PREFIX + layoutName;
assignLayout(project, xmlFile, activityName, layout);
}
}
private static void pickLayout(
@NotNull final Module module,
@NotNull final XmlFile file,
@NotNull final String activityName) {
AndroidFacet facet = AndroidFacet.getInstance(module);
assert facet != null;
ResourcePickerDialog dialog = ResourceChooserHelperKt.createResourcePickerDialog(
"Choose a Layout",
null,
facet,
EnumSet.of(ResourceType.LAYOUT),
null,
true,
false,
file.getVirtualFile()
);
if (dialog.showAndGet()) {
String layout = dialog.getResourceName();
if (!layout.equals(LAYOUT_RESOURCE_PREFIX + file.getName())) {
assignLayout(module.getProject(), file, activityName, layout);
}
}
}
private static void assignLayout(
@NotNull final Project project,
@NotNull final XmlFile file,
@NotNull final String activityName,
@NotNull final String layout) {
WriteCommandAction<Void> action = new WriteCommandAction<Void>(project, "Assign Preview Layout", file) {
@Override
protected void run(@NotNull Result<Void> result) throws Throwable {
IdeResourcesUtil.ensureNamespaceImported(file, TOOLS_URI, null);
Collection<XmlTag> xmlTags = PsiTreeUtil.findChildrenOfType(file, XmlTag.class);
for (XmlTag tag : xmlTags) {
if (tag.getName().equals(VIEW_FRAGMENT)) {
String name = tag.getAttributeValue(ATTR_CLASS);
if (name == null || name.isEmpty()) {
name = tag.getAttributeValue(ATTR_NAME, ANDROID_URI);
}
if (activityName.equals(name)) {
tag.setAttribute(ATTR_LAYOUT, TOOLS_URI, layout);
}
}
}
}
};
action.execute();
}
public String createIgnoreFragmentsUrl() {
return URL_ACTION_IGNORE_FRAGMENTS;
}
private static void handleIgnoreFragments(@NotNull String url, @Nullable EditorDesignSurface surface) {
assert url.equals(URL_ACTION_IGNORE_FRAGMENTS);
RenderLogger.ignoreFragments();
requestRender(surface);
}
public String createEditAttributeUrl(String attribute, String value) {
return URL_EDIT_ATTRIBUTE + attribute + '/' + value;
}
private static void handleEditAttribute(@NotNull String url, @NotNull Module module, @NotNull final PsiFile file) {
assert url.startsWith(URL_EDIT_ATTRIBUTE);
int attributeStart = URL_EDIT_ATTRIBUTE.length();
int valueStart = url.indexOf('/');
final String attributeName = url.substring(attributeStart, valueStart);
final String value = url.substring(valueStart + 1);
XmlAttribute first = ApplicationManager.getApplication().runReadAction((Computable<XmlAttribute>)() -> {
Collection<XmlAttribute> attributes = PsiTreeUtil.findChildrenOfType(file, XmlAttribute.class);
for (XmlAttribute attribute : attributes) {
if (attributeName.equals(attribute.getLocalName()) && value.equals(attribute.getValue())) {
return attribute;
}
}
return null;
});
if (first != null) {
PsiNavigateUtil.navigate(first.getValueElement());
}
else {
// Fall back to just opening the editor
openEditor(module.getProject(), file, 0, -1);
}
}
public String createReplaceAttributeValueUrl(String attribute, String oldValue, String newValue) {
return URL_REPLACE_ATTRIBUTE_VALUE + attribute + '/' + oldValue + '/' + newValue;
}
private static void handleReplaceAttributeValue(@NotNull String url, @NotNull Module module, @NotNull final PsiFile file) {
assert url.startsWith(URL_REPLACE_ATTRIBUTE_VALUE);
int attributeStart = URL_REPLACE_ATTRIBUTE_VALUE.length();
int valueStart = url.indexOf('/');
int newValueStart = url.indexOf('/', valueStart + 1);
final String attributeName = url.substring(attributeStart, valueStart);
final String oldValue = url.substring(valueStart + 1, newValueStart);
final String newValue = url.substring(newValueStart + 1);
WriteCommandAction<Void> action = new WriteCommandAction<Void>(module.getProject(), "Set Attribute Value", file) {
@Override
protected void run(@NotNull Result<Void> result) throws Throwable {
Collection<XmlAttribute> attributes = PsiTreeUtil.findChildrenOfType(file, XmlAttribute.class);
int oldValueLen = oldValue.length();
for (XmlAttribute attribute : attributes) {
if (attributeName.equals(attribute.getLocalName())) {
String attributeValue = attribute.getValue();
if (attributeValue == null) {
continue;
}
if (oldValue.equals(attributeValue)) {
attribute.setValue(newValue);
}
else {
int index = attributeValue.indexOf(oldValue);
if (index != -1) {
if ((index == 0 || attributeValue.charAt(index - 1) == '|') &&
(index + oldValueLen == attributeValue.length() || attributeValue.charAt(index + oldValueLen) == '|')) {
attributeValue = attributeValue.substring(0, index) + newValue + attributeValue.substring(index + oldValueLen);
attribute.setValue(attributeValue);
}
}
}
}
}
}
};
action.execute();
}
public String createDisableSandboxUrl() {
return URL_DISABLE_SANDBOX;
}
private static void handleDisableSandboxUrl(@NotNull Module module, @Nullable EditorDesignSurface surface) {
RenderSecurityManager.sEnabled = false;
requestRender(surface);
Messages.showInfoMessage(module.getProject(),
"The custom view rendering sandbox was disabled for this session.\n\n" +
"You can turn it off permanently by adding\n" +
RenderSecurityManager.ENABLED_PROPERTY + "=" + VALUE_FALSE + "\n" +
"to {install}/bin/idea.properties.",
"Disabled Rendering Sandbox");
}
public String createRefreshRenderUrl() {
return URL_REFRESH_RENDER;
}
public String createClearCacheUrl() {
return URL_CLEAR_CACHE_AND_NOTIFY;
}
private static void handleRefreshRenderUrl(@Nullable EditorDesignSurface surface) {
if (surface != null) {
RenderUtils.clearCache(surface.getConfigurations());
}
}
private static void requestRender(@Nullable EditorDesignSurface surface) {
if (surface != null) {
surface.forceUserRequestedRefresh();
}
}
public String createAddDependencyUrl(GoogleMavenArtifactId artifactId) {
return URL_ADD_DEPENDENCY + artifactId;
}
@VisibleForTesting
static void handleAddDependency(@NotNull String url, @NotNull final Module module) {
assert url.startsWith(URL_ADD_DEPENDENCY) : url;
String coordinateStr = url.substring(URL_ADD_DEPENDENCY.length());
GradleCoordinate coordinate = GradleCoordinate.parseCoordinateString(coordinateStr + ":+");
if (coordinate == null) {
Logger.getInstance(HtmlLinkManager.class).warn("Invalid coordinate " + coordinateStr);
return;
}
if (DependencyManagementUtil.addDependencies(module, Collections.singletonList(coordinate), false, false)
.isEmpty()) {
return;
}
Logger.getInstance(HtmlLinkManager.class).warn("Could not add dependency " + coordinate);
}
@NotNull
public String createEnableLayoutlibNativeUrl() {
return URL_ENABLE_LAYOUTLIB_NATIVE;
}
@NotNull
public String createDisableLayoutlibNativeUrl() {
return URL_DISABLE_LAYOUTLIB_NATIVE;
}
private static void handleEnableLayoutlibNative(boolean enable) {
LayoutEditorEvent.Builder eventBuilder = LayoutEditorEvent.newBuilder();
if (enable) {
eventBuilder.setType(LayoutEditorEvent.LayoutEditorEventType.ENABLE_LAYOUTLIB_NATIVE);
PluginManagerCore.enablePlugin(LAYOUTLIB_NATIVE_PLUGIN);
}
else {
eventBuilder.setType(LayoutEditorEvent.LayoutEditorEventType.DISABLE_LAYOUTLIB_NATIVE);
PluginManagerCore.enablePlugin(LAYOUTLIB_STANDARD_PLUGIN);
PluginManagerCore.disablePlugin(LAYOUTLIB_NATIVE_PLUGIN);
}
AndroidStudioEvent.Builder studioEvent = AndroidStudioEvent.newBuilder()
.setCategory(AndroidStudioEvent.EventCategory.LAYOUT_EDITOR)
.setKind(AndroidStudioEvent.EventKind.LAYOUT_EDITOR_EVENT)
.setLayoutEditorEvent(eventBuilder.build());
UsageTracker.log(studioEvent);
PluginManagerConfigurable.shutdownOrRestartApp();
}
}