Initial support for icon description tooltips (IDEA-237109)

GitOrigin-RevId: 137eb5c9b12d890b4f446b106523207f1b54e96a
This commit is contained in:
Dmitry Jemerov
2020-05-06 15:18:02 +00:00
committed by intellij-monorepo-bot
parent 18c89ad0f9
commit 5403cb8972
20 changed files with 354 additions and 14 deletions
@@ -19,6 +19,7 @@ import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.impl.file.PsiDirectoryFactory;
import com.intellij.ui.IconManager;
import com.intellij.ui.IconWithToolTip;
import com.intellij.util.PlatformIcons;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -53,7 +54,7 @@ final class JavaDirectoryIconProvider extends IconProvider implements DumbAware
symbolIcon = AllIcons.Nodes.Module;
}
else if (isValidPackage(psiDirectory)) {
symbolIcon = PlatformIcons.PACKAGE_ICON;
symbolIcon = IconWithToolTip.tooltipOnlyIfComposite(PlatformIcons.PACKAGE_ICON);
}
else if (!Registry.is("ide.hide.excluded.files") && ProjectRootManager.getInstance(project).getFileIndex().isExcluded(vFile)) {
symbolIcon = AllIcons.Modules.ExcludeRoot;
@@ -12,6 +12,7 @@ import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.util.*;
import com.intellij.ui.IconManager;
import com.intellij.ui.IconWithToolTip;
import com.intellij.ui.icons.RowIcon;
import com.intellij.util.BitUtil;
import com.intellij.util.PlatformIcons;
@@ -142,7 +143,7 @@ public final class ElementPresentationUtil implements PlatformIcons {
private static final TIntObjectHashMap<Icon> BASE_ICON = new TIntObjectHashMap<>(20);
static {
BASE_ICON.put(CLASS_KIND_CLASS, CLASS_ICON);
BASE_ICON.put(CLASS_KIND_CLASS, IconWithToolTip.tooltipOnlyIfComposite(CLASS_ICON));
BASE_ICON.put(CLASS_KIND_CLASS | FLAGS_ABSTRACT, ABSTRACT_CLASS_ICON);
BASE_ICON.put(CLASS_KIND_ANNOTATION, ANNOTATION_TYPE_ICON);
BASE_ICON.put(CLASS_KIND_ANNOTATION | FLAGS_ABSTRACT, ANNOTATION_TYPE_ICON);
@@ -154,9 +155,9 @@ public final class ElementPresentationUtil implements PlatformIcons {
BASE_ICON.put(CLASS_KIND_ENUM | FLAGS_ABSTRACT, ENUM_ICON);
BASE_ICON.put(CLASS_KIND_EXCEPTION, EXCEPTION_CLASS_ICON);
BASE_ICON.put(CLASS_KIND_EXCEPTION | FLAGS_ABSTRACT, AllIcons.Nodes.AbstractException);
BASE_ICON.put(CLASS_KIND_INTERFACE, INTERFACE_ICON);
BASE_ICON.put(CLASS_KIND_INTERFACE | FLAGS_ABSTRACT, INTERFACE_ICON);
BASE_ICON.put(CLASS_KIND_JUNIT_TEST, CLASS_ICON);
BASE_ICON.put(CLASS_KIND_INTERFACE, IconWithToolTip.tooltipOnlyIfComposite(INTERFACE_ICON));
BASE_ICON.put(CLASS_KIND_INTERFACE | FLAGS_ABSTRACT, IconWithToolTip.tooltipOnlyIfComposite(INTERFACE_ICON));
BASE_ICON.put(CLASS_KIND_JUNIT_TEST, IconWithToolTip.tooltipOnlyIfComposite(CLASS_ICON));
BASE_ICON.put(CLASS_KIND_JUNIT_TEST | FLAGS_ABSTRACT, ABSTRACT_CLASS_ICON);
BASE_ICON.put(CLASS_KIND_RECORD, RECORD_ICON);
BASE_ICON.put(CLASS_KIND_RUNNABLE, CLASS_ICON);
@@ -28,6 +28,7 @@ import com.intellij.psi.search.PsiElementProcessor;
import com.intellij.psi.search.PsiFileSystemItemProcessor;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.testFramework.LightVirtualFile;
import com.intellij.ui.IconWithToolTip;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.PlatformIcons;
@@ -527,7 +528,7 @@ public class PsiDirectoryImpl extends PsiElementBase implements PsiDirectory, Qu
@Override
protected Icon getElementIcon(final int flags) {
return PlatformIcons.FOLDER_ICON;
return IconWithToolTip.tooltipOnlyIfComposite(PlatformIcons.FOLDER_ICON);
}
@Override
+49 -1
View File
@@ -1,9 +1,15 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.ui;
import com.intellij.AbstractBundle;
import com.intellij.DynamicBundle;
import com.intellij.ide.IconLayerProvider;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.IconLoader;
import com.intellij.openapi.util.Iconable;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.BitUtil;
import com.intellij.util.IconUtil;
import com.intellij.util.SmartList;
@@ -14,16 +20,20 @@ import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.awt.*;
import java.util.List;
import java.util.ResourceBundle;
import java.util.function.Function;
import java.util.function.Supplier;
public final class CoreIconManager implements IconManager {
private static final List<IconLayer> ourIconLayers = ContainerUtil.createLockFreeCopyOnWriteList();
private static final int FLAGS_LOCKED = 0x800;
private static final Logger LOG = Logger.getInstance(CoreIconManager.class);
@NotNull
@Override
public Icon getIcon(@NotNull String path, @NotNull Class aClass) {
return IconLoader.getIcon(path, aClass);
Icon icon = IconLoader.getIcon(path, aClass);
return IconWithToolTip.create(icon, new IconDescriptionLoader(path));
}
@NotNull
@@ -125,4 +135,42 @@ public final class CoreIconManager implements IconManager {
this.icon = icon;
}
}
private static class IconDescriptionLoader implements Supplier<String> {
private final String myPath;
private String myResult;
private boolean myCalculated;
private IconDescriptionLoader(String path) {
myPath = path;
}
@Override
public String get() {
if (!myCalculated) {
myResult = findIconDescription();
myCalculated = true;
}
return myResult;
}
private String findIconDescription() {
String basePath = StringUtil.trimStart(StringUtil.trimEnd(myPath, ".svg"), "/");
String key = "icon." + basePath.replace('/', '.') + ".tooltip";
Ref<String> result = new Ref<>();
IconDescriptionBundleEP.EP_NAME.processWithPluginDescriptor((ep, descriptor) -> {
ClassLoader classLoader = descriptor == null ? null : descriptor.getPluginClassLoader();
if (classLoader == null) classLoader = getClass().getClassLoader();
ResourceBundle bundle = DynamicBundle.INSTANCE.getResourceBundle(ep.qualifiedName, classLoader);
String description = AbstractBundle.messageOrNull(bundle, key);
if (description != null) {
result.set(description);
}
});
if (result.get() == null && Registry.is("ide.icon.tooltips.trace.missing")) {
LOG.info("Icon tooltip requested but not found for " + myPath);
}
return result.get();
}
}
}
@@ -0,0 +1,12 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.ui;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.util.xmlb.annotations.Attribute;
public class IconDescriptionBundleEP {
public static final ExtensionPointName<IconDescriptionBundleEP> EP_NAME = ExtensionPointName.create("com.intellij.iconDescriptionBundle");
@Attribute("qualifiedName")
public String qualifiedName;
}
@@ -0,0 +1,29 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.ui;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.util.function.Supplier;
/**
* Icon which supports providing a tooltip.
*/
public interface IconWithToolTip extends Icon {
/**
* Returns the tooltip for the icon.
* @param composite if true, this tooltip will be combined with other tooltips (from other layers of a layered icon or parts of a row icon).
* For some icons, it only makes sense to show a tooltip if the icon is composite.
* @return
*/
@Nullable
String getToolTip(boolean composite);
static IconWithToolTip create(Icon icon, Supplier<String> tooltip) {
return new IconWrapperWithToolTip(icon, tooltip);
}
static IconWithToolTip tooltipOnlyIfComposite(Icon icon) {
return new IconWrapperWithToolTipComposite(icon);
}
}
@@ -0,0 +1,49 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.ui;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.awt.*;
import java.util.function.Supplier;
public class IconWrapperWithToolTip implements IconWithToolTip, RetrievableIcon {
private final Icon myIcon;
private final Supplier<String> myToolTip;
public IconWrapperWithToolTip(Icon icon, Supplier<String> toolTip) {
myIcon = icon;
myToolTip = toolTip;
}
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
myIcon.paintIcon(c, g, x, y);
}
@Override
public int getIconWidth() {
return myIcon.getIconWidth();
}
@Override
public int getIconHeight() {
return myIcon.getIconHeight();
}
@Override
public String getToolTip(boolean composite) {
return myToolTip.get();
}
@Override
public String toString() {
return "IconWrapperWithTooltip:" + myIcon;
}
@Override
public @NotNull Icon retrieveIcon() {
return myIcon;
}
}
@@ -0,0 +1,47 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.ui;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
public class IconWrapperWithToolTipComposite implements IconWithToolTip, RetrievableIcon {
private final Icon myIcon;
public IconWrapperWithToolTipComposite(Icon icon) {
myIcon = icon;
}
@Override
public @Nullable String getToolTip(boolean composite) {
if (!composite) return null;
return myIcon instanceof IconWithToolTip ? ((IconWithToolTip)myIcon).getToolTip(true) : null;
}
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
myIcon.paintIcon(c, g, x, y);
}
@Override
public int getIconWidth() {
return myIcon.getIconWidth();
}
@Override
public int getIconHeight() {
return myIcon.getIconHeight();
}
@Override
public @NotNull Icon retrieveIcon() {
return myIcon;
}
@Override
public String toString() {
return "IconWrapperWithTooltipComposite:" + myIcon.toString();
}
}
+46 -1
View File
@@ -10,6 +10,7 @@ import com.intellij.util.IconUtil;
import com.intellij.util.ui.JBCachingScalableIcon;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
@@ -18,7 +19,7 @@ import java.util.Arrays;
import static com.intellij.ui.scale.ScaleType.OBJ_SCALE;
import static com.intellij.ui.scale.ScaleType.USR_SCALE;
public class LayeredIcon extends JBCachingScalableIcon<LayeredIcon> implements DarkIconProvider, CompositeIcon {
public class LayeredIcon extends JBCachingScalableIcon<LayeredIcon> implements DarkIconProvider, CompositeIcon, IconWithToolTip {
private static final Logger LOG = Logger.getInstance("#com.intellij.ui.LayeredIcon");
private final Icon[] myIcons;
private Icon[] myScaledIcons;
@@ -309,4 +310,48 @@ public class LayeredIcon extends JBCachingScalableIcon<LayeredIcon> implements D
public String toString() {
return "Layered icon "+getIconWidth()+"x"+getIconHeight()+". myIcons=" + Arrays.asList(myIcons);
}
@Override
public String getToolTip(boolean composite) {
return combineIconTooltips(myIcons);
}
@Nullable
static String combineIconTooltips(Icon[] icons) {
// If a layered icon contains only a single non-null layer and other layers are null, its tooltip is not a composite one.
Icon singleIcon = null;
for (Icon icon : icons) {
if (icon != null) {
if (singleIcon != null) {
return buildCompositeTooltip(icons);
}
singleIcon = icon;
}
}
if (singleIcon != null) {
return singleIcon instanceof IconWithToolTip ? ((IconWithToolTip) singleIcon).getToolTip(false) : null;
}
return null;
}
@Nullable
private static String buildCompositeTooltip(Icon[] icons) {
StringBuilder result = null;
for (int i = 0; i < icons.length; i++) {
// first layer is the actual object (noun), other layers are modifiers (adjectives), so put first object in last position
Icon icon = i == icons.length - 1 ? icons[0] : icons[i + 1];
if (icon instanceof IconWithToolTip) {
String toolTip = ((IconWithToolTip)icon).getToolTip(true);
if (toolTip != null) {
if (result == null) {
result = new StringBuilder(toolTip);
}
else {
result.append(" ").append(toolTip);
}
}
}
}
return result != null ? result.toString() : null;
}
}
+6 -1
View File
@@ -17,7 +17,7 @@ import java.util.List;
import static com.intellij.ui.scale.ScaleType.OBJ_SCALE;
import static java.lang.Math.ceil;
public class RowIcon extends JBCachingScalableIcon<RowIcon> implements com.intellij.ui.icons.RowIcon {
public class RowIcon extends JBCachingScalableIcon<RowIcon> implements com.intellij.ui.icons.RowIcon, IconWithToolTip {
private final com.intellij.ui.icons.RowIcon.Alignment myAlignment;
private int myWidth;
@@ -202,4 +202,9 @@ public class RowIcon extends JBCachingScalableIcon<RowIcon> implements com.intel
public String toString() {
return "Row icon. myIcons=" + Arrays.asList(myIcons);
}
@Override
public String getToolTip(boolean composite) {
return LayeredIcon.combineIconTooltips(myIcons);
}
}
+1 -1
View File
@@ -188,7 +188,7 @@ public class IconUtil {
}
FileType fileType = vFile.getFileType();
if (vFile.isDirectory() && !(fileType instanceof DirectoryFileType)) {
return PlatformIcons.FOLDER_ICON;
return IconWithToolTip.tooltipOnlyIfComposite(PlatformIcons.FOLDER_ICON);
}
return fileType.getIcon();
}
@@ -38,6 +38,7 @@ import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.reference.SoftReference;
import com.intellij.ui.ColorUtil;
import com.intellij.ui.IconWithToolTip;
import com.intellij.ui.JBColor;
import com.intellij.ui.RetrievableIcon;
import com.intellij.util.IconUtil;
@@ -484,7 +485,7 @@ public final class Bookmark implements Navigatable, Comparable<Bookmark> {
}
}
private static class MyCheckedIcon extends JBCachingScalableIcon<MyCheckedIcon> implements RetrievableIcon {
private static class MyCheckedIcon extends JBCachingScalableIcon<MyCheckedIcon> implements RetrievableIcon, IconWithToolTip {
@NotNull
@Override
public Icon retrieveIcon() {
@@ -515,6 +516,11 @@ public final class Bookmark implements Navigatable, Comparable<Bookmark> {
public MyCheckedIcon copy() {
return new MyCheckedIcon();
}
@Override
public String getToolTip(boolean composite) {
return IdeBundle.message("tooltip.bookmarked");
}
}
private static boolean darkBackground() {
@@ -36,7 +36,7 @@ import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
public final class DeferredIconImpl<T> extends JBCachingScalableIcon<DeferredIconImpl<T>> implements DeferredIcon, RetrievableIcon {
public final class DeferredIconImpl<T> extends JBCachingScalableIcon<DeferredIconImpl<T>> implements DeferredIcon, RetrievableIcon, IconWithToolTip {
private static final Logger LOG = Logger.getInstance(DeferredIconImpl.class);
private static final int MIN_AUTO_UPDATE_MILLIS = 950;
private static final RepaintScheduler ourRepaintScheduler = new RepaintScheduler();
@@ -320,6 +320,14 @@ public final class DeferredIconImpl<T> extends JBCachingScalableIcon<DeferredIco
return myScaledDelegateIcon.getIconHeight();
}
@Override
public String getToolTip(boolean composite) {
if (myScaledDelegateIcon instanceof IconWithToolTip) {
return ((IconWithToolTip) myScaledDelegateIcon).getToolTip(composite);
}
return null;
}
public boolean isDone() {
if (myAutoUpdatable && myDone && myLastCalcTime > 0 && System.currentTimeMillis() - myLastCalcTime > Math.max(MIN_AUTO_UPDATE_MILLIS, 10 * myLastTimeSpent)) {
myDone = false;
@@ -2082,4 +2082,48 @@ button.convert.anyway=Convert anyway
button.reload.anyway=Reload anyway
dialog.title.file.0.can.t.be.reloaded=File ''{0}'' can''t be reloaded in the ''{1}'' encoding.{2}
dialog.title.file.0.most.likely.isn.t.stored=File ''{0}'' most likely isn''t stored in the ''{1}'' encoding.{2}
dumb.service.indexing.paused.due.to=Indexing paused due to {0}
dumb.service.indexing.paused.due.to=Indexing paused due to {0}
tooltip.bookmarked=bookmarked
icon.fileTypes.archive.tooltip=archive file
icon.modules.excludedGeneratedRoot.tooltip=excluded generated sources root
icon.modules.excludeRoot.tooltip=excluded directory
icon.modules.generatedFolder.tooltip=generated folder
icon.modules.generatedSourceRoot.tooltip=generated sources root
icon.modules.generatedTestRoot.tooltip=generated test sources root
icon.modules.output.tooltip=output root
icon.modules.resourcesRoot.tooltip=resources root
icon.modules.sourceRoot.tooltip=sources root
icon.modules.testResourcesRoot.tooltip=test resources root
icon.modules.testRoot.tooltip=test sources root
icon.modules.unloadedModule.tooltip=unloaded module
icon.nodes.abstractClass.tooltip=abstract class
icon.nodes.abstractException.tooltip=abstract exception
icon.nodes.abstractMethod.tooltip=abstract method
icon.nodes.annotationtype.tooltip=annotation type
icon.nodes.anonymousClass.tooltip=anonymous class
icon.nodes.c_plocal.tooltip=package-local
icon.nodes.c_private.tooltip=private
icon.nodes.c_protected.tooltip=protected
icon.nodes.c_public.tooltip=public
icon.nodes.class.tooltip=class
icon.nodes.classInitializer.tooltip=class initializer
icon.nodes.enum.tooltip=enum
icon.nodes.excludedFromCompile.tooltip=excluded from compilation
icon.nodes.field.tooltip=field
icon.nodes.finalMark.tooltip=final
icon.nodes.folder.tooltip=folder
icon.nodes.ideaModule.tooltip=IntelliJ module file
icon.nodes.interface.tooltip=interface
icon.nodes.junitTestMark.tooltip=unit test
icon.nodes.locked.tooltip=read-only
icon.nodes.method.tooltip=method
icon.nodes.Module.tooltip=module
icon.nodes.moduleGroup.tooltip=module group
icon.nodes.package.tooltip=package
icon.nodes.ppJdk.tooltip=JDK
icon.nodes.ppLib.tooltip=external library
icon.nodes.runnableMark.tooltip=runnable
icon.nodes.staticMark.tooltip=static
@@ -8,6 +8,7 @@ import com.intellij.openapi.ui.GraphicsConfig;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.paint.EffectPainter;
import com.intellij.ui.scale.JBUIScale;
@@ -24,6 +25,7 @@ import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.tree.TreeCellRenderer;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.font.FontRenderContext;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
@@ -1078,6 +1080,17 @@ public class SimpleColoredComponent extends JComponent implements Accessible, Co
}
}
@Override
public String getToolTipText(MouseEvent event) {
if (myIcon instanceof IconWithToolTip && findFragmentAt(event.getX()) == FRAGMENT_ICON && Registry.is("ide.icon.tooltips")) {
String iconToolTip = ((IconWithToolTip)myIcon).getToolTip(false);
if (iconToolTip != null) {
return iconToolTip;
}
}
return super.getToolTipText(event);
}
@Override
public AccessibleContext getAccessibleContext() {
if (accessibleContext == null) {
@@ -1276,6 +1276,8 @@
<codeInsight.lineMarkerProvider implementationClass="com.intellij.codeInsight.documentation.render.DocRenderDummyLineMarkerProvider"/>
<psi.treeChangeListener implementation="com.intellij.openapi.fileEditor.impl.FileEditorPsiTreeChangeListener"/>
<iconDescriptionBundle qualifiedName="messages.IdeBundle"/>
</extensions>
<applicationListeners>
<listener class="com.intellij.ide.script.IdeStartupScripts" topic="com.intellij.openapi.project.ProjectManagerListener"/>
@@ -380,5 +380,6 @@
<with attribute="implementationClass" implements="com.intellij.openapi.options.OptionEditorProvider"/>
</extensionPoint>
<extensionPoint name="iconDescriptionBundle" beanClass="com.intellij.ui.IconDescriptionBundleEP" dynamic="true"/>
</extensionPoints>
</idea-plugin>
@@ -281,7 +281,7 @@ public class IconUtilTest extends HeavyPlatformTestCase {
UIUtil.dispatchAllInvocationEvents();
List<Icon> icons = autopsyIconsFrom(icon);
assertOneElement(ContainerUtil.filter(icons, ic -> ic == PlatformIcons.LOCKED_ICON));
assertOneElement(ContainerUtil.filter(icons, ic -> ic == IconTestUtil.unwrapRetrievableIcon(PlatformIcons.LOCKED_ICON)));
}
finally {
WriteCommandAction.runWriteCommandAction(getProject(),
@@ -0,0 +1,22 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.ui;
import com.intellij.openapi.util.IconLoader;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
public class IconTestUtil {
@Nullable
public static String getIconPath(Icon icon) {
icon = unwrapRetrievableIcon(icon);
return ((IconLoader.CachedImageIcon)icon).getOriginalPath();
}
public static Icon unwrapRetrievableIcon(Icon icon) {
while (icon instanceof RetrievableIcon) {
icon = ((RetrievableIcon)icon).retrieveIcon();
}
return icon;
}
}
@@ -1805,4 +1805,10 @@ indexing.progress.indicator.power.description=Indexing progress indicator speed-
ide.background.tasks=true
ide.background.tasks.description=Start backgroundable tasks in background without showing modal dialog with progress bar
ide.icon.tooltips=true
ide.icon.tooltips.description=Show tooltips on icons explaining their meaning
ide.icon.tooltips.trace.missing=false
ide.icon.tooltips.trace.missing.description=Log a message when an icon tooltip is requested but isn't available
# TODO please use EP com.intellij.registryKey for plugin/product specific keys