Window decorations: support native Windows 10 custom window decorations with JetBrains Runtime 11 (issues #47 and #82)

This commit is contained in:
Karl Tauber
2020-05-29 16:44:33 +02:00
parent 023d781daf
commit 436fc545c0
5 changed files with 313 additions and 3 deletions

View File

@@ -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.
* <p>
* To use custom window decorations in your application, enable them with
* (before creating any frames or dialogs):
* <pre>
* JFrame.setDefaultLookAndFeelDecorated( true );
* JDialog.setDefaultLookAndFeelDecorated( true );
* </pre>
* <p>
* Returns {@code true} on Windows, {@code false} otherwise.
* <p>
* Return also {@code false} if running on Windows 10 in
* <a href="https://confluence.jetbrains.com/display/JBR/JetBrains+Runtime">JetBrains Runtime 11 (or later)</a>
* (<a href="https://github.com/JetBrains/JetBrainsRuntime">source code on github</a>)
* 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;
}

View File

@@ -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

View File

@@ -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<Rectangle> 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<Rectangle> 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 ) {}
}
}

View File

@@ -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.
* <ul>
* <li><a href="https://confluence.jetbrains.com/display/JBR/JetBrains+Runtime">https://confluence.jetbrains.com/display/JBR/JetBrains+Runtime</a></li>
* <li><a href="https://github.com/JetBrains/JetBrainsRuntime">https://github.com/JetBrains/JetBrainsRuntime</a></li>
* </ul>
*
* @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<Rectangle> 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
}
}
}

View File

@@ -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);