This is an automated email from the ASF dual-hosted git repository.
ebakke pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/netbeans.git
The following commit(s) were added to refs/heads/master by this push:
new 8d39338e2a2 Adjust the Icon implementation in ImageUtilities to make
SVG/HiDPI icons work in the MacOS menu bar.
8d39338e2a2 is described below
commit 8d39338e2a243b03deefd27a7fa6a1da621be04f
Author: Eirik Bakke <[email protected]>
AuthorDate: Sat Dec 2 18:41:33 2023 -0500
Adjust the Icon implementation in ImageUtilities to make SVG/HiDPI icons
work in the MacOS menu bar.
---
platform/openide.util.ui/manifest.mf | 1 -
.../openide.util.ui/nbproject/project.properties | 3 +-
.../src/org/openide/util/ImageUtilities.java | 87 +++++++++++++++----
.../unit/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 1d8866d38e0..a2a1c97ac14 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 5ba993f13de..d56fbb7147a 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 d66dd82b633..526f659594a 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.geom.AffineTransform;
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.HashMap;
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 final class ImageUtilities {
* @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 @@ public final class ImageUtilities {
// 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 @@ public final class ImageUtilities {
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 @@ public final class ImageUtilities {
}
/**
- * 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;
@@ -1099,6 +1102,8 @@ public final class ImageUtilities {
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 final class ImageUtilities {
}
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 @@ public final class ImageUtilities {
}
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 {
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 421d39d1fe1..840a5e7b94c 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.Window;
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.BufferedImage;
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.ScrollbarPeer;
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 class UtilitiesTest extends NbTestCase {
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 class UtilitiesTest extends NbTestCase {
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 class UtilitiesTest extends NbTestCase {
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 class UtilitiesTest extends NbTestCase {
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 class UtilitiesTest extends NbTestCase {
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.");
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]
For further information about the NetBeans mailing lists, visit:
https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists