diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java
index 02fed69b..e6c89414 100644
--- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java
+++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java
@@ -46,6 +46,7 @@ import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFrame;
+import javax.swing.JRootPane;
import javax.swing.LookAndFeel;
import javax.swing.PopupFactory;
import javax.swing.SwingUtilities;
@@ -59,8 +60,8 @@ import javax.swing.plaf.UIResource;
import javax.swing.plaf.basic.BasicLookAndFeel;
import javax.swing.text.StyleContext;
import javax.swing.text.html.HTMLEditorKit;
+import com.formdev.flatlaf.ui.FlatNativeWindowBorder;
import com.formdev.flatlaf.ui.FlatPopupFactory;
-import com.formdev.flatlaf.ui.JBRCustomDecorations;
import com.formdev.flatlaf.util.GrayFilter;
import com.formdev.flatlaf.util.MultiResolutionImageSupport;
import com.formdev.flatlaf.util.SystemInfo;
@@ -146,27 +147,39 @@ public abstract class FlatLaf
* This depends on the operating system and on the used Java runtime.
*
* To use custom window decorations in your application, enable them with
- * following code (before creating any frames or dialogs). Then custom window
- * decorations are only enabled if this method returns {@code true}.
+ * following code (before creating any frames or dialogs).
*
* JFrame.setDefaultLookAndFeelDecorated( true );
* JDialog.setDefaultLookAndFeelDecorated( true );
*
*
- * Returns {@code true} on Windows 10, {@code false} otherwise.
+ * Then custom window decorations are only enabled if this method returns {@code true}.
+ * In this case, when creating a {@link JFrame} or {@link JDialog}, the frame/dialog will be made
+ * undecorated ({@link JFrame#setUndecorated(boolean)} / {@link JDialog#setUndecorated(boolean)}),
+ * the window decoration style of the {@link JRootPane} will
+ * {@link JRootPane#FRAME} / {@link JRootPane#PLAIN_DIALOG}
+ * (see {@link JRootPane#setWindowDecorationStyle(int)}) and the look and feel
+ * is responsible for the whole frame/dialog border (including window resizing).
*
- * Return also {@code false} if running on Windows 10 in
+ * This method returns {@code true} on Windows 10 (see exception below), {@code false} otherwise.
+ *
+ * Returns also {@code false} on Windows 10 if:
+ *
+ * - FlatLaf native window border support is available (requires {@code flatlaf-native-jna.jar})
+ * - running in
* JetBrains Runtime 11 (or later)
* (source code on github)
- * and JBR supports custom window decorations. In this case, JBR custom decorations
- * are enabled if {@link JFrame#isDefaultLookAndFeelDecorated()} or
+ * and JBR supports custom window decorations
+ *
+ *
+ * In this case, custom decorations are enabled by the root pane
+ * if {@link JFrame#isDefaultLookAndFeelDecorated()} or
* {@link JDialog#isDefaultLookAndFeelDecorated()} return {@code true}.
*/
@Override
public boolean getSupportsWindowDecorations() {
- if( SystemInfo.isJetBrainsJVM_11_orLater &&
- SystemInfo.isWindows_10_orLater &&
- JBRCustomDecorations.isSupported() )
+ if( SystemInfo.isWindows_10_orLater &&
+ FlatNativeWindowBorder.isSupported() )
return false;
return SystemInfo.isWindows_10_orLater;
diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java
index c1065747..5c7fe50f 100644
--- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java
+++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java
@@ -71,6 +71,25 @@ public interface FlatSystemProperties
*/
String USE_WINDOW_DECORATIONS = "flatlaf.useWindowDecorations";
+ /**
+ * Specifies whether FlatLaf native window decorations should be used
+ * when creating {@code JFrame} or {@code JDialog}.
+ * Requires that {@code flatlaf-native-jna.jar} is on classpath/modulepath.
+ *
+ * Setting this to {@code true} forces using FlatLaf native window decorations
+ * even if they are not enabled by the application.
+ *
+ * Setting this to {@code false} disables using FlatLaf native window decorations.
+ *
+ * (requires Window 10)
+ *
+ * Allowed Values {@code false} and {@code true}
+ * Default none
+ *
+ * @since 1.1
+ */
+ String USE_NATIVE_WINDOW_DECORATIONS = "flatlaf.useNativeWindowDecorations";
+
/**
* Specifies whether JetBrains Runtime custom window decorations should be used
* when creating {@code JFrame} or {@code JDialog}.
@@ -81,10 +100,12 @@ public interface FlatSystemProperties
* Setting this to {@code true} forces using JetBrains Runtime custom window decorations
* even if they are not enabled by the application.
*
+ * Setting this to {@code false} disables using JetBrains Runtime custom window decorations.
+ *
* (requires Window 10)
*
* Allowed Values {@code false} and {@code true}
- * Default {@code true}
+ * Default none
*/
String USE_JETBRAINS_CUSTOM_DECORATIONS = "flatlaf.useJetBrainsCustomDecorations";
diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowBorder.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowBorder.java
new file mode 100644
index 00000000..8139e056
--- /dev/null
+++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowBorder.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2021 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.
+ */
+
+package com.formdev.flatlaf.ui;
+
+import java.awt.Rectangle;
+import java.awt.Window;
+import java.beans.PropertyChangeListener;
+import java.lang.reflect.Method;
+import java.util.List;
+import javax.swing.JDialog;
+import javax.swing.JFrame;
+import javax.swing.JRootPane;
+import javax.swing.SwingUtilities;
+import javax.swing.UIManager;
+import com.formdev.flatlaf.FlatLaf;
+import com.formdev.flatlaf.FlatSystemProperties;
+import com.formdev.flatlaf.util.SystemInfo;
+
+/**
+ * Support for custom window decorations with native window border.
+ *
+ * @author Karl Tauber
+ */
+public class FlatNativeWindowBorder
+{
+ // check this field before using class JBRCustomDecorations to avoid unnecessary loading of that class
+ private static final boolean canUseJBRCustomDecorations
+ = SystemInfo.isJetBrainsJVM_11_orLater && SystemInfo.isWindows_10_orLater;
+
+ private static Boolean supported;
+ private static Provider nativeProvider;
+
+ public static boolean isSupported() {
+ if( canUseJBRCustomDecorations )
+ return JBRCustomDecorations.isSupported();
+
+ initialize();
+ return supported;
+ }
+
+ static Object install( JRootPane rootPane ) {
+ if( canUseJBRCustomDecorations )
+ return JBRCustomDecorations.install( rootPane );
+
+ if( !isSupported() )
+ return null;
+
+ // check whether root pane already has a parent, which is the case when switching LaF
+ Window window = SwingUtilities.windowForComponent( rootPane );
+ if( window != null ) {
+ install( window, FlatSystemProperties.USE_NATIVE_WINDOW_DECORATIONS );
+ return null;
+ }
+
+ // Install FlatLaf native window border, which must be done late,
+ // when the native window is already created, because it needs access to the window.
+ // "ancestor" property change event is fired from JComponent.addNotify() and removeNotify().
+ PropertyChangeListener ancestorListener = e -> {
+ Object newValue = e.getNewValue();
+ if( newValue instanceof Window )
+ install( (Window) newValue, FlatSystemProperties.USE_NATIVE_WINDOW_DECORATIONS );
+ else if( newValue == null && e.getOldValue() instanceof Window )
+ uninstall( (Window) e.getOldValue() );
+ };
+ rootPane.addPropertyChangeListener( "ancestor", ancestorListener );
+ return ancestorListener;
+ }
+
+ static void install( Window window, String systemPropertyKey ) {
+ if( hasCustomDecoration( window ) )
+ return;
+
+ // do not enable native window border if LaF provides decorations
+ if( UIManager.getLookAndFeel().getSupportsWindowDecorations() )
+ return;
+
+ if( window instanceof JFrame ) {
+ JFrame frame = (JFrame) window;
+
+ // do not enable native window border if JFrame should use system window decorations
+ // and if not forced to use TODO JBR decorations
+ if( !JFrame.isDefaultLookAndFeelDecorated() &&
+ !FlatSystemProperties.getBoolean( systemPropertyKey, false ))
+ return;
+
+ // do not enable native window border if frame is undecorated
+ if( frame.isUndecorated() )
+ return;
+
+ // enable native window border for window
+ setHasCustomDecoration( frame, true );
+
+ // enable Swing window decoration
+ frame.getRootPane().setWindowDecorationStyle( JRootPane.FRAME );
+
+ } else if( window instanceof JDialog ) {
+ JDialog dialog = (JDialog) window;
+
+ // do not enable native window border if JDialog should use system window decorations
+ // and if not forced to use TODO JBR decorations
+ if( !JDialog.isDefaultLookAndFeelDecorated() &&
+ !FlatSystemProperties.getBoolean( systemPropertyKey, false ))
+ return;
+
+ // do not enable native window border if dialog is undecorated
+ if( dialog.isUndecorated() )
+ return;
+
+ // enable native window border for window
+ setHasCustomDecoration( dialog, true );
+
+ // enable Swing window decoration
+ dialog.getRootPane().setWindowDecorationStyle( JRootPane.PLAIN_DIALOG );
+ }
+ }
+
+ static void uninstall( JRootPane rootPane, Object data ) {
+ if( canUseJBRCustomDecorations ) {
+ JBRCustomDecorations.uninstall( rootPane, data );
+ return;
+ }
+
+ // remove listener
+ if( data instanceof PropertyChangeListener )
+ rootPane.removePropertyChangeListener( "ancestor", (PropertyChangeListener) data );
+
+ // uninstall native window border, except when switching to another FlatLaf theme
+ Window window = SwingUtilities.windowForComponent( rootPane );
+ if( window != null )
+ uninstall( window );
+ }
+
+ private static void uninstall( Window window ) {
+ if( !hasCustomDecoration( window ) )
+ return;
+
+ // do not uninstall when switching to another FlatLaf theme
+ if( UIManager.getLookAndFeel() instanceof FlatLaf )
+ return;
+
+ // disable native window border for window
+ setHasCustomDecoration( window, false );
+
+ if( window instanceof JFrame ) {
+ JFrame frame = (JFrame) window;
+
+ // disable Swing window decoration
+ frame.getRootPane().setWindowDecorationStyle( JRootPane.NONE );
+
+ } else if( window instanceof JDialog ) {
+ JDialog dialog = (JDialog) window;
+
+ // disable Swing window decoration
+ dialog.getRootPane().setWindowDecorationStyle( JRootPane.NONE );
+ }
+ }
+
+ public static boolean hasCustomDecoration( Window window ) {
+ if( canUseJBRCustomDecorations )
+ return JBRCustomDecorations.hasCustomDecoration( window );
+
+ if( !isSupported() )
+ return false;
+
+ return nativeProvider.hasCustomDecoration( window );
+ }
+
+ public static void setHasCustomDecoration( Window window, boolean hasCustomDecoration ) {
+ if( canUseJBRCustomDecorations ) {
+ JBRCustomDecorations.setHasCustomDecoration( window, hasCustomDecoration );
+ return;
+ }
+
+ if( !isSupported() )
+ return;
+
+ nativeProvider.setHasCustomDecoration( window, hasCustomDecoration );
+ }
+
+ static void setTitleBarHeightAndHitTestSpots( Window window, int titleBarHeight, List hitTestSpots ) {
+ if( canUseJBRCustomDecorations ) {
+ JBRCustomDecorations.setTitleBarHeightAndHitTestSpots( window, titleBarHeight, hitTestSpots );
+ return;
+ }
+
+ if( !isSupported() )
+ return;
+
+ nativeProvider.setTitleBarHeight( window, titleBarHeight );
+ nativeProvider.setTitleBarHitTestSpots( window, hitTestSpots );
+ }
+
+ private static void initialize() {
+ if( supported != null )
+ return;
+ supported = false;
+
+ // requires Windows 10 on x86_64
+ if( !SystemInfo.isWindows_10_orLater || !SystemInfo.isX86_64 )
+ return;
+
+ if( !FlatSystemProperties.getBoolean( FlatSystemProperties.USE_NATIVE_WINDOW_DECORATIONS, true ) )
+ return;
+
+ try {
+ Class> cls = Class.forName( "com.formdev.flatlaf.nativejna.windows.FlatWindowsNativeWindowBorder" );
+ Method m = cls.getMethod( "getInstance" );
+ nativeProvider = (Provider) m.invoke( null );
+
+ supported = (nativeProvider != null);
+ } catch( Exception ex ) {
+ // ignore
+ }
+ }
+
+ //---- interface Provider -------------------------------------------------
+
+ public interface Provider
+ {
+ boolean hasCustomDecoration( Window window );
+ void setHasCustomDecoration( Window window, boolean hasCustomDecoration );
+ void setTitleBarHeight( Window window, int titleBarHeight );
+ void setTitleBarHitTestSpots( Window window, List hitTestSpots );
+ }
+}
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 070eb896..40ca3e98 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
@@ -70,16 +70,13 @@ import com.formdev.flatlaf.util.UIScale;
public class FlatRootPaneUI
extends BasicRootPaneUI
{
- // check this field before using class JBRCustomDecorations to avoid unnecessary loading of that class
- static final boolean canUseJBRCustomDecorations
- = SystemInfo.isJetBrainsJVM_11_orLater && SystemInfo.isWindows_10_orLater;
-
protected final Color borderColor = UIManager.getColor( "TitlePane.borderColor" );
protected JRootPane rootPane;
protected FlatTitlePane titlePane;
protected FlatWindowResizer windowResizer;
+ private Object nativeWindowBorderData;
private LayoutManager oldLayout;
public static ComponentUI createUI( JComponent c ) {
@@ -97,8 +94,7 @@ public class FlatRootPaneUI
else
installBorder();
- if( canUseJBRCustomDecorations )
- JBRCustomDecorations.install( rootPane );
+ nativeWindowBorderData = FlatNativeWindowBorder.install( rootPane );
}
protected void installBorder() {
@@ -113,6 +109,8 @@ public class FlatRootPaneUI
public void uninstallUI( JComponent c ) {
super.uninstallUI( c );
+ FlatNativeWindowBorder.uninstall( rootPane, nativeWindowBorderData );
+
uninstallClientDecorations();
rootPane = null;
}
@@ -139,10 +137,10 @@ public class FlatRootPaneUI
}
protected void installClientDecorations() {
- boolean isJBRSupported = canUseJBRCustomDecorations && JBRCustomDecorations.isSupported();
+ boolean isNativeWindowBorderSupported = FlatNativeWindowBorder.isSupported();
// install border
- if( rootPane.getWindowDecorationStyle() != JRootPane.NONE && !isJBRSupported )
+ if( rootPane.getWindowDecorationStyle() != JRootPane.NONE && !isNativeWindowBorderSupported )
LookAndFeel.installBorder( rootPane, "RootPane.border" );
else
LookAndFeel.uninstallBorder( rootPane );
@@ -155,7 +153,7 @@ public class FlatRootPaneUI
rootPane.setLayout( createRootLayout() );
// install window resizer
- if( !isJBRSupported )
+ if( !isNativeWindowBorderSupported )
windowResizer = createWindowResizer();
}
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 ac13a231..dfec73d2 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
@@ -337,7 +337,7 @@ public class FlatTitlePane
// show/hide icon
iconLabel.setVisible( hasIcon );
- updateJBRHitTestSpotsAndTitleBarHeightLater();
+ updateNativeTitleBarHeightAndHitTestSpotsLater();
}
@Override
@@ -355,7 +355,7 @@ public class FlatTitlePane
installWindowListeners();
}
- updateJBRHitTestSpotsAndTitleBarHeightLater();
+ updateNativeTitleBarHeightAndHitTestSpotsLater();
}
@Override
@@ -435,7 +435,7 @@ public class FlatTitlePane
}
protected void menuBarLayouted() {
- updateJBRHitTestSpotsAndTitleBarHeightLater();
+ updateNativeTitleBarHeightAndHitTestSpotsLater();
}
/*debug
@@ -449,8 +449,9 @@ public class FlatTitlePane
}
if( debugHitTestSpots != null ) {
g.setColor( Color.blue );
+ Point offset = SwingUtilities.convertPoint( this, 0, 0, window );
for( Rectangle r : debugHitTestSpots )
- g.drawRect( r.x, r.y, r.width, r.height );
+ g.drawRect( r.x - offset.x, r.y - offset.y, r.width - 1, r.height - 1 );
}
}
debug*/
@@ -503,9 +504,9 @@ debug*/
Frame frame = (Frame) window;
// set maximized bounds to avoid that maximized window overlaps Windows task bar
- // (if not running in JBR and if not modified from the application)
+ // (if not having native window border and if not modified from the application)
Rectangle oldMaximizedBounds = frame.getMaximizedBounds();
- if( !hasJBRCustomDecoration() &&
+ if( !hasNativeCustomDecoration() &&
(oldMaximizedBounds == null ||
Objects.equals( oldMaximizedBounds, rootPane.getClientProperty( "_flatlaf.maximizedBounds" ) )) )
{
@@ -601,46 +602,51 @@ debug*/
window.dispatchEvent( new WindowEvent( window, WindowEvent.WINDOW_CLOSING ) );
}
- protected boolean hasJBRCustomDecoration() {
- return FlatRootPaneUI.canUseJBRCustomDecorations &&
- window != null &&
- JBRCustomDecorations.hasCustomDecoration( window );
+ private boolean hasJBRCustomDecoration() {
+ return window != null && JBRCustomDecorations.hasCustomDecoration( window );
}
- protected void updateJBRHitTestSpotsAndTitleBarHeightLater() {
+ /**
+ * Returns whether windows uses native window border and has custom decorations enabled.
+ */
+ protected boolean hasNativeCustomDecoration() {
+ return window != null && FlatNativeWindowBorder.hasCustomDecoration( window );
+ }
+
+ protected void updateNativeTitleBarHeightAndHitTestSpotsLater() {
EventQueue.invokeLater( () -> {
- updateJBRHitTestSpotsAndTitleBarHeight();
+ updateNativeTitleBarHeightAndHitTestSpots();
} );
}
- protected void updateJBRHitTestSpotsAndTitleBarHeight() {
+ protected void updateNativeTitleBarHeightAndHitTestSpots() {
if( !isDisplayable() )
return;
- if( !hasJBRCustomDecoration() )
+ if( !hasNativeCustomDecoration() )
return;
- List hitTestSpots = new ArrayList<>();
- if( iconLabel.isVisible() )
- addJBRHitTestSpot( iconLabel, false, hitTestSpots );
- addJBRHitTestSpot( buttonPanel, false, hitTestSpots );
- addJBRHitTestSpot( menuBarPlaceholder, true, hitTestSpots );
-
int titleBarHeight = getHeight();
// slightly reduce height so that component receives mouseExit events
if( titleBarHeight > 0 )
titleBarHeight--;
- JBRCustomDecorations.setHitTestSpotsAndTitleBarHeight( window, hitTestSpots, titleBarHeight );
+ List hitTestSpots = new ArrayList<>();
+ if( iconLabel.isVisible() )
+ addNativeHitTestSpot( iconLabel, false, hitTestSpots );
+ addNativeHitTestSpot( buttonPanel, false, hitTestSpots );
+ addNativeHitTestSpot( menuBarPlaceholder, true, hitTestSpots );
+
+ FlatNativeWindowBorder.setTitleBarHeightAndHitTestSpots( window, titleBarHeight, hitTestSpots );
/*debug
- debugHitTestSpots = hitTestSpots;
debugTitleBarHeight = titleBarHeight;
+ debugHitTestSpots = hitTestSpots;
repaint();
debug*/
}
- protected void addJBRHitTestSpot( JComponent c, boolean subtractMenuBarMargins, List hitTestSpots ) {
+ protected void addNativeHitTestSpot( JComponent c, boolean subtractMenuBarMargins, List hitTestSpots ) {
Dimension size = c.getSize();
if( size.width <= 0 || size.height <= 0 )
return;
@@ -655,8 +661,8 @@ debug*/
}
/*debug
- private List debugHitTestSpots;
private int debugTitleBarHeight;
+ private List debugHitTestSpots;
debug*/
//---- class TitlePaneBorder ----------------------------------------------
@@ -730,7 +736,7 @@ debug*/
break;
case "componentOrientation":
- updateJBRHitTestSpotsAndTitleBarHeightLater();
+ updateNativeTitleBarHeightAndHitTestSpotsLater();
break;
}
}
@@ -740,7 +746,7 @@ debug*/
@Override
public void windowActivated( WindowEvent e ) {
activeChanged( true );
- updateJBRHitTestSpotsAndTitleBarHeight();
+ updateNativeTitleBarHeightAndHitTestSpots();
if( hasJBRCustomDecoration() )
JBRWindowTopBorder.getInstance().repaintBorder( FlatTitlePane.this );
@@ -751,7 +757,7 @@ debug*/
@Override
public void windowDeactivated( WindowEvent e ) {
activeChanged( false );
- updateJBRHitTestSpotsAndTitleBarHeight();
+ updateNativeTitleBarHeightAndHitTestSpots();
if( hasJBRCustomDecoration() )
JBRWindowTopBorder.getInstance().repaintBorder( FlatTitlePane.this );
@@ -762,7 +768,7 @@ debug*/
@Override
public void windowStateChanged( WindowEvent e ) {
frameStateChanged();
- updateJBRHitTestSpotsAndTitleBarHeight();
+ updateNativeTitleBarHeightAndHitTestSpots();
}
//---- interface MouseListener ----
@@ -775,7 +781,7 @@ debug*/
if( e.getSource() == iconLabel ) {
// double-click on icon closes window
close();
- } else if( !hasJBRCustomDecoration() &&
+ } else if( !hasNativeCustomDecoration() &&
window instanceof Frame &&
((Frame)window).isResizable() )
{
@@ -808,8 +814,8 @@ debug*/
if( window == null )
return; // should newer occur
- if( hasJBRCustomDecoration() )
- return; // do nothing if running in JBR
+ if( hasNativeCustomDecoration() )
+ return; // do nothing if having native window border
// restore window if it is maximized
if( window instanceof Frame ) {
@@ -852,7 +858,7 @@ debug*/
@Override
public void componentResized( ComponentEvent e ) {
- updateJBRHitTestSpotsAndTitleBarHeightLater();
+ updateNativeTitleBarHeightAndHitTestSpotsLater();
}
@Override
diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/JBRCustomDecorations.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/JBRCustomDecorations.java
index e3d68e7d..680f3699 100644
--- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/JBRCustomDecorations.java
+++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/JBRCustomDecorations.java
@@ -29,11 +29,10 @@ import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Method;
+import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
-import javax.swing.JDialog;
-import javax.swing.JFrame;
import javax.swing.JRootPane;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
@@ -55,26 +54,29 @@ import com.formdev.flatlaf.util.SystemInfo;
*/
public class JBRCustomDecorations
{
- private static boolean initialized;
+ private static Boolean supported;
private static Method Window_hasCustomDecoration;
private static Method Window_setHasCustomDecoration;
- private static Method WWindowPeer_setCustomDecorationHitTestSpots;
private static Method WWindowPeer_setCustomDecorationTitleBarHeight;
+ private static Method WWindowPeer_setCustomDecorationHitTestSpots;
private static Method AWTAccessor_getComponentAccessor;
private static Method AWTAccessor_ComponentAccessor_getPeer;
public static boolean isSupported() {
initialize();
- return Window_setHasCustomDecoration != null;
+ return supported;
}
- static void install( JRootPane rootPane ) {
+ static Object install( JRootPane rootPane ) {
if( !isSupported() )
- return;
+ return null;
// check whether root pane already has a parent, which is the case when switching LaF
- if( rootPane.getParent() != null )
- return;
+ Window window = SwingUtilities.windowForComponent( rootPane );
+ if( window != null ) {
+ FlatNativeWindowBorder.install( window, FlatSystemProperties.USE_JETBRAINS_CUSTOM_DECORATIONS );
+ return null;
+ }
// Use hierarchy listener to wait until the root pane is added to a window.
// Enabling JBR decorations must be done very early, probably before
@@ -88,8 +90,9 @@ public class JBRCustomDecorations
Container parent = e.getChangedParent();
if( parent instanceof Window )
- install( (Window) parent );
+ FlatNativeWindowBorder.install( (Window) parent, FlatSystemProperties.USE_JETBRAINS_CUSTOM_DECORATIONS );
+ // remove listener since it is actually not possible to uninstall JBR decorations
// use invokeLater to remove listener to avoid that listener
// is removed while listener queue is processed
EventQueue.invokeLater( () -> {
@@ -98,54 +101,20 @@ public class JBRCustomDecorations
}
};
rootPane.addHierarchyListener( addListener );
+ return addListener;
}
- static void install( Window window ) {
- if( !isSupported() )
- return;
+ static void uninstall( JRootPane rootPane, Object data ) {
+ // remove listener (if not yet done)
+ if( data instanceof HierarchyListener )
+ rootPane.removeHierarchyListener( (HierarchyListener) data );
- // do not enable JBR decorations if LaF provides decorations
- if( UIManager.getLookAndFeel().getSupportsWindowDecorations() )
- return;
-
- if( window instanceof JFrame ) {
- JFrame frame = (JFrame) window;
-
- // do not enable JBR decorations if JFrame should use system window decorations
- // and if not forced to use JBR decorations
- if( !JFrame.isDefaultLookAndFeelDecorated() &&
- !FlatSystemProperties.getBoolean( FlatSystemProperties.USE_JETBRAINS_CUSTOM_DECORATIONS, false ))
- return;
-
- // do not enable JBR decorations if frame is undecorated
- if( frame.isUndecorated() )
- return;
-
- // enable JBR custom window decoration for window
- setHasCustomDecoration( frame );
-
- // enable Swing window decoration
- frame.getRootPane().setWindowDecorationStyle( JRootPane.FRAME );
-
- } else if( window instanceof JDialog ) {
- JDialog dialog = (JDialog) window;
-
- // do not enable JBR decorations if JDialog should use system window decorations
- // and if not forced to use JBR decorations
- if( !JDialog.isDefaultLookAndFeelDecorated() &&
- !FlatSystemProperties.getBoolean( FlatSystemProperties.USE_JETBRAINS_CUSTOM_DECORATIONS, false ))
- return;
-
- // do not enable JBR decorations if dialog is undecorated
- if( dialog.isUndecorated() )
- return;
-
- // enable JBR custom window decoration for window
- setHasCustomDecoration( dialog );
-
- // enable Swing window decoration
- dialog.getRootPane().setWindowDecorationStyle( JRootPane.PLAIN_DIALOG );
- }
+ // since it is actually not possible to uninstall JBR decorations,
+ // simply reduce titleBarHeight so that it is still possible to resize window
+ // and remove hitTestSpots
+ Window window = SwingUtilities.windowForComponent( rootPane );
+ if( window != null )
+ setHasCustomDecoration( window, false );
}
static boolean hasCustomDecoration( Window window ) {
@@ -160,35 +129,38 @@ public class JBRCustomDecorations
}
}
- static void setHasCustomDecoration( Window window ) {
+ static void setHasCustomDecoration( Window window, boolean hasCustomDecoration ) {
if( !isSupported() )
return;
try {
- Window_setHasCustomDecoration.invoke( window );
+ if( hasCustomDecoration )
+ Window_setHasCustomDecoration.invoke( window );
+ else
+ setTitleBarHeightAndHitTestSpots( window, 4, Collections.emptyList() );
} catch( Exception ex ) {
Logger.getLogger( FlatLaf.class.getName() ).log( Level.SEVERE, null, ex );
}
}
- static void setHitTestSpotsAndTitleBarHeight( Window window, List hitTestSpots, int titleBarHeight ) {
+ static void setTitleBarHeightAndHitTestSpots( Window window, int titleBarHeight, List hitTestSpots ) {
if( !isSupported() )
return;
try {
Object compAccessor = AWTAccessor_getComponentAccessor.invoke( null );
Object peer = AWTAccessor_ComponentAccessor_getPeer.invoke( compAccessor, window );
- WWindowPeer_setCustomDecorationHitTestSpots.invoke( peer, hitTestSpots );
WWindowPeer_setCustomDecorationTitleBarHeight.invoke( peer, titleBarHeight );
+ WWindowPeer_setCustomDecorationHitTestSpots.invoke( peer, hitTestSpots );
} catch( Exception ex ) {
Logger.getLogger( FlatLaf.class.getName() ).log( Level.SEVERE, null, ex );
}
}
private static void initialize() {
- if( initialized )
+ if( supported != null )
return;
- initialized = true;
+ supported = false;
// requires JetBrains Runtime 11 and Windows 10
if( !SystemInfo.isJetBrainsJVM_11_orLater || !SystemInfo.isWindows_10_orLater )
@@ -204,15 +176,17 @@ public class JBRCustomDecorations
AWTAccessor_ComponentAccessor_getPeer = compAccessorClass.getDeclaredMethod( "getPeer", Component.class );
Class> peerClass = Class.forName( "sun.awt.windows.WWindowPeer" );
- WWindowPeer_setCustomDecorationHitTestSpots = peerClass.getDeclaredMethod( "setCustomDecorationHitTestSpots", List.class );
WWindowPeer_setCustomDecorationTitleBarHeight = peerClass.getDeclaredMethod( "setCustomDecorationTitleBarHeight", int.class );
- WWindowPeer_setCustomDecorationHitTestSpots.setAccessible( true );
+ WWindowPeer_setCustomDecorationHitTestSpots = peerClass.getDeclaredMethod( "setCustomDecorationHitTestSpots", List.class );
WWindowPeer_setCustomDecorationTitleBarHeight.setAccessible( true );
+ WWindowPeer_setCustomDecorationHitTestSpots.setAccessible( true );
Window_hasCustomDecoration = Window.class.getDeclaredMethod( "hasCustomDecoration" );
Window_setHasCustomDecoration = Window.class.getDeclaredMethod( "setHasCustomDecoration" );
Window_hasCustomDecoration.setAccessible( true );
Window_setHasCustomDecoration.setAccessible( true );
+
+ supported = true;
} catch( Exception ex ) {
// ignore
}
diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java
index f5296436..d4e2397d 100644
--- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java
+++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java
@@ -38,6 +38,9 @@ public class SystemInfo
public static final boolean isMacOS_10_14_Mojave_orLater;
public static final boolean isMacOS_10_15_Catalina_orLater;
+ // OS architecture
+ public static final boolean isX86_64;
+
// Java versions
public static final long javaVersion;
public static final boolean isJava_9_orLater;
@@ -65,6 +68,10 @@ public class SystemInfo
isMacOS_10_14_Mojave_orLater = (isMacOS && osVersion >= toVersion( 10, 14, 0, 0 ));
isMacOS_10_15_Catalina_orLater = (isMacOS && osVersion >= toVersion( 10, 15, 0, 0 ));
+ // OS architecture
+ String osArch = System.getProperty( "os.arch" );
+ isX86_64 = osArch.equals( "amd64" ) || osArch.equals( "x86_64" );
+
// Java versions
javaVersion = scanVersion( System.getProperty( "java.version" ) );
isJava_9_orLater = (javaVersion >= toVersion( 9, 0, 0, 0 ));
diff --git a/flatlaf-demo/build.gradle.kts b/flatlaf-demo/build.gradle.kts
index 7e5110da..e39dc89d 100644
--- a/flatlaf-demo/build.gradle.kts
+++ b/flatlaf-demo/build.gradle.kts
@@ -27,6 +27,7 @@ repositories {
dependencies {
implementation( project( ":flatlaf-core" ) )
+ implementation( project( ":flatlaf-native-jna" ) )
implementation( project( ":flatlaf-extras" ) )
implementation( project( ":flatlaf-intellij-themes" ) )
implementation( "com.miglayout:miglayout-swing:5.3-SNAPSHOT" )
@@ -36,6 +37,7 @@ dependencies {
tasks {
jar {
dependsOn( ":flatlaf-core:jar" )
+ dependsOn( ":flatlaf-native-jna:jar" )
dependsOn( ":flatlaf-extras:jar" )
dependsOn( ":flatlaf-intellij-themes:jar" )
diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java
index f611ee27..bbb2e8c1 100644
--- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java
+++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java
@@ -33,6 +33,7 @@ import com.formdev.flatlaf.extras.FlatAnimatedLafChange;
import com.formdev.flatlaf.extras.FlatSVGIcon;
import com.formdev.flatlaf.extras.FlatUIDefaultsInspector;
import com.formdev.flatlaf.extras.FlatSVGUtils;
+import com.formdev.flatlaf.ui.FlatNativeWindowBorder;
import com.formdev.flatlaf.ui.JBRCustomDecorations;
import net.miginfocom.layout.ConstraintParser;
import net.miginfocom.layout.LC;
@@ -142,11 +143,16 @@ class DemoFrame
boolean windowDecorations = windowDecorationsCheckBoxMenuItem.isSelected();
// change window decoration of demo main frame
- dispose();
- setUndecorated( windowDecorations );
- getRootPane().setWindowDecorationStyle( windowDecorations ? JRootPane.FRAME : JRootPane.NONE );
+ if( FlatNativeWindowBorder.isSupported() ) {
+ FlatNativeWindowBorder.setHasCustomDecoration( this, windowDecorations );
+ getRootPane().setWindowDecorationStyle( windowDecorations ? JRootPane.FRAME : JRootPane.NONE );
+ } else {
+ dispose();
+ setUndecorated( windowDecorations );
+ getRootPane().setWindowDecorationStyle( windowDecorations ? JRootPane.FRAME : JRootPane.NONE );
+ setVisible( true );
+ }
menuBarEmbeddedCheckBoxMenuItem.setEnabled( windowDecorations );
- setVisible( true );
// enable/disable window decoration for later created frames/dialogs
JFrame.setDefaultLookAndFeelDecorated( windowDecorations );
@@ -722,7 +728,7 @@ class DemoFrame
pasteMenuItem.addActionListener( new DefaultEditorKit.PasteAction() );
boolean supportsWindowDecorations = UIManager.getLookAndFeel()
- .getSupportsWindowDecorations() || JBRCustomDecorations.isSupported();
+ .getSupportsWindowDecorations() || FlatNativeWindowBorder.isSupported();
windowDecorationsCheckBoxMenuItem.setEnabled( supportsWindowDecorations && !JBRCustomDecorations.isSupported() );
menuBarEmbeddedCheckBoxMenuItem.setEnabled( supportsWindowDecorations );
diff --git a/flatlaf-native-jna/build.gradle.kts b/flatlaf-native-jna/build.gradle.kts
new file mode 100644
index 00000000..a085df22
--- /dev/null
+++ b/flatlaf-native-jna/build.gradle.kts
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2021 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.
+ */
+
+plugins {
+ `java-library`
+ `flatlaf-module-info`
+}
+
+dependencies {
+ implementation( project( ":flatlaf-core" ) )
+ implementation( "net.java.dev.jna:jna:5.7.0" )
+ implementation( "net.java.dev.jna:jna-platform:5.7.0" )
+}
+
+flatlafModuleInfo {
+ dependsOn( ":flatlaf-core:jar" )
+}
diff --git a/flatlaf-native-jna/src/main/java/com/formdev/flatlaf/nativejna/windows/FlatWindowsNativeWindowBorder.java b/flatlaf-native-jna/src/main/java/com/formdev/flatlaf/nativejna/windows/FlatWindowsNativeWindowBorder.java
new file mode 100644
index 00000000..28cc636f
--- /dev/null
+++ b/flatlaf-native-jna/src/main/java/com/formdev/flatlaf/nativejna/windows/FlatWindowsNativeWindowBorder.java
@@ -0,0 +1,440 @@
+/*
+ * Copyright 2021 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.
+ */
+
+package com.formdev.flatlaf.nativejna.windows;
+
+import static com.sun.jna.platform.win32.WinUser.*;
+import java.awt.Dialog;
+import java.awt.Frame;
+import java.awt.GraphicsConfiguration;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.Window;
+import java.awt.geom.AffineTransform;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import javax.swing.JDialog;
+import javax.swing.JFrame;
+import com.formdev.flatlaf.ui.FlatNativeWindowBorder;
+import com.formdev.flatlaf.util.SystemInfo;
+import com.sun.jna.Native;
+import com.sun.jna.Pointer;
+import com.sun.jna.Structure;
+import com.sun.jna.Structure.FieldOrder;
+import com.sun.jna.platform.win32.BaseTSD;
+import com.sun.jna.platform.win32.User32;
+import com.sun.jna.platform.win32.WinDef.HWND;
+import com.sun.jna.platform.win32.WinDef.LPARAM;
+import com.sun.jna.platform.win32.WinDef.LRESULT;
+import com.sun.jna.platform.win32.WinDef.RECT;
+import com.sun.jna.platform.win32.WinDef.WPARAM;
+import com.sun.jna.platform.win32.WinUser.WindowProc;
+import com.sun.jna.win32.W32APIOptions;
+
+//
+// Interesting resources:
+// https://github.com/microsoft/terminal/blob/main/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp
+// https://docs.microsoft.com/en-us/windows/win32/dwm/customframe
+// https://github.com/JetBrains/JetBrainsRuntime/blob/master/src/java.desktop/windows/native/libawt/windows/awt_Frame.cpp
+// https://github.com/JetBrains/JetBrainsRuntime/commit/d2820524a1aa211b1c49b30f659b9b4d07a6f96e
+// https://github.com/JetBrains/JetBrainsRuntime/pull/18
+// https://medium.com/swlh/customizing-the-title-bar-of-an-application-window-50a4ac3ed27e
+// https://github.com/kalbetredev/CustomDecoratedJFrame
+// https://github.com/Guerra24/NanoUI-win32
+// https://github.com/rossy/borderless-window
+//
+
+/**
+ * Native window border support for Windows 10 when using custom decorations.
+ *
+ * If the application wants to use custom decorations, the Windows 10 title bar is hidden
+ * (including minimize, maximize and close buttons), but not the resize borders (including drop shadow).
+ * Windows 10 window snapping functionality will remain unaffected:
+ * https://support.microsoft.com/en-us/windows/snap-your-windows-885a9b1e-a983-a3b1-16cd-c531795e6241
+ *
+ * @author Karl Tauber
+ */
+public class FlatWindowsNativeWindowBorder
+ implements FlatNativeWindowBorder.Provider
+{
+ private final Map windowsMap = Collections.synchronizedMap( new IdentityHashMap<>() );
+
+ private static FlatWindowsNativeWindowBorder instance;
+
+ public static FlatNativeWindowBorder.Provider getInstance() {
+ if( instance == null )
+ instance = new FlatWindowsNativeWindowBorder();
+ return instance;
+ }
+
+ private FlatWindowsNativeWindowBorder() {
+ }
+
+ @Override
+ public boolean hasCustomDecoration( Window window ) {
+ return windowsMap.containsKey( window );
+ }
+
+ /**
+ * Tell the window whether the application wants use custom decorations.
+ * If {@code true}, the Windows 10 title bar is hidden (including minimize,
+ * maximize and close buttons), but not the resize borders (including drop shadow).
+ */
+ @Override
+ public void setHasCustomDecoration( Window window, boolean hasCustomDecoration ) {
+ if( hasCustomDecoration )
+ install( window );
+ else
+ uninstall( window );
+ }
+
+ private void install( Window window ) {
+ // requires Windows 10 on x86_64
+ if( !SystemInfo.isWindows_10_orLater || !SystemInfo.isX86_64 )
+ return;
+
+ // only JFrame and JDialog are supported
+ if( !(window instanceof JFrame) && !(window instanceof JDialog) )
+ return;
+
+ // not supported if frame/dialog is undecorated
+ if( (window instanceof Frame && ((Frame)window).isUndecorated()) ||
+ (window instanceof Dialog && ((Dialog)window).isUndecorated()) )
+ return;
+
+ // check whether already installed
+ if( windowsMap.containsKey( window ) )
+ return;
+
+ // install
+ WndProc wndProc = new WndProc( window );
+ windowsMap.put( window, wndProc );
+ }
+
+ private void uninstall( Window window ) {
+ WndProc wndProc = windowsMap.remove( window );
+ if( wndProc != null )
+ wndProc.uninstall();
+ }
+
+ @Override
+ public void setTitleBarHeight( Window window, int titleBarHeight ) {
+ WndProc wndProc = windowsMap.get( window );
+ if( wndProc == null )
+ return;
+
+ wndProc.titleBarHeight = titleBarHeight;
+ }
+
+ @Override
+ public void setTitleBarHitTestSpots( Window window, List hitTestSpots ) {
+ WndProc wndProc = windowsMap.get( window );
+ if( wndProc == null )
+ return;
+
+ wndProc.hitTestSpots = hitTestSpots.toArray( new Rectangle[hitTestSpots.size()] );
+ }
+
+ //---- class WndProc ------------------------------------------------------
+
+ private class WndProc
+ implements WindowProc
+ {
+ private static final int GWLP_WNDPROC = -4;
+
+ private static final int WM_NCCALCSIZE = 0x0083;
+ private static final int WM_NCHITTEST = 0x0084;
+
+ // WM_NCHITTEST mouse position codes
+ private static final int
+ HTCLIENT = 1,
+ HTCAPTION = 2,
+ HTTOP = 12;
+
+ private Window window;
+ private final HWND hwnd;
+ private final BaseTSD.LONG_PTR defaultWndProc;
+
+ private int titleBarHeight;
+ private Rectangle[] hitTestSpots;
+
+ WndProc( Window window ) {
+ this.window = window;
+
+ // get window handle
+ hwnd = new HWND( Native.getComponentPointer( window ) );
+
+ // replace window procedure
+ defaultWndProc = User32Ex.INSTANCE.SetWindowLongPtr( hwnd, GWLP_WNDPROC, this );
+
+ // remove the OS window title bar
+ updateFrame();
+ }
+
+ void uninstall() {
+ // restore original window procedure
+ User32Ex.INSTANCE.SetWindowLongPtr( hwnd, GWLP_WNDPROC, defaultWndProc );
+
+ // show the OS window title bar
+ updateFrame();
+
+ // cleanup
+ window = null;
+ }
+
+ private void updateFrame() {
+ // this sends WM_NCCALCSIZE and removes/shows the window title bar
+ User32.INSTANCE.SetWindowPos( hwnd, hwnd, 0, 0, 0, 0,
+ SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER );
+ }
+
+ /**
+ * NOTE: This method is invoked on the AWT-Windows thread (not the AWT-EventQueue thread).
+ */
+ @Override
+ public LRESULT callback( HWND hwnd, int uMsg, WPARAM wParam, LPARAM lParam ) {
+ switch( uMsg ) {
+ case WM_NCCALCSIZE:
+ return WmNcCalcSize( hwnd, uMsg, wParam, lParam );
+
+ case WM_NCHITTEST:
+ return WmNcHitTest( hwnd, uMsg, wParam, lParam );
+
+ case WM_DESTROY:
+ return WmDestroy( hwnd, uMsg, wParam, lParam );
+
+ default:
+ return User32Ex.INSTANCE.CallWindowProc( defaultWndProc, hwnd, uMsg, wParam, lParam );
+ }
+ }
+
+ /**
+ * Handle WM_DESTROY
+ *
+ * https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-destroy
+ */
+ private LRESULT WmDestroy( HWND hwnd, int uMsg, WPARAM wParam, LPARAM lParam ) {
+ // call original AWT window procedure because it may fire window closed event in AwtWindow::WmDestroy()
+ LRESULT lResult = User32Ex.INSTANCE.CallWindowProc( defaultWndProc, hwnd, uMsg, wParam, lParam );
+
+ // restore original window procedure
+ User32Ex.INSTANCE.SetWindowLongPtr( hwnd, GWLP_WNDPROC, defaultWndProc );
+
+ // cleanup
+ windowsMap.remove( window );
+ window = null;
+
+ return lResult;
+ }
+
+ /**
+ * Handle WM_NCCALCSIZE
+ *
+ * https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize
+ *
+ * See also NonClientIslandWindow::_OnNcCalcSize() here:
+ * https://github.com/microsoft/terminal/blob/main/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp
+ */
+ private LRESULT WmNcCalcSize( HWND hwnd, int uMsg, WPARAM wParam, LPARAM lParam ) {
+ if( wParam.intValue() != 1 )
+ return User32Ex.INSTANCE.CallWindowProc( defaultWndProc, hwnd, uMsg, wParam, lParam );
+
+ NCCALCSIZE_PARAMS params = new NCCALCSIZE_PARAMS( new Pointer( lParam.longValue() ) );
+
+ // store the original top before the default window proc applies the default frame
+ int originalTop = params.rgrc[0].top;
+
+ // apply the default frame
+ LRESULT lResult = User32Ex.INSTANCE.CallWindowProc( defaultWndProc, hwnd, uMsg, wParam, lParam );
+ if( lResult.longValue() != 0 )
+ return lResult;
+
+ // re-read params from native memory because defaultWndProc changed it
+ params.read();
+
+ // re-apply the original top from before the size of the default frame was applied
+ params.rgrc[0].top = originalTop;
+
+ boolean isMaximized = User32Ex.INSTANCE.IsZoomed( hwnd );
+ if( isMaximized && !isFullscreen() ) {
+ // When a window is maximized, its size is actually a little bit more
+ // than the monitor's work area. The window is positioned and sized in
+ // such a way that the resize handles are outside of the monitor and
+ // then the window is clipped to the monitor so that the resize handle
+ // do not appear because you don't need them (because you can't resize
+ // a window when it's maximized unless you restore it).
+ params.rgrc[0].top += getResizeHandleHeight();
+ }
+
+ //TODO support autohide taskbar
+
+ // write changed params back to native memory
+ params.write();
+
+ return lResult;
+ }
+
+ /**
+ * Handle WM_NCHITTEST
+ *
+ * https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-nchittest
+ *
+ * See also NonClientIslandWindow::_OnNcHitTest() here:
+ * https://github.com/microsoft/terminal/blob/main/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp
+ */
+ private LRESULT WmNcHitTest( HWND hwnd, int uMsg, WPARAM wParam, LPARAM lParam ) {
+ // this will handle the left, right and bottom parts of the frame because we didn't change them
+ LRESULT lResult = User32Ex.INSTANCE.CallWindowProc( defaultWndProc, hwnd, uMsg, wParam, lParam );
+ if( lResult.longValue() != HTCLIENT )
+ return lResult;
+
+ // get window rectangle needed to convert mouse x/y from screen to window coordinates
+ RECT rcWindow = new RECT();
+ User32.INSTANCE.GetWindowRect( hwnd, rcWindow );
+
+ // get mouse x/y in window coordinates
+ int x = GET_X_LPARAM( lParam ) - rcWindow.left;
+ int y = GET_Y_LPARAM( lParam ) - rcWindow.top;
+
+ // scale-down mouse x/y
+ Point pt = scaleDown( x, y );
+ int sx = pt.x;
+ int sy = pt.y;
+
+ int resizeBorderHeight = getResizeHandleHeight();
+ boolean isOnResizeBorder = (y < resizeBorderHeight) &&
+ (User32.INSTANCE.GetWindowLong( hwnd, GWL_STYLE ) & WS_THICKFRAME) != 0;
+ boolean isOnTitleBar = (sy < titleBarHeight);
+
+ if( isOnTitleBar ) {
+ // use a second reference to the array to avoid that it can be changed
+ // in another thread while processing the array
+ Rectangle[] hitTestSpots2 = hitTestSpots;
+ for( Rectangle spot : hitTestSpots2 ) {
+ if( spot.contains( sx, sy ) )
+ return new LRESULT( HTCLIENT );
+ }
+ return new LRESULT( isOnResizeBorder ? HTTOP : HTCAPTION );
+ }
+
+ return new LRESULT( isOnResizeBorder ? HTTOP : HTCLIENT );
+ }
+
+ /**
+ * Returns the height of the little space at the top of the window used to
+ * resize the window.
+ *
+ * See also NonClientIslandWindow::_GetResizeHandleHeight() here:
+ * https://github.com/microsoft/terminal/blob/main/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp
+ */
+ private int getResizeHandleHeight() {
+ int dpi = User32Ex.INSTANCE.GetDpiForWindow( hwnd );
+
+ // there isn't a SM_CYPADDEDBORDER for the Y axis
+ return User32Ex.INSTANCE.GetSystemMetricsForDpi( SM_CXPADDEDBORDER, dpi )
+ + User32Ex.INSTANCE.GetSystemMetricsForDpi( SM_CYSIZEFRAME, dpi );
+ }
+
+ /**
+ * Scales down in the same way as AWT.
+ * See AwtWin32GraphicsDevice::ScaleDownX()
+ */
+ private Point scaleDown( int x, int y ) {
+ GraphicsConfiguration gc = window.getGraphicsConfiguration();
+ if( gc == null )
+ return new Point( x, y );
+
+ AffineTransform t = gc.getDefaultTransform();
+ return new Point( clipRound( x / t.getScaleX() ), clipRound( y / t.getScaleY() ) );
+ }
+
+ /**
+ * Rounds in the same way as AWT.
+ * See AwtWin32GraphicsDevice::ClipRound()
+ */
+ private int clipRound( double value ) {
+ value -= 0.5;
+ if( value < Integer.MIN_VALUE )
+ return Integer.MIN_VALUE;
+ if( value > Integer.MAX_VALUE )
+ return Integer.MAX_VALUE;
+ return (int) Math.ceil( value );
+ }
+
+ private boolean isFullscreen() {
+ GraphicsConfiguration gc = window.getGraphicsConfiguration();
+ if( gc == null )
+ return false;
+ return gc.getDevice().getFullScreenWindow() == window;
+ }
+
+ /**
+ * Same implementation as GET_X_LPARAM(lp) macro in windowsx.h.
+ * X-coordinate is in the low-order short and may be negative.
+ *
+ * https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-nchittest#remarks
+ */
+ private int GET_X_LPARAM( LPARAM lParam ) {
+ return (short) (lParam.longValue() & 0xffff);
+ }
+
+ /**
+ * Same implementation as GET_Y_LPARAM(lp) macro in windowsx.h.
+ * Y-coordinate is in the high-order short and may be negative.
+ *
+ * https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-nchittest#remarks
+ */
+ private int GET_Y_LPARAM( LPARAM lParam ) {
+ return (short) ((lParam.longValue() >> 16) & 0xffff);
+ }
+ }
+
+ //---- interface User32Ex -------------------------------------------------
+
+ private interface User32Ex
+ extends User32
+ {
+ User32Ex INSTANCE = Native.load( "user32", User32Ex.class, W32APIOptions.DEFAULT_OPTIONS );
+
+ LONG_PTR SetWindowLongPtr( HWND hWnd, int nIndex, WindowProc wndProc );
+ LONG_PTR SetWindowLongPtr( HWND hWnd, int nIndex, LONG_PTR wndProc );
+ LRESULT CallWindowProc( LONG_PTR lpPrevWndFunc, HWND hWnd, int uMsg, WPARAM wParam, LPARAM lParam );
+
+ int GetDpiForWindow( HWND hwnd );
+ int GetSystemMetricsForDpi( int nIndex, int dpi );
+
+ boolean IsZoomed( HWND hWnd );
+ HANDLE GetProp( HWND hWnd, String lpString );
+ }
+
+ //---- class NCCALCSIZE_PARAMS --------------------------------------------
+
+ @FieldOrder( { "rgrc" } )
+ public static class NCCALCSIZE_PARAMS
+ extends Structure
+ {
+ // real structure contains 3 rectangles, but only first one is needed here
+ public RECT[] rgrc = new RECT[1];
+// public WINDOWPOS lppos;
+
+ public NCCALCSIZE_PARAMS( Pointer pointer ) {
+ super( pointer );
+ read();
+ }
+ }
+}
diff --git a/flatlaf-native-jna/src/main/module-info/module-info.java b/flatlaf-native-jna/src/main/module-info/module-info.java
new file mode 100644
index 00000000..68f3431c
--- /dev/null
+++ b/flatlaf-native-jna/src/main/module-info/module-info.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2021 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.
+ */
+
+/**
+ * @author Karl Tauber
+ */
+module com.formdev.flatlaf.nativejna {
+ requires java.desktop;
+ requires com.sun.jna;
+ requires com.sun.jna.platform;
+ requires com.formdev.flatlaf;
+
+ exports com.formdev.flatlaf.nativejna.windows;
+}
diff --git a/flatlaf-testing/build.gradle.kts b/flatlaf-testing/build.gradle.kts
index 41489b4a..bc4acaf3 100644
--- a/flatlaf-testing/build.gradle.kts
+++ b/flatlaf-testing/build.gradle.kts
@@ -27,6 +27,7 @@ repositories {
dependencies {
implementation( project( ":flatlaf-core" ) )
+ implementation( project( ":flatlaf-native-jna" ) )
implementation( project( ":flatlaf-extras" ) )
implementation( project( ":flatlaf-swingx" ) )
implementation( project( ":flatlaf-jide-oss" ) )
diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatNativeWindowBorderTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatNativeWindowBorderTest.java
new file mode 100644
index 00000000..0184cdf1
--- /dev/null
+++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatNativeWindowBorderTest.java
@@ -0,0 +1,460 @@
+/*
+ * Copyright 2021 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.
+ */
+
+package com.formdev.flatlaf.testing;
+
+import java.awt.*;
+import java.awt.Dialog.ModalityType;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.awt.event.ComponentListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.WindowEvent;
+import java.awt.event.WindowListener;
+import java.util.WeakHashMap;
+import javax.swing.*;
+import com.formdev.flatlaf.FlatLightLaf;
+import com.formdev.flatlaf.extras.FlatInspector;
+import com.formdev.flatlaf.nativejna.windows.FlatWindowsNativeWindowBorder;
+import com.formdev.flatlaf.ui.FlatLineBorder;
+import net.miginfocom.swing.*;
+
+/**
+ * @author Karl Tauber
+ */
+public class FlatNativeWindowBorderTest
+ extends JPanel
+{
+ private static JFrame mainFrame;
+ private static WeakHashMap hiddenWindowsMap = new WeakHashMap<>();
+ private static int nextWindowId = 1;
+
+ private final Window window;
+ private final int windowId;
+
+ public static void main( String[] args ) {
+ SwingUtilities.invokeLater( () -> {
+ FlatLightLaf.install();
+ FlatInspector.install( "ctrl shift alt X" );
+
+ JFrame.setDefaultLookAndFeelDecorated( true );
+ JDialog.setDefaultLookAndFeelDecorated( true );
+
+ mainFrame = showFrame();
+ } );
+ }
+
+ private static JFrame showFrame() {
+ JFrame frame = new MyJFrame( "FlatNativeWindowBorderTest" );
+ frame.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE );
+ frame.add( new FlatNativeWindowBorderTest( frame ) );
+
+ ((JComponent) frame.getContentPane()).registerKeyboardAction( e -> {
+ frame.dispose();
+ }, KeyStroke.getKeyStroke( KeyEvent.VK_ESCAPE, 0, false ), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT );
+
+ frame.pack();
+ frame.setLocationRelativeTo( null );
+ int offset = 20 * Window.getWindows().length;
+ frame.setLocation( frame.getX() + offset, frame.getY() + offset );
+ frame.setVisible( true );
+ return frame;
+ }
+
+ private static void showDialog( Window owner ) {
+ JDialog dialog = new MyJDialog( owner, "FlatNativeWindowBorderTest Dialog", ModalityType.DOCUMENT_MODAL );
+ dialog.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE );
+ dialog.add( new FlatNativeWindowBorderTest( dialog ) );
+
+ ((JComponent) dialog.getContentPane()).registerKeyboardAction( e -> {
+ dialog.dispose();
+ }, KeyStroke.getKeyStroke( KeyEvent.VK_ESCAPE, 0, false ), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT );
+
+ dialog.pack();
+ dialog.setLocationRelativeTo( owner );
+ dialog.setLocation( dialog.getX() + 20, dialog.getY() + 20 );
+ dialog.setVisible( true );
+ }
+
+ private FlatNativeWindowBorderTest( Window window ) {
+ this.window = window;
+ this.windowId = nextWindowId++;
+
+ initComponents();
+
+ if( mainFrame == null )
+ hideWindowButton.setEnabled( false );
+
+ setBorder( new FlatLineBorder( new Insets( 0, 0, 0, 0 ), Color.red ) );
+ setPreferredSize( new Dimension( 800, 600 ) );
+
+ updateInfo();
+
+ ComponentListener componentListener = new ComponentAdapter() {
+ @Override
+ public void componentMoved( ComponentEvent e ) {
+ updateInfo();
+ }
+
+ @Override
+ public void componentResized( ComponentEvent e ) {
+ updateInfo();
+ }
+ };
+ window.addComponentListener( componentListener );
+ addComponentListener( componentListener );
+
+ window.addWindowListener( new WindowListener() {
+ @Override
+ public void windowOpened( WindowEvent e ) {
+ System.out.println( windowId + " windowOpened" );
+ }
+ @Override
+ public void windowClosing( WindowEvent e ) {
+ System.out.println( windowId + " windowClosing" );
+ }
+ @Override
+ public void windowClosed( WindowEvent e ) {
+ System.out.println( windowId + " windowClosed" );
+ }
+ @Override
+ public void windowIconified( WindowEvent e ) {
+ System.out.println( windowId + " windowIconified" );
+ }
+ @Override
+ public void windowDeiconified( WindowEvent e ) {
+ System.out.println( windowId + " windowDeiconified" );
+ }
+ @Override
+ public void windowActivated( WindowEvent e ) {
+ System.out.println( windowId + " windowActivated" );
+ }
+ @Override
+ public void windowDeactivated( WindowEvent e ) {
+ System.out.println( windowId + " windowDeactivated" );
+ }
+ } );
+ }
+
+ private void updateInfo() {
+ Toolkit toolkit = Toolkit.getDefaultToolkit();
+ GraphicsConfiguration gc = window.getGraphicsConfiguration();
+ DisplayMode dm = gc.getDevice().getDisplayMode();
+ Rectangle screenBounds = gc.getBounds();
+ Rectangle windowBounds = window.getBounds();
+ Rectangle clientBounds = new Rectangle( isShowing() ? getLocationOnScreen() : getLocation(), getSize() );
+
+ StringBuilder buf = new StringBuilder( 1500 );
+ buf.append( "" );
+
+ appendRow( buf, "Window bounds", toString( windowBounds ) );
+ appendRow( buf, "Client bounds", toString( clientBounds ) );
+ appendRow( buf, "Window / Panel gap", toString( diff( windowBounds, clientBounds ) ) );
+ if( window instanceof Frame && (((Frame)window).getExtendedState() & Frame.MAXIMIZED_BOTH) != 0 )
+ appendRow( buf, "Screen / Window gap", toString( diff( screenBounds, windowBounds ) ) );
+
+ appendEmptyRow( buf );
+
+ if( window instanceof Frame ) {
+ Rectangle maximizedBounds = ((Frame)window).getMaximizedBounds();
+ if( maximizedBounds != null ) {
+ appendRow( buf, "Maximized bounds", toString( maximizedBounds ) );
+ appendEmptyRow( buf );
+ }
+ }
+
+ appendRow( buf, "Physical screen size", dm.getWidth() + ", " + dm.getHeight() + " (" + dm.getBitDepth() + " Bit)" );
+ appendRow( buf, "Screen bounds", toString( screenBounds ) );
+ appendRow( buf, "Screen insets", toString( toolkit.getScreenInsets( gc ) ) );
+ appendRow( buf, "Scale factor", (int) (gc.getDefaultTransform().getScaleX() * 100) + "%" );
+
+ appendEmptyRow( buf );
+
+ appendRow( buf, "Java version", System.getProperty( "java.version" ) + " / " + System.getProperty( "java.vendor" ) );
+
+ buf.append( "" );
+ buf.append( "
" );
+
+ info.setText( buf.toString() );
+ }
+
+ private static void appendRow( StringBuilder buf, String key, String value ) {
+ buf.append( "| " )
+ .append( key )
+ .append( ": | " )
+ .append( value )
+ .append( " |
" );
+ }
+
+ private static void appendEmptyRow( StringBuilder buf ) {
+ buf.append( " | |
" );
+ }
+
+ private static String toString( Rectangle r ) {
+ if( r == null )
+ return "null";
+ return r.x + ", " + r.y + ", " + r.width + ", " + r.height;
+ }
+
+ private static String toString( Insets insets ) {
+ return insets.top + ", " + insets.left + ", " + insets.bottom + ", " + insets.right;
+ }
+
+ private static Rectangle diff( Rectangle r1, Rectangle r2 ) {
+ return new Rectangle(
+ r2.x - r1.x,
+ r2.y - r1.y,
+ (r1.x + r1.width) - (r2.x + r2.width),
+ (r1.y + r1.height) - (r2.y + r2.height) );
+ }
+
+ private void resizableChanged() {
+ if( window instanceof Frame )
+ ((Frame)window).setResizable( resizableCheckBox.isSelected() );
+ else if( window instanceof Dialog )
+ ((Dialog)window).setResizable( resizableCheckBox.isSelected() );
+ }
+
+ private void undecoratedChanged() {
+ window.dispose();
+
+ if( window instanceof Frame )
+ ((Frame)window).setUndecorated( undecoratedCheckBox.isSelected() );
+ else if( window instanceof Dialog )
+ ((Dialog)window).setUndecorated( undecoratedCheckBox.isSelected() );
+
+ window.setVisible( true );
+ }
+
+ private void maximizedBoundsChanged() {
+ if( window instanceof Frame ) {
+ ((Frame)window).setMaximizedBounds( maximizedBoundsCheckBox.isSelected()
+ ? new Rectangle( 50, 100, 1000, 700 )
+ : null );
+ updateInfo();
+ }
+ }
+
+ private void fullScreenChanged() {
+ boolean fullScreen = fullScreenCheckBox.isSelected();
+
+ GraphicsDevice gd = getGraphicsConfiguration().getDevice();
+ gd.setFullScreenWindow( fullScreen ? window : null );
+ }
+
+ private void nativeChanged() {
+ FlatWindowsNativeWindowBorder.getInstance().setHasCustomDecoration( window, nativeCheckBox.isSelected() );
+ }
+
+ private void revalidateLayout() {
+ window.revalidate();
+ }
+
+ private void replaceRootPane() {
+ JRootPane rootPane = new JRootPane();
+ if( window instanceof RootPaneContainer )
+ rootPane.setWindowDecorationStyle( ((RootPaneContainer)window).getRootPane().getWindowDecorationStyle() );
+ rootPane.getContentPane().add( new FlatNativeWindowBorderTest( window ) );
+
+ if( window instanceof MyJFrame )
+ ((MyJFrame)window).setRootPane( rootPane );
+ else if( window instanceof MyJDialog )
+ ((MyJDialog)window).setRootPane( rootPane );
+
+ window.revalidate();
+ window.repaint();
+ }
+
+ private void openDialog() {
+ showDialog( window );
+ }
+
+ private void openFrame() {
+ showFrame();
+ }
+
+ private void hideWindow() {
+ window.setVisible( false );
+ hiddenWindowsMap.put( window, null );
+ }
+
+ private void showHiddenWindow() {
+ for( Window w : hiddenWindowsMap.keySet() ) {
+ hiddenWindowsMap.remove( w );
+ w.setVisible( true );
+ break;
+ }
+ }
+
+ private void close() {
+ window.dispose();
+ }
+
+ private void initComponents() {
+ // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
+ info = new JLabel();
+ resizableCheckBox = new JCheckBox();
+ maximizedBoundsCheckBox = new JCheckBox();
+ undecoratedCheckBox = new JCheckBox();
+ fullScreenCheckBox = new JCheckBox();
+ nativeCheckBox = new JCheckBox();
+ revalidateButton = new JButton();
+ replaceRootPaneButton = new JButton();
+ openDialogButton = new JButton();
+ openFrameButton = new JButton();
+ hideWindowButton = new JButton();
+ showHiddenWindowButton = new JButton();
+ hSpacer1 = new JPanel(null);
+ closeButton = new JButton();
+
+ //======== this ========
+ setLayout(new MigLayout(
+ "insets dialog,hidemode 3",
+ // columns
+ "[]" +
+ "[]" +
+ "[grow,fill]",
+ // rows
+ "[grow,top]para" +
+ "[]0" +
+ "[]0" +
+ "[]" +
+ "[]"));
+
+ //---- info ----
+ info.setText("text");
+ add(info, "cell 0 0 2 1");
+
+ //---- resizableCheckBox ----
+ resizableCheckBox.setText("resizable");
+ resizableCheckBox.setSelected(true);
+ resizableCheckBox.setMnemonic('R');
+ resizableCheckBox.addActionListener(e -> resizableChanged());
+ add(resizableCheckBox, "cell 0 1");
+
+ //---- maximizedBoundsCheckBox ----
+ maximizedBoundsCheckBox.setText("maximized bounds (50,100, 1000,700)");
+ maximizedBoundsCheckBox.setMnemonic('M');
+ maximizedBoundsCheckBox.addActionListener(e -> maximizedBoundsChanged());
+ add(maximizedBoundsCheckBox, "cell 1 1");
+
+ //---- undecoratedCheckBox ----
+ undecoratedCheckBox.setText("undecorated");
+ undecoratedCheckBox.setMnemonic('U');
+ undecoratedCheckBox.addActionListener(e -> undecoratedChanged());
+ add(undecoratedCheckBox, "cell 0 2");
+
+ //---- fullScreenCheckBox ----
+ fullScreenCheckBox.setText("full screen");
+ fullScreenCheckBox.setMnemonic('F');
+ fullScreenCheckBox.addActionListener(e -> fullScreenChanged());
+ add(fullScreenCheckBox, "cell 1 2");
+
+ //---- nativeCheckBox ----
+ nativeCheckBox.setText("FlatLaf native window decorations");
+ nativeCheckBox.setSelected(true);
+ nativeCheckBox.addActionListener(e -> nativeChanged());
+ add(nativeCheckBox, "cell 0 3 3 1");
+
+ //---- revalidateButton ----
+ revalidateButton.setText("revalidate");
+ revalidateButton.addActionListener(e -> revalidateLayout());
+ add(revalidateButton, "cell 0 3 3 1");
+
+ //---- replaceRootPaneButton ----
+ replaceRootPaneButton.setText("replace rootpane");
+ replaceRootPaneButton.addActionListener(e -> replaceRootPane());
+ add(replaceRootPaneButton, "cell 0 3 3 1");
+
+ //---- openDialogButton ----
+ openDialogButton.setText("Open Dialog");
+ openDialogButton.setMnemonic('D');
+ openDialogButton.addActionListener(e -> openDialog());
+ add(openDialogButton, "cell 0 4 3 1");
+
+ //---- openFrameButton ----
+ openFrameButton.setText("Open Frame");
+ openFrameButton.setMnemonic('A');
+ openFrameButton.addActionListener(e -> openFrame());
+ add(openFrameButton, "cell 0 4 3 1");
+
+ //---- hideWindowButton ----
+ hideWindowButton.setText("Hide");
+ hideWindowButton.addActionListener(e -> hideWindow());
+ add(hideWindowButton, "cell 0 4 3 1");
+
+ //---- showHiddenWindowButton ----
+ showHiddenWindowButton.setText("Show hidden");
+ showHiddenWindowButton.addActionListener(e -> showHiddenWindow());
+ add(showHiddenWindowButton, "cell 0 4 3 1");
+ add(hSpacer1, "cell 0 4 3 1,growx");
+
+ //---- closeButton ----
+ closeButton.setText("Close");
+ closeButton.addActionListener(e -> close());
+ add(closeButton, "cell 0 4 3 1");
+ // JFormDesigner - End of component initialization //GEN-END:initComponents
+ }
+
+ // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables
+ private JLabel info;
+ private JCheckBox resizableCheckBox;
+ private JCheckBox maximizedBoundsCheckBox;
+ private JCheckBox undecoratedCheckBox;
+ private JCheckBox fullScreenCheckBox;
+ private JCheckBox nativeCheckBox;
+ private JButton revalidateButton;
+ private JButton replaceRootPaneButton;
+ private JButton openDialogButton;
+ private JButton openFrameButton;
+ private JButton hideWindowButton;
+ private JButton showHiddenWindowButton;
+ private JPanel hSpacer1;
+ private JButton closeButton;
+ // JFormDesigner - End of variables declaration //GEN-END:variables
+
+ //---- class MyJFrame -----------------------------------------------------
+
+ private static class MyJFrame
+ extends JFrame
+ {
+ MyJFrame( String title ) {
+ super( title );
+ }
+
+ @Override
+ public void setRootPane( JRootPane root ) {
+ super.setRootPane( root );
+ }
+ }
+
+ //---- class MyJDialog ----------------------------------------------------
+
+ private static class MyJDialog
+ extends JDialog
+ {
+ MyJDialog( Window owner, String title, Dialog.ModalityType modalityType ) {
+ super( owner, title, modalityType );
+ }
+
+ @Override
+ public void setRootPane( JRootPane root ) {
+ super.setRootPane( root );
+ }
+ }
+}
diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatNativeWindowBorderTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatNativeWindowBorderTest.jfd
new file mode 100644
index 00000000..7b0512bd
--- /dev/null
+++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatNativeWindowBorderTest.jfd
@@ -0,0 +1,129 @@
+JFDML JFormDesigner: "7.0.3.1.342" Java: "15" encoding: "UTF-8"
+
+new FormModel {
+ contentType: "form/swing"
+ root: new FormRoot {
+ add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
+ "$layoutConstraints": "insets dialog,hidemode 3"
+ "$columnConstraints": "[][][grow,fill]"
+ "$rowConstraints": "[grow,top]para[]0[]0[][]"
+ } ) {
+ name: "this"
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "info"
+ "text": "text"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 0 2 1"
+ } )
+ add( new FormComponent( "javax.swing.JCheckBox" ) {
+ name: "resizableCheckBox"
+ "text": "resizable"
+ "selected": true
+ "mnemonic": 82
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "resizableChanged", false ) )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 1"
+ } )
+ add( new FormComponent( "javax.swing.JCheckBox" ) {
+ name: "maximizedBoundsCheckBox"
+ "text": "maximized bounds (50,100, 1000,700)"
+ "mnemonic": 77
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "maximizedBoundsChanged", false ) )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 1"
+ } )
+ add( new FormComponent( "javax.swing.JCheckBox" ) {
+ name: "undecoratedCheckBox"
+ "text": "undecorated"
+ "mnemonic": 85
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "undecoratedChanged", false ) )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 2"
+ } )
+ add( new FormComponent( "javax.swing.JCheckBox" ) {
+ name: "fullScreenCheckBox"
+ "text": "full screen"
+ "mnemonic": 70
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "fullScreenChanged", false ) )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 2"
+ } )
+ add( new FormComponent( "javax.swing.JCheckBox" ) {
+ name: "nativeCheckBox"
+ "text": "FlatLaf native window decorations"
+ "selected": true
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "nativeChanged", false ) )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 3 3 1"
+ } )
+ add( new FormComponent( "javax.swing.JButton" ) {
+ name: "revalidateButton"
+ "text": "revalidate"
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "revalidateLayout", false ) )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 3 3 1"
+ } )
+ add( new FormComponent( "javax.swing.JButton" ) {
+ name: "replaceRootPaneButton"
+ "text": "replace rootpane"
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "replaceRootPane", false ) )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 3 3 1"
+ } )
+ add( new FormComponent( "javax.swing.JButton" ) {
+ name: "openDialogButton"
+ "text": "Open Dialog"
+ "mnemonic": 68
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "openDialog", false ) )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 4 3 1"
+ } )
+ add( new FormComponent( "javax.swing.JButton" ) {
+ name: "openFrameButton"
+ "text": "Open Frame"
+ "mnemonic": 65
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "openFrame", false ) )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 4 3 1"
+ } )
+ add( new FormComponent( "javax.swing.JButton" ) {
+ name: "hideWindowButton"
+ "text": "Hide"
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "hideWindow", false ) )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 4 3 1"
+ } )
+ add( new FormComponent( "javax.swing.JButton" ) {
+ name: "showHiddenWindowButton"
+ "text": "Show hidden"
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "showHiddenWindow", false ) )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 4 3 1"
+ } )
+ add( new FormComponent( "com.jformdesigner.designer.wrapper.HSpacer" ) {
+ name: "hSpacer1"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 4 3 1,growx"
+ } )
+ add( new FormComponent( "javax.swing.JButton" ) {
+ name: "closeButton"
+ "text": "Close"
+ addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "close", false ) )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 4 3 1"
+ } )
+ }, new FormLayoutConstraints( null ) {
+ "location": new java.awt.Point( 0, 0 )
+ "size": new java.awt.Dimension( 500, 300 )
+ } )
+ }
+}
diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowDecorationsTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowDecorationsTest.java
index f82312e4..d3ed01a0 100644
--- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowDecorationsTest.java
+++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowDecorationsTest.java
@@ -202,6 +202,7 @@ public class FlatWindowDecorationsTest
private void openDialog() {
Window owner = SwingUtilities.windowForComponent( this );
JDialog dialog = new JDialog( owner, "Dialog", ModalityType.APPLICATION_MODAL );
+ dialog.setDefaultCloseOperation( JDialog.DISPOSE_ON_CLOSE );
dialog.add( new FlatWindowDecorationsTest() );
dialog.pack();
dialog.setLocationRelativeTo( this );
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 20e32de3..a69da8d5 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -17,6 +17,7 @@
rootProject.name = "FlatLaf"
include( "flatlaf-core" )
+include( "flatlaf-native-jna" )
include( "flatlaf-extras" )
include( "flatlaf-swingx" )
include( "flatlaf-jide-oss" )