From f7c8f3dedfa423112367c4f53d1fc6b77eb51c64 Mon Sep 17 00:00:00 2001 From: ps-porpoise <152162390+ps-porpoise@users.noreply.github.com> Date: Mon, 11 May 2026 20:06:10 +0100 Subject: [PATCH 1/3] FlatTitlePane: extracted title bar caption hit-test to FullWindowContentSupport --- .../com/formdev/flatlaf/ui/FlatTitlePane.java | 74 +-------------- .../flatlaf/ui/FullWindowContentSupport.java | 90 +++++++++++++++++++ 2 files changed, 91 insertions(+), 73 deletions(-) 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..cc0ea06dd 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,14 @@ 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.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 +222,88 @@ 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(); + } } From ed31fd56b52fb84882def1df42c463674a3677c1 Mon Sep 17 00:00:00 2001 From: ps-porpoise <152162390+ps-porpoise@users.noreply.github.com> Date: Mon, 11 May 2026 20:06:11 +0100 Subject: [PATCH 2/3] macOS: added native API for fullWindowContent title bar drag delegation Adds JNI hooks setupFullWindowContentTitleBarCaption / removeFullWindowContentTitleBarCaption with a per-window callback (FullWindowContentTitleBarCaptionCallback) that decides whether a left mouse down in the title bar area should start a native window drag instead of being delivered to AWT. Implementation installs a FlatTitleBarDragView as a sibling of the standard window buttons; AppKit's normal hit-testing routes button and contentView clicks unchanged, only background clicks land on us and are either handed off via -[NSWindow performWindowDragWithEvent:] or forwarded to the contentView. --- .../flatlaf/ui/FlatNativeMacLibrary.java | 37 ++- ..._formdev_flatlaf_ui_FlatNativeMacLibrary.h | 16 + .../src/main/objcpp/ApiVersion.mm | 2 +- .../src/main/objcpp/MacTitleBarCaption.mm | 293 ++++++++++++++++++ .../src/main/objcpp/MacWindow.mm | 18 +- .../src/main/objcpp/MacWindowInternal.h | 41 +++ 6 files changed, 389 insertions(+), 18 deletions(-) create mode 100644 flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacTitleBarCaption.mm create mode 100644 flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacWindowInternal.h 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-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 ); From 8f6142f9bfd77d3134a3f5cdab51ecfbe44bf569 Mon Sep 17 00:00:00 2001 From: ps-porpoise <152162390+ps-porpoise@users.noreply.github.com> Date: Mon, 11 May 2026 20:06:11 +0100 Subject: [PATCH 3/3] macOS: wired JComponent.titleBarCaption into fullWindowContent mode Registers a per-window callback with the native library when apple.awt.fullWindowContent is enabled. The callback runs the same shared isTitleBarCaptionAt traversal used on Windows, returning true for components marked with JComponent.titleBarCaption (or their children, recursively). --- .../formdev/flatlaf/FlatClientProperties.java | 2 + .../formdev/flatlaf/ui/FlatRootPaneUI.java | 20 ++++- .../flatlaf/ui/FullWindowContentSupport.java | 81 +++++++++++++++++++ .../flatlaf/testing/FlatMacOSTest.java | 53 ++++++++++++ .../formdev/flatlaf/testing/FlatMacOSTest.jfd | 37 ++++++++- 5 files changed, 189 insertions(+), 4 deletions(-) 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/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/FullWindowContentSupport.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FullWindowContentSupport.java index cc0ea06dd..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 @@ -33,6 +33,7 @@ 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; @@ -306,4 +307,84 @@ static boolean isTitleBarCaptionAt( Component c, int x, int y, Component skip ) 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-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" } )