From 8d39338e2a243b03deefd27a7fa6a1da621be04f Mon Sep 17 00:00:00 2001 From: Eirik Bakke Date: Sat, 2 Dec 2023 18:41:33 -0500 Subject: [PATCH] Adjust the Icon implementation in ImageUtilities to make SVG/HiDPI icons work in the MacOS menu bar. --- platform/openide.util.ui/manifest.mf | 1 - .../nbproject/project.properties | 3 +- .../src/org/openide/util/ImageUtilities.java | 87 +++++++++++++--- .../src/org/openide/util/UtilitiesTest.java | 98 +------------------ 4 files changed, 76 insertions(+), 113 deletions(-) diff --git a/platform/openide.util.ui/manifest.mf b/platform/openide.util.ui/manifest.mf index 1d8866d38e0a..a2a1c97ac147 100644 --- a/platform/openide.util.ui/manifest.mf +++ b/platform/openide.util.ui/manifest.mf @@ -2,4 +2,3 @@ Manifest-Version: 1.0 OpenIDE-Module: org.openide.util.ui OpenIDE-Module-Localizing-Bundle: org/openide/util/Bundle.properties OpenIDE-Module-Specification-Version: 9.34 - diff --git a/platform/openide.util.ui/nbproject/project.properties b/platform/openide.util.ui/nbproject/project.properties index 5ba993f13de2..d56fbb7147a2 100644 --- a/platform/openide.util.ui/nbproject/project.properties +++ b/platform/openide.util.ui/nbproject/project.properties @@ -16,8 +16,7 @@ # under the License. javac.compilerargs=-Xlint -Xlint:-serial -Xlint:-processing -javac.source=1.8 -javac.target=1.8 +javac.release=11 module.jar.dir=lib diff --git a/platform/openide.util.ui/src/org/openide/util/ImageUtilities.java b/platform/openide.util.ui/src/org/openide/util/ImageUtilities.java index d66dd82b6334..526f659594a8 100644 --- a/platform/openide.util.ui/src/org/openide/util/ImageUtilities.java +++ b/platform/openide.util.ui/src/org/openide/util/ImageUtilities.java @@ -34,6 +34,7 @@ import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.ImageObserver; +import java.awt.image.MultiResolutionImage; import java.awt.image.RGBImageFilter; import java.awt.image.WritableRaster; import java.io.IOException; @@ -46,6 +47,7 @@ import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -322,10 +324,9 @@ public static final Image mergeImages(Image image1, Image image2, int x, int y) * @return icon corresponding icon */ public static final Icon image2Icon(Image image) { - /* Make sure to always return a ToolTipImage, to take advantage of its rendering tweaks for - HiDPI screens. */ - return (image instanceof ToolTipImage) + ToolTipImage ret = (image instanceof ToolTipImage) ? (ToolTipImage) image : assignToolTipToImageInternal(image, ""); + return ret.asImageIconIfRequiredForRetina(); } /** @@ -376,8 +377,9 @@ methods in this class may be called from any thread, while JLabel's methods and // so let's try second most used one type, it satisfies AbstractButton, JCheckbox. Not all cases are // covered, however. icon.paintIcon(dummyIconComponentButton, g, 0, 0); + } finally { + g.dispose(); } - g.dispose(); return image; } @@ -1049,17 +1051,12 @@ private static final class IconImageIcon extends ImageIcon { it volatile instead, to be completely sure that the class is still thread-safe. */ private volatile Icon delegate; - private IconImageIcon(Icon delegate) { - super(icon2Image(delegate)); + IconImageIcon(ToolTipImage delegate) { + super(delegate); Parameters.notNull("delegate", delegate); this.delegate = delegate; } - private static ImageIcon create(Icon delegate) { - return (delegate instanceof ImageIcon) - ? (ImageIcon) delegate : new IconImageIcon(delegate); - } - @Override public synchronized void paintIcon(Component c, Graphics g, int x, int y) { delegate.paintIcon(c, g, x, y); @@ -1089,9 +1086,15 @@ private void readObjectNoData() throws ObjectStreamException { } /** - * Image with tool tip text (for icons with badges) + * Image with tool tip text (for icons with badges). + * + *

On MacOS, HiDPI (Retina) support in JMenuItem.setIcon(Icon) requires the Icon argument to + * be an instance of ImageIcon wrapping a MultiResolutionImage (see + * com.apple.laf.ScreenMenuIcon.setIcon, com.apple.laf.AquaIcon.getImageForIcon, and + * sun.lwawt.macosx.CImage.Creator.createFromImage). Thus we have this class implement + * MultiResolutionImage, and use asImageIcon when needed via asImageIconIfRequiredForRetina. */ - private static class ToolTipImage extends BufferedImage implements Icon { + private static class ToolTipImage extends BufferedImage implements Icon, MultiResolutionImage { final String toolTipText; // May be null. final Icon delegateIcon; @@ -1099,6 +1102,8 @@ private static class ToolTipImage extends BufferedImage implements Icon { final URL url; // May be null. ImageIcon imageIconVersion; + // May be null. + volatile BufferedImage doubleSizeVariant; public static ToolTipImage createNew(String toolTipText, Image image, URL url) { ImageUtilities.ensureLoaded(image); @@ -1137,9 +1142,16 @@ public ToolTipImage( } public synchronized ImageIcon asImageIcon() { - if (imageIconVersion == null) - imageIconVersion = IconImageIcon.create(this); - return imageIconVersion; + if (imageIconVersion == null) { + imageIconVersion = new IconImageIcon(this); + } + return imageIconVersion; + } + + public Icon asImageIconIfRequiredForRetina() { + /* We could choose to do this only on MacOS, but doing it on all platforms will lower + the chance of undetected platform-specific bugs. */ + return delegateIcon != null ? asImageIcon() : this; } /** @@ -1237,6 +1249,49 @@ Image.UndefinedProperty rather than null (see Javadoc spec for this method), but } return super.getProperty(name, observer); } + + private Image getDoubleSizeVariant() { + if (delegateIcon == null) { + return null; + } + BufferedImage ret = doubleSizeVariant; + if (ret == null) { + int SCALE = 2; + ColorModel model = getColorModel(); + int w = delegateIcon.getIconWidth() * SCALE; + int h = delegateIcon.getIconHeight() * SCALE; + ret = new BufferedImage( + model, + model.createCompatibleWritableRaster(w, h), + model.isAlphaPremultiplied(), null); + Graphics g = ret.createGraphics(); + try { + ((Graphics2D) g).transform(AffineTransform.getScaleInstance(SCALE, SCALE)); + delegateIcon.paintIcon(dummyIconComponentLabel, g, 0, 0); + } finally { + g.dispose(); + } + doubleSizeVariant = ret; + } + return ret; + } + + @Override + public Image getResolutionVariant(double destImageWidth, double destImageHeight) { + if (destImageWidth <= getWidth(null) && destImageHeight <= getHeight(null)) { + /* Returning "this" should be safe here, as the same is done in + sun.awt.image.MultiResolutionToolkitImage. */ + return this; + } + Image ds = getDoubleSizeVariant(); + return ds != null ? ds : this; + } + + @Override + public List getResolutionVariants() { + Image ds = getDoubleSizeVariant(); + return ds == null ? List.of(this) : List.of(this, ds); + } } private static final class DisabledButtonFilter extends RGBImageFilter { diff --git a/platform/openide.util.ui/test/unit/src/org/openide/util/UtilitiesTest.java b/platform/openide.util.ui/test/unit/src/org/openide/util/UtilitiesTest.java index 421d39d1fe16..840a5e7b94cf 100644 --- a/platform/openide.util.ui/test/unit/src/org/openide/util/UtilitiesTest.java +++ b/platform/openide.util.ui/test/unit/src/org/openide/util/UtilitiesTest.java @@ -55,7 +55,7 @@ import java.awt.datatransfer.Clipboard; import java.awt.dnd.DragGestureEvent; import java.awt.dnd.InvalidDnDOperationException; -import java.awt.dnd.peer.DragSourceContextPeer; +//import java.awt.dnd.peer.DragSourceContextPeer; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; @@ -64,6 +64,7 @@ import java.awt.image.ColorModel; import java.awt.image.ImageObserver; import java.awt.image.ImageProducer; +/* import java.awt.peer.ButtonPeer; import java.awt.peer.CanvasPeer; import java.awt.peer.CheckboxMenuItemPeer; @@ -86,6 +87,7 @@ import java.awt.peer.TextAreaPeer; import java.awt.peer.TextFieldPeer; import java.awt.peer.WindowPeer; +*/ import java.beans.PropertyChangeListener; import java.net.URL; import java.util.ArrayList; @@ -320,28 +322,12 @@ public Toolkit getToolkit() { return customToolkit; } } - + private static class NoCustomCursorToolkit extends Toolkit { public FontMetrics getFontMetrics(Font font) { return Toolkit.getDefaultToolkit().getFontMetrics( font ); } - protected TextFieldPeer createTextField(TextField target) throws HeadlessException { - throw new IllegalStateException("Method not implemented"); - } - - protected ListPeer createList(java.awt.List target) throws HeadlessException { - throw new IllegalStateException("Method not implemented"); - } - - protected MenuBarPeer createMenuBar(MenuBar target) throws HeadlessException { - throw new IllegalStateException("Method not implemented"); - } - - public DragSourceContextPeer createDragSourceContextPeer(DragGestureEvent dge) throws InvalidDnDOperationException { - throw new IllegalStateException("Method not implemented"); - } - public boolean prepareImage(Image image, int width, int height, ImageObserver observer) { return Toolkit.getDefaultToolkit().prepareImage( image, width, height, observer ); } @@ -350,30 +336,14 @@ public int checkImage(Image image, int width, int height, ImageObserver observer return Toolkit.getDefaultToolkit().checkImage( image, width, height, observer ); } - protected PopupMenuPeer createPopupMenu(PopupMenu target) throws HeadlessException { - throw new IllegalStateException("Method not implemented"); - } - public PrintJob getPrintJob(Frame frame, String jobtitle, Properties props) { return Toolkit.getDefaultToolkit().getPrintJob( frame, jobtitle, props ); } - protected ButtonPeer createButton(Button target) throws HeadlessException { - throw new IllegalStateException("Method not implemented"); - } - public Image createImage(ImageProducer producer) { return Toolkit.getDefaultToolkit().createImage( producer ); } - protected CanvasPeer createCanvas(Canvas target) { - throw new IllegalStateException("Method not implemented"); - } - - protected ScrollbarPeer createScrollbar(Scrollbar target) throws HeadlessException { - throw new IllegalStateException("Method not implemented"); - } - public Image getImage(String filename) { return Toolkit.getDefaultToolkit().getImage( filename ); } @@ -382,14 +352,6 @@ public Image createImage(String filename) { return Toolkit.getDefaultToolkit().createImage( filename ); } - protected MenuPeer createMenu(Menu target) throws HeadlessException { - throw new IllegalStateException("Method not implemented"); - } - - protected MenuItemPeer createMenuItem(MenuItem target) throws HeadlessException { - throw new IllegalStateException("Method not implemented"); - } - public Map mapInputMethodHighlight(InputMethodHighlight highlight) throws HeadlessException { return Toolkit.getDefaultToolkit().mapInputMethodHighlight( highlight ); } @@ -402,58 +364,10 @@ public Image getImage(URL url) { return Toolkit.getDefaultToolkit().getImage( url ); } - protected CheckboxPeer createCheckbox(Checkbox target) throws HeadlessException { - throw new IllegalStateException("Method not implemented"); - } - public Image createImage(URL url) { return Toolkit.getDefaultToolkit().createImage( url ); } - protected TextAreaPeer createTextArea(TextArea target) throws HeadlessException { - throw new IllegalStateException("Method not implemented"); - } - - protected FileDialogPeer createFileDialog(FileDialog target) throws HeadlessException { - throw new IllegalStateException("Method not implemented"); - } - - protected ScrollPanePeer createScrollPane(ScrollPane target) throws HeadlessException { - throw new IllegalStateException("Method not implemented"); - } - - protected DialogPeer createDialog(Dialog target) throws HeadlessException { - throw new IllegalStateException("Method not implemented"); - } - - protected PanelPeer createPanel(Panel target) { - throw new IllegalStateException("Method not implemented"); - } - - protected ChoicePeer createChoice(Choice target) throws HeadlessException { - throw new IllegalStateException("Method not implemented"); - } - - protected FramePeer createFrame(Frame target) throws HeadlessException { - throw new IllegalStateException("Method not implemented"); - } - - protected LabelPeer createLabel(Label target) throws HeadlessException { - throw new IllegalStateException("Method not implemented"); - } - - protected FontPeer getFontPeer(String name, int style) { - throw new IllegalStateException("Method not implemented"); - } - - protected CheckboxMenuItemPeer createCheckboxMenuItem(CheckboxMenuItem target) throws HeadlessException { - throw new IllegalStateException("Method not implemented"); - } - - protected WindowPeer createWindow(Window target) throws HeadlessException { - throw new IllegalStateException("Method not implemented"); - } - public void sync() { Toolkit.getDefaultToolkit().sync(); } @@ -503,10 +417,6 @@ public Dimension getBestCursorSize(int preferredWidth, int preferredHeight) thro return new Dimension(0,0); } - protected DesktopPeer createDesktopPeer(Desktop target) throws HeadlessException { - throw new UnsupportedOperationException("Not supported yet."); - } - @Override public boolean isModalityTypeSupported(ModalityType modalityType) { throw new UnsupportedOperationException("Not supported yet.");