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 cac59091..a7b1c2a7 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java @@ -43,6 +43,8 @@ import javax.swing.BorderFactory; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JFrame; import javax.swing.LookAndFeel; import javax.swing.PopupFactory; import javax.swing.SwingUtilities; @@ -57,6 +59,7 @@ import javax.swing.plaf.basic.BasicLookAndFeel; import javax.swing.text.StyleContext; import javax.swing.text.html.HTMLEditorKit; 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; @@ -110,8 +113,32 @@ public abstract class FlatLaf public abstract boolean isDark(); + /** + * Returns whether FlatLaf supports custom window decorations. + *

+ * To use custom window decorations in your application, enable them with + * (before creating any frames or dialogs): + *

+	 * JFrame.setDefaultLookAndFeelDecorated( true );
+	 * JDialog.setDefaultLookAndFeelDecorated( true );
+	 * 
+ *

+ * Returns {@code true} on Windows, {@code false} otherwise. + *

+ * Return also {@code false} if running on Windows 10 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 + * {@link JDialog#isDefaultLookAndFeelDecorated()} return {@code true}. + */ @Override public boolean getSupportsWindowDecorations() { + if( SystemInfo.IS_JETBRAINS_JVM_11_OR_LATER && + SystemInfo.IS_WINDOWS_10_OR_LATER && + JBRCustomDecorations.isSupported() ) + return false; + return SystemInfo.IS_WINDOWS; } 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 6396890d..a1952205 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 @@ -30,6 +30,7 @@ import javax.swing.JMenuBar; import javax.swing.JRootPane; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.BasicRootPaneUI; +import com.formdev.flatlaf.util.SystemInfo; /** * Provides the Flat LaF UI delegate for {@link javax.swing.JRootPane}. @@ -55,6 +56,9 @@ public class FlatRootPaneUI if( rootPane.getWindowDecorationStyle() != JRootPane.NONE ) installClientDecorations(); + + if( SystemInfo.IS_JETBRAINS_JVM_11_OR_LATER && SystemInfo.IS_WINDOWS_10_OR_LATER ) + JBRCustomDecorations.install( rootPane ); } @Override 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 1de1b986..45130581 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 @@ -20,14 +20,18 @@ import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dialog; import java.awt.Dimension; +import java.awt.EventQueue; import java.awt.Frame; import java.awt.Graphics; import java.awt.GraphicsConfiguration; import java.awt.Image; import java.awt.Insets; +import java.awt.Point; import java.awt.Rectangle; import java.awt.Window; import java.awt.event.ActionListener; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; @@ -36,6 +40,7 @@ import java.awt.event.WindowEvent; import java.awt.geom.AffineTransform; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.util.ArrayList; import java.util.List; import javax.accessibility.AccessibleContext; import javax.swing.BorderFactory; @@ -233,6 +238,8 @@ class FlatTitlePane titleLabel.setText( getWindowTitle() ); installWindowListeners(); } + + updateJBRHitTestSpotsAndTitleBarHeight(); } @Override @@ -258,6 +265,7 @@ class FlatTitlePane window.addPropertyChangeListener( handler ); window.addWindowListener( handler ); window.addWindowStateListener( handler ); + window.addComponentListener( handler ); } private void uninstallWindowListeners() { @@ -267,6 +275,7 @@ class FlatTitlePane window.removePropertyChangeListener( handler ); window.removeWindowListener( handler ); window.removeWindowStateListener( handler ); + window.removeComponentListener( handler ); } @Override @@ -283,8 +292,12 @@ class FlatTitlePane } private void maximize() { - if( window instanceof Frame ) { - Frame frame = (Frame) window; + if( !(window instanceof Frame) ) + return; + + Frame frame = (Frame) window; + + if( !hasJBRCustomDecoration() ) { GraphicsConfiguration gc = window.getGraphicsConfiguration(); // remember current maximized bounds @@ -325,6 +338,11 @@ class FlatTitlePane // restore old maximized bounds frame.setMaximizedBounds( oldMaximizedBounds ); + } else { + // not necessary to set maximized bounds when running in JBR + + // maximize window + frame.setExtendedState( frame.getExtendedState() | Frame.MAXIMIZED_BOTH ); } } @@ -343,11 +361,45 @@ class FlatTitlePane window.dispatchEvent( new WindowEvent( window, WindowEvent.WINDOW_CLOSING ) ); } + private boolean hasJBRCustomDecoration() { + return window != null && JBRCustomDecorations.hasCustomDecoration( window ); + } + + private void updateJBRHitTestSpotsAndTitleBarHeight() { + if( !isDisplayable() ) + return; + + if( !hasJBRCustomDecoration() ) + return; + + List hitTestSpots = new ArrayList<>(); + addJBRHitTestSpot( buttonPanel, hitTestSpots ); + + int titleBarHeight = getHeight(); + // slightly reduce height so that component receives mouseExit events + if( titleBarHeight > 0 ) + titleBarHeight--; + + JBRCustomDecorations.setHitTestSpotsAndTitleBarHeight( window, hitTestSpots, titleBarHeight ); + } + + private void addJBRHitTestSpot( JComponent c, List hitTestSpots ) { + Dimension size = c.getSize(); + if( size.width <= 0 || size.height <= 0 ) + return; + + Point location = SwingUtilities.convertPoint( c, 0, 0, window ); + Rectangle r = new Rectangle( location, size ); + // slightly increase rectangle so that component receives mouseExit events + r.grow( 2, 2 ); + hitTestSpots.add( r ); + } + //---- class Handler ------------------------------------------------------ private class Handler extends WindowAdapter - implements PropertyChangeListener, MouseListener, MouseMotionListener + implements PropertyChangeListener, MouseListener, MouseMotionListener, ComponentListener { //---- interface PropertyChangeListener ---- @@ -374,16 +426,19 @@ class FlatTitlePane @Override public void windowActivated( WindowEvent e ) { activeChanged( true ); + updateJBRHitTestSpotsAndTitleBarHeight(); } @Override public void windowDeactivated( WindowEvent e ) { activeChanged( false ); + updateJBRHitTestSpotsAndTitleBarHeight(); } @Override public void windowStateChanged( WindowEvent e ) { frameStateChanged(); + updateJBRHitTestSpotsAndTitleBarHeight(); } //---- interface MouseListener ---- @@ -393,6 +448,9 @@ class FlatTitlePane @Override public void mouseClicked( MouseEvent e ) { + if( hasJBRCustomDecoration() ) + return; // do nothing if running in JBR + if( e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton( e ) && window instanceof Frame && @@ -421,6 +479,9 @@ class FlatTitlePane @Override public void mouseDragged( MouseEvent e ) { + if( hasJBRCustomDecoration() ) + return; // do nothing if running in JBR + int xOnScreen = e.getXOnScreen(); int yOnScreen = e.getYOnScreen(); if( lastXOnScreen == xOnScreen && lastYOnScreen == yOnScreen ) @@ -460,5 +521,18 @@ class FlatTitlePane } @Override public void mouseMoved( MouseEvent e ) {} + + //---- interface ComponentListener ---- + + @Override + public void componentResized( ComponentEvent e ) { + EventQueue.invokeLater( () -> { + updateJBRHitTestSpotsAndTitleBarHeight(); + } ); + } + + @Override public void componentMoved( ComponentEvent e ) {} + @Override public void componentShown( ComponentEvent e ) {} + @Override public void componentHidden( ComponentEvent e ) {} } } 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 new file mode 100644 index 00000000..a19bd339 --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/JBRCustomDecorations.java @@ -0,0 +1,199 @@ +/* + * Copyright 2020 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.Component; +import java.awt.Container; +import java.awt.EventQueue; +import java.awt.Rectangle; +import java.awt.Window; +import java.awt.event.HierarchyEvent; +import java.awt.event.HierarchyListener; +import java.lang.reflect.Method; +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.UIManager; +import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.util.SystemInfo; + +/** + * Support for custom window decorations provided by JetBrains Runtime (based on OpenJDK). + * Requires that the application runs on Windows 10 in a JetBrains Runtime 11 or later. + *

+ * + * @author Karl Tauber + */ +public class JBRCustomDecorations +{ + private static boolean initialized; + private static Method Window_hasCustomDecoration; + private static Method Window_setHasCustomDecoration; + private static Method WWindowPeer_setCustomDecorationHitTestSpots; + private static Method WWindowPeer_setCustomDecorationTitleBarHeight; + private static Method AWTAccessor_getComponentAccessor; + private static Method AWTAccessor_ComponentAccessor_getPeer; + + public static boolean isSupported() { + initialize(); + return Window_setHasCustomDecoration != null; + } + + static void install( JRootPane rootPane ) { + boolean frameIsDefaultLookAndFeelDecorated = JFrame.isDefaultLookAndFeelDecorated(); + boolean dialogIsDefaultLookAndFeelDecorated = JDialog.isDefaultLookAndFeelDecorated(); + boolean lafSupportsWindowDecorations = UIManager.getLookAndFeel().getSupportsWindowDecorations(); + + // check whether decorations are enabled + if( !frameIsDefaultLookAndFeelDecorated && !dialogIsDefaultLookAndFeelDecorated ) + return; + + // do not enable JBR decorations if JFrame and JDialog will use LaF decorations + if( lafSupportsWindowDecorations && + frameIsDefaultLookAndFeelDecorated && + dialogIsDefaultLookAndFeelDecorated ) + return; + + if( !isSupported() ) + return; + + // use hierarchy listener to wait until the root pane is added to a window + HierarchyListener addListener = new HierarchyListener() { + @Override + public void hierarchyChanged( HierarchyEvent e ) { + if( (e.getChangeFlags() & HierarchyEvent.PARENT_CHANGED) == 0 ) + return; + + Container parent = e.getChangedParent(); + if( parent instanceof JFrame ) { + JFrame frame = (JFrame) parent; + + // do not enable JBR decorations if JFrame will use LaF decorations + if( lafSupportsWindowDecorations && frameIsDefaultLookAndFeelDecorated ) + 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 + rootPane.setWindowDecorationStyle( JRootPane.FRAME ); + + } else if( parent instanceof JDialog ) { + JDialog dialog = (JDialog)parent; + + // do not enable JBR decorations if JDialog will use LaF decorations + if( lafSupportsWindowDecorations && dialogIsDefaultLookAndFeelDecorated ) + 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 + rootPane.setWindowDecorationStyle( JRootPane.PLAIN_DIALOG ); + } + + // use invokeLater to remove listener to avoid that listener + // is removed while listener queue is processed + EventQueue.invokeLater( () -> { + rootPane.removeHierarchyListener( this ); + } ); + } + }; + rootPane.addHierarchyListener( addListener ); + } + + static boolean hasCustomDecoration( Window window ) { + if( !isSupported() ) + return false; + + try { + return (Boolean) Window_hasCustomDecoration.invoke( window ); + } catch( Exception ex ) { + Logger.getLogger( FlatLaf.class.getName() ).log( Level.SEVERE, null, ex ); + return false; + } + } + + static void setHasCustomDecoration( Window window ) { + if( !isSupported() ) + return; + + try { + Window_setHasCustomDecoration.invoke( window ); + } catch( Exception ex ) { + Logger.getLogger( FlatLaf.class.getName() ).log( Level.SEVERE, null, ex ); + } + } + + static void setHitTestSpotsAndTitleBarHeight( Window window, List hitTestSpots, int titleBarHeight ) { + 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 ); + } catch( Exception ex ) { + Logger.getLogger( FlatLaf.class.getName() ).log( Level.SEVERE, null, ex ); + } + } + + private static void initialize() { + if( initialized ) + return; + initialized = true; + + // requires JetBrains Runtime 11 and Windows 10 + if( !SystemInfo.IS_JETBRAINS_JVM_11_OR_LATER || !SystemInfo.IS_WINDOWS_10_OR_LATER ) + return; + + try { + Class awtAcessorClass = Class.forName( "sun.awt.AWTAccessor" ); + Class compAccessorClass = Class.forName( "sun.awt.AWTAccessor$ComponentAccessor" ); + AWTAccessor_getComponentAccessor = awtAcessorClass.getDeclaredMethod( "getComponentAccessor" ); + 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_setCustomDecorationTitleBarHeight.setAccessible( true ); + + Window_hasCustomDecoration = Window.class.getDeclaredMethod( "hasCustomDecoration" ); + Window_setHasCustomDecoration = Window.class.getDeclaredMethod( "setHasCustomDecoration" ); + Window_hasCustomDecoration.setAccessible( true ); + Window_setHasCustomDecoration.setAccessible( 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 0909fdb1..91aedb84 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 @@ -32,13 +32,16 @@ public class SystemInfo public static final boolean IS_LINUX; // OS versions + public static final boolean IS_WINDOWS_10_OR_LATER; public static final boolean IS_MAC_OS_10_11_EL_CAPITAN_OR_LATER; // Java versions public static final boolean IS_JAVA_9_OR_LATER; + public static final boolean IS_JAVA_11_OR_LATER; // Java VMs public static final boolean IS_JETBRAINS_JVM; + public static final boolean IS_JETBRAINS_JVM_11_OR_LATER; // UI toolkits public static final boolean IS_KDE; @@ -52,15 +55,18 @@ public class SystemInfo // OS versions long osVersion = scanVersion( System.getProperty( "os.version" ) ); + IS_WINDOWS_10_OR_LATER = (IS_WINDOWS && osVersion >= toVersion( 10, 0, 0, 0 )); IS_MAC_OS_10_11_EL_CAPITAN_OR_LATER = (IS_MAC && osVersion >= toVersion( 10, 11, 0, 0 )); // Java versions long javaVersion = scanVersion( System.getProperty( "java.version" ) ); IS_JAVA_9_OR_LATER = (javaVersion >= toVersion( 9, 0, 0, 0 )); + IS_JAVA_11_OR_LATER = (javaVersion >= toVersion( 11, 0, 0, 0 )); // Java VMs IS_JETBRAINS_JVM = System.getProperty( "java.vm.vendor", "Unknown" ) .toLowerCase( Locale.ENGLISH ).contains( "jetbrains" ); + IS_JETBRAINS_JVM_11_OR_LATER = IS_JETBRAINS_JVM && IS_JAVA_11_OR_LATER; // UI toolkits IS_KDE = (IS_LINUX && System.getenv( "KDE_FULL_SESSION" ) != null);