VAqua.src.org.violetlib.aqua.AquaUtils Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of vaqua Show documentation
Show all versions of vaqua Show documentation
An improved native Swing look and feel for macOS
The newest version!
/*
* Copyright (c) 2015-2023 Alan Snyder.
* All rights reserved.
*
* You may not use, copy or modify this file, except in compliance with the license agreement. For details see
* accompanying license terms.
*/
/*
* Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.violetlib.aqua;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.lang.ref.SoftReference;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.function.Supplier;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.plaf.MenuBarUI;
import javax.swing.plaf.UIResource;
import javax.swing.text.View;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.violetlib.aqua.AquaImageFactory.SlicedImageControl;
import org.violetlib.geom.GeneralRoundRectangle;
import org.violetlib.jnr.Insets2D;
import org.violetlib.jnr.Insetter;
import org.violetlib.jnr.aqua.AquaUIPainter;
import static javax.swing.SwingConstants.*;
import static org.violetlib.aqua.JavaSupport.FocusEventCause.*;
final public class AquaUtils {
public static final String TOOLBAR_PANEL_PROPERTY = "Aqua.isToolBarPanel";
public static final String IS_CELL_COMPONENT_KEY = "JComponent.isCellComponent";
public static final int NSModalPanelWindowLevel = 8;
public static final int NSPopUpMenuWindowLevel = 101;
private static final String ANIMATIONS_PROPERTY = "swing.enableAnimations";
private static final int INSET_CORNER_RADIUS = 10;
private static final HierarchyListener toolbarStatusListener = new HierarchyListener() {
@Override
public void hierarchyChanged(HierarchyEvent e) {
toolbarStatusChanged(e);
}
};
private static final HierarchyListener insetViewHierarchyListener = new InsetViewHierarchyListener();
public static @NotNull String capitalize(@NotNull String s)
{
if (!s.isEmpty()) {
char first = s.charAt(0);
if (!Character.isUpperCase(first)) {
return Character.toUpperCase(first) + s.substring(1);
}
}
return s;
}
private interface WindowAppearanceChangedCallback {
void windowAppearanceChanged(@NotNull Window w, @NotNull String appearanceName);
}
/**
* Suppresses default constructor, ensuring non-instantiability.
*/
private AquaUtils() {
}
static {
registerWindowChangedAppearanceCallback(new WindowAppearanceChangedCallback() {
@Override
public void windowAppearanceChanged(@NotNull Window w, @NotNull String appearanceName) {
if (w instanceof RootPaneContainer) {
RootPaneContainer rpc = (RootPaneContainer) w;
SwingUtilities.invokeLater(() -> {
JRootPane rootPane = rpc.getRootPane();
AquaRootPaneUI ui = getUI(rootPane, AquaRootPaneUI.class);
if (ui != null) {
ui.windowAppearanceChanged(appearanceName);
}
});
}
}
});
};
/**
* Return the UI of a component if it satisfies the specified class or interface.
*/
public static @Nullable T getUI(@NotNull JComponent c, Class requestedClass) {
// The getUI() method is public as of Java 9
try {
Class extends JComponent> clazz = c.getClass();
Method getUIMethod = clazz.getMethod("getUI");
Object ui = getUIMethod.invoke(c);
return requestedClass.cast(ui);
} catch (Throwable th) {
return null;
}
}
public static @NotNull String getWindowNameForDebugging(@NotNull Window w) {
String name = w.getName();
if (name == null) {
name = w.getClass().getName() + "@" + Integer.toHexString(w.hashCode());
}
if (w instanceof Frame) {
Frame fr = (Frame) w;
String title = fr.getTitle();
if (title != null) {
if (title.length() > 20) {
title = title.substring(0, 20) + "...";
}
name += " \"" + title + "\"";
}
}
return name;
}
public static Insets combineAsInsets(Insetter s, Insets adjustments) {
if (s != null) {
Insets n = s.asInsets();
return combineInsets(n, adjustments);
} else {
return adjustments != null ? adjustments : new Insets(0, 0, 0, 0);
}
}
public static Insets2D combineAsInsets2D(Insetter s, Insets adjustments) {
if (s != null) {
Insets2D n = s.asInsets2D();
return combineInsets(n, adjustments);
} else {
return adjustments != null ? asInsets2D(adjustments) : new Insets2D(0, 0, 0, 0);
}
}
public static Insets combineInsets(Insets s1, Insets s2) {
if (s1 != null) {
if (s2 != null) {
return new Insets(s1.top + s2.top, s1.left + s2.left, s1.bottom + s2.bottom, s1.right + s2.right);
} else {
return s1;
}
} else if (s2 != null) {
return s2;
} else {
return new Insets(0, 0, 0, 0);
}
}
public static Insets2D combineInsets(Insets2D s1, Insets s2) {
if (s1 != null) {
if (s2 != null) {
return new Insets2D(s1.getTop() + s2.top, s1.getLeft() + s2.left, s1.getBottom() + s2.bottom, s1.getRight() + s2.right);
} else {
return s1;
}
} else if (s2 != null) {
return asInsets2D(s2);
} else {
return new Insets2D(0, 0, 0, 0);
}
}
public static Insets2D asInsets2D(Insets s) {
return new Insets2D(s.top, s.left, s.bottom, s.right);
}
public static Rectangle toRectangle(Rectangle2D r) {
if (r instanceof Rectangle) {
return (Rectangle) r;
}
int x = (int) Math.floor(r.getX());
int y = (int) Math.floor(r.getY());
int width = (int) Math.ceil(r.getWidth());
int height = (int) Math.ceil(r.getHeight());
return new Rectangle(x, y, width, height);
}
public static Rectangle toMinimumRectangle(Rectangle2D r) {
if (r instanceof Rectangle) {
return (Rectangle) r;
}
double xx = r.getX();
double ww = r.getWidth();
int x = (int) Math.ceil(xx);
ww -= (x - xx);
int w = (int) Math.floor(ww);
double yy = r.getY();
double hh = r.getHeight();
int y = (int) Math.ceil(yy);
hh -= (y - yy);
int h = (int) Math.floor(hh);
return new Rectangle(x, y, w, h);
}
/**
* Return the location of a component in screen coordinates.
*/
public static Point getScreenLocation(Component c) {
Point p = new Point(0, 0);
SwingUtilities.convertPointToScreen(p, c);
return p;
}
/**
* Obtain the usable bounds of the screen at a given location.
* This code taken from JPopupMenu.
*/
public static Rectangle getScreenBounds(@NotNull Point location, @Nullable Component invoker) {
GraphicsConfiguration gc = getCurrentGraphicsConfiguration(location, invoker);
return getScreenBounds(gc);
}
/**
* Obtain the usable bounds of the specified screen or the primary screen.
* This code taken from JPopupMenu.
*/
public static @NotNull Rectangle getScreenBounds(@Nullable GraphicsConfiguration gc) {
Rectangle bounds;
Insets insets;
Toolkit toolkit = Toolkit.getDefaultToolkit();
if (gc != null) {
// If we have GraphicsConfiguration use it to get screen bounds
bounds = gc.getBounds();
insets = toolkit.getScreenInsets(gc);
} else {
// If we don't have GraphicsConfiguration use primary screen
bounds = new Rectangle(toolkit.getScreenSize());
insets = new Insets(0, 0, 0, 0);
}
int top = insets.top;
int bottom = insets.bottom;
int left = insets.left;
int right = insets.right;
return new Rectangle(bounds.x + left, bounds.y + top, bounds.width - left - right,
bounds.height - top - bottom);
}
/**
* Try to find GraphicsConfiguration that contains the specified mouse sprite position.
* This code taken from JPopupMenu.
*/
private static @Nullable GraphicsConfiguration getCurrentGraphicsConfiguration(@NotNull Point location,
@Nullable Component invoker) {
GraphicsConfiguration gc = null;
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice[] gds = ge.getScreenDevices();
for (GraphicsDevice gd : gds) {
if (gd.getType() == GraphicsDevice.TYPE_RASTER_SCREEN) {
GraphicsConfiguration dgc = gd.getDefaultConfiguration();
if (dgc.getBounds().contains(location)) {
gc = dgc;
break;
}
}
}
// If not found and we have invoker, ask invoker about his gc
if (gc == null && invoker != null) {
gc = invoker.getGraphicsConfiguration();
}
return gc;
}
/**
* Convenience method to get the root pane of a window.
*/
public static @Nullable JRootPane getRootPane(@NotNull Window w) {
RootPaneContainer rpc = getRootPaneContainer(w);
return rpc != null ? rpc.getRootPane() : null;
}
/**
* Convenience method to get the layered pane of a window.
*/
public static @Nullable JLayeredPane getLayeredPane(@NotNull Window w) {
RootPaneContainer rpc = getRootPaneContainer(w);
return rpc != null ? rpc.getLayeredPane() : null;
}
/**
* Convenience method to map a window to a root pane container.
*/
public static @Nullable RootPaneContainer getRootPaneContainer(@NotNull Window w) {
if (w instanceof RootPaneContainer) {
return (RootPaneContainer) w;
}
// Special case for EmbeddedFrame
if (w.getComponentCount() == 1) {
Component c = w.getComponent(0);
if (c instanceof RootPaneContainer) {
return (RootPaneContainer) c;
}
}
return null;
}
/**
* Convenience function for determining ComponentOrientation. Helps us
* avoid having Munge directives throughout the code.
*/
public static boolean isLeftToRight(Component c) {
return c == null || c.getComponentOrientation().isLeftToRight();
}
public static AquaUIPainter.UILayoutDirection getLayoutDirection(Component c) {
return c == null || c.getComponentOrientation().isLeftToRight() ? AquaUIPainter.UILayoutDirection.LEFT_TO_RIGHT : AquaUIPainter.UILayoutDirection.RIGHT_TO_LEFT;
}
// A component is toolbar sensitive if its layout and/or rendering is different when it is contained in a toolbar or
// a toolbar panel. Changes in toolbar status may occur when the ancestor hierarchy changes. It also may change if
// a container dynamically changes its toolbar panel status. Dynamic changes to toolbar panel status are not
// automatically recognized. The file chooser supports its dynamic toolbar panel status by generating hierarchy
// changed events (using the generateToolbarStatusEvents method, below).
public static void installToolbarSensitivity(@NotNull JComponent c) {
ToolbarSensitiveUI ui = getUI(c, ToolbarSensitiveUI.class);
if (ui != null) {
c.addHierarchyListener(toolbarStatusListener);
}
}
public static void uninstallToolbarSensitivity(@NotNull JComponent c) {
c.removeHierarchyListener(toolbarStatusListener);
}
private static void toolbarStatusChanged(@NotNull HierarchyEvent e) {
Component target = e.getComponent();
if (target instanceof JComponent) {
JComponent jc = (JComponent) target;
ToolbarSensitiveUI ui = getUI(jc, ToolbarSensitiveUI.class);
if (ui != null) {
ui.toolbarStatusChanged(jc);
}
}
}
public static void generateToolbarStatusEvents(@NotNull JComponent c) {
// For a toolbar status change to be recognized by subcomponents, a hierarchy changed event must be delivered
// to each component.
generateToolbarStatusEvents(c, c);
}
private static void generateToolbarStatusEvents(@NotNull JComponent top, @NotNull Component c) {
HierarchyEvent e = new HierarchyEvent(c, HierarchyEvent.HIERARCHY_CHANGED, top, top.getParent());
HierarchyListener[] listeners = c.getHierarchyListeners();
for (HierarchyListener listener : listeners) {
listener.hierarchyChanged(e);
}
if (c instanceof Container) {
Container cc = (Container) c;
int componentCount = cc.getComponentCount();
for (int i = 0; i < componentCount; i++) {
Component child = cc.getComponent(i);
generateToolbarStatusEvents(top, child);
}
}
}
// Any component UI that uses this method should install toolbar sensitivity.
public static boolean isOnToolbar(JComponent b) {
Component parent = b.getParent();
while (parent != null) {
if (isToolBar(parent)) {
return true;
}
parent = parent.getParent();
}
return false;
}
public static boolean isToolBar(@NotNull Component c) {
if (c instanceof JToolBar) {
JToolBar tb = (JToolBar) c;
return !tb.isFloatable();
}
if (c instanceof JComponent) {
JComponent jc = (JComponent) c;
Object o = jc.getClientProperty(TOOLBAR_PANEL_PROPERTY);
return Boolean.TRUE.equals(o);
}
return false;
}
public static boolean isDecorated(@NotNull Window w) {
if (w instanceof Frame) {
return !((Frame) w).isUndecorated();
}
if (w instanceof Dialog) {
return !((Dialog) w).isUndecorated();
}
return false;
}
public static boolean isFullScreenWindow(Window w) {
GraphicsConfiguration gc = w.getGraphicsConfiguration();
GraphicsDevice gd = gc.getDevice();
Window fsw = gd.getFullScreenWindow();
if (fsw != null) {
return fsw == w;
}
// It seems that windows made full screen using a title bar button do not get registered as such with Java...
long result = execute(w, nw -> nativeIsFullScreenWindow(nw) ? 1 : 0);
return result != 0;
}
public static boolean isInsetViewSupported() {
return OSXSystemProperties.OSVersion >= 1016;
}
public static boolean isCellComponent(@NotNull Component c)
{
if (c instanceof JComponent) {
JComponent jc = (JComponent) c;
return Boolean.TRUE.equals(jc.getClientProperty(IS_CELL_COMPONENT_KEY));
}
return false;
}
public static boolean isAutoSelectOnFocusAppropriate(@NotNull FocusEvent e) {
int cause = JavaSupport.getFocusEventCause(e);
switch (cause) {
case TRAVERSAL:
case TRAVERSAL_UP:
case TRAVERSAL_DOWN:
case TRAVERSAL_BACKWARD:
case TRAVERSAL_FORWARD:
case UNKNOWN: // hopefully, a programmatic request for focus
return true;
}
return false;
}
// The following are copied from SwingUtilities, with modification.
/**
* Compute and return the location of the icons origin, the
* location of origin of the text baseline, and a possibly clipped
* version of the compound labels string. Locations are computed
* relative to the viewR rectangle.
* The JComponents orientation (LEADING/TRAILING) will also be taken
* into account and translated into LEFT/RIGHT values accordingly.
*
* @param c the component
* @param fm the instance of {@code FontMetrics}, may be null if the text does not matter
* @param text the text
* @param iconSize the size of the icon, or null if there is no icon
* @param verticalAlignment the vertical alignment
* @param horizontalAlignment the horizontal alignment
* @param verticalTextPosition the vertical text position
* @param horizontalTextPosition the horizontal text position
* @param viewR the available rectangle
* @param iconR the rectangle for the icon
* @param textR the rectangle for the text
* @param textIconGap the gap between text and icon
*
* @return the possibly clipped version of the compound labels string
*/
public static String layoutCompoundLabel(JComponent c,
@Nullable FontMetrics fm,
String text,
@Nullable Dimension iconSize,
int verticalAlignment,
int horizontalAlignment,
int verticalTextPosition,
int horizontalTextPosition,
Rectangle viewR,
Rectangle iconR,
Rectangle textR,
int textIconGap)
{
boolean orientationIsLeftToRight = true;
int hAlign = horizontalAlignment;
int hTextPos = horizontalTextPosition;
if (c != null) {
if (!(c.getComponentOrientation().isLeftToRight())) {
orientationIsLeftToRight = false;
}
}
// Translate LEADING/TRAILING values in horizontalAlignment
// to LEFT/RIGHT values depending on the components orientation
switch (horizontalAlignment) {
case LEADING:
hAlign = (orientationIsLeftToRight) ? LEFT : RIGHT;
break;
case TRAILING:
hAlign = (orientationIsLeftToRight) ? RIGHT : LEFT;
break;
}
// Translate LEADING/TRAILING values in horizontalTextPosition
// to LEFT/RIGHT values depending on the components orientation
switch (horizontalTextPosition) {
case LEADING:
hTextPos = (orientationIsLeftToRight) ? LEFT : RIGHT;
break;
case TRAILING:
hTextPos = (orientationIsLeftToRight) ? RIGHT : LEFT;
break;
}
return layoutCompoundLabelImpl(c,
fm,
text,
iconSize,
verticalAlignment,
hAlign,
verticalTextPosition,
hTextPos,
viewR,
iconR,
textR,
textIconGap);
}
/**
* Compute and return the location of the icons origin, the
* location of origin of the text baseline, and a possibly clipped
* version of the compound labels string. Locations are computed
* relative to the viewR rectangle.
* This layoutCompoundLabel() does not know how to handle LEADING/TRAILING
* values in horizontalTextPosition (they will default to RIGHT) and in
* horizontalAlignment (they will default to CENTER).
* Use the other version of layoutCompoundLabel() instead.
*
* This is the same as SwingUtilities.layoutCompoundLabelImpl, except:
* An icon size is provided instead of the actual icon.
* The algorithm for clipping the text is different. If a text is too long, "..." is
* inserted at the middle of the text instead of at the end.
*/
private static String layoutCompoundLabelImpl(
JComponent c,
@Nullable FontMetrics fm,
String text,
@Nullable Dimension iconSize, // null if no icon
int verticalAlignment,
int horizontalAlignment,
int verticalTextPosition,
int horizontalTextPosition,
Rectangle viewR,
Rectangle iconR,
Rectangle textR,
int textIconGap)
{
/* Initialize the icon bounds rectangle iconR.
*/
if (iconSize != null) {
iconR.width = iconSize.width;
iconR.height = iconSize.height;
} else {
iconR.width = iconR.height = 0;
}
/* Initialize the text bounds rectangle textR. If a null
* or and empty String was specified we substitute "" here
* and use 0,0,0,0 for textR.
*/
boolean textIsEmpty = (text == null) || text.equals("");
int lsb = 0;
View v = null;
if (textIsEmpty) {
textR.width = textR.height = 0;
text = "";
} else {
v = (c != null) ? (View) c.getClientProperty("html") : null;
if (v != null) {
textR.width = (int) v.getPreferredSpan(View.X_AXIS);
textR.height = (int) v.getPreferredSpan(View.Y_AXIS);
} else if (fm != null) {
textR.width = SwingUtilities.computeStringWidth(fm, text);
lsb = getLeftSideBearing(c, fm, text);
if (lsb < 0) {
// If lsb is negative, add it to the width, the
// text bounds will later be adjusted accordingly.
textR.width -= lsb;
}
textR.height = fm.getHeight();
}
}
/* Unless both text and icon are non-null, we effectively ignore
* the value of textIconGap. The code that follows uses the
* value of gap instead of textIconGap.
*/
int gap = (textIsEmpty || (iconSize == null)) ? 0 : textIconGap;
if (!textIsEmpty) {
/* If the label text string is too wide to fit within the available
* space "..." and as many characters as will fit will be
* displayed instead.
*/
int availTextWidth;
if (horizontalTextPosition == CENTER) {
availTextWidth = viewR.width;
} else {
availTextWidth = viewR.width - (iconR.width + gap);
}
if (textR.width > availTextWidth) {
if (v != null || fm == null) {
textR.width = availTextWidth;
} else {
String clipString = "...";
int totalWidth = SwingUtilities.computeStringWidth(fm, clipString);
int nChars;
int len = text.length();
for (nChars = 0; nChars < len; nChars++) {
int charIndex = (nChars % 2 == 0) ? nChars / 2 : len - 1 - nChars / 2;
totalWidth += fm.charWidth(text.charAt(charIndex));
if (totalWidth > availTextWidth) {
break;
}
}
text = text.substring(0, nChars / 2) + clipString + text.substring(len - nChars / 2);
textR.width = SwingUtilities.computeStringWidth(fm, text);
}
}
}
/* Compute textR.x,y given the verticalTextPosition and
* horizontalTextPosition properties
*/
if (verticalTextPosition == TOP) {
if (horizontalTextPosition != CENTER) {
textR.y = 0;
} else {
textR.y = -(textR.height + gap);
}
} else if (verticalTextPosition == CENTER) {
textR.y = (iconR.height / 2) - (textR.height / 2);
} else { // (verticalTextPosition == BOTTOM)
if (horizontalTextPosition != CENTER) {
textR.y = iconR.height - textR.height;
} else {
textR.y = (iconR.height + gap);
}
}
if (horizontalTextPosition == LEFT) {
textR.x = -(textR.width + gap);
} else if (horizontalTextPosition == CENTER) {
textR.x = (iconR.width / 2) - (textR.width / 2);
} else { // (horizontalTextPosition == RIGHT)
textR.x = (iconR.width + gap);
}
/* labelR is the rectangle that contains iconR and textR.
* Move it to its proper position given the labelAlignment
* properties.
*
* To avoid actually allocating a Rectangle, Rectangle.union
* has been inlined below.
*/
int labelR_x = Math.min(iconR.x, textR.x);
int labelR_width = Math.max(iconR.x + iconR.width,
textR.x + textR.width) - labelR_x;
int labelR_y = Math.min(iconR.y, textR.y);
int labelR_height = Math.max(iconR.y + iconR.height,
textR.y + textR.height) - labelR_y;
int dx, dy;
if (verticalAlignment == TOP) {
dy = viewR.y - labelR_y;
} else if (verticalAlignment == CENTER) {
dy = (viewR.y + (viewR.height / 2)) - (labelR_y + (labelR_height / 2));
} else { // (verticalAlignment == BOTTOM)
dy = (viewR.y + viewR.height) - (labelR_y + labelR_height);
}
if (horizontalAlignment == LEFT) {
dx = viewR.x - labelR_x;
} else if (horizontalAlignment == RIGHT) {
dx = (viewR.x + viewR.width) - (labelR_x + labelR_width);
} else { // (horizontalAlignment == CENTER)
dx = (viewR.x + (viewR.width / 2))
- (labelR_x + (labelR_width / 2));
}
/* Translate textR and glypyR by dx,dy.
*/
textR.x += dx;
textR.y += dy;
iconR.x += dx;
iconR.y += dy;
if (lsb < 0) {
// lsb is negative. We previously adjusted the bounds by lsb,
// we now need to shift the x location so that the text is
// drawn at the right location. The result is textR does not
// line up with the actual bounds (on the left side), but we will
// have provided enough space for the text.
textR.width += lsb;
textR.x -= lsb;
}
return text;
}
private static int getLeftSideBearing(JComponent c, FontMetrics fm, String string) {
if ((string == null) || (string.length() == 0)) {
return 0;
}
return nativeGetLeftSideBearing(c, fm, string.charAt(0));
}
public static void paintImmediately(Window w, JComponent c) {
// a possible workaround... the goal is to paint to the AWT view before the window becomes visible
// Note that the paintImmediately() method does nothing if it believes that the component is not visible.
boolean wasVisible = w.isVisible();
if (!wasVisible) {
nativeSetWindowVisibleField(w, true);
}
try {
c.paintImmediately(0, 0, c.getWidth(), c.getHeight());
} finally {
if (!wasVisible) {
nativeSetWindowVisibleField(w, false);
}
}
}
public abstract static class RecyclableSingleton {
final @Nullable T get() {
return getSoftReferenceValue(this, () -> getInstance());
}
void reset() {
put(this, null);
}
protected abstract @Nullable T getInstance();
}
static class RecyclableSingletonFromDefaultConstructor extends RecyclableSingleton {
private final Class clazz;
RecyclableSingletonFromDefaultConstructor(Class clazz) {
this.clazz = clazz;
}
@Override
protected @Nullable T getInstance() {
return instantiate(clazz);
}
}
abstract static class LazyKeyedSingleton {
private Map refs;
V get(K key) {
if (refs == null) refs = new HashMap<>();
V cachedValue = refs.get(key);
if (cachedValue != null) return cachedValue;
V value = getInstance(key);
refs.put(key, value);
return value;
}
protected abstract V getInstance(K key);
}
private static final RecyclableSingleton enableAnimations = new RecyclableSingleton() {
@Override
protected Boolean getInstance() {
String sizeProperty = (String) AccessController.doPrivileged((PrivilegedAction) () -> System.getProperty(
ANIMATIONS_PROPERTY));
return !"false".equals(sizeProperty); // should be true by default
}
};
private static boolean animationsEnabled() {
return enableAnimations.get();
}
private static final int MENU_BLINK_DELAY = 50; // 50ms == 3/60 sec, according to the spec
static void blinkMenu(Selectable selectable) {
if (!animationsEnabled()) return;
try {
selectable.paintSelected(false);
Thread.sleep(MENU_BLINK_DELAY);
selectable.paintSelected(true);
Thread.sleep(MENU_BLINK_DELAY);
} catch (InterruptedException ignored) {
}
}
interface Selectable {
void paintSelected(boolean selected);
}
interface Painter {
void paint(Graphics g, int x, int y, int w, int h);
}
static class ShadowBorder implements Border {
private final Painter prePainter;
private final Painter postPainter;
private final int offsetX;
private final int offsetY;
private final float distance;
private final int blur;
private final Insets insets;
private final ConvolveOp blurOp;
ShadowBorder(Painter prePainter, Painter postPainter, int offsetX, int offsetY, float distance, float intensity, int blur) {
this.prePainter = prePainter;
this.postPainter = postPainter;
this.offsetX = offsetX;
this.offsetY = offsetY;
this.distance = distance;
this.blur = blur;
int halfBlur = blur / 2;
insets = new Insets(halfBlur - offsetY, halfBlur - offsetX, halfBlur + offsetY, halfBlur + offsetX);
float blurry = intensity / (blur * blur);
float[] blurKernel = new float[blur * blur];
for (int i = 0; i < blurKernel.length; i++) blurKernel[i] = blurry;
blurOp = new ConvolveOp(new Kernel(blur, blur, blurKernel));
}
@Override
public final boolean isBorderOpaque() {
return false;
}
@Override
public final Insets getBorderInsets(Component c) {
return insets;
}
@Override
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
BufferedImage img = new BufferedImage(width + blur * 2, height + blur * 2, BufferedImage.TYPE_INT_ARGB_PRE);
paintToImage(img, x, y, width, height);
// debugFrame("border", img);
g.drawImage(img, -blur, -blur, null);
}
private void paintToImage(BufferedImage img, int x, int y, int width, int height) {
// clear the prior image
Graphics2D imgG = (Graphics2D) img.getGraphics();
imgG.setComposite(AlphaComposite.Clear);
imgG.setColor(Color.black);
imgG.fillRect(0, 0, width + blur * 2, height + blur * 2);
int adjX = (int) (x + blur + offsetX + (insets.left * distance));
int adjY = (int) (y + blur + offsetY + (insets.top * distance));
int adjW = (int) (width - (insets.left + insets.right) * distance);
int adjH = (int) (height - (insets.top + insets.bottom) * distance);
// let the delegate paint whatever they want to be blurred
imgG.setComposite(AlphaComposite.DstAtop);
if (prePainter != null) prePainter.paint(imgG, adjX, adjY, adjW, adjH);
imgG.dispose();
// blur the prior image back into the same pixels
imgG = (Graphics2D) img.getGraphics();
imgG.setComposite(AlphaComposite.DstAtop);
imgG.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
imgG.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
imgG.drawImage(img, blurOp, 0, 0);
if (postPainter != null) postPainter.paint(imgG, adjX, adjY, adjW, adjH);
imgG.dispose();
}
}
static class SlicedShadowBorder extends ShadowBorder {
private final SlicedImageControl slices;
SlicedShadowBorder(Painter prePainter, Painter postPainter, int offsetX, int offsetY, float distance, float intensity, int blur, int templateWidth, int templateHeight, int leftCut, int topCut, int rightCut, int bottomCut) {
super(prePainter, postPainter, offsetX, offsetY, distance, intensity, blur);
BufferedImage i = new BufferedImage(templateWidth, templateHeight, BufferedImage.TYPE_INT_ARGB_PRE);
super.paintBorder(null, i.getGraphics(), 0, 0, templateWidth, templateHeight);
// debugFrame("slices", i);
slices = new SlicedImageControl(i, leftCut, topCut, rightCut, bottomCut, false);
}
@Override
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
slices.paint(g, x, y, width, height);
}
}
// options for when to use a magic eraser
public final static int ERASE_IF_TEXTURED = 1<<0; // erase if the window is natively textured
public final static int ERASE_IF_VIBRANT = 1<<1; // erase if the window is vibrant
public final static int ERASE_ALWLAYS = 1<<2;
/**
* Fill the component bounds with the appropriate fill color or magic eraser.
* @param c The component.
*/
public static void fillRect(Graphics g, Component c, int eraserMode) {
fillRect(g, c, eraserMode, 0, 0, c.getWidth(), c.getHeight());
}
/**
* Fill the specified rectangle with the appropriate fill color or magic eraser.
* @param c The component.
*/
public static void fillRect(Graphics g, Component c, int eraserMode, int x, int y, int w, int h) {
Color color = getFillColor(c, eraserMode);
fillRect(g, color, x, y, w, h);
}
/**
* Determine the fill color to use for a component.
* @param c The component.
* @return the fill color, or null to use the magic eraser.
*/
private static Color getFillColor(Component c, int eraserMode) {
if ((eraserMode & ERASE_ALWLAYS) != 0) {
return null;
}
Color bc = c.getBackground();
if (AquaColors.isPriority(bc)) {
return bc;
}
return !isMagicEraser(c, eraserMode) ? bc : null;
}
/**
* Fill the component bounds with the specified fill color or magic eraser.
* @param c The component.
*/
public static void fillRect(Graphics g, Component c, @NotNull Color color, int eraserMode) {
fillRect(g, c, color, eraserMode, 0, 0, c.getWidth(), c.getHeight());
}
/**
* Fill the specified rectangle with the specified fill color or magic eraser.
* @param c The component.
*/
public static void fillRect(Graphics g, Component c, @NotNull Color color, int eraserMode, int x, int y, int w, int h) {
color = getFillColor(c, color, eraserMode);
fillRect(g, color, x, y, w, h);
}
/**
* Determine the fill color to use for a component.
* @param c The component.
* @return the fill color, or null to use the magic eraser.
*/
private static Color getFillColor(Component c, @NotNull Color color, int eraserMode) {
if ((eraserMode & ERASE_ALWLAYS) != 0) {
return null;
}
Color bc = c.getBackground();
if (AquaColors.isPriority(bc)) {
return bc;
}
return !isMagicEraser(c, eraserMode) ? color : null;
}
/**
* Determine whether a component should use a magic eraser instead of painting a background.
* A magic eraser erases the current contents of the frame buffer so that the native window or vibrant view
* background shows through.
* @param c The component.
* @param eraserMode The eraser mode, which selects the features that are tested for.
* @return true if the component should use a magic eraser.
*/
private static boolean isMagicEraser(Component c, int eraserMode) {
if ((eraserMode & ERASE_IF_TEXTURED) != 0) {
JRootPane rootPane = SwingUtilities.getRootPane(c);
if (rootPane != null && isNativeTextured(rootPane)) {
return true;
}
}
if ((eraserMode & ERASE_IF_VIBRANT) != 0) {
if (c instanceof JComponent) {
JComponent jc = (JComponent) c;
if (AquaVibrantSupport.isVibrant(jc)) {
return true;
}
}
}
return false;
}
/**
* Determine the visible bounds of the specified component in the coordinate space of the AWT content view.
* The bounds are normally the bounds of the component. However, if the component is within a viewport view, then
* the bounds are constrained by the viewport.
*
* @param c The component.
* @return the visible bounds, as defined above, in the coordinate space of the top component, or null if the
* component is not visible or not in a window.
*/
public static @Nullable VisibleBounds getVisibleBoundsInContentView(@NotNull Component c) {
if (c.getWidth() > 0 && c.getHeight() > 0) {
Window w = SwingUtilities.getWindowAncestor(c);
if (w != null) {
VisibleBounds bounds = getVisibleBoundsInWindow(c, w);
if (bounds != null) {
Insets s = w.getInsets();
int x = s.left;
int y = s.top;
int sw = w.getWidth() - (s.left + s.right);
int sh = w.getHeight() - (s.top + s.bottom);
Rectangle clip = new Rectangle(x, y, sw, sh);
Rectangle visibleBounds = bounds.visibleBounds.intersection(clip);
Rectangle oldFrame = bounds.frame;
Rectangle frame = new Rectangle(oldFrame.x - x, oldFrame.y - y, oldFrame.width, oldFrame.height);
return new VisibleBounds(visibleBounds, frame);
}
}
}
return null;
}
/**
* Determine the visible bounds of the specified component in the coordinate space of the window.
* The bounds are normally the bounds of the component. However, if the component is within a viewport view, then
* the bounds are constrained by the viewport.
*
* @param c The component.
* @return the visible bounds, as defined above, in the coordinate space of the window, or null if the
* component is not visible or not in a window.
*/
public static @Nullable VisibleBounds getVisibleBoundsInWindow(@NotNull Component c) {
if (c.getWidth() > 0 && c.getHeight() > 0) {
Window w = SwingUtilities.getWindowAncestor(c);
if (w != null) {
return getVisibleBoundsInWindow(c, w);
}
}
return null;
}
private static @Nullable VisibleBounds getVisibleBoundsInWindow(@NotNull Component c, @NotNull Window w) {
if (w.isVisible()) {
Rectangle visibleRegion = new Rectangle();
if (computeVisibleRegion(c, visibleRegion) && !visibleRegion.isEmpty()) {
Rectangle vr = SwingUtilities.convertRectangle(c, visibleRegion, w);
Rectangle frame = SwingUtilities.convertRectangle(c.getParent(), c.getBounds(), w);
return new VisibleBounds(vr, frame);
}
}
return null;
}
private static boolean computeVisibleRegion(@NotNull Component c,
@NotNull Rectangle result) {
if (!c.isVisible()) {
return false;
}
Container p = c.getParent();
if (p == null || !p.isVisible()) {
return false;
}
Rectangle bounds = c.getBounds();
if (p instanceof Window) {
result.setBounds(0, 0, bounds.width, bounds.height);
return true;
} else {
if (!computeVisibleRegion(p, result)) {
return false;
}
result.x -= bounds.x;
result.y -= bounds.y;
SwingUtilities.computeIntersection(0, 0, bounds.width, bounds.height, result);
return true;
}
}
/**
* Determine the appropriate background for a component that displays the window content background color.
* @param c A component in the window.
* @return the color.
*/
public static @NotNull Color getWindowBackground(@NotNull JComponent c) {
EffectName effect = AquaFocusHandler.isActive(c) ? EffectName.EFFECT_NONE : EffectName.EFFECT_DISABLED;
String baseColor = "windowBackground";
JRootPane rp = c.getRootPane();
if (rp != null && isTextured(rp)) {
baseColor = "texturedWindowBackground";
}
return AquaColors.getBackground(c, baseColor, effect);
}
/**
* Determine the appropriate background for a window top or bottom margin.
* This method is not used when the margin is painted with a gradient.
* @param rp The root pane the window.
* @param isTop True for the top margin, false for the bottom margin.
* @return the color.
*/
public static @NotNull Color getWindowMarginBackground(@NotNull JRootPane rp, boolean isTop) {
// In most cases, the margin color when flat matches the content area color.
// One exception is a non-textured window in light mode.
// The other is a dark mode non-textured unified title/tool bar (top margin).
String base = isTextured(rp) ? "TexturedWindowMarginBackground" : "WindowMarginBackground";
String prefix = isTop ? "top" : "bottom";
String suffix = AquaFocusHandler.isActive(rp) ? "" : "_disabled";
String colorName = prefix + base + suffix;
AquaAppearance appearance = AppearanceManager.getAppearance(rp);
Color bc = appearance.getColor(colorName);
if (bc == null) {
// should not happen
Utils.logError("Undefined window margin background color: " + colorName);
return AquaColors.CLEAR;
} else {
return bc;
}
}
public static @NotNull Color getWindowMarginDividerColor(@NotNull JRootPane rp, boolean isTop) {
String base = isTextured(rp) ? "TexturedWindowDivider" : "WindowDivider";
String prefix = isTop ? "top" : "bottom";
String suffix = AquaFocusHandler.isActive(rp) ? "" : "_disabled";
String colorName = prefix + base + suffix;
AquaAppearance appearance = AppearanceManager.getAppearance(rp);
Color color = appearance.getColor(colorName);
if (color == null) {
// should not happen
Utils.logError("Undefined window divider color: " + colorName);
return AquaColors.CLEAR;
} else {
return color;
}
}
public static boolean isTextured(@NotNull JRootPane rp) {
if (isNativeTextured(rp)) {
return true;
}
AquaRootPaneUI ui = getUI(rp, AquaRootPaneUI.class);
if (ui != null) {
AquaCustomStyledWindow customStyledWindow = ui.getCustomStyledWindow();
if (customStyledWindow != null) {
return customStyledWindow.isTextured();
}
}
return false;
}
public static boolean isNativeTextured(@NotNull Window w) {
if (w instanceof RootPaneContainer) {
RootPaneContainer rpc = (RootPaneContainer) w;
JRootPane rootPane = rpc.getRootPane();
return isNativeTextured(rootPane);
}
return false;
}
public static boolean isNativeTextured(@NotNull JRootPane rp) {
Object prop = rp.getClientProperty("apple.awt.brushMetalLook");
if (prop != null) {
if (Boolean.parseBoolean(prop.toString())) {
return true;
}
}
prop = rp.getClientProperty("Window.style");
if (prop != null) {
if (prop.equals("textured")) {
return true;
}
}
return false;
}
private static class InsetViewHierarchyListener implements HierarchyListener {
@Override
public void hierarchyChanged(@NotNull HierarchyEvent e) {
JComponent c = (JComponent) e.getComponent();
AquaViewStyleContainerUI ui = AquaUtils.getUI(c, AquaViewStyleContainerUI.class);
if (ui != null) {
JScrollPane sp = findScrollPaneAncestor(c);
ui.scrollPaneAncestorChanged(sp);
}
}
}
private static @Nullable JScrollPane findScrollPaneAncestor(@NotNull JComponent c) {
Container parent = c.getParent();
if (parent instanceof JViewport) {
parent = parent.getParent();
if (parent instanceof OverlayScrollPaneHack.AquaOverlayViewportHolder) {
parent = parent.getParent();
}
if (parent instanceof JScrollPane) {
return (JScrollPane) parent;
}
}
return null;
}
/**
* Install event listeners to help manage the automatic use of the inset selection style on the specified component.
* @param c The component.
* @throws IllegalArgumentException if {@code c} does not support {@link AquaViewStyleContainerUI}.
*/
public static void installInsetViewListener(@NotNull JComponent c) {
AquaViewStyleContainerUI ui = AquaUtils.getUI(c, AquaViewStyleContainerUI.class);
if (ui != null) {
c.addHierarchyListener(insetViewHierarchyListener);
} else {
throw new IllegalArgumentException("Component must support AquaViewStyleContainerUI");
}
}
/**
* Uninstall the event listeners installed by {@link #installInsetViewListener}.
* @param c The component.
*/
public static void uninstallInsetViewListener(@NotNull Component c) {
c.removeHierarchyListener(insetViewHierarchyListener);
}
/**
* Apply equal side margins to the specified bounds.
*/
public static @NotNull Rectangle applySideMargins(@NotNull Rectangle r, int margin) {
if (margin > 0) {
r = new Rectangle(r.x + margin, r.y, r.width - 2 * margin, r.height);
}
return r.isEmpty() ? new Rectangle(0, 0, 0, 0) : r;
}
/**
* Paint the isolated inset style highlight for a striped row.
*/
public static void paintInsetStripedRow(@NotNull Graphics2D g, int cx, int cy, int cw, int ch) {
int top = 0;
int side = 10;
int radius = INSET_CORNER_RADIUS;
RoundRectangle2D r = new RoundRectangle2D.Float(cx + side, cy + top, cw - 2 * side, ch - 2 * top, radius, radius);
fillAntiAliased(g, r);
}
/**
* Paint the isolated inset style highlight for a selected cell.
*/
public static void paintInsetCellSelection(@NotNull Graphics2D g, int cx, int cy, int cw, int ch) {
int top = 3;
int side = 10;
int radius = INSET_CORNER_RADIUS;
RoundRectangle2D r = new RoundRectangle2D.Float(cx + side, cy + top, cw - 2 * side, ch - 2 * top, radius, radius);
fillAntiAliased(g, r);
}
/**
* Paint the inset style highlight for a selected cell that might be part of a vertical group.
*/
public static void paintInsetCellSelection(@NotNull Graphics2D g, boolean isSelectedAbove, boolean isSelectedBelow,
int cx, int cy, int cw, int ch) {
int top = 0;
int side = 10;
int r = INSET_CORNER_RADIUS;
int x = cx + side;
int w = cw - 2 * side;
Shape s;
if (isSelectedAbove && isSelectedBelow) {
s = new Rectangle(x, cy, w, ch);
} else if (isSelectedAbove) {
s = new GeneralRoundRectangle(x, cy, w, ch - top, 0, 0, 0, 0, r, r, r, r);
} else if (isSelectedBelow) {
s = new GeneralRoundRectangle(x, cy + top, w, ch - top, r, r, r, r, 0, 0, 0, 0);
} else {
s = new RoundRectangle2D.Float(cx + side, cy + top, cw - 2 * side, ch - 2 * top, r, r);
}
fillAntiAliased(g, s);
}
/**
* Paint the inset style highlight for a selected menu item.
*/
public static void paintInsetMenuItemSelection(@NotNull Graphics2D g, int cx, int cy, int cw, int ch) {
int top = 0;
int side = 4;
int radius = INSET_CORNER_RADIUS;
RoundRectangle2D r = new RoundRectangle2D.Float(cx + side, cy + top, cw - 2 * side, ch - 2 * top, radius, radius);
fillAntiAliased(g, r);
}
/**
* Fill shape using anti-aliasing.
*/
public static void fillAntiAliased(@NotNull Graphics2D g, @NotNull Shape s) {
Object preserveAntiAliasingRenderingHint = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.fill(s);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, preserveAntiAliasingRenderingHint);
}
/**
* Draw shape using anti-aliasing.
*/
public static void drawAntiAliased(@NotNull Graphics2D g, @NotNull Shape s) {
Object preserveAntiAliasingRenderingHint = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.draw(s);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, preserveAntiAliasingRenderingHint);
}
/**
* Fill with specified color or erase.
* @param g The graphics context.
* @param color The color to fill, or null to erase
*/
public static void fillRect(Graphics g, @Nullable Color color, int x, int y, int w, int h) {
Graphics cg = g.create();
try {
if (color instanceof GradientColor && cg instanceof Graphics2D) {
GradientColor gradientColor = (GradientColor) color;
Graphics2D gg = (Graphics2D) cg;
if (gradientColor.useMagicEraser()) {
gg.setComposite(AlphaComposite.Src);
gg.setColor(AquaColors.CLEAR);
gg.fillRect(x, y, w, h);
}
Color start = gradientColor.getStart();
Color finish = gradientColor.getFinish();
GradientPaint gp = new GradientPaint(0, y, start, 0, y + h, finish);
gg.setPaint(gp);
gg.fillRect(x, y, w, h);
} else if (color instanceof TintedEraser && cg instanceof Graphics2D) {
TintedEraser tintedEraser = (TintedEraser) color;
Graphics2D gg = (Graphics2D) cg;
gg.setComposite(AlphaComposite.Src);
gg.setColor(AquaColors.CLEAR);
gg.fillRect(x, y, w, h);
cg.setColor(tintedEraser);
cg.fillRect(x, y, w, h);
} else if (color != null && color != AquaColors.MAGIC_ERASER) {
cg.setColor(color);
cg.fillRect(x, y, w, h);
} else if (cg instanceof Graphics2D) {
((Graphics2D) cg).setComposite(AlphaComposite.Src);
cg.setColor(AquaColors.CLEAR);
cg.fillRect(x, y, w, h);
}
} finally {
cg.dispose();
}
}
public static Graphics2D toGraphics2D(Graphics g) {
try {
return (Graphics2D) g;
} catch (ClassCastException ex) {
return null;
}
}
static final Map
© 2015 - 2025 Weber Informatics LLC | Privacy Policy