diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java
index 9962d69fc..37798c3c9 100644
--- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java
+++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java
@@ -296,6 +296,8 @@ public interface FlatClientProperties
* and should therefore return quickly.
*
This function is invoked on 'AWT-Windows' thread (not 'AWT-EventQueue' thread)
* while processing Windows messages.
+ * On macOS in fullWindowContent mode (since 3.7.2), it is invoked on the AppKit
+ * main thread before the mouse event is dispatched to AWT.
* It must not change any component property or layout because this could cause a dead lock.
*
*
diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeMacLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeMacLibrary.java
index e3f1034cb..d53175a2b 100644
--- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeMacLibrary.java
+++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeMacLibrary.java
@@ -45,7 +45,7 @@
*/
public class FlatNativeMacLibrary
{
- private static int API_VERSION_MACOS = 2004;
+ private static int API_VERSION_MACOS = 2005;
/**
* Checks whether native library is loaded/available.
@@ -71,6 +71,41 @@ public static boolean isLoaded() {
/** @since 3.4 */ public native static boolean isWindowFullScreen( Window window );
/** @since 3.4 */ public native static boolean toggleWindowFullScreen( Window window );
+ /**
+ * Registers a hit-test callback used to decide whether a left mouse down event
+ * in a fullWindowContent macOS window's title bar area should start a native
+ * window drag instead of being delivered to AWT.
+ *
+ * The window must already use {@code apple.awt.fullWindowContent=true}.
+ * Passing the same window twice replaces the previously registered callback.
+ *
+ * @since 3.7.2
+ */
+ public native static boolean setupFullWindowContentTitleBarCaption( Window window, FullWindowContentTitleBarCaptionCallback callback );
+
+ /**
+ * Removes the callback previously installed with
+ * {@link #setupFullWindowContentTitleBarCaption(Window, FullWindowContentTitleBarCaptionCallback)}.
+ *
+ * @since 3.7.2
+ */
+ public native static boolean removeFullWindowContentTitleBarCaption( Window window );
+
+ /** @since 3.7.2 */
+ public interface FullWindowContentTitleBarCaptionCallback {
+ /**
+ * Invoked on the AppKit main thread (not the AWT event dispatching thread)
+ * before the mouse event is dispatched to AWT.
+ * It must not change any component property or layout because this could cause a dead lock.
+ *
+ * @param x x coordinate in AWT window coordinates (origin top-left)
+ * @param y y coordinate in AWT window coordinates (origin top-left)
+ * @return {@code true} if the location should behave as title bar caption
+ * (i.e. start a native window drag instead of being delivered to AWT)
+ */
+ boolean isTitleBarCaptionAt( int x, int y );
+ }
+
/** @since 3.7 */
public static final int
diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatRootPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatRootPaneUI.java
index 2db0382fa..a4344a773 100644
--- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatRootPaneUI.java
+++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatRootPaneUI.java
@@ -199,8 +199,11 @@ protected void uninstallDefaults( JRootPane c ) {
protected void installListeners( JRootPane root ) {
super.installListeners( root );
- if( SystemInfo.isMacFullWindowContentSupported )
+ if( SystemInfo.isMacFullWindowContentSupported ) {
macFullWindowContentListener = FullWindowContentSupport.macInstallListeners( root );
+ if( FlatClientProperties.clientPropertyBoolean( root, "apple.awt.fullWindowContent", false ) )
+ FullWindowContentSupport.macInstallTitleBarCaption( root );
+ }
macInstallWindowBackgroundListener( root );
}
@@ -210,6 +213,7 @@ protected void uninstallListeners( JRootPane root ) {
if( SystemInfo.isMacFullWindowContentSupported ) {
FullWindowContentSupport.macUninstallListeners( root, macFullWindowContentListener );
+ FullWindowContentSupport.macUninstallTitleBarCaption( root );
macFullWindowContentListener = null;
}
macUninstallWindowBackgroundListener( root );
@@ -536,8 +540,13 @@ public void propertyChange( PropertyChangeEvent e ) {
// "apple.awt.fullWindowContent" or FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING
// is usually done before the native window is created
// --> try again when native window is created
- if( e.getNewValue() instanceof Window )
+ if( e.getNewValue() instanceof Window ) {
macInstallFullWindowContentSupport();
+ // title bar caption support also requires a native window
+ if( SystemInfo.isMacFullWindowContentSupported &&
+ FlatClientProperties.clientPropertyBoolean( rootPane, "apple.awt.fullWindowContent", false ) )
+ FullWindowContentSupport.macInstallTitleBarCaption( rootPane );
+ }
break;
case FlatClientProperties.MACOS_WINDOW_BUTTONS_SPACING:
@@ -545,8 +554,13 @@ public void propertyChange( PropertyChangeEvent e ) {
break;
case "apple.awt.fullWindowContent":
- if( SystemInfo.isMacFullWindowContentSupported )
+ if( SystemInfo.isMacFullWindowContentSupported ) {
FullWindowContentSupport.macUpdateFullWindowContentButtonsBoundsProperty( rootPane );
+ if( FlatClientProperties.clientPropertyBoolean( rootPane, "apple.awt.fullWindowContent", false ) )
+ FullWindowContentSupport.macInstallTitleBarCaption( rootPane );
+ else
+ FullWindowContentSupport.macUninstallTitleBarCaption( rootPane );
+ }
break;
}
}
diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTitlePane.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTitlePane.java
index 3c32be0ac..ad2a336ab 100644
--- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTitlePane.java
+++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTitlePane.java
@@ -48,7 +48,6 @@
import java.beans.PropertyChangeListener;
import java.util.List;
import java.util.Objects;
-import java.util.function.Function;
import javax.accessibility.AccessibleContext;
import javax.swing.BorderFactory;
import javax.swing.Box;
@@ -65,7 +64,6 @@
import javax.swing.UIManager;
import javax.swing.border.AbstractBorder;
import javax.swing.border.Border;
-import javax.swing.plaf.ComponentUI;
import com.formdev.flatlaf.FlatClientProperties;
import com.formdev.flatlaf.FlatSystemProperties;
import com.formdev.flatlaf.ui.FlatNativeWindowBorder.WindowTopBorder;
@@ -1145,77 +1143,7 @@ private boolean captionHitTest( Point pt ) {
}
private boolean isTitleBarCaptionAt( Component c, int x, int y ) {
- if( !c.isDisplayable() || !c.isVisible() || !contains( c, x, y ) || c == mouseLayer )
- return true; // continue checking with next component
-
- // check enabled component that has mouse listeners
- if( c.isEnabled() &&
- (c.getMouseListeners().length > 0 ||
- c.getMouseMotionListeners().length > 0) )
- {
- if( !(c instanceof JComponent) )
- return false; // assume that this is not a caption because the component has mouse listeners
-
- // check client property boolean value
- Object caption = ((JComponent)c).getClientProperty( COMPONENT_TITLE_BAR_CAPTION );
- if( caption instanceof Boolean )
- return (boolean) caption;
-
- // if component is not fully layouted, do not invoke function
- // because it is too dangerous that the function tries to layout the component,
- // which could cause a dead lock
- if( !c.isValid() ) {
- // revalidate if necessary so that it is valid when invoked again later
- EventQueue.invokeLater( () -> {
- Window w = SwingUtilities.windowForComponent( c );
- if( w != null )
- w.revalidate();
- else
- c.revalidate();
- } );
-
- return false; // assume that this is not a caption because the component has mouse listeners
- }
-
- if( caption instanceof Function ) {
- // check client property function value
- @SuppressWarnings( "unchecked" )
- Function hitTest = (Function) caption;
- Boolean result = hitTest.apply( new Point( x, y ) );
- if( result != null )
- return result;
- } else {
- // check component UI
- ComponentUI ui = JavaCompatibility2.getUI( (JComponent) c );
- if( !(ui instanceof TitleBarCaptionHitTest) )
- return false; // assume that this is not a caption because the component has mouse listeners
-
- Boolean result = ((TitleBarCaptionHitTest)ui).isTitleBarCaptionAt( x, y );
- if( result != null )
- return result;
- }
-
- // else continue checking children
- }
-
- // check children
- if( c instanceof Container ) {
- for( Component child : ((Container)c).getComponents() ) {
- if( !isTitleBarCaptionAt( child, x - child.getX(), y - child.getY() ) )
- return false;
- }
- }
- return true;
- }
-
- /**
- * Same as {@link Component#contains(int, int)}, but not using that method
- * because it may be overridden by custom components and invoke code that
- * tries to request AWT tree lock on 'AWT-Windows' thread.
- * This could freeze the application if AWT tree is already locked on 'AWT-EventQueue' thread.
- */
- private boolean contains( Component c, int x, int y ) {
- return x >= 0 && y >= 0 && x < c.getWidth() && y < c.getHeight();
+ return FullWindowContentSupport.isTitleBarCaptionAt( c, x, y, mouseLayer );
}
private int lastCaptionHitTestX;
diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FullWindowContentSupport.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FullWindowContentSupport.java
index d5e080b95..bc961b859 100644
--- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FullWindowContentSupport.java
+++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FullWindowContentSupport.java
@@ -18,8 +18,11 @@
import java.awt.Color;
import java.awt.Component;
+import java.awt.Container;
import java.awt.Dimension;
+import java.awt.EventQueue;
import java.awt.Graphics;
+import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.ComponentAdapter;
@@ -28,11 +31,15 @@
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
+import java.util.function.Function;
import javax.swing.JComponent;
+import javax.swing.JLayeredPane;
import javax.swing.JRootPane;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
+import javax.swing.plaf.ComponentUI;
import com.formdev.flatlaf.FlatClientProperties;
+import com.formdev.flatlaf.ui.FlatTitlePane.TitleBarCaptionHitTest;
import com.formdev.flatlaf.util.SystemInfo;
/**
@@ -216,4 +223,168 @@ private static void debugPaintRect( Graphics g, Rectangle r ) {
g.drawLine( r.x, y2, x2, r.y );
FlatUIUtils.resetRenderingHints( g, oldRenderingHints );
}
+
+ /**
+ * Returns whether there is a component at the given location that processes
+ * mouse events; checks {@link FlatClientProperties#COMPONENT_TITLE_BAR_CAPTION}
+ * and {@link TitleBarCaptionHitTest} along the way.
+ *
+ * Used by {@link FlatTitlePane} (Windows native window decorations).
+ *
+ * @param skip optional component that is skipped during traversal
+ * (used to skip {@code FlatTitlePane.mouseLayer})
+ */
+ static boolean isTitleBarCaptionAt( Component c, int x, int y, Component skip ) {
+ if( !c.isDisplayable() || !c.isVisible() || !contains( c, x, y ) || c == skip )
+ return true; // continue checking with next component
+
+ // check enabled component that has mouse listeners
+ if( c.isEnabled() &&
+ (c.getMouseListeners().length > 0 ||
+ c.getMouseMotionListeners().length > 0) )
+ {
+ if( !(c instanceof JComponent) )
+ return false; // assume that this is not a caption because the component has mouse listeners
+
+ // check client property boolean value
+ Object caption = ((JComponent)c).getClientProperty( FlatClientProperties.COMPONENT_TITLE_BAR_CAPTION );
+ if( caption instanceof Boolean )
+ return (boolean) caption;
+
+ // if component is not fully layouted, do not invoke function
+ // because it is too dangerous that the function tries to layout the component,
+ // which could cause a dead lock
+ if( !c.isValid() ) {
+ // revalidate if necessary so that it is valid when invoked again later
+ EventQueue.invokeLater( () -> {
+ Window w = SwingUtilities.windowForComponent( c );
+ if( w != null )
+ w.revalidate();
+ else
+ c.revalidate();
+ } );
+
+ return false; // assume that this is not a caption because the component has mouse listeners
+ }
+
+ if( caption instanceof Function ) {
+ // check client property function value
+ @SuppressWarnings( "unchecked" )
+ Function hitTest = (Function) caption;
+ Boolean result = hitTest.apply( new Point( x, y ) );
+ if( result != null )
+ return result;
+ } else {
+ // check component UI
+ ComponentUI ui = JavaCompatibility2.getUI( (JComponent) c );
+ if( !(ui instanceof TitleBarCaptionHitTest) )
+ return false; // assume that this is not a caption because the component has mouse listeners
+
+ Boolean result = ((TitleBarCaptionHitTest)ui).isTitleBarCaptionAt( x, y );
+ if( result != null )
+ return result;
+ }
+
+ // else continue checking children
+ }
+
+ // check children
+ if( c instanceof Container ) {
+ for( Component child : ((Container)c).getComponents() ) {
+ if( !isTitleBarCaptionAt( child, x - child.getX(), y - child.getY(), skip ) )
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Same as {@link Component#contains(int, int)}, but not using that method
+ * because it may be overridden by custom components and invoke code that
+ * tries to request AWT tree lock on 'AWT-Windows' thread.
+ * This could freeze the application if AWT tree is already locked on 'AWT-EventQueue' thread.
+ */
+ private static boolean contains( Component c, int x, int y ) {
+ return x >= 0 && y >= 0 && x < c.getWidth() && y < c.getHeight();
+ }
+
+ //---- macOS title bar caption support ------------------------------------
+ //
+ // On macOS in fullWindowContent mode, the title bar area is transparent and
+ // Swing content extends into it. To make components marked with
+ // JComponent.titleBarCaption act as window-draggable caption (even when they
+ // have mouse listeners), we register a per-window callback with the native
+ // library. The native side (MacTitleBarCaption.mm) intercepts left mouse
+ // down events in the title bar area before AWT dispatch and, on caption
+ // points, hands off to -[NSWindow performWindowDragWithEvent:].
+
+ static void macInstallTitleBarCaption( JRootPane rootPane ) {
+ if( !SystemInfo.isMacFullWindowContentSupported || !FlatNativeMacLibrary.isLoaded() )
+ return;
+
+ Window window = SwingUtilities.getWindowAncestor( rootPane );
+ if( window == null || !window.isDisplayable() )
+ return;
+
+ FlatNativeMacLibrary.setupFullWindowContentTitleBarCaption( window,
+ new MacTitleBarCaptionCallback( rootPane ) );
+ }
+
+ static void macUninstallTitleBarCaption( JRootPane rootPane ) {
+ if( !SystemInfo.isMacFullWindowContentSupported || !FlatNativeMacLibrary.isLoaded() )
+ return;
+
+ Window window = SwingUtilities.getWindowAncestor( rootPane );
+ if( window == null || !window.isDisplayable() )
+ return;
+
+ FlatNativeMacLibrary.removeFullWindowContentTitleBarCaption( window );
+ }
+
+ private static class MacTitleBarCaptionCallback
+ implements FlatNativeMacLibrary.FullWindowContentTitleBarCaptionCallback
+ {
+ private final WeakReference rootPaneRef;
+
+ MacTitleBarCaptionCallback( JRootPane rootPane ) {
+ this.rootPaneRef = new WeakReference<>( rootPane );
+ }
+
+ /**
+ * Invoked on the AppKit main thread (not the AWT event dispatching
+ * thread), before the mouse event is dispatched to AWT.
+ * Must return quickly and must not change any component property or
+ * layout because this could cause a dead lock.
+ */
+ @Override
+ public boolean isTitleBarCaptionAt( int x, int y ) {
+ JRootPane rootPane = rootPaneRef.get();
+ if( rootPane == null || !rootPane.isShowing() )
+ return false;
+
+ // bail out quickly if click is below the title bar
+ Rectangle buttonsBounds = (Rectangle) rootPane.getClientProperty(
+ FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS );
+ int titleBarHeight = (buttonsBounds != null && buttonsBounds.height > 0)
+ ? buttonsBounds.height
+ : 28; // default size, matches macUpdateFullWindowContentButtonsBoundsProperty()
+
+ // convert from AWT window coordinates to layeredPane coordinates
+ // by walking the parent chain; not using SwingUtilities.convertPoint()
+ // because that calls Component.getLocationOnScreen(), which acquires
+ // the AWT tree lock and could cause a dead lock here
+ JLayeredPane layeredPane = rootPane.getLayeredPane();
+ int dx = 0, dy = 0;
+ for( Component c = layeredPane; c != null && !(c instanceof Window); c = c.getParent() ) {
+ dx += c.getX();
+ dy += c.getY();
+ }
+ int lx = x - dx;
+ int ly = y - dy;
+ if( ly < 0 || ly >= titleBarHeight )
+ return false;
+
+ return FullWindowContentSupport.isTitleBarCaptionAt( layeredPane, lx, ly, null );
+ }
+ }
}
diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/headers/com_formdev_flatlaf_ui_FlatNativeMacLibrary.h b/flatlaf-natives/flatlaf-natives-macos/src/main/headers/com_formdev_flatlaf_ui_FlatNativeMacLibrary.h
index 663f16598..e3e6e5277 100644
--- a/flatlaf-natives/flatlaf-natives-macos/src/main/headers/com_formdev_flatlaf_ui_FlatNativeMacLibrary.h
+++ b/flatlaf-natives/flatlaf-natives-macos/src/main/headers/com_formdev_flatlaf_ui_FlatNativeMacLibrary.h
@@ -87,6 +87,22 @@ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_isWi
JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_toggleWindowFullScreen
(JNIEnv *, jclass, jobject);
+/*
+ * Class: com_formdev_flatlaf_ui_FlatNativeMacLibrary
+ * Method: setupFullWindowContentTitleBarCaption
+ * Signature: (Ljava/awt/Window;Lcom/formdev/flatlaf/ui/FlatNativeMacLibrary/FullWindowContentTitleBarCaptionCallback;)Z
+ */
+JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_setupFullWindowContentTitleBarCaption
+ (JNIEnv *, jclass, jobject, jobject);
+
+/*
+ * Class: com_formdev_flatlaf_ui_FlatNativeMacLibrary
+ * Method: removeFullWindowContentTitleBarCaption
+ * Signature: (Ljava/awt/Window;)Z
+ */
+JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_removeFullWindowContentTitleBarCaption
+ (JNIEnv *, jclass, jobject);
+
/*
* Class: com_formdev_flatlaf_ui_FlatNativeMacLibrary
* Method: showFileChooser
diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/ApiVersion.mm b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/ApiVersion.mm
index 5929ad67c..5f9041470 100644
--- a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/ApiVersion.mm
+++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/ApiVersion.mm
@@ -24,7 +24,7 @@
// increase this version if changing API or functionality of native library
// also update version in Java class com.formdev.flatlaf.ui.FlatNativeMacLibrary
-#define API_VERSION_MACOS 2004
+#define API_VERSION_MACOS 2005
//---- JNI methods ------------------------------------------------------------
diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacTitleBarCaption.mm b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacTitleBarCaption.mm
new file mode 100644
index 000000000..19ead1243
--- /dev/null
+++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacTitleBarCaption.mm
@@ -0,0 +1,293 @@
+/*
+ * Copyright 2026 FormDev Software GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import
+#import
+#import "JNIUtils.h"
+#import "JNFRunLoop.h"
+#import "MacWindowInternal.h"
+#import "com_formdev_flatlaf_ui_FlatNativeMacLibrary.h"
+
+/**
+ * Title bar drag delegation for fullWindowContent windows.
+ *
+ * A FlatTitleBarDragView is inserted as a sibling of the standard window
+ * buttons inside NSTitlebarView, ordered below them. AppKit's normal
+ * hit-testing therefore continues to route traffic-light and contentView
+ * clicks correctly; only clicks on the bare title-bar background land on us.
+ *
+ * On mouseDown we ask the registered Java callback whether the click is over
+ * a caption region. If yes and the user starts dragging, we hand off via
+ * -[NSWindow performWindowDragWithEvent:] so AppKit performs the drag with
+ * native edge-snap, magnetic alignment, etc. Otherwise the click is
+ * forwarded to the contentView and Swing sees it as normal.
+ *
+ * Mirrors the JetBrains Runtime AWTWindowDragView pattern.
+ *
+ * @since 3.7.2
+ */
+
+
+static JavaVM* _jvm = NULL;
+static jmethodID _isTitleBarCaptionAtMethodID = NULL;
+
+// Looks up the callback method ID once and caches it. Returns false if the
+// interface or method cannot be resolved.
+static bool ensureMethodIDs( JNIEnv* env ) {
+ if( _isTitleBarCaptionAtMethodID != NULL )
+ return true;
+ jclass cls = findClass( env,
+ "com/formdev/flatlaf/ui/FlatNativeMacLibrary$FullWindowContentTitleBarCaptionCallback",
+ false );
+ if( cls == NULL )
+ return false;
+ _isTitleBarCaptionAtMethodID = getMethodID( env, cls, "isTitleBarCaptionAt", "(II)Z", false );
+ return _isTitleBarCaptionAtMethodID != NULL;
+}
+
+// Asks the registered Java callback whether the event location is over a
+// caption region. Always invoked on the AppKit main thread.
+static bool isCaptionEvent( NSWindow* nsWindow, NSEvent* event ) {
+ if( nsWindow == NULL || _jvm == NULL || _isTitleBarCaptionAtMethodID == NULL )
+ return false;
+ WindowData* windowData = getWindowData( nsWindow, false );
+ if( windowData == NULL || windowData.titleBarCaptionCallback == NULL )
+ return false;
+
+ // convert event location from NSWindow (bottom-up) to AWT window (top-down) coordinates
+ NSPoint loc = event.locationInWindow;
+ jint x = (jint) loc.x;
+ jint y = (jint) (nsWindow.frame.size.height - loc.y);
+
+ jboolean result = JNI_FALSE;
+ JNI_THREAD_ENTER( _jvm, false )
+ result = env->CallBooleanMethod( windowData.titleBarCaptionCallback,
+ _isTitleBarCaptionAtMethodID, x, y );
+ if( env->ExceptionCheck() ) {
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ result = JNI_FALSE;
+ }
+ JNI_THREAD_EXIT( _jvm )
+ return (bool) result;
+}
+
+
+//---- class FlatTitleBarDragView ---------------------------------------------
+
+@interface FlatTitleBarDragView : NSView {
+ BOOL _captionTracking;
+}
+@end
+
+@implementation FlatTitleBarDragView
+
+ // don't let AppKit start an automatic background drag; we own drag dispatch
+ - (BOOL) mouseDownCanMoveWindow { return NO; }
+
+ // allow first click on an inactive window to be a single activate-and-drag gesture
+ - (BOOL) acceptsFirstMouse:(NSEvent*)event { return YES; }
+
+ // claim every click in our bounds; forwarding to the contentView happens
+ // in the mouse handlers below. Returning nil would let NSTitlebarView treat
+ // the click as a background drag.
+ - (NSView*) hitTest:(NSPoint)pointInSuper {
+ NSPoint local = [self.superview convertPoint:pointInSuper toView:self];
+ return NSPointInRect( local, self.bounds ) ? self : nil;
+ }
+
+ - (void) mouseDown:(NSEvent*)event {
+ NSWindow* w = self.window;
+ _captionTracking = isCaptionEvent( w, event );
+
+ // always forward so Swing sees the click. If the user starts dragging
+ // a caption point, -mouseDragged: hands off to AppKit; otherwise the
+ // full click/release sequence reaches Swing as normal.
+ [w.contentView mouseDown:event];
+ }
+
+ - (void) mouseDragged:(NSEvent*)event {
+ if( _captionTracking ) {
+ // performWindowDragWithEvent: is a synchronous modal loop; the
+ // next mouseDragged: cannot arrive until it returns, so a re-entry
+ // guard isn't needed
+ _captionTracking = NO;
+ NSWindow* w = self.window;
+ [w performWindowDragWithEvent:event];
+
+ // AppKit's modal loop ate the mouseUp, so the original mouseDown
+ // we forwarded never sees a release. Synthesize an off-screen
+ // mouseUp so Swing cancels the press without it counting as a
+ // click on the originally-pressed component.
+ NSEvent* synthMouseUp = [NSEvent mouseEventWithType:NSEventTypeLeftMouseUp
+ location:NSMakePoint( -1, -1 )
+ modifierFlags:event.modifierFlags
+ timestamp:[NSDate timeIntervalSinceReferenceDate]
+ windowNumber:event.windowNumber
+ context:nil
+ eventNumber:event.eventNumber
+ clickCount:0
+ pressure:0.0];
+ [w.contentView mouseUp:synthMouseUp];
+ return;
+ }
+ [self.window.contentView mouseDragged:event];
+ }
+
+ - (void) mouseUp:(NSEvent*)event {
+ [self.window.contentView mouseUp:event];
+
+ if( !_captionTracking || event.clickCount != 2 ) {
+ _captionTracking = NO;
+ return;
+ }
+ _captionTracking = NO;
+
+ // run the user's "double click on title bar" preference
+ NSWindow* w = self.window;
+ NSString* action = [[NSUserDefaults standardUserDefaults]
+ stringForKey:@"AppleActionOnDoubleClick"];
+ if( action == nil || [action caseInsensitiveCompare:@"Maximize"] == NSOrderedSame )
+ [w zoom:nil];
+ else if( [action caseInsensitiveCompare:@"Minimize"] == NSOrderedSame )
+ [w miniaturize:nil];
+ }
+
+@end
+
+
+// Idempotent. Must be invoked on the main thread.
+static void attachTitleBarDragViewIfNeeded( NSWindow* nsWindow ) {
+ WindowData* windowData = getWindowData( nsWindow, true );
+ if( windowData.titleBarDragView != nil ) {
+ // AppKit may have detached the view from outside our control
+ // (e.g. across a full-screen overlay swap). Re-attach if so.
+ if( windowData.titleBarDragView.superview != nil )
+ return;
+ windowData.titleBarDragView = nil;
+ }
+
+ NSView* closeButton = [nsWindow standardWindowButton:NSWindowCloseButton];
+ if( closeButton == nil )
+ return;
+ NSView* titlebar = closeButton.superview;
+ if( titlebar == nil )
+ return;
+
+ FlatTitleBarDragView* dragView = [[FlatTitleBarDragView alloc] initWithFrame:titlebar.bounds];
+ dragView.translatesAutoresizingMaskIntoConstraints = NO;
+ // insert below the buttons so they hit-test first
+ [titlebar addSubview:dragView positioned:NSWindowBelow relativeTo:closeButton];
+ [NSLayoutConstraint activateConstraints:@[
+ [dragView.leadingAnchor constraintEqualToAnchor:titlebar.leadingAnchor],
+ [dragView.trailingAnchor constraintEqualToAnchor:titlebar.trailingAnchor],
+ [dragView.topAnchor constraintEqualToAnchor:titlebar.topAnchor],
+ [dragView.bottomAnchor constraintEqualToAnchor:titlebar.bottomAnchor]
+ ]];
+ windowData.titleBarDragView = dragView;
+}
+
+// Must be invoked on the main thread.
+static void detachTitleBarDragView( NSWindow* nsWindow ) {
+ WindowData* windowData = getWindowData( nsWindow, false );
+ if( windowData == nil || windowData.titleBarDragView == nil )
+ return;
+ [windowData.titleBarDragView removeFromSuperview];
+ windowData.titleBarDragView = nil;
+}
+
+// Releases a JNI global ref using the JNIEnv attached to the current thread.
+// No-op if 'ref' or the cached JavaVM is NULL.
+static void releaseGlobalRefOnMainThread( jobject ref ) {
+ if( ref == NULL || _jvm == NULL )
+ return;
+ JNIEnv* mainEnv;
+ if( _jvm->GetEnv( (void**) &mainEnv, JNI_VERSION_1_6 ) == JNI_OK )
+ mainEnv->DeleteGlobalRef( ref );
+}
+
+
+extern "C"
+JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_setupFullWindowContentTitleBarCaption
+ ( JNIEnv* env, jclass cls, jobject window, jobject callback )
+{
+ JNI_COCOA_ENTER()
+
+ NSWindow* nsWindow = getNSWindow( env, cls, window );
+ if( nsWindow == NULL || callback == NULL )
+ return FALSE;
+
+ // cache JavaVM and method ID once
+ if( _jvm == NULL )
+ env->GetJavaVM( &_jvm );
+ if( !ensureMethodIDs( env ) )
+ return FALSE;
+
+ jobject globalCallback = env->NewGlobalRef( callback );
+ if( globalCallback == NULL )
+ return FALSE;
+
+ [FlatJNFRunLoop performOnMainThreadWaiting:YES withBlock:^(){
+ JNI_COCOA_TRY()
+
+ WindowData* windowData = getWindowData( nsWindow, true );
+
+ // release previous callback (if any)
+ jobject previous = windowData.titleBarCaptionCallback;
+ windowData.titleBarCaptionCallback = globalCallback;
+ releaseGlobalRefOnMainThread( previous );
+
+ attachTitleBarDragViewIfNeeded( nsWindow );
+
+ JNI_COCOA_CATCH()
+ }];
+
+ return TRUE;
+
+ JNI_COCOA_EXIT()
+ return FALSE;
+}
+
+extern "C"
+JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_removeFullWindowContentTitleBarCaption
+ ( JNIEnv* env, jclass cls, jobject window )
+{
+ JNI_COCOA_ENTER()
+
+ NSWindow* nsWindow = getNSWindow( env, cls, window );
+ if( nsWindow == NULL )
+ return FALSE;
+
+ [FlatJNFRunLoop performOnMainThreadWaiting:YES withBlock:^(){
+ JNI_COCOA_TRY()
+
+ WindowData* windowData = getWindowData( nsWindow, false );
+ if( windowData != NULL ) {
+ detachTitleBarDragView( nsWindow );
+
+ jobject previous = windowData.titleBarCaptionCallback;
+ windowData.titleBarCaptionCallback = NULL;
+ releaseGlobalRefOnMainThread( previous );
+ }
+
+ JNI_COCOA_CATCH()
+ }];
+
+ return TRUE;
+
+ JNI_COCOA_EXIT()
+ return FALSE;
+}
diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacWindow.mm b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacWindow.mm
index 1958c60bb..5a428c3d6 100644
--- a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacWindow.mm
+++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacWindow.mm
@@ -19,31 +19,17 @@
#import
#import "JNIUtils.h"
#import "JNFRunLoop.h"
+#import "MacWindowInternal.h"
#import "com_formdev_flatlaf_ui_FlatNativeMacLibrary.h"
/**
* @author Karl Tauber
*/
-@interface WindowData : NSObject
- // used when window is full screen
- @property (nonatomic) int lastWindowButtonAreaWidth;
- @property (nonatomic) int lastWindowTitleBarHeight;
-
- // full screen observers
- @property (nonatomic) id willEnterFullScreenObserver;
- @property (nonatomic) id willExitFullScreenObserver;
- @property (nonatomic) id didExitFullScreenObserver;
-@end
-
@implementation WindowData
@end
-// declare exported methods
-NSWindow* getNSWindow( JNIEnv* env, jclass cls, jobject window );
-
// declare internal methods
-static WindowData* getWindowData( NSWindow* nsWindow, bool allocate );
static void setWindowButtonsHidden( NSWindow* nsWindow, bool hidden );
static int getWindowButtonAreaWidth( NSWindow* nsWindow );
static int getWindowTitleBarHeight( NSWindow* nsWindow );
@@ -81,7 +67,7 @@ @implementation WindowData
return (NSWindow *) jlong_to_ptr( env->GetLongField( platformWindow, ptrID ) );
}
-static WindowData* getWindowData( NSWindow* nsWindow, bool allocate ) {
+WindowData* getWindowData( NSWindow* nsWindow, bool allocate ) {
static char key;
WindowData* windowData = objc_getAssociatedObject( nsWindow, &key );
if( windowData == NULL && allocate ) {
diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacWindowInternal.h b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacWindowInternal.h
new file mode 100644
index 000000000..1d0bada2b
--- /dev/null
+++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacWindowInternal.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2026 FormDev Software GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import
+#import
+
+
+// per-NSWindow state shared between MacWindow.mm and MacTitleBarCaption.mm;
+// associated with the NSWindow via objc_setAssociatedObject in getWindowData()
+@interface WindowData : NSObject
+ // used when window is full screen
+ @property (nonatomic) int lastWindowButtonAreaWidth;
+ @property (nonatomic) int lastWindowTitleBarHeight;
+
+ // full screen observers
+ @property (nonatomic) id willEnterFullScreenObserver;
+ @property (nonatomic) id willExitFullScreenObserver;
+ @property (nonatomic) id didExitFullScreenObserver;
+
+ // title bar caption (MacTitleBarCaption.mm)
+ @property (nonatomic) jobject titleBarCaptionCallback;
+ @property (nonatomic) NSView* titleBarDragView;
+@end
+
+
+// defined in MacWindow.mm
+NSWindow* getNSWindow( JNIEnv* env, jclass cls, jobject window );
+WindowData* getWindowData( NSWindow* nsWindow, bool allocate );
diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSTest.java
index d4887189c..9341a437d 100644
--- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSTest.java
+++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSTest.java
@@ -57,6 +57,11 @@ public static void main( String[] args ) {
placeholderPanel.putClientProperty( FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_PLACEHOLDER, "mac zeroInFullScreen" );
UIManager.put( "FlatLaf.debug.panel.showPlaceholders", true );
+
+ // the two buttons demonstrate caption=true vs caption=false side by
+ // side. Toggled via captionDemoCheckBox below.
+ captionTrueButton.putClientProperty( FlatClientProperties.COMPONENT_TITLE_BAR_CAPTION, true );
+ captionFalseButton.putClientProperty( FlatClientProperties.COMPONENT_TITLE_BAR_CAPTION, false );
}
@Override
@@ -130,6 +135,23 @@ private void toggleFullScreen() {
FlatNativeMacLibrary.toggleWindowFullScreen( window );
}
+ private void captionDemoChanged() {
+ captionDemoButtonsPanel.setVisible( captionDemoCheckBox.isSelected() );
+ }
+
+ private void captionTrueClicked() {
+ captionTrueClickCount++;
+ captionTrueButton.setText( "caption=true (clicks: " + captionTrueClickCount + ")" );
+ }
+
+ private void captionFalseClicked() {
+ captionFalseClickCount++;
+ captionFalseButton.setText( "caption=false (clicks: " + captionFalseClickCount + ")" );
+ }
+
+ private int captionTrueClickCount;
+ private int captionFalseClickCount;
+
private void initComponents() {
// JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
JPanel panel1 = new JPanel();
@@ -151,6 +173,10 @@ private void initComponents() {
JLabel nativeButtonsBoundsLabel = new JLabel();
nativeButtonsBoundsField = new JLabel();
JButton toggleFullScreenButton = new JButton();
+ captionDemoButtonsPanel = new JPanel();
+ captionTrueButton = new JButton();
+ captionFalseButton = new JButton();
+ captionDemoCheckBox = new JCheckBox();
//======== this ========
setLayout(new BorderLayout());
@@ -165,6 +191,23 @@ private void initComponents() {
placeholderPanel.setLayout(new FlowLayout());
}
panel1.add(placeholderPanel, BorderLayout.WEST);
+
+ //======== captionDemoButtonsPanel ========
+ {
+ captionDemoButtonsPanel.setVisible(false);
+ captionDemoButtonsPanel.setLayout(new FlowLayout());
+
+ //---- captionTrueButton ----
+ captionTrueButton.setText("caption=true (clicks: 0)");
+ captionTrueButton.addActionListener(e -> captionTrueClicked());
+ captionDemoButtonsPanel.add(captionTrueButton);
+
+ //---- captionFalseButton ----
+ captionFalseButton.setText("caption=false (clicks: 0)");
+ captionFalseButton.addActionListener(e -> captionFalseClicked());
+ captionDemoButtonsPanel.add(captionFalseButton);
+ }
+ panel1.add(captionDemoButtonsPanel, BorderLayout.EAST);
}
add(panel1, BorderLayout.PAGE_START);
@@ -185,6 +228,7 @@ private void initComponents() {
"[fill]" +
"[]" +
"[]para" +
+ "[]" +
"[]"));
//---- fullWindowContentCheckBox ----
@@ -262,6 +306,11 @@ private void initComponents() {
toggleFullScreenButton.setText("Toggle Full Screen");
toggleFullScreenButton.addActionListener(e -> toggleFullScreen());
panel2.add(toggleFullScreenButton, "cell 0 6");
+
+ //---- captionDemoCheckBox ----
+ captionDemoCheckBox.setText("Show caption demo buttons");
+ captionDemoCheckBox.addActionListener(e -> captionDemoChanged());
+ panel2.add(captionDemoCheckBox, "cell 0 7 4 1");
}
add(panel2, BorderLayout.CENTER);
@@ -287,5 +336,9 @@ private void initComponents() {
private JLabel buttonsSpacingHint;
private JLabel fullWindowContentButtonsBoundsField;
private JLabel nativeButtonsBoundsField;
+ private JPanel captionDemoButtonsPanel;
+ private JButton captionTrueButton;
+ private JButton captionFalseButton;
+ private JCheckBox captionDemoCheckBox;
// JFormDesigner - End of variables declaration //GEN-END:variables
}
diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSTest.jfd
index f580097b3..77ac299c7 100644
--- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSTest.jfd
+++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSTest.jfd
@@ -19,13 +19,38 @@ new FormModel {
}, new FormLayoutConstraints( class java.lang.String ) {
"value": "West"
} )
+ add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class java.awt.FlowLayout ) ) {
+ name: "captionDemoButtonsPanel"
+ "visible": false
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ add( new FormComponent( "javax.swing.JButton" ) {
+ name: "captionTrueButton"
+ "text": "caption=true (clicks: 0)"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "captionTrueClicked", false ) )
+ }, new FormLayoutConstraints( null ) )
+ add( new FormComponent( "javax.swing.JButton" ) {
+ name: "captionFalseButton"
+ "text": "caption=false (clicks: 0)"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "captionFalseClicked", false ) )
+ }, new FormLayoutConstraints( null ) )
+ }, new FormLayoutConstraints( class java.lang.String ) {
+ "value": "East"
+ } )
}, new FormLayoutConstraints( class java.lang.String ) {
"value": "First"
} )
add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
"$layoutConstraints": "ltr,insets dialog,hidemode 3"
"$columnConstraints": "[left][left][left][left]para[fill]"
- "$rowConstraints": "[][][][fill][][]para[]"
+ "$rowConstraints": "[][][][fill][][]para[][]"
} ) {
name: "panel2"
add( new FormComponent( "javax.swing.JCheckBox" ) {
@@ -175,6 +200,16 @@ new FormModel {
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 6"
} )
+ add( new FormComponent( "javax.swing.JCheckBox" ) {
+ name: "captionDemoCheckBox"
+ "text": "Show caption demo buttons"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "captionDemoChanged", false ) )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 7 4 1"
+ } )
}, new FormLayoutConstraints( class java.lang.String ) {
"value": "Center"
} )