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 04d73c04..84841dd8 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java @@ -44,6 +44,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; @@ -57,8 +58,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.LoggingFacade; import com.formdev.flatlaf.util.MultiResolutionImageSupport; @@ -144,27 +145,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: + *

+ * 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..45a24b2d 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-natives-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..63e62ee1 --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowBorder.java @@ -0,0 +1,303 @@ +/* + * 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.Color; +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 javax.swing.event.ChangeListener; +import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.FlatSystemProperties; +import com.formdev.flatlaf.ui.JBRCustomDecorations.JBRWindowTopBorder; +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 window, which is the case when switching LaF. + // Also check whether the window is displayable, which is required to install + // FlatLaf native window border. + // If the window is not displayable, then it was probably closed/disposed but not yet removed + // from the list of windows that AWT maintains and returns with Window.getWindows(). + // It could be also be a window that is currently hidden, but may be shown later. + Window window = SwingUtilities.windowForComponent( rootPane ); + if( window != null && window.isDisplayable() ) { + 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 FlatLaf/JBR native window 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 FlatLaf/JBR native window 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, Rectangle appIconBounds ) + { + if( canUseJBRCustomDecorations ) { + JBRCustomDecorations.setTitleBarHeightAndHitTestSpots( window, titleBarHeight, hitTestSpots ); + return; + } + + if( !isSupported() ) + return; + + nativeProvider.setTitleBarHeight( window, titleBarHeight ); + nativeProvider.setTitleBarHitTestSpots( window, hitTestSpots ); + nativeProvider.setTitleBarAppIconBounds( window, appIconBounds ); + } + + 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.natives.jna.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 ); + void setTitleBarAppIconBounds( Window window, Rectangle appIconBounds ); + + boolean isColorizationColorAffectsBorders(); + Color getColorizationColor(); + int getColorizationColorBalance(); + + void addChangeListener( ChangeListener l ); + void removeChangeListener( ChangeListener l ); + } + + //---- class WindowTopBorder ------------------------------------------- + + static class WindowTopBorder + extends JBRCustomDecorations.JBRWindowTopBorder + { + private static WindowTopBorder instance; + + static JBRWindowTopBorder getInstance() { + if( canUseJBRCustomDecorations ) + return JBRWindowTopBorder.getInstance(); + + if( instance == null ) + instance = new WindowTopBorder(); + return instance; + } + + @Override + void installListeners() { + nativeProvider.addChangeListener( e -> { + update(); + + // repaint top borders of all windows + for( Window window : Window.getWindows() ) { + if( window.isDisplayable() ) + window.repaint( 0, 0, window.getWidth(), 1 ); + } + } ); + } + + @Override + boolean isColorizationColorAffectsBorders() { + return nativeProvider.isColorizationColorAffectsBorders(); + } + + @Override + Color getColorizationColor() { + return nativeProvider.getColorizationColor(); + } + + @Override + int getColorizationColorBalance() { + return nativeProvider.getColorizationColorBalance(); + } + } +} 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..52d7a82f 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 @@ -63,7 +63,7 @@ import javax.swing.border.AbstractBorder; import javax.swing.border.Border; import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.FlatSystemProperties; -import com.formdev.flatlaf.ui.JBRCustomDecorations.JBRWindowTopBorder; +import com.formdev.flatlaf.ui.FlatNativeWindowBorder.WindowTopBorder; import com.formdev.flatlaf.util.ScaledImageIcon; import com.formdev.flatlaf.util.SystemInfo; import com.formdev.flatlaf.util.UIScale; @@ -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,15 @@ 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 ); + } + if( debugAppIconBounds != null ) { + g.setColor( Color.red ); + Point offset = SwingUtilities.convertPoint( this, 0, 0, window ); + Rectangle r = debugAppIconBounds; + g.drawRect( r.x - offset.x, r.y - offset.y, r.width - 1, r.height - 1 ); } } debug*/ @@ -503,9 +510,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 +608,66 @@ 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<>(); + Rectangle appIconBounds = null; + if( iconLabel.isVisible() ) { + // compute real icon size (without insets) + Point location = SwingUtilities.convertPoint( iconLabel, 0, 0, window ); + Insets iconInsets = iconLabel.getInsets(); + Rectangle iconBounds = new Rectangle( + location.x + iconInsets.left, + location.y + iconInsets.top, + iconLabel.getWidth() - iconInsets.left - iconInsets.right, + iconLabel.getHeight() - iconInsets.top - iconInsets.bottom ); + + if( hasJBRCustomDecoration() ) + hitTestSpots.add( iconBounds ); + else + appIconBounds = iconBounds; + } + addNativeHitTestSpot( buttonPanel, false, hitTestSpots ); + addNativeHitTestSpot( menuBarPlaceholder, true, hitTestSpots ); + + FlatNativeWindowBorder.setTitleBarHeightAndHitTestSpots( window, titleBarHeight, hitTestSpots, appIconBounds ); /*debug - debugHitTestSpots = hitTestSpots; debugTitleBarHeight = titleBarHeight; + debugHitTestSpots = hitTestSpots; + debugAppIconBounds = appIconBounds; 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 +682,9 @@ debug*/ } /*debug - private List debugHitTestSpots; private int debugTitleBarHeight; + private List debugHitTestSpots; + private Rectangle debugAppIconBounds; debug*/ //---- class TitlePaneBorder ---------------------------------------------- @@ -676,8 +704,8 @@ debug*/ } else if( borderColor != null && (rootPane.getJMenuBar() == null || !rootPane.getJMenuBar().isVisible()) ) insets.bottom += UIScale.scale( 1 ); - if( hasJBRCustomDecoration() ) - insets = FlatUIUtils.addInsets( insets, JBRWindowTopBorder.getInstance().getBorderInsets() ); + if( hasNativeCustomDecoration() ) + insets = FlatUIUtils.addInsets( insets, WindowTopBorder.getInstance().getBorderInsets() ); return insets; } @@ -695,8 +723,8 @@ debug*/ FlatUIUtils.paintFilledRectangle( g, borderColor, x, y + height - lineHeight, width, lineHeight ); } - if( hasJBRCustomDecoration() ) - JBRWindowTopBorder.getInstance().paintBorder( c, g, x, y, width, height ); + if( hasNativeCustomDecoration() ) + WindowTopBorder.getInstance().paintBorder( c, g, x, y, width, height ); } protected Border getMenuBarBorder() { @@ -730,7 +758,7 @@ debug*/ break; case "componentOrientation": - updateJBRHitTestSpotsAndTitleBarHeightLater(); + updateNativeTitleBarHeightAndHitTestSpotsLater(); break; } } @@ -740,10 +768,10 @@ debug*/ @Override public void windowActivated( WindowEvent e ) { activeChanged( true ); - updateJBRHitTestSpotsAndTitleBarHeight(); + updateNativeTitleBarHeightAndHitTestSpots(); - if( hasJBRCustomDecoration() ) - JBRWindowTopBorder.getInstance().repaintBorder( FlatTitlePane.this ); + if( hasNativeCustomDecoration() ) + WindowTopBorder.getInstance().repaintBorder( FlatTitlePane.this ); repaintWindowBorder(); } @@ -751,10 +779,10 @@ debug*/ @Override public void windowDeactivated( WindowEvent e ) { activeChanged( false ); - updateJBRHitTestSpotsAndTitleBarHeight(); + updateNativeTitleBarHeightAndHitTestSpots(); - if( hasJBRCustomDecoration() ) - JBRWindowTopBorder.getInstance().repaintBorder( FlatTitlePane.this ); + if( hasNativeCustomDecoration() ) + WindowTopBorder.getInstance().repaintBorder( FlatTitlePane.this ); repaintWindowBorder(); } @@ -762,7 +790,7 @@ debug*/ @Override public void windowStateChanged( WindowEvent e ) { frameStateChanged(); - updateJBRHitTestSpotsAndTitleBarHeight(); + updateNativeTitleBarHeightAndHitTestSpots(); } //---- interface MouseListener ---- @@ -775,7 +803,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 +836,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 +880,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 e79fb165..ee95f2fb 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,9 +29,8 @@ 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 javax.swing.JDialog; -import javax.swing.JFrame; import javax.swing.JRootPane; import javax.swing.SwingUtilities; import javax.swing.UIManager; @@ -54,26 +53,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 @@ -87,8 +89,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( () -> { @@ -97,54 +100,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 ) { @@ -159,35 +128,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 ) { LoggingFacade.INSTANCE.logSevere( 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 ) { LoggingFacade.INSTANCE.logSevere( 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 ) @@ -203,15 +175,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 } @@ -236,15 +210,22 @@ public class JBRCustomDecorations return instance; } - private JBRWindowTopBorder() { + JBRWindowTopBorder() { super( 1, 0, 0, 0 ); - colorizationAffectsBorders = calculateAffectsBorders(); - activeColor = calculateActiveBorderColor(); + update(); + installListeners(); + } + void update() { + colorizationAffectsBorders = isColorizationColorAffectsBorders(); + activeColor = calculateActiveBorderColor(); + } + + void installListeners() { Toolkit toolkit = Toolkit.getDefaultToolkit(); toolkit.addPropertyChangeListener( "win.dwm.colorizationColor.affects.borders", e -> { - colorizationAffectsBorders = calculateAffectsBorders(); + colorizationAffectsBorders = isColorizationColorAffectsBorders(); activeColor = calculateActiveBorderColor(); } ); @@ -256,46 +237,50 @@ public class JBRCustomDecorations toolkit.addPropertyChangeListener( "win.frame.activeBorderColor", l ); } - private boolean calculateAffectsBorders() { + boolean isColorizationColorAffectsBorders() { Object value = Toolkit.getDefaultToolkit().getDesktopProperty( "win.dwm.colorizationColor.affects.borders" ); return (value instanceof Boolean) ? (Boolean) value : true; } + Color getColorizationColor() { + return (Color) Toolkit.getDefaultToolkit().getDesktopProperty( "win.dwm.colorizationColor" ); + } + + int getColorizationColorBalance() { + Object value = Toolkit.getDefaultToolkit().getDesktopProperty( "win.dwm.colorizationColorBalance" ); + return (value instanceof Integer) ? (Integer) value : -1; + } + private Color calculateActiveBorderColor() { if( !colorizationAffectsBorders ) return defaultActiveBorder; - Toolkit toolkit = Toolkit.getDefaultToolkit(); - Color colorizationColor = (Color) toolkit.getDesktopProperty( "win.dwm.colorizationColor" ); + Color colorizationColor = getColorizationColor(); if( colorizationColor != null ) { - Object colorizationColorBalanceObj = toolkit.getDesktopProperty( "win.dwm.colorizationColorBalance" ); - if( colorizationColorBalanceObj instanceof Integer ) { - int colorizationColorBalance = (Integer) colorizationColorBalanceObj; - if( colorizationColorBalance < 0 || colorizationColorBalance > 100 ) - colorizationColorBalance = 100; + int colorizationColorBalance = getColorizationColorBalance(); + if( colorizationColorBalance < 0 || colorizationColorBalance > 100 ) + colorizationColorBalance = 100; - if( colorizationColorBalance == 0 ) - return new Color( 0xD9D9D9 ); - if( colorizationColorBalance == 100 ) - return colorizationColor; + if( colorizationColorBalance == 0 ) + return new Color( 0xD9D9D9 ); + if( colorizationColorBalance == 100 ) + return colorizationColor; - float alpha = colorizationColorBalance / 100.0f; - float remainder = 1 - alpha; - int r = Math.round( colorizationColor.getRed() * alpha + 0xD9 * remainder ); - int g = Math.round( colorizationColor.getGreen() * alpha + 0xD9 * remainder ); - int b = Math.round( colorizationColor.getBlue() * alpha + 0xD9 * remainder ); + float alpha = colorizationColorBalance / 100.0f; + float remainder = 1 - alpha; + int r = Math.round( colorizationColor.getRed() * alpha + 0xD9 * remainder ); + int g = Math.round( colorizationColor.getGreen() * alpha + 0xD9 * remainder ); + int b = Math.round( colorizationColor.getBlue() * alpha + 0xD9 * remainder ); - // avoid potential IllegalArgumentException in Color constructor - r = Math.min( Math.max( r, 0 ), 255 ); - g = Math.min( Math.max( g, 0 ), 255 ); - b = Math.min( Math.max( b, 0 ), 255 ); + // avoid potential IllegalArgumentException in Color constructor + r = Math.min( Math.max( r, 0 ), 255 ); + g = Math.min( Math.max( g, 0 ), 255 ); + b = Math.min( Math.max( b, 0 ), 255 ); - return new Color( r, g, b ); - } - return colorizationColor; + return new Color( r, g, b ); } - Color activeBorderColor = (Color) toolkit.getDesktopProperty( "win.frame.activeBorderColor" ); + Color activeBorderColor = (Color) Toolkit.getDefaultToolkit().getDesktopProperty( "win.frame.activeBorderColor" ); return (activeBorderColor != null) ? activeBorderColor : UIManager.getColor( "MenuBar.borderColor" ); } 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..2d3d4f2f 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-natives-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-natives-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-natives/flatlaf-natives-jna/build.gradle.kts b/flatlaf-natives/flatlaf-natives-jna/build.gradle.kts new file mode 100644 index 00000000..92ebbe86 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-jna/build.gradle.kts @@ -0,0 +1,25 @@ +/* + * 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` +} + +dependencies { + implementation( project( ":flatlaf-core" ) ) + implementation( "net.java.dev.jna:jna:5.7.0" ) + implementation( "net.java.dev.jna:jna-platform:5.7.0" ) +} diff --git a/flatlaf-natives/flatlaf-natives-jna/src/main/java/com/formdev/flatlaf/natives/jna/windows/FlatWindowsNativeWindowBorder.java b/flatlaf-natives/flatlaf-natives-jna/src/main/java/com/formdev/flatlaf/natives/jna/windows/FlatWindowsNativeWindowBorder.java new file mode 100644 index 00000000..475b172f --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-jna/src/main/java/com/formdev/flatlaf/natives/jna/windows/FlatWindowsNativeWindowBorder.java @@ -0,0 +1,693 @@ +/* + * 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.natives.jna.windows; + +import static com.sun.jna.platform.win32.ShellAPI.*; +import static com.sun.jna.platform.win32.WinReg.*; +import static com.sun.jna.platform.win32.WinUser.*; +import java.awt.Color; +import java.awt.Dialog; +import java.awt.EventQueue; +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 javax.swing.Timer; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.EventListenerList; +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.Advapi32Util; +import com.sun.jna.platform.win32.BaseTSD; +import com.sun.jna.platform.win32.BaseTSD.ULONG_PTR; +import com.sun.jna.platform.win32.Shell32; +import com.sun.jna.platform.win32.User32; +import com.sun.jna.platform.win32.WTypes.LPWSTR; +import com.sun.jna.platform.win32.WinDef.HMENU; +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.UINT_PTR; +import com.sun.jna.platform.win32.WinDef.WPARAM; +import com.sun.jna.platform.win32.WinUser.HMONITOR; +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/oberth/custom-chrome +// 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 final EventListenerList listenerList = new EventListenerList(); + private Timer fireStateChangedTimer; + + private boolean colorizationUpToDate; + private boolean colorizationColorAffectsBorders; + private Color colorizationColor; + private int colorizationColorBalance; + + 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()] ); + } + + @Override + public void setTitleBarAppIconBounds( Window window, Rectangle appIconBounds ) { + WndProc wndProc = windowsMap.get( window ); + if( wndProc == null ) + return; + + wndProc.appIconBounds = (appIconBounds != null) ? new Rectangle( appIconBounds ) : null; + } + + @Override + public boolean isColorizationColorAffectsBorders() { + updateColorization(); + return colorizationColorAffectsBorders; + } + + @Override + public Color getColorizationColor() { + updateColorization(); + return colorizationColor; + } + + @Override + public int getColorizationColorBalance() { + updateColorization(); + return colorizationColorBalance; + } + + private void updateColorization() { + if( colorizationUpToDate ) + return; + colorizationUpToDate = true; + + String subKey = "SOFTWARE\\Microsoft\\Windows\\DWM"; + + int value = RegGetDword( HKEY_CURRENT_USER, subKey, "ColorPrevalence" ); + colorizationColorAffectsBorders = (value > 0); + + value = RegGetDword( HKEY_CURRENT_USER, subKey, "ColorizationColor" ); + colorizationColor = (value != -1) ? new Color( value ) : null; + + colorizationColorBalance = RegGetDword( HKEY_CURRENT_USER, subKey, "ColorizationColorBalance" ); + } + + private static int RegGetDword( HKEY hkey, String lpSubKey, String lpValue ) { + try { + return Advapi32Util.registryGetIntValue( hkey, lpSubKey, lpValue ); + } catch( RuntimeException ex ) { + return -1; + } + } + + @Override + public void addChangeListener( ChangeListener l ) { + listenerList.add( ChangeListener.class, l ); + } + + @Override + public void removeChangeListener( ChangeListener l ) { + listenerList.remove( ChangeListener.class, l ); + } + + private void fireStateChanged() { + Object[] listeners = listenerList.getListenerList(); + if( listeners.length == 0 ) + return; + + ChangeEvent e = new ChangeEvent( this ); + for( int i = 0; i < listeners.length; i += 2 ) { + if( listeners[i] == ChangeListener.class ) + ((ChangeListener)listeners[i+1]).stateChanged( e ); + } + } + + /** + * Because there may be sent many WM_DWMCOLORIZATIONCOLORCHANGED messages, + * slightly delay event firing and fire it only once (on the AWT thread). + */ + void fireStateChangedLaterOnce() { + EventQueue.invokeLater( () -> { + if( fireStateChangedTimer != null ) { + fireStateChangedTimer.restart(); + return; + } + + fireStateChangedTimer = new Timer( 300, e -> { + fireStateChangedTimer = null; + colorizationUpToDate = false; + + fireStateChanged(); + } ); + fireStateChangedTimer.setRepeats( false ); + fireStateChangedTimer.start(); + } ); + } + + //---- class WndProc ------------------------------------------------------ + + private class WndProc + implements WindowProc + { + private static final int GWLP_WNDPROC = -4; + + private static final int + WM_NCCALCSIZE = 0x0083, + WM_NCHITTEST = 0x0084, + WM_NCRBUTTONUP = 0x00A5, + WM_DWMCOLORIZATIONCOLORCHANGED = 0x0320; + + // WM_NCHITTEST mouse position codes + private static final int + HTCLIENT = 1, + HTCAPTION = 2, + HTSYSMENU = 3, + HTTOP = 12; + + private static final int ABS_AUTOHIDE = 0x0000001; + private static final int ABM_GETAUTOHIDEBAREX = 0x0000000b; + + private static final int + SC_SIZE = 0xF000, + SC_MOVE = 0xF010, + SC_MINIMIZE = 0xF020, + SC_MAXIMIZE = 0xF030, + SC_CLOSE = 0xF060, + SC_RESTORE = 0xF120; + + private static final int + MIIM_STATE = 0x00000001, + MFT_STRING = 0x00000000, + MF_ENABLED = 0x00000000, + MF_DISABLED = 0x00000002, + TPM_RETURNCMD = 0x0100; + + private Window window; + private final HWND hwnd; + private final BaseTSD.LONG_PTR defaultWndProc; + + private int titleBarHeight; + private Rectangle[] hitTestSpots; + private Rectangle appIconBounds; + + 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_NCRBUTTONUP: + if( wParam.longValue() == HTCAPTION || wParam.longValue() == HTSYSMENU ) + openSystemMenu( hwnd, GET_X_LPARAM( lParam ), GET_Y_LPARAM( lParam ) ); + break; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + fireStateChangedLaterOnce(); + break; + + case WM_DESTROY: + return WmDestroy( hwnd, uMsg, wParam, lParam ); + } + + 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(); + + // check whether taskbar is in the autohide state + APPBARDATA autohide = new APPBARDATA(); + autohide.cbSize = new DWORD( autohide.size() ); + int state = Shell32.INSTANCE.SHAppBarMessage( new DWORD( ABM_GETSTATE ), autohide ).intValue(); + if( (state & ABS_AUTOHIDE) != 0 ) { + // get monitor info + // (using MONITOR_DEFAULTTONEAREST finds right monitor when restoring from minimized) + HMONITOR hMonitor = User32.INSTANCE.MonitorFromWindow( hwnd, MONITOR_DEFAULTTONEAREST ); + MONITORINFO monitorInfo = new MONITORINFO(); + User32.INSTANCE.GetMonitorInfo( hMonitor, monitorInfo ); + + // If there's a taskbar on any side of the monitor, reduce our size + // a little bit on that edge. + if( hasAutohideTaskbar( ABE_TOP, monitorInfo.rcMonitor ) ) + params.rgrc[0].top++; + if( hasAutohideTaskbar( ABE_BOTTOM, monitorInfo.rcMonitor ) ) + params.rgrc[0].bottom--; + if( hasAutohideTaskbar( ABE_LEFT, monitorInfo.rcMonitor ) ) + params.rgrc[0].left++; + if( hasAutohideTaskbar( ABE_RIGHT, monitorInfo.rcMonitor ) ) + params.rgrc[0].right--; + } + } + + // 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; + + // return HTSYSMENU if mouse is over application icon + // - left-click on HTSYSMENU area shows system menu + // - double-left-click sends WM_CLOSE + if( appIconBounds != null && appIconBounds.contains( sx, sy ) ) + return new LRESULT( HTSYSMENU ); + + 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 ); + } + + /** + * Returns whether there is an autohide taskbar on the given edge. + */ + private boolean hasAutohideTaskbar( int edge, RECT rcMonitor ) { + APPBARDATA data = new APPBARDATA(); + data.cbSize = new DWORD( data.size() ); + data.uEdge = new UINT( edge ); + data.rc = rcMonitor; + UINT_PTR hTaskbar = Shell32.INSTANCE.SHAppBarMessage( new DWORD( ABM_GETAUTOHIDEBAREX ), data ); + return hTaskbar.longValue() != 0; + } + + /** + * Scales down in the same way as AWT. + * See AwtWin32GraphicsDevice::ScaleDownX() and ::ScaleDownY() + */ + 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); + } + + /** + * Opens the window's system menu. + * The system menu is the menu that opens when the user presses Alt+Space or + * right clicks on the title bar + */ + private void openSystemMenu( HWND hwnd, int x, int y ) { + // get system menu + HMENU systemMenu = User32Ex.INSTANCE.GetSystemMenu( hwnd, false ); + + // update system menu + int style = User32.INSTANCE.GetWindowLong( hwnd, GWL_STYLE ); + boolean isMaximized = User32Ex.INSTANCE.IsZoomed( hwnd ); + setMenuItemState( systemMenu, SC_RESTORE, isMaximized ); + setMenuItemState( systemMenu, SC_MOVE, !isMaximized ); + setMenuItemState( systemMenu, SC_SIZE, (style & WS_THICKFRAME) != 0 && !isMaximized ); + setMenuItemState( systemMenu, SC_MINIMIZE, (style & WS_MINIMIZEBOX) != 0 ); + setMenuItemState( systemMenu, SC_MAXIMIZE, (style & WS_MAXIMIZEBOX) != 0 && !isMaximized ); + setMenuItemState( systemMenu, SC_CLOSE, true ); + + // make "Close" item the default to be consistent with the system menu shown + // when pressing Alt+Space + User32Ex.INSTANCE.SetMenuDefaultItem( systemMenu, SC_CLOSE, 0 ); + + // show system menu + int ret = User32Ex.INSTANCE.TrackPopupMenu( systemMenu, TPM_RETURNCMD, + x, y, 0, hwnd, null ).intValue(); + if( ret != 0 ) + User32Ex.INSTANCE.PostMessage( hwnd, WM_SYSCOMMAND, new WPARAM( ret ), null ); + } + + private void setMenuItemState( HMENU systemMenu, int item, boolean enabled ) { + MENUITEMINFO mii = new MENUITEMINFO(); + mii.cbSize = new UINT( mii.size() ); + mii.fMask = new UINT( MIIM_STATE ); + mii.fType = new UINT( MFT_STRING ); + mii.fState = new UINT( enabled ? MF_ENABLED : MF_DISABLED ); + User32Ex.INSTANCE.SetMenuItemInfo( systemMenu, item, false, mii ); + } + } + + //---- 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 ); + + HMENU GetSystemMenu( HWND hWnd, boolean bRevert ); + boolean SetMenuItemInfo( HMENU hmenu, int item, boolean fByPositon, MENUITEMINFO lpmii ); + boolean SetMenuDefaultItem( HMENU hMenu, int uItem, int fByPos ); + BOOL TrackPopupMenu( HMENU hMenu, int uFlags, int x, int y, int nReserved, HWND hWnd, RECT prcRect ); + } + + //---- 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(); + } + } + + //---- class MENUITEMINFO ------------------------------------------------- + + @FieldOrder( { "cbSize", "fMask", "fType", "fState", "wID", "hSubMenu", + "hbmpChecked", "hbmpUnchecked", "dwItemData", "dwTypeData", "cch", "hbmpItem" } ) + public static class MENUITEMINFO + extends Structure + { + public UINT cbSize; + public UINT fMask; + public UINT fType; + public UINT fState; + public UINT wID; + public HMENU hSubMenu; + public HBITMAP hbmpChecked; + public HBITMAP hbmpUnchecked; + public ULONG_PTR dwItemData; + public LPWSTR dwTypeData; + public UINT cch; + public HBITMAP hbmpItem; + } +} diff --git a/flatlaf-testing/build.gradle.kts b/flatlaf-testing/build.gradle.kts index 41489b4a..4cf8b850 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-natives-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..792695be --- /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.ui.FlatLineBorder; +import com.formdev.flatlaf.ui.FlatNativeWindowBorder; +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() { + FlatNativeWindowBorder.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..260ca231 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,3 +24,10 @@ include( "flatlaf-intellij-themes" ) include( "flatlaf-demo" ) include( "flatlaf-testing" ) include( "flatlaf-theme-editor" ) + +includeProject( "flatlaf-natives-jna", "flatlaf-natives/flatlaf-natives-jna" ) + +fun includeProject( projectPath: String, projectDir: String ) { + include( projectPath ) + project( ":$projectPath" ).projectDir = file( projectDir ) +}