VAqua.src.org.violetlib.aqua.OverlayPainterComponent 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) 2014-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.
*/
package org.violetlib.aqua;
import java.awt.*;
import javax.swing.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A base class for a component that paints an overlay over a base component. The overlay tracks the base component as
* it moves, changes visibility, or is scrolled in or out of view.
*/
public abstract class OverlayPainterComponent extends JComponent {
protected final @NotNull Insets margins;
private final ComponentTracker tracker;
private @Nullable Component base; // the currently configured base component
private @Nullable Rectangle baseBounds; // the bounds of the base component in our coordinate space
private @Nullable Rectangle visibleBounds; // the bounds within our coordinate space where we may paint or null if not paintable
private @Nullable Window baseWindow; // the window that contains the base component
private int layer; // the layer number of the layer containing the overlay painter component
/**
* Create a component for painting an overlay over a base component.
* @param margins The margins that determine the size of this component. The size of this component is determined
* by adding the margins to the bounds of the base component.
*/
public OverlayPainterComponent(@NotNull Insets margins) {
this.margins = margins;
this.layer = 1;
// We need to know when the base component is added to a containment hierarchy, removed from a containment
// hierarchy, reparented within a containment hierarchy, or its bounds are changed.
tracker = new ComponentTracker() {
@Override
protected void attached(@Nullable Window w) {
if (w != null) {
OverlayPainterComponent.this.windowChanged(w);
OverlayPainterComponent.this.visibleBoundsChanged();
}
}
@Override
protected void detached(@Nullable Window w) {
if (w != null) {
OverlayPainterComponent.this.windowChanged(null);
}
}
@Override
protected void windowChanged(@Nullable Window oldWindow, @Nullable Window newWindow) {
OverlayPainterComponent.this.windowChanged(newWindow);
}
@Override
protected void ancestorChanged() {
OverlayPainterComponent.this.ancestorChanged();
}
@Override
protected void visibleBoundsChanged(@Nullable Window window) {
OverlayPainterComponent.this.visibleBoundsChanged();
}
};
super.setOpaque(false);
super.setFocusable(false);
setVisible(false);
}
/**
* Attach this component to the specified base component.
* @param c The base component, or null to detach this component from any previous base component.
*/
public void attach(@Nullable JComponent c) {
if (base != c) {
layer = -100000;
if (c != null) {
base = c;
tracker.attach(c);
setVisible(true);
repaint();
} else {
setVisible(false);
if (base != null) {
tracker.attach(null);
base = null;
}
}
}
}
/**
* This method has no effect. It does not make sense for an overlay painter to be focusable.
*/
@Override
public final void setFocusable(boolean b) {
}
/**
* Because the pane may be painted using transparency, it must not be made opaque.
*/
@Override
public final void setOpaque(boolean b) {
}
// When looking for the component under the mouse, e.g. to select a cursor, we want to be invisible.
@Override
public boolean contains(int x, int y) {
return false;
}
/**
* Ensure that this component is properly located in the containment hierarchy that contains the base component.
*/
private void windowChanged(@Nullable Window newWindow) {
if (newWindow == baseWindow) {
return;
}
JLayeredPane oldLayeredPane = baseWindow != null ? AquaUtils.getLayeredPane(baseWindow) : null;
JLayeredPane newLayeredPane = newWindow != null ? AquaUtils.getLayeredPane(newWindow) : null;
baseWindow = newWindow;
if (oldLayeredPane != null) {
Container p = getParent();
if (p != null) {
p.remove(this);
baseWindow = null;
}
}
if (newLayeredPane != null) {
addToLayeredPane(newLayeredPane);
baseWindow = newWindow;
}
}
private void ancestorChanged() {
// I'm not sure this method can ever be called...
if (baseWindow != null) {
JLayeredPane layeredPane = AquaUtils.getLayeredPane(baseWindow);
if (layeredPane != null) {
addToLayeredPane(layeredPane);
}
}
}
private void addToLayeredPane(@NotNull JLayeredPane layeredPane) {
assert base != null;
int componentLayer = AquaUtils.getComponentLayer(base);
int overlayLayer = componentLayer + 1;
if (layer != overlayLayer) {
this.layer = overlayLayer;
// If this component is already a child of the layered pane, it must be removed before calling add.
// The problem is that add will remove the component *after* the new index has been computed, with the
// result that the component may be inserted at the wrong position.
layeredPane.remove(this);
layeredPane.add(this, (Integer) layer);
visibleBoundsChanged();
}
}
/**
* Update our information about the bounds of the base component in our coordinate space and the visible bounds
* into which we are allowed to paint.
*/
private void visibleBoundsChanged() {
JRootPane rp = baseWindow != null ? AquaUtils.getRootPane(baseWindow) : null;
if (rp == null || base == null || !base.isVisible()) {
baseBounds = null;
visibleBounds = null;
return;
}
Dimension baseSize = base.getSize();
if (baseSize.width == 0 || baseSize.height == 0) {
baseBounds = null;
visibleBounds = null;
return;
}
JLayeredPane lp = rp.getLayeredPane();
Point baseLoc = SwingUtilities.convertPoint(base.getParent(), base.getLocation(), lp);
int x = baseLoc.x - margins.left;
int y = baseLoc.y - margins.top;
int width = baseSize.width + margins.left + margins.right;
int height = baseSize.height + margins.top + margins.bottom;
if (x != getX() || y != getY() || width != getWidth() || height != getHeight()) {
super.setBounds(x, y, width, height);
repaint();
}
baseBounds = new Rectangle(margins.left, margins.top, baseSize.width, baseSize.height);
visibleBounds = getVisibleBounds(base);
if (visibleBounds != null) {
visibleBounds = new Rectangle(visibleBounds.x + margins.left, visibleBounds.y + margins.top,
visibleBounds.width, visibleBounds.height);
}
}
@Override
public final void paintComponent(@NotNull Graphics g) {
if (visibleBounds != null) {
assert baseBounds != null;
Graphics2D gg = (Graphics2D) g.create();
// Updating the bounds may make the user clip obsolete, so we remove it.
gg.setClip(-1000, -1000, 1000000, 1000000);
gg.clip(visibleBounds);
gg.translate(baseBounds.x, baseBounds.y);
internalPaint(gg);
}
}
/**
* Paint the overlay for the current base component.
* @param g The graphics context. The top left corner of the component is at the origin of the base component.
*/
protected abstract void internalPaint(@NotNull Graphics2D g);
/**
* Determine the bounds within which it is acceptable to paint an overlay for a given base component. The bounds are
* normally the bounds of the root pane. However, if the base component is within a viewport view, then the bounds
* are constrained by the viewport.
*
* @param base The base component.
* @return the visible bounds, as defined above, in the coordinate space of the base component, or null if the
* component is not visible.
*/
protected static Rectangle getVisibleBounds(@NotNull Component base) {
int x = 0;
int y = 0;
Component c = base;
for (;;) {
if (!c.isVisible()) {
return null;
}
if (c instanceof JRootPane) {
Dimension size = c.getSize();
return new Rectangle(x, y, size.width, size.height);
}
if (c instanceof JViewport) {
JViewport p = (JViewport) c;
Rectangle bounds = p.getVisibleRect();
return new Rectangle(x + bounds.x, y + bounds.y, bounds.width, bounds.height);
}
Container parent = c.getParent();
if (parent == null) {
// should not happen
return null;
}
x -= c.getX();
y -= c.getY();
c = parent;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy