From 1d935d6659c453e38251afa3e692ab90255f5c58 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 2 Feb 2024 10:06:25 +0100 Subject: [PATCH 1/3] Window decorations: support fullWindowContent mode on Windows and Linux --- .../formdev/flatlaf/FlatClientProperties.java | 30 ++- .../flatlaf/ui/FlatNativeWindowBorder.java | 9 +- .../formdev/flatlaf/ui/FlatRootPaneUI.java | 94 +++++-- .../com/formdev/flatlaf/ui/FlatTitlePane.java | 251 ++++++++++++------ .../ui/FlatWindowsNativeWindowBorder.java | 53 ++-- .../com/formdev/flatlaf/FlatLaf.properties | 1 - .../com/formdev/flatlaf/demo/DemoFrame.java | 89 +++++-- .../com/formdev/flatlaf/demo/DemoFrame.jfd | 35 ++- .../formdev/flatlaf/demo/icons/collapse.svg | 4 + .../com/formdev/flatlaf/demo/icons/expand.svg | 4 + .../FlatWindowsNativeWindowBorder.java | 52 ++-- .../dumps/uidefaults/FlatDarkLaf_1.8.0.txt | 1 - .../dumps/uidefaults/FlatLightLaf_1.8.0.txt | 1 - .../dumps/uidefaults/FlatMacDarkLaf_1.8.0.txt | 1 - .../uidefaults/FlatMacLightLaf_1.8.0.txt | 1 - .../dumps/uidefaults/FlatTestLaf_1.8.0.txt | 1 - .../testing/FlatWindowDecorationsTest.java | 95 ++++++- .../testing/FlatWindowDecorationsTest.jfd | 58 +++- .../flatlaf/themeeditor/FlatLafUIKeys.txt | 2 - 19 files changed, 569 insertions(+), 213 deletions(-) create mode 100644 flatlaf-demo/src/main/resources/com/formdev/flatlaf/demo/icons/collapse.svg create mode 100644 flatlaf-demo/src/main/resources/com/formdev/flatlaf/demo/icons/expand.svg 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 f2fc9fea3..75c9c33a0 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java @@ -266,7 +266,9 @@ public interface FlatClientProperties * Value type {@link java.lang.Boolean} * * @since 2.5 + * @deprecated No longer used since FlatLaf 3.4. Retained for API compatibility. */ + @Deprecated String COMPONENT_TITLE_BAR_CAPTION = "JComponent.titleBarCaption"; @@ -274,7 +276,7 @@ public interface FlatClientProperties /** * Marks the panel as placeholder for the iconfify/maximize/close buttons - * in fullWindowContent mode. + * in fullWindowContent mode. See {@link #FULL_WINDOW_CONTENT}. *

* If fullWindowContent mode is enabled, the preferred size of the panel is equal * to the size of the iconfify/maximize/close buttons. Otherwise is is {@code 0,0}. @@ -462,6 +464,32 @@ public interface FlatClientProperties */ String MENU_BAR_EMBEDDED = "JRootPane.menuBarEmbedded"; + /** + * Specifies whether the content pane (and the glass pane) should be extended + * into the window title bar area + * (requires enabled window decorations). Default is {@code false}. + *

+ * On macOS, use client property {@code apple.awt.fullWindowContent} + * (see macOS Full window content). + *

+ * Setting this enables/disables full window content + * for the {@code JFrame} or {@code JDialog} that contains the root pane. + *

+ * If {@code true}, the content pane (and the glass pane) is extended into + * the title bar area. The window icon and title are hidden. + * Only the iconfify/maximize/close buttons stay visible in the upper right corner + * (and overlap the content pane). + *

+ * The user can left-click-and-drag on the title bar area to move the window, + * except when clicking on a component that processes mouse events (e.g. buttons or menus). + *

+ * Component {@link javax.swing.JRootPane}
+ * Value type {@link java.lang.Boolean} + * + * @since 3.4 + */ + String FULL_WINDOW_CONTENT = "FlatLaf.fullWindowContent"; + /** * Contains the current bounds of the iconfify/maximize/close buttons * (in root pane coordinates) if fullWindowContent mode is enabled. 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 ded82a01a..1f4e2d073 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 @@ -21,11 +21,12 @@ import java.awt.Container; import java.awt.Graphics; import java.awt.Graphics2D; +import java.awt.Point; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.Window; import java.beans.PropertyChangeListener; -import java.util.List; +import java.util.function.Predicate; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JRootPane; @@ -218,13 +219,13 @@ public static void setHasCustomDecoration( Window window, boolean hasCustomDecor } static void setTitleBarHeightAndHitTestSpots( Window window, int titleBarHeight, - List hitTestSpots, Rectangle appIconBounds, Rectangle minimizeButtonBounds, + Predicate hitTestCallback, Rectangle appIconBounds, Rectangle minimizeButtonBounds, Rectangle maximizeButtonBounds, Rectangle closeButtonBounds ) { if( !isSupported() ) return; - nativeProvider.updateTitleBarInfo( window, titleBarHeight, hitTestSpots, + nativeProvider.updateTitleBarInfo( window, titleBarHeight, hitTestCallback, appIconBounds, minimizeButtonBounds, maximizeButtonBounds, closeButtonBounds ); } @@ -270,7 +271,7 @@ public interface Provider { boolean hasCustomDecoration( Window window ); void setHasCustomDecoration( Window window, boolean hasCustomDecoration ); - void updateTitleBarInfo( Window window, int titleBarHeight, List hitTestSpots, + void updateTitleBarInfo( Window window, int titleBarHeight, Predicate hitTestCallback, Rectangle appIconBounds, Rectangle minimizeButtonBounds, Rectangle maximizeButtonBounds, Rectangle closeButtonBounds ); diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatRootPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatRootPaneUI.java index 2edef9e9b..a2dfab1c5 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatRootPaneUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatRootPaneUI.java @@ -269,15 +269,28 @@ protected FlatTitlePane createTitlePane() { // 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; + private final static Integer TITLE_PANE_MOUSE_LAYER = JLayeredPane.FRAME_CONTENT_LAYER - 2; + + // for fullWindowContent mode, layer title pane over frame content layer to allow placing title bar buttons over content + /** @since 3.4 */ + protected final static Integer TITLE_PANE_FULL_WINDOW_CONTENT_LAYER = JLayeredPane.FRAME_CONTENT_LAYER + 1; + + private Integer getLayerForTitlePane() { + return isFullWindowContent( rootPane ) ? TITLE_PANE_FULL_WINDOW_CONTENT_LAYER : TITLE_PANE_LAYER; + } protected void setTitlePane( FlatTitlePane newTitlePane ) { JLayeredPane layeredPane = rootPane.getLayeredPane(); - if( titlePane != null ) + if( titlePane != null ) { layeredPane.remove( titlePane ); + layeredPane.remove( titlePane.mouseLayer ); + } - if( newTitlePane != null ) - layeredPane.add( newTitlePane, TITLE_PANE_LAYER ); + if( newTitlePane != null ) { + layeredPane.add( newTitlePane, getLayerForTitlePane() ); + layeredPane.add( newTitlePane.mouseLayer, TITLE_PANE_MOUSE_LAYER ); + } titlePane = newTitlePane; } @@ -430,6 +443,17 @@ public void propertyChange( PropertyChangeEvent e ) { titlePane.titleBarColorsChanged(); break; + case FlatClientProperties.FULL_WINDOW_CONTENT: + if( titlePane != null ) { + rootPane.getLayeredPane().setLayer( titlePane, getLayerForTitlePane() ); + titlePane.updateIcon(); + titlePane.updateVisibility(); + titlePane.updateFullWindowContentButtonsBoundsProperty(); + } + FullWindowContentSupport.revalidatePlaceholders( rootPane ); + rootPane.revalidate(); + break; + case FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS: FullWindowContentSupport.revalidatePlaceholders( rootPane ); break; @@ -471,11 +495,14 @@ public void propertyChange( PropertyChangeEvent e ) { } } + /** @since 3.4 */ + protected static boolean isFullWindowContent( JRootPane rootPane ) { + return FlatClientProperties.clientPropertyBoolean( rootPane, FlatClientProperties.FULL_WINDOW_CONTENT, false ); + } + protected static boolean isMenuBarEmbedded( JRootPane rootPane ) { - RootPaneUI ui = rootPane.getUI(); - return ui instanceof FlatRootPaneUI && - ((FlatRootPaneUI)ui).titlePane != null && - ((FlatRootPaneUI)ui).titlePane.isMenuBarEmbedded(); + FlatTitlePane titlePane = getTitlePane( rootPane ); + return titlePane != null && titlePane.isMenuBarEmbedded(); } /** @since 2.4 */ @@ -511,23 +538,21 @@ public Dimension maximumLayoutSize( Container parent ) { private Dimension computeLayoutSize( Container parent, Function 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(); + : rootPane.getSize(); // same as in JRootPane.RootLayout.preferredLayoutSize() int width = contentSize.width; // title pane width is not considered here - int height = titlePaneSize.height + contentSize.height; + int height = contentSize.height; + if( titlePane != null && !isFullWindowContent( rootPane ) ) + height += getSizeFunc.apply( titlePane ).height; if( titlePane == null || !titlePane.isMenuBarEmbedded() ) { JMenuBar menuBar = rootPane.getJMenuBar(); - Dimension menuBarSize = (menuBar != null && menuBar.isVisible()) - ? getSizeFunc.apply( menuBar ) - : new Dimension(); - - width = Math.max( width, menuBarSize.width ); - height += menuBarSize.height; + if( menuBar != null && menuBar.isVisible() ) { + Dimension menuBarSize = getSizeFunc.apply( menuBar ); + width = Math.max( width, menuBarSize.width ); + height += menuBarSize.height; + } } Insets insets = rootPane.getInsets(); @@ -552,12 +577,23 @@ public void layoutContainer( Container parent ) { if( rootPane.getLayeredPane() != null ) rootPane.getLayeredPane().setBounds( x, y, width, height ); - // title pane + // title pane (is a child of layered pane) int nextY = 0; if( titlePane != null ) { int prefHeight = !isFullScreen ? titlePane.getPreferredSize().height : 0; - titlePane.setBounds( 0, 0, width, prefHeight ); - nextY += prefHeight; + boolean isFullWindowContent = isFullWindowContent( rootPane ); + if( isFullWindowContent && !UIManager.getBoolean( FlatTitlePane.KEY_DEBUG_SHOW_RECTANGLES ) ) { + // place title bar into top-right corner + int tw = Math.min( titlePane.getPreferredSize().width, width ); + int tx = titlePane.getComponentOrientation().isLeftToRight() ? width - tw : 0; + titlePane.setBounds( tx, 0, tw, prefHeight ); + } else + titlePane.setBounds( 0, 0, width, prefHeight ); + + titlePane.mouseLayer.setBounds( 0, 0, width, prefHeight ); + + if( !isFullWindowContent ) + nextY += prefHeight; } // glass pane @@ -568,7 +604,7 @@ public void layoutContainer( Container parent ) { rootPane.getGlassPane().setBounds( x, y + offset, width, height - offset ); } - // menu bar + // menu bar (is a child of layered pane) JMenuBar menuBar = rootPane.getJMenuBar(); if( menuBar != null && menuBar.isVisible() ) { boolean embedded = !isFullScreen && titlePane != null && titlePane.isMenuBarEmbedded(); @@ -576,13 +612,23 @@ public void layoutContainer( Container parent ) { titlePane.validate(); menuBar.setBounds( titlePane.getMenuBarBounds() ); } else { + int mx = 0; + int mw = width; + if( titlePane != null && isFullWindowContent( rootPane ) ) { + // make menu bar width smaller to avoid that it overlaps title bar buttons + int tw = Math.min( titlePane.getPreferredSize().width, width ); + mw -= tw; + if( !titlePane.getComponentOrientation().isLeftToRight() ) + mx = tw; + } + Dimension prefSize = menuBar.getPreferredSize(); - menuBar.setBounds( 0, nextY, width, prefSize.height ); + menuBar.setBounds( mx, nextY, mw, prefSize.height ); nextY += prefSize.height; } } - // content pane + // content pane (is a child of layered pane) Container contentPane = rootPane.getContentPane(); if( contentPane != null ) contentPane.setBounds( 0, nextY, width, Math.max( height - nextY, 0 ) ); 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 4beae363f..c9af7656b 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 @@ -36,6 +36,7 @@ import java.awt.Toolkit; import java.awt.Window; import java.awt.event.ActionListener; +import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.MouseEvent; @@ -46,7 +47,6 @@ 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; @@ -57,7 +57,6 @@ import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JDialog; -import javax.swing.JInternalFrame; import javax.swing.JLabel; import javax.swing.JMenuBar; import javax.swing.JPanel; @@ -98,7 +97,6 @@ * @uiDefault TitlePane.showIconBesideTitle boolean * @uiDefault TitlePane.menuBarTitleGap int * @uiDefault TitlePane.menuBarTitleMinimumGap int - * @uiDefault TitlePane.menuBarResizeHeight int * @uiDefault TitlePane.closeIcon Icon * @uiDefault TitlePane.iconifyIcon Icon * @uiDefault TitlePane.maximizeIcon Icon @@ -109,7 +107,7 @@ public class FlatTitlePane extends JComponent { - private static final String KEY_DEBUG_SHOW_RECTANGLES = "FlatLaf.debug.titlebar.showRectangles"; + static final String KEY_DEBUG_SHOW_RECTANGLES = "FlatLaf.debug.titlebar.showRectangles"; /** @since 2.5 */ protected final Font titleFont; protected final Color activeBackground; @@ -131,7 +129,6 @@ public class FlatTitlePane /** @since 2.4 */ protected final boolean showIconBesideTitle; protected final int menuBarTitleGap; /** @since 2.4 */ protected final int menuBarTitleMinimumGap; - /** @since 2.4 */ protected final int menuBarResizeHeight; protected final JRootPane rootPane; protected final String windowStyle; @@ -150,6 +147,23 @@ public class FlatTitlePane private final Handler handler; + /** + * This panel handles mouse events if FlatLaf window decorations are used + * without native window border. E.g. on Linux. + *

+ * This panel usually has same bounds as the title pane, + * except if fullWindowContent mode is enabled. + *

+ * This panel is not a child of the title pane. + * Instead it is added by FlatRootPaneUI to the layered pane at a layer + * under the title pane and under the frame content. + * The separation is necessary for fullWindowContent mode, where the title pane + * is layered over the frame content (for title pane buttons), but the mousePanel + * needs to be layered under the frame content so that components on content pane + * can receive mouse events when located in title area. + */ + final JPanel mouseLayer; + public FlatTitlePane( JRootPane rootPane ) { this.rootPane = rootPane; @@ -178,7 +192,6 @@ public FlatTitlePane( JRootPane rootPane ) { showIconBesideTitle = FlatUIUtils.getSubUIBoolean( "TitlePane.showIconBesideTitle", windowStyle, false ); menuBarTitleGap = FlatUIUtils.getSubUIInt( "TitlePane.menuBarTitleGap", windowStyle, 40 ); menuBarTitleMinimumGap = FlatUIUtils.getSubUIInt( "TitlePane.menuBarTitleMinimumGap", windowStyle, 12 ); - menuBarResizeHeight = FlatUIUtils.getSubUIInt( "TitlePane.menuBarResizeHeight", windowStyle, 4 ); handler = createHandler(); @@ -187,11 +200,10 @@ public FlatTitlePane( JRootPane rootPane ) { addSubComponents(); activeChanged( true ); - addMouseListener( handler ); - addMouseMotionListener( handler ); - - // necessary for closing window with double-click on icon - iconLabel.addMouseListener( handler ); + mouseLayer = new JPanel(); + mouseLayer.setOpaque( false ); + mouseLayer.addMouseListener( handler ); + mouseLayer.addMouseMotionListener( handler ); applyComponentOrientation( rootPane.getComponentOrientation() ); } @@ -234,6 +246,11 @@ public Dimension getPreferredSize() { setLayout( new BorderLayout() { @Override public void layoutContainer( Container target ) { + if( isFullWindowContent() ) { + super.layoutContainer( target ); + return; + } + // compute available bounds Insets insets = target.getInsets(); int x = insets.left; @@ -247,7 +264,7 @@ public void layoutContainer( Container target ) { int titleWidth = w - leftWidth - buttonsWidth; int minTitleWidth = UIScale.scale( titleMinimumWidth ); - // increase minimum width if icon is show besides the title + // increase minimum width if icon is shown besides the title Icon icon = titleLabel.getIcon(); if( icon != null ) { Insets iconInsets = iconLabel.getInsets(); @@ -295,6 +312,9 @@ public void layoutContainer( Container target ) { horizontalGlue.getWidth(), titleLabel.getHeight() ); } } + + // clear hit-test cache + lastHitTestTime = 0; } } ); @@ -337,6 +357,10 @@ public Dimension getPreferredSize() { buttonPanel.add( maximizeButton ); buttonPanel.add( restoreButton ); } + buttonPanel.addComponentListener( new ComponentAdapter() { + @Override public void componentResized( ComponentEvent e ) { updateFullWindowContentButtonsBoundsProperty(); } + @Override public void componentMoved( ComponentEvent e ) { updateFullWindowContentButtonsBoundsProperty(); } + } ); buttonPanel.add( closeButton ); } @@ -417,7 +441,9 @@ protected void frameStateChanged() { /** @since 3 */ protected void updateVisibility() { - titleLabel.setVisible( clientPropertyBoolean( rootPane, TITLE_BAR_SHOW_TITLE, true ) ); + boolean isFullWindowContent = isFullWindowContent(); + leftPanel.setVisible( !isFullWindowContent ); + titleLabel.setVisible( clientPropertyBoolean( rootPane, TITLE_BAR_SHOW_TITLE, true ) && !isFullWindowContent ); closeButton.setVisible( clientPropertyBoolean( rootPane, TITLE_BAR_SHOW_CLOSE, true ) ); if( window instanceof Frame ) { @@ -443,7 +469,7 @@ protected void updateIcon() { // get window images List images = null; - if( clientPropertyBoolean( rootPane, TITLE_BAR_SHOW_ICON, defaultShowIcon ) ) { + if( clientPropertyBoolean( rootPane, TITLE_BAR_SHOW_ICON, defaultShowIcon ) && !isFullWindowContent() ) { images = window.getIconImages(); if( images.isEmpty() ) { // search in owners @@ -468,6 +494,13 @@ protected void updateIcon() { updateNativeTitleBarHeightAndHitTestSpotsLater(); } + void updateFullWindowContentButtonsBoundsProperty() { + Rectangle bounds = isFullWindowContent() + ? new Rectangle( SwingUtilities.convertPoint( buttonPanel, 0, 0, rootPane ), buttonPanel.getSize() ) + : null; + rootPane.putClientProperty( FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS, bounds ); + } + @Override public void addNotify() { super.addNotify(); @@ -522,6 +555,11 @@ protected void uninstallWindowListeners() { window.removeComponentListener( handler ); } + /** @since 3.4 */ + protected boolean isFullWindowContent() { + return FlatRootPaneUI.isFullWindowContent( rootPane ); + } + /** * Returns whether this title pane currently has a visible and embedded menubar. */ @@ -533,6 +571,9 @@ protected boolean hasVisibleEmbeddedMenuBar( JMenuBar menuBar ) { * Returns whether the menubar should be embedded into the title pane. */ protected boolean isMenuBarEmbedded() { + if( isFullWindowContent() ) + return false; + // not storing value of "TitlePane.menuBarEmbedded" in class to allow changing at runtime return FlatUIUtils.getBoolean( rootPane, FlatSystemProperties.MENUBAR_EMBEDDED, @@ -620,21 +661,45 @@ public void paint( Graphics g ) { return; if( debugTitleBarHeight > 0 ) { + // title bar height is measured from window top edge + int y = SwingUtilities.convertPoint( window, 0, debugTitleBarHeight, this ).y; g.setColor( Color.green ); - g.drawLine( 0, debugTitleBarHeight, getWidth(), debugTitleBarHeight ); + g.drawLine( 0, y, getWidth(), y ); } - if( debugHitTestSpots != null ) { - for( Rectangle r : debugHitTestSpots ) - paintRect( g, Color.red, r ); + + g.setColor( Color.red ); + debugPaintComponentWithMouseListener( g, Color.red, rootPane.getLayeredPane(), 0, 0 ); + + debugPaintRect( g, Color.blue, debugAppIconBounds ); + debugPaintRect( g, Color.blue, debugMinimizeButtonBounds ); + debugPaintRect( g, Color.magenta, debugMaximizeButtonBounds ); + debugPaintRect( g, Color.cyan, debugCloseButtonBounds ); + } + + private void debugPaintComponentWithMouseListener( Graphics g, Color color, Component c, int x, int y ) { + if( !c.isDisplayable() || !c.isVisible() || c == mouseLayer || + c == iconifyButton || c == maximizeButton || c == restoreButton || c == closeButton ) + return; + + if( c.getMouseListeners().length > 0 || + c.getMouseMotionListeners().length > 0 || + c.getMouseWheelListeners().length > 0 ) + { + g.drawRect( x, y, c.getWidth(), c.getHeight() ); + return; + } + + if( c instanceof Container ) { + Rectangle titlePaneBoundsOnWindow = SwingUtilities.convertRectangle( this, new Rectangle( getSize() ), window ); + for( Component child : ((Container)c).getComponents() ) { + Rectangle compBoundsOnWindow = SwingUtilities.convertRectangle( c, new Rectangle( c.getSize() ), window ); + if( compBoundsOnWindow.intersects( titlePaneBoundsOnWindow ) ) + debugPaintComponentWithMouseListener( g, color, child, x + child.getX(), y + child.getY() ); + } } - paintRect( g, Color.cyan, debugCloseButtonBounds ); - paintRect( g, Color.blue, debugAppIconBounds ); - paintRect( g, Color.blue, debugMinimizeButtonBounds ); - paintRect( g, Color.magenta, debugMaximizeButtonBounds ); - paintRect( g, Color.cyan, debugCloseButtonBounds ); } - private void paintRect( Graphics g, Color color, Rectangle r ) { + private void debugPaintRect( Graphics g, Color color, Rectangle r ) { if( r == null ) return; @@ -645,6 +710,9 @@ private void paintRect( Graphics g, Color color, Rectangle r ) { @Override protected void paintComponent( Graphics g ) { + if( isFullWindowContent() ) + return; + // not storing value of "TitlePane.unifiedBackground" in class to allow changing at runtime g.setColor( (UIManager.getBoolean( "TitlePane.unifiedBackground" ) && clientPropertyColor( rootPane, TITLE_BAR_BACKGROUND, null ) == null) @@ -866,11 +934,14 @@ protected void updateNativeTitleBarHeightAndHitTestSpots() { return; int titleBarHeight = getHeight(); + // title bar height must be measured from window top edge + // (when window is maximized, window y location is e.g. -11 and window top inset is 11) + for( Component c = this; c != window && c != null; c = c.getParent() ) + titleBarHeight += c.getY(); // slightly reduce height so that component receives mouseExit events if( titleBarHeight > 0 ) titleBarHeight--; - List hitTestSpots = new ArrayList<>(); Rectangle appIconBounds = null; if( !showIconBesideTitle && iconLabel.isVisible() ) { @@ -928,71 +999,17 @@ protected void updateNativeTitleBarHeightAndHitTestSpots() { } } - Rectangle r = getNativeHitTestSpot( buttonPanel ); - if( r != null ) - hitTestSpots.add( r ); - - JMenuBar menuBar = rootPane.getJMenuBar(); - if( hasVisibleEmbeddedMenuBar( menuBar ) ) { - r = getNativeHitTestSpot( menuBar ); - if( r != null ) { - // if frame is resizable and not maximized, make menu bar hit test spot smaller at top - // to have a small area above the menu bar to resize the window - if( window instanceof Frame && ((Frame)window).isResizable() && !isWindowMaximized() ) { - // limit to 8, because Windows does not use a larger height - int resizeHeight = UIScale.scale( Math.min( menuBarResizeHeight, 8 ) ); - r.y += resizeHeight; - r.height -= resizeHeight; - } - - int count = menuBar.getComponentCount(); - for( int i = count - 1; i >= 0; i-- ) { - Component c = menuBar.getComponent( i ); - if( c instanceof Box.Filler || - (c instanceof JComponent && clientPropertyBoolean( (JComponent) c, COMPONENT_TITLE_BAR_CAPTION, false ) ) ) - { - // If menu bar is embedded and contains a horizontal glue or caption component, - // then split the hit test spot so that - // the glue/caption component area can be used to move the window. - - Point glueLocation = SwingUtilities.convertPoint( c, 0, 0, window ); - int x2 = glueLocation.x + c.getWidth(); - Rectangle r2; - if( getComponentOrientation().isLeftToRight() ) { - r2 = new Rectangle( x2, r.y, (r.x + r.width) - x2, r.height ); - - r.width = glueLocation.x - r.x; - } else { - r2 = new Rectangle( r.x, r.y, glueLocation.x - r.x, r.height ); - - r.width = (r.x + r.width) - x2; - r.x = x2; - } - if( r2.width > 0 ) - hitTestSpots.add( r2 ); - } - } - - hitTestSpots.add( r ); - } - } - - // allow internal frames in layered pane to be moved/resized when placed over title bar - for( Component c : rootPane.getLayeredPane().getComponents() ) { - r = (c instanceof JInternalFrame) ? getNativeHitTestSpot( (JInternalFrame) c ) : null; - if( r != null ) - hitTestSpots.add( r ); - } - Rectangle minimizeButtonBounds = boundsInWindow( iconifyButton ); Rectangle maximizeButtonBounds = boundsInWindow( maximizeButton.isVisible() ? maximizeButton : restoreButton ); Rectangle closeButtonBounds = boundsInWindow( closeButton ); + // clear hit-test cache + lastHitTestTime = 0; + FlatNativeWindowBorder.setTitleBarHeightAndHitTestSpots( window, titleBarHeight, - hitTestSpots, appIconBounds, minimizeButtonBounds, maximizeButtonBounds, closeButtonBounds ); + this::hitTest, appIconBounds, minimizeButtonBounds, maximizeButtonBounds, closeButtonBounds ); debugTitleBarHeight = titleBarHeight; - debugHitTestSpots = hitTestSpots; debugAppIconBounds = appIconBounds; debugMinimizeButtonBounds = minimizeButtonBounds; debugMaximizeButtonBounds = maximizeButtonBounds; @@ -1017,8 +1034,66 @@ protected Rectangle getNativeHitTestSpot( JComponent c ) { return r; } + /** + * Returns wheter there is a component at the given location, that processes + * mouse events. E.g. buttons, menus, etc. + *

+ * Note: + *

+ */ + private boolean hitTest( 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; + } + + // convert pt from window coordinates to layeredPane coordinates + Component layeredPane = rootPane.getLayeredPane(); + int x = pt.x; + int y = pt.y; + for( Component c = layeredPane; c != window && c != null; c = c.getParent() ) { + x -= c.getX(); + y -= c.getY(); + } + + lastHitTestX = pt.x; + lastHitTestY = pt.y; + lastHitTestTime = time; + lastHitTestResult = isComponentWithMouseListenerAt( layeredPane, x, y ); + return lastHitTestResult; + } + + private boolean isComponentWithMouseListenerAt( Component c, int x, int y ) { + if( !c.isDisplayable() || !c.isVisible() || !c.contains( x, y ) || c == mouseLayer ) + return false; + + if( c.getMouseListeners().length > 0 || + c.getMouseMotionListeners().length > 0 || + c.getMouseWheelListeners().length > 0 ) + return true; + + if( c instanceof Container ) { + for( Component child : ((Container)c).getComponents() ) { + if( isComponentWithMouseListenerAt( child, x - child.getX(), y - child.getY() ) ) + return true; + } + } + return false; + } + + private int lastHitTestX; + private int lastHitTestY; + private long lastHitTestTime; + private boolean lastHitTestResult; + private int debugTitleBarHeight; - private List debugHitTestSpots; private Rectangle debugAppIconBounds; private Rectangle debugMinimizeButtonBounds; private Rectangle debugMaximizeButtonBounds; @@ -1116,7 +1191,7 @@ protected String layoutCL( JLabel label, FontMetrics fontMetrics, String text, I } } - // compute icon width and gap (if icon is show besides the title) + // compute icon width and gap (if icon is shown besides the title) int iconTextGap = 0; int iconWidthAndGap = 0; if( icon != null ) { @@ -1125,7 +1200,7 @@ protected String layoutCL( JLabel label, FontMetrics fontMetrics, String text, I iconWidthAndGap = icon.getIconWidth() + iconTextGap; } - // layout title and icon (if show besides the title) + // layout title and icon (if shown besides the title) String clippedText = SwingUtilities.layoutCompoundLabel( label, fontMetrics, text, icon, label.getVerticalAlignment(), label.getHorizontalAlignment(), label.getVerticalTextPosition(), label.getHorizontalTextPosition(), @@ -1275,7 +1350,7 @@ public void mouseClicked( MouseEvent e ) { } if( e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton( e ) ) { - if( e.getSource() == iconLabel ) { + if( SwingUtilities.getDeepestComponentAt( FlatTitlePane.this, e.getX(), e.getY() ) == iconLabel ) { // double-click on icon closes window close(); } else if( !hasNativeCustomDecoration() ) { @@ -1302,7 +1377,7 @@ public void mousePressed( MouseEvent e ) { if( !SwingUtilities.isLeftMouseButton( e ) ) return; - dragOffset = SwingUtilities.convertPoint( FlatTitlePane.this, e.getPoint(), window ); + dragOffset = SwingUtilities.convertPoint( mouseLayer, e.getPoint(), window ); linuxNativeMove = false; // on Linux, move or maximize/restore window 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 2fed26182..64a671a96 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 @@ -29,8 +29,8 @@ import java.beans.PropertyChangeListener; import java.util.Collections; import java.util.IdentityHashMap; -import java.util.List; import java.util.Map; +import java.util.function.Predicate; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.Timer; @@ -159,7 +159,7 @@ private void uninstall( Window window ) { } @Override - public void updateTitleBarInfo( Window window, int titleBarHeight, List hitTestSpots, + public void updateTitleBarInfo( Window window, int titleBarHeight, Predicate hitTestCallback, Rectangle appIconBounds, Rectangle minimizeButtonBounds, Rectangle maximizeButtonBounds, Rectangle closeButtonBounds ) { @@ -168,7 +168,7 @@ public void updateTitleBarInfo( Window window, int titleBarHeight, List hitTestCallback; private Rectangle appIconBounds; private Rectangle minimizeButtonBounds; private Rectangle maximizeButtonBounds; @@ -340,50 +340,61 @@ private void updateWindowBackground() { private int onNcHitTest( int x, int y, boolean isOnResizeBorder ) { // scale-down mouse x/y because Swing coordinates/values may be scaled on a HiDPI screen Point pt = scaleDown( x, y ); - int sx = pt.x; - int sy = pt.y; // return HTSYSMENU if mouse is over application icon // - left-click on HTSYSMENU area shows system menu // - double-left-click sends WM_CLOSE - if( contains( appIconBounds, sx, sy ) ) + if( contains( appIconBounds, pt ) ) return HTSYSMENU; // return HTMINBUTTON if mouse is over minimize button // - hovering mouse over HTMINBUTTON area shows tooltip on Windows 10/11 - if( contains( minimizeButtonBounds, sx, sy ) ) + if( contains( minimizeButtonBounds, pt ) ) return HTMINBUTTON; // return HTMAXBUTTON if mouse is over maximize/restore button // - hovering mouse over HTMAXBUTTON area shows tooltip on Windows 10 // - hovering mouse over HTMAXBUTTON area shows snap layouts menu on Windows 11 // https://docs.microsoft.com/en-us/windows/apps/desktop/modernize/apply-snap-layout-menu - if( contains( maximizeButtonBounds, sx, sy ) ) + if( contains( maximizeButtonBounds, pt ) ) return HTMAXBUTTON; // return HTCLOSE if mouse is over close button // - hovering mouse over HTCLOSE area shows tooltip on Windows 10/11 - if( contains( closeButtonBounds, sx, sy ) ) + if( contains( closeButtonBounds, pt ) ) return HTCLOSE; - boolean isOnTitleBar = (sy < titleBarHeight); + // return HTTOP if mouse is over top resize border + // - hovering mouse shows vertical resize cursor + // - left-click and drag vertically resizes window + if( isOnResizeBorder ) + return HTTOP; + boolean isOnTitleBar = (pt.y < titleBarHeight); if( isOnTitleBar ) { - // use a second reference to the array to avoid that it can be changed - // in another thread while processing the array - Rectangle[] hitTestSpots2 = hitTestSpots; - for( Rectangle spot : hitTestSpots2 ) { - if( spot.contains( sx, sy ) ) + // return HTCLIENT if mouse is over any Swing component in title bar + // that processes mouse events (e.g. buttons, menus, etc) + // - Windows ignores mouse events in this area + try { + if( hitTestCallback != null && hitTestCallback.test( pt ) ) return HTCLIENT; + } catch( Throwable ex ) { + // ignore } - return isOnResizeBorder ? HTTOP : HTCAPTION; + + // return HTCAPTION if mouse is over title bar + // - right-click shows system menu + // - double-left-click maximizes/restores window size + return HTCAPTION; } - return isOnResizeBorder ? HTTOP : HTCLIENT; + // return HTCLIENT + // - Windows ignores mouse events in this area + return HTCLIENT; } - private boolean contains( Rectangle rect, int x, int y ) { - return (rect != null && rect.contains( x, y ) ); + private boolean contains( Rectangle rect, Point pt ) { + return (rect != null && rect.contains( pt ) ); } /** diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties index b26bafe39..204d81f11 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties @@ -829,7 +829,6 @@ TitlePane.centerTitleIfMenuBarEmbedded = true TitlePane.showIconBesideTitle = false TitlePane.menuBarTitleGap = 40 TitlePane.menuBarTitleMinimumGap = 12 -TitlePane.menuBarResizeHeight = 4 TitlePane.closeIcon = com.formdev.flatlaf.icons.FlatWindowCloseIcon TitlePane.iconifyIcon = com.formdev.flatlaf.icons.FlatWindowIconifyIcon TitlePane.maximizeIcon = com.formdev.flatlaf.icons.FlatWindowMaximizeIcon diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java index eef1cf098..e9a9171a7 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java @@ -73,6 +73,7 @@ class DemoFrame initComponents(); updateFontMenuItems(); initAccentColors(); + initFullWindowContent(); controlBar.initialize( this, tabbedPane ); setIconImages( FlatSVGUtils.createWindowIconImages( "/com/formdev/flatlaf/demo/FlatLaf.svg" ) ); @@ -101,9 +102,6 @@ class DemoFrame rootPane.putClientProperty( "apple.awt.windowTitleVisible", false ); else setTitle( null ); - - // uncomment this line to see title bar buttons placeholders in fullWindowContent mode -// UIManager.put( "FlatLaf.debug.panel.showPlaceholders", true ); } // enable full screen mode for this window (for Java 8 - 10; not necessary for Java 11+) @@ -463,9 +461,37 @@ private void updateAccentColorButtons() { accentColorButtons[i].setVisible( isAccentColorSupported ); } + private void initFullWindowContent() { + if( !supportsFlatLafWindowDecorations() ) + return; + + // create fullWindowContent mode toggle button + Icon expandIcon = new FlatSVGIcon( "com/formdev/flatlaf/demo/icons/expand.svg" ); + Icon collapseIcon = new FlatSVGIcon( "com/formdev/flatlaf/demo/icons/collapse.svg" ); + JToggleButton fullWindowContentButton = new JToggleButton( expandIcon ); + fullWindowContentButton.setToolTipText( "Toggle full window content" ); + fullWindowContentButton.addActionListener( e -> { + boolean fullWindowContent = fullWindowContentButton.isSelected(); + fullWindowContentButton.setIcon( fullWindowContent ? collapseIcon : expandIcon ); + menuBar.setVisible( !fullWindowContent ); + toolBar.setVisible( !fullWindowContent ); + getRootPane().putClientProperty( FlatClientProperties.FULL_WINDOW_CONTENT, fullWindowContent ); + } ); + + // add fullWindowContent mode toggle button to tabbed pane + JToolBar trailingToolBar = new JToolBar(); + trailingToolBar.add( Box.createGlue() ); + trailingToolBar.add( fullWindowContentButton ); + tabbedPane.putClientProperty( FlatClientProperties.TABBED_PANE_TRAILING_COMPONENT, trailingToolBar ); + } + + private boolean supportsFlatLafWindowDecorations() { + return FlatLaf.supportsNativeWindowDecorations() || (SystemInfo.isLinux && JFrame.isDefaultLookAndFeelDecorated()); + } + private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents - JMenuBar menuBar1 = new JMenuBar(); + menuBar = new JMenuBar(); JMenu fileMenu = new JMenu(); JMenuItem newMenuItem = new JMenuItem(); JMenuItem openMenuItem = new JMenuItem(); @@ -528,8 +554,10 @@ private void initComponents() { DataComponentsPanel dataComponentsPanel = new DataComponentsPanel(); TabsPanel tabsPanel = new TabsPanel(); OptionPanePanel optionPanePanel = new OptionPanePanel(); - ExtrasPanel extrasPanel1 = new ExtrasPanel(); + ExtrasPanel extrasPanel = new ExtrasPanel(); controlBar = new ControlBar(); + JPanel themesPanelPanel = new JPanel(); + JPanel winFullWindowContentButtonsPlaceholder = new JPanel(); themesPanel = new IJThemesPanel(); //======== this ======== @@ -538,7 +566,7 @@ private void initComponents() { Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout()); - //======== menuBar1 ======== + //======== menuBar ======== { //======== fileMenu ======== @@ -583,7 +611,7 @@ private void initComponents() { exitMenuItem.addActionListener(e -> exitActionPerformed()); fileMenu.add(exitMenuItem); } - menuBar1.add(fileMenu); + menuBar.add(fileMenu); //======== editMenu ======== { @@ -636,7 +664,7 @@ private void initComponents() { deleteMenuItem.addActionListener(e -> menuItemActionPerformed(e)); editMenu.add(deleteMenuItem); } - menuBar1.add(editMenu); + menuBar.add(editMenu); //======== viewMenu ======== { @@ -736,7 +764,7 @@ private void initComponents() { radioButtonMenuItem3.addActionListener(e -> menuItemActionPerformed(e)); viewMenu.add(radioButtonMenuItem3); } - menuBar1.add(viewMenu); + menuBar.add(viewMenu); //======== fontMenu ======== { @@ -760,7 +788,7 @@ private void initComponents() { decrFontMenuItem.addActionListener(e -> decrFont()); fontMenu.add(decrFontMenuItem); } - menuBar1.add(fontMenu); + menuBar.add(fontMenu); //======== optionsMenu ======== { @@ -812,7 +840,7 @@ private void initComponents() { showUIDefaultsInspectorMenuItem.addActionListener(e -> showUIDefaultsInspector()); optionsMenu.add(showUIDefaultsInspectorMenuItem); } - menuBar1.add(optionsMenu); + menuBar.add(optionsMenu); //======== helpMenu ======== { @@ -825,9 +853,9 @@ private void initComponents() { aboutMenuItem.addActionListener(e -> aboutActionPerformed()); helpMenu.add(aboutMenuItem); } - menuBar1.add(helpMenu); + menuBar.add(helpMenu); } - setJMenuBar(menuBar1); + setJMenuBar(menuBar); //======== toolBarPanel ======== { @@ -884,7 +912,7 @@ private void initComponents() { } toolBarPanel.add(toolBar, BorderLayout.CENTER); } - contentPane.add(toolBarPanel, BorderLayout.NORTH); + contentPane.add(toolBarPanel, BorderLayout.PAGE_START); //======== contentPanel ======== { @@ -904,13 +932,25 @@ private void initComponents() { tabbedPane.addTab("Data Components", dataComponentsPanel); tabbedPane.addTab("Tabs", tabsPanel); tabbedPane.addTab("Option Pane", optionPanePanel); - tabbedPane.addTab("Extras", extrasPanel1); + tabbedPane.addTab("Extras", extrasPanel); } contentPanel.add(tabbedPane, "cell 0 0"); } contentPane.add(contentPanel, BorderLayout.CENTER); - contentPane.add(controlBar, BorderLayout.SOUTH); - contentPane.add(themesPanel, BorderLayout.EAST); + contentPane.add(controlBar, BorderLayout.PAGE_END); + + //======== themesPanelPanel ======== + { + themesPanelPanel.setLayout(new BorderLayout()); + + //======== winFullWindowContentButtonsPlaceholder ======== + { + winFullWindowContentButtonsPlaceholder.setLayout(new FlowLayout()); + } + themesPanelPanel.add(winFullWindowContentButtonsPlaceholder, BorderLayout.NORTH); + themesPanelPanel.add(themesPanel, BorderLayout.CENTER); + } + contentPane.add(themesPanelPanel, BorderLayout.LINE_END); //---- buttonGroup1 ---- ButtonGroup buttonGroup1 = new ButtonGroup(); @@ -925,8 +965,8 @@ private void initComponents() { usersButton.setButtonType( ButtonType.toolBarButton ); usersButton.setFocusable( false ); usersButton.addActionListener( e -> JOptionPane.showMessageDialog( null, "Hello User! How are you?", "User", JOptionPane.INFORMATION_MESSAGE ) ); - menuBar1.add( Box.createGlue() ); - menuBar1.add( usersButton ); + menuBar.add( Box.createGlue() ); + menuBar.add( usersButton ); cutMenuItem.addActionListener( new DefaultEditorKit.CutAction() ); copyMenuItem.addActionListener( new DefaultEditorKit.CopyAction() ); @@ -938,7 +978,7 @@ private void initComponents() { for( int i = 1; i <= 100; i++ ) scrollingPopupMenu.add( "Item " + i ); - if( FlatLaf.supportsNativeWindowDecorations() || (SystemInfo.isLinux && JFrame.isDefaultLookAndFeelDecorated()) ) { + if( supportsFlatLafWindowDecorations() ) { if( SystemInfo.isLinux ) unsupported( windowDecorationsCheckBoxMenuItem ); else @@ -959,9 +999,17 @@ private void initComponents() { if( "false".equals( System.getProperty( "flatlaf.animatedLafChange" ) ) ) animatedLafChangeMenuItem.setSelected( false ); + // on macOS, panel left to toolBar is a placeholder for title bar buttons in fullWindowContent mode macFullWindowContentButtonsPlaceholder.putClientProperty( FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_PLACEHOLDER, "mac zeroInFullScreen" ); + // on Windows/Linux, panel above themesPanel is a placeholder for title bar buttons in fullWindowContent mode + winFullWindowContentButtonsPlaceholder.putClientProperty( FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_PLACEHOLDER, "win" ); + + // uncomment this line to see title bar buttons placeholders in fullWindowContent mode +// UIManager.put( "FlatLaf.debug.panel.showPlaceholders", true ); + + // remove contentPanel bottom insets MigLayout layout = (MigLayout) contentPanel.getLayout(); LC lc = ConstraintParser.parseLayoutConstraint( (String) layout.getLayoutConstraints() ); @@ -982,6 +1030,7 @@ private void unsupported( JCheckBoxMenuItem menuItem ) { } // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables + private JMenuBar menuBar; private JMenuItem exitMenuItem; private JMenu scrollingPopupMenu; private JMenuItem htmlMenuItem; diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd index f37f92ca4..08248c1d6 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd @@ -74,7 +74,7 @@ new FormModel { "value": "Center" } ) }, new FormLayoutConstraints( class java.lang.String ) { - "value": "North" + "value": "First" } ) add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "insets dialog,hidemode 3" @@ -115,7 +115,7 @@ new FormModel { "title": "Option Pane" } ) add( new FormComponent( "com.formdev.flatlaf.demo.extras.ExtrasPanel" ) { - name: "extrasPanel1" + name: "extrasPanel" }, new FormLayoutConstraints( null ) { "title": "Extras" } ) @@ -131,19 +131,32 @@ new FormModel { "JavaCodeGenerator.variableLocal": false } }, new FormLayoutConstraints( class java.lang.String ) { - "value": "South" + "value": "Last" } ) - add( new FormComponent( "com.formdev.flatlaf.demo.intellijthemes.IJThemesPanel" ) { - name: "themesPanel" - auxiliary() { - "JavaCodeGenerator.variableLocal": false - "JavaCodeGenerator.variableModifiers": 0 - } + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class java.awt.BorderLayout ) ) { + name: "themesPanelPanel" + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class java.awt.FlowLayout ) ) { + name: "winFullWindowContentButtonsPlaceholder" + }, new FormLayoutConstraints( class java.lang.String ) { + "value": "North" + } ) + add( new FormComponent( "com.formdev.flatlaf.demo.intellijthemes.IJThemesPanel" ) { + name: "themesPanel" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + "JavaCodeGenerator.variableModifiers": 0 + } + }, new FormLayoutConstraints( class java.lang.String ) { + "value": "Center" + } ) }, new FormLayoutConstraints( class java.lang.String ) { - "value": "East" + "value": "After" } ) menuBar: new FormContainer( "javax.swing.JMenuBar", new FormLayoutManager( class javax.swing.JMenuBar ) ) { - name: "menuBar1" + name: "menuBar" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } add( new FormContainer( "javax.swing.JMenu", new FormLayoutManager( class javax.swing.JMenu ) ) { name: "fileMenu" "text": "File" diff --git a/flatlaf-demo/src/main/resources/com/formdev/flatlaf/demo/icons/collapse.svg b/flatlaf-demo/src/main/resources/com/formdev/flatlaf/demo/icons/collapse.svg new file mode 100644 index 000000000..dfef7365a --- /dev/null +++ b/flatlaf-demo/src/main/resources/com/formdev/flatlaf/demo/icons/collapse.svg @@ -0,0 +1,4 @@ + + + + diff --git a/flatlaf-demo/src/main/resources/com/formdev/flatlaf/demo/icons/expand.svg b/flatlaf-demo/src/main/resources/com/formdev/flatlaf/demo/icons/expand.svg new file mode 100644 index 000000000..02c4e5cc8 --- /dev/null +++ b/flatlaf-demo/src/main/resources/com/formdev/flatlaf/demo/icons/expand.svg @@ -0,0 +1,4 @@ + + + + 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 c7a5ec23d..5c0d407c0 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 @@ -32,8 +32,8 @@ import java.beans.PropertyChangeListener; import java.util.Collections; import java.util.IdentityHashMap; -import java.util.List; import java.util.Map; +import java.util.function.Predicate; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.Timer; @@ -164,7 +164,7 @@ private void uninstall( Window window ) { } @Override - public void updateTitleBarInfo( Window window, int titleBarHeight, List hitTestSpots, + public void updateTitleBarInfo( Window window, int titleBarHeight, Predicate hitTestCallback, Rectangle appIconBounds, Rectangle minimizeButtonBounds, Rectangle maximizeButtonBounds, Rectangle closeButtonBounds ) { @@ -173,7 +173,7 @@ public void updateTitleBarInfo( Window window, int titleBarHeight, List hitTestCallback; private Rectangle appIconBounds; private Rectangle minimizeButtonBounds; private Rectangle maximizeButtonBounds; @@ -644,53 +644,65 @@ private LRESULT WmNcHitTest( HWND hwnd, int uMsg, WPARAM wParam, LPARAM lParam ) // scale-down mouse x/y because Swing coordinates/values may be scaled on a HiDPI screen Point pt = scaleDown( x, y ); - int sx = pt.x; - int sy = pt.y; // return HTSYSMENU if mouse is over application icon // - left-click on HTSYSMENU area shows system menu // - double-left-click sends WM_CLOSE - if( contains( appIconBounds, sx, sy ) ) + if( contains( appIconBounds, pt ) ) return new LRESULT( HTSYSMENU ); // return HTMINBUTTON if mouse is over minimize button // - hovering mouse over HTMINBUTTON area shows tooltip on Windows 10/11 - if( contains( minimizeButtonBounds, sx, sy ) ) + if( contains( minimizeButtonBounds, pt ) ) return new LRESULT( HTMINBUTTON ); // return HTMAXBUTTON if mouse is over maximize/restore button // - hovering mouse over HTMAXBUTTON area shows tooltip on Windows 10 // - hovering mouse over HTMAXBUTTON area shows snap layouts menu on Windows 11 // https://docs.microsoft.com/en-us/windows/apps/desktop/modernize/apply-snap-layout-menu - if( contains( maximizeButtonBounds, sx, sy ) ) + if( contains( maximizeButtonBounds, pt ) ) return new LRESULT( HTMAXBUTTON ); // return HTCLOSE if mouse is over close button // - hovering mouse over HTCLOSE area shows tooltip on Windows 10/11 - if( contains( closeButtonBounds, sx, sy ) ) + if( contains( closeButtonBounds, pt ) ) return new LRESULT( HTCLOSE ); int resizeBorderHeight = getResizeHandleHeight(); boolean isOnResizeBorder = (y < resizeBorderHeight) && (User32.INSTANCE.GetWindowLong( hwnd, GWL_STYLE ) & WS_THICKFRAME) != 0; - boolean isOnTitleBar = (sy < titleBarHeight); + // return HTTOP if mouse is over top resize border + // - hovering mouse shows vertical resize cursor + // - left-click and drag vertically resizes window + if( isOnResizeBorder ) + return new LRESULT( HTTOP ); + + boolean isOnTitleBar = (pt.y < titleBarHeight); if( isOnTitleBar ) { - // use a second reference to the array to avoid that it can be changed - // in another thread while processing the array - Rectangle[] hitTestSpots2 = hitTestSpots; - for( Rectangle spot : hitTestSpots2 ) { - if( spot.contains( sx, sy ) ) + // return HTCLIENT if mouse is over any Swing component in title bar + // that processes mouse events (e.g. buttons, menus, etc) + // - Windows ignores mouse events in this area + try { + if( hitTestCallback != null && hitTestCallback.test( pt ) ) return new LRESULT( HTCLIENT ); + } catch( Throwable ex ) { + // ignore } - return new LRESULT( isOnResizeBorder ? HTTOP : HTCAPTION ); + + // return HTCAPTION if mouse is over title bar + // - right-click shows system menu + // - double-left-click maximizes/restores window size + return new LRESULT( HTCAPTION ); } - return new LRESULT( isOnResizeBorder ? HTTOP : HTCLIENT ); + // return HTCLIENT + // - Windows ignores mouse events in this area + return new LRESULT( HTCLIENT ); } - private boolean contains( Rectangle rect, int x, int y ) { - return (rect != null && rect.contains( x, y ) ); + private boolean contains( Rectangle rect, Point pt ) { + return (rect != null && rect.contains( pt ) ); } /** diff --git a/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0.txt b/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0.txt index 585e825ea..42774abdc 100644 --- a/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0.txt +++ b/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0.txt @@ -1264,7 +1264,6 @@ TitlePane.inactiveBackground #303234 HSL 210 4 20 javax.swing.plaf.Colo TitlePane.inactiveForeground #8c8c8c HSL 0 0 55 javax.swing.plaf.ColorUIResource [UI] TitlePane.maximizeIcon [lazy] 44,30 com.formdev.flatlaf.icons.FlatWindowMaximizeIcon [UI] TitlePane.menuBarEmbedded true -TitlePane.menuBarResizeHeight 4 TitlePane.menuBarTitleGap 40 TitlePane.menuBarTitleMinimumGap 12 TitlePane.noIconLeftGap 8 diff --git a/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0.txt b/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0.txt index 20c3ef206..6d061d758 100644 --- a/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0.txt +++ b/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0.txt @@ -1269,7 +1269,6 @@ TitlePane.inactiveBackground #ffffff HSL 0 0 100 javax.swing.plaf.Colo TitlePane.inactiveForeground #8c8c8c HSL 0 0 55 javax.swing.plaf.ColorUIResource [UI] TitlePane.maximizeIcon [lazy] 44,30 com.formdev.flatlaf.icons.FlatWindowMaximizeIcon [UI] TitlePane.menuBarEmbedded true -TitlePane.menuBarResizeHeight 4 TitlePane.menuBarTitleGap 40 TitlePane.menuBarTitleMinimumGap 12 TitlePane.noIconLeftGap 8 diff --git a/flatlaf-testing/dumps/uidefaults/FlatMacDarkLaf_1.8.0.txt b/flatlaf-testing/dumps/uidefaults/FlatMacDarkLaf_1.8.0.txt index 58f29da0a..3254a8812 100644 --- a/flatlaf-testing/dumps/uidefaults/FlatMacDarkLaf_1.8.0.txt +++ b/flatlaf-testing/dumps/uidefaults/FlatMacDarkLaf_1.8.0.txt @@ -1274,7 +1274,6 @@ TitlePane.inactiveBackground #323232 HSL 0 0 20 javax.swing.plaf.Colo TitlePane.inactiveForeground #9a9a9a HSL 0 0 60 javax.swing.plaf.ColorUIResource [UI] TitlePane.maximizeIcon [lazy] 44,30 com.formdev.flatlaf.icons.FlatWindowMaximizeIcon [UI] TitlePane.menuBarEmbedded true -TitlePane.menuBarResizeHeight 4 TitlePane.menuBarTitleGap 40 TitlePane.menuBarTitleMinimumGap 12 TitlePane.noIconLeftGap 8 diff --git a/flatlaf-testing/dumps/uidefaults/FlatMacLightLaf_1.8.0.txt b/flatlaf-testing/dumps/uidefaults/FlatMacLightLaf_1.8.0.txt index 661accbe0..cf9e8bfb3 100644 --- a/flatlaf-testing/dumps/uidefaults/FlatMacLightLaf_1.8.0.txt +++ b/flatlaf-testing/dumps/uidefaults/FlatMacLightLaf_1.8.0.txt @@ -1278,7 +1278,6 @@ TitlePane.inactiveBackground #ececec HSL 0 0 93 javax.swing.plaf.Colo TitlePane.inactiveForeground #b6b6b6 HSL 0 0 71 javax.swing.plaf.ColorUIResource [UI] TitlePane.maximizeIcon [lazy] 44,30 com.formdev.flatlaf.icons.FlatWindowMaximizeIcon [UI] TitlePane.menuBarEmbedded true -TitlePane.menuBarResizeHeight 4 TitlePane.menuBarTitleGap 40 TitlePane.menuBarTitleMinimumGap 12 TitlePane.noIconLeftGap 8 diff --git a/flatlaf-testing/dumps/uidefaults/FlatTestLaf_1.8.0.txt b/flatlaf-testing/dumps/uidefaults/FlatTestLaf_1.8.0.txt index dc851550c..66018f71e 100644 --- a/flatlaf-testing/dumps/uidefaults/FlatTestLaf_1.8.0.txt +++ b/flatlaf-testing/dumps/uidefaults/FlatTestLaf_1.8.0.txt @@ -1305,7 +1305,6 @@ TitlePane.inactiveBackground #008800 HSL 120 100 27 javax.swing.plaf.Colo TitlePane.inactiveForeground #ffffff HSL 0 0 100 javax.swing.plaf.ColorUIResource [UI] TitlePane.maximizeIcon [lazy] 44,30 com.formdev.flatlaf.icons.FlatWindowMaximizeIcon [UI] TitlePane.menuBarEmbedded true -TitlePane.menuBarResizeHeight 4 TitlePane.menuBarTitleGap 40 TitlePane.menuBarTitleMinimumGap 12 TitlePane.noIconLeftGap 8 diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowDecorationsTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowDecorationsTest.java index 85a62fab3..1748ff697 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowDecorationsTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowDecorationsTest.java @@ -41,6 +41,9 @@ public class FlatWindowDecorationsTest extends FlatTestPanel { + // same as in FlatTitlePane + private static final String KEY_DEBUG_SHOW_RECTANGLES = "FlatLaf.debug.titlebar.showRectangles"; + public static void main( String[] args ) { SwingUtilities.invokeLater( () -> { if( SystemInfo.isLinux ) { @@ -51,7 +54,7 @@ public static void main( String[] args ) { FlatTestFrame frame = FlatTestFrame.create( args, "FlatWindowDecorationsTest" ); frame.applyComponentOrientationToFrame = true; - UIManager.put( "FlatLaf.debug.titlebar.showRectangles", true ); + UIManager.put( KEY_DEBUG_SHOW_RECTANGLES, true ); Class cls = FlatWindowDecorationsTest.class; List images = Arrays.asList( @@ -117,6 +120,14 @@ else if( window instanceof Dialog ) rootPane.addPropertyChangeListener( "windowDecorationStyle", e -> { updateDecorationStyleRadioButtons( rootPane ); } ); + rootPane.addPropertyChangeListener( FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS, e -> { + Rectangle bounds = (Rectangle) e.getNewValue(); + if( bounds != null ) { + fullWindowContentButtonsBoundsField.setText( bounds.width + ", " + bounds.height + + " @ " + bounds.x + ", " + bounds.y ); + } else + fullWindowContentButtonsBoundsField.setText( "null" ); + } ); } } @@ -309,12 +320,21 @@ private void addCaption() { JLabel caption = new JLabel( "Caption" ); caption.setBackground( Color.green ); caption.setOpaque( true ); - caption.putClientProperty( FlatClientProperties.COMPONENT_TITLE_BAR_CAPTION, true ); menuBar.add( caption ); menuBar.revalidate(); } + private void addTextField() { + JTextField textField = new JTextField( "text", 10 ); + + JPanel panel = new JPanel( new GridBagLayout() ); + panel.add( textField, new GridBagConstraints() ); + + menuBar.add( panel ); + menuBar.revalidate(); + } + private void removeMenu() { int menuCount = menuBar.getMenuCount(); if( menuCount <= 0 ) @@ -515,13 +535,31 @@ private void showCloseChanged() { rootPane.putClientProperty( FlatClientProperties.TITLE_BAR_SHOW_CLOSE, showCloseCheckBox.isSelected() ? null : false ); } + private void fullWindowContentChanged() { + JRootPane rootPane = getWindowRootPane(); + if( rootPane != null ) { + boolean selected = fullWindowContentCheckBox.isSelected(); + rootPane.putClientProperty( FlatClientProperties.FULL_WINDOW_CONTENT, selected ? true : null ); + + showIconCheckBox.setEnabled( !selected ); + showTitleCheckBox.setEnabled( !selected ); + } + } + private JRootPane getWindowRootPane() { Window window = SwingUtilities.windowForComponent( this ); - if( window instanceof JFrame ) - return ((JFrame)window).getRootPane(); - else if( window instanceof JDialog ) - return ((JDialog)window).getRootPane(); - return null; + return (window instanceof RootPaneContainer) + ? ((RootPaneContainer)window).getRootPane() + : null; + } + + private void showRectangles() { + JRootPane rootPane = getWindowRootPane(); + if( rootPane != null ) { + UIManager.put( KEY_DEBUG_SHOW_RECTANGLES, showRectanglesCheckBox.isSelected() ); + rootPane.revalidate(); + rootPane.repaint(); + } } private void initComponents() { @@ -538,6 +576,9 @@ private void initComponents() { showIconifyCheckBox = new JCheckBox(); showMaximizeCheckBox = new JCheckBox(); showCloseCheckBox = new JCheckBox(); + fullWindowContentCheckBox = new JCheckBox(); + JLabel fullWindowContentButtonsBoundsLabel = new JLabel(); + fullWindowContentButtonsBoundsField = new JLabel(); JPanel panel6 = new JPanel(); menuBarCheckBox = new JCheckBox(); menuBarEmbeddedCheckBox = new JCheckBox(); @@ -548,6 +589,7 @@ private void initComponents() { addMenuButton = new JButton(); addGlueButton = new JButton(); addCaptionButton = new JButton(); + addTextFieldButton = new JButton(); removeMenuButton = new JButton(); changeMenuButton = new JButton(); changeTitleButton = new JButton(); @@ -578,6 +620,7 @@ private void initComponents() { typeNormalRadioButton = new JRadioButton(); typeUtilityRadioButton = new JRadioButton(); typeSmallRadioButton = new JRadioButton(); + showRectanglesCheckBox = new JCheckBox(); menuBar = new JMenuBar(); JMenu fileMenu = new JMenu(); JMenuItem newMenuItem = new JMenuItem(); @@ -616,6 +659,7 @@ private void initComponents() { // rows "[fill]" + "[fill]" + + "[]" + "[]")); //======== panel7 ======== @@ -673,6 +717,8 @@ private void initComponents() { "[]" + "[]" + "[]" + + "[]rel" + + "[]rel" + "[]")); //---- showIconCheckBox ---- @@ -703,6 +749,19 @@ private void initComponents() { showCloseCheckBox.setSelected(true); showCloseCheckBox.addActionListener(e -> showCloseChanged()); panel4.add(showCloseCheckBox, "cell 0 4"); + + //---- fullWindowContentCheckBox ---- + fullWindowContentCheckBox.setText("full window content"); + fullWindowContentCheckBox.addActionListener(e -> fullWindowContentChanged()); + panel4.add(fullWindowContentCheckBox, "cell 0 5"); + + //---- fullWindowContentButtonsBoundsLabel ---- + fullWindowContentButtonsBoundsLabel.setText("Buttons bounds:"); + panel4.add(fullWindowContentButtonsBoundsLabel, "cell 0 6"); + + //---- fullWindowContentButtonsBoundsField ---- + fullWindowContentButtonsBoundsField.setText("null"); + panel4.add(fullWindowContentButtonsBoundsField, "cell 0 6"); } add(panel4, "cell 1 0"); @@ -761,6 +820,7 @@ private void initComponents() { "[]" + "[]" + "[]" + + "[]" + "[]unrel" + "[]")); @@ -779,20 +839,25 @@ private void initComponents() { addCaptionButton.addActionListener(e -> addCaption()); panel3.add(addCaptionButton, "cell 0 2"); + //---- addTextFieldButton ---- + addTextFieldButton.setText("Add text field"); + addTextFieldButton.addActionListener(e -> addTextField()); + panel3.add(addTextFieldButton, "cell 0 3"); + //---- removeMenuButton ---- removeMenuButton.setText("Remove menu"); removeMenuButton.addActionListener(e -> removeMenu()); - panel3.add(removeMenuButton, "cell 0 3"); + panel3.add(removeMenuButton, "cell 0 4"); //---- changeMenuButton ---- changeMenuButton.setText("Change menu"); changeMenuButton.addActionListener(e -> changeMenu()); - panel3.add(changeMenuButton, "cell 0 4"); + panel3.add(changeMenuButton, "cell 0 5"); //---- changeTitleButton ---- changeTitleButton.setText("Change title"); changeTitleButton.addActionListener(e -> changeTitle()); - panel3.add(changeTitleButton, "cell 0 5"); + panel3.add(changeTitleButton, "cell 0 6"); } add(panel3, "cell 3 0 1 2,aligny top,growy 0"); @@ -969,6 +1034,12 @@ private void initComponents() { typeSmallRadioButton.setText("Small"); add(typeSmallRadioButton, "cell 0 2 3 1"); + //---- showRectanglesCheckBox ---- + showRectanglesCheckBox.setText("Show debug title bar rectangles"); + showRectanglesCheckBox.setSelected(true); + showRectanglesCheckBox.addActionListener(e -> showRectangles()); + add(showRectanglesCheckBox, "cell 0 3"); + //======== menuBar ======== { @@ -1176,6 +1247,8 @@ private void initComponents() { private JCheckBox showIconifyCheckBox; private JCheckBox showMaximizeCheckBox; private JCheckBox showCloseCheckBox; + private JCheckBox fullWindowContentCheckBox; + private JLabel fullWindowContentButtonsBoundsField; private JCheckBox menuBarCheckBox; private JCheckBox menuBarEmbeddedCheckBox; private JCheckBox menuBarVisibleCheckBox; @@ -1184,6 +1257,7 @@ private void initComponents() { private JButton addMenuButton; private JButton addGlueButton; private JButton addCaptionButton; + private JButton addTextFieldButton; private JButton removeMenuButton; private JButton changeMenuButton; private JButton changeTitleButton; @@ -1209,6 +1283,7 @@ private void initComponents() { private JRadioButton typeNormalRadioButton; private JRadioButton typeUtilityRadioButton; private JRadioButton typeSmallRadioButton; + private JCheckBox showRectanglesCheckBox; private JMenuBar menuBar; // JFormDesigner - End of variables declaration //GEN-END:variables } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowDecorationsTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowDecorationsTest.jfd index 484c1fac1..c3579666d 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowDecorationsTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowDecorationsTest.jfd @@ -9,7 +9,7 @@ new FormModel { add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "ltr,insets dialog,hidemode 3" "$columnConstraints": "[left][fill][fill][fill]" - "$rowConstraints": "[fill][fill][]" + "$rowConstraints": "[fill][fill][][]" } ) { name: "this" add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { @@ -77,7 +77,7 @@ new FormModel { add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "ltr,hidemode 3,gap 0 0" "$columnConstraints": "[grow,left]" - "$rowConstraints": "[][][][][]" + "$rowConstraints": "[][][][][]rel[]rel[]" } ) { name: "panel4" "border": new javax.swing.border.TitledBorder( "Title Bar" ) @@ -135,6 +135,31 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 4" } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "fullWindowContentCheckBox" + "text": "full window content" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "fullWindowContentChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "fullWindowContentButtonsBoundsLabel" + "text": "Buttons bounds:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "fullWindowContentButtonsBoundsField" + "text": "null" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6" + } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 0" } ) @@ -204,7 +229,7 @@ new FormModel { add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "hidemode 3" "$columnConstraints": "[fill]" - "$rowConstraints": "[][][][][]unrel[]" + "$rowConstraints": "[][][][][][]unrel[]" } ) { name: "panel3" add( new FormComponent( "javax.swing.JButton" ) { @@ -237,6 +262,16 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 2" } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "addTextFieldButton" + "text": "Add text field" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "addTextField", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) add( new FormComponent( "javax.swing.JButton" ) { name: "removeMenuButton" "text": "Remove menu" @@ -245,7 +280,7 @@ new FormModel { } addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "removeMenu", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 3" + "value": "cell 0 4" } ) add( new FormComponent( "javax.swing.JButton" ) { name: "changeMenuButton" @@ -255,7 +290,7 @@ new FormModel { } addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "changeMenu", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 4" + "value": "cell 0 5" } ) add( new FormComponent( "javax.swing.JButton" ) { name: "changeTitleButton" @@ -265,7 +300,7 @@ new FormModel { } addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "changeTitle", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 5" + "value": "cell 0 6" } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 3 0 1 2,aligny top,growy 0" @@ -552,6 +587,17 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 2 3 1" } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "showRectanglesCheckBox" + "text": "Show debug title bar rectangles" + "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "showRectangles", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) "size": new java.awt.Dimension( 960, 495 ) diff --git a/flatlaf-theme-editor/src/main/resources/com/formdev/flatlaf/themeeditor/FlatLafUIKeys.txt b/flatlaf-theme-editor/src/main/resources/com/formdev/flatlaf/themeeditor/FlatLafUIKeys.txt index efb174bbd..7f49289f0 100644 --- a/flatlaf-theme-editor/src/main/resources/com/formdev/flatlaf/themeeditor/FlatLafUIKeys.txt +++ b/flatlaf-theme-editor/src/main/resources/com/formdev/flatlaf/themeeditor/FlatLafUIKeys.txt @@ -1047,7 +1047,6 @@ TitlePane.inactiveBackground TitlePane.inactiveForeground TitlePane.maximizeIcon TitlePane.menuBarEmbedded -TitlePane.menuBarResizeHeight TitlePane.menuBarTitleGap TitlePane.menuBarTitleMinimumGap TitlePane.noIconLeftGap @@ -1079,7 +1078,6 @@ TitlePane.small.iconifyIcon TitlePane.small.inactiveBackground TitlePane.small.inactiveForeground TitlePane.small.maximizeIcon -TitlePane.small.menuBarResizeHeight TitlePane.small.menuBarTitleGap TitlePane.small.menuBarTitleMinimumGap TitlePane.small.noIconLeftGap From a84aceb1ba90d10309ac85d3149bd6b56b2ce285 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 4 Feb 2024 16:30:38 +0100 Subject: [PATCH 2/3] 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: + *
    + *
  • This function is invoked often when mouse is moved over window title bar area + * and should therefore return quickly. + *
  • This function is invoked on 'AWT-Windows' thread (not 'AWT-EventQueue' thread) + * while processing Windows messages. + * It must not change any component property or layout because this could cause a dead lock. + *
+ *

* 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 From b804463b738e5235f97ae12ecab271e0a2ae6e5d Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Tue, 6 Feb 2024 11:30:11 +0100 Subject: [PATCH 3/3] Window decorations: - fixed updating of client property `FULL_WINDOW_CONTENT_BUTTONS_BOUNDS` when resizing window - fixed title bar buttons placeholder debug painting --- .../main/java/com/formdev/flatlaf/ui/FlatTitlePane.java | 9 ++++++--- .../com/formdev/flatlaf/ui/FullWindowContentSupport.java | 6 ++++-- 2 files changed, 10 insertions(+), 5 deletions(-) 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 01cb991f7..82f5a22b6 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 @@ -359,11 +359,14 @@ public Dimension getPreferredSize() { buttonPanel.add( maximizeButton ); buttonPanel.add( restoreButton ); } - buttonPanel.addComponentListener( new ComponentAdapter() { + buttonPanel.add( closeButton ); + + ComponentListener l = new ComponentAdapter() { @Override public void componentResized( ComponentEvent e ) { updateFullWindowContentButtonsBoundsProperty(); } @Override public void componentMoved( ComponentEvent e ) { updateFullWindowContentButtonsBoundsProperty(); } - } ); - buttonPanel.add( closeButton ); + }; + buttonPanel.addComponentListener( l ); + addComponentListener( l ); } protected JButton createButton( String iconKey, String accessibleName, ActionListener action ) { diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FullWindowContentSupport.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FullWindowContentSupport.java index 87950f32e..d5e080b95 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FullWindowContentSupport.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FullWindowContentSupport.java @@ -209,9 +209,11 @@ private static void debugPaintRect( Graphics g, Rectangle r ) { g.drawRect( r.x, r.y, r.width - 1, r.height - 1 ); // draw diagonal cross + int x2 = r.x + r.width - 1; + int y2 = r.y + r.height - 1; Object[] oldRenderingHints = FlatUIUtils.setRenderingHints( g ); - g.drawLine( r.x, r.y, r.width - 1, r.height - 1 ); - g.drawLine( r.x, r.height - 1, r.width - 1, r.y ); + g.drawLine( r.x, r.y, x2, y2 ); + g.drawLine( r.x, y2, x2, r.y ); FlatUIUtils.resetRenderingHints( g, oldRenderingHints ); } }