/* * Copyright 2020 FormDev Software GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.formdev.flatlaf.ui; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dialog; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Frame; import java.awt.Graphics; import java.awt.GraphicsConfiguration; import java.awt.Image; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.Window; import java.awt.event.ActionListener; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; 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; import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.Icon; import javax.swing.ImageIcon; 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.SwingUtilities; import javax.swing.UIManager; import javax.swing.border.AbstractBorder; import javax.swing.border.Border; import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.FlatSystemProperties; import com.formdev.flatlaf.ui.JBRCustomDecorations.JBRWindowTopBorder; import com.formdev.flatlaf.util.ScaledImageIcon; import com.formdev.flatlaf.util.SystemInfo; import com.formdev.flatlaf.util.UIScale; /** * Provides the Flat LaF title bar. * * @uiDefault TitlePane.background Color * @uiDefault TitlePane.inactiveBackground Color * @uiDefault TitlePane.foreground Color * @uiDefault TitlePane.inactiveForeground Color * @uiDefault TitlePane.embeddedForeground Color * @uiDefault TitlePane.iconSize Dimension * @uiDefault TitlePane.iconMargins Insets * @uiDefault TitlePane.titleMargins Insets * @uiDefault TitlePane.menuBarMargins Insets * @uiDefault TitlePane.menuBarEmbedded boolean * @uiDefault TitlePane.buttonMaximizedHeight int * @uiDefault TitlePane.closeIcon Icon * @uiDefault TitlePane.iconifyIcon Icon * @uiDefault TitlePane.maximizeIcon Icon * @uiDefault TitlePane.restoreIcon Icon * * @author Karl Tauber */ public class FlatTitlePane extends JComponent { private final Color activeBackground = UIManager.getColor( "TitlePane.background" ); private final Color inactiveBackground = UIManager.getColor( "TitlePane.inactiveBackground" ); private final Color activeForeground = UIManager.getColor( "TitlePane.foreground" ); private final Color inactiveForeground = UIManager.getColor( "TitlePane.inactiveForeground" ); private final Color embeddedForeground = UIManager.getColor( "TitlePane.embeddedForeground" ); private final Insets menuBarMargins = UIManager.getInsets( "TitlePane.menuBarMargins" ); private final Dimension iconSize = UIManager.getDimension( "TitlePane.iconSize" ); private final int buttonMaximizedHeight = UIManager.getInt( "TitlePane.buttonMaximizedHeight" ); private final JRootPane rootPane; private JPanel leftPanel; private JLabel iconLabel; private JComponent menuBarPlaceholder; private JLabel titleLabel; private JPanel buttonPanel; private JButton iconifyButton; private JButton maximizeButton; private JButton restoreButton; private JButton closeButton; private final Handler handler; private Window window; public FlatTitlePane( JRootPane rootPane ) { this.rootPane = rootPane; handler = createHandler(); setBorder( createTitlePaneBorder() ); addSubComponents(); activeChanged( true ); addMouseListener( handler ); addMouseMotionListener( handler ); // necessary for closing window with double-click on icon iconLabel.addMouseListener( handler ); } protected FlatTitlePaneBorder createTitlePaneBorder() { return new FlatTitlePaneBorder(); } protected Handler createHandler() { return new Handler(); } protected void addSubComponents() { leftPanel = new JPanel(); iconLabel = new JLabel(); titleLabel = new JLabel(); iconLabel.setBorder( new FlatEmptyBorder( UIManager.getInsets( "TitlePane.iconMargins" ) ) ); titleLabel.setBorder( new FlatEmptyBorder( UIManager.getInsets( "TitlePane.titleMargins" ) ) ); leftPanel.setLayout( new BoxLayout( leftPanel, BoxLayout.LINE_AXIS ) ); leftPanel.setOpaque( false ); leftPanel.add( iconLabel ); menuBarPlaceholder = new JComponent() { @Override public Dimension getPreferredSize() { JMenuBar menuBar = rootPane.getJMenuBar(); return (menuBar != null && isMenuBarEmbedded()) ? FlatUIUtils.addInsets( menuBar.getPreferredSize(), UIScale.scale( menuBarMargins ) ) : new Dimension(); } }; leftPanel.add( menuBarPlaceholder ); createButtons(); setLayout( new BorderLayout() ); add( leftPanel, BorderLayout.LINE_START ); add( titleLabel, BorderLayout.CENTER ); add( buttonPanel, BorderLayout.LINE_END ); } protected void createButtons() { iconifyButton = createButton( "TitlePane.iconifyIcon", "Iconify", e -> iconify() ); maximizeButton = createButton( "TitlePane.maximizeIcon", "Maximize", e -> maximize() ); restoreButton = createButton( "TitlePane.restoreIcon", "Restore", e -> restore() ); closeButton = createButton( "TitlePane.closeIcon", "Close", e -> close() ); buttonPanel = new JPanel() { @Override public Dimension getPreferredSize() { Dimension size = super.getPreferredSize(); if( buttonMaximizedHeight > 0 && window instanceof Frame && (((Frame)window).getExtendedState() & Frame.MAXIMIZED_BOTH) != 0 ) { // make title pane height smaller when frame is maximized size = new Dimension( size.width, Math.min( size.height, UIScale.scale( buttonMaximizedHeight ) ) ); } return size; } }; buttonPanel.setOpaque( false ); buttonPanel.setLayout( new BoxLayout( buttonPanel, BoxLayout.LINE_AXIS ) ); if( rootPane.getWindowDecorationStyle() == JRootPane.FRAME ) { // JRootPane.FRAME works only for frames (and not for dialogs) // but at this time the owner window type is unknown (not yet added) // so we add the iconify/maximize/restore buttons and they are hidden // later in frameStateChanged(), which is invoked from addNotify() restoreButton.setVisible( false ); buttonPanel.add( iconifyButton ); buttonPanel.add( maximizeButton ); buttonPanel.add( restoreButton ); } buttonPanel.add( closeButton ); } protected JButton createButton( String iconKey, String accessibleName, ActionListener action ) { JButton button = new JButton( UIManager.getIcon( iconKey ) ); button.setFocusable( false ); button.setContentAreaFilled( false ); button.setBorder( BorderFactory.createEmptyBorder() ); button.putClientProperty( AccessibleContext.ACCESSIBLE_NAME_PROPERTY, accessibleName ); button.addActionListener( action ); return button; } protected void activeChanged( boolean active ) { Color background = FlatUIUtils.nonUIResource( active ? activeBackground : inactiveBackground ); Color foreground = FlatUIUtils.nonUIResource( active ? (rootPane.getJMenuBar() != null && isMenuBarEmbedded() ? embeddedForeground : activeForeground) : inactiveForeground ); setBackground( background ); titleLabel.setForeground( foreground ); // this is necessary because hover/pressed colors are derived from background color iconifyButton.setBackground( background ); maximizeButton.setBackground( background ); restoreButton.setBackground( background ); closeButton.setBackground( background ); } protected void frameStateChanged() { if( window == null || rootPane.getWindowDecorationStyle() != JRootPane.FRAME ) return; if( window instanceof Frame ) { Frame frame = (Frame) window; boolean resizable = frame.isResizable(); boolean maximized = ((frame.getExtendedState() & Frame.MAXIMIZED_BOTH) != 0); iconifyButton.setVisible( true ); maximizeButton.setVisible( resizable && !maximized ); restoreButton.setVisible( resizable && maximized ); } else { // hide buttons because they are only supported in frames iconifyButton.setVisible( false ); maximizeButton.setVisible( false ); restoreButton.setVisible( false ); revalidate(); repaint(); } } protected void updateIcon() { // get window images List<Image> images = window.getIconImages(); if( images.isEmpty() ) { // search in owners for( Window owner = window.getOwner(); owner != null; owner = owner.getOwner() ) { images = owner.getIconImages(); if( !images.isEmpty() ) break; } } boolean hasIcon = true; // set icon if( !images.isEmpty() ) iconLabel.setIcon( FlatTitlePaneIcon.create( images, iconSize ) ); else { // no icon set on window --> use default icon Icon defaultIcon = UIManager.getIcon( "InternalFrame.icon" ); if( defaultIcon != null ) { if( defaultIcon instanceof ImageIcon ) defaultIcon = new ScaledImageIcon( (ImageIcon) defaultIcon, iconSize.width, iconSize.height ); iconLabel.setIcon( defaultIcon ); } else hasIcon = false; } // show/hide icon iconLabel.setVisible( hasIcon ); updateJBRHitTestSpotsAndTitleBarHeightLater(); } @Override public void addNotify() { super.addNotify(); uninstallWindowListeners(); window = SwingUtilities.getWindowAncestor( this ); if( window != null ) { frameStateChanged(); activeChanged( window.isActive() ); updateIcon(); titleLabel.setText( getWindowTitle() ); installWindowListeners(); } updateJBRHitTestSpotsAndTitleBarHeightLater(); } @Override public void removeNotify() { super.removeNotify(); uninstallWindowListeners(); window = null; } protected String getWindowTitle() { if( window instanceof Frame ) return ((Frame)window).getTitle(); if( window instanceof Dialog ) return ((Dialog)window).getTitle(); return null; } protected void installWindowListeners() { if( window == null ) return; window.addPropertyChangeListener( handler ); window.addWindowListener( handler ); window.addWindowStateListener( handler ); window.addComponentListener( handler ); } protected void uninstallWindowListeners() { if( window == null ) return; window.removePropertyChangeListener( handler ); window.removeWindowListener( handler ); window.removeWindowStateListener( handler ); window.removeComponentListener( handler ); } protected boolean isMenuBarEmbedded() { // not storing value of "TitlePane.menuBarEmbedded" in class to allow changing at runtime return UIManager.getBoolean( "TitlePane.menuBarEmbedded" ) && FlatClientProperties.clientPropertyBoolean( rootPane, FlatClientProperties.MENU_BAR_EMBEDDED, true ) && FlatSystemProperties.getBoolean( FlatSystemProperties.MENUBAR_EMBEDDED, true ); } protected Rectangle getMenuBarBounds() { Insets insets = rootPane.getInsets(); Rectangle bounds = new Rectangle( SwingUtilities.convertPoint( menuBarPlaceholder, -insets.left, -insets.top, rootPane ), menuBarPlaceholder.getSize() ); // add menu bar bottom border insets to bounds so that menu bar overlaps // title pane border (menu bar border is painted over title pane border) Insets borderInsets = getBorder().getBorderInsets( this ); bounds.height += borderInsets.bottom; return FlatUIUtils.subtractInsets( bounds, UIScale.scale( getMenuBarMargins() ) ); } protected void menuBarChanged() { menuBarPlaceholder.invalidate(); // update title foreground color EventQueue.invokeLater( () -> { activeChanged( window == null || window.isActive() ); } ); } protected Insets getMenuBarMargins() { return getComponentOrientation().isLeftToRight() ? menuBarMargins : new Insets( menuBarMargins.top, menuBarMargins.right, menuBarMargins.bottom, menuBarMargins.left ); } @Override protected void paintComponent( Graphics g ) { g.setColor( getBackground() ); g.fillRect( 0, 0, getWidth(), getHeight() ); } /** * Iconifies the window. */ protected void iconify() { if( window instanceof Frame ) { Frame frame = (Frame) window; frame.setExtendedState( frame.getExtendedState() | Frame.ICONIFIED ); } } /** * Maximizes the window. */ protected void maximize() { if( !(window instanceof Frame) ) return; Frame frame = (Frame) window; // set maximized bounds to avoid that maximized window overlaps Windows task bar // (if not running in JBR and if not modified from the application) if( !hasJBRCustomDecoration() && (frame.getMaximizedBounds() == null || Objects.equals( frame.getMaximizedBounds(), rootPane.getClientProperty( "_flatlaf.maximizedBounds" ) )) ) { GraphicsConfiguration gc = window.getGraphicsConfiguration(); // Screen bounds, which may be smaller than physical size on Java 9+. // E.g. if running a 3840x2160 screen at 200%, screenBounds.size is 1920x1080. // In Java 9+, each screen can have its own scale factor. // // On Java 8, which does not scale, screenBounds.size of the primary screen // is identical to its physical size. But when the primary screen is scaled, // then screenBounds.size of secondary screens is scaled with the scale factor // of the primary screen. // E.g. primary 3840x2160 screen at 150%, secondary 1920x1080 screen at 100%, // then screenBounds.size is 3840x2160 on primary and 2880x1560 on secondary. Rectangle screenBounds = gc.getBounds(); int maximizedX = screenBounds.x; int maximizedY = screenBounds.y; int maximizedWidth = screenBounds.width; int maximizedHeight = screenBounds.height; if( !SystemInfo.IS_JAVA_15_OR_LATER ) { // on Java 8 to 14, maximized x,y are 0,0 based on all screens in a multi-screen environment maximizedX = 0; maximizedY = 0; // scale maximized screen size to get physical screen size for Java 9 to 14 AffineTransform defaultTransform = gc.getDefaultTransform(); maximizedWidth = (int) (maximizedWidth * defaultTransform.getScaleX()); maximizedHeight = (int) (maximizedHeight * defaultTransform.getScaleY()); } // screen insets are in physical size, except for Java 15+ // (see https://bugs.openjdk.java.net/browse/JDK-8243925) // and except for Java 8 on secondary screens where primary screen is scaled Insets screenInsets = window.getToolkit().getScreenInsets( gc ); // maximized bounds are required in physical size, except for Java 15+ // (see https://bugs.openjdk.java.net/browse/JDK-8231564 and // https://bugs.openjdk.java.net/browse/JDK-8176359) // and except for Java 8 on secondary screens where primary screen is scaled Rectangle maximizedBounds = new Rectangle( maximizedX + screenInsets.left, maximizedY + screenInsets.top, maximizedWidth - screenInsets.left - screenInsets.right, maximizedHeight - screenInsets.top - screenInsets.bottom ); // change maximized bounds frame.setMaximizedBounds( maximizedBounds ); // remember maximized bounds in client property to be able to detect // whether maximized bounds are modified from the application rootPane.putClientProperty( "_flatlaf.maximizedBounds", maximizedBounds ); } // maximize window frame.setExtendedState( frame.getExtendedState() | Frame.MAXIMIZED_BOTH ); } /** * Restores the window size. */ protected void restore() { if( window instanceof Frame ) { Frame frame = (Frame) window; int state = frame.getExtendedState(); frame.setExtendedState( ((state & Frame.ICONIFIED) != 0) ? (state & ~Frame.ICONIFIED) : (state & ~Frame.MAXIMIZED_BOTH) ); } } /** * Closes the window. */ protected void close() { if( window != null ) window.dispatchEvent( new WindowEvent( window, WindowEvent.WINDOW_CLOSING ) ); } protected boolean hasJBRCustomDecoration() { return FlatRootPaneUI.canUseJBRCustomDecorations && window != null && JBRCustomDecorations.hasCustomDecoration( window ); } protected void updateJBRHitTestSpotsAndTitleBarHeightLater() { EventQueue.invokeLater( () -> { updateJBRHitTestSpotsAndTitleBarHeight(); } ); } protected void updateJBRHitTestSpotsAndTitleBarHeight() { if( !isDisplayable() ) return; if( !hasJBRCustomDecoration() ) return; List<Rectangle> hitTestSpots = new ArrayList<>(); if( iconLabel.isVisible() ) addJBRHitTestSpot( iconLabel, false, hitTestSpots ); addJBRHitTestSpot( buttonPanel, false, hitTestSpots ); addJBRHitTestSpot( menuBarPlaceholder, true, hitTestSpots ); int titleBarHeight = getHeight(); // slightly reduce height so that component receives mouseExit events if( titleBarHeight > 0 ) titleBarHeight--; JBRCustomDecorations.setHitTestSpotsAndTitleBarHeight( window, hitTestSpots, titleBarHeight ); } protected void addJBRHitTestSpot( JComponent c, boolean subtractMenuBarMargins, List<Rectangle> hitTestSpots ) { Dimension size = c.getSize(); if( size.width <= 0 || size.height <= 0 ) return; Point location = SwingUtilities.convertPoint( c, 0, 0, window ); Rectangle r = new Rectangle( location, size ); if( subtractMenuBarMargins ) r = FlatUIUtils.subtractInsets( r, UIScale.scale( getMenuBarMargins() ) ); // slightly increase rectangle so that component receives mouseExit events r.grow( 2, 2 ); hitTestSpots.add( r ); } //---- class TitlePaneBorder ---------------------------------------------- protected class FlatTitlePaneBorder extends AbstractBorder { @Override public Insets getBorderInsets( Component c, Insets insets ) { super.getBorderInsets( c, insets ); // if menu bar is embedded, add bottom insets of menu bar border Border menuBarBorder = getMenuBarBorder(); if( menuBarBorder != null ) { Insets menuBarInsets = menuBarBorder.getBorderInsets( c ); insets.bottom += menuBarInsets.bottom; } if( hasJBRCustomDecoration() ) insets = FlatUIUtils.addInsets( insets, JBRWindowTopBorder.getInstance().getBorderInsets() ); return insets; } @Override public void paintBorder( Component c, Graphics g, int x, int y, int width, int height ) { // if menu bar is embedded, paint menu bar border Border menuBarBorder = getMenuBarBorder(); if( menuBarBorder != null ) menuBarBorder.paintBorder( c, g, x, y, width, height ); if( hasJBRCustomDecoration() ) JBRWindowTopBorder.getInstance().paintBorder( c, g, x, y, width, height ); } protected Border getMenuBarBorder() { JMenuBar menuBar = rootPane.getJMenuBar(); return (menuBar != null && isMenuBarEmbedded()) ? menuBar.getBorder() : null; } } //---- class Handler ------------------------------------------------------ protected class Handler extends WindowAdapter implements PropertyChangeListener, MouseListener, MouseMotionListener, ComponentListener { //---- interface PropertyChangeListener ---- @Override public void propertyChange( PropertyChangeEvent e ) { switch( e.getPropertyName() ) { case "title": titleLabel.setText( getWindowTitle() ); break; case "resizable": if( window instanceof Frame ) frameStateChanged(); break; case "iconImage": updateIcon(); break; case "componentOrientation": updateJBRHitTestSpotsAndTitleBarHeightLater(); break; } } //---- interface WindowListener ---- @Override public void windowActivated( WindowEvent e ) { activeChanged( true ); updateJBRHitTestSpotsAndTitleBarHeight(); if( hasJBRCustomDecoration() ) JBRWindowTopBorder.getInstance().repaintBorder( FlatTitlePane.this ); } @Override public void windowDeactivated( WindowEvent e ) { activeChanged( false ); updateJBRHitTestSpotsAndTitleBarHeight(); if( hasJBRCustomDecoration() ) JBRWindowTopBorder.getInstance().repaintBorder( FlatTitlePane.this ); } @Override public void windowStateChanged( WindowEvent e ) { frameStateChanged(); updateJBRHitTestSpotsAndTitleBarHeight(); } //---- interface MouseListener ---- private int lastXOnScreen; private int lastYOnScreen; @Override public void mouseClicked( MouseEvent e ) { if( e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton( e ) ) { if( e.getSource() == iconLabel ) { // double-click on icon closes window close(); } else if( !hasJBRCustomDecoration() && window instanceof Frame && ((Frame)window).isResizable() ) { // maximize/restore on double-click Frame frame = (Frame) window; if( (frame.getExtendedState() & Frame.MAXIMIZED_BOTH) != 0 ) restore(); else maximize(); } } } @Override public void mousePressed( MouseEvent e ) { lastXOnScreen = e.getXOnScreen(); lastYOnScreen = e.getYOnScreen(); } @Override public void mouseReleased( MouseEvent e ) {} @Override public void mouseEntered( MouseEvent e ) {} @Override public void mouseExited( MouseEvent e ) {} //---- interface MouseMotionListener ---- @Override public void mouseDragged( MouseEvent e ) { if( hasJBRCustomDecoration() ) return; // do nothing if running in JBR int xOnScreen = e.getXOnScreen(); int yOnScreen = e.getYOnScreen(); if( lastXOnScreen == xOnScreen && lastYOnScreen == yOnScreen ) return; // restore window if it is maximized if( window instanceof Frame ) { Frame frame = (Frame) window; int state = frame.getExtendedState(); if( (state & Frame.MAXIMIZED_BOTH) != 0 ) { int maximizedX = window.getX(); int maximizedY = window.getY(); // restore window size, which also moves window to pre-maximized location frame.setExtendedState( state & ~Frame.MAXIMIZED_BOTH ); int restoredWidth = window.getWidth(); int newX = maximizedX; JComponent rightComp = getComponentOrientation().isLeftToRight() ? buttonPanel : leftPanel; if( xOnScreen >= maximizedX + restoredWidth - rightComp.getWidth() - 10 ) newX = xOnScreen + rightComp.getWidth() + 10 - restoredWidth; // move window near mouse window.setLocation( newX, maximizedY ); return; } } // compute new window location int newX = window.getX() + (xOnScreen - lastXOnScreen); int newY = window.getY() + (yOnScreen - lastYOnScreen); // move window window.setLocation( newX, newY ); lastXOnScreen = xOnScreen; lastYOnScreen = yOnScreen; } @Override public void mouseMoved( MouseEvent e ) {} //---- interface ComponentListener ---- @Override public void componentResized( ComponentEvent e ) { updateJBRHitTestSpotsAndTitleBarHeightLater(); } @Override public void componentMoved( ComponentEvent e ) {} @Override public void componentShown( ComponentEvent e ) {} @Override public void componentHidden( ComponentEvent e ) {} } }