From 7858e42e37794653d5331adc31ee0924e3213fcb Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Mon, 5 Sep 2022 14:47:17 +0200 Subject: [PATCH] fixed AWT components on macOS (issue #583) - fixed missing focus indicator - fixed round corners - fixed java.awt.Button background - fixed java.awt.Choice background - fixed java.awt.Checkbox hover --- CHANGELOG.md | 2 + .../com/formdev/flatlaf/ui/FlatButtonUI.java | 3 + .../formdev/flatlaf/ui/FlatComboBoxUI.java | 3 + .../formdev/flatlaf/ui/FlatRadioButtonUI.java | 81 +++++++++++++++++++ .../com/formdev/flatlaf/ui/FlatUIUtils.java | 30 +++++-- .../formdev/flatlaf/testing/FlatAWTTest.java | 49 +++++++++++ 6 files changed, 160 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d739a49..68905028 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ FlatLaf Change Log - Fixed missing UI value `MenuItem.acceleratorDelimiter` on macOS. (was `null`, is now an empty string) - Fixed possible exception in `FlatUIUtils.resetRenderingHints()`. (issue #575) +- Fixed AWT components on macOS, which use Swing components internally. (issue + #583) ## 2.4 diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatButtonUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatButtonUI.java index e7a02584..b286ff9a 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatButtonUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatButtonUI.java @@ -629,6 +629,9 @@ public class FlatButtonUI } protected Color getBackgroundBase( JComponent c, boolean def ) { + if( FlatUIUtils.isAWTPeer( c ) ) + return background; + // use component background if explicitly set Color bg = c.getBackground(); if( isCustomBackground( bg ) ) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatComboBoxUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatComboBoxUI.java index 88fd7ff5..a7b78c2d 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatComboBoxUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatComboBoxUI.java @@ -631,6 +631,9 @@ public class FlatComboBoxUI protected Color getBackground( boolean enabled ) { if( enabled ) { + if( FlatUIUtils.isAWTPeer( comboBox ) ) + return UIManager.getColor( "ComboBox.background" ); + Color background = comboBox.getBackground(); // always use explicitly set color diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatRadioButtonUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatRadioButtonUI.java index 26fea08e..df9304d1 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatRadioButtonUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatRadioButtonUI.java @@ -18,12 +18,16 @@ package com.formdev.flatlaf.ui; import static com.formdev.flatlaf.util.UIScale.scale; import java.awt.Color; +import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Insets; import java.awt.Rectangle; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; import java.util.Map; import java.util.Objects; import javax.swing.AbstractButton; @@ -31,6 +35,7 @@ import javax.swing.CellRendererPane; import javax.swing.Icon; import javax.swing.JComponent; import javax.swing.LookAndFeel; +import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.BasicButtonListener; @@ -92,9 +97,20 @@ public class FlatRadioButtonUI public void installUI( JComponent c ) { super.installUI( c ); + if( FlatUIUtils.isAWTPeer( c ) ) + AWTPeerMouseExitedFix.install( c ); + installStyle( (AbstractButton) c ); } + @Override + public void uninstallUI( JComponent c ) { + super.uninstallUI( c ); + + if( FlatUIUtils.isAWTPeer( c ) ) + AWTPeerMouseExitedFix.uninstall( c ); + } + @Override public void installDefaults( AbstractButton b ) { super.installDefaults( b ); @@ -321,4 +337,69 @@ public class FlatRadioButtonUI FlatRadioButtonUI.this.propertyChange( b, e ); } } + + //---- class AWTPeerMouseExitedFix ---------------------------------------- + + /** + * Hack for missing mouse-exited event for java.awt.Checkbox on macOS (to fix hover effect). + * + * On macOS, AWT components internally use Swing components. + * This is implemented in class sun.lwawt.LWCheckboxPeer, which uses + * a container component CheckboxDelegate that has a JCheckBox and a JRadioButton + * as children. Only one of them is visible. + * + * The reason that mouse-exited event is not sent to the JCheckBox or JRadioButton + * is that sun.lwawt.LWComponentPeer.createDelegateEvent() uses + * SwingUtilities.getDeepestComponentAt() to find the event target, + * which finds the container component CheckboxDelegate, + * which receives the mouse-exited event. + * + * This class adds listeners and forwards the mouse-exited event + * from CheckboxDelegate to JCheckBox or JRadioButton. + */ + private static class AWTPeerMouseExitedFix + extends MouseAdapter + implements PropertyChangeListener + { + private final JComponent button; + + static void install( JComponent button ) { + AWTPeerMouseExitedFix l = new AWTPeerMouseExitedFix( button ); + button.addPropertyChangeListener( "ancestor", l ); + Container parent = button.getParent(); + if( parent != null ) + parent.addMouseListener( l ); + } + + static void uninstall( JComponent button ) { + for( PropertyChangeListener l : button.getPropertyChangeListeners( "ancestor" ) ) { + if( l instanceof AWTPeerMouseExitedFix ) { + button.removePropertyChangeListener( "ancestor", l ); + Container parent = button.getParent(); + if( parent != null ) + parent.removeMouseListener( (AWTPeerMouseExitedFix) l ); + break; + } + } + } + + AWTPeerMouseExitedFix( JComponent button ) { + this.button = button; + } + + @Override + public void propertyChange( PropertyChangeEvent e ) { + if( e.getOldValue() instanceof Component ) + ((Component)e.getOldValue()).removeMouseListener( this ); + if( e.getNewValue() instanceof Component ) { + ((Component)e.getNewValue()).removeMouseListener( this ); // avoid duplicate listeners + ((Component)e.getNewValue()).addMouseListener( this ); + } + } + + @Override + public void mouseExited( MouseEvent e ) { + button.dispatchEvent( SwingUtilities.convertMouseEvent( e.getComponent(), e, button ) ); + } + } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatUIUtils.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatUIUtils.java index 53440bcc..df5fd2a8 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatUIUtils.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatUIUtils.java @@ -59,6 +59,7 @@ import com.formdev.flatlaf.FlatSystemProperties; import com.formdev.flatlaf.util.DerivedColor; import com.formdev.flatlaf.util.Graphics2DProxy; import com.formdev.flatlaf.util.HiDPIUtils; +import com.formdev.flatlaf.util.SystemInfo; import com.formdev.flatlaf.util.UIScale; /** @@ -241,6 +242,11 @@ public class FlatUIUtils } } + // invoke hasFocus() here because components may have overridden this method + // (e.g. Swing delegate components used for AWT components on macOS) + if( c.hasFocus() ) + return true; + return keyboardFocusManager.getPermanentFocusOwner() == c && isInActiveWindow( c, keyboardFocusManager.getActiveWindow() ); } @@ -251,6 +257,13 @@ public class FlatUIUtils (window != null && window.getType() == Window.Type.POPUP && window.getOwner() == activeWindow); } + static boolean isAWTPeer( JComponent c ) { + // on macOS, Swing components are used for AWT components + if( SystemInfo.isMacOS ) + return c.getClass().getName().startsWith( "sun.lwawt.LW" ); + return false; + } + /** * Returns whether the given component is in a window that is in full-screen mode. */ @@ -335,7 +348,7 @@ public class FlatUIUtils */ public static Object[] setRenderingHints( Graphics g ) { Graphics2D g2 = (Graphics2D) g; - Object[] oldRenderingHints = new Object[] { + Object[] oldRenderingHints = { g2.getRenderingHint( RenderingHints.KEY_ANTIALIASING ), g2.getRenderingHint( RenderingHints.KEY_STROKE_CONTROL ), }; @@ -378,7 +391,7 @@ public class FlatUIUtils } Graphics2D g2 = (Graphics2D) g; - Object[] oldRenderingHints2 = new Object[] { + Object[] oldRenderingHints2 = { g2.getRenderingHint( RenderingHints.KEY_ANTIALIASING ), g2.getRenderingHint( RenderingHints.KEY_STROKE_CONTROL ), }; @@ -666,9 +679,9 @@ public class FlatUIUtils * is smaller than its bounds (for the focus decoration). */ public static void paintParentBackground( Graphics g, JComponent c ) { - Container parent = findOpaqueParent( c ); - if( parent != null ) { - g.setColor( parent.getBackground() ); + Color background = getParentBackground( c ); + if( background != null ) { + g.setColor( background ); g.fillRect( 0, 0, c.getWidth(), c.getHeight() ); } } @@ -678,9 +691,10 @@ public class FlatUIUtils */ public static Color getParentBackground( JComponent c ) { Container parent = findOpaqueParent( c ); - return (parent != null) - ? parent.getBackground() - : UIManager.getColor( "Panel.background" ); // fallback, probably never used + // parent.getBackground() may return null + // (e.g. for Swing delegate components used for AWT components on macOS) + Color background = (parent != null) ? parent.getBackground() : null; + return (background != null) ? background : UIManager.getColor( "Panel.background" ); } /** diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAWTTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAWTTest.java index 2a9691f1..f2eb6dfe 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAWTTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAWTTest.java @@ -17,8 +17,11 @@ package com.formdev.flatlaf.testing; import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; +import javax.swing.SwingUtilities; import com.formdev.flatlaf.FlatLightLaf; /** @@ -69,6 +72,52 @@ public class FlatAWTTest frame.add( new Panel() ); frame.add( new Canvas() ); + Panel controlPanel = new Panel(); + frame.add( controlPanel ); + + Checkbox enabledCheckBox = new Checkbox( "enabled", true ); + enabledCheckBox.addItemListener( e -> { + boolean enabled = enabledCheckBox.getState(); + for( Component c : frame.getComponents() ) { + if( c != controlPanel ) + c.setEnabled( enabled ); + } + } ); + controlPanel.add( enabledCheckBox ); + + Checkbox explicitColorsCheckBox = new Checkbox( "explicit colors" ); + explicitColorsCheckBox.addItemListener( e -> { + boolean explicit = explicitColorsCheckBox.getState(); + for( Component c : frame.getComponents() ) { + if( c != controlPanel ) + c.setBackground( explicit ? Color.green : null ); + } + } ); + controlPanel.add( explicitColorsCheckBox ); + + Menu menu = new Menu( "File" ); + menu.add( new MenuItem( "New" ) ); + menu.add( new MenuItem( "Open" ) ); + menu.add( new MenuItem( "Save" ) ); + + MenuBar menuBar = new MenuBar(); + menuBar.add( menu ); + frame.setMenuBar( menuBar ); + + PopupMenu popupMenu = new PopupMenu(); + popupMenu.add( new MenuItem( "item 1" ) ); + popupMenu.add( new MenuItem( "item 2" ) ); + popupMenu.add( new MenuItem( "item 3" ) ); + list.add( popupMenu ); + list.addMouseListener( new MouseAdapter() { + @Override + public + void mousePressed( MouseEvent e ) { + if( SwingUtilities.isRightMouseButton( e ) ) + popupMenu.show( list, 0, 0 ); + } + } ); + frame.setSize( 800, 600 ); frame.setVisible( true ); });