diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f94b41c..70d26659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,22 @@ FlatLaf Change Log ## 2.4-SNAPSHOT +#### New features and improvements + +- Native window decorations (Windows 10/11 only): + - There is now a small area at top of the embedded menu bar to resize the + window. + - Improved window title bar layout for small window widths: + - Width of iconify/maximize/close buttons is reduced (if necessary) to give + more space to embedded menu bar and title. + - Window title now has a minimum width to always allow moving window + (click-and-drag on window title). Instead, embedded menu bar is made + smaller. + - Option to show window icon beside window title, if menu bar is embedded or + title is centered. Set UI value `TitlePane.showIconBesideTitle` to `true`. + - No longer reduce height of window title bar if it has an embedded menu bar + and is maximized. + #### Fixed bugs - ComboBox: Fixed vertical alignment of text in popup list with text in combo @@ -25,6 +41,8 @@ FlatLaf Change Log `true` on Windows 10. (issue #540) - Fixed missing top window border in dark themes if window drop shadows are disabled in system settings. (issue #554; Windows 10 only) + - Right-to-left component orientation of title bar was lost when switching + theme. ## 2.3 diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatMenuBarUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatMenuBarUI.java index 273fb080..e167c368 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatMenuBarUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatMenuBarUI.java @@ -18,8 +18,10 @@ package com.formdev.flatlaf.ui; import java.awt.Color; import java.awt.Component; +import java.awt.Container; import java.awt.Graphics; import java.awt.Insets; +import java.awt.LayoutManager; import java.awt.Window; import java.awt.event.ActionEvent; import java.beans.PropertyChangeListener; @@ -27,6 +29,7 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import javax.swing.AbstractAction; import javax.swing.ActionMap; +import javax.swing.BoxLayout; import javax.swing.JComponent; import javax.swing.JMenu; import javax.swing.JMenuBar; @@ -40,11 +43,13 @@ import javax.swing.plaf.ActionMapUIResource; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.UIResource; import javax.swing.plaf.basic.BasicMenuBarUI; +import javax.swing.plaf.basic.DefaultMenuLayout; import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; import com.formdev.flatlaf.util.LoggingFacade; import com.formdev.flatlaf.util.SystemInfo; +import com.formdev.flatlaf.util.UIScale; /** * Provides the Flat LaF UI delegate for {@link javax.swing.JMenuBar}. @@ -100,6 +105,10 @@ public class FlatMenuBarUI super.installDefaults(); LookAndFeel.installProperty( menuBar, "opaque", false ); + + LayoutManager layout = menuBar.getLayout(); + if( layout == null || layout instanceof UIResource ) + menuBar.setLayout( new FlatMenuBarLayout( menuBar ) ); } @Override @@ -221,6 +230,130 @@ public class FlatMenuBarUI rootPane.getWindowDecorationStyle() != JRootPane.NONE; } + //---- class FlatMenuBarLayout -------------------------------------------- + + /** + * @since 2.4 + */ + protected static class FlatMenuBarLayout + extends DefaultMenuLayout + { + public FlatMenuBarLayout( Container target ) { + super( target, BoxLayout.LINE_AXIS ); + } + + @Override + public void layoutContainer( Container target ) { + super.layoutContainer( target ); + + + // The only purpose of the code below is to make sure that a horizontal glue, + // which can be used to move window and displays the window title in embedded menu bar, + // is always visible within the menu bar bounds and has a minimum width. + // If this is not the case, the horizontal glue is made larger and + // components that are on the left side of the glue are made smaller. + + + // get root pane and check whether this menu bar is the root pane menu bar + JRootPane rootPane = SwingUtilities.getRootPane( target ); + if( rootPane == null || rootPane.getJMenuBar() != target ) + return; + + // get title pane and check whether menu bar is embedded + FlatTitlePane titlePane = FlatRootPaneUI.getTitlePane( rootPane ); + if( titlePane == null || !titlePane.isMenuBarEmbedded() ) + return; + + // check whether there is a horizontal glue (used for window title in embedded menu bar) + // and check minimum width of horizontal glue + Component horizontalGlue = titlePane.findHorizontalGlue( (JMenuBar) target ); + int minTitleWidth = UIScale.scale( titlePane.titleMinimumWidth ); + if( horizontalGlue != null && horizontalGlue.getWidth() < minTitleWidth ) { + // get index of glue component + int glueIndex = -1; + Component[] components = target.getComponents(); + for( int i = components.length - 1; i >= 0; i-- ) { + if( components[i] == horizontalGlue ) { + glueIndex = i; + break; + } + } + if( glueIndex < 0 ) + return; // should never happen + + if( target.getComponentOrientation().isLeftToRight() ) { + // left-to-right + + // make horizontal glue wider (minimum title width) + int offset = minTitleWidth - horizontalGlue.getWidth(); + horizontalGlue.setSize( minTitleWidth, horizontalGlue.getHeight() ); + + // check whether glue is fully visible + int minGlueX = target.getWidth() - target.getInsets().right - minTitleWidth; + if( minGlueX < horizontalGlue.getX() ) { + // move glue to the left to make it fully visible + offset -= (horizontalGlue.getX() - minGlueX); + horizontalGlue.setLocation( minGlueX, horizontalGlue.getY() ); + + // shrink and move components that are on the left side of the glue + for( int i = glueIndex - 1; i >= 0; i-- ) { + Component c = components[i]; + if( c.getX() > minGlueX ) { + // move component and set width to zero + c.setBounds( minGlueX, c.getY(), 0, c.getHeight() ); + } else { + // reduce size of component + c.setSize( minGlueX - c.getX(), c.getHeight() ); + break; + } + } + } + + // move components that are on the right side of the glue + for( int i = glueIndex + 1; i < components.length; i++ ) { + Component c = components[i]; + c.setLocation( c.getX() + offset, c.getY() ); + } + } else { + // right-to-left + + // make horizontal glue wider (minimum title width) + int offset = minTitleWidth - horizontalGlue.getWidth(); + horizontalGlue.setBounds( horizontalGlue.getX() - offset, horizontalGlue.getY(), + minTitleWidth, horizontalGlue.getHeight() ); + + // check whether glue is fully visible + int minGlueX = target.getInsets().left; + if( minGlueX > horizontalGlue.getX() ) { + // move glue to the right to make it fully visible + offset -= (horizontalGlue.getX() - minGlueX); + horizontalGlue.setLocation( minGlueX, horizontalGlue.getY() ); + + // shrink and move components that are on the right side of the glue + int x = horizontalGlue.getX() + horizontalGlue.getWidth(); + for( int i = glueIndex - 1; i >= 0; i-- ) { + Component c = components[i]; + if( c.getX() + c.getWidth() < x ) { + // move component and set width to zero + c.setBounds( x, c.getY(), 0, c.getHeight() ); + } else { + // move component and reduce size + c.setBounds( x, c.getY(), c.getWidth() - (x - c.getX()), c.getHeight() ); + break; + } + } + } + + // move components that are on the left side of the glue + for( int i = glueIndex + 1; i < components.length; i++ ) { + Component c = components[i]; + c.setLocation( c.getX() - offset, c.getY() ); + } + } + } + } + } + //---- class TakeFocus ---------------------------------------------------- /** diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatPopupMenuUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatPopupMenuUI.java index 3c0014ab..2ea093a5 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatPopupMenuUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatPopupMenuUI.java @@ -130,7 +130,7 @@ public class FlatPopupMenuUI LayoutManager layout = popupMenu.getLayout(); if( layout == null || layout instanceof UIResource ) - popupMenu.setLayout( new FlatMenuLayout( popupMenu, BoxLayout.Y_AXIS ) ); + popupMenu.setLayout( new FlatPopupMenuLayout( popupMenu, BoxLayout.Y_AXIS ) ); } @Override @@ -230,12 +230,15 @@ public class FlatPopupMenuUI return screenBounds.height - screenInsets.top - screenInsets.bottom; } - //---- class FlatMenuLayout ----------------------------------------------- + //---- class FlatPopupMenuLayout ------------------------------------------ - protected static class FlatMenuLayout + /** + * @since 2.4 + */ + protected static class FlatPopupMenuLayout extends DefaultMenuLayout { - public FlatMenuLayout( Container target, int axis ) { + public FlatPopupMenuLayout( Container target, int axis ) { super( target, axis ); } 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 eee1b3c7..54640afd 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 @@ -364,6 +364,12 @@ public class FlatRootPaneUI ((FlatRootPaneUI)ui).titlePane.isMenuBarEmbedded(); } + /** @since 2.4 */ + protected static FlatTitlePane getTitlePane( JRootPane rootPane ) { + RootPaneUI ui = rootPane.getUI(); + return ui instanceof FlatRootPaneUI ? ((FlatRootPaneUI)ui).titlePane : null; + } + //---- class FlatRootLayout ----------------------------------------------- protected class FlatRootLayout 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 4854274f..48301ab9 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 @@ -24,6 +24,7 @@ import java.awt.Container; import java.awt.Dialog; import java.awt.Dimension; import java.awt.EventQueue; +import java.awt.FontMetrics; import java.awt.Frame; import java.awt.Graphics; import java.awt.GraphicsConfiguration; @@ -50,13 +51,13 @@ import javax.accessibility.AccessibleContext; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; +import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JMenuBar; import javax.swing.JPanel; import javax.swing.JRootPane; -import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.border.AbstractBorder; @@ -83,10 +84,14 @@ import com.formdev.flatlaf.util.UIScale; * @uiDefault TitlePane.iconMargins Insets * @uiDefault TitlePane.titleMargins Insets * @uiDefault TitlePane.menuBarEmbedded boolean + * @uiDefault TitlePane.titleMinimumWidth int + * @uiDefault TitlePane.buttonMinimumWidth int * @uiDefault TitlePane.buttonMaximizedHeight int * @uiDefault TitlePane.centerTitle boolean * @uiDefault TitlePane.centerTitleIfMenuBarEmbedded boolean + * @uiDefault TitlePane.showIconBesideTitle boolean * @uiDefault TitlePane.menuBarTitleGap int + * @uiDefault TitlePane.menuBarResizeHeight int * @uiDefault TitlePane.closeIcon Icon * @uiDefault TitlePane.iconifyIcon Icon * @uiDefault TitlePane.maximizeIcon Icon @@ -107,10 +112,15 @@ public class FlatTitlePane /** @since 2 */ protected final boolean showIcon = FlatUIUtils.getUIBoolean( "TitlePane.showIcon", true ); /** @since 2 */ protected final int noIconLeftGap = FlatUIUtils.getUIInt( "TitlePane.noIconLeftGap", 8 ); protected final Dimension iconSize = UIManager.getDimension( "TitlePane.iconSize" ); + /** @since 2.4 */ protected final int titleMinimumWidth = FlatUIUtils.getUIInt( "TitlePane.titleMinimumWidth", 60 ); + /** @since 2.4 */ protected final int buttonMinimumWidth = FlatUIUtils.getUIInt( "TitlePane.buttonMinimumWidth", 30 ); protected final int buttonMaximizedHeight = UIManager.getInt( "TitlePane.buttonMaximizedHeight" ); protected final boolean centerTitle = UIManager.getBoolean( "TitlePane.centerTitle" ); protected final boolean centerTitleIfMenuBarEmbedded = FlatUIUtils.getUIBoolean( "TitlePane.centerTitleIfMenuBarEmbedded", true ); - protected final int menuBarTitleGap = FlatUIUtils.getUIInt( "TitlePane.menuBarTitleGap", 20 ); + /** @since 2.4 */ protected final boolean showIconBesideTitle = UIManager.getBoolean( "TitlePane.showIconBesideTitle" ); + protected final int menuBarTitleGap = FlatUIUtils.getUIInt( "TitlePane.menuBarTitleGap", 40 ); + /** @since 2.4 */ protected final int menuBarTitleMinimumGap = FlatUIUtils.getUIInt( "TitlePane.menuBarTitleMinimumGap", 12 ); + /** @since 2.4 */ protected final int menuBarResizeHeight = FlatUIUtils.getUIInt( "TitlePane.menuBarResizeHeight", 4 ); protected final JRootPane rootPane; @@ -142,6 +152,8 @@ public class FlatTitlePane // necessary for closing window with double-click on icon iconLabel.addMouseListener( handler ); + + applyComponentOrientation( rootPane.getComponentOrientation() ); } protected FlatTitlePaneBorder createTitlePaneBorder() { @@ -163,7 +175,6 @@ public class FlatTitlePane }; iconLabel.setBorder( new FlatEmptyBorder( UIManager.getInsets( "TitlePane.iconMargins" ) ) ); titleLabel.setBorder( new FlatEmptyBorder( UIManager.getInsets( "TitlePane.titleMargins" ) ) ); - titleLabel.setHorizontalAlignment( SwingConstants.CENTER ); leftPanel.setLayout( new BoxLayout( leftPanel, BoxLayout.LINE_AXIS ) ); leftPanel.setOpaque( false ); @@ -183,18 +194,52 @@ public class FlatTitlePane setLayout( new BorderLayout() { @Override public void layoutContainer( Container target ) { - super.layoutContainer( target ); - - // make left panel (with embedded menu bar) smaller if horizontal space is rare - // to avoid that embedded menu bar overlaps button bar + // compute available bounds Insets insets = target.getInsets(); - int width = target.getWidth() - insets.left - insets.right; - if( leftPanel.getWidth() + buttonPanel.getWidth() > width ) { - int oldWidth = leftPanel.getWidth(); - int newWidth = Math.max( width - buttonPanel.getWidth(), 0 ); - leftPanel.setSize( newWidth, leftPanel.getHeight() ); - if( !getComponentOrientation().isLeftToRight() ) - leftPanel.setLocation( leftPanel.getX() + (oldWidth - newWidth), leftPanel.getY() ); + int x = insets.left; + int y = insets.top; + int w = target.getWidth() - insets.left - insets.right; + int h = target.getHeight() - insets.top - insets.bottom; + + // compute widths + int leftWidth = leftPanel.getPreferredSize().width; + int buttonsWidth = buttonPanel.getPreferredSize().width; + int titleWidth = w - leftWidth - buttonsWidth; + int minTitleWidth = UIScale.scale( titleMinimumWidth ); + + // increase minimum width if icon is show besides the title + Icon icon = titleLabel.getIcon(); + if( icon != null ) { + Insets iconInsets = iconLabel.getInsets(); + int iconTextGap = titleLabel.getComponentOrientation().isLeftToRight() ? iconInsets.right : iconInsets.left; + minTitleWidth += icon.getIconWidth() + iconTextGap; + } + + // if title is too small, reduce width of buttons + if( titleWidth < minTitleWidth ) { + buttonsWidth = Math.max( buttonsWidth - (minTitleWidth - titleWidth), buttonPanel.getMinimumSize().width ); + titleWidth = w - leftWidth - buttonsWidth; + } + + // if title is still too small, reduce width of left panel (icon and embedded menu bar) + if( titleWidth < minTitleWidth ) { + int minLeftWidth = iconLabel.isVisible() + ? iconLabel.getWidth() - iconLabel.getInsets().right + : UIScale.scale( noIconLeftGap ); + leftWidth = Math.max( leftWidth - (minTitleWidth - titleWidth), minLeftWidth ); + titleWidth = w - leftWidth - buttonsWidth; + } + + if( target.getComponentOrientation().isLeftToRight() ) { + // left-to-right + leftPanel.setBounds( x, y, leftWidth, h ); + titleLabel.setBounds( x + leftWidth, y, titleWidth, h ); + buttonPanel.setBounds( x + leftWidth + titleWidth, y, buttonsWidth, h ); + } else { + // right-to-left + buttonPanel.setBounds( x, y, buttonsWidth, h ); + titleLabel.setBounds( x + buttonsWidth, y, titleWidth, h ); + leftPanel.setBounds( x + buttonsWidth + titleWidth, y, leftWidth, h ); } // If menu bar is embedded and contains a horizontal glue component, @@ -233,10 +278,7 @@ public class FlatTitlePane @Override public Dimension getPreferredSize() { Dimension size = super.getPreferredSize(); - if( buttonMaximizedHeight > 0 && - window instanceof Frame && - (((Frame)window).getExtendedState() & Frame.MAXIMIZED_BOTH) != 0 ) - { + if( buttonMaximizedHeight > 0 && isWindowMaximized() && !hasVisibleEmbeddedMenuBar( rootPane.getJMenuBar() ) ) { // make title pane height smaller when frame is maximized size = new Dimension( size.width, Math.min( size.height, UIScale.scale( buttonMaximizedHeight ) ) ); } @@ -259,7 +301,13 @@ public class FlatTitlePane } protected JButton createButton( String iconKey, String accessibleName, ActionListener action ) { - JButton button = new JButton( UIManager.getIcon( iconKey ) ); + JButton button = new JButton( UIManager.getIcon( iconKey ) ) { + @Override + public Dimension getMinimumSize() { + // allow the button to shrink if space is rare + return new Dimension( UIScale.scale( buttonMinimumWidth ), super.getMinimumSize().height ); + } + }; button.setFocusable( false ); button.setContentAreaFilled( false ); button.setBorder( BorderFactory.createEmptyBorder() ); @@ -356,11 +404,12 @@ public class FlatTitlePane boolean hasIcon = (images != null && !images.isEmpty()); // set icon - iconLabel.setIcon( hasIcon ? new FlatTitlePaneIcon( images, iconSize ) : null ); + iconLabel.setIcon( hasIcon && !showIconBesideTitle ? new FlatTitlePaneIcon( images, iconSize ) : null ); + titleLabel.setIcon( hasIcon && showIconBesideTitle ? new FlatTitlePaneIcon( images, iconSize ) : null ); // show/hide icon - iconLabel.setVisible( hasIcon ); - leftPanel.setBorder( hasIcon ? null : FlatUIUtils.nonUIResource( new FlatEmptyBorder( 0, noIconLeftGap, 0, 0 ) ) ); + iconLabel.setVisible( hasIcon && !showIconBesideTitle ); + leftPanel.setBorder( hasIcon && !showIconBesideTitle ? null : FlatUIUtils.nonUIResource( new FlatEmptyBorder( 0, noIconLeftGap, 0, 0 ) ) ); updateNativeTitleBarHeightAndHitTestSpotsLater(); } @@ -567,6 +616,11 @@ debug*/ frame.setExtendedState( frame.getExtendedState() | Frame.ICONIFIED ); } + /** @since 2.4 */ + protected boolean isWindowMaximized() { + return window instanceof Frame && (((Frame)window).getExtendedState() & Frame.MAXIMIZED_BOTH) != 0; + } + /** * Maximizes the window. */ @@ -729,7 +783,7 @@ debug*/ List hitTestSpots = new ArrayList<>(); Rectangle appIconBounds = null; - if( iconLabel.isVisible() ) { + if( !showIconBesideTitle && iconLabel.isVisible() ) { // compute real icon size (without insets; 1px larger for easier hitting) Point location = SwingUtilities.convertPoint( iconLabel, 0, 0, window ); Insets iconInsets = iconLabel.getInsets(); @@ -741,9 +795,7 @@ debug*/ // if frame is maximized, increase icon bounds to upper-left corner // of window to allow closing window via double-click in upper-left corner - if( window instanceof Frame && - (((Frame)window).getExtendedState() & Frame.MAXIMIZED_BOTH) != 0 ) - { + if( isWindowMaximized() ) { iconBounds.height += iconBounds.y; iconBounds.y = 0; @@ -758,6 +810,38 @@ debug*/ hitTestSpots.add( iconBounds ); else appIconBounds = iconBounds; + } else if( showIconBesideTitle && titleLabel.getIcon() != null && titleLabel.getUI() instanceof FlatTitleLabelUI ) { + FlatTitleLabelUI ui = (FlatTitleLabelUI) titleLabel.getUI(); + + // compute real icon bounds + Insets insets = titleLabel.getInsets(); + Rectangle viewR = new Rectangle( insets.left, insets.top, + titleLabel.getWidth() - insets.left - insets.right, + titleLabel.getHeight() - insets.top - insets.bottom ); + Rectangle iconR = new Rectangle(); + Rectangle textR = new Rectangle(); + ui.layoutCL( titleLabel, titleLabel.getFontMetrics( titleLabel.getFont() ), + titleLabel.getText(), titleLabel.getIcon(), + viewR, iconR, textR ); + + // Windows shows the window system menu only in the upper-left corner + if( iconR.x == 0 ) { + // convert icon location to window coordinates + Point location = SwingUtilities.convertPoint( titleLabel, 0, 0, window ); + iconR.x += location.x; + iconR.y += location.y; + + // make icon bounds 1px larger for easier hitting + iconR.x -= 1; + iconR.y -= 1; + iconR.width += 2; + iconR.height += 2; + + if( hasJBRCustomDecoration() ) + hitTestSpots.add( iconR ); + else + appIconBounds = iconR; + } } Rectangle r = getNativeHitTestSpot( buttonPanel ); @@ -768,6 +852,15 @@ debug*/ 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; + } + Component horizontalGlue = findHorizontalGlue( menuBar ); if( horizontalGlue != null ) { // If menu bar is embedded and contains a horizontal glue component, @@ -854,7 +947,7 @@ debug*/ } else if( borderColor != null && (rootPane.getJMenuBar() == null || !rootPane.getJMenuBar().isVisible()) ) insets.bottom += UIScale.scale( 1 ); - if( !SystemInfo.isWindows_11_orLater && hasNativeCustomDecoration() && !isWindowMaximized( c ) ) + if( !SystemInfo.isWindows_11_orLater && hasNativeCustomDecoration() && !isWindowMaximized() ) insets = FlatUIUtils.addInsets( insets, WindowTopBorder.getInstance().getBorderInsets() ); return insets; @@ -873,7 +966,7 @@ debug*/ FlatUIUtils.paintFilledRectangle( g, borderColor, x, y + height - lineHeight, width, lineHeight ); } - if( !SystemInfo.isWindows_11_orLater && hasNativeCustomDecoration() && !isWindowMaximized( c ) ) + if( !SystemInfo.isWindows_11_orLater && hasNativeCustomDecoration() && !isWindowMaximized() ) WindowTopBorder.getInstance().paintBorder( c, g, x, y, width, height ); } @@ -881,10 +974,6 @@ debug*/ JMenuBar menuBar = rootPane.getJMenuBar(); return hasVisibleEmbeddedMenuBar( menuBar ) ? menuBar.getBorder() : null; } - - protected boolean isWindowMaximized( Component c ) { - return window instanceof Frame && (((Frame) window).getExtendedState() & Frame.MAXIMIZED_BOTH) != 0; - } } //---- class FlatTitleLabelUI --------------------------------------------- @@ -898,36 +987,86 @@ debug*/ } @Override - protected void paintEnabledText( JLabel l, Graphics g, String s, int textX, int textY ) { + protected String layoutCL( JLabel label, FontMetrics fontMetrics, String text, Icon icon, + Rectangle viewR, Rectangle iconR, Rectangle textR ) + { JMenuBar menuBar = rootPane.getJMenuBar(); - boolean hasEmbeddedMenuBar = hasVisibleEmbeddedMenuBar( menuBar ) && hasMenus( menuBar ); - int labelWidth = l.getWidth(); - int textWidth = labelWidth - (textX * 2); - int gap = UIScale.scale( menuBarTitleGap ); + boolean hasEmbeddedMenuBar = hasVisibleEmbeddedMenuBar( menuBar ); + boolean hasEmbeddedLeadingMenus = hasEmbeddedMenuBar && hasLeadingMenus( menuBar ); + boolean leftToRight = getComponentOrientation().isLeftToRight(); - // The passed in textX coordinate is always to horizontally center the text within the label bounds. - // Modify textX so that the text is painted either centered within the window bounds or leading aligned. - boolean center = hasEmbeddedMenuBar ? centerTitleIfMenuBarEmbedded : centerTitle; - if( center ) { - // If window is wide enough, center title within window bounds. - // Otherwise, leave it centered within free space (label bounds). - int centeredTextX = ((l.getParent().getWidth() - textWidth) / 2) - l.getX(); - if( centeredTextX >= gap && centeredTextX + textWidth <= labelWidth - gap ) - textX = centeredTextX; - } else { - // leading aligned - boolean leftToRight = getComponentOrientation().isLeftToRight(); - Insets insets = l.getInsets(); - int leadingInset = hasEmbeddedMenuBar ? gap : (leftToRight ? insets.left : insets.right); - int leadingTextX = leftToRight ? leadingInset : labelWidth - leadingInset - textWidth; - if( leftToRight ? leadingTextX < textX : leadingTextX > textX ) - textX = leadingTextX; + if( hasEmbeddedMenuBar ) { + int minGap = UIScale.scale( menuBarTitleMinimumGap ); + + // apply minimum leading gap (between embedded menu bar and title) + if( hasEmbeddedLeadingMenus ) { + if( leftToRight ) + viewR.x += minGap; + viewR.width -= minGap; + } + + // apply minimum trailing gap (between title and right aligned components of embedded menu bar) + Component horizontalGlue = findHorizontalGlue( menuBar ); + if( horizontalGlue != null && menuBar.getComponent( menuBar.getComponentCount() - 1 ) != horizontalGlue ) { + if( !leftToRight ) + viewR.x += minGap; + viewR.width -= minGap; + } } - super.paintEnabledText( l, g, s, textX, textY ); + // compute icon width and gap (if icon is show besides the title) + int iconTextGap = 0; + int iconWidthAndGap = 0; + if( icon != null ) { + Insets iconInsets = iconLabel.getInsets(); + iconTextGap = leftToRight ? iconInsets.right : iconInsets.left; + iconWidthAndGap = icon.getIconWidth() + iconTextGap; + } + + // layout title and icon (if show besides the title) + String clippedText = SwingUtilities.layoutCompoundLabel( label, fontMetrics, text, icon, + label.getVerticalAlignment(), label.getHorizontalAlignment(), + label.getVerticalTextPosition(), label.getHorizontalTextPosition(), + viewR, iconR, textR, + iconTextGap ); + + // compute text X location + if( !clippedText.equals( text ) ) { + // if text is clipped, align to left (or right) + textR.x = leftToRight + ? viewR.x + iconWidthAndGap + : viewR.x + viewR.width - iconWidthAndGap - textR.width; + } else { + int leadingGap = hasEmbeddedLeadingMenus ? UIScale.scale( menuBarTitleGap - menuBarTitleMinimumGap ) : 0; + + boolean center = hasEmbeddedLeadingMenus ? centerTitleIfMenuBarEmbedded : centerTitle; + if( center ) { + // If window is wide enough, center title within window bounds. + // Otherwise, center within free space (label bounds). + Container parent = label.getParent(); + int centeredTextX = (parent != null) ? ((parent.getWidth() - textR.width - iconWidthAndGap) / 2) + iconWidthAndGap - label.getX() : -1; + textR.x = (centeredTextX >= viewR.x + leadingGap && centeredTextX + textR.width <= viewR.x + viewR.width - leadingGap) + ? centeredTextX + : viewR.x + ((viewR.width - textR.width - iconWidthAndGap) / 2) + iconWidthAndGap; + } else { + // leading aligned with leading gap, which is reduced if space is rare + textR.x = leftToRight + ? Math.min( viewR.x + leadingGap + iconWidthAndGap, viewR.x + viewR.width - textR.width ) + : Math.max( viewR.x + viewR.width - leadingGap - iconWidthAndGap - textR.width, viewR.x ); + } + } + + // compute icon X location (relative to text X location) + if( icon != null ) { + iconR.x = leftToRight + ? textR.x - iconWidthAndGap + : textR.x + textR.width + iconTextGap; + } + + return clippedText; } - private boolean hasMenus( JMenuBar menuBar ) { + private boolean hasLeadingMenus( JMenuBar menuBar ) { // check whether menu bar is empty if( menuBar.getComponentCount() == 0 || menuBar.getWidth() == 0 ) return false; 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 5939219c..5d24952f 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties @@ -789,11 +789,16 @@ TitlePane.noIconLeftGap = 8 TitlePane.iconSize = 16,16 TitlePane.iconMargins = 3,8,3,8 TitlePane.titleMargins = 3,0,3,0 +TitlePane.titleMinimumWidth = 60 TitlePane.buttonSize = 44,30 +TitlePane.buttonMinimumWidth = 30 TitlePane.buttonMaximizedHeight = 22 TitlePane.centerTitle = false TitlePane.centerTitleIfMenuBarEmbedded = true -TitlePane.menuBarTitleGap = 20 +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-testing/dumps/uidefaults/FlatDarkLaf_1.8.0.txt b/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0.txt index 18ee11f2..daeb5837 100644 --- a/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0.txt +++ b/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0.txt @@ -1214,6 +1214,7 @@ TextPaneUI com.formdev.flatlaf.ui.FlatTextPaneUI TitlePane.background #303234 HSL 210 4 20 javax.swing.plaf.ColorUIResource [UI] TitlePane.buttonHoverBackground #55585c HSL 214 4 35 com.formdev.flatlaf.util.DerivedColor [UI] lighten(15% autoInverse) TitlePane.buttonMaximizedHeight 22 +TitlePane.buttonMinimumWidth 30 TitlePane.buttonPressedBackground #484c4f HSL 206 5 30 com.formdev.flatlaf.util.DerivedColor [UI] lighten(10% autoInverse) TitlePane.buttonSize 44,30 javax.swing.plaf.DimensionUIResource [UI] TitlePane.centerTitle false @@ -1233,11 +1234,15 @@ 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.menuBarTitleGap 20 +TitlePane.menuBarResizeHeight 4 +TitlePane.menuBarTitleGap 40 +TitlePane.menuBarTitleMinimumGap 12 TitlePane.noIconLeftGap 8 TitlePane.restoreIcon [lazy] 44,30 com.formdev.flatlaf.icons.FlatWindowRestoreIcon [UI] TitlePane.showIcon true +TitlePane.showIconBesideTitle false TitlePane.titleMargins 3,0,3,0 javax.swing.plaf.InsetsUIResource [UI] +TitlePane.titleMinimumWidth 60 TitlePane.unifiedBackground true TitlePane.useWindowDecorations true diff --git a/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0.txt b/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0.txt index 13731937..f6448d31 100644 --- a/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0.txt +++ b/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0.txt @@ -1219,6 +1219,7 @@ TextPaneUI com.formdev.flatlaf.ui.FlatTextPaneUI TitlePane.background #ffffff HSL 0 0 100 javax.swing.plaf.ColorUIResource [UI] TitlePane.buttonHoverBackground #e6e6e6 HSL 0 0 90 com.formdev.flatlaf.util.DerivedColor [UI] darken(10% autoInverse) TitlePane.buttonMaximizedHeight 22 +TitlePane.buttonMinimumWidth 30 TitlePane.buttonPressedBackground #ebebeb HSL 0 0 92 com.formdev.flatlaf.util.DerivedColor [UI] darken(8% autoInverse) TitlePane.buttonSize 44,30 javax.swing.plaf.DimensionUIResource [UI] TitlePane.centerTitle false @@ -1238,11 +1239,15 @@ 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.menuBarTitleGap 20 +TitlePane.menuBarResizeHeight 4 +TitlePane.menuBarTitleGap 40 +TitlePane.menuBarTitleMinimumGap 12 TitlePane.noIconLeftGap 8 TitlePane.restoreIcon [lazy] 44,30 com.formdev.flatlaf.icons.FlatWindowRestoreIcon [UI] TitlePane.showIcon true +TitlePane.showIconBesideTitle false TitlePane.titleMargins 3,0,3,0 javax.swing.plaf.InsetsUIResource [UI] +TitlePane.titleMinimumWidth 60 TitlePane.unifiedBackground true TitlePane.useWindowDecorations true diff --git a/flatlaf-testing/dumps/uidefaults/FlatTestLaf_1.8.0.txt b/flatlaf-testing/dumps/uidefaults/FlatTestLaf_1.8.0.txt index 8900a298..7cdc3e39 100644 --- a/flatlaf-testing/dumps/uidefaults/FlatTestLaf_1.8.0.txt +++ b/flatlaf-testing/dumps/uidefaults/FlatTestLaf_1.8.0.txt @@ -1249,6 +1249,7 @@ TextPaneUI com.formdev.flatlaf.ui.FlatTextPaneUI TitlePane.background #00ff00 HSL 120 100 50 javax.swing.plaf.ColorUIResource [UI] TitlePane.borderColor #ff0000 HSL 0 100 50 javax.swing.plaf.ColorUIResource [UI] TitlePane.buttonMaximizedHeight 22 +TitlePane.buttonMinimumWidth 30 TitlePane.buttonSize 44,30 javax.swing.plaf.DimensionUIResource [UI] TitlePane.centerTitle false TitlePane.centerTitleIfMenuBarEmbedded true @@ -1266,11 +1267,15 @@ 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.menuBarTitleGap 20 +TitlePane.menuBarResizeHeight 4 +TitlePane.menuBarTitleGap 40 +TitlePane.menuBarTitleMinimumGap 12 TitlePane.noIconLeftGap 8 TitlePane.restoreIcon [lazy] 44,30 com.formdev.flatlaf.icons.FlatWindowRestoreIcon [UI] TitlePane.showIcon true +TitlePane.showIconBesideTitle false TitlePane.titleMargins 3,0,3,0 javax.swing.plaf.InsetsUIResource [UI] +TitlePane.titleMinimumWidth 60 TitlePane.unifiedBackground true TitlePane.useWindowDecorations true 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 c8f99783..c7e2937f 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 @@ -241,6 +241,23 @@ public class FlatWindowDecorationsTest if( c instanceof Box.Filler ) return; } + +/*debug + menuBar.add( new Box.Filler( new Dimension(), new Dimension(), + new Dimension( Short.MAX_VALUE, Short.MAX_VALUE ) ) + { + @Override + protected void paintComponent( Graphics g ) { + int w = getWidth(); + int h = getHeight(); + g.setColor( Color.blue ); + g.drawRect( 0, 0, w - 1, h - 1 ); + g.drawLine( 0, 0, w, h ); + g.drawLine( 0, h, w, 0 ); + } + } ); +debug*/ + menuBar.add( Box.createGlue() ); menuBar.revalidate(); } 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 1ce4fc69..9f0d4025 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 @@ -980,6 +980,7 @@ TitlePane.background TitlePane.borderColor TitlePane.buttonHoverBackground TitlePane.buttonMaximizedHeight +TitlePane.buttonMinimumWidth TitlePane.buttonPressedBackground TitlePane.buttonSize TitlePane.centerTitle @@ -999,11 +1000,15 @@ TitlePane.inactiveBackground TitlePane.inactiveForeground TitlePane.maximizeIcon TitlePane.menuBarEmbedded +TitlePane.menuBarResizeHeight TitlePane.menuBarTitleGap +TitlePane.menuBarTitleMinimumGap TitlePane.noIconLeftGap TitlePane.restoreIcon TitlePane.showIcon +TitlePane.showIconBesideTitle TitlePane.titleMargins +TitlePane.titleMinimumWidth TitlePane.unifiedBackground TitlePane.useWindowDecorations TitledBorder.border