Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 118 additions & 27 deletions platform/openide.util.ui/src/org/openide/util/ImageUtilities.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,20 +19,21 @@

package org.openide.util;

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.HeadlessException;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Toolkit;
import java.awt.Transparency;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageObserver;
import java.awt.image.ImageProducer;
import java.awt.image.IndexColorModel;
import java.awt.image.RGBImageFilter;
import java.awt.image.WritableRaster;
import java.io.IOException;
Expand DownExpand Up@@ -342,7 +343,7 @@ public static final Image addToolTipToImage(Image image, String text) {
*/
public static Icon createDisabledIcon(Icon icon) {
Parameters.notNull("icon", icon);
return new LazyDisabledIcon(icon2Image(icon));
return new DisabledIcon(icon);
}

/**
Expand All@@ -353,7 +354,7 @@ public static Icon createDisabledIcon(Icon icon) {
*/
public static Image createDisabledImage(Image image) {
Parameters.notNull("image", image);
return LazyDisabledIcon.createDisabledImage(image);
return DisabledButtonFilter.createDisabledImage(image);
}

/**
Expand DownExpand Up@@ -880,44 +881,129 @@ public Object getProperty(String name, ImageObserver observer) {
}
}

private static class LazyDisabledIcon implements Icon {

/** Shared instance of filter for disabled icons */
private static final RGBImageFilter DISABLED_BUTTON_FILTER = new DisabledButtonFilter();
private Image img;
private Icon disabledIcon;

public LazyDisabledIcon(Image img) {
assert null != img;
this.img = img;
}

public void paintIcon(Component c, Graphics g, int x, int y) {
getDisabledIcon().paintIcon(c, g, x, y);
/* This method was tested with both a VectorIcon and an ImageIcon, including with the latter
delegating to a MultiResolutionImage, on OpenJDK 10.0.2 on Windows 10 with a HiDPI/non-HiDPI
dual monitor setup. Also tested the VectorIcon case on MacOS with Retina and non-Retina
configurations. */
/**
* A disabled variation of a provided delegate icon. Any kind of delegate implementation can be
* used. In particular, this class preserves the full fidelity of HiDPI icons, such as instances
* of {@link VectorIcon}, or {@link ImageIcon} instances delegating to a
* {@code java.awt.image.MultiResolutionImage} (available since Java 9 and above).
*/
private static final class DisabledIcon implements Icon {
private final Icon delegate;
/* Since previous versions of ImageUtilities.createDisabledImage always cached the
generated image, we do so here as well. If the associated transform changes, for instance
if the user moves the window to a different monitor with a different HiDPI scaling level,
the icon is regenerated. Don't bother trying to keep multiple versions of the same icon
around; this could lead to memory leaks if GraphicsConfiguration/AffineTransform equals()
checks fail for some reason. */
private volatile CachedFilteredImage cachedFilteredImage;

private DisabledIcon(Icon delegate) {
Parameters.notNull("delegate", delegate);
this.delegate = delegate;
}

@Override
public int getIconWidth() {
return getDisabledIcon().getIconWidth();
return delegate.getIconWidth();
}

@Override
public int getIconHeight() {
return getDisabledIcon().getIconHeight();
return delegate.getIconHeight();
}

@Override
public void paintIcon(Component c, Graphics g0, int x, int y) {
final Graphics2D g = (Graphics2D) g0;
final AffineTransform oldTransform = g.getTransform();
g.translate(x, y);
final AffineTransform tx = g.getTransform();

/* Paint the icon to a BufferedImage, then run it through the ImageFilter. If the Graphics
object has a scaling transform on it, it might be for HiDPI rendering purposes. In this case we
should create a correspondingly larger BufferedImage to preserve the full device resolution. */
final double scaling;
final int txType = tx.getType();
AffineTransform useImageTransform;
if (txType == AffineTransform.TYPE_UNIFORM_SCALE ||
txType == (AffineTransform.TYPE_UNIFORM_SCALE | AffineTransform.TYPE_TRANSLATION))
{
scaling = tx.getScaleX();
/* Apply the scaling. Also preserve any device pixel misalignment that exists in the
original Graphics2D's transform, in case the delegate Icon knows how to handle it. See
the VectorIcon class for an example of such handling. */
double misalignX = tx.getTranslateX() - (int) tx.getTranslateX();
double misalignY = tx.getTranslateY() - (int) tx.getTranslateY();
useImageTransform = new AffineTransform(scaling, 0, 0, scaling, misalignX, misalignY);
} else {
// Unsupported transform type; don't apply HiDPI scaling.
scaling = 1.0;
useImageTransform = AffineTransform.getScaleInstance(1.0, 1.0);
}
Image filteredImage = getFilteredImage(c, g.getDeviceConfiguration(), useImageTransform);
if (scaling != 1.0) {
// Scale the image down to its logical dimensions, and draw it at the device pixel boundary.
AffineTransform tx2 = g.getTransform();
g.setTransform(new AffineTransform(1, 0, 0, 1,
(int) tx2.getTranslateX(),
(int) tx2.getTranslateY()));
}
g.drawImage(filteredImage, 0, 0, null);
g.setTransform(oldTransform);
}

private synchronized Icon getDisabledIcon() {
if (null == disabledIcon) {
disabledIcon = new ImageIcon(createDisabledImage(img));
/**
* @param tx transform that incorporates HiDPI scaling and any HiDPI misalignment adjustment
*/
private Image getFilteredImage(Component c, GraphicsConfiguration gc, AffineTransform tx) {
CachedFilteredImage ret = this.cachedFilteredImage;
/* Technically, the "Component c" parameter may also contain state that the delegate
might use during painting here. But we don't know what that state might be, so we
have no way to compare it for the sake of determining if the cache is still valid or
not. Allow the image to be cached anyway, retaining the behavior of the previous version
of ImageUtilities.createDisabledImage. */
if (ret != null && ret.gc.equals(gc) && ret.tx.equals(tx)) {
return ret.img;
}
return disabledIcon;
final BufferedImage img = gc.createCompatibleImage(
(int) Math.ceil(getIconWidth() * tx.getScaleX()),
(int) Math.ceil(getIconHeight() * tx.getScaleY()), Transparency.TRANSLUCENT);
final Graphics2D imgG = (Graphics2D) img.getGraphics();
try {
imgG.setTransform(tx);
delegate.paintIcon(c, imgG, 0, 0);
} finally {
imgG.dispose();
}
ret = new CachedFilteredImage(DisabledButtonFilter.createDisabledImage(img), gc, tx);
this.cachedFilteredImage = ret;
return ret.img;
}

static Image createDisabledImage(Image img) {
ImageProducer prod = new FilteredImageSource(img.getSource(), DISABLED_BUTTON_FILTER);
return Toolkit.getDefaultToolkit().createImage(prod);
// Immutable.
private static final class CachedFilteredImage {
final Image img;
final GraphicsConfiguration gc;
final AffineTransform tx;

public CachedFilteredImage(Image img, GraphicsConfiguration gc, AffineTransform tx) {
Parameters.notNull("img", img);
Parameters.notNull("gc", gc);
Parameters.notNull("tx", tx);
this.img = img;
this.gc = gc;
this.tx = tx;
}
}
}

private static class DisabledButtonFilter extends RGBImageFilter {
/** Shared instance of filter for disabled icons */
private static final RGBImageFilter INSTANCE = new DisabledButtonFilter();

DisabledButtonFilter() {
canFilterIndexColorModel = true;
Expand All@@ -935,5 +1021,10 @@ public void setProperties(Hashtable props) {
props = (Hashtable) props.clone();
consumer.setProperties(props);
}

static Image createDisabledImage(Image img) {
ImageProducer prod = new FilteredImageSource(img.getSource(), INSTANCE);
return Toolkit.getDefaultToolkit().createImage(prod);
}
}
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 118 additions & 27 deletions platform/openide.util.ui/src/org/openide/util/ImageUtilities.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,20 +19,21 @@

package org.openide.util;

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.HeadlessException;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Toolkit;
import java.awt.Transparency;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageObserver;
import java.awt.image.ImageProducer;
import java.awt.image.IndexColorModel;
import java.awt.image.RGBImageFilter;
import java.awt.image.WritableRaster;
import java.io.IOException;
Expand DownExpand Up@@ -342,7 +343,7 @@ public static final Image addToolTipToImage(Image image, String text) {
*/
public static Icon createDisabledIcon(Icon icon) {
Parameters.notNull("icon", icon);
return new LazyDisabledIcon(icon2Image(icon));
return new DisabledIcon(icon);
}

/**
Expand All@@ -353,7 +354,7 @@ public static Icon createDisabledIcon(Icon icon) {
*/
public static Image createDisabledImage(Image image) {
Parameters.notNull("image", image);
return LazyDisabledIcon.createDisabledImage(image);
return DisabledButtonFilter.createDisabledImage(image);
}

/**
Expand DownExpand Up@@ -880,44 +881,129 @@ public Object getProperty(String name, ImageObserver observer) {
}
}

private static class LazyDisabledIcon implements Icon {

/** Shared instance of filter for disabled icons */
private static final RGBImageFilter DISABLED_BUTTON_FILTER = new DisabledButtonFilter();
private Image img;
private Icon disabledIcon;

public LazyDisabledIcon(Image img) {
assert null != img;
this.img = img;
}

public void paintIcon(Component c, Graphics g, int x, int y) {
getDisabledIcon().paintIcon(c, g, x, y);
/* This method was tested with both a VectorIcon and an ImageIcon, including with the latter
delegating to a MultiResolutionImage, on OpenJDK 10.0.2 on Windows 10 with a HiDPI/non-HiDPI
dual monitor setup. Also tested the VectorIcon case on MacOS with Retina and non-Retina
configurations. */
/**
* A disabled variation of a provided delegate icon. Any kind of delegate implementation can be
* used. In particular, this class preserves the full fidelity of HiDPI icons, such as instances
* of {@link VectorIcon}, or {@link ImageIcon} instances delegating to a
* {@code java.awt.image.MultiResolutionImage} (available since Java 9 and above).
*/
private static final class DisabledIcon implements Icon {
private final Icon delegate;
/* Since previous versions of ImageUtilities.createDisabledImage always cached the
generated image, we do so here as well. If the associated transform changes, for instance
if the user moves the window to a different monitor with a different HiDPI scaling level,
the icon is regenerated. Don't bother trying to keep multiple versions of the same icon
around; this could lead to memory leaks if GraphicsConfiguration/AffineTransform equals()
checks fail for some reason. */
private volatile CachedFilteredImage cachedFilteredImage;

private DisabledIcon(Icon delegate) {
Parameters.notNull("delegate", delegate);
this.delegate = delegate;
}

@Override
public int getIconWidth() {
return getDisabledIcon().getIconWidth();
return delegate.getIconWidth();
}

@Override
public int getIconHeight() {
return getDisabledIcon().getIconHeight();
return delegate.getIconHeight();
}

@Override
public void paintIcon(Component c, Graphics g0, int x, int y) {
final Graphics2D g = (Graphics2D) g0;
final AffineTransform oldTransform = g.getTransform();
g.translate(x, y);
final AffineTransform tx = g.getTransform();

/* Paint the icon to a BufferedImage, then run it through the ImageFilter. If the Graphics
object has a scaling transform on it, it might be for HiDPI rendering purposes. In this case we
should create a correspondingly larger BufferedImage to preserve the full device resolution. */
final double scaling;
final int txType = tx.getType();
AffineTransform useImageTransform;
if (txType == AffineTransform.TYPE_UNIFORM_SCALE ||
txType == (AffineTransform.TYPE_UNIFORM_SCALE | AffineTransform.TYPE_TRANSLATION))
{
scaling = tx.getScaleX();
/* Apply the scaling. Also preserve any device pixel misalignment that exists in the
original Graphics2D's transform, in case the delegate Icon knows how to handle it. See
the VectorIcon class for an example of such handling. */
double misalignX = tx.getTranslateX() - (int) tx.getTranslateX();
double misalignY = tx.getTranslateY() - (int) tx.getTranslateY();
useImageTransform = new AffineTransform(scaling, 0, 0, scaling, misalignX, misalignY);
} else {
// Unsupported transform type; don't apply HiDPI scaling.
scaling = 1.0;
useImageTransform = AffineTransform.getScaleInstance(1.0, 1.0);
}
Image filteredImage = getFilteredImage(c, g.getDeviceConfiguration(), useImageTransform);
if (scaling != 1.0) {
// Scale the image down to its logical dimensions, and draw it at the device pixel boundary.
AffineTransform tx2 = g.getTransform();
g.setTransform(new AffineTransform(1, 0, 0, 1,
(int) tx2.getTranslateX(),
(int) tx2.getTranslateY()));
}
g.drawImage(filteredImage, 0, 0, null);
g.setTransform(oldTransform);
}

private synchronized Icon getDisabledIcon() {
if (null == disabledIcon) {
disabledIcon = new ImageIcon(createDisabledImage(img));
/**
* @param tx transform that incorporates HiDPI scaling and any HiDPI misalignment adjustment
*/
private Image getFilteredImage(Component c, GraphicsConfiguration gc, AffineTransform tx) {
CachedFilteredImage ret = this.cachedFilteredImage;
/* Technically, the "Component c" parameter may also contain state that the delegate
might use during painting here. But we don't know what that state might be, so we
have no way to compare it for the sake of determining if the cache is still valid or
not. Allow the image to be cached anyway, retaining the behavior of the previous version
of ImageUtilities.createDisabledImage. */
if (ret != null && ret.gc.equals(gc) && ret.tx.equals(tx)) {
return ret.img;
}
return disabledIcon;
final BufferedImage img = gc.createCompatibleImage(
(int) Math.ceil(getIconWidth() * tx.getScaleX()),
(int) Math.ceil(getIconHeight() * tx.getScaleY()), Transparency.TRANSLUCENT);
final Graphics2D imgG = (Graphics2D) img.getGraphics();
try {
imgG.setTransform(tx);
delegate.paintIcon(c, imgG, 0, 0);
} finally {
imgG.dispose();
}
ret = new CachedFilteredImage(DisabledButtonFilter.createDisabledImage(img), gc, tx);
this.cachedFilteredImage = ret;
return ret.img;
}

static Image createDisabledImage(Image img) {
ImageProducer prod = new FilteredImageSource(img.getSource(), DISABLED_BUTTON_FILTER);
return Toolkit.getDefaultToolkit().createImage(prod);
// Immutable.
private static final class CachedFilteredImage {
final Image img;
final GraphicsConfiguration gc;
final AffineTransform tx;

public CachedFilteredImage(Image img, GraphicsConfiguration gc, AffineTransform tx) {
Parameters.notNull("img", img);
Parameters.notNull("gc", gc);
Parameters.notNull("tx", tx);
this.img = img;
this.gc = gc;
this.tx = tx;
}
}
}

private static class DisabledButtonFilter extends RGBImageFilter {
/** Shared instance of filter for disabled icons */
private static final RGBImageFilter INSTANCE = new DisabledButtonFilter();

DisabledButtonFilter() {
canFilterIndexColorModel = true;
Expand All@@ -935,5 +1021,10 @@ public void setProperties(Hashtable props) {
props = (Hashtable) props.clone();
consumer.setProperties(props);
}

static Image createDisabledImage(Image img) {
ImageProducer prod = new FilteredImageSource(img.getSource(), INSTANCE);
return Toolkit.getDefaultToolkit().createImage(prod);
}
}
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 118 additions & 27 deletions platform/openide.util.ui/src/org/openide/util/ImageUtilities.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,20 +19,21 @@

package org.openide.util;

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.HeadlessException;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Toolkit;
import java.awt.Transparency;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageObserver;
import java.awt.image.ImageProducer;
import java.awt.image.IndexColorModel;
import java.awt.image.RGBImageFilter;
import java.awt.image.WritableRaster;
import java.io.IOException;
Expand DownExpand Up@@ -342,7 +343,7 @@ public static final Image addToolTipToImage(Image image, String text) {
*/
public static Icon createDisabledIcon(Icon icon) {
Parameters.notNull("icon", icon);
return new LazyDisabledIcon(icon2Image(icon));
return new DisabledIcon(icon);
}

/**
Expand All@@ -353,7 +354,7 @@ public static Icon createDisabledIcon(Icon icon) {
*/
public static Image createDisabledImage(Image image) {
Parameters.notNull("image", image);
return LazyDisabledIcon.createDisabledImage(image);
return DisabledButtonFilter.createDisabledImage(image);
}

/**
Expand DownExpand Up@@ -880,44 +881,129 @@ public Object getProperty(String name, ImageObserver observer) {
}
}

private static class LazyDisabledIcon implements Icon {

/** Shared instance of filter for disabled icons */
private static final RGBImageFilter DISABLED_BUTTON_FILTER = new DisabledButtonFilter();
private Image img;
private Icon disabledIcon;

public LazyDisabledIcon(Image img) {
assert null != img;
this.img = img;
}

public void paintIcon(Component c, Graphics g, int x, int y) {
getDisabledIcon().paintIcon(c, g, x, y);
/* This method was tested with both a VectorIcon and an ImageIcon, including with the latter
delegating to a MultiResolutionImage, on OpenJDK 10.0.2 on Windows 10 with a HiDPI/non-HiDPI
dual monitor setup. Also tested the VectorIcon case on MacOS with Retina and non-Retina
configurations. */
/**
* A disabled variation of a provided delegate icon. Any kind of delegate implementation can be
* used. In particular, this class preserves the full fidelity of HiDPI icons, such as instances
* of {@link VectorIcon}, or {@link ImageIcon} instances delegating to a
* {@code java.awt.image.MultiResolutionImage} (available since Java 9 and above).
*/
private static final class DisabledIcon implements Icon {
private final Icon delegate;
/* Since previous versions of ImageUtilities.createDisabledImage always cached the
generated image, we do so here as well. If the associated transform changes, for instance
if the user moves the window to a different monitor with a different HiDPI scaling level,
the icon is regenerated. Don't bother trying to keep multiple versions of the same icon
around; this could lead to memory leaks if GraphicsConfiguration/AffineTransform equals()
checks fail for some reason. */
private volatile CachedFilteredImage cachedFilteredImage;

private DisabledIcon(Icon delegate) {
Parameters.notNull("delegate", delegate);
this.delegate = delegate;
}

@Override
public int getIconWidth() {
return getDisabledIcon().getIconWidth();
return delegate.getIconWidth();
}

@Override
public int getIconHeight() {
return getDisabledIcon().getIconHeight();
return delegate.getIconHeight();
}

@Override
public void paintIcon(Component c, Graphics g0, int x, int y) {
final Graphics2D g = (Graphics2D) g0;
final AffineTransform oldTransform = g.getTransform();
g.translate(x, y);
final AffineTransform tx = g.getTransform();

/* Paint the icon to a BufferedImage, then run it through the ImageFilter. If the Graphics
object has a scaling transform on it, it might be for HiDPI rendering purposes. In this case we
should create a correspondingly larger BufferedImage to preserve the full device resolution. */
final double scaling;
final int txType = tx.getType();
AffineTransform useImageTransform;
if (txType == AffineTransform.TYPE_UNIFORM_SCALE ||
txType == (AffineTransform.TYPE_UNIFORM_SCALE | AffineTransform.TYPE_TRANSLATION))
{
scaling = tx.getScaleX();
/* Apply the scaling. Also preserve any device pixel misalignment that exists in the
original Graphics2D's transform, in case the delegate Icon knows how to handle it. See
the VectorIcon class for an example of such handling. */
double misalignX = tx.getTranslateX() - (int) tx.getTranslateX();
double misalignY = tx.getTranslateY() - (int) tx.getTranslateY();
useImageTransform = new AffineTransform(scaling, 0, 0, scaling, misalignX, misalignY);
} else {
// Unsupported transform type; don't apply HiDPI scaling.
scaling = 1.0;
useImageTransform = AffineTransform.getScaleInstance(1.0, 1.0);
}
Image filteredImage = getFilteredImage(c, g.getDeviceConfiguration(), useImageTransform);
if (scaling != 1.0) {
// Scale the image down to its logical dimensions, and draw it at the device pixel boundary.
AffineTransform tx2 = g.getTransform();
g.setTransform(new AffineTransform(1, 0, 0, 1,
(int) tx2.getTranslateX(),
(int) tx2.getTranslateY()));
}
g.drawImage(filteredImage, 0, 0, null);
g.setTransform(oldTransform);
}

private synchronized Icon getDisabledIcon() {
if (null == disabledIcon) {
disabledIcon = new ImageIcon(createDisabledImage(img));
/**
* @param tx transform that incorporates HiDPI scaling and any HiDPI misalignment adjustment
*/
private Image getFilteredImage(Component c, GraphicsConfiguration gc, AffineTransform tx) {
CachedFilteredImage ret = this.cachedFilteredImage;
/* Technically, the "Component c" parameter may also contain state that the delegate
might use during painting here. But we don't know what that state might be, so we
have no way to compare it for the sake of determining if the cache is still valid or
not. Allow the image to be cached anyway, retaining the behavior of the previous version
of ImageUtilities.createDisabledImage. */
if (ret != null && ret.gc.equals(gc) && ret.tx.equals(tx)) {
return ret.img;
}
return disabledIcon;
final BufferedImage img = gc.createCompatibleImage(
(int) Math.ceil(getIconWidth() * tx.getScaleX()),
(int) Math.ceil(getIconHeight() * tx.getScaleY()), Transparency.TRANSLUCENT);
final Graphics2D imgG = (Graphics2D) img.getGraphics();
try {
imgG.setTransform(tx);
delegate.paintIcon(c, imgG, 0, 0);
} finally {
imgG.dispose();
}
ret = new CachedFilteredImage(DisabledButtonFilter.createDisabledImage(img), gc, tx);
this.cachedFilteredImage = ret;
return ret.img;
}

static Image createDisabledImage(Image img) {
ImageProducer prod = new FilteredImageSource(img.getSource(), DISABLED_BUTTON_FILTER);
return Toolkit.getDefaultToolkit().createImage(prod);
// Immutable.
private static final class CachedFilteredImage {
final Image img;
final GraphicsConfiguration gc;
final AffineTransform tx;

public CachedFilteredImage(Image img, GraphicsConfiguration gc, AffineTransform tx) {
Parameters.notNull("img", img);
Parameters.notNull("gc", gc);
Parameters.notNull("tx", tx);
this.img = img;
this.gc = gc;
this.tx = tx;
}
}
}

private static class DisabledButtonFilter extends RGBImageFilter {
/** Shared instance of filter for disabled icons */
private static final RGBImageFilter INSTANCE = new DisabledButtonFilter();

DisabledButtonFilter() {
canFilterIndexColorModel = true;
Expand All@@ -935,5 +1021,10 @@ public void setProperties(Hashtable props) {
props = (Hashtable) props.clone();
consumer.setProperties(props);
}

static Image createDisabledImage(Image img) {
ImageProducer prod = new FilteredImageSource(img.getSource(), INSTANCE);
return Toolkit.getDefaultToolkit().createImage(prod);
}
}
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 118 additions & 27 deletions platform/openide.util.ui/src/org/openide/util/ImageUtilities.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,20 +19,21 @@

package org.openide.util;

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.HeadlessException;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Toolkit;
import java.awt.Transparency;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageObserver;
import java.awt.image.ImageProducer;
import java.awt.image.IndexColorModel;
import java.awt.image.RGBImageFilter;
import java.awt.image.WritableRaster;
import java.io.IOException;
Expand DownExpand Up@@ -342,7 +343,7 @@ public static final Image addToolTipToImage(Image image, String text) {
*/
public static Icon createDisabledIcon(Icon icon) {
Parameters.notNull("icon", icon);
return new LazyDisabledIcon(icon2Image(icon));
return new DisabledIcon(icon);
}

/**
Expand All@@ -353,7 +354,7 @@ public static Icon createDisabledIcon(Icon icon) {
*/
public static Image createDisabledImage(Image image) {
Parameters.notNull("image", image);
return LazyDisabledIcon.createDisabledImage(image);
return DisabledButtonFilter.createDisabledImage(image);
}

/**
Expand DownExpand Up@@ -880,44 +881,129 @@ public Object getProperty(String name, ImageObserver observer) {
}
}

private static class LazyDisabledIcon implements Icon {

/** Shared instance of filter for disabled icons */
private static final RGBImageFilter DISABLED_BUTTON_FILTER = new DisabledButtonFilter();
private Image img;
private Icon disabledIcon;

public LazyDisabledIcon(Image img) {
assert null != img;
this.img = img;
}

public void paintIcon(Component c, Graphics g, int x, int y) {
getDisabledIcon().paintIcon(c, g, x, y);
/* This method was tested with both a VectorIcon and an ImageIcon, including with the latter
delegating to a MultiResolutionImage, on OpenJDK 10.0.2 on Windows 10 with a HiDPI/non-HiDPI
dual monitor setup. Also tested the VectorIcon case on MacOS with Retina and non-Retina
configurations. */
/**
* A disabled variation of a provided delegate icon. Any kind of delegate implementation can be
* used. In particular, this class preserves the full fidelity of HiDPI icons, such as instances
* of {@link VectorIcon}, or {@link ImageIcon} instances delegating to a
* {@code java.awt.image.MultiResolutionImage} (available since Java 9 and above).
*/
private static final class DisabledIcon implements Icon {
private final Icon delegate;
/* Since previous versions of ImageUtilities.createDisabledImage always cached the
generated image, we do so here as well. If the associated transform changes, for instance
if the user moves the window to a different monitor with a different HiDPI scaling level,
the icon is regenerated. Don't bother trying to keep multiple versions of the same icon
around; this could lead to memory leaks if GraphicsConfiguration/AffineTransform equals()
checks fail for some reason. */
private volatile CachedFilteredImage cachedFilteredImage;

private DisabledIcon(Icon delegate) {
Parameters.notNull("delegate", delegate);
this.delegate = delegate;
}

@Override
public int getIconWidth() {
return getDisabledIcon().getIconWidth();
return delegate.getIconWidth();
}

@Override
public int getIconHeight() {
return getDisabledIcon().getIconHeight();
return delegate.getIconHeight();
}

@Override
public void paintIcon(Component c, Graphics g0, int x, int y) {
final Graphics2D g = (Graphics2D) g0;
final AffineTransform oldTransform = g.getTransform();
g.translate(x, y);
final AffineTransform tx = g.getTransform();

/* Paint the icon to a BufferedImage, then run it through the ImageFilter. If the Graphics
object has a scaling transform on it, it might be for HiDPI rendering purposes. In this case we
should create a correspondingly larger BufferedImage to preserve the full device resolution. */
final double scaling;
final int txType = tx.getType();
AffineTransform useImageTransform;
if (txType == AffineTransform.TYPE_UNIFORM_SCALE ||
txType == (AffineTransform.TYPE_UNIFORM_SCALE | AffineTransform.TYPE_TRANSLATION))
{
scaling = tx.getScaleX();
/* Apply the scaling. Also preserve any device pixel misalignment that exists in the
original Graphics2D's transform, in case the delegate Icon knows how to handle it. See
the VectorIcon class for an example of such handling. */
double misalignX = tx.getTranslateX() - (int) tx.getTranslateX();
double misalignY = tx.getTranslateY() - (int) tx.getTranslateY();
useImageTransform = new AffineTransform(scaling, 0, 0, scaling, misalignX, misalignY);
} else {
// Unsupported transform type; don't apply HiDPI scaling.
scaling = 1.0;
useImageTransform = AffineTransform.getScaleInstance(1.0, 1.0);
}
Image filteredImage = getFilteredImage(c, g.getDeviceConfiguration(), useImageTransform);
if (scaling != 1.0) {
// Scale the image down to its logical dimensions, and draw it at the device pixel boundary.
AffineTransform tx2 = g.getTransform();
g.setTransform(new AffineTransform(1, 0, 0, 1,
(int) tx2.getTranslateX(),
(int) tx2.getTranslateY()));
}
g.drawImage(filteredImage, 0, 0, null);
g.setTransform(oldTransform);
}

private synchronized Icon getDisabledIcon() {
if (null == disabledIcon) {
disabledIcon = new ImageIcon(createDisabledImage(img));
/**
* @param tx transform that incorporates HiDPI scaling and any HiDPI misalignment adjustment
*/
private Image getFilteredImage(Component c, GraphicsConfiguration gc, AffineTransform tx) {
CachedFilteredImage ret = this.cachedFilteredImage;
/* Technically, the "Component c" parameter may also contain state that the delegate
might use during painting here. But we don't know what that state might be, so we
have no way to compare it for the sake of determining if the cache is still valid or
not. Allow the image to be cached anyway, retaining the behavior of the previous version
of ImageUtilities.createDisabledImage. */
if (ret != null && ret.gc.equals(gc) && ret.tx.equals(tx)) {
return ret.img;
}
return disabledIcon;
final BufferedImage img = gc.createCompatibleImage(
(int) Math.ceil(getIconWidth() * tx.getScaleX()),
(int) Math.ceil(getIconHeight() * tx.getScaleY()), Transparency.TRANSLUCENT);
final Graphics2D imgG = (Graphics2D) img.getGraphics();
try {
imgG.setTransform(tx);
delegate.paintIcon(c, imgG, 0, 0);
} finally {
imgG.dispose();
}
ret = new CachedFilteredImage(DisabledButtonFilter.createDisabledImage(img), gc, tx);
this.cachedFilteredImage = ret;
return ret.img;
}

static Image createDisabledImage(Image img) {
ImageProducer prod = new FilteredImageSource(img.getSource(), DISABLED_BUTTON_FILTER);
return Toolkit.getDefaultToolkit().createImage(prod);
// Immutable.
private static final class CachedFilteredImage {
final Image img;
final GraphicsConfiguration gc;
final AffineTransform tx;

public CachedFilteredImage(Image img, GraphicsConfiguration gc, AffineTransform tx) {
Parameters.notNull("img", img);
Parameters.notNull("gc", gc);
Parameters.notNull("tx", tx);
this.img = img;
this.gc = gc;
this.tx = tx;
}
}
}

private static class DisabledButtonFilter extends RGBImageFilter {
/** Shared instance of filter for disabled icons */
private static final RGBImageFilter INSTANCE = new DisabledButtonFilter();

DisabledButtonFilter() {
canFilterIndexColorModel = true;
Expand All@@ -935,5 +1021,10 @@ public void setProperties(Hashtable props) {
props = (Hashtable) props.clone();
consumer.setProperties(props);
}

static Image createDisabledImage(Image img) {
ImageProducer prod = new FilteredImageSource(img.getSource(), INSTANCE);
return Toolkit.getDefaultToolkit().createImage(prod);
}
}
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 118 additions & 27 deletions platform/openide.util.ui/src/org/openide/util/ImageUtilities.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,20 +19,21 @@

package org.openide.util;

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.HeadlessException;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Toolkit;
import java.awt.Transparency;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageObserver;
import java.awt.image.ImageProducer;
import java.awt.image.IndexColorModel;
import java.awt.image.RGBImageFilter;
import java.awt.image.WritableRaster;
import java.io.IOException;
Expand DownExpand Up@@ -342,7 +343,7 @@ public static final Image addToolTipToImage(Image image, String text) {
*/
public static Icon createDisabledIcon(Icon icon) {
Parameters.notNull("icon", icon);
return new LazyDisabledIcon(icon2Image(icon));
return new DisabledIcon(icon);
}

/**
Expand All@@ -353,7 +354,7 @@ public static Icon createDisabledIcon(Icon icon) {
*/
public static Image createDisabledImage(Image image) {
Parameters.notNull("image", image);
return LazyDisabledIcon.createDisabledImage(image);
return DisabledButtonFilter.createDisabledImage(image);
}

/**
Expand DownExpand Up@@ -880,44 +881,129 @@ public Object getProperty(String name, ImageObserver observer) {
}
}

private static class LazyDisabledIcon implements Icon {

/** Shared instance of filter for disabled icons */
private static final RGBImageFilter DISABLED_BUTTON_FILTER = new DisabledButtonFilter();
private Image img;
private Icon disabledIcon;

public LazyDisabledIcon(Image img) {
assert null != img;
this.img = img;
}

public void paintIcon(Component c, Graphics g, int x, int y) {
getDisabledIcon().paintIcon(c, g, x, y);
/* This method was tested with both a VectorIcon and an ImageIcon, including with the latter
delegating to a MultiResolutionImage, on OpenJDK 10.0.2 on Windows 10 with a HiDPI/non-HiDPI
dual monitor setup. Also tested the VectorIcon case on MacOS with Retina and non-Retina
configurations. */
/**
* A disabled variation of a provided delegate icon. Any kind of delegate implementation can be
* used. In particular, this class preserves the full fidelity of HiDPI icons, such as instances
* of {@link VectorIcon}, or {@link ImageIcon} instances delegating to a
* {@code java.awt.image.MultiResolutionImage} (available since Java 9 and above).
*/
private static final class DisabledIcon implements Icon {
private final Icon delegate;
/* Since previous versions of ImageUtilities.createDisabledImage always cached the
generated image, we do so here as well. If the associated transform changes, for instance
if the user moves the window to a different monitor with a different HiDPI scaling level,
the icon is regenerated. Don't bother trying to keep multiple versions of the same icon
around; this could lead to memory leaks if GraphicsConfiguration/AffineTransform equals()
checks fail for some reason. */
private volatile CachedFilteredImage cachedFilteredImage;

private DisabledIcon(Icon delegate) {
Parameters.notNull("delegate", delegate);
this.delegate = delegate;
}

@Override
public int getIconWidth() {
return getDisabledIcon().getIconWidth();
return delegate.getIconWidth();
}

@Override
public int getIconHeight() {
return getDisabledIcon().getIconHeight();
return delegate.getIconHeight();
}

@Override
public void paintIcon(Component c, Graphics g0, int x, int y) {
final Graphics2D g = (Graphics2D) g0;
final AffineTransform oldTransform = g.getTransform();
g.translate(x, y);
final AffineTransform tx = g.getTransform();

/* Paint the icon to a BufferedImage, then run it through the ImageFilter. If the Graphics
object has a scaling transform on it, it might be for HiDPI rendering purposes. In this case we
should create a correspondingly larger BufferedImage to preserve the full device resolution. */
final double scaling;
final int txType = tx.getType();
AffineTransform useImageTransform;
if (txType == AffineTransform.TYPE_UNIFORM_SCALE ||
txType == (AffineTransform.TYPE_UNIFORM_SCALE | AffineTransform.TYPE_TRANSLATION))
{
scaling = tx.getScaleX();
/* Apply the scaling. Also preserve any device pixel misalignment that exists in the
original Graphics2D's transform, in case the delegate Icon knows how to handle it. See
the VectorIcon class for an example of such handling. */
double misalignX = tx.getTranslateX() - (int) tx.getTranslateX();
double misalignY = tx.getTranslateY() - (int) tx.getTranslateY();
useImageTransform = new AffineTransform(scaling, 0, 0, scaling, misalignX, misalignY);
} else {
// Unsupported transform type; don't apply HiDPI scaling.
scaling = 1.0;
useImageTransform = AffineTransform.getScaleInstance(1.0, 1.0);
}
Image filteredImage = getFilteredImage(c, g.getDeviceConfiguration(), useImageTransform);
if (scaling != 1.0) {
// Scale the image down to its logical dimensions, and draw it at the device pixel boundary.
AffineTransform tx2 = g.getTransform();
g.setTransform(new AffineTransform(1, 0, 0, 1,
(int) tx2.getTranslateX(),
(int) tx2.getTranslateY()));
}
g.drawImage(filteredImage, 0, 0, null);
g.setTransform(oldTransform);
}

private synchronized Icon getDisabledIcon() {
if (null == disabledIcon) {
disabledIcon = new ImageIcon(createDisabledImage(img));
/**
* @param tx transform that incorporates HiDPI scaling and any HiDPI misalignment adjustment
*/
private Image getFilteredImage(Component c, GraphicsConfiguration gc, AffineTransform tx) {
CachedFilteredImage ret = this.cachedFilteredImage;
/* Technically, the "Component c" parameter may also contain state that the delegate
might use during painting here. But we don't know what that state might be, so we
have no way to compare it for the sake of determining if the cache is still valid or
not. Allow the image to be cached anyway, retaining the behavior of the previous version
of ImageUtilities.createDisabledImage. */
if (ret != null && ret.gc.equals(gc) && ret.tx.equals(tx)) {
return ret.img;
}
return disabledIcon;
final BufferedImage img = gc.createCompatibleImage(
(int) Math.ceil(getIconWidth() * tx.getScaleX()),
(int) Math.ceil(getIconHeight() * tx.getScaleY()), Transparency.TRANSLUCENT);
final Graphics2D imgG = (Graphics2D) img.getGraphics();
try {
imgG.setTransform(tx);
delegate.paintIcon(c, imgG, 0, 0);
} finally {
imgG.dispose();
}
ret = new CachedFilteredImage(DisabledButtonFilter.createDisabledImage(img), gc, tx);
this.cachedFilteredImage = ret;
return ret.img;
}

static Image createDisabledImage(Image img) {
ImageProducer prod = new FilteredImageSource(img.getSource(), DISABLED_BUTTON_FILTER);
return Toolkit.getDefaultToolkit().createImage(prod);
// Immutable.
private static final class CachedFilteredImage {
final Image img;
final GraphicsConfiguration gc;
final AffineTransform tx;

public CachedFilteredImage(Image img, GraphicsConfiguration gc, AffineTransform tx) {
Parameters.notNull("img", img);
Parameters.notNull("gc", gc);
Parameters.notNull("tx", tx);
this.img = img;
this.gc = gc;
this.tx = tx;
}
}
}

private static class DisabledButtonFilter extends RGBImageFilter {
/** Shared instance of filter for disabled icons */
private static final RGBImageFilter INSTANCE = new DisabledButtonFilter();

DisabledButtonFilter() {
canFilterIndexColorModel = true;
Expand All@@ -935,5 +1021,10 @@ public void setProperties(Hashtable props) {
props = (Hashtable) props.clone();
consumer.setProperties(props);
}

static Image createDisabledImage(Image img) {
ImageProducer prod = new FilteredImageSource(img.getSource(), INSTANCE);
return Toolkit.getDefaultToolkit().createImage(prod);
}
}
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 118 additions & 27 deletions platform/openide.util.ui/src/org/openide/util/ImageUtilities.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,20 +19,21 @@

package org.openide.util;

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.HeadlessException;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Toolkit;
import java.awt.Transparency;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageObserver;
import java.awt.image.ImageProducer;
import java.awt.image.IndexColorModel;
import java.awt.image.RGBImageFilter;
import java.awt.image.WritableRaster;
import java.io.IOException;
Expand DownExpand Up@@ -342,7 +343,7 @@ public static final Image addToolTipToImage(Image image, String text) {
*/
public static Icon createDisabledIcon(Icon icon) {
Parameters.notNull("icon", icon);
return new LazyDisabledIcon(icon2Image(icon));
return new DisabledIcon(icon);
}

/**
Expand All@@ -353,7 +354,7 @@ public static Icon createDisabledIcon(Icon icon) {
*/
public static Image createDisabledImage(Image image) {
Parameters.notNull("image", image);
return LazyDisabledIcon.createDisabledImage(image);
return DisabledButtonFilter.createDisabledImage(image);
}

/**
Expand DownExpand Up@@ -880,44 +881,129 @@ public Object getProperty(String name, ImageObserver observer) {
}
}

private static class LazyDisabledIcon implements Icon {

/** Shared instance of filter for disabled icons */
private static final RGBImageFilter DISABLED_BUTTON_FILTER = new DisabledButtonFilter();
private Image img;
private Icon disabledIcon;

public LazyDisabledIcon(Image img) {
assert null != img;
this.img = img;
}

public void paintIcon(Component c, Graphics g, int x, int y) {
getDisabledIcon().paintIcon(c, g, x, y);
/* This method was tested with both a VectorIcon and an ImageIcon, including with the latter
delegating to a MultiResolutionImage, on OpenJDK 10.0.2 on Windows 10 with a HiDPI/non-HiDPI
dual monitor setup. Also tested the VectorIcon case on MacOS with Retina and non-Retina
configurations. */
/**
* A disabled variation of a provided delegate icon. Any kind of delegate implementation can be
* used. In particular, this class preserves the full fidelity of HiDPI icons, such as instances
* of {@link VectorIcon}, or {@link ImageIcon} instances delegating to a
* {@code java.awt.image.MultiResolutionImage} (available since Java 9 and above).
*/
private static final class DisabledIcon implements Icon {
private final Icon delegate;
/* Since previous versions of ImageUtilities.createDisabledImage always cached the
generated image, we do so here as well. If the associated transform changes, for instance
if the user moves the window to a different monitor with a different HiDPI scaling level,
the icon is regenerated. Don't bother trying to keep multiple versions of the same icon
around; this could lead to memory leaks if GraphicsConfiguration/AffineTransform equals()
checks fail for some reason. */
private volatile CachedFilteredImage cachedFilteredImage;

private DisabledIcon(Icon delegate) {
Parameters.notNull("delegate", delegate);
this.delegate = delegate;
}

@Override
public int getIconWidth() {
return getDisabledIcon().getIconWidth();
return delegate.getIconWidth();
}

@Override
public int getIconHeight() {
return getDisabledIcon().getIconHeight();
return delegate.getIconHeight();
}

@Override
public void paintIcon(Component c, Graphics g0, int x, int y) {
final Graphics2D g = (Graphics2D) g0;
final AffineTransform oldTransform = g.getTransform();
g.translate(x, y);
final AffineTransform tx = g.getTransform();

/* Paint the icon to a BufferedImage, then run it through the ImageFilter. If the Graphics
object has a scaling transform on it, it might be for HiDPI rendering purposes. In this case we
should create a correspondingly larger BufferedImage to preserve the full device resolution. */
final double scaling;
final int txType = tx.getType();
AffineTransform useImageTransform;
if (txType == AffineTransform.TYPE_UNIFORM_SCALE ||
txType == (AffineTransform.TYPE_UNIFORM_SCALE | AffineTransform.TYPE_TRANSLATION))
{
scaling = tx.getScaleX();
/* Apply the scaling. Also preserve any device pixel misalignment that exists in the
original Graphics2D's transform, in case the delegate Icon knows how to handle it. See
the VectorIcon class for an example of such handling. */
double misalignX = tx.getTranslateX() - (int) tx.getTranslateX();
double misalignY = tx.getTranslateY() - (int) tx.getTranslateY();
useImageTransform = new AffineTransform(scaling, 0, 0, scaling, misalignX, misalignY);
} else {
// Unsupported transform type; don't apply HiDPI scaling.
scaling = 1.0;
useImageTransform = AffineTransform.getScaleInstance(1.0, 1.0);
}
Image filteredImage = getFilteredImage(c, g.getDeviceConfiguration(), useImageTransform);
if (scaling != 1.0) {
// Scale the image down to its logical dimensions, and draw it at the device pixel boundary.
AffineTransform tx2 = g.getTransform();
g.setTransform(new AffineTransform(1, 0, 0, 1,
(int) tx2.getTranslateX(),
(int) tx2.getTranslateY()));
}
g.drawImage(filteredImage, 0, 0, null);
g.setTransform(oldTransform);
}

private synchronized Icon getDisabledIcon() {
if (null == disabledIcon) {
disabledIcon = new ImageIcon(createDisabledImage(img));
/**
* @param tx transform that incorporates HiDPI scaling and any HiDPI misalignment adjustment
*/
private Image getFilteredImage(Component c, GraphicsConfiguration gc, AffineTransform tx) {
CachedFilteredImage ret = this.cachedFilteredImage;
/* Technically, the "Component c" parameter may also contain state that the delegate
might use during painting here. But we don't know what that state might be, so we
have no way to compare it for the sake of determining if the cache is still valid or
not. Allow the image to be cached anyway, retaining the behavior of the previous version
of ImageUtilities.createDisabledImage. */
if (ret != null && ret.gc.equals(gc) && ret.tx.equals(tx)) {
return ret.img;
}
return disabledIcon;
final BufferedImage img = gc.createCompatibleImage(
(int) Math.ceil(getIconWidth() * tx.getScaleX()),
(int) Math.ceil(getIconHeight() * tx.getScaleY()), Transparency.TRANSLUCENT);
final Graphics2D imgG = (Graphics2D) img.getGraphics();
try {
imgG.setTransform(tx);
delegate.paintIcon(c, imgG, 0, 0);
} finally {
imgG.dispose();
}
ret = new CachedFilteredImage(DisabledButtonFilter.createDisabledImage(img), gc, tx);
this.cachedFilteredImage = ret;
return ret.img;
}

static Image createDisabledImage(Image img) {
ImageProducer prod = new FilteredImageSource(img.getSource(), DISABLED_BUTTON_FILTER);
return Toolkit.getDefaultToolkit().createImage(prod);
// Immutable.
private static final class CachedFilteredImage {
final Image img;
final GraphicsConfiguration gc;
final AffineTransform tx;

public CachedFilteredImage(Image img, GraphicsConfiguration gc, AffineTransform tx) {
Parameters.notNull("img", img);
Parameters.notNull("gc", gc);
Parameters.notNull("tx", tx);
this.img = img;
this.gc = gc;
this.tx = tx;
}
}
}

private static class DisabledButtonFilter extends RGBImageFilter {
/** Shared instance of filter for disabled icons */
private static final RGBImageFilter INSTANCE = new DisabledButtonFilter();

DisabledButtonFilter() {
canFilterIndexColorModel = true;
Expand All@@ -935,5 +1021,10 @@ public void setProperties(Hashtable props) {
props = (Hashtable) props.clone();
consumer.setProperties(props);
}

static Image createDisabledImage(Image img) {
ImageProducer prod = new FilteredImageSource(img.getSource(), INSTANCE);
return Toolkit.getDefaultToolkit().createImage(prod);
}
}
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 118 additions & 27 deletions platform/openide.util.ui/src/org/openide/util/ImageUtilities.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,20 +19,21 @@

package org.openide.util;

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.HeadlessException;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Toolkit;
import java.awt.Transparency;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageObserver;
import java.awt.image.ImageProducer;
import java.awt.image.IndexColorModel;
import java.awt.image.RGBImageFilter;
import java.awt.image.WritableRaster;
import java.io.IOException;
Expand DownExpand Up@@ -342,7 +343,7 @@ public static final Image addToolTipToImage(Image image, String text) {
*/
public static Icon createDisabledIcon(Icon icon) {
Parameters.notNull("icon", icon);
return new LazyDisabledIcon(icon2Image(icon));
return new DisabledIcon(icon);
}

/**
Expand All@@ -353,7 +354,7 @@ public static Icon createDisabledIcon(Icon icon) {
*/
public static Image createDisabledImage(Image image) {
Parameters.notNull("image", image);
return LazyDisabledIcon.createDisabledImage(image);
return DisabledButtonFilter.createDisabledImage(image);
}

/**
Expand DownExpand Up@@ -880,44 +881,129 @@ public Object getProperty(String name, ImageObserver observer) {
}
}

private static class LazyDisabledIcon implements Icon {

/** Shared instance of filter for disabled icons */
private static final RGBImageFilter DISABLED_BUTTON_FILTER = new DisabledButtonFilter();
private Image img;
private Icon disabledIcon;

public LazyDisabledIcon(Image img) {
assert null != img;
this.img = img;
}

public void paintIcon(Component c, Graphics g, int x, int y) {
getDisabledIcon().paintIcon(c, g, x, y);
/* This method was tested with both a VectorIcon and an ImageIcon, including with the latter
delegating to a MultiResolutionImage, on OpenJDK 10.0.2 on Windows 10 with a HiDPI/non-HiDPI
dual monitor setup. Also tested the VectorIcon case on MacOS with Retina and non-Retina
configurations. */
/**
* A disabled variation of a provided delegate icon. Any kind of delegate implementation can be
* used. In particular, this class preserves the full fidelity of HiDPI icons, such as instances
* of {@link VectorIcon}, or {@link ImageIcon} instances delegating to a
* {@code java.awt.image.MultiResolutionImage} (available since Java 9 and above).
*/
private static final class DisabledIcon implements Icon {
private final Icon delegate;
/* Since previous versions of ImageUtilities.createDisabledImage always cached the
generated image, we do so here as well. If the associated transform changes, for instance
if the user moves the window to a different monitor with a different HiDPI scaling level,
the icon is regenerated. Don't bother trying to keep multiple versions of the same icon
around; this could lead to memory leaks if GraphicsConfiguration/AffineTransform equals()
checks fail for some reason. */
private volatile CachedFilteredImage cachedFilteredImage;

private DisabledIcon(Icon delegate) {
Parameters.notNull("delegate", delegate);
this.delegate = delegate;
}

@Override
public int getIconWidth() {
return getDisabledIcon().getIconWidth();
return delegate.getIconWidth();
}

@Override
public int getIconHeight() {
return getDisabledIcon().getIconHeight();
return delegate.getIconHeight();
}

@Override
public void paintIcon(Component c, Graphics g0, int x, int y) {
final Graphics2D g = (Graphics2D) g0;
final AffineTransform oldTransform = g.getTransform();
g.translate(x, y);
final AffineTransform tx = g.getTransform();

/* Paint the icon to a BufferedImage, then run it through the ImageFilter. If the Graphics
object has a scaling transform on it, it might be for HiDPI rendering purposes. In this case we
should create a correspondingly larger BufferedImage to preserve the full device resolution. */
final double scaling;
final int txType = tx.getType();
AffineTransform useImageTransform;
if (txType == AffineTransform.TYPE_UNIFORM_SCALE ||
txType == (AffineTransform.TYPE_UNIFORM_SCALE | AffineTransform.TYPE_TRANSLATION))
{
scaling = tx.getScaleX();
/* Apply the scaling. Also preserve any device pixel misalignment that exists in the
original Graphics2D's transform, in case the delegate Icon knows how to handle it. See
the VectorIcon class for an example of such handling. */
double misalignX = tx.getTranslateX() - (int) tx.getTranslateX();
double misalignY = tx.getTranslateY() - (int) tx.getTranslateY();
useImageTransform = new AffineTransform(scaling, 0, 0, scaling, misalignX, misalignY);
} else {
// Unsupported transform type; don't apply HiDPI scaling.
scaling = 1.0;
useImageTransform = AffineTransform.getScaleInstance(1.0, 1.0);
}
Image filteredImage = getFilteredImage(c, g.getDeviceConfiguration(), useImageTransform);
if (scaling != 1.0) {
// Scale the image down to its logical dimensions, and draw it at the device pixel boundary.
AffineTransform tx2 = g.getTransform();
g.setTransform(new AffineTransform(1, 0, 0, 1,
(int) tx2.getTranslateX(),
(int) tx2.getTranslateY()));
}
g.drawImage(filteredImage, 0, 0, null);
g.setTransform(oldTransform);
}

private synchronized Icon getDisabledIcon() {
if (null == disabledIcon) {
disabledIcon = new ImageIcon(createDisabledImage(img));
/**
* @param tx transform that incorporates HiDPI scaling and any HiDPI misalignment adjustment
*/
private Image getFilteredImage(Component c, GraphicsConfiguration gc, AffineTransform tx) {
CachedFilteredImage ret = this.cachedFilteredImage;
/* Technically, the "Component c" parameter may also contain state that the delegate
might use during painting here. But we don't know what that state might be, so we
have no way to compare it for the sake of determining if the cache is still valid or
not. Allow the image to be cached anyway, retaining the behavior of the previous version
of ImageUtilities.createDisabledImage. */
if (ret != null && ret.gc.equals(gc) && ret.tx.equals(tx)) {
return ret.img;
}
return disabledIcon;
final BufferedImage img = gc.createCompatibleImage(
(int) Math.ceil(getIconWidth() * tx.getScaleX()),
(int) Math.ceil(getIconHeight() * tx.getScaleY()), Transparency.TRANSLUCENT);
final Graphics2D imgG = (Graphics2D) img.getGraphics();
try {
imgG.setTransform(tx);
delegate.paintIcon(c, imgG, 0, 0);
} finally {
imgG.dispose();
}
ret = new CachedFilteredImage(DisabledButtonFilter.createDisabledImage(img), gc, tx);
this.cachedFilteredImage = ret;
return ret.img;
}

static Image createDisabledImage(Image img) {
ImageProducer prod = new FilteredImageSource(img.getSource(), DISABLED_BUTTON_FILTER);
return Toolkit.getDefaultToolkit().createImage(prod);
// Immutable.
private static final class CachedFilteredImage {
final Image img;
final GraphicsConfiguration gc;
final AffineTransform tx;

public CachedFilteredImage(Image img, GraphicsConfiguration gc, AffineTransform tx) {
Parameters.notNull("img", img);
Parameters.notNull("gc", gc);
Parameters.notNull("tx", tx);
this.img = img;
this.gc = gc;
this.tx = tx;
}
}
}

private static class DisabledButtonFilter extends RGBImageFilter {
/** Shared instance of filter for disabled icons */
private static final RGBImageFilter INSTANCE = new DisabledButtonFilter();

DisabledButtonFilter() {
canFilterIndexColorModel = true;
Expand All@@ -935,5 +1021,10 @@ public void setProperties(Hashtable props) {
props = (Hashtable) props.clone();
consumer.setProperties(props);
}

static Image createDisabledImage(Image img) {
ImageProducer prod = new FilteredImageSource(img.getSource(), INSTANCE);
return Toolkit.getDefaultToolkit().createImage(prod);
}
}
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 118 additions & 27 deletions platform/openide.util.ui/src/org/openide/util/ImageUtilities.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,20 +19,21 @@

package org.openide.util;

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.HeadlessException;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Toolkit;
import java.awt.Transparency;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageObserver;
import java.awt.image.ImageProducer;
import java.awt.image.IndexColorModel;
import java.awt.image.RGBImageFilter;
import java.awt.image.WritableRaster;
import java.io.IOException;
Expand DownExpand Up@@ -342,7 +343,7 @@ public static final Image addToolTipToImage(Image image, String text) {
*/
public static Icon createDisabledIcon(Icon icon) {
Parameters.notNull("icon", icon);
return new LazyDisabledIcon(icon2Image(icon));
return new DisabledIcon(icon);
}

/**
Expand All@@ -353,7 +354,7 @@ public static Icon createDisabledIcon(Icon icon) {
*/
public static Image createDisabledImage(Image image) {
Parameters.notNull("image", image);
return LazyDisabledIcon.createDisabledImage(image);
return DisabledButtonFilter.createDisabledImage(image);
}

/**
Expand DownExpand Up@@ -880,44 +881,129 @@ public Object getProperty(String name, ImageObserver observer) {
}
}

private static class LazyDisabledIcon implements Icon {

/** Shared instance of filter for disabled icons */
private static final RGBImageFilter DISABLED_BUTTON_FILTER = new DisabledButtonFilter();
private Image img;
private Icon disabledIcon;

public LazyDisabledIcon(Image img) {
assert null != img;
this.img = img;
}

public void paintIcon(Component c, Graphics g, int x, int y) {
getDisabledIcon().paintIcon(c, g, x, y);
/* This method was tested with both a VectorIcon and an ImageIcon, including with the latter
delegating to a MultiResolutionImage, on OpenJDK 10.0.2 on Windows 10 with a HiDPI/non-HiDPI
dual monitor setup. Also tested the VectorIcon case on MacOS with Retina and non-Retina
configurations. */
/**
* A disabled variation of a provided delegate icon. Any kind of delegate implementation can be
* used. In particular, this class preserves the full fidelity of HiDPI icons, such as instances
* of {@link VectorIcon}, or {@link ImageIcon} instances delegating to a
* {@code java.awt.image.MultiResolutionImage} (available since Java 9 and above).
*/
private static final class DisabledIcon implements Icon {
private final Icon delegate;
/* Since previous versions of ImageUtilities.createDisabledImage always cached the
generated image, we do so here as well. If the associated transform changes, for instance
if the user moves the window to a different monitor with a different HiDPI scaling level,
the icon is regenerated. Don't bother trying to keep multiple versions of the same icon
around; this could lead to memory leaks if GraphicsConfiguration/AffineTransform equals()
checks fail for some reason. */
private volatile CachedFilteredImage cachedFilteredImage;

private DisabledIcon(Icon delegate) {
Parameters.notNull("delegate", delegate);
this.delegate = delegate;
}

@Override
public int getIconWidth() {
return getDisabledIcon().getIconWidth();
return delegate.getIconWidth();
}

@Override
public int getIconHeight() {
return getDisabledIcon().getIconHeight();
return delegate.getIconHeight();
}

@Override
public void paintIcon(Component c, Graphics g0, int x, int y) {
final Graphics2D g = (Graphics2D) g0;
final AffineTransform oldTransform = g.getTransform();
g.translate(x, y);
final AffineTransform tx = g.getTransform();

/* Paint the icon to a BufferedImage, then run it through the ImageFilter. If the Graphics
object has a scaling transform on it, it might be for HiDPI rendering purposes. In this case we
should create a correspondingly larger BufferedImage to preserve the full device resolution. */
final double scaling;
final int txType = tx.getType();
AffineTransform useImageTransform;
if (txType == AffineTransform.TYPE_UNIFORM_SCALE ||
txType == (AffineTransform.TYPE_UNIFORM_SCALE | AffineTransform.TYPE_TRANSLATION))
{
scaling = tx.getScaleX();
/* Apply the scaling. Also preserve any device pixel misalignment that exists in the
original Graphics2D's transform, in case the delegate Icon knows how to handle it. See
the VectorIcon class for an example of such handling. */
double misalignX = tx.getTranslateX() - (int) tx.getTranslateX();
double misalignY = tx.getTranslateY() - (int) tx.getTranslateY();
useImageTransform = new AffineTransform(scaling, 0, 0, scaling, misalignX, misalignY);
} else {
// Unsupported transform type; don't apply HiDPI scaling.
scaling = 1.0;
useImageTransform = AffineTransform.getScaleInstance(1.0, 1.0);
}
Image filteredImage = getFilteredImage(c, g.getDeviceConfiguration(), useImageTransform);
if (scaling != 1.0) {
// Scale the image down to its logical dimensions, and draw it at the device pixel boundary.
AffineTransform tx2 = g.getTransform();
g.setTransform(new AffineTransform(1, 0, 0, 1,
(int) tx2.getTranslateX(),
(int) tx2.getTranslateY()));
}
g.drawImage(filteredImage, 0, 0, null);
g.setTransform(oldTransform);
}

private synchronized Icon getDisabledIcon() {
if (null == disabledIcon) {
disabledIcon = new ImageIcon(createDisabledImage(img));
/**
* @param tx transform that incorporates HiDPI scaling and any HiDPI misalignment adjustment
*/
private Image getFilteredImage(Component c, GraphicsConfiguration gc, AffineTransform tx) {
CachedFilteredImage ret = this.cachedFilteredImage;
/* Technically, the "Component c" parameter may also contain state that the delegate
might use during painting here. But we don't know what that state might be, so we
have no way to compare it for the sake of determining if the cache is still valid or
not. Allow the image to be cached anyway, retaining the behavior of the previous version
of ImageUtilities.createDisabledImage. */
if (ret != null && ret.gc.equals(gc) && ret.tx.equals(tx)) {
return ret.img;
}
return disabledIcon;
final BufferedImage img = gc.createCompatibleImage(
(int) Math.ceil(getIconWidth() * tx.getScaleX()),
(int) Math.ceil(getIconHeight() * tx.getScaleY()), Transparency.TRANSLUCENT);
final Graphics2D imgG = (Graphics2D) img.getGraphics();
try {
imgG.setTransform(tx);
delegate.paintIcon(c, imgG, 0, 0);
} finally {
imgG.dispose();
}
ret = new CachedFilteredImage(DisabledButtonFilter.createDisabledImage(img), gc, tx);
this.cachedFilteredImage = ret;
return ret.img;
}

static Image createDisabledImage(Image img) {
ImageProducer prod = new FilteredImageSource(img.getSource(), DISABLED_BUTTON_FILTER);
return Toolkit.getDefaultToolkit().createImage(prod);
// Immutable.
private static final class CachedFilteredImage {
final Image img;
final GraphicsConfiguration gc;
final AffineTransform tx;

public CachedFilteredImage(Image img, GraphicsConfiguration gc, AffineTransform tx) {
Parameters.notNull("img", img);
Parameters.notNull("gc", gc);
Parameters.notNull("tx", tx);
this.img = img;
this.gc = gc;
this.tx = tx;
}
}
}

private static class DisabledButtonFilter extends RGBImageFilter {
/** Shared instance of filter for disabled icons */
private static final RGBImageFilter INSTANCE = new DisabledButtonFilter();

DisabledButtonFilter() {
canFilterIndexColorModel = true;
Expand All@@ -935,5 +1021,10 @@ public void setProperties(Hashtable props) {
props = (Hashtable) props.clone();
consumer.setProperties(props);
}

static Image createDisabledImage(Image img) {
ImageProducer prod = new FilteredImageSource(img.getSource(), INSTANCE);
return Toolkit.getDefaultToolkit().createImage(prod);
}
}
}