Skip to content

Commit

Permalink
Adjust the Icon implementation in ImageUtilities to make SVG/HiDPI ic…
Browse files Browse the repository at this point in the history
…ons work in the MacOS menu bar.
  • Loading branch information
eirikbakke committed Jul 17, 2024
1 parent a6911ed commit 8d39338
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 113 deletions.
1 change: 0 additions & 1 deletion platform/openide.util.ui/manifest.mf
Original file line number Diff line number Diff line change
Expand Up @@ -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

3 changes: 1 addition & 2 deletions platform/openide.util.ui/nbproject/project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
87 changes: 71 additions & 16 deletions platform/openide.util.ui/src/org/openide/util/ImageUtilities.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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();
}

/**
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1089,16 +1086,24 @@ private void readObjectNoData() throws ObjectStreamException {
}

/**
* Image with tool tip text (for icons with badges)
* Image with tool tip text (for icons with badges).
*
* <p>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;
// May be null.
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);
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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<Image> getResolutionVariants() {
Image ds = getDoubleSizeVariant();
return ds == null ? List.of(this) : List.of(this, ds);
}
}

private static final class DisabledButtonFilter extends RGBImageFilter {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 );
}
Expand All @@ -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 );
}
Expand All @@ -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 );
}
Expand All @@ -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();
}
Expand Down Expand Up @@ -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.");
Expand Down

0 comments on commit 8d39338

Please sign in to comment.