From a84aceb1ba90d10309ac85d3149bd6b56b2ce285 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 4 Feb 2024 16:30:38 +0100 Subject: [PATCH] Window decorations: improved caption hit testing to better support TabbedPane, SplitPane and ToolBar in title bar area (e.g. for fullWindowContent mode) --- .../formdev/flatlaf/FlatClientProperties.java | 31 ++++- .../flatlaf/ui/FlatNativeWindowBorder.java | 6 +- .../formdev/flatlaf/ui/FlatSplitPaneUI.java | 11 +- .../formdev/flatlaf/ui/FlatTabbedPaneUI.java | 13 +- .../com/formdev/flatlaf/ui/FlatTitlePane.java | 122 ++++++++++++------ .../com/formdev/flatlaf/ui/FlatToolBarUI.java | 11 +- .../ui/FlatWindowsNativeWindowBorder.java | 8 +- .../jideoss/ui/FlatJideTabbedPaneUI.java | 22 ++++ .../FlatWindowsNativeWindowBorder.java | 8 +- 9 files changed, 177 insertions(+), 55 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java index 75c9c33a0..12e04775b 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java @@ -257,18 +257,39 @@ public interface FlatClientProperties String COMPONENT_FOCUS_OWNER = "JComponent.focusOwner"; /** - * Specifies whether a component in an embedded menu bar should behave as caption + * Specifies whether a component shown in a window title bar area should behave as caption * (left-click allows moving window, right-click shows window system menu). - * The component does not receive mouse pressed/released/clicked/dragged events, + * The caption component does not receive mouse pressed/released/clicked/dragged events, * but it gets mouse entered/exited/moved events. *

+ * Since 3.4, this client property also supports using a function that can check + * whether a given location in the component should behave as caption. + * Useful for components that do not use mouse input on whole component bounds. + * + *

{@code
+	 * myComponent.putClientProperty( "JComponent.titleBarCaption",
+	 *     (Function) pt -> {
+	 *         // parameter pt contains mouse location (in myComponent coordinates)
+	 *         // return true if the component is not interested in mouse input at the given location
+	 *         // return false if the component wants process mouse input at the given location
+	 *         // return null if the component children should be checked
+	 *         return ...; // check here
+	 *     } );
+	 * }
+ * Warning: + * + *

* Component {@link javax.swing.JComponent}
- * Value type {@link java.lang.Boolean} + * Value type {@link java.lang.Boolean} or {@link java.util.function.Function}<Point, Boolean> * * @since 2.5 - * @deprecated No longer used since FlatLaf 3.4. Retained for API compatibility. */ - @Deprecated String COMPONENT_TITLE_BAR_CAPTION = "JComponent.titleBarCaption"; diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowBorder.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowBorder.java index 1f4e2d073..c634f493e 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowBorder.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowBorder.java @@ -219,13 +219,13 @@ public static void setHasCustomDecoration( Window window, boolean hasCustomDecor } static void setTitleBarHeightAndHitTestSpots( Window window, int titleBarHeight, - Predicate hitTestCallback, Rectangle appIconBounds, Rectangle minimizeButtonBounds, + Predicate captionHitTestCallback, Rectangle appIconBounds, Rectangle minimizeButtonBounds, Rectangle maximizeButtonBounds, Rectangle closeButtonBounds ) { if( !isSupported() ) return; - nativeProvider.updateTitleBarInfo( window, titleBarHeight, hitTestCallback, + nativeProvider.updateTitleBarInfo( window, titleBarHeight, captionHitTestCallback, appIconBounds, minimizeButtonBounds, maximizeButtonBounds, closeButtonBounds ); } @@ -271,7 +271,7 @@ public interface Provider { boolean hasCustomDecoration( Window window ); void setHasCustomDecoration( Window window, boolean hasCustomDecoration ); - void updateTitleBarInfo( Window window, int titleBarHeight, Predicate hitTestCallback, + void updateTitleBarInfo( Window window, int titleBarHeight, Predicate captionHitTestCallback, Rectangle appIconBounds, Rectangle minimizeButtonBounds, Rectangle maximizeButtonBounds, Rectangle closeButtonBounds ); diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatSplitPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatSplitPaneUI.java index 273d1fbf1..335c11d54 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatSplitPaneUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatSplitPaneUI.java @@ -84,7 +84,7 @@ */ public class FlatSplitPaneUI extends BasicSplitPaneUI - implements StyleableUI + implements StyleableUI, FlatTitlePane.TitleBarCaptionHitTest { @Styleable protected String arrowType; /** @since 3.3 */ @Styleable protected Color draggingColor; @@ -227,6 +227,15 @@ private void paintDragDivider( Graphics g, int dividerLocation ) { ((FlatSplitPaneDivider)divider).paintStyle( g, x, y, width, height ); } + //---- interface FlatTitlePane.TitleBarCaptionHitTest ---- + + /** @since 3.4 */ + @Override + public Boolean isTitleBarCaptionAt( int x, int y ) { + // necessary because BasicSplitPaneDivider adds some mouse listeners for dragging divider + return null; // check children + } + //---- class FlatSplitPaneDivider ----------------------------------------- protected class FlatSplitPaneDivider diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java index 4a202f86b..dfff891e6 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java @@ -182,7 +182,7 @@ */ public class FlatTabbedPaneUI extends BasicTabbedPaneUI - implements StyleableUI + implements StyleableUI, FlatTitlePane.TitleBarCaptionHitTest { // tab type /** @since 2 */ protected static final int TAB_TYPE_UNDERLINED = 0; @@ -2300,6 +2300,17 @@ private int rectsTotalHeight() { return (rects[last].y + rects[last].height) - rects[0].y; } + //---- interface FlatTitlePane.TitleBarCaptionHitTest ---- + + /** @since 3.4 */ + @Override + public Boolean isTitleBarCaptionAt( int x, int y ) { + if( tabForCoordinate( tabPane, x, y ) >= 0 ) + return false; + + return null; // check children + } + //---- class TabCloseButton ----------------------------------------------- private static class TabCloseButton diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTitlePane.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTitlePane.java index c9af7656b..01cb991f7 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTitlePane.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTitlePane.java @@ -49,6 +49,7 @@ import java.beans.PropertyChangeListener; import java.util.List; import java.util.Objects; +import java.util.function.Function; import javax.accessibility.AccessibleContext; import javax.swing.BorderFactory; import javax.swing.Box; @@ -65,6 +66,7 @@ import javax.swing.UIManager; import javax.swing.border.AbstractBorder; import javax.swing.border.Border; +import javax.swing.plaf.ComponentUI; import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.FlatSystemProperties; import com.formdev.flatlaf.ui.FlatNativeWindowBorder.WindowTopBorder; @@ -314,7 +316,7 @@ public void layoutContainer( Container target ) { } // clear hit-test cache - lastHitTestTime = 0; + lastCaptionHitTestTime = 0; } } ); @@ -1004,10 +1006,10 @@ protected void updateNativeTitleBarHeightAndHitTestSpots() { Rectangle closeButtonBounds = boundsInWindow( closeButton ); // clear hit-test cache - lastHitTestTime = 0; + lastCaptionHitTestTime = 0; FlatNativeWindowBorder.setTitleBarHeightAndHitTestSpots( window, titleBarHeight, - this::hitTest, appIconBounds, minimizeButtonBounds, maximizeButtonBounds, closeButtonBounds ); + this::captionHitTest, appIconBounds, minimizeButtonBounds, maximizeButtonBounds, closeButtonBounds ); debugTitleBarHeight = titleBarHeight; debugAppIconBounds = appIconBounds; @@ -1024,18 +1026,8 @@ private Rectangle boundsInWindow( JComponent c ) { : null; } - protected Rectangle getNativeHitTestSpot( JComponent c ) { - Dimension size = c.getSize(); - if( size.width <= 0 || size.height <= 0 ) - return null; - - Point location = SwingUtilities.convertPoint( c, 0, 0, window ); - Rectangle r = new Rectangle( location, size ); - return r; - } - /** - * Returns wheter there is a component at the given location, that processes + * Returns whether there is a component at the given location, that processes * mouse events. E.g. buttons, menus, etc. *

* Note: @@ -1046,12 +1038,12 @@ protected Rectangle getNativeHitTestSpot( JComponent c ) { * while processing Windows messages. * */ - private boolean hitTest( Point pt ) { + private boolean captionHitTest( Point pt ) { // Windows invokes this method every ~200ms, even if the mouse has not moved long time = System.currentTimeMillis(); - if( pt.x == lastHitTestX && pt.y == lastHitTestY && time < lastHitTestTime + 300 ) { - lastHitTestTime = time; - return lastHitTestResult; + if( pt.x == lastCaptionHitTestX && pt.y == lastCaptionHitTestY && time < lastCaptionHitTestTime + 300 ) { + lastCaptionHitTestTime = time; + return lastCaptionHitTestResult; } // convert pt from window coordinates to layeredPane coordinates @@ -1063,35 +1055,70 @@ private boolean hitTest( Point pt ) { y -= c.getY(); } - lastHitTestX = pt.x; - lastHitTestY = pt.y; - lastHitTestTime = time; - lastHitTestResult = isComponentWithMouseListenerAt( layeredPane, x, y ); - return lastHitTestResult; + lastCaptionHitTestX = pt.x; + lastCaptionHitTestY = pt.y; + lastCaptionHitTestTime = time; + lastCaptionHitTestResult = isTitleBarCaptionAt( layeredPane, x, y ); + return lastCaptionHitTestResult; } - private boolean isComponentWithMouseListenerAt( Component c, int x, int y ) { + private boolean isTitleBarCaptionAt( Component c, int x, int y ) { if( !c.isDisplayable() || !c.isVisible() || !c.contains( x, y ) || c == mouseLayer ) - return false; + return true; // continue checking with next component - if( c.getMouseListeners().length > 0 || - c.getMouseMotionListeners().length > 0 || - c.getMouseWheelListeners().length > 0 ) - return true; + if( c.isEnabled() && + (c.getMouseListeners().length > 0 || + c.getMouseMotionListeners().length > 0) ) + { + if( !(c instanceof JComponent) ) + return false; // assume that this is not a caption because the component has mouse listeners + + // check client property boolean value + Object caption = ((JComponent)c).getClientProperty( COMPONENT_TITLE_BAR_CAPTION ); + if( caption instanceof Boolean ) + return (boolean) caption; + + // if component is not fully layouted, do not invoke function + // because it is too dangerous that the function tries to layout the component, + // which could cause a dead lock + if( !c.isValid() ) + return false; // assume that this is not a caption because the component has mouse listeners + + if( caption instanceof Function ) { + // check client property function value + @SuppressWarnings( "unchecked" ) + Function hitTest = (Function) caption; + Boolean result = hitTest.apply( new Point( x, y ) ); + if( result != null ) + return result; + } else { + // check component UI + ComponentUI ui = JavaCompatibility2.getUI( (JComponent) c ); + if( !(ui instanceof TitleBarCaptionHitTest) ) + return false; // assume that this is not a caption because the component has mouse listeners + + Boolean result = ((TitleBarCaptionHitTest)ui).isTitleBarCaptionAt( x, y ); + if( result != null ) + return result; + } + // else continue checking children + } + + // check children if( c instanceof Container ) { for( Component child : ((Container)c).getComponents() ) { - if( isComponentWithMouseListenerAt( child, x - child.getX(), y - child.getY() ) ) - return true; + if( !isTitleBarCaptionAt( child, x - child.getX(), y - child.getY() ) ) + return false; } } - return false; + return true; } - private int lastHitTestX; - private int lastHitTestY; - private long lastHitTestTime; - private boolean lastHitTestResult; + private int lastCaptionHitTestX; + private int lastCaptionHitTestY; + private long lastCaptionHitTestTime; + private boolean lastCaptionHitTestResult; private int debugTitleBarHeight; private Rectangle debugAppIconBounds; @@ -1490,4 +1517,27 @@ public void componentShown( ComponentEvent e ) { @Override public void componentMoved( ComponentEvent e ) {} @Override public void componentHidden( ComponentEvent e ) {} } + + //---- interface TitleBarCaptionHitTest ----------------------------------- + + /** + * For custom components use {@link FlatClientProperties#COMPONENT_TITLE_BAR_CAPTION} + * instead of this interface. + * + * @since 3.4 + */ + public interface TitleBarCaptionHitTest { + /** + * Invoked for a component that is enabled and has mouse listeners, + * to check whether it processes mouse input at the given x/y location. + * Useful for components that do not use mouse input on whole component bounds. + * E.g. a tabbed pane with a few tabs has some empty space beside the tabs + * that can be used to move the window. + * + * @return {@code true} if the component is not interested in mouse input at the given location + * {@code false} if the component wants process mouse input at the given location + * {@code null} if the component children should be checked + */ + Boolean isTitleBarCaptionAt( int x, int y ); + } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatToolBarUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatToolBarUI.java index c6d97f5d9..7bc13fb88 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatToolBarUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatToolBarUI.java @@ -82,7 +82,7 @@ */ public class FlatToolBarUI extends BasicToolBarUI - implements StyleableUI + implements StyleableUI, FlatTitlePane.TitleBarCaptionHitTest { /** @since 1.4 */ @Styleable protected boolean focusableButtons; /** @since 2 */ @Styleable protected boolean arrowKeysOnlyNavigation; @@ -453,6 +453,15 @@ private ButtonGroup getButtonGroup( AbstractButton b ) { : null; } + //---- interface FlatTitlePane.TitleBarCaptionHitTest ---- + + /** @since 3.4 */ + @Override + public Boolean isTitleBarCaptionAt( int x, int y ) { + // necessary because BasicToolBarUI adds some mouse listeners for dragging when toolbar is floatable + return null; // check children + } + //---- class FlatToolBarFocusTraversalPolicy ------------------------------ /** diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatWindowsNativeWindowBorder.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatWindowsNativeWindowBorder.java index 64a671a96..dda40c281 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatWindowsNativeWindowBorder.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatWindowsNativeWindowBorder.java @@ -159,7 +159,7 @@ private void uninstall( Window window ) { } @Override - public void updateTitleBarInfo( Window window, int titleBarHeight, Predicate hitTestCallback, + public void updateTitleBarInfo( Window window, int titleBarHeight, Predicate captionHitTestCallback, Rectangle appIconBounds, Rectangle minimizeButtonBounds, Rectangle maximizeButtonBounds, Rectangle closeButtonBounds ) { @@ -168,7 +168,7 @@ public void updateTitleBarInfo( Window window, int titleBarHeight, Predicate hitTestCallback; + private Predicate captionHitTestCallback; private Rectangle appIconBounds; private Rectangle minimizeButtonBounds; private Rectangle maximizeButtonBounds; @@ -376,7 +376,7 @@ private int onNcHitTest( int x, int y, boolean isOnResizeBorder ) { // that processes mouse events (e.g. buttons, menus, etc) // - Windows ignores mouse events in this area try { - if( hitTestCallback != null && hitTestCallback.test( pt ) ) + if( captionHitTestCallback != null && !captionHitTestCallback.test( pt ) ) return HTCLIENT; } catch( Throwable ex ) { // ignore diff --git a/flatlaf-jide-oss/src/main/java/com/formdev/flatlaf/jideoss/ui/FlatJideTabbedPaneUI.java b/flatlaf-jide-oss/src/main/java/com/formdev/flatlaf/jideoss/ui/FlatJideTabbedPaneUI.java index 60b5da361..f34589419 100644 --- a/flatlaf-jide-oss/src/main/java/com/formdev/flatlaf/jideoss/ui/FlatJideTabbedPaneUI.java +++ b/flatlaf-jide-oss/src/main/java/com/formdev/flatlaf/jideoss/ui/FlatJideTabbedPaneUI.java @@ -16,6 +16,7 @@ package com.formdev.flatlaf.jideoss.ui; +import static com.formdev.flatlaf.FlatClientProperties.COMPONENT_TITLE_BAR_CAPTION; import static com.formdev.flatlaf.FlatClientProperties.TABBED_PANE_HAS_FULL_BORDER; import static com.formdev.flatlaf.FlatClientProperties.TABBED_PANE_SHOW_TAB_SEPARATORS; import static com.formdev.flatlaf.FlatClientProperties.clientPropertyBoolean; @@ -30,6 +31,7 @@ import java.awt.Graphics2D; import java.awt.Insets; import java.awt.LayoutManager; +import java.awt.Point; import java.awt.Rectangle; import java.awt.Shape; import java.awt.event.MouseListener; @@ -37,6 +39,7 @@ import java.awt.geom.Path2D; import java.awt.geom.Rectangle2D; import java.beans.PropertyChangeListener; +import java.util.function.Function; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JComponent; @@ -100,6 +103,25 @@ public static ComponentUI createUI( JComponent c ) { return new FlatJideTabbedPaneUI(); } + @Override + public void installUI( JComponent c ) { + super.installUI( c ); + + c.putClientProperty( COMPONENT_TITLE_BAR_CAPTION, + (Function) pt -> { + if( tabForCoordinate( _tabPane, pt.x, pt.y ) >= 0 ) + return false; + + return null; // check children + } ); + } + + @Override + public void uninstallUI( JComponent c ) { + super.uninstallUI( c ); + c.putClientProperty( COMPONENT_TITLE_BAR_CAPTION, null ); + } + @Override protected void installDefaults() { super.installDefaults(); diff --git a/flatlaf-natives/flatlaf-natives-jna/src/main/java/com/formdev/flatlaf/natives/jna/windows/FlatWindowsNativeWindowBorder.java b/flatlaf-natives/flatlaf-natives-jna/src/main/java/com/formdev/flatlaf/natives/jna/windows/FlatWindowsNativeWindowBorder.java index 5c0d407c0..bdf31aa70 100644 --- a/flatlaf-natives/flatlaf-natives-jna/src/main/java/com/formdev/flatlaf/natives/jna/windows/FlatWindowsNativeWindowBorder.java +++ b/flatlaf-natives/flatlaf-natives-jna/src/main/java/com/formdev/flatlaf/natives/jna/windows/FlatWindowsNativeWindowBorder.java @@ -164,7 +164,7 @@ private void uninstall( Window window ) { } @Override - public void updateTitleBarInfo( Window window, int titleBarHeight, Predicate hitTestCallback, + public void updateTitleBarInfo( Window window, int titleBarHeight, Predicate captionHitTestCallback, Rectangle appIconBounds, Rectangle minimizeButtonBounds, Rectangle maximizeButtonBounds, Rectangle closeButtonBounds ) { @@ -173,7 +173,7 @@ public void updateTitleBarInfo( Window window, int titleBarHeight, Predicate hitTestCallback; + private Predicate captionHitTestCallback; private Rectangle appIconBounds; private Rectangle minimizeButtonBounds; private Rectangle maximizeButtonBounds; @@ -684,7 +684,7 @@ private LRESULT WmNcHitTest( HWND hwnd, int uMsg, WPARAM wParam, LPARAM lParam ) // that processes mouse events (e.g. buttons, menus, etc) // - Windows ignores mouse events in this area try { - if( hitTestCallback != null && hitTestCallback.test( pt ) ) + if( captionHitTestCallback != null && !captionHitTestCallback.test( pt ) ) return new LRESULT( HTCLIENT ); } catch( Throwable ex ) { // ignore