blob: 54526e43fc5daa109542b7c88fb5d254cfb200ca [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 com.android.annotations.NonNull;
import com.android.annotations.VisibleForTesting;
import com.android.builder.model.AndroidProject;
import com.android.ide.common.rendering.RenderSecurityManager;
import com.android.ide.common.rendering.api.LayoutLog;
import com.android.ide.common.resources.ResourceResolver;
import com.android.resources.Density;
import com.android.sdklib.IAndroidTarget;
import com.android.tools.idea.configurations.RenderContext;
import com.android.tools.idea.gradle.service.notification.hyperlink.FixGradleModelVersionHyperlink;
import com.android.tools.idea.model.AndroidModuleInfo;
import com.android.utils.HtmlBuilder;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.intellij.compiler.impl.javaCompiler.javac.JavacConfiguration;
import com.intellij.icons.AllIcons;
import com.intellij.ide.DataManager;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.compiler.CompilerManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.options.ShowSettingsUtil;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.JavaSdk;
import com.intellij.openapi.projectRoots.JavaSdkVersion;
import com.intellij.openapi.projectRoots.ProjectJdkTable;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.ui.configuration.ClasspathEditor;
import com.intellij.openapi.roots.ui.configuration.ModulesConfigurator;
import com.intellij.openapi.roots.ui.configuration.ProjectStructureConfigurable;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.problems.WolfTheProblemSolver;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiFile;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.searches.ClassInheritorsSearch;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.util.containers.HashSet;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.android.dom.attrs.AttributeDefinition;
import org.jetbrains.android.dom.attrs.AttributeDefinitions;
import org.jetbrains.android.dom.attrs.AttributeFormat;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.sdk.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.model.java.compiler.JpsJavaCompilerOptions;
import javax.swing.*;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.text.Document;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLFrameHyperlinkEvent;
import java.awt.*;
import java.awt.datatransfer.StringSelection;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.android.SdkConstants.*;
import static com.android.ide.common.rendering.api.LayoutLog.TAG_RESOURCES_PREFIX;
import static com.android.ide.common.rendering.api.LayoutLog.TAG_RESOURCES_RESOLVE_THEME_ATTR;
import static com.android.tools.idea.configurations.RenderContext.UsageType.LAYOUT_EDITOR;
import static com.android.tools.idea.gradle.util.GradleUtil.hasLayoutRenderingIssue;
import static com.android.tools.idea.rendering.HtmlLinkManager.URL_ACTION_CLOSE;
import static com.android.tools.idea.rendering.RenderLogger.TAG_STILL_BUILDING;
import static com.android.tools.idea.rendering.ResourceHelper.viewNeedsPackage;
import static com.android.tools.lint.detector.api.LintUtils.editDistance;
import static com.android.tools.lint.detector.api.LintUtils.stripIdPrefix;
import static com.intellij.openapi.util.SystemInfo.JAVA_VERSION;
import static org.jetbrains.android.sdk.AndroidSdkUtils.getAndroidSdkAdditionalData;
import static org.jetbrains.android.sdk.AndroidSdkUtils.isAndroidSdk;
/**
* Panel which can show render errors, along with embedded hyperlinks to perform actions such as
* showing relevant source errors, or adding missing attributes, etc.
* <p>
* Partially based on {@link com.intellij.codeInspection.ui.Browser}, the inspections result HTML pane, modified
* to show render errors instead
*/
public class RenderErrorPanel extends JPanel {
public static final boolean SIZE_ERROR_PANEL_DYNAMICALLY = true;
private static final int ERROR_PANEL_OPACITY = UIUtil.isUnderDarcula() ? 224 : 208; // out of 255
/** Class of the render session implementation class; for render errors, we cut off stack dumps at this frame */
private static final String RENDER_SESSION_IMPL_FQCN = "com.android.layoutlib.bridge.impl.RenderSessionImpl";
private static final Logger LOG = Logger.getInstance(RenderErrorPanel.class);
private JEditorPane myHTMLViewer;
private final HyperlinkListener myHyperLinkListener;
private RenderResult myResult;
private HighlightSeverity mySeverity; // severity of messages shown, currently just warning or error
private HtmlLinkManager myLinkManager;
private final JScrollPane myScrollPane;
public RenderErrorPanel() {
super(new BorderLayout());
setOpaque(false);
myHTMLViewer = new JEditorPane(UIUtil.HTML_MIME, "<HTML><BODY>Render Problems</BODY></HTML>");
myHTMLViewer.setEditable(false);
myHyperLinkListener = new HyperlinkListener() {
@Override
public void hyperlinkUpdate(HyperlinkEvent e) {
if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
JEditorPane pane = (JEditorPane)e.getSource();
if (e instanceof HTMLFrameHyperlinkEvent) {
HTMLFrameHyperlinkEvent evt = (HTMLFrameHyperlinkEvent)e;
HTMLDocument doc = (HTMLDocument)pane.getDocument();
doc.processHTMLFrameHyperlinkEvent(evt);
return;
}
String url = e.getDescription();
if (url.equals(URL_ACTION_CLOSE)) {
close();
return;
}
performClick(url);
}
}
};
myHTMLViewer.addHyperlinkListener(myHyperLinkListener);
myHTMLViewer.setMargin(new Insets(3, 3, 3, 3));
myScrollPane = ScrollPaneFactory.createScrollPane(myHTMLViewer);
setupStyle();
add(myScrollPane, BorderLayout.CENTER);
}
public void dispose(){
removeAll();
if (myHTMLViewer != null) {
myHTMLViewer.removeHyperlinkListener(myHyperLinkListener);
myHTMLViewer = null;
}
}
@Nullable
public String showErrors(@NotNull final RenderResult result) {
RenderLogger logger = result.getLogger();
if (!logger.hasProblems()) {
showErrors(null, null, null);
return null;
}
try {
String html = generateHtml(result, result.getLogger().getLinkManager());
showErrors(html, result, logger.getLinkManager());
return html;
}
catch (Exception e) {
showEmpty();
return null;
}
}
public void showErrors(@Nullable String html, @Nullable RenderResult result, @Nullable HtmlLinkManager linkManager) {
showErrors(HighlightSeverity.ERROR, html, result, linkManager);
}
public void showWarning(@Nullable String html) {
showErrors(HighlightSeverity.WARNING, html, null, null);
}
private void showErrors(@NonNull HighlightSeverity severity,
@Nullable String html,
@Nullable RenderResult result,
@Nullable HtmlLinkManager linkManager) {
mySeverity = severity;
if (html == null) {
myResult = null;
showEmpty();
return;
}
try {
myHTMLViewer.read(new StringReader(html), null);
setupStyle();
myHTMLViewer.setCaretPosition(0);
myResult = result;
myLinkManager = linkManager;
}
catch (Exception e) {
showEmpty();
}
}
@VisibleForTesting
public void performClick(@NotNull String url) {
Module module = myResult.getModule();
PsiFile file = myResult.getFile();
DataContext dataContext = DataManager.getInstance().getDataContext(this);
assert dataContext != null;
myLinkManager.handleUrl(url, module, file, dataContext, myResult);
}
private void close() {
this.setVisible(false);
}
private void setupStyle() {
// Make the scrollPane transparent
if (myScrollPane != null) {
JViewport viewPort = myScrollPane.getViewport();
viewPort.setOpaque(false);
viewPort.setBackground(null);
myScrollPane.setOpaque(false);
myScrollPane.setBackground(null);
}
Document document = myHTMLViewer.getDocument();
if (!(document instanceof StyledDocument)) {
return;
}
StyledDocument styledDocument = (StyledDocument)document;
EditorColorsManager colorsManager = EditorColorsManager.getInstance();
EditorColorsScheme scheme = colorsManager.getGlobalScheme();
Style style = styledDocument.addStyle("active", null);
StyleConstants.setFontFamily(style, scheme.getEditorFontName());
StyleConstants.setFontSize(style, scheme.getEditorFontSize());
styledDocument.setCharacterAttributes(0, document.getLength(), style, false);
// Make background semitransparent
Color background = myHTMLViewer.getBackground();
if (background != null) {
background = new Color(background.getRed(), background.getGreen(), background.getBlue(), ERROR_PANEL_OPACITY);
myHTMLViewer.setBackground(background);
}
}
@VisibleForTesting
public JEditorPane getEditorPane() {
return myHTMLViewer;
}
public int getPreferredHeight(@SuppressWarnings("UnusedParameters") int width) {
return myHTMLViewer.getPreferredSize().height;
}
@Nullable
public HighlightSeverity getSeverity() {
return mySeverity;
}
public String generateHtml(@NotNull RenderResult result, @NotNull HtmlLinkManager linkManager) {
myResult = result;
myLinkManager = linkManager;
RenderLogger logger = result.getLogger();
RenderTask renderTask = result.getRenderTask();
assert logger.hasProblems();
HtmlBuilder builder = new HtmlBuilder(new StringBuilder(300));
builder.openHtmlBody();
// Construct close button. Sadly <img align="right"> doesn't work in JEditorPanes; would
// have looked a lot nicer with the image flushed to the right!
builder.addHtml("<A HREF=\"");
builder.addHtml(URL_ACTION_CLOSE);
builder.addHtml("\">");
builder.addIcon(HtmlBuilderHelper.getCloseIconPath());
builder.addHtml("</A>");
builder.addHeading("Rendering Problems", HtmlBuilderHelper.getHeaderFontColor()).newline();
reportMissingStyles(logger, builder);
if (renderTask != null) {
reportOldNinePathRenderLib(logger, builder, renderTask);
reportRelevantCompilationErrors(logger, builder, renderTask);
reportMissingSizeAttributes(logger, builder, renderTask);
reportMissingClasses(logger, builder, renderTask);
}
reportBrokenClasses(logger, builder);
reportInstantiationProblems(logger, builder);
reportOtherProblems(logger, builder);
reportUnknownFragments(logger, builder);
if (renderTask != null) {
reportRenderingFidelityProblems(logger, builder, renderTask);
}
builder.closeHtmlBody();
return builder.getHtml();
}
private void reportMissingClasses(@NotNull RenderLogger logger, @NotNull HtmlBuilder builder, @NotNull RenderTask renderTask) {
Set<String> missingClasses = logger.getMissingClasses();
if (missingClasses != null && !missingClasses.isEmpty()) {
if (missingClasses.contains("CalendarView")) {
builder.add("The ").addBold("CalendarView").add(" widget does not work correctly with this render target. " +
"As a workaround, try using the API 15 (Android 4.0.3) render target library by selecting it from the " +
"toolbar menu above.");
if (missingClasses.size() == 1) {
return;
}
}
boolean missingResourceClass = logger.isMissingResourceClass() && logger.getResourceClass() != null && logger.hasLoadedClasses();
builder.add("The following classes could not be found:");
builder.beginList();
Collection<String> customViews = null;
Collection<String> androidViewClassNames = null;
Collection<String> views = getAllViews(logger.getModule());
if (!views.isEmpty()) {
customViews = Lists.newArrayListWithExpectedSize(Math.max(10, views.size() - 80)); // most will be framework views
androidViewClassNames = Lists.newArrayListWithExpectedSize(views.size());
for (String fqcn : views) {
if (fqcn.startsWith("android.") && !viewNeedsPackage(fqcn)) {
androidViewClassNames.add(fqcn);
} else {
customViews.add(fqcn);
}
}
}
if (missingResourceClass) {
builder.listItem();
builder.add(logger.getResourceClass());
}
boolean foundCustomView = false;
for (String className : missingClasses) {
builder.listItem();
builder.add(className);
builder.add(" (");
foundCustomView |= addTypoSuggestions(builder, className, customViews, false);
addTypoSuggestions(builder, className, customViews, true);
addTypoSuggestions(builder, className, androidViewClassNames, false);
if (myLinkManager == null) {
return;
}
builder.addLink("Fix Build Path", myLinkManager.createEditClassPathUrl());
RenderContext renderContext = renderTask.getRenderContext();
if (renderContext != null && renderContext.getType() == LAYOUT_EDITOR) {
builder.add(", ");
builder.addLink("Edit XML", myLinkManager.createShowTagUrl(className));
}
// Offer to create the class, but only if it looks like a custom view
// TODO: Check to see if it looks like it's the name of a custom view and the
// user didn't realize a FQN is required here
if (className.indexOf('.') != -1) {
builder.add(", ");
builder.addLink("Create Class", myLinkManager.createNewClassUrl(className));
}
builder.add(")");
}
builder.endList();
builder.addIcon(HtmlBuilderHelper.getTipIconPath());
builder.addLink("Tip: Try to ", "build", " the project.",
myLinkManager.createCompileModuleUrl());
if (foundCustomView) {
builder.newline();
builder.add("One or more missing custom views were found in the project, but does not appear to have been compiled yet.");
}
builder.newline().newline();
}
}
private boolean addTypoSuggestions(@NotNull HtmlBuilder builder,
String actual,
@Nullable Collection<String> views,
boolean compareWithPackage) {
if (views == null || views.isEmpty()) {
return false;
}
// Look for typos and try to match with custom views and android views
String actualBase = actual.substring(actual.lastIndexOf('.') + 1);
String match = compareWithPackage ? actual : actualBase;
int maxDistance = actualBase.length() >= 4 ? 2 : 1;
if (views.size() > 0) {
for (String suggested : views) {
String suggestedBase = suggested.substring(suggested.lastIndexOf('.') + 1);
String matchWith = compareWithPackage ? suggested : suggestedBase;
if (Math.abs(actualBase.length() - suggestedBase.length()) > maxDistance) {
// The string lengths differ more than the allowed edit distance;
// no point in even attempting to compute the edit distance (requires
// O(n*m) storage and O(n*m) speed, where n and m are the string lengths)
continue;
}
boolean sameBase = actualBase.equals(suggestedBase);
if (!compareWithPackage && sameBase) {
// This view is an exact match for one of the known views.
// That probably means it's a valid class, but the project needs to be built.
continue;
}
if (compareWithPackage) {
if (!sameBase) {
// If they differ in the base name, handled by separate call with !compareWithPackage
continue;
} else if (actualBase.equals(actual) && !actualBase.equals(suggested) && viewNeedsPackage(suggested)) {
// Custom view needs to be specified with a fully qualified path
builder.addLink(String.format("Change to %1$s", suggested),
myLinkManager.createReplaceTagsUrl(actual, suggested));
builder.add(", ");
continue;
}
}
if (compareWithPackage && Math.abs(match.length() - matchWith.length()) > maxDistance) {
continue;
}
if (match.equals(matchWith)) {
// Exact match: Likely that we're looking for a valid package, but project has
// not yet been built
return true;
}
if (editDistance(match, matchWith) <= maxDistance) {
// Suggest this class as a typo for the given class
String labelClass = (suggestedBase.equals(actual) || actual.indexOf('.') != -1) ? suggested : suggestedBase;
builder.addLink(String.format("Change to %1$s", labelClass),
myLinkManager.createReplaceTagsUrl(actual,
// Only show full package name if class name
// is the same
(viewNeedsPackage(suggested) ? suggested : suggestedBase)));
builder.add(", ");
}
}
}
return false;
}
private void reportUnknownFragments(@NotNull final RenderLogger logger, @NotNull final HtmlBuilder builder) {
List<String> fragmentNames = logger.getMissingFragments();
if (fragmentNames != null && !fragmentNames.isEmpty()) {
builder.add("A ").addHtml("<code>").add("<fragment>").addHtml("</code>").add(" tag allows a layout file to dynamically include " +
"different layouts at runtime. ");
builder.add("At layout editing time the specific layout to be used is not known. You can choose which layout you would " +
"like previewed while editing the layout.");
builder.beginList();
// TODO: Add link to not warn any more for this session
for (final String className : fragmentNames) {
builder.listItem();
boolean isIdentified = className != null && !className.isEmpty();
boolean isActivityKnown = isIdentified && !className.startsWith(PREFIX_RESOURCE_REF);
if (isIdentified) {
builder.add("<fragment ");
builder.addBold(className);
builder.add(" ...>");
} else {
builder.add("<fragment>");
}
builder.add(" (");
if (isActivityKnown) {
final Module module = logger.getModule();
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
public void run() {
// TODO: Look up layout references in the given layout, if possible
// Find activity class
// Look for R references in the layout
assert module != null;
Project project = module.getProject();
GlobalSearchScope scope = GlobalSearchScope.allScope(project);
PsiClass clz = JavaPsiFacade.getInstance(project).findClass(className, scope);
String layoutName = myResult.getFile().getName();
boolean separate = false;
if (clz != null) {
// TODO: Should instead find all R.layout elements
// HACK AHEAD!
String matchText = clz.getText();
final Pattern LAYOUT_FIELD_PATTERN = Pattern.compile("R\\.layout\\.([a-z0-9_]+)"); //$NON-NLS-1$
Matcher matcher = LAYOUT_FIELD_PATTERN.matcher(matchText);
Set<String> layouts = Sets.newTreeSet();
int index = 0;
while (true) {
if (matcher.find(index)) {
layouts.add(matcher.group(1));
index = matcher.end();
} else {
break;
}
}
for (String layout : layouts) {
if (layout.equals(layoutName)) { // Don't include self
continue;
}
if (separate) {
builder.add(", ");
}
builder.addLink("Use @layout/" + layout, myLinkManager.createAssignLayoutUrl(className, layout));
separate = true;
}
}
if (separate) {
builder.add(", ");
}
builder.addLink("Pick Layout...", myLinkManager.createPickLayoutUrl(className));
}
});
} else {
builder.addLink("Choose Fragment Class...", myLinkManager.createAssignFragmentUrl(className));
}
builder.add(")");
}
builder.endList();
builder.newline();
// TODO: URLs
builder.addLink("Do not warn about <fragment> tags in this session", myLinkManager.createIgnoreFragmentsUrl());
builder.newline();
}
}
@NotNull
private static Collection<String> getAllViews(@Nullable final Module module) {
if (module == null) {
return Collections.emptyList();
}
if (!ApplicationManager.getApplication().isReadAccessAllowed()) {
return ApplicationManager.getApplication().runReadAction(new Computable<Collection<String>>() {
@Override
public Collection<String> compute() {
return getAllViews(module);
}
});
}
Set<String> names = new java.util.HashSet<String>();
for (PsiClass psiClass : findInheritors(module, CLASS_VIEW)) {
String name = psiClass.getQualifiedName();
if (name != null ) {
names.add(name);
}
}
return names;
}
@NotNull
private static Collection<PsiClass> findInheritors(@NotNull final Module module, @NotNull final String name) {
if (!ApplicationManager.getApplication().isReadAccessAllowed()) {
return ApplicationManager.getApplication().runReadAction(new Computable<Collection<PsiClass>>() {
@Override
public Collection<PsiClass> compute() {
return findInheritors(module, name);
}
});
}
Project project = module.getProject();
try {
PsiClass base = JavaPsiFacade.getInstance(project).findClass(name, GlobalSearchScope.allScope(project));
if (base != null) {
GlobalSearchScope scope = GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module, false);
return ClassInheritorsSearch.search(base, scope, true).findAll();
}
}
catch (IndexNotReadyException ignored) {
}
return Collections.emptyList();
}
private void reportBrokenClasses(@NotNull RenderLogger logger, @NotNull HtmlBuilder builder) {
Map<String,Throwable> brokenClasses = logger.getBrokenClasses();
if (brokenClasses != null && !brokenClasses.isEmpty()) {
final Module module = logger.getModule();
if (module != null) {
AndroidFacet facet = AndroidFacet.getInstance(module);
if (facet != null && facet.requiresAndroidModel() && facet.getAndroidModel() != null) {
AndroidProject androidProject = facet.getAndroidModel().getAndroidProject();
String modelVersion = androidProject.getModelVersion();
if (hasLayoutRenderingIssue(androidProject)) {
builder.addBold("Using an obsolete version of the Gradle plugin (" + modelVersion +
"); this can lead to layouts not rendering correctly.").newline();
builder.addIcon(HtmlBuilderHelper.getTipIconPath());
Runnable runnable = new Runnable() {
@Override
public void run() {
FixGradleModelVersionHyperlink quickFix = new FixGradleModelVersionHyperlink(GRADLE_PLUGIN_RECOMMENDED_VERSION,
null, false);
quickFix.executeIfClicked(module.getProject(),
new HyperlinkEvent(this, HyperlinkEvent.EventType.ACTIVATED, null, quickFix.getUrl()));
}
};
builder.add("Tip: Either ")
.addLink("update the Gradle plugin build version to 1.2.3", myLinkManager.createRunnableLink(runnable))
.add(" or later, or downgrade to version 1.1.3, or as a workaround, ");
builder.beginList();
builder.listItem().addLink("", "Build the project", ", then", myLinkManager.createCompileModuleUrl());
builder.listItem().addLink("", "Gradle Sync the project", ", then", myLinkManager.createSyncProjectUrl());
builder.listItem().addLink("Manually ", "refresh the layout", " (or restart the IDE)", myLinkManager.createRefreshRenderUrl());
builder.endList();
builder.newline();
}
}
}
for (Throwable throwable : brokenClasses.values()) {
if (RenderLogger.isIssue164378(throwable)) {
RenderLogger.addHtmlForIssue164378(throwable, module, myLinkManager, builder, false);
break;
}
}
builder.add("The following classes could not be instantiated:");
Throwable firstThrowable = null;
builder.beginList();
for (Map.Entry<String,Throwable> entry : brokenClasses.entrySet()) {
String className = entry.getKey();
Throwable throwable = entry.getValue();
builder.listItem();
builder.add(className);
builder.add(" (");
builder.addLink("Open Class", myLinkManager.createOpenClassUrl(className));
if (throwable != null) {
builder.add(", ");
ShowExceptionFix detailsFix = new ShowExceptionFix(logger.getModule().getProject(), throwable);
builder.addLink("Show Exception", myLinkManager.createRunnableLink(detailsFix));
}
builder.add(", ");
builder.addLink("Clear Cache", myLinkManager.createRefreshRenderUrl());
builder.add(")");
if (firstThrowable == null && throwable != null) {
firstThrowable = throwable;
}
}
builder.endList();
builder.addIcon(HtmlBuilderHelper.getTipIconPath());
builder.addLink("Tip: Use ", "View.isInEditMode()", " in your custom views to skip code or show sample data when shown in the IDE",
"http://developer.android.com/reference/android/view/View.html#isInEditMode()");
if (firstThrowable != null) {
builder.newline().newline();
builder.addHeading("Exception Details", HtmlBuilderHelper.getHeaderFontColor()).newline();
reportThrowable(builder, firstThrowable, false);
reportSandboxError(builder, firstThrowable, true, false);
}
builder.newline().newline();
}
}
private void reportSandboxError(@NotNull HtmlBuilder builder, Throwable throwable, boolean newlineBefore, boolean newlineAfter) {
if (throwable instanceof SecurityException) {
if (newlineBefore) {
builder.newline();
}
builder.addLink("Turn off custom view rendering sandbox", myLinkManager.createDisableSandboxUrl());
String lastFailedPath = RenderSecurityManager.getLastFailedPath();
if (lastFailedPath != null) {
builder.newline().newline();
builder.add("Diagnostic info for Studio bug report:").newline();
builder.add("Failed path: ").add(lastFailedPath).newline();
String tempDir = System.getProperty("java.io.tmpdir");
builder.add("Normal temp dir: ").add(tempDir).newline();
File normalized = new File(tempDir);
builder.add("Normalized temp dir: ").add(normalized.getPath()).newline();
try {
builder.add("Canonical temp dir: ").add(normalized.getCanonicalPath()).newline();
}
catch (IOException e) {
// ignore
}
builder.add("os.name: ").add(SystemInfo.OS_NAME).newline();
builder.add("os.version: ").add(SystemInfo.OS_VERSION).newline();
builder.add("java.runtime.version: ").add(SystemInfo.JAVA_RUNTIME_VERSION);
}
if (throwable.getMessage().equals("Unable to create temporary file")) {
if (JAVA_VERSION.startsWith("1.7.0_")) {
int version = Integer.parseInt(JAVA_VERSION.substring(JAVA_VERSION.indexOf('_') + 1));
if (version > 0 && version < 45) {
builder.newline();
builder.addIcon(HtmlBuilderHelper.getTipIconPath());
builder.add("Tip: This may be caused by using an older version of JDK 1.7.0; try using at least 1.7.0_45 " +
"(you are using " + JAVA_VERSION + ")");
}
}
}
if (newlineAfter) {
builder.newline().newline();
}
}
}
private void reportRenderingFidelityProblems(@NotNull RenderLogger logger, @NotNull HtmlBuilder builder,
@NotNull final RenderTask renderTask) {
List<RenderProblem> fidelityWarnings = logger.getFidelityWarnings();
if (fidelityWarnings != null && !fidelityWarnings.isEmpty()) {
builder.add("The graphics preview in the layout editor may not be accurate:").newline();
builder.beginList();
int count = 0;
for (final RenderProblem warning : fidelityWarnings) {
builder.listItem();
warning.appendHtml(builder.getStringBuilder());
final Object clientData = warning.getClientData();
if (clientData != null) {
builder.addLink(" (Ignore for this session)", myLinkManager.createRunnableLink(new Runnable() {
@Override
public void run() {
RenderLogger.ignoreFidelityWarning(clientData);
RenderContext renderContext = renderTask.getRenderContext();
if (renderContext != null) {
renderContext.requestRender();
}
}
}));
}
builder.newline();
count++;
// Only display the first 3 render fidelity issues
if (count == 3) {
@SuppressWarnings("ConstantConditions")
int remaining = fidelityWarnings.size() - count;
if (remaining > 0) {
builder.add("(").addHtml(Integer.toString(remaining)).add(" additional render fidelity issues hidden)");
break;
}
}
}
builder.endList();
builder.addLink("Ignore all fidelity warnings for this session", myLinkManager.createRunnableLink(new Runnable() {
@Override
public void run() {
RenderLogger.ignoreAllFidelityWarnings();
RenderContext renderContext = renderTask.getRenderContext();
if (renderContext != null) {
renderContext.requestRender();
}
}
}));
builder.newline();
}
}
private static void reportMissingStyles(RenderLogger logger, HtmlBuilder builder) {
if (logger.seenTagPrefix(TAG_STILL_BUILDING)) {
builder.addBold("Project Still Building: May cause rendering errors until the build is done.").newline();
builder.newline().newline();
} else if (logger.seenTagPrefix(TAG_RESOURCES_RESOLVE_THEME_ATTR)) {
builder.addBold("Missing styles. Is the correct theme chosen for this layout?").newline();
builder.addIcon(HtmlBuilderHelper.getTipIconPath());
builder.add("Use the Theme combo box above the layout to choose a different layout, or fix the theme style references.");
builder.newline().newline();
}
}
private static void reportOldNinePathRenderLib(RenderLogger logger, HtmlBuilder builder, @NotNull RenderTask renderTask) {
for (Throwable trace : logger.getTraces()) {
if (trace.toString().contains("java.lang.IndexOutOfBoundsException: Index: 2, Size: 2") //$NON-NLS-1$
&& renderTask.getConfiguration().getDensity() == Density.TV) {
builder.addBold("It looks like you are using a render target where the layout library does not support the tvdpi density.");
builder.newline().newline();
builder.add("Please try either updating to the latest available version (using the SDK manager), or if no updated " +
"version is available for this specific version of Android, try using a more recent render target version.");
builder.newline().newline();
break;
}
}
}
private static void reportRelevantCompilationErrors(RenderLogger logger, HtmlBuilder builder, RenderTask renderTask) {
Module module = logger.getModule();
Project project = module.getProject();
WolfTheProblemSolver wolfgang = WolfTheProblemSolver.getInstance(project);
if (wolfgang.hasProblemFilesBeneath(module)) {
if (logger.seenTagPrefix(TAG_RESOURCES_PREFIX)) {
// Do we have errors in the res/ files?
// See if it looks like we have aapt problems
boolean haveResourceErrors = wolfgang.hasProblemFilesBeneath(new Condition<VirtualFile>() {
@Override
public boolean value(VirtualFile virtualFile) {
return virtualFile.getFileType() == StdFileTypes.XML;
}
});
if (haveResourceErrors) {
builder.addBold("NOTE: This project contains resource errors, so aapt did not succeed, " +
"which can cause rendering failures. Fix resource problems first.");
builder.newline().newline();
}
} else if (renderTask.getLayoutlibCallback() != null && renderTask.getLayoutlibCallback().isUsed()) {
boolean hasJavaErrors = wolfgang.hasProblemFilesBeneath(new Condition<VirtualFile>() {
@Override
public boolean value(VirtualFile virtualFile) {
return virtualFile.getFileType() == StdFileTypes.JAVA;
}
});
if (hasJavaErrors) {
builder.addBold("NOTE: This project contains Java compilation errors, " +
"which can cause rendering failures for custom views. " +
"Fix compilation problems first.");
builder.newline().newline();
}
}
}
}
private void reportMissingSizeAttributes(@NotNull final RenderLogger logger, final HtmlBuilder builder, RenderTask renderTask) {
Module module = logger.getModule();
Project project = module.getProject();
if (logger.isMissingSize()) {
// Emit hyperlink about missing attributes; the action will operate on all of them
builder.addBold("NOTE: One or more layouts are missing the layout_width or layout_height attributes. " +
"These are required in most layouts.").newline();
final ResourceResolver resourceResolver = renderTask.getResourceResolver();
XmlFile psiFile = renderTask.getPsiFile();
if (psiFile == null) {
LOG.error("PsiFile is missing in RenderTask used in RenderErrorPanel!");
return;
}
AddMissingAttributesFix fix = new AddMissingAttributesFix(project, psiFile, resourceResolver);
List<XmlTag> missing = fix.findViewsMissingSizes();
// See whether we should offer match_parent instead of fill_parent
AndroidModuleInfo moduleInfo = AndroidModuleInfo.get(module);
final String fill = moduleInfo == null
|| moduleInfo.getBuildSdkVersion() == null
|| moduleInfo.getBuildSdkVersion().getApiLevel() >= 8
? VALUE_MATCH_PARENT : VALUE_FILL_PARENT;
for (final XmlTag tag : missing) {
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
public void run() {
boolean missingWidth = !AddMissingAttributesFix.definesWidth(tag, resourceResolver);
boolean missingHeight = !AddMissingAttributesFix.definesHeight(tag, resourceResolver);
assert missingWidth || missingHeight;
String id = tag.getAttributeValue(ATTR_ID);
if (id == null || id.length() == 0) {
id = '<' + tag.getName() + '>';
}
else {
id = '"' + stripIdPrefix(id) + '"';
}
if (missingWidth) {
reportMissingSize(builder, logger, fill, tag, id, ATTR_LAYOUT_WIDTH);
}
if (missingHeight) {
reportMissingSize(builder, logger, fill, tag, id, ATTR_LAYOUT_HEIGHT);
}
}
});
}
builder.newline();
builder.add("Or: ");
builder.addLink("Automatically add all missing attributes", myLinkManager.createCommandLink(fix)).newline();
builder.newline().newline();
}
}
private void reportOtherProblems(RenderLogger logger, HtmlBuilder builder) {
List<RenderProblem> messages = logger.getMessages();
if (messages != null && !messages.isEmpty()) {
Set<String> seenTags = Sets.newHashSet();
for (RenderProblem message : messages) {
String tag = message.getTag();
if (tag != null && seenTags.contains(tag)) {
continue;
}
seenTags.add(tag);
HighlightSeverity severity = message.getSeverity();
if (severity == HighlightSeverity.ERROR) {
builder.addIcon(HtmlBuilderHelper.getErrorIconPath());
} else if (severity == HighlightSeverity.WARNING) {
builder.addIcon(HtmlBuilderHelper.getWarningIconPath());
}
String html = message.getHtml();
builder.getStringBuilder().append(html);
builder.newlineIfNecessary();
Throwable throwable = message.getThrowable();
if (throwable != null) {
reportSandboxError(builder, throwable, false, true);
reportThrowable(builder, throwable, !html.isEmpty());
}
if (tag != null) {
if (LayoutLog.TAG_RESOURCES_FORMAT.equals(tag)) {
appendFlagValueSuggestions(builder, message);
}
int count = logger.getTagCount(tag);
if (count > 1) {
builder.add(" (").addHtml(Integer.toString(count)).add(" similar errors not shown)");
}
}
builder.newline();
}
}
}
private void appendFlagValueSuggestions(HtmlBuilder builder, RenderProblem message) {
Object clientData = message.getClientData();
if (!(clientData instanceof String[])) {
return;
}
String[] strings = (String[])clientData;
if (strings.length != 2) {
return;
}
RenderTask renderTask = myResult.getRenderTask();
if (renderTask == null) {
return;
}
IAndroidTarget target = renderTask.getConfiguration().getTarget();
if (target == null) {
return;
}
AndroidPlatform platform = renderTask.getPlatform();
if (platform == null) {
return;
}
AndroidTargetData targetData = platform.getSdkData().getTargetData(target);
AttributeDefinitions definitionLookup = targetData.getPublicAttrDefs(myResult.getFile().getProject());
final String attributeName = strings[0];
final String currentValue = strings[1];
if (definitionLookup == null) {
return;
}
AttributeDefinition definition = definitionLookup.getAttrDefByName(attributeName);
if (definition == null) {
return;
}
Set<AttributeFormat> formats = definition.getFormats();
if (formats.contains(AttributeFormat.Flag) || formats.contains(AttributeFormat.Enum)) {
String[] values = definition.getValues();
if (values.length > 0) {
builder.newline();
builder.addNbsps(4);
builder.add("Change ").add(currentValue).add(" to: ");
boolean first = true;
for (String value : values) {
if (first) {
first = false;
} else {
builder.add(", ");
}
builder.addLink(value, myLinkManager.createReplaceAttributeValueUrl(attributeName, currentValue, value));
}
}
}
}
/** Display the problem list encountered during a render */
private void reportThrowable(@NotNull HtmlBuilder builder, @NotNull final Throwable throwable, boolean hideIfIrrelevant) {
StackTraceElement[] frames = throwable.getStackTrace();
int end = -1;
boolean haveInterestingFrame = false;
for (int i = 0; i < frames.length; i++) {
StackTraceElement frame = frames[i];
if (isInterestingFrame(frame)) {
haveInterestingFrame = true;
}
String className = frame.getClassName();
if (className.equals(RENDER_SESSION_IMPL_FQCN)) {
end = i;
break;
}
}
if (end == -1 || !haveInterestingFrame) {
// Not a recognized stack trace range: just skip it
if (hideIfIrrelevant) {
if (RenderLogger.isLoggingAllErrors()) {
ShowExceptionFix detailsFix = new ShowExceptionFix(myResult.getModule().getProject(), throwable);
builder.addLink("Show Exception", myLinkManager.createRunnableLink(detailsFix));
}
return;
} else {
// List just the top frames
for (int i = 0; i < frames.length; i++) {
StackTraceElement frame = frames[i];
if (!isVisible(frame)) {
end = i;
if (end == 0) {
// Find end instead
for (int j = 0; j < frames.length; j++) {
frame = frames[j];
String className = frame.getClassName();
if (className.equals(RENDER_SESSION_IMPL_FQCN)) {
end = j;
break;
}
}
}
break;
}
}
}
}
builder.add(throwable.toString()).newline();
boolean wasHidden = false;
int indent = 2;
File platformSource = null;
boolean platformSourceExists = true;
for (int i = 0; i < end; i++) {
StackTraceElement frame = frames[i];
if (isHiddenFrame(frame)) {
wasHidden = true;
continue;
}
String className = frame.getClassName();
String methodName = frame.getMethodName();
builder.addNbsps(indent);
builder.add("at ").add(className).add(".").add(methodName);
String fileName = frame.getFileName();
if (fileName != null && !fileName.isEmpty()) {
int lineNumber = frame.getLineNumber();
String location = fileName + ':' + lineNumber;
if (isInterestingFrame(frame)) {
if (wasHidden) {
builder.addNbsps(indent);
builder.add(" ...").newline();
wasHidden = false;
}
String url = myLinkManager.createOpenStackUrl(className, methodName, fileName, lineNumber);
builder.add("(").addLink(location, url).add(")");
} else {
// Try to link to local documentation
String url = null;
if (isFramework(frame) && platformSourceExists) { // try to link to documentation, if available
if (platformSource == null) {
IAndroidTarget target = myResult.getRenderTask().getConfiguration().getTarget();
platformSource = AndroidSdkUtils.findPlatformSources(target);
platformSourceExists = platformSource != null;
}
if (platformSourceExists) {
File classFile = new File(platformSource, frame.getClassName().replace('.', File.separatorChar) + DOT_JAVA);
if (!classFile.exists()) {
// Probably an innerclass like foo.bar.Outer.Inner; the above would look for foo/bar/Outer/Inner.java; try
// again at foo/bar/
File parentFile = classFile.getParentFile();
classFile = new File(parentFile.getParentFile(), parentFile.getName() + DOT_JAVA);
if (!classFile.exists()) {
classFile = null; // in theory we should keep trying this repeatedly for more deeply nested inner classes
}
}
if (classFile != null) {
url = HtmlLinkManager.createFilePositionUrl(classFile, lineNumber, 0);
}
}
}
if (url != null) {
builder.add("(").addLink(location, url).add(")");
} else {
builder.add("(").add(location).add(")");
}
}
builder.newline();
}
}
builder.addLink("Copy stack to clipboard", myLinkManager.createRunnableLink(new Runnable() {
@Override
public void run() {
String text = Throwables.getStackTraceAsString(throwable);
try {
CopyPasteManager.getInstance().setContents(new StringSelection(text));
}
catch (Exception ignore) {
}
}
}));
}
private static boolean isHiddenFrame(StackTraceElement frame) {
String className = frame.getClassName();
return
className.startsWith("sun.reflect.") ||
className.equals("android.view.BridgeInflater") ||
className.startsWith("com.android.tools.") ||
className.startsWith("org.jetbrains.");
}
private static boolean isInterestingFrame(StackTraceElement frame) {
String className = frame.getClassName();
return !(className.startsWith("android.") //$NON-NLS-1$
|| className.startsWith("org.jetbrains.") //$NON-NLS-1$
|| className.startsWith("com.android.") //$NON-NLS-1$
|| className.startsWith("java.") //$NON-NLS-1$
|| className.startsWith("javax.") //$NON-NLS-1$
|| className.startsWith("sun.")); //$NON-NLS-1$
}
private static boolean isFramework(StackTraceElement frame) {
String className = frame.getClassName();
return (className.startsWith("android.") //$NON-NLS-1$
|| className.startsWith("java.") //$NON-NLS-1$
|| className.startsWith("javax.") //$NON-NLS-1$
|| className.startsWith("sun.")); //$NON-NLS-1$
}
private static boolean isVisible(StackTraceElement frame) {
String className = frame.getClassName();
return !(isFramework(frame) || className.startsWith("sun.")); //$NON-NLS-1$
}
private void reportMissingSize(@NotNull HtmlBuilder builder,
@NotNull RenderLogger logger,
@NotNull String fill,
@NotNull XmlTag tag,
@NotNull String id,
@NotNull String attribute) {
Project project = logger.getModule().getProject();
String wrapUrl = myLinkManager.createCommandLink(new SetAttributeFix(project, tag, attribute, ANDROID_URI, VALUE_WRAP_CONTENT));
String fillUrl = myLinkManager.createCommandLink(new SetAttributeFix(project, tag, attribute, ANDROID_URI, fill));
builder.add(String.format("%1$s does not set the required %2$s attribute: ", id, attribute));
builder.newline();
builder.addNbsps(4);
builder.addLink("Set to wrap_content", wrapUrl);
builder.add(", ");
builder.addLink("Set to " + fill, fillUrl);
builder.newline();
}
@SuppressWarnings({"HardCodedStringLiteral"})
private void showEmpty() {
try {
myHTMLViewer.read(new StringReader("<html><body></body></html>"), null);
}
catch (IOException e) {
// can't be
}
}
private void reportInstantiationProblems(@NotNull final RenderLogger logger, @NotNull HtmlBuilder builder) {
Map<String, Throwable> classesWithIncorrectFormat = logger.getClassesWithIncorrectFormat();
if (classesWithIncorrectFormat != null && !classesWithIncorrectFormat.isEmpty()) {
builder.add("Preview might be incorrect: unsupported class version.").newline();
builder.addIcon(HtmlBuilderHelper.getTipIconPath());
builder.add("Tip: ");
builder.add("You need to run the IDE with the highest JDK version that you are compiling custom views with. ");
int highest = ClassConverter.findHighestMajorVersion(classesWithIncorrectFormat.values());
if (highest > 0 && highest > ClassConverter.getCurrentClassVersion()) {
builder.add("One or more views have been compiled with JDK ");
String required = ClassConverter.classVersionToJdk(highest);
builder.add(required);
builder.add(", but you are running the IDE on JDK ");
builder.add(ClassConverter.getCurrentJdkVersion());
builder.add(". ");
} else {
builder.add("For example, if you are compiling with sourceCompatibility 1.7, you must run the IDE with JDK 1.7. ");
}
builder.add("Running on a higher JDK is necessary such that these classes can be run in the layout renderer. " +
"(Or, extract your custom views into a library which you compile with a lower JDK version.)");
builder.newline().newline();
builder.addLink("If you have just accidentally built your code with a later JDK, try to ", "build", " the project.",
myLinkManager.createCompileModuleUrl());
builder.newline().newline();
builder.add("Classes with incompatible format:");
builder.beginList();
List<String> names = Lists.newArrayList(classesWithIncorrectFormat.keySet());
Collections.sort(names);
for (String className : names) {
builder.listItem();
builder.add(className);
Throwable throwable = classesWithIncorrectFormat.get(className);
if (throwable instanceof InconvertibleClassError) {
InconvertibleClassError error = (InconvertibleClassError)throwable;
builder.add(" (Compiled with ");
builder.add(ClassConverter.classVersionToJdk(error.getMajor()));
builder.add(")");
}
}
builder.endList();
Module module = logger.getModule();
final List<Module> problemModules = getProblemModules(module);
if (!problemModules.isEmpty()) {
builder.add("The following modules are built with incompatible JDK:").newline();
for (Iterator<Module> it = problemModules.iterator(); it.hasNext(); ) {
Module problemModule = it.next();
builder.add(problemModule.getName());
if (it.hasNext()) {
builder.add(", ");
}
}
builder.newline();
}
AndroidFacet facet = AndroidFacet.getInstance(logger.getModule());
if (facet != null && !facet.requiresAndroidModel()) {
Project project = logger.getModule().getProject();
builder.addLink("Rebuild project with '-target 1.6'", myLinkManager.createRunnableLink(new RebuildWith16Fix(project)));
builder.newline();
if (!problemModules.isEmpty()) {
builder.addLink("Change Java SDK to 1.5/1.6", myLinkManager.createRunnableLink(new SwitchTo16Fix(project, problemModules)));
builder.newline();
}
}
}
}
// Code copied from the old RenderUtil
private static void askAndRebuild(Project project) {
final int r = Messages.showYesNoDialog(project, "You have to rebuild project to see the fixed preview. Would you like to do it?",
"Rebuild Project", Messages.getQuestionIcon());
if (r == Messages.YES) {
CompilerManager.getInstance(project).rebuild(null);
}
}
@NotNull
private static Set<String> getSdkNamesFromModules(@NotNull Collection<Module> modules) {
final Set<String> result = new HashSet<String>();
for (Module module : modules) {
final Sdk sdk = ModuleRootManager.getInstance(module).getSdk();
if (sdk != null) {
result.add(sdk.getName());
}
}
return result;
}
@NotNull
private static List<Module> getProblemModules(@NotNull Module root) {
final List<Module> result = new ArrayList<Module>();
collectProblemModules(root, new HashSet<Module>(), result);
return result;
}
private static void collectProblemModules(@NotNull Module module, @NotNull Set<Module> visited, @NotNull Collection<Module> result) {
if (!visited.add(module)) {
return;
}
if (isBuiltByJdk7OrHigher(module)) {
result.add(module);
}
for (Module depModule : ModuleRootManager.getInstance(module).getDependencies(false)) {
collectProblemModules(depModule, visited, result);
}
}
private static boolean isBuiltByJdk7OrHigher(@NotNull Module module) {
Sdk sdk = ModuleRootManager.getInstance(module).getSdk();
if (sdk == null) {
return false;
}
if (isAndroidSdk(sdk)) {
AndroidSdkAdditionalData data = getAndroidSdkAdditionalData(sdk);
if (data != null) {
Sdk jdk = data.getJavaSdk();
if (jdk != null) {
sdk = jdk;
}
}
}
return sdk.getSdkType() instanceof JavaSdk &&
JavaSdk.getInstance().isOfVersionOrHigher(sdk, JavaSdkVersion.JDK_1_7);
}
private static class RebuildWith16Fix implements Runnable {
private final Project myProject;
private RebuildWith16Fix(Project project) {
myProject = project;
}
@Override
public void run() {
final JpsJavaCompilerOptions settings = JavacConfiguration.getOptions(myProject, JavacConfiguration.class);
if (settings.ADDITIONAL_OPTIONS_STRING.length() > 0) {
settings.ADDITIONAL_OPTIONS_STRING += ' ';
}
settings.ADDITIONAL_OPTIONS_STRING += "-target 1.6";
CompilerManager.getInstance(myProject).rebuild(null);
}
}
private static class SwitchTo16Fix implements Runnable {
private final Project myProject;
final List<Module> myProblemModules;
private SwitchTo16Fix(Project project, List<Module> problemModules) {
myProject = project;
myProblemModules = problemModules;
}
@Override
public void run() {
final Set<String> sdkNames = getSdkNamesFromModules(myProblemModules);
if (sdkNames.size() == 1) {
final Sdk sdk = ProjectJdkTable.getInstance().findJdk(sdkNames.iterator().next());
if (sdk != null && sdk.getSdkType() instanceof AndroidSdkType) {
final ProjectStructureConfigurable config = ProjectStructureConfigurable.getInstance(myProject);
if (ShowSettingsUtil.getInstance().editConfigurable(myProject, config, new Runnable() {
@Override
public void run() {
config.select(sdk, true);
}
})) {
askAndRebuild(myProject);
}
return;
}
}
final String moduleToSelect = myProblemModules.size() > 0
? myProblemModules.iterator().next().getName()
: null;
if (ModulesConfigurator.showDialog(myProject, moduleToSelect, ClasspathEditor.NAME)) {
askAndRebuild(myProject);
}
}
}
public static class HtmlBuilderHelper {
@Nullable
private static String getIconPath(String relative) {
// TODO: Find a way to do this more efficiently; not referencing assets but the corresponding
// AllIcons constants, and loading them into HTML class loader contexts?
URL resource = AllIcons.class.getClassLoader().getResource(relative);
try {
return (resource != null) ? resource.toURI().toURL().toExternalForm() : null;
}
catch (MalformedURLException e) {
return null;
}
catch (URISyntaxException e) {
return null;
}
}
@Nullable
public static String getCloseIconPath() {
return getIconPath("/actions/closeNew.png");
}
@Nullable
public static String getTipIconPath() {
return getIconPath("/actions/createFromUsage.png");
}
@Nullable
public static String getWarningIconPath() {
return getIconPath("/actions/warning.png");
}
@Nullable
public static String getErrorIconPath() {
return getIconPath("/actions/error.png");
}
public static String getHeaderFontColor() {
// See om.intellij.codeInspection.HtmlComposer.appendHeading
// (which operates on StringBuffers)
return UIUtil.isUnderDarcula() ? "#A5C25C" : "#005555";
}
}
}