Window decorations: improved caption hit testing to better support TabbedPane, SplitPane and ToolBar in title bar area (e.g. for fullWindowContent mode)

This commit is contained in:
Karl Tauber
2024-02-04 16:30:38 +01:00
parent 1d935d6659
commit a84aceb1ba
9 changed files with 177 additions and 55 deletions

View File

@@ -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.
* <p>
* 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.
*
* <pre>{@code
* myComponent.putClientProperty( "JComponent.titleBarCaption",
* (Function<Point, Boolean>) 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
* } );
* }</pre>
* <b>Warning</b>:
* <ul>
* <li>This function is invoked often when mouse is moved over window title bar area
* and should therefore return quickly.
* <li>This function is invoked on 'AWT-Windows' thread (not 'AWT-EventQueue' thread)
* while processing Windows messages.
* It <b>must not</b> change any component property or layout because this could cause a dead lock.
* </ul>
* <p>
* <strong>Component</strong> {@link javax.swing.JComponent}<br>
* <strong>Value type</strong> {@link java.lang.Boolean}
* <strong>Value type</strong> {@link java.lang.Boolean} or {@link java.util.function.Function}&lt;Point, Boolean&gt;
*
* @since 2.5
* @deprecated No longer used since FlatLaf 3.4. Retained for API compatibility.
*/
@Deprecated
String COMPONENT_TITLE_BAR_CAPTION = "JComponent.titleBarCaption";

View File

@@ -219,13 +219,13 @@ public class FlatNativeWindowBorder
}
static void setTitleBarHeightAndHitTestSpots( Window window, int titleBarHeight,
Predicate<Point> hitTestCallback, Rectangle appIconBounds, Rectangle minimizeButtonBounds,
Predicate<Point> 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 class FlatNativeWindowBorder
{
boolean hasCustomDecoration( Window window );
void setHasCustomDecoration( Window window, boolean hasCustomDecoration );
void updateTitleBarInfo( Window window, int titleBarHeight, Predicate<Point> hitTestCallback,
void updateTitleBarInfo( Window window, int titleBarHeight, Predicate<Point> captionHitTestCallback,
Rectangle appIconBounds, Rectangle minimizeButtonBounds, Rectangle maximizeButtonBounds,
Rectangle closeButtonBounds );

View File

@@ -84,7 +84,7 @@ import com.formdev.flatlaf.util.UIScale;
*/
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 @@ public class FlatSplitPaneUI
((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

View File

@@ -182,7 +182,7 @@ import com.formdev.flatlaf.util.UIScale;
*/
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 @@ debug*/
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

View File

@@ -49,6 +49,7 @@ import java.beans.PropertyChangeEvent;
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.SwingUtilities;
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 class FlatTitlePane
}
// clear hit-test cache
lastHitTestTime = 0;
lastCaptionHitTestTime = 0;
}
} );
@@ -1004,10 +1006,10 @@ public class FlatTitlePane
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 @@ public class FlatTitlePane
: 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.
* <p>
* Note:
@@ -1046,12 +1038,12 @@ public class FlatTitlePane
* while processing Windows messages.
* </ul>
*/
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 @@ public class FlatTitlePane
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<Point, Boolean> hitTest = (Function<Point, Boolean>) 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 @@ debug*/
@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 );
}
}

View File

@@ -82,7 +82,7 @@ import com.formdev.flatlaf.util.UIScale;
*/
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 @@ public class FlatToolBarUI
: 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 ------------------------------
/**

View File

@@ -159,7 +159,7 @@ class FlatWindowsNativeWindowBorder
}
@Override
public void updateTitleBarInfo( Window window, int titleBarHeight, Predicate<Point> hitTestCallback,
public void updateTitleBarInfo( Window window, int titleBarHeight, Predicate<Point> captionHitTestCallback,
Rectangle appIconBounds, Rectangle minimizeButtonBounds, Rectangle maximizeButtonBounds,
Rectangle closeButtonBounds )
{
@@ -168,7 +168,7 @@ class FlatWindowsNativeWindowBorder
return;
wndProc.titleBarHeight = titleBarHeight;
wndProc.hitTestCallback = hitTestCallback;
wndProc.captionHitTestCallback = captionHitTestCallback;
wndProc.appIconBounds = cloneRectange( appIconBounds );
wndProc.minimizeButtonBounds = cloneRectange( minimizeButtonBounds );
wndProc.maximizeButtonBounds = cloneRectange( maximizeButtonBounds );
@@ -289,7 +289,7 @@ class FlatWindowsNativeWindowBorder
// Swing coordinates/values may be scaled on a HiDPI screen
private int titleBarHeight; // measured from window top edge, which may be out-of-screen if maximized
private Predicate<Point> hitTestCallback;
private Predicate<Point> captionHitTestCallback;
private Rectangle appIconBounds;
private Rectangle minimizeButtonBounds;
private Rectangle maximizeButtonBounds;
@@ -376,7 +376,7 @@ class FlatWindowsNativeWindowBorder
// 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

View File

@@ -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.Graphics;
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.event.MouseMotionListener;
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 class FlatJideTabbedPaneUI
return new FlatJideTabbedPaneUI();
}
@Override
public void installUI( JComponent c ) {
super.installUI( c );
c.putClientProperty( COMPONENT_TITLE_BAR_CAPTION,
(Function<Point, Boolean>) 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();

View File

@@ -164,7 +164,7 @@ public class FlatWindowsNativeWindowBorder
}
@Override
public void updateTitleBarInfo( Window window, int titleBarHeight, Predicate<Point> hitTestCallback,
public void updateTitleBarInfo( Window window, int titleBarHeight, Predicate<Point> captionHitTestCallback,
Rectangle appIconBounds, Rectangle minimizeButtonBounds, Rectangle maximizeButtonBounds,
Rectangle closeButtonBounds )
{
@@ -173,7 +173,7 @@ public class FlatWindowsNativeWindowBorder
return;
wndProc.titleBarHeight = titleBarHeight;
wndProc.hitTestCallback = hitTestCallback;
wndProc.captionHitTestCallback = captionHitTestCallback;
wndProc.appIconBounds = cloneRectange( appIconBounds );
wndProc.minimizeButtonBounds = cloneRectange( minimizeButtonBounds );
wndProc.maximizeButtonBounds = cloneRectange( maximizeButtonBounds );
@@ -351,7 +351,7 @@ public class FlatWindowsNativeWindowBorder
// Swing coordinates/values may be scaled on a HiDPI screen
private int titleBarHeight;
private Predicate<Point> hitTestCallback;
private Predicate<Point> captionHitTestCallback;
private Rectangle appIconBounds;
private Rectangle minimizeButtonBounds;
private Rectangle maximizeButtonBounds;
@@ -684,7 +684,7 @@ public class FlatWindowsNativeWindowBorder
// 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