VAqua.src.org.violetlib.aqua.AquaCustomStyledWindow 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-2021 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 java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.*;
import javax.swing.border.AbstractBorder;
import javax.swing.border.Border;
import javax.swing.plaf.UIResource;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import static org.violetlib.aqua.AquaUtils.*;
/**
* This class supports custom window styles that use the NSView full content view option on a decorated window. In all
* cases the content pane occupies the entire window and title bar paints over the content pane. The styles differ in
* the treatment of the title bar, the tool bar (if any), and the window background.
*
*
* - {@code STYLE_OVERLAY}
- A normal title bar is used. The content pane will be visible beneath the title bar
* when the title bar is translucent. The default border for the content pane has a top inset that matches the height of
* the title bar.
*
* - {@code STYLE_TRANSPARENT}
- A title bar with a transparent background is used. The content pane will be
* visible beneath the title bar. The default border for the content pane has a top inset that matches the height of the
* title bar.
*
* - {@code STYLE_HIDDEN}
- A title bar with a transparent background is used, and an attempt is made to ensure
* that the title bar does not paint anything. (The application should avoid setting a title on the window.) This
* option is used to create a window with rounded corners but no (apparent) title bar. It is up to the application to
* implement window dragging. Optional top and bottom window margins are painted if defined. If either a top or bottom
* margin is defined, the content pane is set to not-opaque to expose the margin backgrounds.
*
* - {@code STYLE_UNIFIED}
- This option requires a non-floatable JToolBar or toolbar panel as a child component
* of the content pane positioned at the top of the content pane. It creates a unified title bar and tool bar by using a
* transparent title bar, painting a textured window background that includes a gradient under the title bar and tool
* bar, and by installing a default tool bar border with a top inset, so that the tool bar is positioned below the title
* bar. The content pane and tool bar are set to not-opaque to expose the textured background. The window title is
* cleared; the application should avoid setting a title on the window. A mouse listener is attached to the toolbar to
* support dragging the window.
*
* - {@code STYLE_COMBINED}
- This option requires a non-floatable JToolBar or toolbar panel as a child
* component of the content pane positioned at the top of the content pane. It creates a combined title bar and tool bar
* by using a transparent title bar, painting a textured window background that includes a gradient under the title bar
* and tool bar, and by installing a default tool bar border with a left inset, so that the tool bar is positioned to
* the right of the title bar buttons. The content pane and tool bar are set to not-opaque to expose the textured
* background. The window title is cleared; the application should avoid setting a title on the window. A mouse listener
* is attached to the toolbar to support dragging the window.
*
* - {@code STYLE_TEXTURED_HIDDEN}
- This option requires a non-floatable JToolBar or toolbar panel as a child
* component of the content pane positioned at the top of the content pane. It creates a textured window with a tool bar
* instead of a title bar by using a transparent title bar, and by painting a textured window background that includes a
* gradient under the tool bar. The content pane and tool bar are set to not-opaque to expose the textured background.
* The window title is cleared; the application should avoid setting a title on the window. A mouse listener is attached
* to the toolbar to support dragging the window.
*/
public class AquaCustomStyledWindow {
public static class RequiredToolBarNotFoundException extends IllegalArgumentException {
public RequiredToolBarNotFoundException() {
super("Window content pane must contain a non-floatable JToolBar or a toolbar panel identified by "
+ TOOLBAR_PANEL_PROPERTY + "=true");
}
}
public static final int STYLE_OVERLAY = 0; // AKA overlay title bar
public static final int STYLE_TRANSPARENT = 1; // AKA transparent title bar
public static final int STYLE_HIDDEN = 2; // AKA no title bar
public static final int STYLE_UNIFIED = 3; // AKA unified tool bar
public static final int STYLE_TEXTURED_HIDDEN = 4; // AKA textured tool bar
public static final int STYLE_COMBINED = 5; // AKA combined title and tool bar
public static final int STYLE_UNDECORATED = 6; // Internal use
protected final int TITLE_BAR_HEIGHT = OSXSystemProperties.OSVersion >= 1016 ? 27: 22;
protected final int TITLE_BAR_BUTTONS_WIDTH = 78;
protected @Nullable Window w;
protected @Nullable JRootPane rp;
protected final int style;
/**
* The top margin height, if specified by a client property, otherwise -1.
*/
protected int declaredTopMarginHeight;
/**
* The bottom margin height, if specified by a client property, otherwise -1.
*/
protected int declaredBottomMarginHeight;
/**
* The top margin height, if it is fixed based on the window style or the declared top margin height,
* otherwise -1.
*/
protected int fixedTopMarginHeight;
/**
* The bottom margin height, if it is fixed based on the window style or the declared bottom margin height,
* otherwise -1.
*/
protected int fixedBottomMarginHeight;
/**
* The computed top margin height, used when painting and handling mouse events.
*/
protected int topMarginHeight;
/**
* The computed bottom margin height, used when painting and handling mouse events.
*/
protected int bottomMarginHeight;
protected final int titleBarStyle;
/**
* If true, a textured background is painted.
*/
protected final boolean isTextured;
protected JComponent contentPane; // the window content pane
protected JComponent windowToolBar; // the window tool bar, if we care about it -- depends on the style
protected WindowPropertyChangeListener propertyChangeListener;
protected WindowDraggingMouseListener windowDraggingMouseListener;
protected WindowMarginDraggingMouseListener windowMarginDraggingMouseListener;
protected HierarchyListener toolbarHierarchyListener;
/**
* Enable a custom window style.
* @param w The window. If the undecorated window style is selected, the window must not be decorated.
* Otherwise, the window must be decorated. The window should be fully configured and populated. This
* class does not support subsequent replacement of the content pane or the tool bar.
* @param style The window style.
* @param declaredTopMarginHeight The declared top margin height, or -1 if not declared.
* @param declaredBottomMarginHeight The declared bottom margin height, or -1 if not declared.
* @throws IllegalArgumentException if the style is not valid, the window is not appropriately decorated, the content
* pane is not a JComponent, or a required JToolBar or toolbar panel is not found.
*/
public AquaCustomStyledWindow(@NotNull Window w, int style, int declaredTopMarginHeight, int declaredBottomMarginHeight)
throws IllegalArgumentException {
boolean isDecorated = AquaUtils.isDecorated(w);
if (style == STYLE_UNDECORATED) {
if (isDecorated) {
throw new IllegalArgumentException("Window is decorated");
}
} else {
if (!isDecorated) {
throw new IllegalArgumentException("Window is not decorated");
}
}
this.w = w;
JRootPane rootPane = AquaUtils.getRootPane(w);
if (rootPane == null) {
throw new IllegalArgumentException("Window lacks a root pane");
}
this.rp = rootPane;
this.style = style;
this.declaredTopMarginHeight = declaredTopMarginHeight;
this.declaredBottomMarginHeight = declaredBottomMarginHeight;
this.fixedTopMarginHeight = getFixedTopMarginHeight();
this.fixedBottomMarginHeight = getFixedBottomMarginHeight();
contentPane = getContentPane();
if (contentPane == null) {
throw new IllegalArgumentException("Window content pane is not a Swing component");
}
windowToolBar = getWindowToolbar();
titleBarStyle = getTitleBarStyleForWindowStyle(style);
isTextured = getTexturedStyleForWindowStyle(style);
if (isTextured) {
if (windowToolBar == null) {
throw new RequiredToolBarNotFoundException();
}
setupToolbar(windowToolBar);
}
setupContentPane(contentPane);
propertyChangeListener = new WindowPropertyChangeListener();
rp.addPropertyChangeListener(AquaFocusHandler.FRAME_ACTIVE_PROPERTY, propertyChangeListener);
if (style == STYLE_COMBINED) {
AquaUtils.setWindowTitle(w, "");
}
AquaUtils.setTitleBarStyle(w, titleBarStyle);
}
public static void preconfigureWindowStyle(@NotNull Window w, int style) {
int titleBarStyle = getTitleBarStyleForWindowStyle(style);
AquaUtils.preconfigureTitleBarStyle(w, titleBarStyle);
}
public static int getTitleBarStyleForWindowStyle(int style) {
switch (style) {
case STYLE_OVERLAY:
return TITLE_BAR_OVERLAY;
case STYLE_TRANSPARENT:
case STYLE_UNIFIED:
case STYLE_COMBINED:
return TITLE_BAR_TRANSPARENT;
case STYLE_HIDDEN:
case STYLE_TEXTURED_HIDDEN:
return TITLE_BAR_HIDDEN;
case STYLE_UNDECORATED:
return TITLE_BAR_NONE;
default:
throw new IllegalArgumentException("Invalid style");
}
}
public boolean getTexturedStyleForWindowStyle(int style) {
switch (style) {
case STYLE_OVERLAY:
case STYLE_TRANSPARENT:
case STYLE_HIDDEN:
case STYLE_UNDECORATED:
return false;
case STYLE_UNIFIED:
case STYLE_TEXTURED_HIDDEN:
case STYLE_COMBINED:
return true;
default:
throw new IllegalArgumentException("Invalid style");
}
}
/**
* Create a replacement custom styled window without resetting the window properties.
* @param style The window style.
* @param top The declared top margin height, or -1 if not declared.
* @param bottom The declared bottom margin height, or -1 if not declared.
* @return the replacement custom styled window.
* @throws IllegalArgumentException if the style is not valid, the window is not appropriately decorated, the content
* pane is not a JComponent, or a required JToolBar or toolbar panel is not found.
*/
public @NotNull AquaCustomStyledWindow reconfigure(int style, int top, int bottom) {
assert w != null;
AquaCustomStyledWindow replacement = new AquaCustomStyledWindow(w, style, top, bottom);
removeListeners();
// do not dispose, as that alters the window
return replacement;
}
public int getStyle() {
return style;
}
public boolean isValid(int style, int declaredTopMarginHeight, int declaredBottomMarginHeight) {
return style == this.style
&& declaredTopMarginHeight == this.declaredTopMarginHeight
&& declaredBottomMarginHeight == this.declaredBottomMarginHeight;
}
public boolean isTextured() {
return isTextured;
}
public void dispose() {
if (w != null) {
removeListeners();
propertyChangeListener = null;
toolbarHierarchyListener = null;
windowDraggingMouseListener = null;
windowMarginDraggingMouseListener = null;
AquaUtils.setTitleBarStyle(w, AquaUtils.TITLE_BAR_ORDINARY);
if (windowToolBar != null) {
resetBorder(windowToolBar);
}
if (contentPane != null) {
resetBorder(contentPane);
}
w = null;
rp = null;
contentPane = null;
windowToolBar = null;
}
}
private void removeListeners() {
if (rp != null && propertyChangeListener != null) {
rp.removePropertyChangeListener(AquaFocusHandler.FRAME_ACTIVE_PROPERTY, propertyChangeListener);
}
if (windowToolBar != null && toolbarHierarchyListener != null) {
windowToolBar.removeHierarchyListener(toolbarHierarchyListener);
}
if (windowDraggingMouseListener != null) {
windowDraggingMouseListener.detach();
}
if (windowMarginDraggingMouseListener != null) {
windowMarginDraggingMouseListener.detach();
}
}
protected class WindowPropertyChangeListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
rp.repaint();
}
}
protected void setupContentPane(JComponent cp) {
if (style == STYLE_OVERLAY || (style == STYLE_TRANSPARENT && declaredTopMarginHeight < 0)) {
installContentPaneBorder(cp, TITLE_BAR_HEIGHT, 0, 0, 0);
} else {
installContentPaneBorder(cp, 0, 0, 0, 0);
}
// Don't allow the content pane to paint a background unless we are sure there is nothing
// underneath the content pane that we want to be visible.
if (fixedTopMarginHeight != 0 || fixedBottomMarginHeight != 0 || style == STYLE_OVERLAY) {
assert rp != null;
cp.setOpaque(false);
rp.getLayeredPane().setOpaque(false);
attachWindowMarginDraggingMouseListener(cp);
}
}
protected void setupToolbar(JComponent tb) {
tb.setOpaque(false);
Container p = tb;
while ((p = p.getParent()) != contentPane && p != null) {
if (p instanceof JComponent) {
((JComponent) p).setOpaque(false);
}
}
installToolbarBorder(tb);
attachWindowDraggingMouseListener(tb);
attachHierarchyListener(tb);
}
protected void installToolbarBorder(JComponent tb) {
Border b = tb.getBorder();
if (b == null || b instanceof UIResource) {
boolean isTall = AquaToolBarUI.isTallFormatToolBar(tb);
int left = 4;
int top = 4;
int bottom = isTall ? 0 : 4;
if (style == STYLE_UNIFIED) {
tb.setBorder(new CustomToolbarBorder(left, TITLE_BAR_HEIGHT, bottom));
} else if (style == STYLE_COMBINED) {
tb.setBorder(new CustomToolbarBorder(TITLE_BAR_BUTTONS_WIDTH, top, bottom));
} else if (style == STYLE_TEXTURED_HIDDEN){
tb.setBorder(new CustomToolbarBorder(left, top, bottom));
}
}
}
protected void installContentPaneBorder(JComponent c, int top, int left, int bottom, int right) {
Border b = c.getBorder();
if (b == null || b instanceof UIResource) {
c.setBorder(new CustomContentPaneBorder(top, left, bottom, right));
}
}
protected void resetBorder(JComponent c) {
Border b = c.getBorder();
if (b == null || b instanceof UIResource) {
if (c instanceof JToolBar) {
c.setBorder(AquaToolBarUI.getToolBarBorder((JToolBar) c));
} else {
c.setBorder(null);
}
}
}
protected class WindowMarginDraggingMouseListener extends WindowDraggingMouseListener {
public WindowMarginDraggingMouseListener() {
super(0);
}
@Override
protected boolean isDragArea(Component c, Point p) {
int y = p.y;
if (topMarginHeight > 0) {
if (y < topMarginHeight) return true;
}
if (bottomMarginHeight > 0) {
if (y >= c.getHeight() - bottomMarginHeight) {
return true;
}
}
return false;
}
}
protected void attachWindowDraggingMouseListener(JComponent c) {
if (c != null) {
if (windowDraggingMouseListener == null) {
int topExclude = titleBarStyle == TITLE_BAR_TRANSPARENT ? TITLE_BAR_HEIGHT : 0;
windowDraggingMouseListener = new WindowDraggingMouseListener(topExclude);
windowDraggingMouseListener.attach(c);
}
}
}
protected void attachWindowMarginDraggingMouseListener(JComponent c) {
if (c != null) {
if (windowMarginDraggingMouseListener == null) {
windowMarginDraggingMouseListener = new WindowMarginDraggingMouseListener();
windowMarginDraggingMouseListener.attach(c);
}
}
}
protected void attachHierarchyListener(JComponent tb) {
if (toolbarHierarchyListener == null) {
toolbarHierarchyListener = new ToolbarHierarchyListener();
}
tb.addHierarchyListener(toolbarHierarchyListener);
}
public void paintMarginBackgrounds(@NotNull Graphics g) {
// Update the possibly dynamic margin heights
topMarginHeight = calculateTopMarginHeight();
bottomMarginHeight = calculateBottomMarginHeight();
assert rp != null;
if (topMarginHeight > 0) {
paintMarginBackground(g, 0, topMarginHeight, true);
}
if (bottomMarginHeight > 0) {
int y = rp.getHeight() - bottomMarginHeight;
paintMarginBackground(g, y, bottomMarginHeight, false);
}
}
protected void paintMarginBackground(@NotNull Graphics g, int y, int height, boolean isTop) {
if (height > 0) {
Graphics2D gg = (Graphics2D) g.create();
try {
boolean isActive = AquaFocusHandler.isActive(rp);
boolean isSheet = AquaSheetSupport.isSheet(rp);
Color c;
if (isSheet) {
c = AquaColors.CLEAR;
} else {
c = AquaUtils.getWindowMarginBackground(rp, isTop);
}
AquaUtils.fillRect(g, c, 0, y, rp.getWidth(), height);
int dividerY;
if (isTop) {
dividerY = y + height - 1;
} else {
dividerY = y;
}
int rootPaneHeight = rp.getHeight();
if (dividerY < rootPaneHeight) {
int width = rp.getWidth();
paintUnifiedDivider(gg, dividerY, width, isActive, isTop);
}
} finally {
gg.dispose();
}
}
}
protected void paintUnifiedDivider(Graphics2D g, int y, int width, boolean isActive, boolean isTop) {
assert rp != null;
Color c = AquaUtils.getWindowMarginDividerColor(rp, isTop);
AquaUtils.fillRect(g, c, 0, y, width, 1);
}
/**
* Compute a top margin height based only on the style and the declared top margin height.
* @return the top margin height, as described, or -1 if a top margin is not supported or must be computed
* dynamically.
*/
protected int getFixedTopMarginHeight() {
switch (style) {
case STYLE_OVERLAY:
if (declaredTopMarginHeight >= 0) {
return declaredTopMarginHeight + TITLE_BAR_HEIGHT;
} else {
return -1;
}
case STYLE_HIDDEN:
case STYLE_TRANSPARENT:
case STYLE_UNDECORATED:
return declaredTopMarginHeight > 0 ? declaredTopMarginHeight : 0;
default:
return -1;
}
}
/**
* Compute a bottom margin height based only on the style and the declared bottom margin height.
* @return the bottom margin height, as described, or -1 if a bottom margin is not supported or must be computed
* dynamically.
*/
protected int getFixedBottomMarginHeight() {
switch (style) {
case STYLE_OVERLAY:
case STYLE_HIDDEN:
case STYLE_TRANSPARENT:
case STYLE_UNIFIED:
case STYLE_TEXTURED_HIDDEN:
case STYLE_COMBINED:
case STYLE_UNDECORATED:
return declaredBottomMarginHeight > 0 ? declaredBottomMarginHeight : 0;
}
return -1;
}
protected int calculateTopMarginHeight() {
if (fixedTopMarginHeight >= 0) {
return fixedTopMarginHeight;
}
int toolbarHeight = windowToolBar != null ? windowToolBar.getHeight() : 0;
switch (style) {
case STYLE_UNIFIED:
case STYLE_TEXTURED_HIDDEN:
return toolbarHeight;
case STYLE_COMBINED:
return Math.max(toolbarHeight, TITLE_BAR_HEIGHT);
default:
return 0;
}
}
protected int calculateBottomMarginHeight() {
return fixedBottomMarginHeight;
}
protected JComponent getContentPane() {
Container c = rp.getContentPane();
return c instanceof JComponent ? (JComponent) c : null;
}
public @Nullable JComponent getWindowToolbar() {
Container c = rp.getContentPane();
return getWindowToolbar(c);
}
protected @Nullable JComponent getWindowToolbar(Container c) {
int count = c.getComponentCount();
for (int i = 0; i < count; i++) {
Component m = c.getComponent(i);
if (AquaUtils.isToolBar(m)) {
return (JComponent) m;
}
}
for (int i = 0; i < count; i++) {
Component m = c.getComponent(i);
if (m instanceof Container) {
JComponent tb = getWindowToolbar((Container) m);
if (tb != null) {
return tb;
}
}
}
return null;
}
protected class ToolbarHierarchyListener implements HierarchyListener {
@Override
public void hierarchyChanged(HierarchyEvent e) {
if ((e.getChangeFlags() & HierarchyEvent.PARENT_CHANGED) != 0) {
JComponent tb = (JComponent) e.getComponent();
if (SwingUtilities.getWindowAncestor(tb) != w) {
resetBorder(tb);
if (tb == windowToolBar) {
windowToolBar = null;
}
tb.removeHierarchyListener(toolbarHierarchyListener);
if (windowDraggingMouseListener != null) {
windowDraggingMouseListener.detach();
windowDraggingMouseListener = null;
}
}
}
}
}
protected class CustomBorderBase
extends AbstractBorder
implements UIResource {
}
protected class CustomContentPaneBorder extends CustomBorderBase {
private int top;
private int left;
private int bottom;
private int right;
public CustomContentPaneBorder(int top, int left, int bottom, int right) {
this.top = top;
this.left = left;
this.bottom = bottom;
this.right = right;
}
@Override
public Insets getBorderInsets(Component c, Insets insets)
{
insets.top = top;
insets.left = left;
insets.bottom = bottom;
insets.right = right;
return insets;
}
}
protected class CustomToolbarBorder extends CustomBorderBase {
protected int extraTop;
protected int extraLeft;
protected int extraBottom;
public CustomToolbarBorder(int extraLeft, int extraTop, int extraBottom) {
this.extraLeft = extraLeft;
this.extraTop = extraTop;
this.extraBottom = extraBottom;
}
@Override
public Insets getBorderInsets(Component c, Insets insets)
{
Insets margin = c instanceof JToolBar ? ((JToolBar) c).getMargin() : new Insets(0, 0, 0, 0);
insets.left = margin.left + extraLeft;
insets.top = margin.top + extraTop;
insets.right = margin.right;
insets.bottom = margin.bottom + extraBottom + 1;
return insets;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy