Merge branch 'origin/custom-window-decorations' into master

# Conflicts:
#	flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java
This commit is contained in:
Karl Tauber
2020-06-29 15:59:38 +02:00
45 changed files with 3487 additions and 37 deletions

View File

@@ -183,6 +183,15 @@ public interface FlatClientProperties
*/
String PROGRESS_BAR_SQUARE = "JProgressBar.square";
/**
* Specifies whether the menu bar is embedded into the title pane if custom
* window decorations are enabled. Default is {@code true}.
* <p>
* <strong>Component</strong> {@link javax.swing.JRootPane}<br>
* <strong>Value type</strong> {@link java.lang.Boolean}
*/
String MENU_BAR_EMBEDDED = "JRootPane.menuBarEmbedded";
/**
* Specifies whether the decrease/increase arrow buttons of a scrollbar are shown.
* <p>

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;
@@ -85,6 +88,9 @@ public abstract class FlatLaf
private Consumer<UIDefaults> postInitialization;
private Boolean oldFrameWindowDecorated;
private Boolean oldDialogWindowDecorated;
public static boolean install( LookAndFeel newLookAndFeel ) {
try {
UIManager.setLookAndFeel( newLookAndFeel );
@@ -110,6 +116,45 @@ public abstract class FlatLaf
public abstract boolean isDark();
/**
* Checks whether the current look and feel is dark.
*/
public static boolean isLafDark() {
LookAndFeel lookAndFeel = UIManager.getLookAndFeel();
return lookAndFeel instanceof FlatLaf && ((FlatLaf)lookAndFeel).isDark();
}
/**
* Returns whether FlatLaf supports custom window decorations.
* This depends on the operating system and on the used Java runtime.
* <p>
* 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}.
* <pre>
* JFrame.setDefaultLookAndFeelDecorated( true );
* JDialog.setDefaultLookAndFeelDecorated( true );
* </pre>
* <p>
* Returns {@code true} on Windows 10, {@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_10_OR_LATER;
}
@Override
public boolean isNativeLookAndFeel() {
return false;
@@ -200,6 +245,16 @@ public abstract class FlatLaf
String.format( "a { color: #%06x; }", linkColor.getRGB() & 0xffffff ) );
}
};
// enable/disable window decorations, but only if system property is either
// "true" or "false"; in other cases it is not changed
Boolean useWindowDecorations = FlatSystemProperties.getBooleanStrict( FlatSystemProperties.USE_WINDOW_DECORATIONS, null );
if( useWindowDecorations != null ) {
oldFrameWindowDecorated = JFrame.isDefaultLookAndFeelDecorated();
oldDialogWindowDecorated = JDialog.isDefaultLookAndFeelDecorated();
JFrame.setDefaultLookAndFeelDecorated( useWindowDecorations );
JDialog.setDefaultLookAndFeelDecorated( useWindowDecorations );
}
}
@Override
@@ -232,6 +287,14 @@ public abstract class FlatLaf
new HTMLEditorKit().getStyleSheet().addRule( "a { color: blue; }" );
postInitialization = null;
// restore enable/disable window decorations
if( oldFrameWindowDecorated != null ) {
JFrame.setDefaultLookAndFeelDecorated( oldFrameWindowDecorated );
JDialog.setDefaultLookAndFeelDecorated( oldDialogWindowDecorated );
oldFrameWindowDecorated = null;
oldDialogWindowDecorated = null;
}
super.uninitialize();
}

View File

@@ -0,0 +1,118 @@
/*
* 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;
import javax.swing.JDialog;
import javax.swing.JFrame;
/**
* Defines/documents own system properties used in FlatLaf.
*
* @author Karl Tauber
*/
public interface FlatSystemProperties
{
/**
* Specifies a custom scale factor used to scale the UI.
* <p>
* If Java runtime scales (Java 9 or later), this scale factor is applied on top
* of the Java system scale factor. Java 8 does not scale and this scale factor
* replaces the user scale factor that FlatLaf computes based on the font.
* To replace the Java 9+ system scale factor, use system property "sun.java2d.uiScale",
* which has the same syntax as this one.
* <p>
* <strong>Allowed Values</strong> e.g. {@code 1.5}, {@code 1.5x}, {@code 150%} or {@code 144dpi} (96dpi is 100%)<br>
*/
String UI_SCALE = "flatlaf.uiScale";
/**
* Specifies whether Ubuntu font should be used on Ubuntu Linux.
* By default, if not running in a JetBrains Runtime, the Liberation Sans font
* is used because there are rendering issues (in Java) with Ubuntu fonts.
* <p>
* <strong>Allowed Values</strong> {@code false} and {@code true}<br>
* <strong>Default</strong> {@code false}
*/
String USE_UBUNTU_FONT = "flatlaf.useUbuntuFont";
/**
* Specifies whether custom look and feel window decorations should be used
* when creating {@code JFrame} or {@code JDialog}.
* <p>
* If this system property is set, FlatLaf invokes {@link JFrame#setDefaultLookAndFeelDecorated(boolean)}
* and {@link JDialog#setDefaultLookAndFeelDecorated(boolean)} on LaF initialization.
* <p>
* <strong>Allowed Values</strong> {@code false} and {@code true}<br>
* <strong>Default</strong> none
*/
String USE_WINDOW_DECORATIONS = "flatlaf.useWindowDecorations";
/**
* Specifies whether JetBrains Runtime custom window decorations should be used
* when creating {@code JFrame} or {@code JDialog}.
* Requires that the application runs in a
* <a href="https://confluence.jetbrains.com/display/JBR/JetBrains+Runtime">JetBrains Runtime</a>
* (based on OpenJDK).
* <p>
* Setting this to {@code true} forces using JetBrains Runtime custom window decorations
* even if they are not enabled by the application.
* <p>
* <strong>Allowed Values</strong> {@code false} and {@code true}<br>
* <strong>Default</strong> {@code true}
*/
String USE_JETBRAINS_CUSTOM_DECORATIONS = "flatlaf.useJetBrainsCustomDecorations";
/**
* Specifies whether menubar is embedded into custom window decorations.
* <p>
* <strong>Allowed Values</strong> {@code false} and {@code true}<br>
* <strong>Default</strong> {@code true}
*/
String MENUBAR_EMBEDDED = "flatlaf.menuBarEmbedded";
/**
* Specifies whether vertical text position is corrected when UI is scaled on HiDPI screens.
* <p>
* <strong>Allowed Values</strong> {@code false} and {@code true}<br>
* <strong>Default</strong> {@code true}
*/
String USE_TEXT_Y_CORRECTION = "flatlaf.useTextYCorrection";
/**
* Checks whether a system property is set and returns {@code true} if its value
* is {@code "true"} (case-insensitive), otherwise it returns {@code false}.
* If the system property is not set, {@code defaultValue} is returned.
*/
static boolean getBoolean( String key, boolean defaultValue ) {
String value = System.getProperty( key );
return (value != null) ? Boolean.parseBoolean( value ) : defaultValue;
}
/**
* Checks whether a system property is set and returns {@code Boolean.TRUE} if its value
* is {@code "true"} (case-insensitive) or returns {@code Boolean.FALSE} if its value
* is {@code "false"} (case-insensitive). Otherwise {@code defaultValue} is returned.
*/
static Boolean getBooleanStrict( String key, Boolean defaultValue ) {
String value = System.getProperty( key );
if( "true".equalsIgnoreCase( value ) )
return Boolean.TRUE;
if( "false".equalsIgnoreCase( value ) )
return Boolean.FALSE;
return defaultValue;
}
}

View File

@@ -510,6 +510,10 @@ public class IntelliJTheme
// Slider
uiKeyMapping.put( "Slider.trackWidth", "" ); // ignore (used in Material Theme UI Lite)
// TitlePane
uiKeyMapping.put( "TitlePane.infoForeground", "TitlePane.foreground" );
uiKeyMapping.put( "TitlePane.inactiveInfoForeground", "TitlePane.inactiveForeground" );
for( Map.Entry<String, String> e : uiKeyMapping.entrySet() )
uiKeyInverseMapping.put( e.getValue(), e.getKey() );

View File

@@ -78,7 +78,7 @@ class LinuxFontPolicy
// --> use Liberation Sans font
if( family.startsWith( "Ubuntu" ) &&
!SystemInfo.IS_JETBRAINS_JVM &&
!Boolean.parseBoolean( System.getProperty( "flatlaf.useUbuntuFont" ) ) )
!FlatSystemProperties.getBoolean( FlatSystemProperties.USE_UBUNTU_FONT, false ) )
family = "Liberation Sans";
// scale font size

View File

@@ -0,0 +1,76 @@
/*
* 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.icons;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics2D;
import javax.swing.UIManager;
import com.formdev.flatlaf.ui.FlatButtonUI;
import com.formdev.flatlaf.ui.FlatUIUtils;
import com.formdev.flatlaf.util.HiDPIUtils;
/**
* Base class for window icons.
*
* @uiDefault TitlePane.buttonSize Dimension
* @uiDefault TitlePane.buttonHoverBackground Color
* @uiDefault TitlePane.buttonPressedBackground Color
*
* @author Karl Tauber
*/
public abstract class FlatWindowAbstractIcon
extends FlatAbstractIcon
{
private final Color hoverBackground;
private final Color pressedBackground;
public FlatWindowAbstractIcon() {
this( UIManager.getDimension( "TitlePane.buttonSize" ),
UIManager.getColor( "TitlePane.buttonHoverBackground" ),
UIManager.getColor( "TitlePane.buttonPressedBackground" ) );
}
public FlatWindowAbstractIcon( Dimension size, Color hoverBackground, Color pressedBackground ) {
super( size.width, size.height, null );
this.hoverBackground = hoverBackground;
this.pressedBackground = pressedBackground;
}
@Override
protected void paintIcon( Component c, Graphics2D g ) {
paintBackground( c, g );
g.setColor( getForeground( c ) );
HiDPIUtils.paintAtScale1x( g, 0, 0, width, height, this::paintIconAt1x );
}
protected abstract void paintIconAt1x( Graphics2D g, int x, int y, int width, int height, double scaleFactor );
protected void paintBackground( Component c, Graphics2D g ) {
Color background = FlatButtonUI.buttonStateColor( c, null, null, null, hoverBackground, pressedBackground );
if( background != null ) {
g.setColor( FlatUIUtils.deriveColor( background, c.getBackground() ) );
g.fillRect( 0, 0, width, height );
}
}
protected Color getForeground( Component c ) {
return c.getForeground();
}
}

View File

@@ -0,0 +1,70 @@
/*
* 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.icons;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics2D;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import javax.swing.UIManager;
import com.formdev.flatlaf.ui.FlatButtonUI;
/**
* "close" icon for windows (frames and dialogs).
*
* @uiDefault TitlePane.closeHoverBackground Color
* @uiDefault TitlePane.closePressedBackground Color
* @uiDefault TitlePane.closeHoverForeground Color
* @uiDefault TitlePane.closePressedForeground Color
*
* @author Karl Tauber
*/
public class FlatWindowCloseIcon
extends FlatWindowAbstractIcon
{
private final Color hoverForeground = UIManager.getColor( "TitlePane.closeHoverForeground" );
private final Color pressedForeground = UIManager.getColor( "TitlePane.closePressedForeground" );
public FlatWindowCloseIcon() {
super( UIManager.getDimension( "TitlePane.buttonSize" ),
UIManager.getColor( "TitlePane.closeHoverBackground" ),
UIManager.getColor( "TitlePane.closePressedBackground" ) );
}
@Override
protected void paintIconAt1x( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) {
int iwh = (int) (10 * scaleFactor);
int ix = x + ((width - iwh) / 2);
int iy = y + ((height - iwh) / 2);
int ix2 = ix + iwh - 1;
int iy2 = iy + iwh - 1;
int thickness = (int) scaleFactor;
Path2D path = new Path2D.Float( Path2D.WIND_EVEN_ODD );
path.append( new Line2D.Float( ix, iy, ix2, iy2 ), false );
path.append( new Line2D.Float( ix, iy2, ix2, iy ), false );
g.setStroke( new BasicStroke( thickness ) );
g.draw( path );
}
@Override
protected Color getForeground( Component c ) {
return FlatButtonUI.buttonStateColor( c, c.getForeground(), null, null, hoverForeground, pressedForeground );
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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.icons;
import java.awt.Graphics2D;
/**
* "iconify" icon for windows (frames and dialogs).
*
* @author Karl Tauber
*/
public class FlatWindowIconifyIcon
extends FlatWindowAbstractIcon
{
public FlatWindowIconifyIcon() {
}
@Override
protected void paintIconAt1x( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) {
int iw = (int) (10 * scaleFactor);
int ih = (int) scaleFactor;
int ix = x + ((width - iw) / 2);
int iy = y + ((height - ih) / 2);
g.fillRect( ix, iy, iw, ih );
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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.icons;
import java.awt.Graphics2D;
import com.formdev.flatlaf.ui.FlatUIUtils;
/**
* "maximize" icon for windows (frames and dialogs).
*
* @author Karl Tauber
*/
public class FlatWindowMaximizeIcon
extends FlatWindowAbstractIcon
{
public FlatWindowMaximizeIcon() {
}
@Override
protected void paintIconAt1x( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) {
int iwh = (int) (10 * scaleFactor);
int ix = x + ((width - iwh) / 2);
int iy = y + ((height - iwh) / 2);
int thickness = (int) scaleFactor;
g.fill( FlatUIUtils.createRectangle( ix, iy, iwh, iwh, thickness ) );
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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.icons;
import java.awt.Graphics2D;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import com.formdev.flatlaf.ui.FlatUIUtils;
/**
* "restore" icon for windows (frames and dialogs).
*
* @author Karl Tauber
*/
public class FlatWindowRestoreIcon
extends FlatWindowAbstractIcon
{
public FlatWindowRestoreIcon() {
}
@Override
protected void paintIconAt1x( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) {
int iwh = (int) (10 * scaleFactor);
int ix = x + ((width - iwh) / 2);
int iy = y + ((height - iwh) / 2);
int thickness = (int) scaleFactor;
int rwh = (int) (8 * scaleFactor);
int ro2 = iwh - rwh;
Path2D r1 = FlatUIUtils.createRectangle( ix + ro2, iy, rwh, rwh, thickness );
Path2D r2 = FlatUIUtils.createRectangle( ix, iy + ro2, rwh, rwh, thickness );
Area area = new Area( r1 );
area.subtract( new Area( new Rectangle2D.Float( ix, iy + ro2, rwh, rwh ) ) );
g.fill( area );
g.fill( r2 );
}
}

View File

@@ -40,7 +40,7 @@ public class FlatMenuItemBorder
if( c.getParent() instanceof JMenuBar ) {
insets.top = scale( menuBarItemMargins.top );
insets.left = scale( menuBarItemMargins.left );
insets.bottom = scale( menuBarItemMargins.bottom + 1 );
insets.bottom = scale( menuBarItemMargins.bottom );
insets.right = scale( menuBarItemMargins.right );
return insets;
} else

View File

@@ -17,30 +17,84 @@
package com.formdev.flatlaf.ui;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.LayoutManager2;
import java.awt.Toolkit;
import java.beans.PropertyChangeEvent;
import java.util.function.Function;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLayeredPane;
import javax.swing.JMenuBar;
import javax.swing.JRootPane;
import javax.swing.LookAndFeel;
import javax.swing.UIManager;
import javax.swing.plaf.BorderUIResource;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.UIResource;
import javax.swing.plaf.basic.BasicRootPaneUI;
import com.formdev.flatlaf.FlatClientProperties;
import com.formdev.flatlaf.FlatLaf;
import com.formdev.flatlaf.util.HiDPIUtils;
import com.formdev.flatlaf.util.SystemInfo;
/**
* Provides the Flat LaF UI delegate for {@link javax.swing.JRootPane}.
*
* <!-- FlatRootPaneUI -->
*
* @uiDefault RootPane.border Border
*
* <!-- FlatWindowResizer -->
*
* @uiDefault RootPane.borderDragThickness int
* @uiDefault RootPane.cornerDragWidth int
* @uiDefault RootPane.honorMinimumSizeOnResize boolean
*
* @author Karl Tauber
*/
public class FlatRootPaneUI
extends BasicRootPaneUI
{
private static ComponentUI instance;
// check this field before using class JBRCustomDecorations to avoid unnecessary loading of that class
static final boolean canUseJBRCustomDecorations
= SystemInfo.IS_JETBRAINS_JVM_11_OR_LATER && SystemInfo.IS_WINDOWS_10_OR_LATER;
private JRootPane rootPane;
private FlatTitlePane titlePane;
private LayoutManager oldLayout;
private FlatWindowResizer windowResizer;
public static ComponentUI createUI( JComponent c ) {
if( instance == null )
instance = new FlatRootPaneUI();
return instance;
return new FlatRootPaneUI();
}
@Override
public void installUI( JComponent c ) {
super.installUI( c );
rootPane = (JRootPane) c;
if( rootPane.getWindowDecorationStyle() != JRootPane.NONE )
installClientDecorations();
if( canUseJBRCustomDecorations )
JBRCustomDecorations.install( rootPane );
}
@Override
public void uninstallUI( JComponent c ) {
super.uninstallUI( c );
uninstallClientDecorations();
rootPane = null;
}
@Override
@@ -58,5 +112,235 @@ public class FlatRootPaneUI
if( background == null || background instanceof UIResource )
parent.setBackground( UIManager.getColor( "control" ) );
}
// enable dark window appearance on macOS when running in JetBrains Runtime
if( SystemInfo.IS_JETBRAINS_JVM && SystemInfo.IS_MAC_OS_10_14_MOJAVE ) {
LookAndFeel laf = UIManager.getLookAndFeel();
boolean isDark = laf instanceof FlatLaf && ((FlatLaf)laf).isDark();
c.putClientProperty( "jetbrains.awt.windowDarkAppearance", isDark );
}
}
protected void installClientDecorations() {
boolean isJBRSupported = canUseJBRCustomDecorations && JBRCustomDecorations.isSupported();
// install border
if( rootPane.getWindowDecorationStyle() != JRootPane.NONE && !isJBRSupported )
LookAndFeel.installBorder( rootPane, "RootPane.border" );
else
LookAndFeel.uninstallBorder( rootPane );
// install title pane
setTitlePane( createTitlePane() );
// install layout
oldLayout = rootPane.getLayout();
rootPane.setLayout( createRootLayout() );
// install window resizer
if( !isJBRSupported )
windowResizer = createWindowResizer();
}
protected void uninstallClientDecorations() {
LookAndFeel.uninstallBorder( rootPane );
setTitlePane( null );
if( windowResizer != null ) {
windowResizer.uninstall();
windowResizer = null;
}
if( oldLayout != null ) {
rootPane.setLayout( oldLayout );
oldLayout = null;
}
if( rootPane.getWindowDecorationStyle() == JRootPane.NONE ) {
rootPane.revalidate();
rootPane.repaint();
}
}
protected FlatRootLayout createRootLayout() {
return new FlatRootLayout();
}
protected FlatWindowResizer createWindowResizer() {
return new FlatWindowResizer( rootPane );
}
protected FlatTitlePane createTitlePane() {
return new FlatTitlePane( rootPane );
}
// layer title pane under frame content layer to allow placing menu bar over title pane
protected final static Integer TITLE_PANE_LAYER = JLayeredPane.FRAME_CONTENT_LAYER - 1;
protected void setTitlePane( FlatTitlePane newTitlePane ) {
JLayeredPane layeredPane = rootPane.getLayeredPane();
if( titlePane != null )
layeredPane.remove( titlePane );
if( newTitlePane != null )
layeredPane.add( newTitlePane, TITLE_PANE_LAYER );
titlePane = newTitlePane;
}
@Override
public void propertyChange( PropertyChangeEvent e ) {
super.propertyChange( e );
switch( e.getPropertyName() ) {
case "windowDecorationStyle":
uninstallClientDecorations();
if( rootPane.getWindowDecorationStyle() != JRootPane.NONE )
installClientDecorations();
break;
case FlatClientProperties.MENU_BAR_EMBEDDED:
if( titlePane != null ) {
titlePane.menuBarChanged();
rootPane.revalidate();
rootPane.repaint();
}
break;
}
}
//---- class FlatRootLayout -----------------------------------------------
protected class FlatRootLayout
implements LayoutManager2
{
@Override public void addLayoutComponent( String name, Component comp ) {}
@Override public void addLayoutComponent( Component comp, Object constraints ) {}
@Override public void removeLayoutComponent( Component comp ) {}
@Override
public Dimension preferredLayoutSize( Container parent ) {
return computeLayoutSize( parent, c -> c.getPreferredSize() );
}
@Override
public Dimension minimumLayoutSize( Container parent ) {
return computeLayoutSize( parent, c -> c.getMinimumSize() );
}
@Override
public Dimension maximumLayoutSize( Container parent ) {
return new Dimension( Integer.MAX_VALUE, Integer.MAX_VALUE );
}
private Dimension computeLayoutSize( Container parent, Function<Component, Dimension> getSizeFunc ) {
JRootPane rootPane = (JRootPane) parent;
Dimension titlePaneSize = (titlePane != null)
? getSizeFunc.apply( titlePane )
: new Dimension();
Dimension contentSize = (rootPane.getContentPane() != null)
? getSizeFunc.apply( rootPane.getContentPane() )
: rootPane.getSize();
int width = Math.max( titlePaneSize.width, contentSize.width );
int height = titlePaneSize.height + contentSize.height;
if( titlePane == null || !titlePane.isMenuBarEmbedded() ) {
Dimension menuBarSize = (rootPane.getJMenuBar() != null)
? getSizeFunc.apply( rootPane.getJMenuBar() )
: new Dimension();
width = Math.max( width, menuBarSize.width );
height += menuBarSize.height;
}
Insets insets = rootPane.getInsets();
return new Dimension(
width + insets.left + insets.right,
height + insets.top + insets.bottom );
}
@Override
public void layoutContainer( Container parent ) {
JRootPane rootPane = (JRootPane) parent;
Insets insets = rootPane.getInsets();
int x = insets.left;
int y = insets.top;
int width = rootPane.getWidth() - insets.left - insets.right;
int height = rootPane.getHeight() - insets.top - insets.bottom;
if( rootPane.getLayeredPane() != null )
rootPane.getLayeredPane().setBounds( x, y, width, height );
if( rootPane.getGlassPane() != null )
rootPane.getGlassPane().setBounds( x, y, width, height );
int nextY = 0;
if( titlePane != null ) {
Dimension prefSize = titlePane.getPreferredSize();
titlePane.setBounds( 0, 0, width, prefSize.height );
nextY += prefSize.height;
}
JMenuBar menuBar = rootPane.getJMenuBar();
if( menuBar != null ) {
if( titlePane != null && titlePane.isMenuBarEmbedded() ) {
titlePane.validate();
menuBar.setBounds( titlePane.getMenuBarBounds() );
} else {
Dimension prefSize = menuBar.getPreferredSize();
menuBar.setBounds( 0, nextY, width, prefSize.height );
nextY += prefSize.height;
}
}
Container contentPane = rootPane.getContentPane();
if( contentPane != null )
contentPane.setBounds( 0, nextY, width, Math.max( height - nextY, 0 ) );
}
@Override
public void invalidateLayout( Container parent ) {
if( titlePane != null )
titlePane.menuBarChanged();
}
@Override
public float getLayoutAlignmentX( Container target ) {
return 0;
}
@Override
public float getLayoutAlignmentY( Container target ) {
return 0;
}
}
//---- class FlatWindowBorder ---------------------------------------------
public static class FlatWindowBorder
extends BorderUIResource.EmptyBorderUIResource
{
public FlatWindowBorder() {
super( 1, 1, 1, 1 );
}
@Override
public void paintBorder( Component c, Graphics g, int x, int y, int width, int height ) {
Object borderColorObj = Toolkit.getDefaultToolkit().getDesktopProperty(
"win.frame.activeBorderColor" );
Color borderColor = (borderColorObj instanceof Color)
? (Color) borderColorObj
: UIManager.getColor( "windowBorder" );
g.setColor( borderColor );
HiDPIUtils.paintAtScale1x( (Graphics2D) g, x, y, width, height, this::paintImpl );
}
private void paintImpl( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) {
g.drawRect( x, y, width - 1, height - 1 );
}
}
}

View File

@@ -0,0 +1,727 @@
/*
* 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.BorderLayout;
import java.awt.Color;
import java.awt.Component;
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;
import java.awt.event.WindowAdapter;
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 java.util.Objects;
import javax.accessibility.AccessibleContext;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JMenuBar;
import javax.swing.JPanel;
import javax.swing.JRootPane;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
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.util.ScaledImageIcon;
import com.formdev.flatlaf.util.SystemInfo;
import com.formdev.flatlaf.util.UIScale;
/**
* Provides the Flat LaF title bar.
*
* @uiDefault TitlePane.background Color
* @uiDefault TitlePane.inactiveBackground Color
* @uiDefault TitlePane.foreground Color
* @uiDefault TitlePane.inactiveForeground Color
* @uiDefault TitlePane.embeddedForeground Color
* @uiDefault TitlePane.iconSize Dimension
* @uiDefault TitlePane.iconMargins Insets
* @uiDefault TitlePane.titleMargins Insets
* @uiDefault TitlePane.menuBarMargins Insets
* @uiDefault TitlePane.menuBarEmbedded boolean
* @uiDefault TitlePane.buttonMaximizedHeight int
* @uiDefault TitlePane.closeIcon Icon
* @uiDefault TitlePane.iconifyIcon Icon
* @uiDefault TitlePane.maximizeIcon Icon
* @uiDefault TitlePane.restoreIcon Icon
*
* @author Karl Tauber
*/
public class FlatTitlePane
extends JComponent
{
private final Color activeBackground = UIManager.getColor( "TitlePane.background" );
private final Color inactiveBackground = UIManager.getColor( "TitlePane.inactiveBackground" );
private final Color activeForeground = UIManager.getColor( "TitlePane.foreground" );
private final Color inactiveForeground = UIManager.getColor( "TitlePane.inactiveForeground" );
private final Color embeddedForeground = UIManager.getColor( "TitlePane.embeddedForeground" );
private final Insets menuBarMargins = UIManager.getInsets( "TitlePane.menuBarMargins" );
private final Dimension iconSize = UIManager.getDimension( "TitlePane.iconSize" );
private final int buttonMaximizedHeight = UIManager.getInt( "TitlePane.buttonMaximizedHeight" );
private final JRootPane rootPane;
private JPanel leftPanel;
private JLabel iconLabel;
private JComponent menuBarPlaceholder;
private JLabel titleLabel;
private JPanel buttonPanel;
private JButton iconifyButton;
private JButton maximizeButton;
private JButton restoreButton;
private JButton closeButton;
private final Handler handler;
private Window window;
public FlatTitlePane( JRootPane rootPane ) {
this.rootPane = rootPane;
handler = createHandler();
setBorder( createTitlePaneBorder() );
addSubComponents();
activeChanged( true );
addMouseListener( handler );
addMouseMotionListener( handler );
// necessary for closing window with double-click on icon
iconLabel.addMouseListener( handler );
}
protected FlatTitlePaneBorder createTitlePaneBorder() {
return new FlatTitlePaneBorder();
}
protected Handler createHandler() {
return new Handler();
}
protected void addSubComponents() {
leftPanel = new JPanel();
iconLabel = new JLabel();
titleLabel = new JLabel();
iconLabel.setBorder( new FlatEmptyBorder( UIManager.getInsets( "TitlePane.iconMargins" ) ) );
titleLabel.setBorder( new FlatEmptyBorder( UIManager.getInsets( "TitlePane.titleMargins" ) ) );
leftPanel.setLayout( new BoxLayout( leftPanel, BoxLayout.LINE_AXIS ) );
leftPanel.setOpaque( false );
leftPanel.add( iconLabel );
menuBarPlaceholder = new JComponent() {
@Override
public Dimension getPreferredSize() {
JMenuBar menuBar = rootPane.getJMenuBar();
return (menuBar != null && isMenuBarEmbedded())
? FlatUIUtils.addInsets( menuBar.getPreferredSize(), UIScale.scale( menuBarMargins ) )
: new Dimension();
}
};
leftPanel.add( menuBarPlaceholder );
createButtons();
setLayout( new BorderLayout() );
add( leftPanel, BorderLayout.LINE_START );
add( titleLabel, BorderLayout.CENTER );
add( buttonPanel, BorderLayout.LINE_END );
}
protected void createButtons() {
iconifyButton = createButton( "TitlePane.iconifyIcon", "Iconify", e -> iconify() );
maximizeButton = createButton( "TitlePane.maximizeIcon", "Maximize", e -> maximize() );
restoreButton = createButton( "TitlePane.restoreIcon", "Restore", e -> restore() );
closeButton = createButton( "TitlePane.closeIcon", "Close", e -> close() );
buttonPanel = new JPanel() {
@Override
public Dimension getPreferredSize() {
Dimension size = super.getPreferredSize();
if( buttonMaximizedHeight > 0 &&
window instanceof Frame &&
(((Frame)window).getExtendedState() & Frame.MAXIMIZED_BOTH) != 0 )
{
// make title pane height smaller when frame is maximized
size = new Dimension( size.width, Math.min( size.height, UIScale.scale( buttonMaximizedHeight ) ) );
}
return size;
}
};
buttonPanel.setOpaque( false );
buttonPanel.setLayout( new BoxLayout( buttonPanel, BoxLayout.LINE_AXIS ) );
if( rootPane.getWindowDecorationStyle() == JRootPane.FRAME ) {
// JRootPane.FRAME works only for frames (and not for dialogs)
// but at this time the owner window type is unknown (not yet added)
// so we add the iconify/maximize/restore buttons and they are hidden
// later in frameStateChanged(), which is invoked from addNotify()
restoreButton.setVisible( false );
buttonPanel.add( iconifyButton );
buttonPanel.add( maximizeButton );
buttonPanel.add( restoreButton );
}
buttonPanel.add( closeButton );
}
protected JButton createButton( String iconKey, String accessibleName, ActionListener action ) {
JButton button = new JButton( UIManager.getIcon( iconKey ) );
button.setFocusable( false );
button.setContentAreaFilled( false );
button.setBorder( BorderFactory.createEmptyBorder() );
button.putClientProperty( AccessibleContext.ACCESSIBLE_NAME_PROPERTY, accessibleName );
button.addActionListener( action );
return button;
}
protected void activeChanged( boolean active ) {
Color background = FlatUIUtils.nonUIResource( active ? activeBackground : inactiveBackground );
Color foreground = FlatUIUtils.nonUIResource( active
? (rootPane.getJMenuBar() != null && isMenuBarEmbedded() ? embeddedForeground : activeForeground)
: inactiveForeground );
setBackground( background );
titleLabel.setForeground( foreground );
// this is necessary because hover/pressed colors are derived from background color
iconifyButton.setBackground( background );
maximizeButton.setBackground( background );
restoreButton.setBackground( background );
closeButton.setBackground( background );
}
protected void frameStateChanged() {
if( window == null || rootPane.getWindowDecorationStyle() != JRootPane.FRAME )
return;
if( window instanceof Frame ) {
Frame frame = (Frame) window;
boolean resizable = frame.isResizable();
boolean maximized = ((frame.getExtendedState() & Frame.MAXIMIZED_BOTH) != 0);
iconifyButton.setVisible( true );
maximizeButton.setVisible( resizable && !maximized );
restoreButton.setVisible( resizable && maximized );
} else {
// hide buttons because they are only supported in frames
iconifyButton.setVisible( false );
maximizeButton.setVisible( false );
restoreButton.setVisible( false );
revalidate();
repaint();
}
}
protected void updateIcon() {
// get window images
List<Image> images = window.getIconImages();
if( images.isEmpty() ) {
// search in owners
for( Window owner = window.getOwner(); owner != null; owner = owner.getOwner() ) {
images = owner.getIconImages();
if( !images.isEmpty() )
break;
}
}
boolean hasIcon = true;
// set icon
if( !images.isEmpty() )
iconLabel.setIcon( FlatTitlePaneIcon.create( images, iconSize ) );
else {
// no icon set on window --> use default icon
Icon defaultIcon = UIManager.getIcon( "InternalFrame.icon" );
if( defaultIcon != null ) {
if( defaultIcon instanceof ImageIcon )
defaultIcon = new ScaledImageIcon( (ImageIcon) defaultIcon, iconSize.width, iconSize.height );
iconLabel.setIcon( defaultIcon );
} else
hasIcon = false;
}
// show/hide icon
iconLabel.setVisible( hasIcon );
updateJBRHitTestSpotsAndTitleBarHeightLater();
}
@Override
public void addNotify() {
super.addNotify();
uninstallWindowListeners();
window = SwingUtilities.getWindowAncestor( this );
if( window != null ) {
frameStateChanged();
activeChanged( window.isActive() );
updateIcon();
titleLabel.setText( getWindowTitle() );
installWindowListeners();
}
updateJBRHitTestSpotsAndTitleBarHeightLater();
}
@Override
public void removeNotify() {
super.removeNotify();
uninstallWindowListeners();
window = null;
}
protected String getWindowTitle() {
if( window instanceof Frame )
return ((Frame)window).getTitle();
if( window instanceof Dialog )
return ((Dialog)window).getTitle();
return null;
}
protected void installWindowListeners() {
if( window == null )
return;
window.addPropertyChangeListener( handler );
window.addWindowListener( handler );
window.addWindowStateListener( handler );
window.addComponentListener( handler );
}
protected void uninstallWindowListeners() {
if( window == null )
return;
window.removePropertyChangeListener( handler );
window.removeWindowListener( handler );
window.removeWindowStateListener( handler );
window.removeComponentListener( handler );
}
protected boolean isMenuBarEmbedded() {
// not storing value of "TitlePane.menuBarEmbedded" in class to allow changing at runtime
return UIManager.getBoolean( "TitlePane.menuBarEmbedded" ) &&
FlatClientProperties.clientPropertyBoolean( rootPane, FlatClientProperties.MENU_BAR_EMBEDDED, true ) &&
FlatSystemProperties.getBoolean( FlatSystemProperties.MENUBAR_EMBEDDED, true );
}
protected Rectangle getMenuBarBounds() {
Insets insets = rootPane.getInsets();
Rectangle bounds = new Rectangle(
SwingUtilities.convertPoint( menuBarPlaceholder, -insets.left, -insets.top, rootPane ),
menuBarPlaceholder.getSize() );
// add menu bar bottom border insets to bounds so that menu bar overlaps
// title pane border (menu bar border is painted over title pane border)
Insets borderInsets = getBorder().getBorderInsets( this );
bounds.height += borderInsets.bottom;
return FlatUIUtils.subtractInsets( bounds, UIScale.scale( getMenuBarMargins() ) );
}
protected void menuBarChanged() {
menuBarPlaceholder.invalidate();
// update title foreground color
EventQueue.invokeLater( () -> {
activeChanged( window == null || window.isActive() );
} );
}
protected Insets getMenuBarMargins() {
return getComponentOrientation().isLeftToRight()
? menuBarMargins
: new Insets( menuBarMargins.top, menuBarMargins.right, menuBarMargins.bottom, menuBarMargins.left );
}
@Override
protected void paintComponent( Graphics g ) {
g.setColor( getBackground() );
g.fillRect( 0, 0, getWidth(), getHeight() );
}
/**
* Iconifies the window.
*/
protected void iconify() {
if( window instanceof Frame ) {
Frame frame = (Frame) window;
frame.setExtendedState( frame.getExtendedState() | Frame.ICONIFIED );
}
}
/**
* Maximizes the window.
*/
protected void maximize() {
if( !(window instanceof Frame) )
return;
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( !hasJBRCustomDecoration() &&
(frame.getMaximizedBounds() == null ||
Objects.equals( frame.getMaximizedBounds(), rootPane.getClientProperty( "_flatlaf.maximizedBounds" ) )) )
{
GraphicsConfiguration gc = window.getGraphicsConfiguration();
// Screen bounds, which may be smaller than physical size on Java 9+.
// E.g. if running a 3840x2160 screen at 200%, screenBounds.size is 1920x1080.
// In Java 9+, each screen can have its own scale factor.
//
// On Java 8, which does not scale, screenBounds.size of the primary screen
// is identical to its physical size. But when the primary screen is scaled,
// then screenBounds.size of secondary screens is scaled with the scale factor
// of the primary screen.
// E.g. primary 3840x2160 screen at 150%, secondary 1920x1080 screen at 100%,
// then screenBounds.size is 3840x2160 on primary and 2880x1560 on secondary.
Rectangle screenBounds = gc.getBounds();
int maximizedX = screenBounds.x;
int maximizedY = screenBounds.y;
int maximizedWidth = screenBounds.width;
int maximizedHeight = screenBounds.height;
if( !SystemInfo.IS_JAVA_15_OR_LATER ) {
// on Java 8 to 14, maximized x,y are 0,0 based on all screens in a multi-screen environment
maximizedX = 0;
maximizedY = 0;
// scale maximized screen size to get physical screen size for Java 9 to 14
AffineTransform defaultTransform = gc.getDefaultTransform();
maximizedWidth = (int) (maximizedWidth * defaultTransform.getScaleX());
maximizedHeight = (int) (maximizedHeight * defaultTransform.getScaleY());
}
// screen insets are in physical size, except for Java 15+
// (see https://bugs.openjdk.java.net/browse/JDK-8243925)
// and except for Java 8 on secondary screens where primary screen is scaled
Insets screenInsets = window.getToolkit().getScreenInsets( gc );
// maximized bounds are required in physical size, except for Java 15+
// (see https://bugs.openjdk.java.net/browse/JDK-8231564 and
// https://bugs.openjdk.java.net/browse/JDK-8176359)
// and except for Java 8 on secondary screens where primary screen is scaled
Rectangle maximizedBounds = new Rectangle(
maximizedX + screenInsets.left,
maximizedY + screenInsets.top,
maximizedWidth - screenInsets.left - screenInsets.right,
maximizedHeight - screenInsets.top - screenInsets.bottom );
// change maximized bounds
frame.setMaximizedBounds( maximizedBounds );
// remember maximized bounds in client property to be able to detect
// whether maximized bounds are modified from the application
rootPane.putClientProperty( "_flatlaf.maximizedBounds", maximizedBounds );
}
// maximize window
frame.setExtendedState( frame.getExtendedState() | Frame.MAXIMIZED_BOTH );
}
/**
* Restores the window size.
*/
protected void restore() {
if( window instanceof Frame ) {
Frame frame = (Frame) window;
int state = frame.getExtendedState();
frame.setExtendedState( ((state & Frame.ICONIFIED) != 0)
? (state & ~Frame.ICONIFIED)
: (state & ~Frame.MAXIMIZED_BOTH) );
}
}
/**
* Closes the window.
*/
protected void close() {
if( window != null )
window.dispatchEvent( new WindowEvent( window, WindowEvent.WINDOW_CLOSING ) );
}
protected boolean hasJBRCustomDecoration() {
return FlatRootPaneUI.canUseJBRCustomDecorations &&
window != null &&
JBRCustomDecorations.hasCustomDecoration( window );
}
protected void updateJBRHitTestSpotsAndTitleBarHeightLater() {
EventQueue.invokeLater( () -> {
updateJBRHitTestSpotsAndTitleBarHeight();
} );
}
protected void updateJBRHitTestSpotsAndTitleBarHeight() {
if( !isDisplayable() )
return;
if( !hasJBRCustomDecoration() )
return;
List<Rectangle> 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 );
}
protected void addJBRHitTestSpot( JComponent c, boolean subtractMenuBarMargins, 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 );
if( subtractMenuBarMargins )
r = FlatUIUtils.subtractInsets( r, UIScale.scale( getMenuBarMargins() ) );
// slightly increase rectangle so that component receives mouseExit events
r.grow( 2, 2 );
hitTestSpots.add( r );
}
//---- class TitlePaneBorder ----------------------------------------------
protected class FlatTitlePaneBorder
extends AbstractBorder
{
@Override
public Insets getBorderInsets( Component c, Insets insets ) {
super.getBorderInsets( c, insets );
// if menu bar is embedded, add bottom insets of menu bar border
Border menuBarBorder = getMenuBarBorder();
if( menuBarBorder != null ) {
Insets menuBarInsets = menuBarBorder.getBorderInsets( c );
insets.bottom += menuBarInsets.bottom;
}
if( hasJBRCustomDecoration() )
insets = FlatUIUtils.addInsets( insets, JBRWindowTopBorder.getInstance().getBorderInsets() );
return insets;
}
@Override
public void paintBorder( Component c, Graphics g, int x, int y, int width, int height ) {
// if menu bar is embedded, paint menu bar border
Border menuBarBorder = getMenuBarBorder();
if( menuBarBorder != null )
menuBarBorder.paintBorder( c, g, x, y, width, height );
if( hasJBRCustomDecoration() )
JBRWindowTopBorder.getInstance().paintBorder( c, g, x, y, width, height );
}
protected Border getMenuBarBorder() {
JMenuBar menuBar = rootPane.getJMenuBar();
return (menuBar != null && isMenuBarEmbedded()) ? menuBar.getBorder() : null;
}
}
//---- class Handler ------------------------------------------------------
protected class Handler
extends WindowAdapter
implements PropertyChangeListener, MouseListener, MouseMotionListener, ComponentListener
{
//---- interface PropertyChangeListener ----
@Override
public void propertyChange( PropertyChangeEvent e ) {
switch( e.getPropertyName() ) {
case "title":
titleLabel.setText( getWindowTitle() );
break;
case "resizable":
if( window instanceof Frame )
frameStateChanged();
break;
case "iconImage":
updateIcon();
break;
case "componentOrientation":
updateJBRHitTestSpotsAndTitleBarHeightLater();
break;
}
}
//---- interface WindowListener ----
@Override
public void windowActivated( WindowEvent e ) {
activeChanged( true );
updateJBRHitTestSpotsAndTitleBarHeight();
if( hasJBRCustomDecoration() )
JBRWindowTopBorder.getInstance().repaintBorder( FlatTitlePane.this );
}
@Override
public void windowDeactivated( WindowEvent e ) {
activeChanged( false );
updateJBRHitTestSpotsAndTitleBarHeight();
if( hasJBRCustomDecoration() )
JBRWindowTopBorder.getInstance().repaintBorder( FlatTitlePane.this );
}
@Override
public void windowStateChanged( WindowEvent e ) {
frameStateChanged();
updateJBRHitTestSpotsAndTitleBarHeight();
}
//---- interface MouseListener ----
private int lastXOnScreen;
private int lastYOnScreen;
@Override
public void mouseClicked( MouseEvent e ) {
if( e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton( e ) ) {
if( e.getSource() == iconLabel ) {
// double-click on icon closes window
close();
} else if( !hasJBRCustomDecoration() &&
window instanceof Frame &&
((Frame)window).isResizable() )
{
// maximize/restore on double-click
Frame frame = (Frame) window;
if( (frame.getExtendedState() & Frame.MAXIMIZED_BOTH) != 0 )
restore();
else
maximize();
}
}
}
@Override
public void mousePressed( MouseEvent e ) {
lastXOnScreen = e.getXOnScreen();
lastYOnScreen = e.getYOnScreen();
}
@Override public void mouseReleased( MouseEvent e ) {}
@Override public void mouseEntered( MouseEvent e ) {}
@Override public void mouseExited( MouseEvent e ) {}
//---- interface MouseMotionListener ----
@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 )
return;
// restore window if it is maximized
if( window instanceof Frame ) {
Frame frame = (Frame) window;
int state = frame.getExtendedState();
if( (state & Frame.MAXIMIZED_BOTH) != 0 ) {
int maximizedX = window.getX();
int maximizedY = window.getY();
// restore window size, which also moves window to pre-maximized location
frame.setExtendedState( state & ~Frame.MAXIMIZED_BOTH );
int restoredWidth = window.getWidth();
int newX = maximizedX;
JComponent rightComp = getComponentOrientation().isLeftToRight() ? buttonPanel : leftPanel;
if( xOnScreen >= maximizedX + restoredWidth - rightComp.getWidth() - 10 )
newX = xOnScreen + rightComp.getWidth() + 10 - restoredWidth;
// move window near mouse
window.setLocation( newX, maximizedY );
return;
}
}
// compute new window location
int newX = window.getX() + (xOnScreen - lastXOnScreen);
int newY = window.getY() + (yOnScreen - lastYOnScreen);
// move window
window.setLocation( newX, newY );
lastXOnScreen = xOnScreen;
lastYOnScreen = yOnScreen;
}
@Override public void mouseMoved( MouseEvent e ) {}
//---- interface ComponentListener ----
@Override
public void componentResized( ComponentEvent e ) {
updateJBRHitTestSpotsAndTitleBarHeightLater();
}
@Override public void componentMoved( ComponentEvent e ) {}
@Override public void componentShown( ComponentEvent e ) {}
@Override public void componentHidden( ComponentEvent e ) {}
}
}

View File

@@ -0,0 +1,70 @@
/*
* 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.Dimension;
import java.awt.Image;
import java.util.ArrayList;
import java.util.List;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import com.formdev.flatlaf.util.MultiResolutionImageSupport;
import com.formdev.flatlaf.util.ScaledImageIcon;
/**
* @author Karl Tauber
*/
public class FlatTitlePaneIcon
extends ScaledImageIcon
{
public static Icon create( List<Image> images, Dimension size ) {
// collect all images including multi-resolution variants
List<Image> allImages = new ArrayList<>();
for( Image image : images ) {
if( MultiResolutionImageSupport.isMultiResolutionImage( image ) )
allImages.addAll( MultiResolutionImageSupport.getResolutionVariants( image ) );
else
allImages.add( image );
}
// sort images by size
allImages.sort( (image1, image2) -> {
return image1.getWidth( null ) - image2.getWidth( null );
} );
// create icon
return new FlatTitlePaneIcon( allImages, size );
}
private final List<Image> images;
private FlatTitlePaneIcon( List<Image> images, Dimension size ) {
super( new ImageIcon( images.get( 0 ) ), size.width, size.height );
this.images = images;
}
@Override
protected Image getResolutionVariant( int destImageWidth, int destImageHeight ) {
for( Image image : images ) {
if( destImageWidth <= image.getWidth( null ) &&
destImageHeight <= image.getHeight( null ) )
return image;
}
return images.get( images.size() - 1 );
}
}

View File

@@ -0,0 +1,325 @@
/*
* 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 static java.awt.Cursor.*;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dialog;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Window;
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;
import java.awt.event.WindowEvent;
import java.awt.event.WindowStateListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.JComponent;
import javax.swing.JLayeredPane;
import javax.swing.JRootPane;
import javax.swing.UIManager;
import com.formdev.flatlaf.util.UIScale;
/**
* Resizes frames and dialogs.
*
* @author Karl Tauber
*/
public class FlatWindowResizer
extends JComponent
implements PropertyChangeListener, WindowStateListener, ComponentListener
{
private final static Integer WINDOW_RESIZER_LAYER = JLayeredPane.DRAG_LAYER + 1;
private final JRootPane rootPane;
private final int borderDragThickness = FlatUIUtils.getUIInt( "RootPane.borderDragThickness", 5 );
private final int cornerDragWidth = FlatUIUtils.getUIInt( "RootPane.cornerDragWidth", 16 );
private final boolean honorMinimumSizeOnResize = UIManager.getBoolean( "RootPane.honorMinimumSizeOnResize" );
private Window window;
public FlatWindowResizer( JRootPane rootPane ) {
this.rootPane = rootPane;
setLayout( new BorderLayout() );
add( createDragBorderComponent( NW_RESIZE_CURSOR, N_RESIZE_CURSOR, NE_RESIZE_CURSOR ), BorderLayout.NORTH );
add( createDragBorderComponent( SW_RESIZE_CURSOR, S_RESIZE_CURSOR, SE_RESIZE_CURSOR ), BorderLayout.SOUTH );
add( createDragBorderComponent( NW_RESIZE_CURSOR, W_RESIZE_CURSOR, SW_RESIZE_CURSOR ), BorderLayout.WEST );
add( createDragBorderComponent( NE_RESIZE_CURSOR, E_RESIZE_CURSOR, SE_RESIZE_CURSOR ), BorderLayout.EAST );
rootPane.addComponentListener( this );
rootPane.getLayeredPane().add( this, WINDOW_RESIZER_LAYER );
if( rootPane.isDisplayable() )
setBounds( 0, 0, rootPane.getWidth(), rootPane.getHeight() );
}
protected DragBorderComponent createDragBorderComponent( int leadingResizeDir, int centerResizeDir, int trailingResizeDir ) {
return new DragBorderComponent( leadingResizeDir, centerResizeDir, trailingResizeDir );
}
public void uninstall() {
rootPane.removeComponentListener( this );
rootPane.getLayeredPane().remove( this );
}
@Override
public void addNotify() {
super.addNotify();
Container parent = rootPane.getParent();
window = (parent instanceof Window) ? (Window) parent : null;
if( window instanceof Frame ) {
window.addPropertyChangeListener( "resizable", this );
window.addWindowStateListener( this );
}
updateVisibility();
}
@Override
public void removeNotify() {
super.removeNotify();
if( window instanceof Frame ) {
window.removePropertyChangeListener( "resizable", this );
window.removeWindowStateListener( this );
}
window = null;
updateVisibility();
}
@Override
protected void paintChildren( Graphics g ) {
super.paintChildren( g );
// this is necessary because Dialog.setResizable() does not fire events
if( window instanceof Dialog )
updateVisibility();
}
private void updateVisibility() {
boolean visible = isWindowResizable();
if( visible == getComponent( 0 ).isVisible() )
return;
for( Component c : getComponents() )
c.setVisible( visible );
}
private boolean isWindowResizable() {
if( window instanceof Frame )
return ((Frame)window).isResizable() && (((Frame)window).getExtendedState() & Frame.MAXIMIZED_BOTH) == 0;
if( window instanceof Dialog )
return ((Dialog)window).isResizable();
return false;
}
@Override
public void propertyChange( PropertyChangeEvent e ) {
updateVisibility();
}
@Override
public void windowStateChanged( WindowEvent e ) {
updateVisibility();
}
@Override
public void componentResized( ComponentEvent e ) {
setBounds( 0, 0, rootPane.getWidth(), rootPane.getHeight() );
validate();
}
@Override public void componentMoved( ComponentEvent e ) {}
@Override public void componentShown( ComponentEvent e ) {}
@Override public void componentHidden( ComponentEvent e ) {}
//---- class DragBorderComponent ------------------------------------------
protected class DragBorderComponent
extends JComponent
implements MouseListener, MouseMotionListener
{
private final int leadingResizeDir;
private final int centerResizeDir;
private final int trailingResizeDir;
private int resizeDir = -1;
private int dragStartMouseX;
private int dragStartMouseY;
private Rectangle dragStartWindowBounds;
protected DragBorderComponent( int leadingResizeDir, int centerResizeDir, int trailingResizeDir ) {
this.leadingResizeDir = leadingResizeDir;
this.centerResizeDir = centerResizeDir;
this.trailingResizeDir = trailingResizeDir;
setResizeDir( centerResizeDir );
setVisible( false );
addMouseListener( this );
addMouseMotionListener( this );
}
protected void setResizeDir( int resizeDir ) {
if( this.resizeDir == resizeDir )
return;
this.resizeDir = resizeDir;
setCursor( getPredefinedCursor( resizeDir ) );
}
@Override
public Dimension getPreferredSize() {
int thickness = UIScale.scale( borderDragThickness );
return new Dimension( thickness, thickness );
}
/*debug
@Override
protected void paintComponent( Graphics g ) {
g.setColor( java.awt.Color.red );
g.drawRect( 0, 0, getWidth() - 1, getHeight() - 1 );
}
debug*/
@Override
public void mouseClicked( MouseEvent e ) {
}
@Override
public void mousePressed( MouseEvent e ) {
if( window == null )
return;
dragStartMouseX = e.getXOnScreen();
dragStartMouseY = e.getYOnScreen();
dragStartWindowBounds = window.getBounds();
}
@Override
public void mouseReleased( MouseEvent e ) {
dragStartWindowBounds = null;
}
@Override
public void mouseEntered( MouseEvent e ) {
}
@Override
public void mouseExited( MouseEvent e ) {
}
@Override
public void mouseMoved( MouseEvent e ) {
boolean topBottom = (centerResizeDir == N_RESIZE_CURSOR || centerResizeDir == S_RESIZE_CURSOR);
int xy = topBottom ? e.getX() : e.getY();
int wh = topBottom ? getWidth() : getHeight();
int cornerWH = UIScale.scale( cornerDragWidth - (topBottom ? 0 : borderDragThickness) );
setResizeDir( xy <= cornerWH
? leadingResizeDir
: (xy >= wh - cornerWH
? trailingResizeDir
: centerResizeDir) );
}
@Override
public void mouseDragged( MouseEvent e ) {
if( dragStartWindowBounds == null )
return;
if( !isWindowResizable() )
return;
int mouseDeltaX = e.getXOnScreen() - dragStartMouseX;
int mouseDeltaY = e.getYOnScreen() - dragStartMouseY;
int deltaX = 0;
int deltaY = 0;
int deltaWidth = 0;
int deltaHeight = 0;
// north
if( resizeDir == N_RESIZE_CURSOR || resizeDir == NW_RESIZE_CURSOR || resizeDir == NE_RESIZE_CURSOR ) {
deltaY = mouseDeltaY;
deltaHeight = -mouseDeltaY;
}
// south
if( resizeDir == S_RESIZE_CURSOR || resizeDir == SW_RESIZE_CURSOR || resizeDir == SE_RESIZE_CURSOR )
deltaHeight = mouseDeltaY;
// west
if( resizeDir == W_RESIZE_CURSOR || resizeDir == NW_RESIZE_CURSOR || resizeDir == SW_RESIZE_CURSOR ) {
deltaX = mouseDeltaX;
deltaWidth = -mouseDeltaX;
}
// east
if( resizeDir == E_RESIZE_CURSOR || resizeDir == NE_RESIZE_CURSOR || resizeDir == SE_RESIZE_CURSOR )
deltaWidth = mouseDeltaX;
// compute new window bounds
Rectangle newBounds = new Rectangle( dragStartWindowBounds );
newBounds.x += deltaX;
newBounds.y += deltaY;
newBounds.width += deltaWidth;
newBounds.height += deltaHeight;
// apply minimum window size
Dimension minimumSize = honorMinimumSizeOnResize ? window.getMinimumSize() : null;
if( minimumSize == null )
minimumSize = UIScale.scale( new Dimension( 150, 50 ) );
if( newBounds.width < minimumSize.width ) {
if( deltaX != 0 )
newBounds.x -= (minimumSize.width - newBounds.width);
newBounds.width = minimumSize.width;
}
if( newBounds.height < minimumSize.height ) {
if( deltaY != 0 )
newBounds.y -= (minimumSize.height - newBounds.height);
newBounds.height = minimumSize.height;
}
// set window bounds
if( !newBounds.equals( dragStartWindowBounds ) ) {
window.setBounds( newBounds );
// immediately layout drag border components
FlatWindowResizer.this.setBounds( 0, 0, newBounds.width, newBounds.height );
FlatWindowResizer.this.validate();
if( Toolkit.getDefaultToolkit().isDynamicLayoutActive() ) {
window.validate();
rootPane.repaint();
}
}
}
}
}

View File

@@ -0,0 +1,315 @@
/*
* 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.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.beans.PropertyChangeListener;
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.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.plaf.BorderUIResource;
import com.formdev.flatlaf.FlatLaf;
import com.formdev.flatlaf.FlatSystemProperties;
import com.formdev.flatlaf.util.HiDPIUtils;
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 ) {
if( !isSupported() )
return;
// check whether root pane already has a parent, which is the case when switching LaF
if( rootPane.getParent() != null )
return;
// Use hierarchy listener to wait until the root pane is added to a window.
// Enabling JBR decorations must be done very early, probably before
// window becomes displayable (window.isDisplayable()). Tried also using
// "ancestor" property change event on root pane, but this is invoked too late.
HierarchyListener addListener = new HierarchyListener() {
@Override
public void hierarchyChanged( HierarchyEvent e ) {
if( e.getChanged() != rootPane || (e.getChangeFlags() & HierarchyEvent.PARENT_CHANGED) == 0 )
return;
Container parent = e.getChangedParent();
if( parent instanceof Window )
install( (Window) parent );
// 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 void install( Window window ) {
if( !isSupported() )
return;
// 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 );
}
}
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;
if( !FlatSystemProperties.getBoolean( FlatSystemProperties.USE_JETBRAINS_CUSTOM_DECORATIONS, true ) )
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
}
}
//---- class JBRWindowTopBorder -------------------------------------------
static class JBRWindowTopBorder
extends BorderUIResource.EmptyBorderUIResource
{
private static JBRWindowTopBorder instance;
private final Color defaultActiveBorder = new Color( 0x707070 );
private final Color inactiveLightColor = new Color( 0xaaaaaa );
private final Color inactiveDarkColor = new Color( 0x3f3f3f );
private boolean colorizationAffectsBorders;
private Color activeColor = defaultActiveBorder;
static JBRWindowTopBorder getInstance() {
if( instance == null )
instance = new JBRWindowTopBorder();
return instance;
}
private JBRWindowTopBorder() {
super( 1, 0, 0, 0 );
colorizationAffectsBorders = calculateAffectsBorders();
activeColor = calculateActiveBorderColor();
Toolkit toolkit = Toolkit.getDefaultToolkit();
toolkit.addPropertyChangeListener( "win.dwm.colorizationColor.affects.borders", e -> {
colorizationAffectsBorders = calculateAffectsBorders();
activeColor = calculateActiveBorderColor();
} );
PropertyChangeListener l = e -> {
activeColor = calculateActiveBorderColor();
};
toolkit.addPropertyChangeListener( "win.dwm.colorizationColor", l );
toolkit.addPropertyChangeListener( "win.dwm.colorizationColorBalance", l );
toolkit.addPropertyChangeListener( "win.frame.activeBorderColor", l );
}
private boolean calculateAffectsBorders() {
Object value = Toolkit.getDefaultToolkit().getDesktopProperty( "win.dwm.colorizationColor.affects.borders" );
return (value instanceof Boolean) ? (Boolean) value : true;
}
private Color calculateActiveBorderColor() {
if( !colorizationAffectsBorders )
return defaultActiveBorder;
Toolkit toolkit = Toolkit.getDefaultToolkit();
Color colorizationColor = (Color) toolkit.getDesktopProperty( "win.dwm.colorizationColor" );
if( colorizationColor != null ) {
Object colorizationColorBalanceObj = toolkit.getDesktopProperty( "win.dwm.colorizationColorBalance" );
if( colorizationColorBalanceObj instanceof Integer ) {
int colorizationColorBalance = (Integer) colorizationColorBalanceObj;
if( colorizationColorBalance < 0 )
colorizationColorBalance = 100;
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) );
return new Color( r, g, b );
}
return colorizationColor;
}
Color activeBorderColor = (Color) toolkit.getDesktopProperty( "win.frame.activeBorderColor" );
return (activeBorderColor != null) ? activeBorderColor : UIManager.getColor( "MenuBar.borderColor" );
}
@Override
public void paintBorder( Component c, Graphics g, int x, int y, int width, int height ) {
Window window = SwingUtilities.windowForComponent( c );
boolean active = (window != null) ? window.isActive() : false;
g.setColor( active ? activeColor : (FlatLaf.isLafDark() ? inactiveDarkColor : inactiveLightColor) );
HiDPIUtils.paintAtScale1x( (Graphics2D) g, x, y, width, height, this::paintImpl );
}
private void paintImpl( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) {
g.drawRect( x, y, width - 1, 0 );
}
void repaintBorder( Component c ) {
c.repaint( 0, 0, c.getWidth(), 1 );
}
}
}

View File

@@ -23,6 +23,7 @@ import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.text.AttributedCharacterIterator;
import javax.swing.JComponent;
import com.formdev.flatlaf.FlatSystemProperties;
/**
* @author Karl Tauber
@@ -104,7 +105,7 @@ public class HiDPIUtils
private static boolean useTextYCorrection() {
if( useTextYCorrection == null )
useTextYCorrection = Boolean.valueOf( System.getProperty( "flatlaf.useTextYCorrection", "true" ) );
useTextYCorrection = FlatSystemProperties.getBoolean( FlatSystemProperties.USE_TEXT_Y_CORRECTION, true );
return useTextYCorrection;
}

View File

@@ -37,30 +37,38 @@ public class ScaledImageIcon
implements Icon
{
private final ImageIcon imageIcon;
private final int iconWidth;
private final int iconHeight;
private double lastSystemScaleFactor;
private float lastUserScaleFactor;
private Image lastImage;
public ScaledImageIcon( ImageIcon imageIcon ) {
this( imageIcon, imageIcon.getIconWidth(), imageIcon.getIconHeight() );
}
public ScaledImageIcon( ImageIcon imageIcon, int iconWidth, int iconHeight ) {
this.imageIcon = imageIcon;
this.iconWidth = iconWidth;
this.iconHeight = iconHeight;
}
@Override
public int getIconWidth() {
return UIScale.scale( imageIcon.getIconWidth() );
return UIScale.scale( iconWidth );
}
@Override
public int getIconHeight() {
return UIScale.scale( imageIcon.getIconHeight() );
return UIScale.scale( iconHeight );
}
@Override
public void paintIcon( Component c, Graphics g, int x, int y ) {
/*debug
g.setColor( Color.red );
g.drawRect( x, y, getIconWidth(), getIconHeight() );
g.drawRect( x, y, getIconWidth() - 1, getIconHeight() - 1 );
debug*/
// scale factor
@@ -69,7 +77,7 @@ debug*/
double scaleFactor = systemScaleFactor * userScaleFactor;
// paint input image icon if not necessary to scale
if( scaleFactor == 1 ) {
if( scaleFactor == 1 && iconWidth == imageIcon.getIconWidth() && iconHeight == imageIcon.getIconHeight() ) {
imageIcon.paintIcon( c, g, x, y );
return;
}
@@ -84,12 +92,11 @@ debug*/
}
// destination image size
int destImageWidth = (int) Math.round( imageIcon.getIconWidth() * scaleFactor );
int destImageHeight = (int) Math.round( imageIcon.getIconHeight() * scaleFactor );
int destImageWidth = (int) Math.round( iconWidth * scaleFactor );
int destImageHeight = (int) Math.round( iconHeight * scaleFactor );
// get resolution variant of image if it is a multi-resolution image
Image image = MultiResolutionImageSupport.getResolutionVariant(
imageIcon.getImage(), destImageWidth, destImageHeight );
Image image = getResolutionVariant( destImageWidth, destImageHeight );
// size of image
int imageWidth = image.getWidth( null );
@@ -124,6 +131,11 @@ debug*/
paintLastImage( g, x, y );
}
protected Image getResolutionVariant( int destImageWidth, int destImageHeight ) {
return MultiResolutionImageSupport.getResolutionVariant(
imageIcon.getImage(), destImageWidth, destImageHeight );
}
private void paintLastImage( Graphics g, int x, int y ) {
if( lastSystemScaleFactor > 1 ) {
HiDPIUtils.paintAtScale1x( (Graphics2D) g, x, y, 100, 100, // width and height are not used

View File

@@ -32,14 +32,19 @@ 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;
public static final boolean IS_MAC_OS_10_14_MOJAVE;
public static final boolean IS_MAC_OS_10_15_CATALINA_OR_LATER;
// Java versions
public static final boolean IS_JAVA_9_OR_LATER;
public static final boolean IS_JAVA_11_OR_LATER;
public static final boolean IS_JAVA_15_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;
@@ -53,16 +58,21 @@ 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 ));
IS_MAC_OS_10_14_MOJAVE = (IS_MAC && osVersion >= toVersion( 10, 14, 0, 0 ));
IS_MAC_OS_10_15_CATALINA_OR_LATER = (IS_MAC && osVersion >= toVersion( 10, 15, 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 ));
IS_JAVA_15_OR_LATER = (javaVersion >= toVersion( 15, 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);

View File

@@ -32,6 +32,7 @@ import javax.swing.plaf.DimensionUIResource;
import javax.swing.plaf.FontUIResource;
import javax.swing.plaf.InsetsUIResource;
import javax.swing.plaf.UIResource;
import com.formdev.flatlaf.FlatSystemProperties;
/**
* Two scaling modes are supported for HiDPI displays:
@@ -195,8 +196,7 @@ public class UIScale
private static boolean isUserScalingEnabled() {
// same as in IntelliJ IDEA
String hidpi = System.getProperty( "hidpi" );
return (hidpi != null) ? Boolean.parseBoolean( hidpi ) : true;
return FlatSystemProperties.getBoolean( "hidpi", true );
}
/**
@@ -204,7 +204,7 @@ public class UIScale
* to the given font.
*/
public static FontUIResource applyCustomScaleFactor( FontUIResource font ) {
String uiScale = System.getProperty( "flatlaf.uiScale" );
String uiScale = System.getProperty( FlatSystemProperties.UI_SCALE );
float scaleFactor = parseScaleFactor( uiScale );
if( scaleFactor <= 0 )
return font;

View File

@@ -293,6 +293,13 @@ TableHeader.separatorColor=lighten($TableHeader.background,10%)
TableHeader.bottomSeparatorColor=$TableHeader.separatorColor
#---- TitlePane ----
TitlePane.embeddedForeground=darken($TitlePane.foreground,15%)
TitlePane.buttonHoverBackground=lighten($TitlePane.background,10%,derived)
TitlePane.buttonPressedBackground=lighten($TitlePane.background,20%,derived)
#---- ToggleButton ----
ToggleButton.selectedBackground=lighten($ToggleButton.background,10%,derived)

View File

@@ -425,6 +425,14 @@ RadioButtonMenuItem.borderPainted=true
RadioButtonMenuItem.background=@menuBackground
#---- RootPane ----
RootPane.border=com.formdev.flatlaf.ui.FlatRootPaneUI$FlatWindowBorder
RootPane.borderDragThickness=5
RootPane.cornerDragWidth=16
RootPane.honorMinimumSizeOnResize=true
#---- ScrollBar ----
ScrollBar.width=10
@@ -578,6 +586,31 @@ TitledBorder.titleColor=@foreground
TitledBorder.border=1,1,1,1,$Separator.foreground
#---- TitlePane ----
TitlePane.menuBarEmbedded=true
TitlePane.iconSize=16,16
TitlePane.iconMargins=3,8,3,0
TitlePane.menuBarMargins=0,8,0,22
TitlePane.titleMargins=3,8,3,8
TitlePane.buttonSize=44,30
TitlePane.buttonMaximizedHeight=22
TitlePane.closeIcon=com.formdev.flatlaf.icons.FlatWindowCloseIcon
TitlePane.iconifyIcon=com.formdev.flatlaf.icons.FlatWindowIconifyIcon
TitlePane.maximizeIcon=com.formdev.flatlaf.icons.FlatWindowMaximizeIcon
TitlePane.restoreIcon=com.formdev.flatlaf.icons.FlatWindowRestoreIcon
TitlePane.background=$MenuBar.background
TitlePane.inactiveBackground=$TitlePane.background
TitlePane.foreground=@foreground
TitlePane.inactiveForeground=@disabledText
TitlePane.closeHoverBackground=#e81123
TitlePane.closePressedBackground=rgba($TitlePane.closeHoverBackground,60%)
TitlePane.closeHoverForeground=#fff
TitlePane.closePressedForeground=#fff
#---- ToggleButton ----
ToggleButton.border=com.formdev.flatlaf.ui.FlatButtonBorder

View File

@@ -305,6 +305,13 @@ TableHeader.separatorColor=darken($TableHeader.background,10%)
TableHeader.bottomSeparatorColor=$TableHeader.separatorColor
#---- TitlePane ----
TitlePane.embeddedForeground=lighten($TitlePane.foreground,35%)
TitlePane.buttonHoverBackground=darken($TitlePane.background,10%,derived)
TitlePane.buttonPressedBackground=darken($TitlePane.background,20%,derived)
#---- ToggleButton ----
ToggleButton.selectedBackground=darken($ToggleButton.background,20%,derived)